// 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 \n#include \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(""))?.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(""); 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 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 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 // +0x54 BlobArray 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`:`k__BackingField` off=**16**(u32), // `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; }; }); })();