Files

4321 lines
182 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// node_modules/frida-il2cpp-bridge/dist/index.js
var __decorate = function(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var Il2Cpp2;
(function(Il2Cpp3) {
Il2Cpp3.application = {
/**
* Gets the data path name of the current application, e.g.
* `/data/emulated/0/Android/data/com.example.application/files`
* on Android.
*
* **This information is not guaranteed to exist.**
*
* ```ts
* Il2Cpp.perform(() => {
* // prints /data/emulated/0/Android/data/com.example.application/files
* console.log(Il2Cpp.application.dataPath);
* });
* ```
*/
get dataPath() {
return unityEngineCall("get_persistentDataPath");
},
/**
* Gets the identifier name of the current application, e.g.
* `com.example.application` on Android.
*
* In case the identifier cannot be retrieved, the main module name is
* returned instead, which typically is the process name.
*
* ```ts
* Il2Cpp.perform(() => {
* // prints com.example.application
* console.log(Il2Cpp.application.identifier);
* });
* ```
*/
get identifier() {
return unityEngineCall("get_identifier") ?? unityEngineCall("get_bundleIdentifier") ?? Process.mainModule.name;
},
/**
* Gets the version name of the current application, e.g. `4.12.8`.
*
* In case the version cannot be retrieved, an hash of the IL2CPP
* module is returned instead.
*
* ```ts
* Il2Cpp.perform(() => {
* // prints 4.12.8
* console.log(Il2Cpp.application.version);
* });
* ```
*/
get version() {
return unityEngineCall("get_version") ?? exportsHash(Il2Cpp3.module).toString(16);
}
};
getter(Il2Cpp3, "unityVersion", () => {
try {
const unityVersion = Il2Cpp3.$config.unityVersion ?? unityEngineCall("get_unityVersion");
if (unityVersion != null) {
return unityVersion;
}
} catch (_) {
}
const searchPattern = "69 6c 32 63 70 70";
for (const range of Il2Cpp3.module.enumerateRanges("r--").concat(Process.getRangeByAddress(Il2Cpp3.module.base))) {
for (let { address } of Memory.scanSync(range.base, range.size, searchPattern)) {
while (address.readU8() != 0) {
address = address.sub(1);
}
const match = UnityVersion.find(address.add(1).readCString());
if (match != void 0) {
return match;
}
}
}
raise("couldn't determine the Unity version, please specify it manually");
}, lazy);
getter(Il2Cpp3, "unityVersionIsBelow201830", () => {
return UnityVersion.lt(Il2Cpp3.unityVersion, "2018.3.0");
}, lazy);
getter(Il2Cpp3, "unityVersionIsBelow202120", () => {
return UnityVersion.lt(Il2Cpp3.unityVersion, "2021.2.0");
}, lazy);
function unityEngineCall(method) {
const handle = Il2Cpp3.exports.resolveInternalCall(Memory.allocUtf8String("UnityEngine.Application::" + method));
const nativeFunction = new NativeFunction(handle, "pointer", []);
return nativeFunction.isNull() ? null : new Il2Cpp3.String(nativeFunction()).asNullable()?.content ?? null;
}
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function boxed(value, type) {
const mapping = {
int8: "System.SByte",
uint8: "System.Byte",
int16: "System.Int16",
uint16: "System.UInt16",
int32: "System.Int32",
uint32: "System.UInt32",
int64: "System.Int64",
uint64: "System.UInt64",
char: "System.Char",
intptr: "System.IntPtr",
uintptr: "System.UIntPtr"
};
const className = typeof value == "boolean" ? "System.Boolean" : typeof value == "number" ? mapping[type ?? "int32"] : value instanceof Int64 ? "System.Int64" : value instanceof UInt64 ? "System.UInt64" : value instanceof NativePointer ? mapping[type ?? "intptr"] : raise(`Cannot create boxed primitive using value of type '${typeof value}'`);
const object = Il2Cpp3.corlib.class(className ?? raise(`Unknown primitive type name '${type}'`)).alloc();
(object.tryField("m_value") ?? object.tryField("_pointer") ?? raise(`Could not find primitive field in class '${className}'`)).value = value;
return object;
}
Il2Cpp3.boxed = boxed;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
Il2Cpp3.$config = {
moduleName: void 0,
unityVersion: void 0,
exports: void 0
};
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function dump(fileName, path) {
fileName = fileName ?? `${Il2Cpp3.application.identifier}_${Il2Cpp3.application.version}.cs`;
path = path ?? Il2Cpp3.application.dataPath ?? Process.getCurrentDir();
createDirectoryRecursively(path);
const destination = `${path}/${fileName}`;
const file = new File(destination, "w");
for (const assembly of Il2Cpp3.domain.assemblies) {
inform(`dumping ${assembly.name}...`);
for (const klass of assembly.image.classes) {
file.write(`${klass}
`);
}
}
file.flush();
file.close();
ok(`dump saved to ${destination}`);
showDeprecationNotice();
}
Il2Cpp3.dump = dump;
function dumpTree(path, ignoreAlreadyExistingDirectory = false) {
path = path ?? `${Il2Cpp3.application.dataPath ?? Process.getCurrentDir()}/${Il2Cpp3.application.identifier}_${Il2Cpp3.application.version}`;
if (!ignoreAlreadyExistingDirectory && directoryExists(path)) {
raise(`directory ${path} already exists - pass ignoreAlreadyExistingDirectory = true to skip this check`);
}
for (const assembly of Il2Cpp3.domain.assemblies) {
inform(`dumping ${assembly.name}...`);
const destination = `${path}/${assembly.name.replaceAll(".", "/")}.cs`;
createDirectoryRecursively(destination.substring(0, destination.lastIndexOf("/")));
const file = new File(destination, "w");
for (const klass of assembly.image.classes) {
file.write(`${klass}
`);
}
file.flush();
file.close();
}
ok(`dump saved to ${path}`);
showDeprecationNotice();
}
Il2Cpp3.dumpTree = dumpTree;
function directoryExists(path) {
return Il2Cpp3.corlib.class("System.IO.Directory").method("Exists").invoke(Il2Cpp3.string(path));
}
function createDirectoryRecursively(path) {
Il2Cpp3.corlib.class("System.IO.Directory").method("CreateDirectory").invoke(Il2Cpp3.string(path));
}
function showDeprecationNotice() {
warn("this api will be removed in a future release, please use `npx frida-il2cpp-bridge dump` instead");
}
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function installExceptionListener(targetThread = "current") {
const currentThread = Il2Cpp3.exports.threadGetCurrent();
return Interceptor.attach(Il2Cpp3.module.getExportByName("__cxa_throw"), function(args) {
if (targetThread == "current" && !Il2Cpp3.exports.threadGetCurrent().equals(currentThread)) {
return;
}
inform(new Il2Cpp3.Object(args[0].readPointer()));
});
}
Il2Cpp3.installExceptionListener = installExceptionListener;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
Il2Cpp3.exports = {
get alloc() {
return r("il2cpp_alloc", "pointer", ["size_t"]);
},
get arrayGetLength() {
return r("il2cpp_array_length", "uint32", ["pointer"]);
},
get arrayNew() {
return r("il2cpp_array_new", "pointer", ["pointer", "uint32"]);
},
get assemblyGetImage() {
return r("il2cpp_assembly_get_image", "pointer", ["pointer"]);
},
get classForEach() {
return r("il2cpp_class_for_each", "void", ["pointer", "pointer"]);
},
get classFromName() {
return r("il2cpp_class_from_name", "pointer", ["pointer", "pointer", "pointer"]);
},
get classFromObject() {
return r("il2cpp_class_from_system_type", "pointer", ["pointer"]);
},
get classGetArrayClass() {
return r("il2cpp_array_class_get", "pointer", ["pointer", "uint32"]);
},
get classGetArrayElementSize() {
return r("il2cpp_class_array_element_size", "int", ["pointer"]);
},
get classGetAssemblyName() {
return r("il2cpp_class_get_assemblyname", "pointer", ["pointer"]);
},
get classGetBaseType() {
return r("il2cpp_class_enum_basetype", "pointer", ["pointer"]);
},
get classGetDeclaringType() {
return r("il2cpp_class_get_declaring_type", "pointer", ["pointer"]);
},
get classGetElementClass() {
return r("il2cpp_class_get_element_class", "pointer", ["pointer"]);
},
get classGetFieldFromName() {
return r("il2cpp_class_get_field_from_name", "pointer", ["pointer", "pointer"]);
},
get classGetFields() {
return r("il2cpp_class_get_fields", "pointer", ["pointer", "pointer"]);
},
get classGetFlags() {
return r("il2cpp_class_get_flags", "int", ["pointer"]);
},
get classGetImage() {
return r("il2cpp_class_get_image", "pointer", ["pointer"]);
},
get classGetInstanceSize() {
return r("il2cpp_class_instance_size", "int32", ["pointer"]);
},
get classGetInterfaces() {
return r("il2cpp_class_get_interfaces", "pointer", ["pointer", "pointer"]);
},
get classGetMethodFromName() {
return r("il2cpp_class_get_method_from_name", "pointer", ["pointer", "pointer", "int"]);
},
get classGetMethods() {
return r("il2cpp_class_get_methods", "pointer", ["pointer", "pointer"]);
},
get classGetName() {
return r("il2cpp_class_get_name", "pointer", ["pointer"]);
},
get classGetNamespace() {
return r("il2cpp_class_get_namespace", "pointer", ["pointer"]);
},
get classGetNestedClasses() {
return r("il2cpp_class_get_nested_types", "pointer", ["pointer", "pointer"]);
},
get classGetParent() {
return r("il2cpp_class_get_parent", "pointer", ["pointer"]);
},
get classGetStaticFieldData() {
return r("il2cpp_class_get_static_field_data", "pointer", ["pointer"]);
},
get classGetValueTypeSize() {
return r("il2cpp_class_value_size", "int32", ["pointer", "pointer"]);
},
get classGetType() {
return r("il2cpp_class_get_type", "pointer", ["pointer"]);
},
get classHasReferences() {
return r("il2cpp_class_has_references", "bool", ["pointer"]);
},
get classInitialize() {
return r("il2cpp_runtime_class_init", "void", ["pointer"]);
},
get classIsAbstract() {
return r("il2cpp_class_is_abstract", "bool", ["pointer"]);
},
get classIsAssignableFrom() {
return r("il2cpp_class_is_assignable_from", "bool", ["pointer", "pointer"]);
},
get classIsBlittable() {
return r("il2cpp_class_is_blittable", "bool", ["pointer"]);
},
get classIsEnum() {
return r("il2cpp_class_is_enum", "bool", ["pointer"]);
},
get classIsGeneric() {
return r("il2cpp_class_is_generic", "bool", ["pointer"]);
},
get classIsInflated() {
return r("il2cpp_class_is_inflated", "bool", ["pointer"]);
},
get classIsInterface() {
return r("il2cpp_class_is_interface", "bool", ["pointer"]);
},
get classIsSubclassOf() {
return r("il2cpp_class_is_subclass_of", "bool", ["pointer", "pointer", "bool"]);
},
get classIsValueType() {
return r("il2cpp_class_is_valuetype", "bool", ["pointer"]);
},
get domainGetAssemblyFromName() {
return r("il2cpp_domain_assembly_open", "pointer", ["pointer", "pointer"]);
},
get domainGet() {
return r("il2cpp_domain_get", "pointer", []);
},
get domainGetAssemblies() {
return r("il2cpp_domain_get_assemblies", "pointer", ["pointer", "pointer"]);
},
get fieldGetClass() {
return r("il2cpp_field_get_parent", "pointer", ["pointer"]);
},
get fieldGetFlags() {
return r("il2cpp_field_get_flags", "int", ["pointer"]);
},
get fieldGetName() {
return r("il2cpp_field_get_name", "pointer", ["pointer"]);
},
get fieldGetOffset() {
return r("il2cpp_field_get_offset", "int32", ["pointer"]);
},
get fieldGetStaticValue() {
return r("il2cpp_field_static_get_value", "void", ["pointer", "pointer"]);
},
get fieldGetType() {
return r("il2cpp_field_get_type", "pointer", ["pointer"]);
},
get fieldSetStaticValue() {
return r("il2cpp_field_static_set_value", "void", ["pointer", "pointer"]);
},
get free() {
return r("il2cpp_free", "void", ["pointer"]);
},
get gcCollect() {
return r("il2cpp_gc_collect", "void", ["int"]);
},
get gcCollectALittle() {
return r("il2cpp_gc_collect_a_little", "void", []);
},
get gcDisable() {
return r("il2cpp_gc_disable", "void", []);
},
get gcEnable() {
return r("il2cpp_gc_enable", "void", []);
},
get gcGetHeapSize() {
return r("il2cpp_gc_get_heap_size", "int64", []);
},
get gcGetMaxTimeSlice() {
return r("il2cpp_gc_get_max_time_slice_ns", "int64", []);
},
get gcGetUsedSize() {
return r("il2cpp_gc_get_used_size", "int64", []);
},
get gcHandleGetTarget() {
return r("il2cpp_gchandle_get_target", "pointer", ["uint32"]);
},
get gcHandleFree() {
return r("il2cpp_gchandle_free", "void", ["uint32"]);
},
get gcHandleNew() {
return r("il2cpp_gchandle_new", "uint32", ["pointer", "bool"]);
},
get gcHandleNewWeakRef() {
return r("il2cpp_gchandle_new_weakref", "uint32", ["pointer", "bool"]);
},
get gcIsDisabled() {
return r("il2cpp_gc_is_disabled", "bool", []);
},
get gcIsIncremental() {
return r("il2cpp_gc_is_incremental", "bool", []);
},
get gcSetMaxTimeSlice() {
return r("il2cpp_gc_set_max_time_slice_ns", "void", ["int64"]);
},
get gcStartIncrementalCollection() {
return r("il2cpp_gc_start_incremental_collection", "void", []);
},
get gcStartWorld() {
return r("il2cpp_start_gc_world", "void", []);
},
get gcStopWorld() {
return r("il2cpp_stop_gc_world", "void", []);
},
get getCorlib() {
return r("il2cpp_get_corlib", "pointer", []);
},
get imageGetAssembly() {
return r("il2cpp_image_get_assembly", "pointer", ["pointer"]);
},
get imageGetClass() {
return r("il2cpp_image_get_class", "pointer", ["pointer", "uint"]);
},
get imageGetClassCount() {
return r("il2cpp_image_get_class_count", "uint32", ["pointer"]);
},
get imageGetName() {
return r("il2cpp_image_get_name", "pointer", ["pointer"]);
},
get initialize() {
return r("il2cpp_init", "void", ["pointer"]);
},
get livenessAllocateStruct() {
return r("il2cpp_unity_liveness_allocate_struct", "pointer", ["pointer", "int", "pointer", "pointer", "pointer"]);
},
get livenessCalculationBegin() {
return r("il2cpp_unity_liveness_calculation_begin", "pointer", ["pointer", "int", "pointer", "pointer", "pointer", "pointer"]);
},
get livenessCalculationEnd() {
return r("il2cpp_unity_liveness_calculation_end", "void", ["pointer"]);
},
get livenessCalculationFromStatics() {
return r("il2cpp_unity_liveness_calculation_from_statics", "void", ["pointer"]);
},
get livenessFinalize() {
return r("il2cpp_unity_liveness_finalize", "void", ["pointer"]);
},
get livenessFreeStruct() {
return r("il2cpp_unity_liveness_free_struct", "void", ["pointer"]);
},
get memorySnapshotCapture() {
return r("il2cpp_capture_memory_snapshot", "pointer", []);
},
get memorySnapshotFree() {
return r("il2cpp_free_captured_memory_snapshot", "void", ["pointer"]);
},
get memorySnapshotGetClasses() {
return r("il2cpp_memory_snapshot_get_classes", "pointer", ["pointer", "pointer"]);
},
get memorySnapshotGetObjects() {
return r("il2cpp_memory_snapshot_get_objects", "pointer", ["pointer", "pointer"]);
},
get methodGetClass() {
return r("il2cpp_method_get_class", "pointer", ["pointer"]);
},
get methodGetFlags() {
return r("il2cpp_method_get_flags", "uint32", ["pointer", "pointer"]);
},
get methodGetName() {
return r("il2cpp_method_get_name", "pointer", ["pointer"]);
},
get methodGetObject() {
return r("il2cpp_method_get_object", "pointer", ["pointer", "pointer"]);
},
get methodGetParameterCount() {
return r("il2cpp_method_get_param_count", "uint8", ["pointer"]);
},
get methodGetParameterName() {
return r("il2cpp_method_get_param_name", "pointer", ["pointer", "uint32"]);
},
get methodGetParameters() {
return r("il2cpp_method_get_parameters", "pointer", ["pointer", "pointer"]);
},
get methodGetParameterType() {
return r("il2cpp_method_get_param", "pointer", ["pointer", "uint32"]);
},
get methodGetReturnType() {
return r("il2cpp_method_get_return_type", "pointer", ["pointer"]);
},
get methodIsGeneric() {
return r("il2cpp_method_is_generic", "bool", ["pointer"]);
},
get methodIsInflated() {
return r("il2cpp_method_is_inflated", "bool", ["pointer"]);
},
get methodIsInstance() {
return r("il2cpp_method_is_instance", "bool", ["pointer"]);
},
get monitorEnter() {
return r("il2cpp_monitor_enter", "void", ["pointer"]);
},
get monitorExit() {
return r("il2cpp_monitor_exit", "void", ["pointer"]);
},
get monitorPulse() {
return r("il2cpp_monitor_pulse", "void", ["pointer"]);
},
get monitorPulseAll() {
return r("il2cpp_monitor_pulse_all", "void", ["pointer"]);
},
get monitorTryEnter() {
return r("il2cpp_monitor_try_enter", "bool", ["pointer", "uint32"]);
},
get monitorTryWait() {
return r("il2cpp_monitor_try_wait", "bool", ["pointer", "uint32"]);
},
get monitorWait() {
return r("il2cpp_monitor_wait", "void", ["pointer"]);
},
get objectGetClass() {
return r("il2cpp_object_get_class", "pointer", ["pointer"]);
},
get objectGetVirtualMethod() {
return r("il2cpp_object_get_virtual_method", "pointer", ["pointer", "pointer"]);
},
get objectInitialize() {
return r("il2cpp_runtime_object_init_exception", "void", ["pointer", "pointer"]);
},
get objectNew() {
return r("il2cpp_object_new", "pointer", ["pointer"]);
},
get objectGetSize() {
return r("il2cpp_object_get_size", "uint32", ["pointer"]);
},
get objectUnbox() {
return r("il2cpp_object_unbox", "pointer", ["pointer"]);
},
get resolveInternalCall() {
return r("il2cpp_resolve_icall", "pointer", ["pointer"]);
},
get stringGetChars() {
return r("il2cpp_string_chars", "pointer", ["pointer"]);
},
get stringGetLength() {
return r("il2cpp_string_length", "int32", ["pointer"]);
},
get stringNew() {
return r("il2cpp_string_new", "pointer", ["pointer"]);
},
get valueTypeBox() {
return r("il2cpp_value_box", "pointer", ["pointer", "pointer"]);
},
get threadAttach() {
return r("il2cpp_thread_attach", "pointer", ["pointer"]);
},
get threadDetach() {
return r("il2cpp_thread_detach", "void", ["pointer"]);
},
get threadGetAttachedThreads() {
return r("il2cpp_thread_get_all_attached_threads", "pointer", ["pointer"]);
},
get threadGetCurrent() {
return r("il2cpp_thread_current", "pointer", []);
},
get threadIsVm() {
return r("il2cpp_is_vm_thread", "bool", ["pointer"]);
},
get typeEquals() {
return r("il2cpp_type_equals", "bool", ["pointer", "pointer"]);
},
get typeGetClass() {
return r("il2cpp_class_from_type", "pointer", ["pointer"]);
},
get typeGetName() {
return r("il2cpp_type_get_name", "pointer", ["pointer"]);
},
get typeGetObject() {
return r("il2cpp_type_get_object", "pointer", ["pointer"]);
},
get typeGetTypeEnum() {
return r("il2cpp_type_get_type", "int", ["pointer"]);
}
};
decorate(Il2Cpp3.exports, lazy);
getter(Il2Cpp3, "memorySnapshotExports", () => new CModule("#include <stdint.h>\n#include <string.h>\n\ntypedef struct Il2CppManagedMemorySnapshot Il2CppManagedMemorySnapshot;\ntypedef struct Il2CppMetadataType Il2CppMetadataType;\n\nstruct Il2CppManagedMemorySnapshot\n{\n struct Il2CppManagedHeap\n {\n uint32_t section_count;\n void * sections;\n } heap;\n struct Il2CppStacks\n {\n uint32_t stack_count;\n void * stacks;\n } stacks;\n struct Il2CppMetadataSnapshot\n {\n uint32_t type_count;\n Il2CppMetadataType * types;\n } metadata_snapshot;\n struct Il2CppGCHandles\n {\n uint32_t tracked_object_count;\n void ** pointers_to_objects;\n } gc_handles;\n struct Il2CppRuntimeInformation\n {\n uint32_t pointer_size;\n uint32_t object_header_size;\n uint32_t array_header_size;\n uint32_t array_bounds_offset_in_header;\n uint32_t array_size_offset_in_header;\n uint32_t allocation_granularity;\n } runtime_information;\n void * additional_user_information;\n};\n\nstruct Il2CppMetadataType\n{\n uint32_t flags;\n void * fields;\n uint32_t field_count;\n uint32_t statics_size;\n uint8_t * statics;\n uint32_t base_or_element_type_index;\n char * name;\n const char * assembly_name;\n uint64_t type_info_address;\n uint32_t size;\n};\n\nuintptr_t\nil2cpp_memory_snapshot_get_classes (\n const Il2CppManagedMemorySnapshot * snapshot, Il2CppMetadataType ** iter)\n{\n const int zero = 0;\n const void * null = 0;\n\n if (iter != NULL && snapshot->metadata_snapshot.type_count > zero)\n {\n if (*iter == null)\n {\n *iter = snapshot->metadata_snapshot.types;\n return (uintptr_t) (*iter)->type_info_address;\n }\n else\n {\n Il2CppMetadataType * metadata_type = *iter + 1;\n\n if (metadata_type < snapshot->metadata_snapshot.types +\n snapshot->metadata_snapshot.type_count)\n {\n *iter = metadata_type;\n return (uintptr_t) (*iter)->type_info_address;\n }\n }\n }\n return 0;\n}\n\nvoid **\nil2cpp_memory_snapshot_get_objects (\n const Il2CppManagedMemorySnapshot * snapshot, uint32_t * size)\n{\n *size = snapshot->gc_handles.tracked_object_count;\n return snapshot->gc_handles.pointers_to_objects;\n}\n"), lazy);
function r(exportName, retType, argTypes) {
const handle = Il2Cpp3.$config.exports?.[exportName]?.() ?? Il2Cpp3.module.findExportByName(exportName) ?? Il2Cpp3.memorySnapshotExports[exportName];
const target = new NativeFunction(handle ?? NULL, retType, argTypes);
return target.isNull() ? new Proxy(target, {
get(value, name) {
const property = value[name];
return typeof property === "function" ? property.bind(value) : property;
},
apply() {
if (handle == null) {
raise(`couldn't resolve export ${exportName}`);
} else if (handle.isNull()) {
raise(`export ${exportName} points to NULL IL2CPP library has likely been stripped, obfuscated, or customized`);
}
}
}) : target;
}
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function is(klass) {
return (element) => {
if (element instanceof Il2Cpp3.Class) {
return klass.isAssignableFrom(element);
} else {
return klass.isAssignableFrom(element.class);
}
};
}
Il2Cpp3.is = is;
function isExactly(klass) {
return (element) => {
if (element instanceof Il2Cpp3.Class) {
return element.equals(klass);
} else {
return element.class.equals(klass);
}
};
}
Il2Cpp3.isExactly = isExactly;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
Il2Cpp3.gc = {
/**
* Gets the heap size in bytes.
*/
get heapSize() {
return Il2Cpp3.exports.gcGetHeapSize();
},
/**
* Determines whether the garbage collector is enabled.
*/
get isEnabled() {
return !Il2Cpp3.exports.gcIsDisabled();
},
/**
* Determines whether the garbage collector is incremental
* ([source](https://docs.unity3d.com/Manual/performance-incremental-garbage-collection.html)).
*/
get isIncremental() {
return !!Il2Cpp3.exports.gcIsIncremental();
},
/**
* Gets the number of nanoseconds the garbage collector can spend in a
* collection step.
*/
get maxTimeSlice() {
return Il2Cpp3.exports.gcGetMaxTimeSlice();
},
/**
* Gets the used heap size in bytes.
*/
get usedHeapSize() {
return Il2Cpp3.exports.gcGetUsedSize();
},
/**
* Enables or disables the garbage collector.
*/
set isEnabled(value) {
value ? Il2Cpp3.exports.gcEnable() : Il2Cpp3.exports.gcDisable();
},
/**
* Sets the number of nanoseconds the garbage collector can spend in
* a collection step.
*/
set maxTimeSlice(nanoseconds) {
Il2Cpp3.exports.gcSetMaxTimeSlice(nanoseconds);
},
/**
* Returns the heap allocated objects of the specified class. \
* This variant reads GC descriptors.
*/
choose(klass) {
const matches = [];
const callback = (objects, size) => {
for (let i = 0; i < size; i++) {
matches.push(new Il2Cpp3.Object(objects.add(i * Process.pointerSize).readPointer()));
}
};
const chooseCallback = new NativeCallback(callback, "void", ["pointer", "int", "pointer"]);
if (Il2Cpp3.unityVersionIsBelow202120) {
const onWorld = new NativeCallback(() => {
}, "void", []);
const state = Il2Cpp3.exports.livenessCalculationBegin(klass, 0, chooseCallback, NULL, onWorld, onWorld);
Il2Cpp3.exports.livenessCalculationFromStatics(state);
Il2Cpp3.exports.livenessCalculationEnd(state);
} else {
const realloc = (handle, size) => {
if (!handle.isNull() && size.compare(0) == 0) {
Il2Cpp3.free(handle);
return NULL;
} else {
return Il2Cpp3.alloc(size);
}
};
const reallocCallback = new NativeCallback(realloc, "pointer", ["pointer", "size_t", "pointer"]);
this.stopWorld();
const state = Il2Cpp3.exports.livenessAllocateStruct(klass, 0, chooseCallback, NULL, reallocCallback);
Il2Cpp3.exports.livenessCalculationFromStatics(state);
Il2Cpp3.exports.livenessFinalize(state);
this.startWorld();
Il2Cpp3.exports.livenessFreeStruct(state);
}
return matches;
},
/**
* Forces a garbage collection of the specified generation.
*/
collect(generation) {
Il2Cpp3.exports.gcCollect(generation < 0 ? 0 : generation > 2 ? 2 : generation);
},
/**
* Forces a garbage collection.
*/
collectALittle() {
Il2Cpp3.exports.gcCollectALittle();
},
/**
* Resumes all the previously stopped threads.
*/
startWorld() {
return Il2Cpp3.exports.gcStartWorld();
},
/**
* Performs an incremental garbage collection.
*/
startIncrementalCollection() {
return Il2Cpp3.exports.gcStartIncrementalCollection();
},
/**
* Stops all threads which may access the garbage collected heap, other
* than the caller.
*/
stopWorld() {
return Il2Cpp3.exports.gcStopWorld();
}
};
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Android;
(function(Android2) {
getter(Android2, "apiLevel", () => {
const value = getProperty("ro.build.version.sdk");
return value ? parseInt(value) : null;
}, lazy);
function getProperty(name) {
const handle = Process.findModuleByName("libc.so")?.findExportByName("__system_property_get");
if (handle) {
const __system_property_get = new NativeFunction(handle, "void", ["pointer", "pointer"]);
const value = Memory.alloc(92).writePointer(NULL);
__system_property_get(Memory.allocUtf8String(name), value);
return value.readCString() ?? void 0;
}
}
})(Android || (Android = {}));
function raise(message) {
const error = new Error(message);
error.name = "Il2CppError";
error.stack = error.stack?.replace(/^(Il2Cpp)?Error/, "\x1B[0m\x1B[38;5;9mil2cpp\x1B[0m")?.replace(/\n at (.+) \((.+):(.+)\)/, "\x1B[3m\x1B[2m")?.concat("\x1B[0m");
throw error;
}
function warn(message) {
globalThis.console.log(`\x1B[38;5;11mil2cpp\x1B[0m: ${message}`);
}
function ok(message) {
globalThis.console.log(`\x1B[38;5;10mil2cpp\x1B[0m: ${message}`);
}
function inform(message) {
globalThis.console.log(`\x1B[38;5;12mil2cpp\x1B[0m: ${message}`);
}
function decorate(target, decorator, descriptors = Object.getOwnPropertyDescriptors(target)) {
for (const key in descriptors) {
descriptors[key] = decorator(target, key, descriptors[key]);
}
Object.defineProperties(target, descriptors);
return target;
}
function getter(target, key, get, decorator) {
globalThis.Object.defineProperty(target, key, decorator?.(target, key, { get, configurable: true }) ?? { get, configurable: true });
}
function cyrb53(str) {
let h1 = 3735928559;
let h2 = 1103547991;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
}
function exportsHash(module) {
return cyrb53(module.enumerateExports().sort((a, b) => a.name.localeCompare(b.name)).map((_) => _.name + _.address.sub(module.base)).join(""));
}
function lazy(_, propertyKey, descriptor) {
const getter2 = descriptor.get;
if (!getter2) {
throw new Error("@lazy can only be applied to getter accessors");
}
descriptor.get = function() {
const value = getter2.call(this);
Object.defineProperty(this, propertyKey, {
value,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
writable: false
});
return value;
};
return descriptor;
}
var NativeStruct = class {
handle;
constructor(handleOrWrapper) {
if (handleOrWrapper instanceof NativePointer) {
this.handle = handleOrWrapper;
} else {
this.handle = handleOrWrapper.handle;
}
}
equals(other) {
return this.handle.equals(other.handle);
}
isNull() {
return this.handle.isNull();
}
asNullable() {
return this.isNull() ? null : this;
}
};
function addFlippedEntries(obj) {
return Object.keys(obj).reduce((obj2, key) => (obj2[obj2[key]] = key, obj2), obj);
}
NativePointer.prototype.offsetOf = function(condition, depth) {
depth ??= 512;
for (let i = 0; depth > 0 ? i < depth : i < -depth; i++) {
if (condition(depth > 0 ? this.add(i) : this.sub(i))) {
return i;
}
}
return null;
};
function readNativeIterator(block) {
const array = [];
const iterator = Memory.alloc(Process.pointerSize);
let handle = block(iterator);
while (!handle.isNull()) {
array.push(handle);
handle = block(iterator);
}
return array;
}
function readNativeList(block) {
const lengthPointer = Memory.alloc(Process.pointerSize);
const startPointer = block(lengthPointer);
if (startPointer.isNull()) {
return [];
}
const array = new Array(lengthPointer.readInt());
for (let i = 0; i < array.length; i++) {
array[i] = startPointer.add(i * Process.pointerSize).readPointer();
}
return array;
}
function recycle(Class) {
return new Proxy(Class, {
cache: /* @__PURE__ */ new Map(),
construct(Target, argArray) {
const handle = argArray[0].toUInt32();
if (!this.cache.has(handle)) {
this.cache.set(handle, new Target(argArray[0]));
}
return this.cache.get(handle);
}
});
}
var UnityVersion;
(function(UnityVersion2) {
const pattern = /(6\d{3}|20\d{2}|\d)\.(\d)\.(\d{1,2})(?:[abcfp]|rc){0,2}\d?/;
function find(string) {
return string?.match(pattern)?.[0];
}
UnityVersion2.find = find;
function gte(a, b) {
return compare(a, b) >= 0;
}
UnityVersion2.gte = gte;
function lt(a, b) {
return compare(a, b) < 0;
}
UnityVersion2.lt = lt;
function compare(a, b) {
const aMatches = a.match(pattern);
const bMatches = b.match(pattern);
for (let i = 1; i <= 3; i++) {
const a2 = Number(aMatches?.[i] ?? -1);
const b2 = Number(bMatches?.[i] ?? -1);
if (a2 > b2)
return 1;
else if (a2 < b2)
return -1;
}
return 0;
}
})(UnityVersion || (UnityVersion = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function alloc(size = Process.pointerSize) {
return Il2Cpp3.exports.alloc(size);
}
Il2Cpp3.alloc = alloc;
function free(pointer) {
return Il2Cpp3.exports.free(pointer);
}
Il2Cpp3.free = free;
function read(pointer, type) {
switch (type.enumValue) {
case Il2Cpp3.Type.Enum.BOOLEAN:
return !!pointer.readS8();
case Il2Cpp3.Type.Enum.BYTE:
return pointer.readS8();
case Il2Cpp3.Type.Enum.UBYTE:
return pointer.readU8();
case Il2Cpp3.Type.Enum.SHORT:
return pointer.readS16();
case Il2Cpp3.Type.Enum.USHORT:
return pointer.readU16();
case Il2Cpp3.Type.Enum.INT:
return pointer.readS32();
case Il2Cpp3.Type.Enum.UINT:
return pointer.readU32();
case Il2Cpp3.Type.Enum.CHAR:
return pointer.readU16();
case Il2Cpp3.Type.Enum.LONG:
return pointer.readS64();
case Il2Cpp3.Type.Enum.ULONG:
return pointer.readU64();
case Il2Cpp3.Type.Enum.FLOAT:
return pointer.readFloat();
case Il2Cpp3.Type.Enum.DOUBLE:
return pointer.readDouble();
case Il2Cpp3.Type.Enum.NINT:
case Il2Cpp3.Type.Enum.NUINT:
return pointer.readPointer();
case Il2Cpp3.Type.Enum.POINTER:
return new Il2Cpp3.Pointer(pointer.readPointer(), type.class.baseType);
case Il2Cpp3.Type.Enum.VALUE_TYPE:
return new Il2Cpp3.ValueType(pointer, type);
case Il2Cpp3.Type.Enum.OBJECT:
case Il2Cpp3.Type.Enum.CLASS:
return new Il2Cpp3.Object(pointer.readPointer());
case Il2Cpp3.Type.Enum.GENERIC_INSTANCE:
return type.class.isValueType ? new Il2Cpp3.ValueType(pointer, type) : new Il2Cpp3.Object(pointer.readPointer());
case Il2Cpp3.Type.Enum.STRING:
return new Il2Cpp3.String(pointer.readPointer());
case Il2Cpp3.Type.Enum.ARRAY:
case Il2Cpp3.Type.Enum.NARRAY:
return new Il2Cpp3.Array(pointer.readPointer());
}
raise(`couldn't read the value from ${pointer} using an unhandled or unknown type ${type.name} (${type.enumValue}), please file an issue`);
}
Il2Cpp3.read = read;
function write(pointer, value, type) {
switch (type.enumValue) {
case Il2Cpp3.Type.Enum.BOOLEAN:
return pointer.writeS8(+value);
case Il2Cpp3.Type.Enum.BYTE:
return pointer.writeS8(value);
case Il2Cpp3.Type.Enum.UBYTE:
return pointer.writeU8(value);
case Il2Cpp3.Type.Enum.SHORT:
return pointer.writeS16(value);
case Il2Cpp3.Type.Enum.USHORT:
return pointer.writeU16(value);
case Il2Cpp3.Type.Enum.INT:
return pointer.writeS32(value);
case Il2Cpp3.Type.Enum.UINT:
return pointer.writeU32(value);
case Il2Cpp3.Type.Enum.CHAR:
return pointer.writeU16(value);
case Il2Cpp3.Type.Enum.LONG:
return pointer.writeS64(value);
case Il2Cpp3.Type.Enum.ULONG:
return pointer.writeU64(value);
case Il2Cpp3.Type.Enum.FLOAT:
return pointer.writeFloat(value);
case Il2Cpp3.Type.Enum.DOUBLE:
return pointer.writeDouble(value);
case Il2Cpp3.Type.Enum.NINT:
case Il2Cpp3.Type.Enum.NUINT:
case Il2Cpp3.Type.Enum.POINTER:
case Il2Cpp3.Type.Enum.STRING:
case Il2Cpp3.Type.Enum.ARRAY:
case Il2Cpp3.Type.Enum.NARRAY:
return pointer.writePointer(value);
case Il2Cpp3.Type.Enum.VALUE_TYPE:
return Memory.copy(pointer, value, type.class.valueTypeSize), pointer;
case Il2Cpp3.Type.Enum.OBJECT:
case Il2Cpp3.Type.Enum.CLASS:
case Il2Cpp3.Type.Enum.GENERIC_INSTANCE:
return value instanceof Il2Cpp3.ValueType ? (Memory.copy(pointer, value, type.class.valueTypeSize), pointer) : pointer.writePointer(value);
}
raise(`couldn't write value ${value} to ${pointer} using an unhandled or unknown type ${type.name} (${type.enumValue}), please file an issue`);
}
Il2Cpp3.write = write;
function fromFridaValue(value, type) {
if (globalThis.Array.isArray(value)) {
const handle = Memory.alloc(type.class.valueTypeSize);
const fields = type.class.fields.filter((_) => !_.isStatic);
for (let i = 0; i < fields.length; i++) {
const convertedValue = fromFridaValue(value[i], fields[i].type);
write(handle.add(fields[i].offset).sub(Il2Cpp3.Object.headerSize), convertedValue, fields[i].type);
}
return new Il2Cpp3.ValueType(handle, type);
} else if (value instanceof NativePointer) {
if (type.isByReference) {
return new Il2Cpp3.Reference(value, type);
}
switch (type.enumValue) {
case Il2Cpp3.Type.Enum.POINTER:
return new Il2Cpp3.Pointer(value, type.class.baseType);
case Il2Cpp3.Type.Enum.STRING:
return new Il2Cpp3.String(value);
case Il2Cpp3.Type.Enum.CLASS:
case Il2Cpp3.Type.Enum.GENERIC_INSTANCE:
case Il2Cpp3.Type.Enum.OBJECT:
return new Il2Cpp3.Object(value);
case Il2Cpp3.Type.Enum.ARRAY:
case Il2Cpp3.Type.Enum.NARRAY:
return new Il2Cpp3.Array(value);
default:
return value;
}
} else if (type.enumValue == Il2Cpp3.Type.Enum.BOOLEAN) {
return !!value;
} else if (type.enumValue == Il2Cpp3.Type.Enum.VALUE_TYPE && type.class.isEnum) {
return fromFridaValue([value], type);
} else {
return value;
}
}
Il2Cpp3.fromFridaValue = fromFridaValue;
function toFridaValue(value) {
if (typeof value == "boolean") {
return +value;
} else if (value instanceof Il2Cpp3.ValueType) {
if (value.type.class.isEnum) {
return value.field("value__").value;
} else {
const _ = value.type.class.fields.filter((_2) => !_2.isStatic).map((_2) => toFridaValue(_2.bind(value).value));
return _.length == 0 ? [0] : _;
}
} else {
return value;
}
}
Il2Cpp3.toFridaValue = toFridaValue;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
getter(Il2Cpp3, "module", () => {
return tryModule() ?? raise("Could not find IL2CPP module");
});
async function initialize(blocking = false) {
const module = tryModule() ?? await new Promise((resolve) => {
const [moduleName, fallbackModuleName] = getExpectedModuleNames();
const timeout = setTimeout(() => {
warn(`after 10 seconds, IL2CPP module '${moduleName}' has not been loaded yet, is the app running?`);
}, 1e4);
const moduleObserver = Process.attachModuleObserver({
onAdded(module2) {
if (module2.name == moduleName || fallbackModuleName && module2.name == fallbackModuleName) {
clearTimeout(timeout);
setImmediate(() => {
resolve(module2);
moduleObserver.detach();
});
}
}
});
});
Reflect.defineProperty(Il2Cpp3, "module", { value: module });
if (Il2Cpp3.exports.getCorlib().isNull()) {
return await new Promise((resolve) => {
const timeout = setTimeout(() => {
if (!Il2Cpp3.exports.getCorlib().isNull()) {
warn(`resuming execution despite IL2CPP initialization not being captured in time, please open an issue as this is suboptimal`);
interceptor.detach();
resolve(false);
}
}, 1e3);
const interceptor = Interceptor.attach(Il2Cpp3.exports.initialize, {
onEnter() {
clearTimeout(timeout);
},
onLeave() {
interceptor.detach();
blocking ? resolve(true) : setImmediate(() => resolve(false));
}
});
});
}
return false;
}
Il2Cpp3.initialize = initialize;
function tryModule() {
const [moduleName, fallback] = getExpectedModuleNames();
return Process.findModuleByName(moduleName) ?? Process.findModuleByName(fallback ?? moduleName) ?? (Process.platform == "darwin" ? Process.findModuleByAddress(DebugSymbol.fromName("il2cpp_init").address) : void 0) ?? void 0;
}
function getExpectedModuleNames() {
if (Il2Cpp3.$config.moduleName) {
return [Il2Cpp3.$config.moduleName];
}
switch (Process.platform) {
case "linux":
return [Android.apiLevel ? "libil2cpp.so" : "GameAssembly.so"];
case "windows":
return ["GameAssembly.dll"];
case "darwin":
return ["UnityFramework", "GameAssembly.dylib"];
}
raise(`${Process.platform} is not supported yet`);
}
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function nullable(valueOrNull, klass) {
const actualClass = typeof valueOrNull == "boolean" ? Il2Cpp3.corlib.class("System.Boolean") : typeof valueOrNull == "number" ? klass ?? Il2Cpp3.corlib.class("System.Int32") : valueOrNull instanceof Int64 ? Il2Cpp3.corlib.class("System.Int64") : valueOrNull instanceof UInt64 ? Il2Cpp3.corlib.class("System.UInt64") : valueOrNull instanceof NativePointer ? klass ?? Il2Cpp3.corlib.class("System.IntPtr") : valueOrNull instanceof Il2Cpp3.ValueType ? valueOrNull.type.class : klass ?? raise(`A class must be specified when constructing a nullable for value '${valueOrNull}'`);
if (actualClass.isValueType == false) {
raise(`Cannot create nullable value type out of a reference type '${actualClass.type.name}'`);
}
const inflatedClass = Il2Cpp3.corlib.class("System.Nullable`1").inflate(actualClass);
const struct = new Il2Cpp3.ValueType(Memory.alloc(inflatedClass.valueTypeSize), inflatedClass.type);
(struct.tryField("hasValue") ?? struct.field("has_value")).value = valueOrNull != null;
if (valueOrNull != null) {
struct.field("value").value = valueOrNull;
}
return struct;
}
Il2Cpp3.nullable = nullable;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
async function perform(block, flag = "bind") {
let attachedThread = null;
try {
const isInMainThread = await Il2Cpp3.initialize(flag == "main");
if (flag == "main" && !isInMainThread) {
return perform(() => Il2Cpp3.mainThread.schedule(block), "free");
}
if (Il2Cpp3.currentThread == null) {
attachedThread = Il2Cpp3.domain.attach();
}
if (flag == "bind" && attachedThread != null) {
Script.bindWeak(globalThis, () => attachedThread?.detach());
}
const result = block();
return result instanceof Promise ? await result : result;
} catch (error) {
Script.nextTick((_) => {
throw _;
}, error);
return Promise.reject(error);
} finally {
if (flag == "free" && attachedThread != null) {
attachedThread.detach();
}
}
}
Il2Cpp3.perform = perform;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Tracer {
/** @internal */
#state = {
depth: 0,
buffer: [],
history: /* @__PURE__ */ new Set(),
flush: () => {
if (this.#state.depth == 0) {
const message = `
${this.#state.buffer.join("\n")}
`;
if (this.#verbose) {
inform(message);
} else {
const hash = cyrb53(message);
if (!this.#state.history.has(hash)) {
this.#state.history.add(hash);
inform(message);
}
}
this.#state.buffer.length = 0;
}
}
};
/** @internal */
#threadId = Il2Cpp3.mainThread.id;
/** @internal */
#verbose = false;
/** @internal */
#applier;
/** @internal */
#targets = [];
/** @internal */
#domain;
/** @internal */
#assemblies;
/** @internal */
#classes;
/** @internal */
#methods;
/** @internal */
#assemblyFilter;
/** @internal */
#classFilter;
/** @internal */
#methodFilter;
/** @internal */
#parameterFilter;
constructor(applier) {
this.#applier = applier;
}
/** */
thread(thread) {
this.#threadId = thread.id;
return this;
}
/** Determines whether print duplicate logs. */
verbose(value) {
this.#verbose = value;
return this;
}
/** Sets the application domain as the place where to find the target methods. */
domain() {
this.#domain = Il2Cpp3.domain;
return this;
}
/** Sets the passed `assemblies` as the place where to find the target methods. */
assemblies(...assemblies) {
this.#assemblies = assemblies;
return this;
}
/** Sets the passed `classes` as the place where to find the target methods. */
classes(...classes) {
this.#classes = classes;
return this;
}
/** Sets the passed `methods` as the target methods. */
methods(...methods) {
this.#methods = methods;
return this;
}
/** Filters the assemblies where to find the target methods. */
filterAssemblies(filter) {
this.#assemblyFilter = filter;
return this;
}
/** Filters the classes where to find the target methods. */
filterClasses(filter) {
this.#classFilter = filter;
return this;
}
/** Filters the target methods. */
filterMethods(filter) {
this.#methodFilter = filter;
return this;
}
/** Filters the target methods. */
filterParameters(filter) {
this.#parameterFilter = filter;
return this;
}
/** Commits the current changes by finding the target methods. */
and() {
const filterMethod = (method) => {
if (this.#parameterFilter == void 0) {
this.#targets.push(method);
return;
}
for (const parameter of method.parameters) {
if (this.#parameterFilter(parameter)) {
this.#targets.push(method);
break;
}
}
};
const filterMethods = (values) => {
for (const method of values) {
filterMethod(method);
}
};
const filterClass = (klass) => {
if (this.#methodFilter == void 0) {
filterMethods(klass.methods);
return;
}
for (const method of klass.methods) {
if (this.#methodFilter(method)) {
filterMethod(method);
}
}
};
const filterClasses = (values) => {
for (const klass of values) {
filterClass(klass);
}
};
const filterAssembly = (assembly) => {
if (this.#classFilter == void 0) {
filterClasses(assembly.image.classes);
return;
}
for (const klass of assembly.image.classes) {
if (this.#classFilter(klass)) {
filterClass(klass);
}
}
};
const filterAssemblies = (assemblies) => {
for (const assembly of assemblies) {
filterAssembly(assembly);
}
};
const filterDomain = (domain) => {
if (this.#assemblyFilter == void 0) {
filterAssemblies(domain.assemblies);
return;
}
for (const assembly of domain.assemblies) {
if (this.#assemblyFilter(assembly)) {
filterAssembly(assembly);
}
}
};
this.#methods ? filterMethods(this.#methods) : this.#classes ? filterClasses(this.#classes) : this.#assemblies ? filterAssemblies(this.#assemblies) : this.#domain ? filterDomain(this.#domain) : void 0;
this.#assemblies = void 0;
this.#classes = void 0;
this.#methods = void 0;
this.#assemblyFilter = void 0;
this.#classFilter = void 0;
this.#methodFilter = void 0;
this.#parameterFilter = void 0;
return this;
}
/** Starts tracing. */
attach() {
for (const target of this.#targets) {
if (!target.virtualAddress.isNull()) {
try {
this.#applier(target, this.#state, this.#threadId);
} catch (e) {
switch (e.message) {
case /unable to intercept function at \w+; please file a bug/.exec(e.message)?.input:
case "already replaced this function":
break;
default:
throw e;
}
}
}
}
}
}
Il2Cpp3.Tracer = Tracer;
function trace(parameters = false) {
const applier = () => (method, state, threadId) => {
const paddedVirtualAddress = method.relativeVirtualAddress.toString(16).padStart(8, "0");
Interceptor.attach(method.virtualAddress, {
onEnter() {
if (this.threadId == threadId) {
state.buffer.push(`\x1B[2m0x${paddedVirtualAddress}\x1B[0m ${`\u2502 `.repeat(state.depth++)}\u250C\u2500\x1B[35m${method.class.type.name}::\x1B[1m${method.name}\x1B[0m\x1B[0m`);
}
},
onLeave() {
if (this.threadId == threadId) {
state.buffer.push(`\x1B[2m0x${paddedVirtualAddress}\x1B[0m ${`\u2502 `.repeat(--state.depth)}\u2514\u2500\x1B[33m${method.class.type.name}::\x1B[1m${method.name}\x1B[0m\x1B[0m`);
state.flush();
}
}
});
};
const applierWithParameters = () => (method, state, threadId) => {
const paddedVirtualAddress = method.relativeVirtualAddress.toString(16).padStart(8, "0");
const startIndex = +!method.isStatic | +Il2Cpp3.unityVersionIsBelow201830;
const callback = function(...args) {
if (this.threadId == threadId) {
const thisParameter = method.isStatic ? void 0 : new Il2Cpp3.Parameter("this", -1, method.class.type);
const parameters2 = thisParameter ? [thisParameter].concat(method.parameters) : method.parameters;
state.buffer.push(`\x1B[2m0x${paddedVirtualAddress}\x1B[0m ${`\u2502 `.repeat(state.depth++)}\u250C\u2500\x1B[35m${method.class.type.name}::\x1B[1m${method.name}\x1B[0m\x1B[0m(${parameters2.map((e) => `\x1B[32m${e.name}\x1B[0m = \x1B[31m${Il2Cpp3.fromFridaValue(args[e.position + startIndex], e.type)}\x1B[0m`).join(", ")})`);
}
const returnValue = method.nativeFunction(...args);
if (this.threadId == threadId) {
state.buffer.push(`\x1B[2m0x${paddedVirtualAddress}\x1B[0m ${`\u2502 `.repeat(--state.depth)}\u2514\u2500\x1B[33m${method.class.type.name}::\x1B[1m${method.name}\x1B[0m\x1B[0m${returnValue == void 0 ? "" : ` = \x1B[36m${Il2Cpp3.fromFridaValue(returnValue, method.returnType)}`}\x1B[0m`);
state.flush();
}
return returnValue;
};
method.revert();
const nativeCallback = new NativeCallback(callback, method.returnType.fridaAlias, method.fridaSignature);
Interceptor.replace(method.virtualAddress, nativeCallback);
};
return new Il2Cpp3.Tracer(parameters ? applierWithParameters() : applier());
}
Il2Cpp3.trace = trace;
function backtrace(mode) {
const methods = Il2Cpp3.domain.assemblies.flatMap((_) => _.image.classes.flatMap((_2) => _2.methods.filter((_3) => !_3.virtualAddress.isNull()))).sort((_, __) => _.virtualAddress.compare(__.virtualAddress));
const searchInsert = (target) => {
let left = 0;
let right = methods.length - 1;
while (left <= right) {
const pivot = Math.floor((left + right) / 2);
const comparison = methods[pivot].virtualAddress.compare(target);
if (comparison == 0) {
return methods[pivot];
} else if (comparison > 0) {
right = pivot - 1;
} else {
left = pivot + 1;
}
}
return methods[right];
};
const applier = () => (method, state, threadId) => {
Interceptor.attach(method.virtualAddress, function() {
if (this.threadId == threadId) {
const handles = globalThis.Thread.backtrace(this.context, mode);
handles.unshift(method.virtualAddress);
for (const handle of handles) {
if (handle.compare(Il2Cpp3.module.base) > 0 && handle.compare(Il2Cpp3.module.base.add(Il2Cpp3.module.size)) < 0) {
const method2 = searchInsert(handle);
if (method2) {
const offset = handle.sub(method2.virtualAddress);
if (offset.compare(4095) < 0) {
state.buffer.push(`\x1B[2m0x${method2.relativeVirtualAddress.toString(16).padStart(8, "0")}\x1B[0m\x1B[2m+0x${offset.toString(16).padStart(3, `0`)}\x1B[0m ${method2.class.type.name}::\x1B[1m${method2.name}\x1B[0m`);
}
}
}
}
state.flush();
}
});
};
return new Il2Cpp3.Tracer(applier());
}
Il2Cpp3.backtrace = backtrace;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Array2 extends NativeStruct {
/** Gets the Il2CppArray struct size, possibly equal to `Process.pointerSize * 4`. */
static get headerSize() {
return Il2Cpp3.corlib.class("System.Array").instanceSize;
}
/** @internal Gets a pointer to the first element of the current array. */
get elements() {
const array2 = Il2Cpp3.string("vfsfitvnm").object.method("ToCharArray", 0).invoke();
const offset = Memory.scanSync(array2.handle, 255, "76 00 66 00 73 00 66 00 69 00 74 00 76 00 6e 00 6d 00")[0]?.address?.sub(array2.handle) ?? raise("couldn't find the elements offset in the native array struct");
getter(Il2Cpp3.Array.prototype, "elements", function() {
return new Il2Cpp3.Pointer(this.handle.add(offset), this.elementType);
}, lazy);
return this.elements;
}
/** Gets the size of the object encompassed by the current array. */
get elementSize() {
return this.elementType.class.arrayElementSize;
}
/** Gets the type of the object encompassed by the current array. */
get elementType() {
return this.object.class.type.class.baseType;
}
/** Gets the total number of elements in all the dimensions of the current array. */
get length() {
return Il2Cpp3.exports.arrayGetLength(this);
}
/** Gets the encompassing object of the current array. */
get object() {
return new Il2Cpp3.Object(this);
}
/** Gets the element at the specified index of the current array. */
get(index) {
if (index < 0 || index >= this.length) {
raise(`cannot get element at index ${index} as the array length is ${this.length}`);
}
return this.elements.get(index);
}
/** Sets the element at the specified index of the current array. */
set(index, value) {
if (index < 0 || index >= this.length) {
raise(`cannot set element at index ${index} as the array length is ${this.length}`);
}
this.elements.set(index, value);
}
/** */
toString() {
return this.isNull() ? "null" : `[${this.elements.read(this.length, 0)}]`;
}
/** Iterable. */
*[Symbol.iterator]() {
for (let i = 0; i < this.length; i++) {
yield this.elements.get(i);
}
}
}
__decorate([
lazy
], Array2.prototype, "elementSize", null);
__decorate([
lazy
], Array2.prototype, "elementType", null);
__decorate([
lazy
], Array2.prototype, "length", null);
__decorate([
lazy
], Array2.prototype, "object", null);
__decorate([
lazy
], Array2, "headerSize", null);
Il2Cpp3.Array = Array2;
function array(klass, lengthOrElements) {
const length = typeof lengthOrElements == "number" ? lengthOrElements : lengthOrElements.length;
const array2 = new Il2Cpp3.Array(Il2Cpp3.exports.arrayNew(klass, length));
if (globalThis.Array.isArray(lengthOrElements)) {
array2.elements.write(lengthOrElements);
}
return array2;
}
Il2Cpp3.array = array;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
let Assembly = class Assembly extends NativeStruct {
/** Gets the image of this assembly. */
get image() {
if (Il2Cpp3.exports.assemblyGetImage.isNull()) {
const runtimeModule = this.object.tryMethod("GetType", 1)?.invoke(Il2Cpp3.string("<Module>"))?.asNullable()?.tryMethod("get_Module")?.invoke() ?? this.object.tryMethod("GetModules", 1)?.invoke(false)?.get(0) ?? raise(`couldn't find the runtime module object of assembly ${this.name}`);
return new Il2Cpp3.Image(runtimeModule.field("_impl").value);
}
return new Il2Cpp3.Image(Il2Cpp3.exports.assemblyGetImage(this));
}
/** Gets the name of this assembly. */
get name() {
return this.image.name.replace(".dll", "");
}
/** Gets the encompassing object of the current assembly. */
get object() {
for (const _ of Il2Cpp3.domain.object.method("GetAssemblies", 1).invoke(false)) {
if (_.field("_mono_assembly").value.equals(this)) {
return _;
}
}
raise("couldn't find the object of the native assembly struct");
}
};
__decorate([
lazy
], Assembly.prototype, "name", null);
__decorate([
lazy
], Assembly.prototype, "object", null);
Assembly = __decorate([
recycle
], Assembly);
Il2Cpp3.Assembly = Assembly;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
let Class = class Class extends NativeStruct {
/** Gets the actual size of the instance of the current class. */
get actualInstanceSize() {
const SystemString = Il2Cpp3.corlib.class("System.String");
const offset = SystemString.handle.offsetOf((_) => _.readInt() == SystemString.instanceSize - 2) ?? raise("couldn't find the actual instance size offset in the native class struct");
getter(Il2Cpp3.Class.prototype, "actualInstanceSize", function() {
return this.handle.add(offset).readS32();
}, lazy);
return this.actualInstanceSize;
}
/** Gets the array class which encompass the current class. */
get arrayClass() {
return new Il2Cpp3.Class(Il2Cpp3.exports.classGetArrayClass(this, 1));
}
/** Gets the size of the object encompassed by the current array class. */
get arrayElementSize() {
return Il2Cpp3.exports.classGetArrayElementSize(this);
}
/** Gets the name of the assembly in which the current class is defined. */
get assemblyName() {
return Il2Cpp3.exports.classGetAssemblyName(this).readUtf8String().replace(".dll", "");
}
/** Gets the class that declares the current nested class. */
get declaringClass() {
return new Il2Cpp3.Class(Il2Cpp3.exports.classGetDeclaringType(this)).asNullable();
}
/** Gets the encompassed type of this array, reference, pointer or enum type. */
get baseType() {
return new Il2Cpp3.Type(Il2Cpp3.exports.classGetBaseType(this)).asNullable();
}
/** Gets the class of the object encompassed or referred to by the current array, pointer or reference class. */
get elementClass() {
return new Il2Cpp3.Class(Il2Cpp3.exports.classGetElementClass(this)).asNullable();
}
/** Gets the fields of the current class. */
get fields() {
return readNativeIterator((_) => Il2Cpp3.exports.classGetFields(this, _)).map((_) => new Il2Cpp3.Field(_));
}
/** Gets the flags of the current class. */
get flags() {
return Il2Cpp3.exports.classGetFlags(this);
}
/** Gets the full name (namespace + name) of the current class. */
get fullName() {
return this.namespace ? `${this.namespace}.${this.name}` : this.name;
}
/** Gets the generic class of the current class if the current class is inflated. */
get genericClass() {
const klass = this.image.tryClass(this.fullName)?.asNullable();
return klass?.equals(this) ? null : klass ?? null;
}
/** Gets the generics parameters of this generic class. */
get generics() {
if (!this.isGeneric && !this.isInflated) {
return [];
}
const types = this.type.object.method("GetGenericArguments").invoke();
return globalThis.Array.from(types).map((_) => new Il2Cpp3.Class(Il2Cpp3.exports.classFromObject(_)));
}
/** Determines whether the GC has tracking references to the current class instances. */
get hasReferences() {
return !!Il2Cpp3.exports.classHasReferences(this);
}
/** Determines whether ther current class has a valid static constructor. */
get hasStaticConstructor() {
const staticConstructor = this.tryMethod(".cctor");
return staticConstructor != null && !staticConstructor.virtualAddress.isNull();
}
/** Gets the image in which the current class is defined. */
get image() {
return new Il2Cpp3.Image(Il2Cpp3.exports.classGetImage(this));
}
/** Gets the size of the instance of the current class. */
get instanceSize() {
return Il2Cpp3.exports.classGetInstanceSize(this);
}
/** Determines whether the current class is abstract. */
get isAbstract() {
return !!Il2Cpp3.exports.classIsAbstract(this);
}
/** Determines whether the current class is blittable. */
get isBlittable() {
return !!Il2Cpp3.exports.classIsBlittable(this);
}
/** Determines whether the current class is an enumeration. */
get isEnum() {
return !!Il2Cpp3.exports.classIsEnum(this);
}
/** Determines whether the current class is a generic one. */
get isGeneric() {
return !!Il2Cpp3.exports.classIsGeneric(this);
}
/** Determines whether the current class is inflated. */
get isInflated() {
return !!Il2Cpp3.exports.classIsInflated(this);
}
/** Determines whether the current class is an interface. */
get isInterface() {
return !!Il2Cpp3.exports.classIsInterface(this);
}
/** Determines whether the current class is a struct. */
get isStruct() {
return this.isValueType && !this.isEnum;
}
/** Determines whether the current class is a value type. */
get isValueType() {
return !!Il2Cpp3.exports.classIsValueType(this);
}
/** Gets the interfaces implemented or inherited by the current class. */
get interfaces() {
return readNativeIterator((_) => Il2Cpp3.exports.classGetInterfaces(this, _)).map((_) => new Il2Cpp3.Class(_));
}
/** Gets the methods implemented by the current class. */
get methods() {
return readNativeIterator((_) => Il2Cpp3.exports.classGetMethods(this, _)).map((_) => new Il2Cpp3.Method(_));
}
/** Gets the name of the current class. */
get name() {
return Il2Cpp3.exports.classGetName(this).readUtf8String();
}
/** Gets the namespace of the current class. */
get namespace() {
return Il2Cpp3.exports.classGetNamespace(this).readUtf8String() || void 0;
}
/** Gets the classes nested inside the current class. */
get nestedClasses() {
return readNativeIterator((_) => Il2Cpp3.exports.classGetNestedClasses(this, _)).map((_) => new Il2Cpp3.Class(_));
}
/** Gets the class from which the current class directly inherits. */
get parent() {
return new Il2Cpp3.Class(Il2Cpp3.exports.classGetParent(this)).asNullable();
}
/** Gets the pointer class of the current class. */
get pointerClass() {
return new Il2Cpp3.Class(Il2Cpp3.exports.classFromObject(this.type.object.method("MakePointerType").invoke()));
}
/** Gets the rank (number of dimensions) of the current array class. */
get rank() {
let rank = 0;
const name = this.name;
for (let i = this.name.length - 1; i > 0; i--) {
const c = name[i];
if (c == "]")
rank++;
else if (c == "[" || rank == 0)
break;
else if (c == ",")
rank++;
else
break;
}
return rank;
}
/** Gets a pointer to the static fields of the current class. */
get staticFieldsData() {
return Il2Cpp3.exports.classGetStaticFieldData(this);
}
/** Gets the size of the instance - as a value type - of the current class. */
get valueTypeSize() {
return Il2Cpp3.exports.classGetValueTypeSize(this, NULL);
}
/** Gets the type of the current class. */
get type() {
return new Il2Cpp3.Type(Il2Cpp3.exports.classGetType(this));
}
/** Allocates a new object of the current class. */
alloc() {
return new Il2Cpp3.Object(Il2Cpp3.exports.objectNew(this));
}
/** Gets the field identified by the given name. */
field(name) {
return this.tryField(name) ?? raise(`couldn't find field ${name} in class ${this.type.name}`);
}
/** Gets the hierarchy of the current class. */
*hierarchy(options) {
let klass = options?.includeCurrent ?? true ? this : this.parent;
while (klass) {
yield klass;
klass = klass.parent;
}
}
/** Builds a generic instance of the current generic class. */
inflate(...classes) {
if (!this.isGeneric) {
raise(`cannot inflate class ${this.type.name} as it has no generic parameters`);
}
if (this.generics.length != classes.length) {
raise(`cannot inflate class ${this.type.name} as it needs ${this.generics.length} generic parameter(s), not ${classes.length}`);
}
const types = classes.map((_) => _.type.object);
const typeArray = Il2Cpp3.array(Il2Cpp3.corlib.class("System.Type"), types);
const inflatedType = this.type.object.method("MakeGenericType", 1).invoke(typeArray);
return new Il2Cpp3.Class(Il2Cpp3.exports.classFromObject(inflatedType));
}
/** Calls the static constructor of the current class. */
initialize() {
Il2Cpp3.exports.classInitialize(this);
return this;
}
/** Determines whether an instance of `other` class can be assigned to a variable of the current type. */
isAssignableFrom(other) {
return !!Il2Cpp3.exports.classIsAssignableFrom(this, other);
}
/** Determines whether the current class derives from `other` class. */
isSubclassOf(other, checkInterfaces) {
return !!Il2Cpp3.exports.classIsSubclassOf(this, other, +checkInterfaces);
}
/** Gets the method identified by the given name and parameter count. */
method(name, parameterCount = -1) {
return this.tryMethod(name, parameterCount) ?? raise(`couldn't find method ${name} in class ${this.type.name}`);
}
/** Gets the nested class with the given name. */
nested(name) {
return this.tryNested(name) ?? raise(`couldn't find nested class ${name} in class ${this.type.name}`);
}
/** Allocates a new object of the current class and calls its default constructor. */
new() {
const object = this.alloc();
const exceptionArray = Memory.alloc(Process.pointerSize);
Il2Cpp3.exports.objectInitialize(object, exceptionArray);
const exception = exceptionArray.readPointer();
if (!exception.isNull()) {
raise(new Il2Cpp3.Object(exception).toString());
}
return object;
}
/** Gets the field with the given name. */
tryField(name) {
return new Il2Cpp3.Field(Il2Cpp3.exports.classGetFieldFromName(this, Memory.allocUtf8String(name))).asNullable();
}
/** Gets the method with the given name and parameter count. */
tryMethod(name, parameterCount = -1) {
return new Il2Cpp3.Method(Il2Cpp3.exports.classGetMethodFromName(this, Memory.allocUtf8String(name), parameterCount)).asNullable();
}
/** Gets the nested class with the given name. */
tryNested(name) {
return this.nestedClasses.find((_) => _.name == name);
}
/** */
toString() {
const inherited = [this.parent].concat(this.interfaces);
return `// ${this.assemblyName}
${this.isEnum ? `enum` : this.isStruct ? `struct` : this.isInterface ? `interface` : `class`} ${this.type.name}${inherited ? ` : ${inherited.map((_) => _?.type.name).join(`, `)}` : ``}
{
${this.fields.join(`
`)}
${this.methods.join(`
`)}
}`;
}
/** Executes a callback for every defined class. */
static enumerate(block) {
const callback = new NativeCallback((_) => block(new Il2Cpp3.Class(_)), "void", ["pointer", "pointer"]);
return Il2Cpp3.exports.classForEach(callback, NULL);
}
};
__decorate([
lazy
], Class.prototype, "arrayClass", null);
__decorate([
lazy
], Class.prototype, "arrayElementSize", null);
__decorate([
lazy
], Class.prototype, "assemblyName", null);
__decorate([
lazy
], Class.prototype, "declaringClass", null);
__decorate([
lazy
], Class.prototype, "baseType", null);
__decorate([
lazy
], Class.prototype, "elementClass", null);
__decorate([
lazy
], Class.prototype, "fields", null);
__decorate([
lazy
], Class.prototype, "flags", null);
__decorate([
lazy
], Class.prototype, "fullName", null);
__decorate([
lazy
], Class.prototype, "generics", null);
__decorate([
lazy
], Class.prototype, "hasReferences", null);
__decorate([
lazy
], Class.prototype, "hasStaticConstructor", null);
__decorate([
lazy
], Class.prototype, "image", null);
__decorate([
lazy
], Class.prototype, "instanceSize", null);
__decorate([
lazy
], Class.prototype, "isAbstract", null);
__decorate([
lazy
], Class.prototype, "isBlittable", null);
__decorate([
lazy
], Class.prototype, "isEnum", null);
__decorate([
lazy
], Class.prototype, "isGeneric", null);
__decorate([
lazy
], Class.prototype, "isInflated", null);
__decorate([
lazy
], Class.prototype, "isInterface", null);
__decorate([
lazy
], Class.prototype, "isValueType", null);
__decorate([
lazy
], Class.prototype, "interfaces", null);
__decorate([
lazy
], Class.prototype, "methods", null);
__decorate([
lazy
], Class.prototype, "name", null);
__decorate([
lazy
], Class.prototype, "namespace", null);
__decorate([
lazy
], Class.prototype, "nestedClasses", null);
__decorate([
lazy
], Class.prototype, "parent", null);
__decorate([
lazy
], Class.prototype, "pointerClass", null);
__decorate([
lazy
], Class.prototype, "rank", null);
__decorate([
lazy
], Class.prototype, "staticFieldsData", null);
__decorate([
lazy
], Class.prototype, "valueTypeSize", null);
__decorate([
lazy
], Class.prototype, "type", null);
Class = __decorate([
recycle
], Class);
Il2Cpp3.Class = Class;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
function delegate(klass, block) {
const SystemDelegate = Il2Cpp3.corlib.class("System.Delegate");
const SystemMulticastDelegate = Il2Cpp3.corlib.class("System.MulticastDelegate");
if (!SystemDelegate.isAssignableFrom(klass)) {
raise(`cannot create a delegate for ${klass.type.name} as it's a non-delegate class`);
}
if (klass.equals(SystemDelegate) || klass.equals(SystemMulticastDelegate)) {
raise(`cannot create a delegate for neither ${SystemDelegate.type.name} nor ${SystemMulticastDelegate.type.name}, use a subclass instead`);
}
const delegate2 = klass.alloc();
const key = delegate2.handle.toString();
const Invoke = delegate2.tryMethod("Invoke") ?? raise(`cannot create a delegate for ${klass.type.name}, there is no Invoke method`);
delegate2.method(".ctor").invoke(delegate2, Invoke.handle);
const callback = Invoke.wrap(block);
delegate2.field("method_ptr").value = callback;
delegate2.field("invoke_impl").value = callback;
Il2Cpp3._callbacksToKeepAlive[key] = callback;
return delegate2;
}
Il2Cpp3.delegate = delegate;
Il2Cpp3._callbacksToKeepAlive = {};
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
let Domain = class Domain extends NativeStruct {
/** Gets the assemblies that have been loaded into the execution context of the application domain. */
get assemblies() {
let handles = readNativeList((_) => Il2Cpp3.exports.domainGetAssemblies(this, _));
if (handles.length == 0) {
const assemblyObjects = this.object.method("GetAssemblies").overload().invoke();
handles = globalThis.Array.from(assemblyObjects).map((_) => _.field("_mono_assembly").value);
}
return handles.map((_) => new Il2Cpp3.Assembly(_));
}
/** Gets the encompassing object of the application domain. */
get object() {
return Il2Cpp3.corlib.class("System.AppDomain").method("get_CurrentDomain").invoke();
}
/** Opens and loads the assembly with the given name. */
assembly(name) {
return this.tryAssembly(name) ?? raise(`couldn't find assembly ${name}`);
}
/** Attached a new thread to the application domain. */
attach() {
return new Il2Cpp3.Thread(Il2Cpp3.exports.threadAttach(this));
}
/** Opens and loads the assembly with the given name. */
tryAssembly(name) {
return new Il2Cpp3.Assembly(Il2Cpp3.exports.domainGetAssemblyFromName(this, Memory.allocUtf8String(name))).asNullable();
}
};
__decorate([
lazy
], Domain.prototype, "assemblies", null);
__decorate([
lazy
], Domain.prototype, "object", null);
Domain = __decorate([
recycle
], Domain);
Il2Cpp3.Domain = Domain;
getter(Il2Cpp3, "domain", () => {
return new Il2Cpp3.Domain(Il2Cpp3.exports.domainGet());
}, lazy);
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Field extends NativeStruct {
/** Gets the class in which this field is defined. */
get class() {
return new Il2Cpp3.Class(Il2Cpp3.exports.fieldGetClass(this));
}
/** Gets the flags of the current field. */
get flags() {
return Il2Cpp3.exports.fieldGetFlags(this);
}
/** Determines whether this field value is known at compile time. */
get isLiteral() {
return (this.flags & 64) != 0;
}
/** Determines whether this field is static. */
get isStatic() {
return (this.flags & 16) != 0;
}
/** Determines whether this field is thread static. */
get isThreadStatic() {
const offset = Il2Cpp3.corlib.class("System.AppDomain").field("type_resolve_in_progress").offset;
getter(Il2Cpp3.Field.prototype, "isThreadStatic", function() {
return this.offset == offset;
}, lazy);
return this.isThreadStatic;
}
/** Gets the access modifier of this field. */
get modifier() {
switch (this.flags & 7) {
case 1:
return "private";
case 2:
return "private protected";
case 3:
return "internal";
case 4:
return "protected";
case 5:
return "protected internal";
case 6:
return "public";
}
}
/** Gets the name of this field. */
get name() {
return Il2Cpp3.exports.fieldGetName(this).readUtf8String();
}
/** Gets the offset of this field, calculated as the difference with its owner virtual address. */
get offset() {
return Il2Cpp3.exports.fieldGetOffset(this);
}
/** Gets the type of this field. */
get type() {
return new Il2Cpp3.Type(Il2Cpp3.exports.fieldGetType(this));
}
/** Gets the value of this field. */
get value() {
if (!this.isStatic) {
raise(`cannot access instance field ${this.class.type.name}::${this.name} from a class, use an object instead`);
}
const handle = Memory.alloc(Process.pointerSize);
Il2Cpp3.exports.fieldGetStaticValue(this.handle, handle);
return Il2Cpp3.read(handle, this.type);
}
/** Sets the value of this field. Thread static or literal values cannot be altered yet. */
set value(value) {
if (!this.isStatic) {
raise(`cannot access instance field ${this.class.type.name}::${this.name} from a class, use an object instead`);
}
if (this.isThreadStatic || this.isLiteral) {
raise(`cannot write the value of field ${this.name} as it's thread static or literal`);
}
const handle = (
// pointer-like values should be passed as-is, but boxed
// value types (primitives included) must be unboxed first
value instanceof Il2Cpp3.Object && this.type.class.isValueType ? value.unbox() : value instanceof NativeStruct ? value.handle : value instanceof NativePointer ? value : Il2Cpp3.write(Memory.alloc(this.type.class.valueTypeSize), value, this.type)
);
Il2Cpp3.exports.fieldSetStaticValue(this.handle, handle);
}
/** */
toString() {
return `${this.isThreadStatic ? `[ThreadStatic] ` : ``}${this.isStatic ? `static ` : ``}${this.type.name} ${this.name}${this.isLiteral ? ` = ${this.type.class.isEnum ? Il2Cpp3.read(this.value.handle, this.type.class.baseType) : this.value}` : ``};${this.isThreadStatic || this.isLiteral ? `` : ` // 0x${this.offset.toString(16)}`}`;
}
/**
* @internal
* Binds the current field to a {@link Il2Cpp.Object} or a
* {@link Il2Cpp.ValueType} (also known as *instances*), so that it is
* possible to retrieve its value - see {@link Il2Cpp.Field.value} for
* details. \
* Binding a static field is forbidden.
*/
bind(instance) {
if (this.isStatic) {
raise(`cannot bind static field ${this.class.type.name}::${this.name} to an instance`);
}
const offset = this.offset - (instance instanceof Il2Cpp3.ValueType ? Il2Cpp3.Object.headerSize : 0);
return new Proxy(this, {
get(target, property) {
if (property == "value") {
return Il2Cpp3.read(instance.handle.add(offset), target.type);
}
return Reflect.get(target, property);
},
set(target, property, value) {
if (property == "value") {
Il2Cpp3.write(instance.handle.add(offset), value, target.type);
return true;
}
return Reflect.set(target, property, value);
}
});
}
}
__decorate([
lazy
], Field.prototype, "class", null);
__decorate([
lazy
], Field.prototype, "flags", null);
__decorate([
lazy
], Field.prototype, "isLiteral", null);
__decorate([
lazy
], Field.prototype, "isStatic", null);
__decorate([
lazy
], Field.prototype, "isThreadStatic", null);
__decorate([
lazy
], Field.prototype, "modifier", null);
__decorate([
lazy
], Field.prototype, "name", null);
__decorate([
lazy
], Field.prototype, "offset", null);
__decorate([
lazy
], Field.prototype, "type", null);
Il2Cpp3.Field = Field;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class GCHandle {
handle;
/** @internal */
constructor(handle) {
this.handle = handle;
}
/** Gets the object associated to this handle. */
get target() {
return new Il2Cpp3.Object(Il2Cpp3.exports.gcHandleGetTarget(this.handle)).asNullable();
}
/** Frees this handle. */
free() {
return Il2Cpp3.exports.gcHandleFree(this.handle);
}
}
Il2Cpp3.GCHandle = GCHandle;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
let Image = class Image extends NativeStruct {
/** Gets the assembly in which the current image is defined. */
get assembly() {
return new Il2Cpp3.Assembly(Il2Cpp3.exports.imageGetAssembly(this));
}
/** Gets the amount of classes defined in this image. */
get classCount() {
if (Il2Cpp3.unityVersionIsBelow201830) {
return this.classes.length;
} else {
return Il2Cpp3.exports.imageGetClassCount(this);
}
}
/** Gets the classes defined in this image. */
get classes() {
if (Il2Cpp3.unityVersionIsBelow201830) {
const types = this.assembly.object.method("GetTypes").invoke(false);
const classes = globalThis.Array.from(types, (_) => new Il2Cpp3.Class(Il2Cpp3.exports.classFromObject(_)));
const Module = this.tryClass("<Module>");
if (Module) {
classes.unshift(Module);
}
return classes;
} else {
return globalThis.Array.from(globalThis.Array(this.classCount), (_, i) => new Il2Cpp3.Class(Il2Cpp3.exports.imageGetClass(this, i)));
}
}
/** Gets the name of this image. */
get name() {
return Il2Cpp3.exports.imageGetName(this).readUtf8String();
}
/** Gets the class with the specified name defined in this image. */
class(name) {
return this.tryClass(name) ?? raise(`couldn't find class ${name} in assembly ${this.name}`);
}
/** Gets the class with the specified name defined in this image. */
tryClass(name) {
const dotIndex = name.lastIndexOf(".");
const classNamespace = Memory.allocUtf8String(dotIndex == -1 ? "" : name.slice(0, dotIndex));
const className = Memory.allocUtf8String(name.slice(dotIndex + 1));
return new Il2Cpp3.Class(Il2Cpp3.exports.classFromName(this, classNamespace, className)).asNullable();
}
};
__decorate([
lazy
], Image.prototype, "assembly", null);
__decorate([
lazy
], Image.prototype, "classCount", null);
__decorate([
lazy
], Image.prototype, "classes", null);
__decorate([
lazy
], Image.prototype, "name", null);
Image = __decorate([
recycle
], Image);
Il2Cpp3.Image = Image;
getter(Il2Cpp3, "corlib", () => {
return new Il2Cpp3.Image(Il2Cpp3.exports.getCorlib());
}, lazy);
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class MemorySnapshot extends NativeStruct {
/** Captures a memory snapshot. */
static capture() {
return new Il2Cpp3.MemorySnapshot();
}
/** Creates a memory snapshot with the given handle. */
constructor(handle = Il2Cpp3.exports.memorySnapshotCapture()) {
super(handle);
}
/** Gets any initialized class. */
get classes() {
return readNativeIterator((_) => Il2Cpp3.exports.memorySnapshotGetClasses(this, _)).map((_) => new Il2Cpp3.Class(_));
}
/** Gets the objects tracked by this memory snapshot. */
get objects() {
return readNativeList((_) => Il2Cpp3.exports.memorySnapshotGetObjects(this, _)).filter((_) => !_.isNull()).map((_) => new Il2Cpp3.Object(_));
}
/** Frees this memory snapshot. */
free() {
Il2Cpp3.exports.memorySnapshotFree(this);
}
}
__decorate([
lazy
], MemorySnapshot.prototype, "classes", null);
__decorate([
lazy
], MemorySnapshot.prototype, "objects", null);
Il2Cpp3.MemorySnapshot = MemorySnapshot;
function memorySnapshot(block) {
const memorySnapshot2 = Il2Cpp3.MemorySnapshot.capture();
const result = block(memorySnapshot2);
memorySnapshot2.free();
return result;
}
Il2Cpp3.memorySnapshot = memorySnapshot;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Method extends NativeStruct {
/** Gets the class in which this method is defined. */
get class() {
return new Il2Cpp3.Class(Il2Cpp3.exports.methodGetClass(this));
}
/** Gets the flags of the current method. */
get flags() {
return Il2Cpp3.exports.methodGetFlags(this, NULL);
}
/** Gets the implementation flags of the current method. */
get implementationFlags() {
const implementationFlagsPointer = Memory.alloc(Process.pointerSize);
Il2Cpp3.exports.methodGetFlags(this, implementationFlagsPointer);
return implementationFlagsPointer.readU32();
}
/** */
get fridaSignature() {
const types = [];
for (const parameter of this.parameters) {
types.push(parameter.type.fridaAlias);
}
if (!this.isStatic || Il2Cpp3.unityVersionIsBelow201830) {
types.unshift("pointer");
}
if (this.isInflated) {
types.push("pointer");
}
return types;
}
/** Gets the generic parameters of this generic method. */
get generics() {
if (!this.isGeneric) {
return [];
}
const types = this.object.method("GetGenericArguments").invoke();
return globalThis.Array.from(types).map((_) => new Il2Cpp3.Class(Il2Cpp3.exports.classFromObject(_)));
}
/** Determines whether this method is external. */
get isExternal() {
return (this.implementationFlags & 4096) != 0;
}
/** Determines whether this method is generic. */
get isGeneric() {
return !!Il2Cpp3.exports.methodIsGeneric(this);
}
/** Determines whether this method is inflated (generic with a concrete type parameter). */
get isInflated() {
return !!Il2Cpp3.exports.methodIsInflated(this);
}
/** Determines whether this method is static. */
get isStatic() {
return !Il2Cpp3.exports.methodIsInstance(this);
}
/** Determines whether this method is synchronized. */
get isSynchronized() {
return (this.implementationFlags & 32) != 0;
}
/** Gets the access modifier of this method. */
get modifier() {
switch (this.flags & 7) {
case 1:
return "private";
case 2:
return "private protected";
case 3:
return "internal";
case 4:
return "protected";
case 5:
return "protected internal";
case 6:
return "public";
}
}
/** Gets the name of this method. */
get name() {
return Il2Cpp3.exports.methodGetName(this).readUtf8String();
}
/** @internal */
get nativeFunction() {
return new NativeFunction(this.virtualAddress, this.returnType.fridaAlias, this.fridaSignature);
}
/** Gets the encompassing object of the current method. */
get object() {
return new Il2Cpp3.Object(Il2Cpp3.exports.methodGetObject(this, NULL));
}
/** Gets the amount of parameters of this method. */
get parameterCount() {
return Il2Cpp3.exports.methodGetParameterCount(this);
}
/** Gets the parameters of this method. */
get parameters() {
return globalThis.Array.from(globalThis.Array(this.parameterCount), (_, i) => {
const parameterName = Il2Cpp3.exports.methodGetParameterName(this, i).readUtf8String();
const parameterType = Il2Cpp3.exports.methodGetParameterType(this, i);
return new Il2Cpp3.Parameter(parameterName, i, new Il2Cpp3.Type(parameterType));
});
}
/** Gets the relative virtual address (RVA) of this method. */
get relativeVirtualAddress() {
return this.virtualAddress.sub(Il2Cpp3.module.base);
}
/** Gets the return type of this method. */
get returnType() {
return new Il2Cpp3.Type(Il2Cpp3.exports.methodGetReturnType(this));
}
/** Gets the virtual address (VA) of this method. */
get virtualAddress() {
const FilterTypeName = Il2Cpp3.corlib.class("System.Reflection.Module").initialize().field("FilterTypeName").value;
const FilterTypeNameMethodPointer = FilterTypeName.field("method_ptr").value;
const FilterTypeNameMethod = FilterTypeName.field("method").value;
const offset = FilterTypeNameMethod.offsetOf((_) => _.readPointer().equals(FilterTypeNameMethodPointer)) ?? raise("couldn't find the virtual address offset in the native method struct");
getter(Il2Cpp3.Method.prototype, "virtualAddress", function() {
return this.handle.add(offset).readPointer();
}, lazy);
Il2Cpp3.corlib.class("System.Reflection.Module").method(".cctor").invoke();
return this.virtualAddress;
}
/** Replaces the body of this method. */
set implementation(block) {
try {
Interceptor.replace(this.virtualAddress, this.wrap(block));
} catch (e) {
switch (e.message) {
case "access violation accessing 0x0":
raise(`couldn't set implementation for method ${this.name} as it has a NULL virtual address`);
case /unable to intercept function at \w+; please file a bug/.exec(e.message)?.input:
warn(`couldn't set implementation for method ${this.name} as it may be a thunk`);
break;
case "already replaced this function":
warn(`couldn't set implementation for method ${this.name} as it has already been replaced by a thunk`);
break;
default:
throw e;
}
}
}
/** Creates a generic instance of the current generic method. */
inflate(...classes) {
if (!this.isGeneric || this.generics.length != classes.length) {
for (const method of this.overloads()) {
if (method.isGeneric && method.generics.length == classes.length) {
return method.inflate(...classes);
}
}
raise(`could not find inflatable signature of method ${this.name} with ${classes.length} generic parameter(s)`);
}
const types = classes.map((_) => _.type.object);
const typeArray = Il2Cpp3.array(Il2Cpp3.corlib.class("System.Type"), types);
const inflatedMethodObject = this.object.method("MakeGenericMethod", 1).invoke(typeArray);
return new Il2Cpp3.Method(inflatedMethodObject.field("mhandle").value);
}
/** Invokes this method. */
invoke(...parameters) {
if (!this.isStatic) {
raise(`cannot invoke non-static method ${this.name} as it must be invoked throught a Il2Cpp.Object, not a Il2Cpp.Class`);
}
return this.invokeRaw(NULL, ...parameters);
}
/** @internal */
invokeRaw(instance, ...parameters) {
const allocatedParameters = parameters.map(Il2Cpp3.toFridaValue);
if (!this.isStatic || Il2Cpp3.unityVersionIsBelow201830) {
allocatedParameters.unshift(instance);
}
if (this.isInflated) {
allocatedParameters.push(this.handle);
}
try {
const returnValue = this.nativeFunction(...allocatedParameters);
return Il2Cpp3.fromFridaValue(returnValue, this.returnType);
} catch (e) {
if (e == null) {
raise("an unexpected native invocation exception occurred, this is due to parameter types mismatch");
}
switch (e.message) {
case "bad argument count":
raise(`couldn't invoke method ${this.name} as it needs ${this.parameterCount} parameter(s), not ${parameters.length}`);
case "expected a pointer":
case "expected number":
case "expected array with fields":
raise(`couldn't invoke method ${this.name} using incorrect parameter types`);
}
throw e;
}
}
/** Gets the overloaded method with the given parameter types. */
overload(...typeNamesOrClasses) {
const method = this.tryOverload(...typeNamesOrClasses);
return method ?? raise(`couldn't find overloaded method ${this.name}(${typeNamesOrClasses.map((_) => _ instanceof Il2Cpp3.Class ? _.type.name : _)})`);
}
/** @internal */
*overloads() {
for (const klass of this.class.hierarchy()) {
for (const method of klass.methods) {
if (this.name == method.name) {
yield method;
}
}
}
}
/** Gets the parameter with the given name. */
parameter(name) {
return this.tryParameter(name) ?? raise(`couldn't find parameter ${name} in method ${this.name}`);
}
/** Restore the original method implementation. */
revert() {
Interceptor.revert(this.virtualAddress);
Interceptor.flush();
}
/** Gets the overloaded method with the given parameter types. */
tryOverload(...typeNamesOrClasses) {
const minScore = typeNamesOrClasses.length * 1;
const maxScore = typeNamesOrClasses.length * 2;
let candidate = void 0;
loop: for (const method of this.overloads()) {
if (method.parameterCount != typeNamesOrClasses.length)
continue;
let score = 0;
let i = 0;
for (const parameter of method.parameters) {
const desiredTypeNameOrClass = typeNamesOrClasses[i];
if (desiredTypeNameOrClass instanceof Il2Cpp3.Class) {
if (parameter.type.is(desiredTypeNameOrClass.type)) {
score += 2;
} else if (parameter.type.class.isAssignableFrom(desiredTypeNameOrClass)) {
score += 1;
} else {
continue loop;
}
} else if (parameter.type.name == desiredTypeNameOrClass) {
score += 2;
} else {
continue loop;
}
i++;
}
if (score < minScore) {
continue;
} else if (score == maxScore) {
return method;
} else if (candidate == void 0 || score > candidate[0]) {
candidate = [score, method];
} else if (score == candidate[0]) {
let i2 = 0;
for (const parameter of candidate[1].parameters) {
if (parameter.type.class.isAssignableFrom(method.parameters[i2].type.class)) {
candidate = [score, method];
continue loop;
}
i2++;
}
}
}
return candidate?.[1];
}
/** Gets the parameter with the given name. */
tryParameter(name) {
return this.parameters.find((_) => _.name == name);
}
/** */
toString() {
return `${this.isStatic ? `static ` : ``}${this.returnType.name} ${this.name}${this.generics.length > 0 ? `<${this.generics.map((_) => _.type.name).join(",")}>` : ""}(${this.parameters.join(`, `)});${this.virtualAddress.isNull() ? `` : ` // 0x${this.relativeVirtualAddress.toString(16).padStart(8, `0`)}`}`;
}
/**
* @internal
* Binds the current method to a {@link Il2Cpp.Object} or a
* {@link Il2Cpp.ValueType} (also known as *instances*), so that it is
* possible to invoke it - see {@link Il2Cpp.Method.invoke} for
* details. \
* Binding a static method is forbidden.
*/
bind(instance) {
if (this.isStatic) {
raise(`cannot bind static method ${this.class.type.name}::${this.name} to an instance`);
}
return new Proxy(this, {
get(target, property, receiver) {
switch (property) {
case "invoke":
const handle = instance instanceof Il2Cpp3.ValueType ? target.class.isValueType ? instance.handle.sub(structMethodsRequireObjectInstances() ? Il2Cpp3.Object.headerSize : 0) : raise(`cannot invoke method ${target.class.type.name}::${target.name} against a value type, you must box it first`) : target.class.isValueType ? instance.handle.add(structMethodsRequireObjectInstances() ? 0 : Il2Cpp3.Object.headerSize) : instance.handle;
return target.invokeRaw.bind(target, handle);
case "overloads":
return function* () {
for (const method of target[property]()) {
if (!method.isStatic) {
yield method;
}
}
};
case "inflate":
case "overload":
case "tryOverload":
const member = Reflect.get(target, property).bind(receiver);
return function(...args) {
return member(...args)?.bind(instance);
};
}
return Reflect.get(target, property);
}
});
}
/** @internal */
wrap(block) {
const startIndex = +!this.isStatic | +Il2Cpp3.unityVersionIsBelow201830;
return new NativeCallback((...args) => {
const thisObject = this.isStatic ? this.class : this.class.isValueType ? new Il2Cpp3.ValueType(args[0].add(structMethodsRequireObjectInstances() ? Il2Cpp3.Object.headerSize : 0), this.class.type) : new Il2Cpp3.Object(args[0]);
const parameters = this.parameters.map((_, i) => Il2Cpp3.fromFridaValue(args[i + startIndex], _.type));
const result = block.call(thisObject, ...parameters);
return Il2Cpp3.toFridaValue(result);
}, this.returnType.fridaAlias, this.fridaSignature);
}
}
__decorate([
lazy
], Method.prototype, "class", null);
__decorate([
lazy
], Method.prototype, "flags", null);
__decorate([
lazy
], Method.prototype, "implementationFlags", null);
__decorate([
lazy
], Method.prototype, "fridaSignature", null);
__decorate([
lazy
], Method.prototype, "generics", null);
__decorate([
lazy
], Method.prototype, "isExternal", null);
__decorate([
lazy
], Method.prototype, "isGeneric", null);
__decorate([
lazy
], Method.prototype, "isInflated", null);
__decorate([
lazy
], Method.prototype, "isStatic", null);
__decorate([
lazy
], Method.prototype, "isSynchronized", null);
__decorate([
lazy
], Method.prototype, "modifier", null);
__decorate([
lazy
], Method.prototype, "name", null);
__decorate([
lazy
], Method.prototype, "nativeFunction", null);
__decorate([
lazy
], Method.prototype, "object", null);
__decorate([
lazy
], Method.prototype, "parameterCount", null);
__decorate([
lazy
], Method.prototype, "parameters", null);
__decorate([
lazy
], Method.prototype, "relativeVirtualAddress", null);
__decorate([
lazy
], Method.prototype, "returnType", null);
Il2Cpp3.Method = Method;
let structMethodsRequireObjectInstances = () => {
const object = Il2Cpp3.corlib.class("System.Int64").alloc();
object.field("m_value").value = 3735928559;
const result = object.method("Equals", 1).overload(object.class).invokeRaw(object, 3735928559);
return (structMethodsRequireObjectInstances = () => result)();
};
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Object2 extends NativeStruct {
/** Gets the Il2CppObject struct size, possibly equal to `Process.pointerSize * 2`. */
static get headerSize() {
return Il2Cpp3.corlib.class("System.Object").instanceSize;
}
/**
* Returns the same object, but having its parent class as class.
* It basically is the C# `base` keyword, so that parent members can be
* accessed.
*
* **Example** \
* Consider the following classes:
* ```csharp
* class Foo
* {
* int foo()
* {
* return 1;
* }
* }
* class Bar : Foo
* {
* new int foo()
* {
* return 2;
* }
* }
* ```
* then:
* ```ts
* const Bar: Il2Cpp.Class = ...;
* const bar = Bar.new();
*
* console.log(bar.foo()); // 2
* console.log(bar.base.foo()); // 1
* ```
*/
get base() {
if (this.class.parent == null) {
raise(`class ${this.class.type.name} has no parent`);
}
return new Proxy(this, {
get(target, property, receiver) {
if (property == "class") {
return Reflect.get(target, property).parent;
} else if (property == "base") {
return Reflect.getOwnPropertyDescriptor(Il2Cpp3.Object.prototype, property).get.bind(receiver)();
}
return Reflect.get(target, property);
}
});
}
/** Gets the class of this object. */
get class() {
return new Il2Cpp3.Class(Il2Cpp3.exports.objectGetClass(this));
}
/** Returns a monitor for this object. */
get monitor() {
return new Il2Cpp3.Object.Monitor(this);
}
/** Gets the size of the current object. */
get size() {
return Il2Cpp3.exports.objectGetSize(this);
}
/** Gets the non-static field with the given name of the current class hierarchy. */
field(name) {
return this.tryField(name) ?? raise(`couldn't find non-static field ${name} in hierarchy of class ${this.class.type.name}`);
}
/** Gets the non-static method with the given name (and optionally parameter count) of the current class hierarchy. */
method(name, parameterCount = -1) {
return this.tryMethod(name, parameterCount) ?? raise(`couldn't find non-static method ${name} in hierarchy of class ${this.class.type.name}`);
}
/** Creates a reference to this object. */
ref(pin) {
return new Il2Cpp3.GCHandle(Il2Cpp3.exports.gcHandleNew(this, +pin));
}
/** Gets the correct virtual method from the given virtual method. */
virtualMethod(method) {
return new Il2Cpp3.Method(Il2Cpp3.exports.objectGetVirtualMethod(this, method)).bind(this);
}
/** Gets the non-static field with the given name of the current class hierarchy, if it exists. */
tryField(name) {
const field = this.class.tryField(name);
if (field?.isStatic) {
for (const klass of this.class.hierarchy({ includeCurrent: false })) {
for (const field2 of klass.fields) {
if (field2.name == name && !field2.isStatic) {
return field2.bind(this);
}
}
}
return void 0;
}
return field?.bind(this);
}
/** Gets the non-static method with the given name (and optionally parameter count) of the current class hierarchy, if it exists. */
tryMethod(name, parameterCount = -1) {
const method = this.class.tryMethod(name, parameterCount);
if (method?.isStatic) {
for (const klass of this.class.hierarchy()) {
for (const method2 of klass.methods) {
if (method2.name == name && !method2.isStatic && (parameterCount < 0 || method2.parameterCount == parameterCount)) {
return method2.bind(this);
}
}
}
return void 0;
}
return method?.bind(this);
}
/** */
toString() {
return this.isNull() ? "null" : this.method("ToString", 0).invoke().content ?? "null";
}
/** Unboxes the value type (either a primitive, a struct or an enum) out of this object. */
unbox() {
return this.class.isValueType ? new Il2Cpp3.ValueType(Il2Cpp3.exports.objectUnbox(this), this.class.type) : raise(`couldn't unbox instances of ${this.class.type.name} as they are not value types`);
}
/** Creates a weak reference to this object. */
weakRef(trackResurrection) {
return new Il2Cpp3.GCHandle(Il2Cpp3.exports.gcHandleNewWeakRef(this, +trackResurrection));
}
}
__decorate([
lazy
], Object2.prototype, "class", null);
__decorate([
lazy
], Object2.prototype, "size", null);
__decorate([
lazy
], Object2, "headerSize", null);
Il2Cpp3.Object = Object2;
(function(Object3) {
class Monitor {
handle;
/** @internal */
constructor(handle) {
this.handle = handle;
}
/** Acquires an exclusive lock on the current object. */
enter() {
return Il2Cpp3.exports.monitorEnter(this.handle);
}
/** Release an exclusive lock on the current object. */
exit() {
return Il2Cpp3.exports.monitorExit(this.handle);
}
/** Notifies a thread in the waiting queue of a change in the locked object's state. */
pulse() {
return Il2Cpp3.exports.monitorPulse(this.handle);
}
/** Notifies all waiting threads of a change in the object's state. */
pulseAll() {
return Il2Cpp3.exports.monitorPulseAll(this.handle);
}
/** Attempts to acquire an exclusive lock on the current object. */
tryEnter(timeout) {
return !!Il2Cpp3.exports.monitorTryEnter(this.handle, timeout);
}
/** Releases the lock on an object and attempts to block the current thread until it reacquires the lock. */
tryWait(timeout) {
return !!Il2Cpp3.exports.monitorTryWait(this.handle, timeout);
}
/** Releases the lock on an object and blocks the current thread until it reacquires the lock. */
wait() {
return Il2Cpp3.exports.monitorWait(this.handle);
}
}
Object3.Monitor = Monitor;
})(Object2 = Il2Cpp3.Object || (Il2Cpp3.Object = {}));
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Parameter {
/** Name of this parameter. */
name;
/** Position of this parameter. */
position;
/** Type of this parameter. */
type;
constructor(name, position, type) {
this.name = name;
this.position = position;
this.type = type;
}
/** */
toString() {
return `${this.type.name} ${this.name}`;
}
}
Il2Cpp3.Parameter = Parameter;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Pointer extends NativeStruct {
type;
constructor(handle, type) {
super(handle);
this.type = type;
}
/** Gets the element at the given index. */
get(index) {
return Il2Cpp3.read(this.handle.add(index * this.type.class.arrayElementSize), this.type);
}
/** Reads the given amount of elements starting at the given offset. */
read(length, offset = 0) {
const values = new globalThis.Array(length);
for (let i = 0; i < length; i++) {
values[i] = this.get(i + offset);
}
return values;
}
/** Sets the given element at the given index */
set(index, value) {
Il2Cpp3.write(this.handle.add(index * this.type.class.arrayElementSize), value, this.type);
}
/** */
toString() {
return this.handle.toString();
}
/** Writes the given elements starting at the given index. */
write(values, offset = 0) {
for (let i = 0; i < values.length; i++) {
this.set(i + offset, values[i]);
}
}
}
Il2Cpp3.Pointer = Pointer;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Reference extends NativeStruct {
type;
constructor(handle, type) {
super(handle);
this.type = type;
}
/** Gets the element referenced by the current reference. */
get value() {
return Il2Cpp3.read(this.handle, this.type);
}
/** Sets the element referenced by the current reference. */
set value(value) {
Il2Cpp3.write(this.handle, value, this.type);
}
/** */
toString() {
return this.isNull() ? "null" : `->${this.value}`;
}
}
Il2Cpp3.Reference = Reference;
function reference(value, type) {
const handle = Memory.alloc(Process.pointerSize);
switch (typeof value) {
case "boolean":
return new Il2Cpp3.Reference(handle.writeS8(+value), Il2Cpp3.corlib.class("System.Boolean").type);
case "number":
switch (type?.enumValue) {
case Il2Cpp3.Type.Enum.UBYTE:
return new Il2Cpp3.Reference(handle.writeU8(value), type);
case Il2Cpp3.Type.Enum.BYTE:
return new Il2Cpp3.Reference(handle.writeS8(value), type);
case Il2Cpp3.Type.Enum.CHAR:
case Il2Cpp3.Type.Enum.USHORT:
return new Il2Cpp3.Reference(handle.writeU16(value), type);
case Il2Cpp3.Type.Enum.SHORT:
return new Il2Cpp3.Reference(handle.writeS16(value), type);
case Il2Cpp3.Type.Enum.UINT:
return new Il2Cpp3.Reference(handle.writeU32(value), type);
case Il2Cpp3.Type.Enum.INT:
return new Il2Cpp3.Reference(handle.writeS32(value), type);
case Il2Cpp3.Type.Enum.ULONG:
return new Il2Cpp3.Reference(handle.writeU64(value), type);
case Il2Cpp3.Type.Enum.LONG:
return new Il2Cpp3.Reference(handle.writeS64(value), type);
case Il2Cpp3.Type.Enum.FLOAT:
return new Il2Cpp3.Reference(handle.writeFloat(value), type);
case Il2Cpp3.Type.Enum.DOUBLE:
return new Il2Cpp3.Reference(handle.writeDouble(value), type);
}
case "object":
if (value instanceof Il2Cpp3.ValueType || value instanceof Il2Cpp3.Pointer) {
return new Il2Cpp3.Reference(value.handle, value.type);
} else if (value instanceof Il2Cpp3.Object) {
return new Il2Cpp3.Reference(handle.writePointer(value), value.class.type);
} else if (value instanceof Il2Cpp3.String || value instanceof Il2Cpp3.Array) {
return new Il2Cpp3.Reference(handle.writePointer(value), value.object.class.type);
} else if (value instanceof NativePointer) {
switch (type?.enumValue) {
case Il2Cpp3.Type.Enum.NUINT:
case Il2Cpp3.Type.Enum.NINT:
return new Il2Cpp3.Reference(handle.writePointer(value), type);
}
} else if (value instanceof Int64) {
return new Il2Cpp3.Reference(handle.writeS64(value), Il2Cpp3.corlib.class("System.Int64").type);
} else if (value instanceof UInt64) {
return new Il2Cpp3.Reference(handle.writeU64(value), Il2Cpp3.corlib.class("System.UInt64").type);
}
default:
raise(`couldn't create a reference to ${value} using an unhandled type ${type?.name}`);
}
}
Il2Cpp3.reference = reference;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class String2 extends NativeStruct {
/** Gets the content of this string. */
get content() {
return Il2Cpp3.exports.stringGetChars(this).readUtf16String(this.length);
}
/** @unsafe Sets the content of this string - it may write out of bounds! */
set content(value) {
const offset = Il2Cpp3.string("vfsfitvnm").handle.offsetOf((_) => _.readInt() == 9) ?? raise("couldn't find the length offset in the native string struct");
globalThis.Object.defineProperty(Il2Cpp3.String.prototype, "content", {
set(value2) {
Il2Cpp3.exports.stringGetChars(this).writeUtf16String(value2 ?? "");
this.handle.add(offset).writeS32(value2?.length ?? 0);
}
});
this.content = value;
}
/** Gets the length of this string. */
get length() {
return Il2Cpp3.exports.stringGetLength(this);
}
/** Gets the encompassing object of the current string. */
get object() {
return new Il2Cpp3.Object(this);
}
/** */
toString() {
return this.isNull() ? "null" : `"${this.content}"`;
}
}
Il2Cpp3.String = String2;
function string(content) {
return new Il2Cpp3.String(Il2Cpp3.exports.stringNew(Memory.allocUtf8String(content ?? "")));
}
Il2Cpp3.string = string;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class Thread extends NativeStruct {
/** Gets the native id of the current thread. */
get id() {
let get = function() {
return this.internal.field("thread_id").value.toNumber();
};
if (Process.platform != "windows") {
const currentThreadId = Process.getCurrentThreadId();
const currentPosixThread = ptr(get.apply(Il2Cpp3.currentThread));
const offset = currentPosixThread.offsetOf((_) => _.readS32() == currentThreadId, 1024) ?? raise(`couldn't find the offset for determining the kernel id of a posix thread`);
const _get = get;
get = function() {
return ptr(_get.apply(this)).add(offset).readS32();
};
}
getter(Il2Cpp3.Thread.prototype, "id", get, lazy);
return this.id;
}
/** Gets the encompassing internal object (System.Threding.InternalThreead) of the current thread. */
get internal() {
return this.object.tryField("internal_thread")?.value ?? this.object;
}
/** Determines whether the current thread is the garbage collector finalizer one. */
get isFinalizer() {
return !Il2Cpp3.exports.threadIsVm(this);
}
/** Gets the managed id of the current thread. */
get managedId() {
return this.object.method("get_ManagedThreadId").invoke();
}
/** Gets the encompassing object of the current thread. */
get object() {
return new Il2Cpp3.Object(this);
}
/** @internal */
get staticData() {
return this.internal.field("static_data").value;
}
/** @internal */
get synchronizationContext() {
const get_ExecutionContext = this.object.tryMethod("GetMutableExecutionContext") ?? this.object.method("get_ExecutionContext");
const executionContext = get_ExecutionContext.invoke();
const synchronizationContext = executionContext.tryField("_syncContext")?.value ?? executionContext.tryMethod("get_SynchronizationContext")?.invoke() ?? this.tryLocalValue(Il2Cpp3.corlib.class("System.Threading.SynchronizationContext"));
return synchronizationContext?.asNullable() ?? null;
}
/** Detaches the thread from the application domain. */
detach() {
return Il2Cpp3.exports.threadDetach(this);
}
/** Schedules a callback on the current thread. */
schedule(block) {
const Post = this.synchronizationContext?.tryMethod("Post");
if (Post == null) {
return Process.runOnThread(this.id, block);
}
return new Promise((resolve) => {
const delegate = Il2Cpp3.delegate(Il2Cpp3.corlib.class("System.Threading.SendOrPostCallback"), () => {
const result = block();
setImmediate(() => resolve(result));
});
Script.bindWeak(globalThis, () => {
delegate.field("method_ptr").value = delegate.field("invoke_impl").value = Il2Cpp3.exports.domainGet;
});
Post.invoke(delegate, NULL);
});
}
/** @internal */
tryLocalValue(klass) {
for (let i = 0; i < 16; i++) {
const base = this.staticData.add(i * Process.pointerSize).readPointer();
if (!base.isNull()) {
const object = new Il2Cpp3.Object(base.readPointer()).asNullable();
if (object?.class?.isSubclassOf(klass, false)) {
return object;
}
}
}
}
}
__decorate([
lazy
], Thread.prototype, "internal", null);
__decorate([
lazy
], Thread.prototype, "isFinalizer", null);
__decorate([
lazy
], Thread.prototype, "managedId", null);
__decorate([
lazy
], Thread.prototype, "object", null);
__decorate([
lazy
], Thread.prototype, "staticData", null);
__decorate([
lazy
], Thread.prototype, "synchronizationContext", null);
Il2Cpp3.Thread = Thread;
getter(Il2Cpp3, "attachedThreads", () => {
if (Il2Cpp3.exports.threadGetAttachedThreads.isNull()) {
const currentThreadHandle = Il2Cpp3.currentThread?.handle ?? raise("Current thread is not attached to IL2CPP");
const pattern = currentThreadHandle.toMatchPattern();
const threads = [];
for (const range of Process.enumerateRanges("rw-")) {
if (range.file == void 0) {
const matches = Memory.scanSync(range.base, range.size, pattern);
if (matches.length == 1) {
while (true) {
const handle = matches[0].address.sub(matches[0].size * threads.length).readPointer();
if (handle.isNull() || !handle.readPointer().equals(currentThreadHandle.readPointer())) {
break;
}
threads.unshift(new Il2Cpp3.Thread(handle));
}
break;
}
}
}
return threads;
}
return readNativeList(Il2Cpp3.exports.threadGetAttachedThreads).map((_) => new Il2Cpp3.Thread(_));
});
getter(Il2Cpp3, "currentThread", () => {
return new Il2Cpp3.Thread(Il2Cpp3.exports.threadGetCurrent()).asNullable();
});
getter(Il2Cpp3, "mainThread", () => {
return Il2Cpp3.attachedThreads[0];
});
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
let Type = class Type extends NativeStruct {
/** */
static get Enum() {
const _ = (_2, block = (_3) => _3) => block(Il2Cpp3.corlib.class(_2)).type.enumValue;
const initial = {
VOID: _("System.Void"),
BOOLEAN: _("System.Boolean"),
CHAR: _("System.Char"),
BYTE: _("System.SByte"),
UBYTE: _("System.Byte"),
SHORT: _("System.Int16"),
USHORT: _("System.UInt16"),
INT: _("System.Int32"),
UINT: _("System.UInt32"),
LONG: _("System.Int64"),
ULONG: _("System.UInt64"),
NINT: _("System.IntPtr"),
NUINT: _("System.UIntPtr"),
FLOAT: _("System.Single"),
DOUBLE: _("System.Double"),
POINTER: _("System.IntPtr", (_2) => _2.field("m_value")),
VALUE_TYPE: _("System.Decimal"),
OBJECT: _("System.Object"),
STRING: _("System.String"),
CLASS: _("System.Array"),
ARRAY: _("System.Void", (_2) => _2.arrayClass),
NARRAY: _("System.Void", (_2) => new Il2Cpp3.Class(Il2Cpp3.exports.classGetArrayClass(_2, 2))),
GENERIC_INSTANCE: _("System.Int32", (_2) => _2.interfaces.find((_3) => _3.name.endsWith("`1")))
};
Reflect.defineProperty(this, "Enum", { value: initial });
return addFlippedEntries({
...initial,
VAR: _("System.Action`1", (_2) => _2.generics[0]),
MVAR: _("System.Array", (_2) => _2.method("AsReadOnly", 1).generics[0])
});
}
/** Gets the class of this type. */
get class() {
return new Il2Cpp3.Class(Il2Cpp3.exports.typeGetClass(this));
}
/** */
get fridaAlias() {
function getValueTypeFields(type) {
const instanceFields = type.class.fields.filter((_) => !_.isStatic);
return instanceFields.length == 0 ? ["char"] : instanceFields.map((_) => _.type.fridaAlias);
}
if (this.isByReference) {
return "pointer";
}
switch (this.enumValue) {
case Il2Cpp3.Type.Enum.VOID:
return "void";
case Il2Cpp3.Type.Enum.BOOLEAN:
return "bool";
case Il2Cpp3.Type.Enum.CHAR:
return "uchar";
case Il2Cpp3.Type.Enum.BYTE:
return "int8";
case Il2Cpp3.Type.Enum.UBYTE:
return "uint8";
case Il2Cpp3.Type.Enum.SHORT:
return "int16";
case Il2Cpp3.Type.Enum.USHORT:
return "uint16";
case Il2Cpp3.Type.Enum.INT:
return "int32";
case Il2Cpp3.Type.Enum.UINT:
return "uint32";
case Il2Cpp3.Type.Enum.LONG:
return "int64";
case Il2Cpp3.Type.Enum.ULONG:
return "uint64";
case Il2Cpp3.Type.Enum.FLOAT:
return "float";
case Il2Cpp3.Type.Enum.DOUBLE:
return "double";
case Il2Cpp3.Type.Enum.NINT:
case Il2Cpp3.Type.Enum.NUINT:
case Il2Cpp3.Type.Enum.POINTER:
case Il2Cpp3.Type.Enum.STRING:
case Il2Cpp3.Type.Enum.ARRAY:
case Il2Cpp3.Type.Enum.NARRAY:
return "pointer";
case Il2Cpp3.Type.Enum.VALUE_TYPE:
return this.class.isEnum ? this.class.baseType.fridaAlias : getValueTypeFields(this);
case Il2Cpp3.Type.Enum.CLASS:
case Il2Cpp3.Type.Enum.OBJECT:
case Il2Cpp3.Type.Enum.GENERIC_INSTANCE:
return this.class.isStruct ? getValueTypeFields(this) : this.class.isEnum ? this.class.baseType.fridaAlias : "pointer";
default:
return "pointer";
}
}
/** Determines whether this type is passed by reference. */
get isByReference() {
return this.name.endsWith("&");
}
/** Determines whether this type is primitive. */
get isPrimitive() {
switch (this.enumValue) {
case Il2Cpp3.Type.Enum.BOOLEAN:
case Il2Cpp3.Type.Enum.CHAR:
case Il2Cpp3.Type.Enum.BYTE:
case Il2Cpp3.Type.Enum.UBYTE:
case Il2Cpp3.Type.Enum.SHORT:
case Il2Cpp3.Type.Enum.USHORT:
case Il2Cpp3.Type.Enum.INT:
case Il2Cpp3.Type.Enum.UINT:
case Il2Cpp3.Type.Enum.LONG:
case Il2Cpp3.Type.Enum.ULONG:
case Il2Cpp3.Type.Enum.FLOAT:
case Il2Cpp3.Type.Enum.DOUBLE:
case Il2Cpp3.Type.Enum.NINT:
case Il2Cpp3.Type.Enum.NUINT:
return true;
default:
return false;
}
}
/** Gets the name of this type. */
get name() {
const handle = Il2Cpp3.exports.typeGetName(this);
try {
return handle.readUtf8String();
} finally {
Il2Cpp3.free(handle);
}
}
/** Gets the encompassing object of the current type. */
get object() {
return new Il2Cpp3.Object(Il2Cpp3.exports.typeGetObject(this));
}
/** Gets the {@link Il2Cpp.Type.Enum} value of the current type. */
get enumValue() {
return Il2Cpp3.exports.typeGetTypeEnum(this);
}
is(other) {
if (Il2Cpp3.exports.typeEquals.isNull()) {
return this.object.method("Equals").invoke(other.object);
}
return !!Il2Cpp3.exports.typeEquals(this, other);
}
/** */
toString() {
return this.name;
}
};
__decorate([
lazy
], Type.prototype, "class", null);
__decorate([
lazy
], Type.prototype, "fridaAlias", null);
__decorate([
lazy
], Type.prototype, "isByReference", null);
__decorate([
lazy
], Type.prototype, "isPrimitive", null);
__decorate([
lazy
], Type.prototype, "name", null);
__decorate([
lazy
], Type.prototype, "object", null);
__decorate([
lazy
], Type.prototype, "enumValue", null);
__decorate([
lazy
], Type, "Enum", null);
Type = __decorate([
recycle
], Type);
Il2Cpp3.Type = Type;
})(Il2Cpp2 || (Il2Cpp2 = {}));
var Il2Cpp2;
(function(Il2Cpp3) {
class ValueType extends NativeStruct {
type;
constructor(handle, type) {
super(handle);
this.type = type;
}
/** Boxes the current value type in a object. */
box() {
return new Il2Cpp3.Object(Il2Cpp3.exports.valueTypeBox(this.type.class, this));
}
/** Gets the non-static field with the given name of the current class hierarchy. */
field(name) {
return this.tryField(name) ?? raise(`couldn't find non-static field ${name} in hierarchy of class ${this.type.name}`);
}
/** Gets the non-static method with the given name (and optionally parameter count) of the current class hierarchy. */
method(name, parameterCount = -1) {
return this.tryMethod(name, parameterCount) ?? raise(`couldn't find non-static method ${name} in hierarchy of class ${this.type.name}`);
}
/** Gets the non-static field with the given name of the current class hierarchy, if it exists. */
tryField(name) {
const field = this.type.class.tryField(name);
if (field?.isStatic) {
for (const klass of this.type.class.hierarchy()) {
for (const field2 of klass.fields) {
if (field2.name == name && !field2.isStatic) {
return field2.bind(this);
}
}
}
return void 0;
}
return field?.bind(this);
}
/** Gets the non-static method with the given name (and optionally parameter count) of the current class hierarchy, if it exists. */
tryMethod(name, parameterCount = -1) {
const method = this.type.class.tryMethod(name, parameterCount);
if (method?.isStatic) {
for (const klass of this.type.class.hierarchy()) {
for (const method2 of klass.methods) {
if (method2.name == name && !method2.isStatic && (parameterCount < 0 || method2.parameterCount == parameterCount)) {
return method2.bind(this);
}
}
}
return void 0;
}
return method?.bind(this);
}
/** */
toString() {
const ToString = this.method("ToString", 0);
return this.isNull() ? "null" : (
// If ToString is defined within a value type class, we can
// avoid a boxing operation.
ToString.class.isValueType ? ToString.invoke().content ?? "null" : this.box().toString() ?? "null"
);
}
}
Il2Cpp3.ValueType = ValueType;
})(Il2Cpp2 || (Il2Cpp2 = {}));
globalThis.Il2Cpp = Il2Cpp2;
"use strict";
// ================== 打气球客户端补丁 BF-BALLOON(常驻版)==================
//
// 定位:与保龄球 bowl_fix.js 同源的第二份补丁(R98)。
//
// 【为什么需要它】—— 根因与保龄球球瓶完全同构(R97 定案)
// 客户端造 SceneItemHold 的**唯一**入口是
// LX6.Item.SceneItemBlob.TryGetSceneItemPlaceInfoDataByPlaceId(
// UInt64 uniqueId, out MA_Ref<SceneItemPlaceInfoData> data, out SceneItemBlobType& blobType)
// 它是对「关卡烘焙摆位表」的一次查表。官方关卡里**没有烘焙任何气球**
// (离线证据:碗窑/龙栖村摊位区的 vfc 块里没有任何 80005796..80005800 的 placeInfo),
// 服务端推来的 0xBA11_xxxx_xxxx id 查不到 → 返回 false → 不调 SyncAddSceneItemByLocalData
// → 不建 hold → 「面板开了、枪拿得起来、就是看不见气球」。
//
// 本补丁让这个函数对「服务端的气球 id」返回 true,并把一个构造好的
// SceneItemPlaceInfoData 交出去 ⇒ 客户端原生流程全通:
// SyncAddSceneItemByLocalData → SyncAddSceneItemByIdData → DoSceneItemHolderEnterWorld
//
// 【与保龄球版的差异】
// 1) id 段不同:气球 = 0xBA11_0000_0000 + 序号(1..12);保龄球 = 0xD3E03A...。
// **两段互不重叠,同一条 isOurs 判定可以同时覆盖两个玩法。**
// 2) 位姿不再自己算:改为读服务端 side-channel
// D:\Ananta\gym_tools\_balloon_push.json
// (由 GameRouter.Balloon.cs 在每次 push 前落盘,字段 = 我们真正发出去的 DestructibleInfo)。
// 理由:气球墙是「玩家前方 4×3」——位置依赖玩家当时的 yaw,客户端侧自己算容易错一帧;
// 保龄球能用固定常量(球道坐标写死)是因为球馆坐标本来就固定。
//
// 【SceneItemPlaceInfoData 实测内存布局】(instanceSize=112;frida 的 field.offset 比实际大 16)
// +0x00 BlobArray<Int32> linkPath {ptr,len} +0x10 UInt64 uniqueId
// +0x18 UInt32 sceneItemCfgId +0x1C Int32 pathId
// +0x20 Int32 groupId +0x24 f32 posX
// +0x28 f32 posY +0x2C f32 posZ
// +0x30 f32 eulerX +0x34 f32 eulerY
// +0x38 f32 eulerZ +0x3C Int32 iScale
// +0x40 Int32 navmeshId +0x44 BlobArray<SpecialParamData>
// +0x54 BlobArray<UInt64> linkSceneItemId
// ★ linkPath / navmeshId / linkSceneItemId 是 BlobArray —— Destructible 报文里根本没有这些字段,
// 所以只能「查一个真 placeId 拿合法模板 → 克隆 → 只覆写 id/cfg/path/位姿/scale」。
//
// 做法:① 等客户端自己发起一次**成功的**真实查表,顺手收一份 112 B 模板(比保龄球版更稳,
// 不依赖球馆格 (67,39) 是否加载);② 对本补丁认领的 id,写槽 + 回 1。
(function () {
'use strict';
function sendlog(s) { try { send(JSON.stringify({ bf: String(s) })); } catch (e) { } }
// ★★ 2026-09-30 卡死修复:热路径(hook 回调)**绝不能**直接 send()。
// send() 是给 Python 宿主的 IPC:宿主正忙 / 出站队列背压时会**阻塞调用它的线程**,
// 而 hook 回调跑在**客户端物件加载线程**上 ⇒ 整条加载流水线停摆(静默死锁)。
// ⇒ 热路径只往缓冲区塞一行,由 setInterval 在 frida 自己的 JS 线程上批量发出。
var LOGBUF = [];
var LOGBUF_MAX = 200; // 满了就丢,绝不扩张、绝不阻塞
function sendlogLater(s) {
try { if (LOGBUF.length < LOGBUF_MAX) LOGBUF.push(String(s)); } catch (e) { }
}
function flushLog() {
if (!LOGBUF.length) return;
try {
var n = Math.min(LOGBUF.length, 30);
for (var i = 0; i < n; i++) send(JSON.stringify({ bf: LOGBUF[i] }));
LOGBUF.splice(0, n);
} catch (e) { }
}
var LOGIV = setInterval(flushLog, 700);
// ---- 气球 id 段:BalloonWireContracts.cs BalloonInstanceIdBase(0x0000_BA11_0000_0000) + 1
// ≤48 位(Lua number 只有 53 位尾数,低位被抹平会把 12 个气球塌成 1 个 —— 铁律 39)
var BALLOON_ID_BASE = 0xba1100000001;
var BALLOON_PREFIX = 'ba11';
var BOWLING_PREFIX = 'd3e03a'; // 只用于观测,不伪造
var MAX_BATCH = 32; // 服务端 BalloonCount 默认 12,留余量
// ★ 保底开关(默认 false):万一客户端在这个扇区**一次成功的真实查表都没有**
// (铁律 29「某层资源没加载 ⇒ 上层查询全静默返 nil」),就拿不到 112 B 模板。
// 置 true 时用「全零槽」顶上(linkPath/navmeshId/linkSceneItemId 三个 BlobArray 全空)。
// 未实测 ⇒ 默认关闭,只在 tpl:false 且 hits 持续增长时才考虑打开。
var ALLOW_ZERO_TPL = false;
// ---- 服务端 side-channel(GameRouter.Balloon.cs 每次 push 前写)----
// {"t":…,"kind":"balloon","count":12,"yaw":…,
// "items":[{"id":<十进制u64>,"cfg":…,"path":…,"x":…,"y":…,"z":…,"scale":…,"i":…}],
// "player":{"x":…,"z":…}}
// ★ R146(2026-09-28):单路径 → **候选路径列表**(exe 加载器会把这一行整个改写成
// 运行期算出来的本机多路径)。为什么改:
// ① 读不到时原先只报一个 'unreadable' —— 异常被 `catch (e) { }` 吞掉了,
// 完全分不清是「路径算错 / 权限不够 / 文件还没写」。R146 为此白查了一整轮。
// ② 整包发给朋友时,「共享路径」的算法只要服务端与注入器有一处不一致就整块失效,
// 多给几个候选(共享路径 / 服务端 base 目录 / 开发机旧位置)可自愈。
var PUSH_PATHS = ['D:\\Ananta\\gym_tools\\_balloon_push.json'];
var PUSH_SRC = ''; // 本局实际读到的是哪个候选(排障用)
var PUSH = {}, PUSH_N = 0, PUSH_T = 0, PUSH_RAW_T = 0, PUSH_ERR = 'none';
var PUSH_LAST_ERR = ''; // 最近一次读取失败的真实异常文本
var PUSH_HITS = 0;
var TPL = null, TPLPTR = null, TPLSRC = '', SLOTS = null;
var ARMED = false;
var HITS = 0, FAKED = 0, MISS = 0, NOTPL = 0, ZEROTPL = 0;
// ★ R128f 计时仪器:FAKE_FIRST_T = 本局客户端**首次成功建球**的墙钟(UTC ms);
// PUSH_SEEN_T = 注入器**首次看到本局坐标**的墙钟(UTC ms)。两者都可以直接减去
// 服务端的 UTC 秒(`t` ⇒ `t*1000`)得到延迟毫秒数。每局随新 push 自动清零。
var FAKE_FIRST_T = 0, PUSH_SEEN_T = 0;
// ★ R128f:MISS 兜底的重读登记(每颗球本局只强制重读一次)+ 兜底救回来的次数
var MISS_RETRY = {}, MISS_RECOVER = 0;
// ★ 2026-09-30:热路径(QUERY_VA 的 onLeave)里不再直接读文件;miss 时只置这个标志,
// 由 250ms 定时器在 frida 自己的线程上代读一次(效果等价,零游戏线程 I/O)。
var MISS_FORCE = 0;
// ★★ 2026-09-30:停机标志 + 监听器清单。⛑ 气球模块原先**完全没有停机出口**
// (bowling 有 bowlShutdown,气球一个都没有)⇒ 关掉加载器后它的 hook 全留在进程里。
var LISTENERS = [];
var DISARMED = false;
var HOLDS_OK = 0, HOLDS_NIL = 0, ENTERED = 0;
var QUERY_VA = null;
// ================= BF-KINEMATIC(R128b,2026-09-28)=================
// 治的病:球「出现 → 1.5 s 后自由落体到地面 → 5 s 心跳把它放回墙上 → 再掉」的死循环。
// 实测铁证(packets-latest.log,局 5 / 10:00:40)单颗球的时间序列:
// 49.659 Y=169.239 → 51.133 Y=168.606(地面) → 54.557 Y=169.592(复位) → 56.276 Y=168.606 …
// 周期恒为 5.1 s(= SceneItemGrid 的 heartbeatMs),回墙→落地固定滞后 1.47 s。
// ⇒ 与 CanBreak 无关(局 4/5 连金色一起掉;+604 偏移已交叉验证无误)。
// 为什么官方气球不掉:官方板上的 16 个 BalloonPoint **全是空节点(ch=0)**,
// 球是玩法运行时创建的、创建时就做了物理固定;我们走的是通用 SceneItem 通道,没有这一步。
// 修法:抄客户端自己的正规做法 —— 保龄球 Lua
// LX6/MiniGame/BowlingGame/BowlingGameManager.lua:907-915
// hold:SetKinematic(true)
// hold:SetUseGravity(false)
// ⇒ 把不用的球「钉」在原地。实测 LX6.Item.SceneItemHold(194 方法 / 136 字段)具备:
// SetKinematic(bool) va=7ffd086c8c70 SetUseGravity(bool) va=7ffd086d5d60
// 触发点:补丁已 hook LX6.Item.SceneItemMgr.GetSceneItemHold —— onLeave 的返回值就是 hold。
var KIN_ENABLED = false; // ★★★ R128e(10:24)**改为 false** —— 见下方事故说明 ★★★
// ⛔ 为什么关:两次客户端主线程卡死(10:17 R128c、10:23 R128d)的共同因子就是这里的
// 「拿到 hold 就用缓存指针调 il2cpp 原生方法」。R128d 只剩这一条也照样卡死
// (`_balloonfix_out.txt`:`[10]/[11] TIMEOUT` + `[!] detach 10s 未返回`;
// Player.log 末行 `10:23:02 OnApplicationFocus Change. False` 之后停写;
// 服务端 10:24:24 仍在推 SCENE-ITEM-GRID ⇒ **进程活着、主线程停了**)。
// ⛔ 而且 L1 **本来就没治好掉落**:R128b 同代码 `kinSeen=59/kinOk=59/kinErr=0` 球照掉。
// ★ 关键推论:R128d 那两局「16/16 全爆、零掉落」**很可能不是 L1 的功劳**,
// 而是**布局 v3**(左右半宽 2.50→2.37m、最小间距 0.50→0.42m、球位全部收回板内)的功劳。
// ⇒ 关掉 L1 正好是一次**干净的判别实验**:若仍不掉 ⇒ v3 是功臣,收工;
// 若又掉 ⇒ L1 有效但危险,须换成不碰缓存指针的实现。
var KIN_SETKIN = null, KIN_SETGRAV = null, KIN_CLS = null;
var KIN_OK = 0, KIN_ERR = 0, KIN_LOG = 0, KIN_ERRMSG = '', KIN_SKIP = 0;
var KIN_LAST = {}, KIN_SEEN = 0;
// ---- R128c(2026-09-28):「钉一次」不够,必须「拦得住」 ----
// 实测:kinSeen=59 / kinOk=59 / kinErr=0 —— 钉球调用全部成功,球照样掉。
// ⇒ 说明钉完之后 **有人又把它恢复了**。
// 铁证在客户端自己的 Lua 里(LX6/MiniGame/BowlingGame/BowlingGameManager.lua):
// ActivateSceneItem = function(self, hold) -- 883-886 ★ 启用
// self:SetCollidersEnabled(hold.SceneItemObj, true)
// hold:SetUseGravity(true) -- ★★★ 启用 = 开重力
// end
// DisableSceneItemSimple = function(self, hold) -- 907-923 ★ 收回
// hold:SetKinematic(true); hold:SetUseGravity(false)
// end
// 「启用就开重力」是客户端的正规做法 ⇒ 我们钉完,紧接着被 Activate 打开 ⇒ 自由落体。
// 三层防御(缺一不可):
// L1 register 取到 hold 就登记进 HOLD_SET(后续所有拦截都靠它认人)
// L2 intercept hook SetUseGravity/SetKinematic **本身**:对已登记的球 hold,
// 凡「开重力」一律驳回、凡「解除 kinematic」一律驳回 ⇒ 谁也别想放它下来
// L3 patrol 每 800 ms 对 HOLD_SET 里的 hold 补一次钉(零 gc.choose,纯原生调用)
// 兜住「刚创建时默认就带重力、压根没调过 SetUseGravity」这种情况
var HOLD_SET = {}, HOLD_SET_N = 0, HOLD_T = {};
var GUARD_ARMED = false;
var GV_ATTACHED = false, KN_ATTACHED = false;
var GV_HITS = 0, GV_TRUE = 0, GV_BLOCKED = 0, GV_LAST = '';
var KN_HITS = 0, KN_FALSE = 0, KN_BLOCKED = 0;
var KIN_PATROL_N = 0, KIN_PATROL_MS = 800;
// ★★★ R128d 止血(2026-09-28 10:17)—— 两个开关**默认全关**,别改回来 ★★★
// 事故:10:15 那版把 KIN_PATROL_ON=true / guard 常开,客户端跑 ~5 秒主线程卡死
// (`_balloonfix_out.txt` 末行 `[ 2] TIMEOUT`;服务端 10:17:44 `session error:
// IOException: 远程主机强迫关闭了一个现有的连接`)。
// 定罪数据(同一次输出):
// kinPatrol:96 gvHits:96 gvTrue:0 gvBlocked:0 knHits:96 knFalse:0 knBlocked:0
// ⇒ 96 = 96 = 96 **全部是我们自己巡检产生的**(16 球 × 6 轮),
// 客户端**一次都没调过** SetUseGravity/SetKinematic ⇒ 被指为元凶的 L3 巡检,
// 正是「用可能已失效的 hold 指针反复调 il2cpp 原生方法」⇒ 高危。
// L3 = 巡检,**永久关闭**;L2 = 拦截,实测客户端从不调用 ⇒ 无收益,一并关闭。
// 保留 L1(拿到 hold 时钉一次):R128b 已跑多局,**证明安全**(只是治不好掉落)。
var KIN_PATROL_ON = false;
var KIN_GUARD_ON = false;
// ★ 2026-09-30 卡死修复 ⑤:热路径(hook 回调)里**绝不做原生方法调用**。
// pinHold 里的 KIN_FN1 / KIN_FN2 就是 `SetKinematic` / `SetUseGravity` ——
// 在 `GetSceneItemHold` 的 onLeave 里同步调它们 = 「在物件加载线程上调用游戏自己的
// 物理/场景方法」,和 IL2CPP invoke 是同一类死锁(抢同一把游戏侧锁)。
// ⇒ 回调里**只登记**进 HOLD_SET,真正的「钉」交给 L3 巡检定时器(frida 自己的线程)。
// 想保留即时钉的效果就开 KIN_PATROL_ON(默认 false);本开关默认 false,别乱开。
var KIN_PIN_IN_HOOK = false;
// ★ 卡死修复 ⑦:QUERY_VA 的「按需挂载」判据 ——
// 只有「最近 BALLOON_ACTIVE_MS 内真的收到过一局气球推送」时才挂本模块的 hook。
// 用 PUSH_SEEN_T(注入器**看到新坐标**的墙钟)而不是文件内容的时间戳,
// 这样上一局遗留的 balloon_push.json 不会让 hook 一直挂着。
var BALLOON_ACTIVE_MS = 300000;
var PUSH_IDLE_SKIP = 0;
var PUSH_LAST_CHANGE = 0;
var PATCH_VER = 'r147-nostopworld'; // ★ r147(2026-09-30 14:2x):删掉 3 s tick 的
// gc.choose(stopWorld 全堆扫描 = 卡死根因)与
// fetchTpl 重入;__active 加文件新鲜度;getter/
// KIN guard hook 补 DISARMED + LISTENERS。
// ==================================================================
var LAST = [];
function readAllText(p) {
// ★ R146:异常**不再静默吞掉** —— 写进 PUSH_LAST_ERR,随状态一起打出来。
// 历史教训(R143→R146):`catch (e) { }` 把
// `Failed to open file "…": Invalid argument` 吃掉了,只剩一句 'unreadable',
// 害得排查方向全跑偏(查权限、查 frida 版本、查进程……都不是)。
PUSH_LAST_ERR = '';
try {
if (typeof File !== 'undefined' && File.readAllText) return File.readAllText(p);
PUSH_LAST_ERR = 'File.readAllText 不存在';
} catch (e) { PUSH_LAST_ERR = 'static: ' + e; }
try {
var f = new File(p, 'r');
var s = f.readAll();
f.close();
return s;
} catch (e) { PUSH_LAST_ERR = (PUSH_LAST_ERR ? PUSH_LAST_ERR + ' | ' : '') + 'ctor: ' + e; }
return null;
}
function loadPush() {
var now = Date.now();
// ★ R128f:节流 1000 → 200 ms(配合下方 250 ms 快读定时器;1000 会把快读也压成 1 s)
if (now - PUSH_T < 200) return;
PUSH_T = now;
try {
// ★ R146:逐个候选路径试(服务端会同时写共享路径 + base 目录 + 旧位置)。
var txt = null, used = '';
for (var pi = 0; pi < PUSH_PATHS.length; pi++) {
txt = readAllText(PUSH_PATHS[pi]);
if (txt) { used = PUSH_PATHS[pi]; break; }
}
if (!txt) {
PUSH_ERR = 'unreadable(' + PUSH_PATHS.length + ' 路径全失败): ' + PUSH_LAST_ERR;
return;
}
var o = JSON.parse(txt);
if (!o || !o.items || !o.items.length) { PUSH_ERR = 'no items @' + used; return; }
var m = {};
for (var i = 0; i < o.items.length; i++) {
var it = o.items[i];
if (it === null || it === undefined || it.id === undefined) continue;
// id 十进制 ~2.05e14 < 2^53 ⇒ Number() 精确,可安全转 16 进制
var hx = Number(it.id).toString(16);
m[hx] = {
cfg: it.cfg | 0, path: it.path | 0,
x: Number(it.x), y: Number(it.y), z: Number(it.z),
s: (it.scale === undefined ? 1000 : it.scale) | 0
};
}
PUSH = m; PUSH_N = o.items.length;
var newT = o.t || 0;
if (newT !== PUSH_RAW_T) {
// 新一局坐标到货:记下注入器看到它的时刻,并把本局的建球计时清零
PUSH_SEEN_T = Date.now();
FAKE_FIRST_T = 0;
MISS_RETRY = {}; // ★ R128f:新一局 ⇒ 兜底重读登记清零
}
PUSH_RAW_T = newT; PUSH_ERR = ''; PUSH_SRC = used;
} catch (e) {
PUSH_ERR = String(e).slice(0, 90);
}
}
function bfClass(full) {
var asms;
try { asms = Il2Cpp.domain.assemblies; } catch (e) { return null; }
for (var j = 0; j < asms.length; j++) {
try { var cc = asms[j].image.class(full); if (cc) return cc; } catch (e) { }
}
for (var i = 0; i < asms.length; i++) {
var classes = null;
try { classes = asms[i].image.classes; } catch (e) { continue; }
if (!classes) continue;
for (var c = 0; c < classes.length; c++) {
var nm = null;
try { nm = String(classes[c].name); } catch (e) { continue; }
if (nm === full) return classes[c];
}
}
return null;
}
function cp(dst, src, len) {
try { dst.writeByteArray(src.readByteArray(len)); return true; } catch (e) { return false; }
}
function hexOf(p) { try { return p.toString(16); } catch (e) { return ''; } }
function isBalloon(p) {
var h = hexOf(p);
return h.length >= 4 && h.indexOf(BALLOON_PREFIX) === 0;
}
function isOurs(p) {
var h = hexOf(p);
return h.indexOf(BALLOON_PREFIX) === 0 || h.indexOf(BOWLING_PREFIX) === 0;
}
function dumpHex(ptr0, len) {
try {
var b = ptr0.readByteArray(len);
var v = new Uint8Array(b), out = [];
for (var i = 0; i < v.length; i++) {
out.push(('0' + v[i].toString(16)).slice(-2));
if ((i & 15) === 15) { out.push('\n'); } else { out.push(' '); }
}
return out.join('');
} catch (e) { return 'hexdump err ' + e; }
}
Il2Cpp.perform(function () {
var blob = bfClass('LX6.Item.SceneItemBlob');
if (!blob) { sendlog('ERR: SceneItemBlob not found'); return; }
try { QUERY_VA = blob.method('TryGetSceneItemPlaceInfoDataByPlaceId').virtualAddress; }
catch (e) { sendlog('ERR: query method ' + e); return; }
SLOTS = Memory.alloc(112 * MAX_BATCH);
// ★★★★★ R99 第二处修正 —— **少写**(skill/REF 里标为「本技能最贵的一课」)。
//
// 原版本除了 identity + 坐标,还额外覆写了 groupId(0x20) / euler(0x30,0x34,0x38)
// / iScale(0x3C = 1000)。其中 **0x3C 是最危险的一格**:
// 它的真身不是「缩放倍率」,而是「尺寸类型索引」——
// R99 探针 dump 出的 `SceneItemPathInfoRuntime.classFields` 里明确有
// System.Byte sizeIndex
// LX6.Item.SceneItemSizeType sizeType
// 而真实模板里这一格**恒为 0**(= 默认尺寸)。
// 写 1000 是**越界枚举值** ⇒ 引擎把 localScale 算成退化值 ⇒
// 症状恰是「hold 拿到了、坐标对、有碰撞有音效、**就是看不见**」——
// 我们这台机器上「看不见气球」正是这个症状。
// ⇒ 铁律:**含义没吃透的字段,保持模板原值永远比写个你猜的值安全。**
// euler 同理(模板是 355.88/21.25/0;气球是球体,无所谓,那就别动它)。
function writeSlot(slot, idPtr, it) {
if (TPL) { cp(slot, TPL, 112); } // 整份拷模板(含 linkPath/navmeshId/两个 blob 数组)
else { slot.writeByteArray(new ArrayBuffer(112)); } // 保底:全零槽
slot.add(0x10).writePointer(idPtr); // uniqueId
slot.add(0x18).writeU32(it.cfg >>> 0); // sceneItemCfgId
slot.add(0x1C).writeS32(it.path | 0); // pathId ← 必须是 SceneItemPathInfoRuntime 的值
slot.add(0x24).writeFloat(it.x); // posX
slot.add(0x28).writeFloat(it.y); // posY
slot.add(0x2C).writeFloat(it.z); // posZ
// ⛔ 其余**一概不写**:groupId(0x20) / eulerX,Y,Z(0x30/34/38) / iScale(0x3C)
// / navmeshId(0x40) / SpecialParamData(0x44) / linkSceneItemId(0x54) / +0x50
}
function recordLine(it) {
return 'cfg=' + it.cfg + ' path=' + it.path + ' pos=(' +
it.x.toFixed(2) + ',' + it.y.toFixed(2) + ',' + it.z.toFixed(2) + ') s=' + it.s;
}
// 主动探一个真 placeId 拿模板(气球摊位区**不在**球馆,所以这里失败是正常的,
// 主路径是 hook 里「任何一次成功的真实查表」顺手收模板)
var TPL_CANDIDATES = [
'0x482716fd0e9d2f30', '0x92204198eaef66de', '0x9bc102684b95d776',
'0xce21103f6e9c1809', '0xebf189795fb8d298'
];
function fetchTpl() {
for (var i = 0; i < TPL_CANDIDATES.length; i++) {
try {
var fn = new NativeFunction(ptr(QUERY_VA), 'int', ['uint64', 'pointer', 'pointer']);
var bA = Memory.alloc(64), bB = Memory.alloc(64);
var zero = new ArrayBuffer(64);
bA.writeByteArray(zero); bB.writeByteArray(zero);
if (!fn(uint64(TPL_CANDIDATES[i]), bA, bB)) continue;
var p = bA.readPointer();
if (!p || p.toString() === '0x0') continue;
var t = Memory.alloc(112);
if (!cp(t, p, 112)) continue;
TPL = t; TPLPTR = p; TPLSRC = 'probe ' + TPL_CANDIDATES[i];
sendlog('template captured (' + TPLSRC + ') -> ' + p.toString(16) +
' cfg=' + TPL.add(0x18).readU32());
sendlog('template hex:\n' + dumpHex(TPL, 112));
return true;
} catch (e) { }
}
return false;
}
// ---------------- 核心 hook ----------------
// ★ 卡死修复 ⑦:QUERY_VA 被 bowling_agent.js 和本模块**双重 hook**
// (两个 script 各自 attach 同一个地址 ⇒ 每次调用要过两遍 trampoline + 两遍 JS 回调,
// 而这条路径就是客户端物件加载线程的热路径)。
// ⇒ 本模块的 hook 改成**按需挂载**:只在真的在打气球(最近收到过一局气球坐标)时挂,
// 其余时间热路径上只剩 bowling 一个 hook。挂载/卸载都在定时器线程上做,不在回调里做。
var QUERY_LS = null;
function ensureQueryHook() {
if (QUERY_LS || !QUERY_VA) return;
QUERY_LS = Interceptor.attach(ptr(QUERY_VA), {
onEnter: function (args) {
this.id = args[0];
this.outA = args[1];
this.outB = args[2];
},
onLeave: function (retval) {
if (DISARMED) return; // ★ 停机后一个字节都不再碰
var ok = 0;
try { ok = retval.toInt32(); } catch (e) { }
if (ok !== 0) {
// 真实 placeId 成功 —— 收模板(★ 但绝不能把自己伪造的槽当成模板)
if (!TPL && !isOurs(this.id)) {
try {
var p = this.outA.readPointer();
if (p && p.toString() !== '0x0') {
var t = Memory.alloc(112);
if (cp(t, p, 112)) {
TPL = t; TPLPTR = p; TPLSRC = 'hook (real lookup ' + hexOf(this.id) + ')';
sendlogLater('template captured (' + TPLSRC + ') -> ' + p.toString(16) +
' cfg=' + TPL.add(0x18).readU32());
sendlogLater('template hex:\n' + dumpHex(TPL, 112));
}
}
} catch (e) { }
}
return;
}
if (!isBalloon(this.id)) return; // 保龄球的 id 交给 bowl_fix.js,不在这里伪造
HITS++;
var hx = hexOf(this.id);
/*loadPush();*/ // ★ 卡死修复:热路径不做文件 I/O(改由 250ms 定时器刷)
var it = PUSH[hx];
if (!it) {
// ★★★★★ R128f —— MISS 兜底 = **"球晚几秒才出来"的真正病根**。
//
// 【为什么一定是 MISS】服务端 `SpawnBalloonsAsync` 的顺序是:
// :1021 await NotifyRawAsync(SyncDestructibleGridAOIDecrease) ← 建 hold 的报文,**先发**
// :1088 File.WriteAllText(_balloon_push.json) ← 坐标文件,**后写**
// 而**每一局的球 id 都是全新的**(`BalloonInstanceIdBase + ordinal`)⇒
// 客户端一收到报文就建 hold、立刻查 placeInfo,此时注入器手里那张表
// 里**根本没有这批新 id**(连"上一条船"都不是)⇒ 100% MISS
// ⇒ 球这一轮画不出来 ⇒ 只能等下一个 **5 s 心跳**把 Protected 的球
// 重新塞回 `aoiInfo.AddInfos` 重建 hold 再查一次才补上。
//
// 【兜底】无条件绕过节流强制重读文件一次,再查。
// ⛑ 频率保护:**每颗球本局只强制一次**(`MISS_RETRY[hx]`,新一局清空)。
// 因为服务端从 :1021 发报到 :1088 写文件只隔了「拼字符串 + 一次落盘」
// (微秒~毫秒级),而客户端要等网络包到达 + 主线程下一帧才处理
// ⇒ 我们这里重读时文件**必然已经落盘**。
// 每颗球只一次 ⇒ 每局最多 16 次小文件读,主线程零风险
// (对比:若无条件每次都读,客户端若每帧查 16 球就是 ~960 次/秒)。
if (!MISS_RETRY[hx]) {
MISS_RETRY[hx] = 1;
MISS_FORCE = 1;
/*loadPush();*/ // ★ 卡死修复:同上,交给 MISS_FORCE + 定时器
it = PUSH[hx];
if (it) MISS_RECOVER++;
}
}
if (!it) {
MISS++;
if (MISS <= 10) {
sendlogLater('MISS id=0x' + hx + ' (push.json n=' + PUSH_N +
' t=' + PUSH_RAW_T + ' err=' + PUSH_ERR + ')');
}
return;
}
// ★ R128 仪器:**客户端正要建 hold 的这一刻**,回读该 cfg 行的 CanBreak 现值。
// cbAtHold1 > 0 且球仍"被打中就掉" ⇒ 说明 CanBreak 不是那道闸,别再往这儿修。
try {
var rp = CB_MAP[it.cfg];
if (rp) {
var cv = rp.add(CB_OFF).readU8();
if (cv) CB_ATHOLD1++; else CB_ATHOLD0++;
} else {
CB_ATHOLD_NOROW++;
}
} catch (e) { }
if (!TPL) {
// ★ 卡死修复 ⑨(2026-09-30 14:2x):原版这里调 fetchTpl() ——
// 通过被 hook 的 QUERY_VA 重入自己的 onLeave(脚本线程自锁)。
// 模板只剩:真实查表成功的被动捕获(上方 ok!==0 分支)+ 手动 bfRefetch。
NOTPL++;
if (!ALLOW_ZERO_TPL) {
if (NOTPL <= 10) sendlogLater('no template yet, id=0x' + hx + ' pending');
return;
}
if (ZEROTPL === 0) sendlogLater('WARN: using ZERO template (ALLOW_ZERO_TPL)');
ZEROTPL++;
}
var n = 0;
try { n = parseInt(hx, 16); } catch (e) { }
var idx = n - BALLOON_ID_BASE;
if (!(idx >= 0 && idx < MAX_BATCH)) { idx = (n - BALLOON_ID_BASE) % MAX_BATCH; if (idx < 0) idx += MAX_BATCH; }
try {
writeSlot(SLOTS.add(idx * 112), this.id, it);
this.outA.writePointer(SLOTS.add(idx * 112));
retval.replace(ptr(1));
FAKED++;
PUSH_HITS++;
// ★ R128f 仪器:客户端**首次成功建球**的墙钟(UTC ms)。
// `Date.now()` 是 UTC ms,服务端 `t` 是 UTC 秒 ⇒ 可直接相减:
// fakeLagMs = FAKE_FIRST_T - PUSH_RAW_T*1000
// = 「服务端写 push 文件」→「客户端真正把球画出来」的毫秒数。
// 这就是用户说的"晚几秒"的**精确值**,不用肉眼估。
if (!FAKE_FIRST_T) FAKE_FIRST_T = Date.now();
LAST.push({ id: hx, idx: idx });
if (LAST.length > 24) LAST.shift();
if (FAKED <= 20) sendlogLater('FAKED 0x' + hx + ' idx=' + idx + ' ' + recordLine(it));
} catch (e) { sendlogLater('fake err ' + e); }
}
});
LISTENERS.push(QUERY_LS);
sendlog('balloon: QUERY_VA hook 已挂载(检测到本局气球推送)');
}
function dropQueryHook() {
if (!QUERY_LS) return;
try { QUERY_LS.detach(); } catch (e) { }
var k = LISTENERS.indexOf(QUERY_LS);
if (k >= 0) LISTENERS.splice(k, 1);
QUERY_LS = null;
sendlog('balloon: QUERY_VA hook 已卸载(气球玩法闲置)');
}
// ⛑ 启动时不挂 —— 由下方 250ms 定时器按「是否真的在打气球」挂载。
ARMED = true;
sendlog('armed (balloon). id base=0x' + BALLOON_ID_BASE.toString(16) +
'; waiting for _balloon_push.json + a real lookup for the template');
// ---- 观测 1:客户端给自己认领的 id 找 hold,非空 = 真正建出来了 ----
// ---- ★ R128b:同一个 hook 顺手把球「钉住」(BF-KINEMATIC)----
var KIN_KLASS = null, KIN_FN1 = null, KIN_FN2 = null;
try {
KIN_CLS = bfClass('LX6.Item.SceneItemHold');
if (KIN_CLS) {
KIN_SETKIN = KIN_CLS.method('SetKinematic').virtualAddress;
KIN_SETGRAV = KIN_CLS.method('SetUseGravity').virtualAddress;
try { KIN_KLASS = KIN_CLS.handle; } catch (e2) { KIN_KLASS = null; }
KIN_FN1 = new NativeFunction(KIN_SETKIN, 'void', ['pointer', 'int']);
KIN_FN2 = new NativeFunction(KIN_SETGRAV, 'void', ['pointer', 'int']);
sendlog('KINEMATIC resolved: SetKinematic=0x' + KIN_SETKIN.toString(16) +
' SetUseGravity=0x' + KIN_SETGRAV.toString(16) +
' klass=' + (KIN_KLASS ? KIN_KLASS.toString() : 'null'));
} else {
KIN_ERRMSG = 'SceneItemHold class not found';
sendlog('KINEMATIC: ' + KIN_ERRMSG);
}
} catch (e) { KIN_ERRMSG = 'resolve ' + e; sendlog('KINEMATIC resolve err ' + e); }
// ★ 卡死修复 ⑤:armKinGuards() 内部会 Interceptor.attach —— 改代码要拿 frida 的独占锁,
// 而此时「物件加载线程」可能正堵在同一个 script 的运行时锁上 ⇒ 互等死锁。
// ⇒ 挪出 hook 回调,放到启动后的定时器里(默认 KIN_GUARD_ON=false,本来就不会 arm)。
setTimeout(function () { if (KIN_GUARD_ON) { try { armKinGuards(); } catch (e) { } } }, 1500);
// ---- L2:拦截「启用场景物 = 开重力」这一步(R128c 核心)----
// 客户端 Lua(BowlingGameManager.ActivateSceneItem:885)的正规做法就是
// hold:SetUseGravity(true) ⇒ 谁把它放下来,我们就把谁驳回。
// 只在 HOLD_SET(= 我们的球)上生效,其余场景物一律放行,零影响。
function armKinGuards() {
if (GUARD_ARMED) return;
GUARD_ARMED = true;
if (!KIN_ENABLED || !KIN_SETGRAV || !KIN_SETKIN) { sendlog('KIN GUARD: no va, skipped'); return; }
try {
LISTENERS.push(Interceptor.attach(ptr(KIN_SETGRAV), {
onEnter: function (args) {
if (DISARMED) return; // ★ 卡死修复 ⑫:停机后一个字节都不再碰
var k = args[0].toString();
if (!HOLD_SET[k]) return;
GV_HITS++;
var v = 0;
try { v = args[1].toInt32(); } catch (e) { }
GV_LAST = k + '->' + v;
if (v !== 0) {
GV_TRUE++;
args[1] = ptr(0);
GV_BLOCKED++;
if (GV_BLOCKED <= 12) sendlogLater('GV BLOCK ' + k + ' gravity true->false');
}
}
}));
GV_ATTACHED = true;
} catch (e) { sendlog('GV attach err ' + e); }
try {
LISTENERS.push(Interceptor.attach(ptr(KIN_SETKIN), {
onEnter: function (args) {
if (DISARMED) return; // ★ 卡死修复 ⑫:停机后一个字节都不再碰
var k = args[0].toString();
if (!HOLD_SET[k]) return;
KN_HITS++;
var v = 1;
try { v = args[1].toInt32(); } catch (e) { }
if (v === 0) {
KN_FALSE++;
args[1] = ptr(1);
KN_BLOCKED++;
if (KN_BLOCKED <= 12) sendlogLater('KN BLOCK ' + k + ' kinematic false->true');
}
}
}));
KN_ATTACHED = true;
} catch (e) { sendlog('KN attach err ' + e); }
sendlog('KIN GUARD armed. gv=0x' + KIN_SETGRAV.toString(16) +
' kn=0x' + KIN_SETKIN.toString(16) + ' gvAtt=' + GV_ATTACHED + ' knAtt=' + KN_ATTACHED);
}
// ---- L3:巡检(兜住「创建时默认就带重力、压根没调过 SetUseGravity」)----
// 只碰最近 15 s 内登记过、且地址还可读的 hold;最多 32 个;纯原生调用,零 gc.choose。
if (KIN_PATROL_ON) {
setInterval(function () {
if (!GUARD_ARMED || !KIN_FN1 || HOLD_SET_N === 0) return;
var ks = Object.keys(HOLD_SET), now = Date.now(), n = 0;
for (var i = 0; i < ks.length && n < 32; i++) {
if (!HOLD_T[ks[i]] || now - HOLD_T[ks[i]] > 15000) continue;
var p = null;
try { p = ptr(ks[i]); if (!Process.findRangeByAddress(p)) continue; } catch (e) { continue; }
try { KIN_FN1(p, 1); if (KIN_FN2) KIN_FN2(p, 0); KIN_PATROL_N++; n++; } catch (e) { }
}
}, KIN_PATROL_MS);
}
function pinHold(hp, why) {
if (!KIN_ENABLED || !KIN_FN1 || !hp || hp.isNull()) return;
if (KIN_KLASS) {
try { if (!hp.readPointer().equals(KIN_KLASS)) { KIN_SKIP++; return; } }
catch (e) { KIN_SKIP++; return; }
}
// ---- L1:先认人(登记进 HOLD_SET),再钉 ----
var hk = hp.toString();
if (!HOLD_SET[hk]) {
if (HOLD_SET_N > 4096) { HOLD_SET = {}; HOLD_T = {}; HOLD_SET_N = 0; }
HOLD_SET[hk] = 1; HOLD_SET_N++;
if (HOLD_SET_N <= 24) sendlogLater('HOLD reg #' + HOLD_SET_N + ' ' + hk + ' (' + why + ')');
}
HOLD_T[hk] = Date.now();
// ★ 卡死修复 ⑤:armKinGuards() 内部会 Interceptor.attach(= 改代码、要 frida 的锁),
// 绝不能在 hook 回调里做 ⇒ 已挪到启动后的定时器里(见下方 setTimeout)。
// ---- L1 收尾:落地钉一次(★ 卡死修复 ⑤:默认**不钉**,只登记)----
if (!KIN_PIN_IN_HOOK) return;
try {
KIN_FN1(hp, 1);
if (KIN_FN2) KIN_FN2(hp, 0);
KIN_OK++;
if (KIN_LOG < 12) { KIN_LOG++; sendlogLater('PIN(' + why + ') hold=' + hp); }
} catch (e) {
KIN_ERR++;
if (KIN_ERR <= 6) sendlogLater('PIN err(' + why + ') ' + e);
}
}
try {
var mgrCls = bfClass('LX6.Item.SceneItemMgr');
var GET_VA = mgrCls.method('GetSceneItemHold').virtualAddress;
LISTENERS.push(Interceptor.attach(ptr(GET_VA), {
onEnter: function (args) {
this.idp = isBalloon(args[1]) ? args[1] : (isBalloon(args[0]) ? args[0] : null);
},
onLeave: function (rv) {
if (!this.idp) return;
var s = '0x' + rv.toString(16);
if (s === '0x0') {
HOLDS_NIL++;
if (HOLDS_NIL <= 20) sendlogLater('hold NIL for 0x' + this.idp.toString(16));
} else {
HOLDS_OK++;
if (HOLDS_OK <= 20) sendlogLater('hold OK for 0x' + this.idp.toString(16));
// ★ R128b:就地钉住。同一 id 400 ms 内只鞭一次(hold 是池化复用的,
// 持续鞭打才能覆盖「重建 → 又变成非 kinematic」)。
var kk = this.idp.toString();
var nowms = Date.now();
if (!KIN_LAST[kk] || nowms - KIN_LAST[kk] > 400) {
KIN_LAST[kk] = nowms;
KIN_SEEN++;
pinHold(rv, 'hold');
}
}
}
}));
try {
var EW_VA = mgrCls.method('DoSceneItemHolderEnterWorld').virtualAddress;
LISTENERS.push(Interceptor.attach(ptr(EW_VA), {
onEnter: function (args) {
if (isBalloon(args[1])) {
ENTERED++;
sendlogLater('holder entered world: 0x' + args[1].toString(16));
}
}
}));
} catch (e) { }
} catch (e) { sendlog('watch hook err ' + e); }
// ★★★ 卡死修复 ⑪(2026-09-30 14:2x)—— 本 tick **整个删除**,这是「注入器还卡死」的根因。
// 【证据链】上一轮(§34-36)只修了 bowling 的热路径 + 本模块的定时器 I/O,
// 但这个 3 s tick 里留下了两个致命调用:
// ① cbSweep → Il2Cpp.gc.choose —— 它的实现就是 stopWorld() + 全堆 liveness 扫描
// (见上方 choose() 源码 :686-690:gcStopWorld → livenessCalculationFromStatics
// → gcStartWorld)。每 3 s 冻结一次全部托管线程、扫 30 万个活对象,
// 而且是**无条件**的(不管你玩不玩气球)。
// ② fetchTpl —— QUERY hook 挂载期间从定时器 NativeFunction 调 QUERY_VA
// = 在被 hook 的函数里重入(修复 ⑨ 同源;遗留 balloon_push.json 会让
// hook 在开局前 300 s 一直挂着,见下方 __active 的修复 ⑩)。
// 【冻结机理】gc.choose 停世期间本 script 握着 V8 运行时锁;游戏物件加载线程
// 只要正走在 QUERY_VA / GetSceneItemHold / DoSceneItemHolderEnterWorld /
// get_CanBreak / SceneitemConfig.ctor 任何一个 hook 的 trampoline 里
// (等 V8 锁),就永远到不了 GC 的停世安全点 ⇒ stopWorld 等线程、线程等
// V8 锁、V8 锁等 stopWorld 结束 —— 三角互等 = 游戏整机冻结。
// 【时间线吻合(13:14 局)】13:16:54 进 Spiko Hotel(娃娃机/游戏机/人台密集区,
// 物件加载流量最大)→ 13:17:08 冻结,间隔 ≈ 4~5 个扫描周期;
// §36.2 保龄球馆(10:43)那次也是「进高密度室内 → 很快冻结」同款。
// 【CanBreak 三条腿的新分工】① gc.choose 全堆扫描 —— 删;
// ② get_CanBreak getter hook —— 保留(纯内存);③ .ctor hook —— 保留
// (R126 实测官方三条 cfg 行对象「用到才出生」,出生必经我们的 ctor hook,
// 不需要全堆扫描兜底)。手动兜底走 rpc bfSweep(带警告,别在游戏加载时按)。
// 【模板】只剩两个安全来源:hook 里真实查表成功时的被动捕获 + 手动 rpc bfRefetch。
var tick = null; // 保留变量名:shutdown 里的 clearInterval(tick) 不用改
// ★★★★★ R128f(2026-09-28)—— push 坐标**高频**读取,修「球晚几秒才出来」。
//
// 【病因(实测铁证)】本块原先只挂在上面那个 3 s 的 tick 里,而 `loadPush()` 内部
// 还有一道 1 s 节流 ⇒ 注入器最坏晚 **3 s** 才读到服务端刚写好的坐标。
// 而服务端 `SpawnBalloonsAsync` 的顺序是:
// ① `File.WriteAllText(_balloon_push.json)`(Balloon.cs:1078-1091)
// ② **才** `conn.NotifyRawAsync(SyncDestructibleObjectAddList, …)`(:1095)
// ⇒ 文件**从不迟到**,迟到的是注入器。
// 客户端一收到 spawn 报文就**当场**建 hold 并查 placeInfo,那一次注入器手里还是
// 旧数据 ⇒ 查不到(铁律 49)⇒ 球建不出来,只能等下一个 **5 s 心跳**把 Protected
// 的球重新塞进 aoiInfo.AddInfos 重建时才补上 ⇒ 用户看到"进区后要等几秒才有球"。
// 实测对照:服务端 `10:31:39 EnterBalloonZone`(= 写文件),第一次带球的
// `SCENE-ITEM-GRID push protected=16` 在 `10:31:43`;客户端拿枪 `10:31:39.853`。
//
// 【修法】单独用 250 ms 定时器只干「读文件」这一件事;读到新的 `t` 立刻补跑
// `cbSweep`(CanBreak 也因此提前落地,不再等 3 s tick)。
// ⛑ 安全性:本块只做 `File.readAllText` + `JSON.parse`,**零 il2cpp 调用、零原生
// 方法、零 gc.choose** ⇒ 不触碰铁律 116 的血案路径(卡死是"用缓存指针调原生
// 方法"造成的)。读的是 ~3 KB 小文件、命中 OS 缓存,主线程开销可忽略。
//
// 【回退】删掉本块、把 `loadPush()` 的节流改回 1000 ms、并把 `cbSweep('push')`
// 那段挪回 3 s tick —— 即回 R128e 行为("球晚几秒",但功能正常)。
setInterval(function () {
// ★ 卡死修复 ⑥:这个定时器会持有本 script 的**运行时锁**,而游戏线程的 hook 回调
// 要抢同一把锁 ⇒ 回调越长,物件加载线程停顿越久。
// ⇒ 没有新推送时把 250ms 降频到 1s(每 4 拍才真跑一次)。
var __now = Date.now();
if (PUSH_RAW_T !== PUSH_LAST_CHANGE) { PUSH_LAST_CHANGE = PUSH_RAW_T; PUSH_IDLE_SKIP = 0; }
else if (PUSH_IDLE_SKIP++ % 4 !== 0) return;
// ★ 卡死修复 ⑦:按需挂载/卸载 QUERY_VA hook(消掉与 bowling 的双重 hook)
// ★ 卡死修复 ⑩(2026-09-30 14:2x):__active **再加一道「文件自身新鲜度」**。
// 原判据只看 PUSH_SEEN_T(注入器看到内容变化的墙钟),而 PUSH_RAW_T 初始为 0
// ⇒ 开局第一次读到**上一局遗留的** balloon_push.json 也会把 PUSH_SEEN_T 置成
// 「刚刚」⇒ 旧文件让 QUERY hook 白白挂 300 s(修复 ⑨ 的重入面也因此变热)。
// 现在要求文件里的 t(UTC 秒)距现在也不超过 BALLOON_ACTIVE_MS,旧文件直接不挂。
var __active = PUSH_N > 0 && PUSH_SEEN_T > 0 && (__now - PUSH_SEEN_T) < BALLOON_ACTIVE_MS
&& PUSH_RAW_T > 0 && (__now - PUSH_RAW_T * 1000) < BALLOON_ACTIVE_MS;
// ★ 卡死修复 ⑬(2026-09-30 15:0x):拿到模板(TPL)前**保持挂载**。
// 模板唯一的安全来源 = hook 里「任何一次成功的真实查表」的被动捕获
// (挂 hook 前调 fetchTpl = 重入自锁,⑨ 已删)。若玩家走到气球区才挂
// hook,服务端 +1.8s 延迟重推到达时 TPL 多半还是 null ⇒ 全 NOTPL ⇒
// 气球还是出不来。挂载期间每次查表只多一次「retval + 前缀比较」的
// 纯内存检查,成本可忽略;拿到 TPL 后闲置即卸载(双重 hook 消掉)。
var needSeed = !TPL;
if (needSeed || __active) ensureQueryHook(); else dropQueryHook();
if (MISS_FORCE) { PUSH_T = 0; MISS_FORCE = 0; } // ★ 热路径 miss 的代读
loadPush();
// ★ 卡死修复 ⑪:新一局推送后的三次 gc.choose 补扫 —— 删(stopWorld 是冻结根因;
// ctor hook 在行对象出生那一刻就写好 CanBreak,不需要全堆扫描兜底)。
}, 250);
// ==================== BF-CANBREAK(R126,2026-09-28)====================
//
// 【为什么需要它】颜色绑 **cfgId**(铁律 83,`SceneitemResPath` 是颜色唯一载体)
// ⇒ 想要官方蓝/紫/绿的真气球,就必须把 balloonCfgSweep 换成官方 cfgId
// (80005797 蓝3 / 80005799 紫5 / 80005800 绿1)。
// 但这三条 `CanBreak = False`(客户端本地配表)⇒ 客户端不当它们是可破坏物
// ⇒ 打中只发 AskMove(掉下去、不加分),永远发不出 AskBreak(铁律 71,8/8 实测)。
// ⇒ 唯一出路 = 在客户端把这三条的 `CanBreak` 改成 true。本块就是干这个。
//
// 【实证数据(R126 探针 `_r126_canbreak_out.txt`,frida 直读活内存)】
// `LT.ConfigGen.SceneitemConfig`:`<Id>k__BackingField` off=**16**(u32),
// `<CanBreak>k__BackingField` off=**604**(bool),`get_CanBreak` va=0x…d32f0。
// 实测:`obj.handle.add(16).readU32()` 精确等于 cfg id;`add(604).readU8()` 与
// 配置里的 CanBreak 8/8 一致(金/lqc=1,官方非金=0)⇒ 两个偏移都可信。
//
// 【两条腿,缺一不可】
// ① `gc.choose` 扫活实例写字段 —— 但**官方那三条在"被用到之前"根本不存在**
// (实测:全表 364 个活实例里只有我们真用过的 4 条)⇒ 必须在收到新一局
// 推送后反复扫(客户端处理 AOI 报文时会创建这三行)。
// ② hook `get_CanBreak` —— 不依赖实例何时创建,且覆盖「代码走属性」的路径。
// 两条都做,任一条生效即可。
var CB_IDS = [80005797, 80005799, 80005800]; // 官方蓝 / 紫 / 绿
var CB_IDOFF = 16, CB_OFF = 604;
var CB_KLASS = null, CB_DONE = {}, CB_N = 0, CB_ERR = '';
var CB_SWEEPS = 0, CB_HOOKED = false, CB_BUSY = false;
var CB_GET_HITS = 0, CB_GET_FORCED = 0;
// ★ R128 新增:ctor 钩子(行对象出生即写)+ 建 hold 时的实读计数
var CB_MAP = {}, CB_CTOR_HOOKED = false, CB_CTOR_HITS = 0, CB_CTOR_FIX = 0;
var CB_RETRY = 0, CB_ATHOLD1 = 0, CB_ATHOLD0 = 0, CB_ATHOLD_NOROW = 0;
function cbNeed() { return CB_N < CB_IDS.length; }
function cbSweep(why) {
if (CB_BUSY) return;
CB_BUSY = true;
try {
Il2Cpp.perform(function () {
CB_SWEEPS++;
if (!CB_KLASS) CB_KLASS = bfClass('LT.ConfigGen.SceneitemConfig');
if (!CB_KLASS) { CB_ERR = 'class-not-found'; return; }
var objs = Il2Cpp.gc.choose(CB_KLASS);
if (!objs) { CB_ERR = 'choose-null'; return; }
for (var i = 0; i < objs.length; i++) {
var p = null;
try { p = objs[i].handle; } catch (e) { continue; }
if (!p) continue;
var id = 0;
try { id = p.add(CB_IDOFF).readU32() >>> 0; } catch (e) { continue; }
if (CB_IDS.indexOf(id) < 0) continue;
CB_MAP[id] = p; // ★ R128:留句柄,供「建 hold 那一刻」回读
var cur = -1;
try { cur = p.add(CB_OFF).readU8(); } catch (e) { continue; }
if (cur === 1) { // ★ R128:先读再写 —— 幂等、可自愈
if (!CB_DONE[id]) {
CB_DONE[id] = 1; CB_N++;
sendlog('CANBREAK already-ok cfg=' + id + ' CanBreak=1 (why=' + why + ')');
}
continue;
}
try {
p.add(CB_OFF).writeU8(1);
CB_DONE[id] = 1; CB_N++; CB_RETRY++;
sendlog('CANBREAK patched cfg=' + id + ' -> CanBreak=1 (off=' + CB_OFF + ', why=' + why + ')');
} catch (e) { CB_ERR = 'write ' + e; }
}
});
} catch (e) { CB_ERR = String(e).slice(0, 100); }
CB_BUSY = false;
}
// ② 属性 getter hook(不依赖实例存在)
// ★ 卡死修复 ⑫(2026-09-30):这个 hook 原先**没进 LISTENERS、也没查 DISARMED**
// ⇒ shutdown 之后它还留在进程里,每个场景物的可破坏性检查都要过一遍。
// 现在两处都补上(shutdown 能真正摘掉 + 停机后一个字节不碰)。
try {
if (!CB_KLASS) CB_KLASS = bfClass('LT.ConfigGen.SceneitemConfig');
if (CB_KLASS) {
var cbVA = CB_KLASS.method('get_CanBreak').virtualAddress;
LISTENERS.push(Interceptor.attach(ptr(cbVA), {
onEnter: function (args) { this.self = args[0]; },
onLeave: function (rv) {
if (DISARMED) return; // ★ 停机后一个字节都不再碰
CB_GET_HITS++;
try {
var id = this.self.add(CB_IDOFF).readU32() >>> 0;
if (CB_IDS.indexOf(id) >= 0) { rv.replace(1); CB_GET_FORCED++; }
} catch (e) { }
}
}));
CB_HOOKED = true;
sendlog('CANBREAK getter hooked va=' + cbVA.toString(16));
}
} catch (e) { CB_ERR = 'hook ' + e; }
// ③ ★★★★★ R128:`ctor` hook —— **配表行对象一出生就写**,消灭「补丁比客户端读取慢」的竞态。
//
// 【为什么必须加这一条(R128 实测时间线)】
// 服务端日志 `[BALLOON] spawn` 与注入 `bfStatus` 对齐后可见:
// 第 1 次轮询 → `cbPatched:0, cbSweeps:7, entered:0`
// 第 2 次轮询 → `cbPatched:3, cbSweeps:10, entered:16`
// 即 **这三行配表对象是在"第一局进场"时才被创建** 的(铁律 106:惰性创建),
// 而 `gc.choose` 轮询最快也要 3 秒后才扫到它 ⇒ 客户端建 hold 时读到的仍是 False。
// 患者症状:cols1~3(蓝/紫/绿)**每局"被打中就掉"**,col0(金,原生 True)零掉落。
// ⇒ 只能把写入点提前到「行对象构造完成的那一瞬间」。
//
// 实测 API(R128 探针 `_r128_ctor_out.txt`):`.ctor` va 可解析(0 参数),
// 另有 `LoadAt` / `GetTableIds` / `InitKeyValue` ⇒ 行确实是运行时逐行装载。
// 手法:`onLeave` 立刻检查一次 + `setTimeout(0/20/120ms)` 补检三次
// (ctor 返回后调用方才填 Id 字段,延后一点读才准)。
var CB_CTOR_DELAYS = [0, 20, 120];
function cbFixRow(p, why) {
if (!p) return;
try {
var id = p.add(CB_IDOFF).readU32() >>> 0;
if (CB_IDS.indexOf(id) < 0) return;
CB_MAP[id] = p;
if (p.add(CB_OFF).readU8() === 1) return;
p.add(CB_OFF).writeU8(1);
CB_CTOR_FIX++;
if (CB_CTOR_FIX <= 12) sendlogLater('CANBREAK ctor-fix cfg=' + id + ' -> CanBreak=1 (' + why + ')');
} catch (e) { }
}
try {
if (!CB_KLASS) CB_KLASS = bfClass('LT.ConfigGen.SceneitemConfig');
if (CB_KLASS) {
var ctorVA2 = CB_KLASS.method('.ctor').virtualAddress;
LISTENERS.push(Interceptor.attach(ptr(ctorVA2), {
onEnter: function (args) { this.self = args[0]; },
onLeave: function () {
CB_CTOR_HITS++;
var self = this.self;
cbFixRow(self, 'ctor');
for (var q = 0; q < CB_CTOR_DELAYS.length; q++) {
(function (d) {
setTimeout(function () { cbFixRow(self, 'ctor+' + d + 'ms'); }, d);
})(CB_CTOR_DELAYS[q]);
}
}
}));
CB_CTOR_HOOKED = true;
sendlog('CANBREAK ctor hooked va=' + ctorVA2.toString(16));
}
} catch (e) { CB_ERR = 'ctor-hook ' + e; }
rpc.exports = rpc.exports || {};
rpc.exports.bfStatus = function () {
return JSON.stringify({
ver: PATCH_VER,
armed: ARMED, play: 'balloon',
tpl: !!TPL, tplSrc: TPLSRC,
tplPtr: TPLPTR ? TPLPTR.toString(16) : null,
tplCfg: TPL ? TPL.add(0x18).readU32() : null,
pushN: PUSH_N, pushT: PUSH_RAW_T, pushErr: PUSH_ERR, pushSrc: PUSH_SRC,
// ★ R128f 延迟仪器(毫秒):
// pushLagMs = 服务端写 push 文件 → 注入器看到它 (本次修的就是这一段)
// fakeLagMs = 服务端写 push 文件 → 客户端首次建球画出来(用户感知的"晚几秒")
// 两者都是 UTC 毫秒相减,可直接读;null = 本局还没发生。
pushLagMs: (PUSH_SEEN_T && PUSH_RAW_T) ? (PUSH_SEEN_T - PUSH_RAW_T * 1000) : null,
fakeLagMs: (FAKE_FIRST_T && PUSH_RAW_T) ? (FAKE_FIRST_T - PUSH_RAW_T * 1000) : null,
fakeFirstT: FAKE_FIRST_T || null, pushSeenT: PUSH_SEEN_T || null,
missRetry: Object.keys(MISS_RETRY).length, missRecover: MISS_RECOVER,
hits: HITS, faked: FAKED, miss: MISS, noTpl: NOTPL, zeroTpl: ZEROTPL,
holdsOk: HOLDS_OK, holdsNil: HOLDS_NIL, entered: ENTERED,
cbHooked: CB_HOOKED, cbPatched: CB_N, cbDone: Object.keys(CB_DONE),
cbSweeps: CB_SWEEPS, cbGetHits: CB_GET_HITS, cbGetForced: CB_GET_FORCED,
cbCtorHooked: CB_CTOR_HOOKED, cbCtorHits: CB_CTOR_HITS, cbCtorFix: CB_CTOR_FIX,
cbRetry: CB_RETRY, cbAtHold1: CB_ATHOLD1, cbAtHold0: CB_ATHOLD0,
cbNoRow: CB_ATHOLD_NOROW,
cbErr: CB_ERR,
kinEnabled: KIN_ENABLED,
kinVa: KIN_SETKIN ? KIN_SETKIN.toString(16) : null,
kinOk: KIN_OK, kinErr: KIN_ERR, kinSeen: KIN_SEEN, kinSkip: KIN_SKIP,
kinErrMsg: KIN_ERRMSG,
holdSet: HOLD_SET_N, guardArmed: GUARD_ARMED,
guardOn: KIN_GUARD_ON, patrolOn: KIN_PATROL_ON,
gvAtt: GV_ATTACHED, knAtt: KN_ATTACHED,
gvHits: GV_HITS, gvTrue: GV_TRUE, gvBlocked: GV_BLOCKED, gvLast: GV_LAST,
knHits: KN_HITS, knFalse: KN_FALSE, knBlocked: KN_BLOCKED,
kinPatrol: KIN_PATROL_N,
last: LAST.slice(-6)
});
};
rpc.exports.bfRefetch = function () {
// ★ 卡死修复 ⑨:QUERY hook 挂载期间从脚本线程直接调 QUERY_VA = 自锁。
// 手动重取模板前先摘 hook(250 ms 定时器在还活跃时会自动再挂上)。
if (QUERY_LS) dropQueryHook();
TPL = null;
return fetchTpl() ? 'ok' : 'fail';
};
// ★ 卡死修复 ⑪ 配套:cbSweep 不再被任何定时器/推送触发,只留这个**手动**出口。
// ⛑☠ gc.choose = stopWorld + 全堆扫描 —— 游戏正在加载物件/玩的时候调它
// 就是在赌死锁(本次卡死的根因就是每 3 s 自动跑一次)。只允许排障时手动按,
// 且最好站在空旷处。正常情况下永远不需要:ctor hook 在行对象出生时就写好了。
rpc.exports.bfSweep = function () { cbSweep('manual'); return 'swept'; };
// ★★ 2026-09-30:气球模块的停机出口(此前完全没有 ⇒ hook 留在进程里)。
// loader_common 停机时依次试 ("shutdown", "bowlShutdown", "abShutdown", "stop"),
// 所以这里必须叫 **shutdown**。
rpc.exports.shutdown = function () {
DISARMED = true;
try { clearInterval(tick); } catch (e) { }
try { clearInterval(LOGIV); } catch (e) { }
var n = 0;
for (var i = 0; i < LISTENERS.length; i++) {
try { LISTENERS[i].detach(); n++; } catch (e) { }
}
LISTENERS = [];
try { flushLog(); } catch (e) { }
try { sendlog('BALLOON SHUTDOWN: disabled + detached ' + n + ' listener(s)'); } catch (e) { }
return JSON.stringify({ ok: true, detached: n, faked: FAKED, miss: MISS });
};
rpc.exports.bfReloadPush = function () { PUSH_T = 0; loadPush(); return PUSH_N; };
});
})();