// 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"; // ================= 保龄球场景实体探针(bowl_probe) ================= // 独立探针:前置是 frida-il2cpp-bridge bundle(由 _build_bowl_probe.py 拼接), // 所以这里可以直接用 Il2Cpp / Il2Cpp.perform,不需要 gym_agent 的任何东西。 // 目的:回答 P0 —— 保龄球/球瓶的 sceneItem 是「场景里已存在」还是「需服务端生成」。 var BP_CACHE = {}; function bpClass(full) { if (BP_CACHE[full] !== undefined) return BP_CACHE[full]; var res = null; // 快路径:image.class(fullName) 直接查(gym_agent 同款) try { var asms = Il2Cpp.domain.assemblies; for (var i = 0; i < asms.length; i++) { try { var c = asms[i].image.class(full); if (c) { res = c; break; } } catch (e) { } } } catch (e) { } BP_CACHE[full] = res; return res; } function bpStr(v) { if (v === null || v === undefined) return null; try { if (v.isNull && v.isNull()) return null; } catch (e) { } try { var c = v.content; if (typeof c === "string") return c; } catch (e) { } try { return String(v); } catch (e) { } return null; } function bpNum(v) { try { if (v === null || v === undefined) return null; if (typeof v === "number" || typeof v === "bigint") return Number(v); if (typeof v === "boolean") return v; var x = Number(v); if (!isNaN(x)) return x; } catch (e) { } return null; } // ★ 2026-09-24 修复:IL2CPP 里 System.String 的类名是 "System.String"(带命名空间), // 旧代码只判 === "String" ⇒ 判等恒假 ⇒ 所有字符串字段一律落到 ""。 // 后果:paths() 的「按资源名搜」从未生效(命中全靠 BP_BOWL_CFGIDS 白名单兜底), // so 它永远搜不到「以另一个 cfg 存在的球瓶」。此函数统一收口。 // ★★ 2026-09-24 二次修复(致命):上一版写成了 // return bpIsStr(cn) || cn === "System.String" || ... // 第一个操作数就是自己 ⇒ **无限递归** ⇒ RangeError: Maximum call stack size exceeded // ⇒ bpField() 一调用就抛 ⇒ holds() / bowlst() / paths() / blobstat() 全部只返回 // out.err="Maximum call stack size exceeded",一条有效数据都拿不到。 // 正确写法就是下面这行纯字符串比对,**不许再引用自身**。 function bpIsStr(cn) { return cn === "String" || cn === "System.String" || cn === "string"; } // 读字段:字符串类字段自动取 content;枚举自动取数值 function bpField(o, name) { var v; try { v = o.field(name).value; } catch (e) { return ""; } if (v === null || v === undefined) return null; try { if (v.isNull && v.isNull()) return null; } catch (e) { } var cn = ""; try { cn = String(v.class.name); } catch (e) { } if (bpIsStr(cn)) return bpStr(v); if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") return Number(v); try { if (v.class.isEnum) return bpNum(bpEnumNum(v)); } catch (e) { } return ""; } // 枚举对象 → 数值 function bpEnumNum(v) { // frida-il2cpp-bridge:enum 实例的 value 是底层整数 try { var u = v.value; if (typeof u === "number" || typeof u === "bigint") return u; } catch (e) { } try { var f = v.field("value__").value; return f; } catch (e) { } try { return v.method("get_value__").invoke(); } catch (e) { } return null; } var BP_BOWL_MODELIDS = [52007069, 52007070, 52007071, 52007072, 52007073]; var BP_BOWL_CFGIDS = [80002967, 80002968, 80002969, 80002970, 80003145, 80004058]; function bpRpc(name, fn) { return { name: name, fn: fn }; } var BOWL_RPCS = []; // ---------- R1: sceneItem 注册表扫描(决定性) ---------- BOWL_RPCS.push(bpRpc("paths", function (kw, maxOut) { var t0 = Date.now(); var out = { rpc: "paths", kw: String(kw || ""), total: 0, hits: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemPathInfoRuntime"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.total = objs ? objs.length : 0; var kwl = out.kw.toLowerCase(); var limit = parseInt(maxOut || "400", 10); var F = ["path", "pathId", "modelId", "sceneItemConfigId", "interactionType", "sizeType", "destructionType", "isConfigCanMove", "isConfigCanMind"]; for (var i = 0; i < out.total; i++) { var o = objs[i]; var rec = null; try { var p = bpField(o, "path"); var mid = bpField(o, "modelId"); var cid = bpField(o, "sceneItemConfigId"); var pid = bpField(o, "pathId"); var ps = (p === null ? "" : String(p)); var hit = false; if (kwl) { hit = ps.toLowerCase().indexOf(kwl) >= 0; } else { hit = true; } if (!hit && BP_BOWL_MODELIDS.indexOf(mid) >= 0) hit = true; if (!hit && BP_BOWL_CFGIDS.indexOf(cid) >= 0) hit = true; if (hit) { rec = { path: ps, pathId: pid, modelId: mid, sceneItemConfigId: cid }; for (var k = 4; k < F.length; k++) { try { rec[F[k]] = bpField(o, F[k]); } catch (e) { } } } } catch (e) { } if (rec) { out.hits.push(rec); if (out.hits.length >= limit) { out.capped = true; break; } } } out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R2: 枚举真值(数值) ---------- BOWL_RPCS.push(bpRpc("enumv", function (cls) { var out = { rpc: "enumv", cls: String(cls || ""), items: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var flds = K.fields; for (var i = 0; i < flds.length; i++) { var f = flds[i]; try { if (!f.isStatic) continue; if (String(f.name).indexOf("NativeFieldInfoPtr") === 0) continue; if (String(f.name) === "value__") continue; var v = f.value; var n = bpNum(bpEnumNum(v)); if (n === null) n = bpNum(v); out.items.push(String(f.name) + "=" + String(n)); } catch (e) { out.items.push(String(f.name) + "=ERR"); } } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R3: SceneItemMgr 集合/网格状态 ---------- BOWL_RPCS.push(bpRpc("simgrid", function () { var out = { rpc: "simgrid", err: null }; try { var K = bpClass("LX6.Item.SceneItemMgr"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var inst = null; try { inst = K.method("get_Instance").invoke(); } catch (e) { } if (!inst || (inst.isNull && inst.isNull())) { var all = Il2Cpp.gc.choose(K); inst = (all && all.length) ? all[0] : null; } if (!inst) { out.err = "no-instance"; return JSON.stringify(out); } var colls = ["SceneItemInWorld", "SceneItemInAwake", "BallsInstanceIdSet", "BasketballInstanceIdSet", "interactItemIds", "AllSceneItemGrid", "GridIndexInfoDic", "IdToMetro", "waitToAddSceneItemId", "sceneItemOccupyData", "playerOccupyDataMap", "noClearWhiteList"]; out.counts = {}; for (var i = 0; i < colls.length; i++) { var n = colls[i], c = null, kind = ""; var v; try { v = inst.field(n).value; } catch (e) { out.counts[n] = "nofield"; continue; } if (v === null || v === undefined) { out.counts[n] = "null"; continue; } try { if (v.isNull && v.isNull()) { out.counts[n] = "null"; continue; } } catch (e) { } try { c = Number(v.method("get_Count").invoke()); kind = "count"; } catch (e) { } if (c === null || isNaN(c)) { try { c = Number(v.method("get_Length").invoke()); kind = "len"; } catch (e) { } } if (c === null || isNaN(c)) { try { c = Number(v.method("get_Count").invoke()); kind = "count"; } catch (e) { } } out.counts[n] = (c === null || isNaN(c)) ? "" : (kind + ":" + c); } // BallsInstanceIdSet 内容(若有) try { var bs = inst.field("BallsInstanceIdSet").value; if (bs && !(bs.isNull && bs.isNull())) { var cnt = Number(bs.method("get_Count").invoke()); var en = bs.method("GetEnumerator").invoke(); var arr = []; for (var q = 0; q < Math.min(cnt, 64); q++) { if (!en.method("MoveNext").invoke()) break; arr.push(String(en.method("get_Current").invoke())); } out.balls = { count: cnt, sample: arr }; } } catch (e) { out.ballsErr = String(e && e.message ? e.message : e); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R4: 玩家坐标 ---------- BOWL_RPCS.push(bpRpc("pos", function () { var out = { rpc: "pos", err: null }; try { var KU = bpClass("LX6.Game.MyPlayerManager"); if (!KU) { out.err = "no-player-mgr"; return JSON.stringify(out); } var u = KU.method("get_PlayerUnit").invoke(); if (!u || (u.isNull && u.isNull())) { out.err = "no-player-unit"; return JSON.stringify(out); } var v = u.field("lastPosition").value; out.pos = [v.handle.readFloat(), v.handle.add(4).readFloat(), v.handle.add(8).readFloat()]; out.unitId = String(u.field("instanceId").value); out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R6: 活着的 SceneItemHold 实例(决定性:球/瓶在不在世界里) ---------- BOWL_RPCS.push(bpRpc("holds", function (maxOut, kw) { var t0 = Date.now(); var out = { rpc: "holds", count: 0, byCfg: {}, hits: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemHold"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.count = objs ? objs.length : 0; var limit = parseInt(maxOut || "40", 10); var kwl = String(kw || "").toLowerCase(); // 字段名只取一次 var names = []; try { var c = K, guard = 0; while (c && guard++ < 8) { for (var i = 0; i < c.fields.length; i++) { var fn = String(c.fields[i].name); if (fn.indexOf("Native") === 0) continue; if (names.indexOf(fn) < 0) names.push(fn); } c = c.baseType; } } catch (e) { out.fieldErr = String(e && e.message ? e.message : e); } out.fieldCount = names.length; for (var q = 0; q < out.count; q++) { var o = objs[q]; var rec = {}; var goName = null; for (var k = 0; k < names.length; k++) { var nm = names[k]; var val; try { val = bpField(o, nm); } catch (e) { continue; } if (val === "") { // 尝试拿 GameObject 名字 / 配置名 try { var ob = o.field(nm).value; var cn = String(ob.class.name); if (cn === "GameObject" || (ob.class.namespace === "UnityEngine" && cn === "GameObject")) { var g = bpStr(ob.method("get_name").invoke()); rec[nm] = ""; goName = g; } else { var sf = null; try { sf = bpStr(ob.field("name").value); } catch (e2) { } if (sf === null) { try { sf = bpStr(ob.method("get_name").invoke()); } catch (e2) { } } rec[nm] = "<" + cn + (sf ? ":" + sf : "") + ">"; } } catch (e2) { rec[nm] = ""; } } else { rec[nm] = val; } } var blob = JSON.stringify(rec).toLowerCase(); var keep = (kwl === "" ? true : blob.indexOf(kwl) >= 0); // 也按保龄球 cfg id 抓 if (!keep) { for (var z = 0; z < BP_BOWL_CFGIDS.length; z++) { if (blob.indexOf('"' + BP_BOWL_CFGIDS[z] + '"') >= 0) { keep = true; break; } } } if (keep && out.hits.length < limit) { rec.__go = goName; out.hits.push(rec); } // 统计 cfg id try { var cfg = rec["sceneItemConfigId"] !== undefined ? rec["sceneItemConfigId"] : (rec["SceneItemConfigId"] !== undefined ? rec["SceneItemConfigId"] : rec["cfgId"]); if (cfg !== undefined && cfg !== null) { var kk = String(cfg); out.byCfg[kk] = (out.byCfg[kk] || 0) + 1; } } catch (e3) { } } out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R6b: 极轻量 hold 汇总(★ 自动监视专用) ---------- // 与 holds() 的区别:只读 1~2 个字段、不遍历类字段名、不 JSON.stringify 全字段 // ⇒ 可以每 3~5 秒调一次做趋势监视(holds() 全量太贵,只能手动按需调)。 // // ★ 决胜读数:bowlCfg["80003145"].obj = 「场景里真实存在、且带 GameObject 的球瓶 hold 个数」。 // · 交互前 > 0 且交互后变 0 ⇒ 就是 hideSceneItem 干的(SetActive(false)) // · 交互后 obj 也 > 0 ⇒ 瓶还在,问题在渲染/位置而不是 hold // · 全程都是 0/缺失 ⇒ 关卡里真没有球瓶场景物,只能靠 BF1 骗出来 BOWL_RPCS.push(bpRpc("holdsum", function () { var t0 = Date.now(); var out = { rpc: "holdsum", count: 0, withObj: 0, cfgs: 0, bowlCfg: {}, top: [], ms: 0, err: null }; try { var K = bpClass("LX6.Item.SceneItemHold"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.count = objs ? objs.length : 0; var hist = {}; var want = {}; for (var w = 0; w < BP_BOWL_CFGIDS.length; w++) { want[String(BP_BOWL_CFGIDS[w])] = { n: 0, obj: 0, xMin: null, xMax: null, y: null, s: null }; } var CFG_FIELDS = ["sceneItemConfigId", "SceneItemConfigId", "cfgId", "CfgId", "configId", "ConfigId"]; for (var q = 0; q < out.count; q++) { var h = objs[q]; // ---- ① SceneItemObj 是否非空(= hold 有没有真正的 GameObject)---- var hasObj = false; try { var ov = h.field("SceneItemObj").value; hasObj = !(ov === null || ov === undefined); if (hasObj) { try { if (ov.isNull && ov.isNull()) hasObj = false; } catch (e) { } } } catch (e) { try { hasObj = bpFieldTry(h, ["SceneItemObj", "sceneItemObj"]) !== null; } catch (e2) { } } if (hasObj) out.withObj++; // ---- ①b 只对「球瓶/球的 cfg」读一次世界坐标 + 缩放(≤20 个 hold,开销可忽略)---- // 这是回答「瓶到底摆在哪 / 音效为什么偏晚」的核心读数。 var gx = null, gy = null, gs = null; if (hasObj) { var cfgWant = false; for (var w3 = 0; w3 < BP_BOWL_CFGIDS.length; w3++) { try { var cv3 = bpField(h, "sceneItemConfigId"); if (typeof cv3 === "number" && Math.round(cv3) === BP_BOWL_CFGIDS[w3]) { cfgWant = true; } } catch (e) { } if (cfgWant) break; } if (cfgWant) { try { var ov2 = h.field("SceneItemObj").value; if (ov2 && !(ov2.isNull && ov2.isNull())) { var tt = ov2.method("get_transform").invoke(); var pp = tt.method("get_position").invoke(); gx = +Number(pp.field("x").value).toFixed(3); gy = +Number(pp.field("y").value).toFixed(3); var ss = tt.method("get_localScale").invoke(); gs = +Number(ss.field("x").value).toFixed(4); } } catch (e) { } } } // ---- ② cfg id(走与 holds() 相同的 bpField 通道,保证读数一致)---- var cfg = null; for (var ci = 0; ci < CFG_FIELDS.length; ci++) { var cv; try { cv = bpField(h, CFG_FIELDS[ci]); } catch (e) { continue; } if (typeof cv === "number" && isFinite(cv) && cv > 1000) { cfg = Math.round(cv); break; } } var kk = (cfg === null ? "?" : String(cfg)); if (hist[kk] === undefined) hist[kk] = { n: 0, obj: 0 }; hist[kk].n++; if (hasObj) hist[kk].obj++; if (want[kk]) { want[kk].n++; if (hasObj) want[kk].obj++; if (gx !== null) { if (want[kk].xMin === null || gx < want[kk].xMin) want[kk].xMin = gx; if (want[kk].xMax === null || gx > want[kk].xMax) want[kk].xMax = gx; want[kk].y = gy; want[kk].s = gs; } } } out.bowlCfg = want; for (var key in hist) { out.top.push([key, hist[key].n, hist[key].obj]); } out.cfgs = out.top.length; out.top.sort(function (a, b) { return b[1] - a[1]; }); out.top = out.top.slice(0, 12); out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R6b2: ★★ scale 读数对照实验(判断读出来的 [0,0,1] 是真值还是读法有误)---------- // 动机:holdpos 读出「球瓶 SceneItemObj 的 localScale = (0,0,1)」—— x/y 为 0 会让模型 // 退化到画不出来,正好解释「有碰撞有音效但看不见」。但必须排除**读法**的问题。 // 做法:拿一批「确定带 MeshRenderer」的 GameObject(一定在画面上)做对照样本, // 读 localScale + lossyScale。若它们也返回 (0,0,1),就是读法坏,不是瓶子坏。 BOWL_RPCS.push(bpRpc("ctrlscale", function (n) { var t0 = Date.now(); var out = { rpc: "ctrlscale", samples: [], dist: {}, err: null }; try { var lim = parseInt(n || "24", 10); var R = bpClass("UnityEngine.MeshRenderer"); if (!R) { out.err = "no-meshrenderer"; return JSON.stringify(out); } var rs = Il2Cpp.gc.choose(R); out.renderers = rs ? rs.length : 0; if (!rs || !rs.length) { out.err = "no-renderer-instances"; return JSON.stringify(out); } var step = Math.max(1, Math.floor(rs.length / lim)); function v3(v) { try { return [+Number(v.field("x").value).toFixed(4), +Number(v.field("y").value).toFixed(4), +Number(v.field("z").value).toFixed(4)]; } catch (e) { return null; } } for (var i = 0; i < rs.length && out.samples.length < lim; i += step) { var rec = {}; try { var go = rs[i].method("get_gameObject").invoke(); try { rec.name = String(bpStr(go.method("get_name").invoke())); } catch (e) { } var tt = go.method("get_transform").invoke(); rec.local = v3(tt.method("get_localScale").invoke()); rec.lossy = v3(tt.method("get_lossyScale").invoke()); rec.pos = v3(tt.method("get_position").invoke()); } catch (e) { rec.err = String(e && e.message ? e.message : e).slice(0, 80); } out.samples.push(rec); var k = JSON.stringify(rec.local); out.dist[k] = (out.dist[k] || 0) + 1; } out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R6c: ★★ 生成位置 + 可见性 实测(回答「交互后瓶生成在哪 / 为什么看不见」)---------- // 背景:用户观测到「听得到击瓶音效(有碰撞)但看不见瓶,且音效明显偏晚」。 // 本 RPC 一次给三组硬读数: // ① objPos —— hold.SceneItemObj 的**世界坐标**(= 物理/碰撞实际生效的位置) // ② subR/subN—— SceneItemObj 子树里 Renderer 的个数 / 节点总数 // subR = 0 ⇒ GameObject 是空壳 ⇒ 「有碰撞没画面」的直接原因 // ③ actSelf —— 是否被 SetActive(false)(hideSceneItem 干的) // 再配合 bowlst("pin") 量「旁边道看得见的瓶」的世界坐标 ⇒ 两者 X 一减就是「音效偏晚」的米数。 BOWL_RPCS.push(bpRpc("holdpos", function (maxOut) { var t0 = Date.now(); var out = { rpc: "holdpos", holdCount: 0, rows: [], agg: {}, err: null }; var limit = parseInt(maxOut || "60", 10); try { var K = bpClass("LX6.Item.SceneItemHold"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } // ---- 先建「哪些 GameObject 上有 Renderer」的指针集合(同 bowlst 的做法)---- var hasR = {}; var RSETS = ["UnityEngine.MeshRenderer", "UnityEngine.SkinnedMeshRenderer"]; for (var q = 0; q < RSETS.length; q++) { var RC = null; try { RC = bpClass(RSETS[q]); } catch (e) { } if (!RC) continue; var rs = null; try { rs = Il2Cpp.gc.choose(RC); } catch (e) { continue; } if (!rs) continue; for (var z = 0; z < rs.length; z++) { try { var g = rs[z].method("get_gameObject").invoke(); if (g && g.handle && !g.handle.isNull()) hasR[g.handle.toString()] = 1; } catch (e) { } } } out.rendererGos = Object.keys(hasR).length; function goName(g) { try { return String(bpStr(g.method("get_name").invoke())); } catch (e) { return "?"; } } function actOf(g, m) { try { return String(g.method(m).invoke()); } catch (e) { return "?"; } } var hs = Il2Cpp.gc.choose(K); out.holdCount = hs ? hs.length : 0; var CFG_FIELDS = ["sceneItemConfigId", "SceneItemConfigId", "cfgId", "CfgId", "configId", "ConfigId"]; var want = {}; for (var w = 0; w < BP_BOWL_CFGIDS.length; w++) want[BP_BOWL_CFGIDS[w]] = { n: 0, objOk: 0, subR: 0, active: 0, xMin: null, xMax: null }; for (var i = 0; i < out.holdCount && out.rows.length < limit; i++) { var cfg = null; for (var ci = 0; ci < CFG_FIELDS.length; ci++) { try { var cv = bpField(hs[i], CFG_FIELDS[ci]); if (typeof cv === "number" && isFinite(cv) && cv > 1000) { cfg = Math.round(cv); break; } } catch (e) { } } if (cfg === null || !want[cfg]) continue; var r = { cfg: cfg }; try { r.id = String(bpField(hs[i], "instanceId")); } catch (e) { } var ov = null; try { ov = hs[i].field("SceneItemObj").value; } catch (e) { } var objOk = !(ov === null || ov === undefined); if (objOk) { try { if (ov.isNull && ov.isNull()) objOk = false; } catch (e) { } } r.objOk = objOk; want[cfg].n++; if (objOk) { want[cfg].objOk++; r.name = goName(ov); r.actSelf = actOf(ov, "get_activeSelf"); r.actHier = actOf(ov, "get_activeInHierarchy"); // ★ 2026-09-24:这里原来写 `r.actSelf === "True"`,而 actOf 返回的是**小写** "true" // ⇒ active 恒为 0,和旁边 actSelf 分布打印出 {'true':10} 自相矛盾,误导判读。 if (String(r.actSelf).toLowerCase() === "true") want[cfg].active++; try { var t = ov.method("get_transform").invoke(); var p = t.method("get_position").invoke(); r.pos = [+Number(p.field("x").value).toFixed(3), +Number(p.field("y").value).toFixed(3), +Number(p.field("z").value).toFixed(3)]; var ls = t.method("get_localScale").invoke(); r.scale = [+Number(ls.field("x").value).toFixed(4), +Number(ls.field("y").value).toFixed(4), +Number(ls.field("z").value).toFixed(4)]; // ★ 加 lossyScale(不含父级缩放的对照)+ 父链,便于判断 scale=0 是真值还是读法问题 try { var ly = t.method("get_lossyScale").invoke(); r.lossy = [+Number(ly.field("x").value).toFixed(4), +Number(ly.field("y").value).toFixed(4), +Number(ly.field("z").value).toFixed(4)]; } catch (e) { } try { var chain = [], cur = t, gch = 0; while (cur && cur.handle && !cur.handle.isNull() && gch++ < 6) { chain.push(String(bpStr(cur.method("get_gameObject").invoke() .method("get_name").invoke()))); var pt = cur.method("get_parent").invoke(); if (!pt || !pt.handle || pt.handle.isNull()) break; cur = pt; } r.path = chain.reverse().join("/"); } catch (e) { } // ★★ 父级 lossy(最多上溯 3 层):用来区分两种可能 —— // (a) 我们写进 placeInfo 的缩放是 0 ⇒ 本级 local 就是 0; // (b) 容器(SceneItemInstanceRoot / DefaultSceneItem)被压成 0 ⇒ 本级 local=1 但 lossy=0。 try { var pcur = t, pch2 = 0, lply = []; while (pcur && pcur.handle && !pcur.handle.isNull() && pch2++ < 3) { var pyt = pcur.method("get_parent").invoke(); if (!pyt || !pyt.handle || pyt.handle.isNull()) break; var ly2 = pyt.method("get_lossyScale").invoke(); lply.push([+Number(ly2.field("x").value).toFixed(4), +Number(ly2.field("y").value).toFixed(4), +Number(ly2.field("z").value).toFixed(4)]); pcur = pyt; } r.lossyP = lply; } catch (e) { } // 子树:数节点 + 数 Renderer var nSub = 0, nR = 0, firstR = null; var q2 = [{ t: t, d: 0 }]; while (q2.length && nSub < 300) { var it2 = q2.shift(); nSub++; try { var gk = it2.t.method("get_gameObject").invoke(); if (gk && gk.handle && hasR[gk.handle.toString()]) { nR++; if (firstR === null) firstR = goName(gk); } } catch (e) { } if (it2.d < 5) { try { var cc = Number(it2.t.method("get_childCount").invoke()); for (var k2 = 0; k2 < cc; k2++) { try { q2.push({ t: it2.t.method("GetChild").invoke(k2), d: it2.d + 1 }); } catch (e) { } } } catch (e) { } } } r.subN = nSub; r.subR = nR; r.subRName = firstR; if (nR > 0) want[cfg].subR++; if (r.pos) { var A = want[cfg]; if (A.xMin === null || r.pos[0] < A.xMin) A.xMin = r.pos[0]; if (A.xMax === null || r.pos[0] > A.xMax) A.xMax = r.pos[0]; } } catch (e) { r.tErr = String(e && e.message ? e.message : e); } } out.rows.push(r); } // 排序:按 X 升序(道头 → 瓶台) out.rows.sort(function (a, b) { var ax = (a.pos ? a.pos[0] : 1e9), bx = (b.pos ? b.pos[0] : 1e9); return ax - bx; }); out.agg = want; out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R6d: ★★ 锚点子树实测(回答「看得见的瓶到底在哪」)---------- // 背景(服务端 GameRouter.GameGround.cs 第148~163行的实测记录): // · 官方场景里有锚点 GameObject,名字形如 BowlingPinPlacementL / R、BowlingGameNode、 // BowlingGameStart、MiniGameMachine_Bowling —— 共 4 台机器、每台 2 条道 = 8 条道。 // · 服务端 BowlingLanes[] 的 PinApexX 就是抄自 BowlingPinPlacement 的世界坐标。 // · **看得见的那些瓶网格 = BowlingPinPlacement 的子节点**(名字里未必有 "pin")。 // ⇒ 想回答「瓶该在哪 / 现在在哪 / 差多少米」,必须把锚点的**整棵子树连世界坐标**倒出来。 // 用法:kids("BowlingPinPlacement", 8, 4, 80) BOWL_RPCS.push(bpRpc("kids", function (kw, maxRoots, maxDepth, maxKids) { var t0 = Date.now(); var out = { rpc: "kids", kw: String(kw || ""), goTotal: 0, roots: [], err: null }; try { var kwl = out.kw.toLowerCase(); if (!kwl) { out.err = "need-keyword"; return JSON.stringify(out); } var G = bpClass("UnityEngine.GameObject"); if (!G) { out.err = "no-gameobject-class"; return JSON.stringify(out); } // 先建「有 Renderer 的 GameObject」指针集合,用来判断哪些节点是看得见的 var hasR = {}; var RSETS = ["UnityEngine.MeshRenderer", "UnityEngine.SkinnedMeshRenderer"]; for (var q = 0; q < RSETS.length; q++) { var RC = null; try { RC = bpClass(RSETS[q]); } catch (e) { } if (!RC) continue; var rs = null; try { rs = Il2Cpp.gc.choose(RC); } catch (e) { continue; } if (!rs) continue; for (var z = 0; z < rs.length; z++) { try { var g = rs[z].method("get_gameObject").invoke(); if (g && g.handle && !g.handle.isNull()) hasR[g.handle.toString()] = 1; } catch (e) { } } } out.rendererGos = Object.keys(hasR).length; function nm(g) { try { return String(bpStr(g.method("get_name").invoke())); } catch (e) { return "?"; } } function wpos(t) { try { var p = t.method("get_position").invoke(); return [+Number(p.field("x").value).toFixed(3), +Number(p.field("y").value).toFixed(3), +Number(p.field("z").value).toFixed(3)]; } catch (e) { return null; } } function hasRen(g) { try { return !!(g.handle && hasR[g.handle.toString()]); } catch (e) { return false; } } function act(g, m) { try { return String(g.method(m).invoke()); } catch (e) { return "?"; } } // ★★ 关键改动(2026-09-24 08:17 事故后):**不再用 Il2Cpp.gc.choose(GameObject)**。 // 那是"遍历整个 GC 堆",是全部探针里最贵的一类调用(现场就是这一族把游戏拖住 30 秒)。 // 改成从 SceneManager 的活动场景根节点做 BFS:节点数带硬上限、不碰 GC 堆、 // 而且场景里的对象本来就都在根节点的子树里 ⇒ 覆盖率等价。 var sm = bpClass("UnityEngine.SceneManagement.SceneManager"); if (!sm) { out.err = "no-scenemanager"; return JSON.stringify(out); } var sc = sm.method("GetActiveScene").invoke(); try { out.scene = String(bpStr(sc.method("get_name").invoke())); } catch (e) { } var rts = sc.method("GetRootGameObjects").invoke(); var rc = 0; try { rc = Number(rts.method("get_Length").invoke()); } catch (e) { } out.rootCount = rc; var wantRoots = parseInt(maxRoots || "8", 10); var wantDepth = parseInt(maxDepth || "4", 10); var wantKids = parseInt(maxKids || "80", 10); var maxNodes = 120000; var bfs = []; for (var i0 = 0; i0 < rc; i0++) { try { bfs.push({ t: rts.method("get_Item").invoke(i0).method("get_transform").invoke(), d: 0 }); } catch (e) { } } var visited = 0; while (bfs.length && visited < maxNodes && out.roots.length < wantRoots) { var node = bfs.shift(); visited++; var g1 = null, name = "?"; try { g1 = node.t.method("get_gameObject").invoke(); name = nm(g1); } catch (e) { } if (String(name).toLowerCase().indexOf(kwl) >= 0) { var r = { name: name, actSelf: act(g1, "get_activeSelf"), actHier: act(g1, "get_activeInHierarchy") }; try { var rt = node.t; r.pos = wpos(rt); r.hasR = hasRen(g1); var sc3 = rt.method("get_localScale").invoke(); r.scale = [+Number(sc3.field("x").value).toFixed(4), +Number(sc3.field("y").value).toFixed(4), +Number(sc3.field("z").value).toFixed(4)]; // 子树 BFS:连世界坐标一起给 var kids = []; var q2 = []; var cc0 = 0; try { cc0 = Number(rt.method("get_childCount").invoke()); } catch (e) { } for (var c0 = 0; c0 < cc0; c0++) { try { q2.push({ t: rt.method("GetChild").invoke(c0), d: 1 }); } catch (e) { } } while (q2.length && kids.length < wantKids) { var it2 = q2.shift(); var k = { d: it2.d }; try { var g2 = it2.t.method("get_gameObject").invoke(); k.name = nm(g2); k.hasR = hasRen(g2); k.actSelf = act(g2, "get_activeSelf"); k.actHier = act(g2, "get_activeInHierarchy"); } catch (e) { } k.pos = wpos(it2.t); kids.push(k); if (it2.d < wantDepth) { var cn = 0; try { cn = Number(it2.t.method("get_childCount").invoke()); } catch (e) { } for (var k2 = 0; k2 < cn; k2++) { try { q2.push({ t: it2.t.method("GetChild").invoke(k2), d: it2.d + 1 }); } catch (e) { } } } } r.kidN = kids.length; r.kids = kids; } catch (e) { r.tErr = String(e && e.message ? e.message : e); } out.roots.push(r); } if (node.d < wantDepth) { var cc1 = 0; try { cc1 = Number(node.t.method("get_childCount").invoke()); } catch (e) { } for (var c1 = 0; c1 < cc1; c1++) { try { bfs.push({ t: node.t.method("GetChild").invoke(c1), d: node.d + 1 }); } catch (e) { } } } } out.visited = visited; // 按 X 排序(道头 → 瓶台),便于和 PinApexX 直接比 out.roots.sort(function (a, b) { var ax = (a.pos ? a.pos[0] : 1e9), bx = (b.pos ? b.pos[0] : 1e9); return ax - bx; }); out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R7: 枚举所有活着的 GameObject(名字搜索,兜底用) ---------- BOWL_RPCS.push(bpRpc("gobjs", function (kw, maxOut) { var t0 = Date.now(); var out = { rpc: "gobjs", kw: String(kw || ""), count: 0, hits: [], err: null }; try { var K = bpClass("UnityEngine.GameObject"); if (!K) { out.err = "no-gameobject-class"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.count = objs ? objs.length : 0; var kwl = out.kw.toLowerCase(); var limit = parseInt(maxOut || "100", 10); for (var i = 0; i < out.count; i++) { var go = objs[i]; var nm = null; try { nm = bpStr(go.method("get_name").invoke()); } catch (e) { continue; } if (nm === null) continue; if (kwl && String(nm).toLowerCase().indexOf(kwl) < 0) continue; var p3 = null; try { var t = go.method("get_transform").invoke(); if (t && !(t.isNull && t.isNull())) { var wp = t.method("get_position").invoke(); p3 = [wp.field("x").value, wp.field("y").value, wp.field("z").value]; } } catch (e) { } out.hits.push({ name: nm, pos: p3 }); if (out.hits.length >= limit) { out.capped = true; break; } } out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R5: 场景 GameObject 名称搜索 ---------- BOWL_RPCS.push(bpRpc("objs", function (kw, maxNodes, maxDepth) { var out = { rpc: "objs", kw: String(kw || ""), visited: 0, hits: [], err: null }; try { kw = out.kw.toLowerCase(); maxNodes = parseInt(maxNodes || "8000", 10); maxDepth = parseInt(maxDepth || "6", 10); var sm = bpClass("UnityEngine.SceneManagement.SceneManager"); if (!sm) { out.err = "no-scenemanager"; return JSON.stringify(out); } var sc = sm.method("GetActiveScene").invoke(); out.scene = bpStr(sc.method("get_name").invoke()); var roots = sc.method("GetRootGameObjects").invoke(); var rc = 0; try { rc = Number(roots.method("get_Length").invoke()); } catch (e) { } out.rootCount = rc; var queue = []; for (var i = 0; i < rc; i++) { try { queue.push({ go: roots.method("get_Item").invoke(i), depth: 0, path: "?" }); } catch (e) { } } var visited = 0; while (queue.length && visited < maxNodes) { var it = queue.shift(); visited++; var nm = "?"; try { nm = bpStr(it.go.method("get_name").invoke()); } catch (e) { } var path = it.path === "?" ? nm : (it.path + "/" + nm); if (kw && String(nm).toLowerCase().indexOf(kw) >= 0) { var w = null; try { var t = it.go.method("get_transform").invoke(); var wp = t.method("get_position").invoke(); w = [wp.field("x").value, wp.field("y").value, wp.field("z").value]; } catch (e) { } out.hits.push({ path: path, pos: w, active: String(it.go.method("get_activeSelf").invoke()) }); if (out.hits.length >= 200) break; } if (it.depth < maxDepth) { var ch = null; try { ch = it.go.method("get_transform").invoke().method("get_childCount").invoke(); } catch (e) { } if (ch !== null) { for (var c = 0; c < Number(ch); c++) { try { queue.push({ go: it.go.method("get_transform").invoke().method("GetChild").invoke(c), depth: it.depth + 1, path: path }); } catch (e) { } } } } } out.visited = visited; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R9: 保龄球专项状态(★ 只读):名字 + active + 父链 + 组件 + 缩放 ---------- // 用途:回答「交互前看得见的瓶,交互后去哪了」—— // · 若它们还在 gc 里、只是 actSelf/actHier = false ⇒ 被 SetActive(false) 藏了(可逆!) // · 若 hasRenderer = false ⇒ 被换成了空壳 // · 若连对象都没了 ⇒ 被 Destroy 了 BOWL_RPCS.push(bpRpc("bowlst", function (kw, maxOut, wantComps) { var t0 = Date.now(); var out = { rpc: "bowlst", kw: String(kw || ""), total: 0, hits: [], err: null }; try { var K = bpClass("UnityEngine.GameObject"); if (!K) { out.err = "no-gameobject-class"; return JSON.stringify(out); } // ★ wantComps:默认只建 Renderer 两个集合("r")。 // 以前无条件建 7 个集合(4 种 Collider + Rigidbody),在 2026-09-24 08:17 现场 // 正是它把游戏拖住 ~30 秒。要 collider/rigidbody 就显式传 "rc" 或 "all"。 var wc = String(wantComps || "r").toLowerCase(); out.want = wc; var hasR = {}, hasC = {}, hasRb = {}; var sets = [["UnityEngine.MeshRenderer", hasR], ["UnityEngine.SkinnedMeshRenderer", hasR]]; if (wc === "rc" || wc === "all") { sets.push(["UnityEngine.BoxCollider", hasC], ["UnityEngine.SphereCollider", hasC], ["UnityEngine.CapsuleCollider", hasC], ["UnityEngine.MeshCollider", hasC]); } if (wc === "all") sets.push(["UnityEngine.Rigidbody", hasRb]); for (var q = 0; q < sets.length; q++) { var CC = null; try { CC = bpClass(sets[q][0]); } catch (e) { } if (!CC) continue; var cs = null; try { cs = Il2Cpp.gc.choose(CC); } catch (e) { continue; } if (!cs) continue; for (var z = 0; z < cs.length; z++) { try { var g = cs[z].method("get_gameObject").invoke(); if (g && g.handle && !g.handle.isNull()) sets[q][1][g.handle.toString()] = 1; } catch (e) { } } } out.rC = Object.keys(hasR).length; out.cC = Object.keys(hasC).length; out.rbC = Object.keys(hasRb).length; var objs = Il2Cpp.gc.choose(K); out.total = objs ? objs.length : 0; var kwl = out.kw.toLowerCase(); var limit = parseInt(maxOut || "300", 10); for (var i = 0; i < out.total; i++) { var go = objs[i]; var nm = null; try { nm = bpStr(go.method("get_name").invoke()); } catch (e) { continue; } if (nm === null || nm === undefined) continue; nm = String(nm); if (kwl && nm.toLowerCase().indexOf(kwl) < 0) continue; var r = { name: nm }; var h = null; try { h = go.handle.toString(); } catch (e) { } r.hasRenderer = !!(h && hasR[h]); r.hasCollider = !!(h && hasC[h]); r.hasRigidbody = !!(h && hasRb[h]); try { r.actSelf = String(go.method("get_activeSelf").invoke()); } catch (e) { } try { r.actHier = String(go.method("get_activeInHierarchy").invoke()); } catch (e) { } try { r.layer = Number(go.method("get_layer").invoke()); } catch (e) { } try { var t = go.method("get_transform").invoke(); if (t && t.handle && !t.handle.isNull()) { try { var wp = t.method("get_position").invoke(); r.pos = [+Number(wp.field("x").value).toFixed(3), +Number(wp.field("y").value).toFixed(3), +Number(wp.field("z").value).toFixed(3)]; } catch (e) { } try { var ls = t.method("get_localScale").invoke(); r.scale = [+Number(ls.field("x").value).toFixed(4), +Number(ls.field("y").value).toFixed(4), +Number(ls.field("z").value).toFixed(4)]; } catch (e) { } try { r.child = Number(t.method("get_childCount").invoke()); } catch (e) { } try { var chain = [], cur = t, guard = 0; while (cur && cur.handle && !cur.handle.isNull() && guard++ < 14) { try { chain.push(String(bpStr(cur.method("get_gameObject").invoke() .method("get_name").invoke()))); } catch (e) { chain.push("?"); } try { var pt = cur.method("get_parent").invoke(); if (!pt || !pt.handle || pt.handle.isNull()) break; cur = pt; } catch (e) { break; } } r.path = chain.reverse().join("/"); } catch (e) { } } } catch (e) { r.tErr = String(e); } out.hits.push(r); if (out.hits.length >= limit) { out.capped = true; break; } } out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R8: 类字段/静态值 ---------- BOWL_RPCS.push(bpRpc("clsfields", function (cn) { var out = { rpc: "clsfields", cls: String(cn || ""), fields: [], methods: [], bases: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var c = K, guard = 0; while (c && guard++ < 8) { var own = []; try { for (var i = 0; i < c.fields.length; i++) { var f = c.fields[i]; var tn = "?"; try { tn = String(f.type.name); } catch (e) { } own.push(String(f.name) + ":" + tn + (f.isStatic ? " (static)" : "")); } } catch (e) { own.push("ERRFIELD"); } out.fields.push({ cls: String(c.fullName), fields: own }); out.bases.push(String(c.fullName)); try { if (guard === 1) { for (var m = 0; m < c.methods.length; m++) { var mm = c.methods[m]; var ps = []; try { for (var p = 0; p < mm.parameters.length; p++) ps.push(String(mm.parameters[p].type.name)); } catch (e) { } out.methods.push(String(mm.name) + "(" + ps.join(",") + ")"); } } } catch (e) { } try { c = c.baseType; } catch (e) { c = null; } } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R9: SceneItemBlobGrid 网格清单(决定性:网格坐标系 + 覆盖范围) ---------- BOWL_RPCS.push(bpRpc("grids", function (samples) { var t0 = Date.now(); var out = { rpc: "grids", count: 0, xs: [], zs: [], pairs: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemBlobGrid"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.count = objs ? objs.length : 0; var sx = {}, sz = {}; for (var i = 0; i < out.count; i++) { try { var x = Number(objs[i].field("indexX").value); var z = Number(objs[i].field("indexZ").value); if (!isNaN(x)) sx[x] = (sx[x] || 0) + 1; if (!isNaN(z)) sz[z] = (sz[z] || 0) + 1; } catch (e) { } } out.xs = Object.keys(sx).map(Number).sort(function (a, b) { return a - b; }); out.zs = Object.keys(sz).map(Number).sort(function (a, b) { return a - b; }); out.xRange = out.xs.length ? [out.xs[0], out.xs[out.xs.length - 1]] : null; out.zRange = out.zs.length ? [out.zs[0], out.zs[out.zs.length - 1]] : null; out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R10: 指定网格的完整字段(找出里面装了什么) ---------- BOWL_RPCS.push(bpRpc("griddump", function (ix, iz) { var out = { rpc: "griddump", want: [Number(ix), Number(iz)], found: 0, rows: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemBlobGrid"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); var names = []; var c = K, guard = 0; while (c && guard++ < 8) { for (var i = 0; i < c.fields.length; i++) { var fn = String(c.fields[i].name); if (fn.indexOf("Native") === 0) continue; if (names.indexOf(fn) < 0) names.push(fn); } c = c.baseType; } out.fieldNames = names; for (var q = 0; q < (objs ? objs.length : 0); q++) { var o = objs[q]; var x, z; try { x = Number(o.field("indexX").value); z = Number(o.field("indexZ").value); } catch (e) { continue; } if (x !== out.want[0] || z !== out.want[1]) continue; out.found++; if (out.rows.length >= 3) continue; var rec = {}; for (var k = 0; k < names.length; k++) { var nm = names[k]; var v; try { v = o.field(nm).value; } catch (e) { rec[nm] = ""; continue; } if (v === null || v === undefined) { rec[nm] = null; continue; } try { if (v.isNull && v.isNull()) { rec[nm] = null; continue; } } catch (e) { } var cn = ""; try { cn = String(v.class.name); } catch (e) { } if (bpIsStr(cn)) { rec[nm] = bpStr(v); continue; } if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") { rec[nm] = Number(v); continue; } if (cn === "Int32" || cn === "UInt32" || cn === "Single") { try { rec[nm] = Number(v.method("ToString").invoke().content); } catch (e) { rec[nm] = ""; } continue; } var cnt = null; try { cnt = Number(v.method("get_Count").invoke()); } catch (e) { } if (cnt === null) { try { cnt = Number(v.method("get_Length").invoke()); } catch (e) { } } rec[nm] = cnt === null ? ("<" + cn + ">") : ("<" + cn + ":" + cnt + ">"); } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R11: SceneItemBlob 静态状态 ---------- BOWL_RPCS.push(bpRpc("blobstat", function () { var out = { rpc: "blobstat", cls: {}, err: null }; try { var K = bpClass("LX6.Item.SceneItemBlob"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } out.name = String(K.fullName); out.fields = []; var c = K, guard = 0; while (c && guard++ < 6) { for (var i = 0; i < c.fields.length; i++) { var f = c.fields[i]; var nm = String(f.name); if (nm.indexOf("Native") === 0) continue; var tn = "?"; try { tn = String(f.type.name); } catch (e) { } var val = ""; try { var v = f.value; if (v === null || v === undefined) val = "null"; else if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") val = String(v); else { var cn = ""; try { cn = String(v.class.name); } catch (e) { } if (bpIsStr(cn)) val = String(bpStr(v)); else { var cnt = null; try { cnt = Number(v.method("get_Count").invoke()); } catch (e) { } if (cnt === null) { try { cnt = Number(v.method("get_Length").invoke()); } catch (e) { } } val = cnt === null ? ("<" + cn + ">") : ("<" + cn + ":" + cnt + ">"); } } } catch (e) { val = ""; } out.fields.push(nm + ":" + tn + (f.isStatic ? " static" : "") + " = " + val); } try { c = c.baseType; } catch (e) { c = null; } } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R12: 注册表 path 字符串(找出真实 prefab 名,用来搜 GameObject) ---------- BOWL_RPCS.push(bpRpc("pathstr", function (kw, maxOut) { var t0 = Date.now(); var out = { rpc: "pathstr", kw: String(kw || ""), total: 0, rows: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemPathInfoRuntime"); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var objs = Il2Cpp.gc.choose(K); out.total = objs ? objs.length : 0; var kwl = out.kw.toLowerCase(); var limit = parseInt(maxOut || "40", 10); var strategies = []; for (var i = 0; i < out.total && out.rows.length < limit; i++) { var o = objs[i]; var cfg, mid, raw = null, how = ""; try { cfg = Number(o.field("sceneItemConfigId").value); } catch (e) { continue; } try { mid = Number(o.field("modelId").value); } catch (e) { } // 多策略取 path 字符串 try { var v = o.field("path").value; var cn = ""; try { cn = String(v.class.name); } catch (e) { } var cand = [ ["content", function () { return v.content; }], ["ToString", function () { return v.method("ToString").invoke().content; }], ["str", function () { return String(v); }], ["field_value", function () { return bpStr(v.field("value").value); }], ["f_name", function () { return bpStr(v.field("name").value); }], ["f_path", function () { return bpStr(v.field("path").value); }], ["f_str", function () { return bpStr(v.field("str").value); }] ]; for (var s = 0; s < cand.length; s++) { try { var r = cand[s][1](); if (typeof r === "string" && r.length && r !== "") { raw = r; how = cand[s][0]; break; } } catch (e) { } } if (raw === null) { if (strategies.indexOf(cn) < 0) strategies.push(cn); raw = ""; how = "none"; } } catch (e) { raw = ""; how = "err"; } var blob = String(raw).toLowerCase(); var keep = !kwl || blob.indexOf(kwl) >= 0 || BP_BOWL_CFGIDS.indexOf(cfg) >= 0; if (keep) out.rows.push({ cfg: cfg, modelId: mid, path: raw, how: how }); } out.objClasses = strategies; out.ms = Date.now() - t0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R13: 读类静态字段的「值」(找 blob 文件路径 / 注册表大小) ---------- BOWL_RPCS.push(bpRpc("statv", function (cn, nameFilter) { var out = { rpc: "statv", cls: String(cn || ""), rows: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var nf = String(nameFilter || "").toLowerCase(); var c = K, guard = 0; while (c && guard++ < 8) { var flds = []; try { flds = c.fields; } catch (e) { } for (var i = 0; i < flds.length; i++) { var f = flds[i]; var nm = String(f.name); if (!f.isStatic) continue; if (nm.indexOf("NativeFieldInfoPtr") === 0) continue; if (nf && nm.toLowerCase().indexOf(nf) < 0) continue; var tn = "?"; try { tn = String(f.type.name); } catch (e) { } var val = ""; try { var v = f.value; if (v === null || v === undefined) val = "null"; else if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") val = String(v); else { var cn2 = ""; try { cn2 = String(v.class.name); } catch (e) { } if (bpIsStr(cn2)) val = "STR:" + String(bpStr(v)); else { var cnt = null; try { cnt = Number(v.method("get_Count").invoke()); } catch (e) { } if (cnt === null || isNaN(cnt)) { try { cnt = Number(v.method("get_Length").invoke()); } catch (e) { } } val = (cnt === null || isNaN(cnt)) ? ("<" + cn2 + ">") : ("<" + cn2 + ":" + cnt + ">"); } } } catch (e) { val = ""; } out.rows.push({ cls: String(c.fullName), field: nm, type: tn, value: val }); } try { c = c.baseType; } catch (e) { c = null; } } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R14: 泛型/嵌套类型的字段清单(SceneItemPlaceInfoData 等) ---------- BOWL_RPCS.push(bpRpc("placefields", function () { var out = { rpc: "placefields", classes: [], err: null }; var names = ["LX6.Item.SceneItemPlaceInfoData", "LX6.Item.ChunkBlobData`1", "LX6.Item.SceneItemBlobHeadInfo", "LX6.Item.DestructionType", "LX6.Item.SceneItemSizeType", "LX6.Item.GridItemType"]; for (var n = 0; n < names.length; n++) { var rec = { cls: names[n], found: false, fields: [], methods: [] }; try { var K = bpClass(names[n]); if (K) { rec.found = true; rec.real = String(K.fullName); var c = K, guard = 0; while (c && guard++ < 4) { try { for (var i = 0; i < c.fields.length; i++) { var f = c.fields[i]; var tn = "?"; try { tn = String(f.type.name); } catch (e) { } rec.fields.push(String(c.fullName) + "." + String(f.name) + ":" + tn); } } catch (e) { } try { c = c.baseType; } catch (e) { c = null; } } } } catch (e) { rec.err = String(e && e.message ? e.message : e); } out.classes.push(rec); } out.ok = true; return JSON.stringify(out); })); // ---------- R15: 枚举 NativeHashMap / Dictionary 的键值(SceneItemChunkMap / placeIdToGridSeekId) ---------- BOWL_RPCS.push(bpRpc("mapdump", function (cn, fieldName, maxOut) { var out = { rpc: "mapdump", cls: String(cn || ""), field: String(fieldName || ""), count: null, rows: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "class-not-found"; return JSON.stringify(out); } var v = null; try { v = K.field(out.field).value; } catch (e) { out.err = "no-field"; return JSON.stringify(out); } if (v === null || v === undefined) { out.err = "null"; return JSON.stringify(out); } try { if (v.isNull && v.isNull()) { out.err = "null-inst"; return JSON.stringify(out); } } catch (e) { } try { out.count = Number(v.method("get_Count").invoke()); } catch (e) { try { out.count = Number(v.method("get_Length").invoke()); } catch (e2) { } } var lim = parseInt(maxOut || "60", 10); // 尝试多种枚举姿势 var tried = []; function tryIt(tag, fn) { try { var r = fn(); tried.push(tag + "=OK(" + r + ")"); return true; } catch (e) { tried.push(tag + "=" + String(e && e.message ? e.message : e).slice(0, 60)); return false; } } // 姿势1: GetEnumerator tryIt("GetEnumerator", function () { var en = v.method("GetEnumerator").invoke(); var n = 0; while (n < lim && en.method("MoveNext").invoke()) { var kv = en.method("get_Current").invoke(); var ks = "", vs = ""; try { ks = String(bpField(kv, "Key")); } catch (e) { try { ks = String(bpField(kv, "key")); } catch (e2) { } } try { vs = String(bpField(kv, "Value")); } catch (e) { try { vs = String(bpField(kv, "value")); } catch (e2) { } } out.rows.push({ k: ks, v: vs }); n++; } return n; }); out.tried = tried; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R16: Dictionary 内省 + 尝试枚举 placeIdToGridSeekId ---------- BOWL_RPCS.push(bpRpc("placekeys", function (maxOut, fieldName) { var out = { rpc: "placekeys", dictCls: null, dictFields: [], count: null, entriesField: null, arrCls: null, arrLen: null, entryFields: [], rows: [], tried: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemBlob"); if (!K) { out.err = "no-blob"; return JSON.stringify(out); } var d; try { d = K.field(fieldName || "placeIdToGridSeekId").value; } catch (e) { out.err = "no-field"; return JSON.stringify(out); } if (!d) { out.err = "null-dict"; return JSON.stringify(out); } try { out.dictCls = String(d.class.fullName); } catch (e) { } // 1) dict 自身字段名 try { for (var i = 0; i < d.class.fields.length; i++) { var fn = String(d.class.fields[i].name); if (fn.indexOf("Native") === 0) continue; out.dictFields.push(fn); } } catch (e) { } // 2) count try { out.count = Number(d.field("_count").value); } catch (e) { out.tried.push("_count:" + String(e.message).slice(0, 50)); } if (out.count === null) { try { out.count = Number(d.method("get_Count").invoke()); } catch (e) { } } // 3) entries 数组 var arr = null; var cands = ["_entries", "entries"]; for (var c = 0; c < cands.length; c++) { try { arr = d.field(cands[c]).value; if (arr) { out.entriesField = cands[c]; break; } } catch (e) { } } if (!arr) { out.err = "no-entries-array"; return JSON.stringify(out); } try { out.arrCls = String(arr.class.fullName); } catch (e) { } try { out.arrLen = Number(arr.method("get_Length").invoke()); } catch (e) { out.tried.push("arrLen:" + String(e.message).slice(0, 40)); } // 4) 取一个 Entry 看字段 var e0 = null; var getters = ["GetValue", "Get"]; for (var g = 0; g < getters.length && !e0; g++) { try { e0 = arr.method(getters[g]).invoke(0); out.tried.push(getters[g] + "=OK"); } catch (e) { out.tried.push(getters[g] + "=" + String(e.message).slice(0, 50)); } } if (!e0) { out.err = "no-entry-getter"; return JSON.stringify(out); } try { out.entryCls = String(e0.class.fullName); for (var q = 0; q < e0.class.fields.length; q++) { var ef = String(e0.class.fields[q].name); if (ef.indexOf("Native") === 0) continue; out.entryFields.push(ef); } } catch (e) { out.entryCls_err = String(e.message); } // 5) 正式枚举 var lim = parseInt(maxOut || "40", 10); var n = out.count !== null ? out.count : out.arrLen; for (var i = 0; i < n && out.rows.length < lim; i++) { var e = null; for (var g2 = 0; g2 < getters.length && !e; g2++) { try { e = arr.method(getters[g2]).invoke(i); } catch (err) { } } if (!e) continue; var rec = {}; for (var w = 0; w < out.entryFields.length; w++) { var nm = out.entryFields[w]; try { var val = e.field(nm).value; if (val === null || val === undefined) { rec[nm] = null; continue; } var cn = ""; try { cn = String(val.class.name); } catch (err) { } if (typeof val === "number" || typeof val === "bigint" || typeof val === "boolean") { rec[nm] = Number(val); continue; } if (bpIsStr(cn)) { rec[nm] = String(bpStr(val)); continue; } // 值类型/结构:展开一层 var sub = {}; try { for (var z = 0; z < val.class.fields.length && z < 16; z++) { var zf = String(val.class.fields[z].name); if (zf.indexOf("Native") === 0) continue; try { sub[zf] = String(bpField(val, zf)); } catch (err) { } } } catch (err) { } rec[nm] = sub; } catch (err) { rec[nm] = ""; } } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // 内存 hex dump 小工具 function hexOf(ptr, len) { try { var bytes = new Uint8Array(ptr.readByteArray(len)); var s = ""; for (var i = 0; i < bytes.length; i++) { var h = bytes[i].toString(16); if (h.length < 2) h = "0" + h; s += h; } return s; } catch (e) { return "hexx:" + String(e.message || e); } } // 多名字段取读:全部失败返回 null(区别于 bpField 的 "") function bpFieldTry(o, names) { for (var i = 0; i < names.length; i++) { try { var v = o.field(names[i]).value; if (v === null || v === undefined) continue; try { if (v.isNull && v.isNull()) continue; } catch (e) { } return v; } catch (e) { } } return null; } // 把「可能装箱的值/结构」拍平成一个对象 function bpFlatten(v, maxFields) { if (v === null || v === undefined) return null; if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") return Number(v); var cn = ""; try { cn = String(v.class.name); } catch (e) { } if (bpIsStr(cn)) return String(bpStr(v)); try { if (v.class.isEnum) return bpNum(bpEnumNum(v)); } catch (e) { } var out = {}; try { var lim = maxFields || 20; for (var z = 0; z < v.class.fields.length && z < lim; z++) { var zf = String(v.class.fields[z].name); if (zf.indexOf("Native") === 0) continue; try { var zv = v.field(zf).value; if (zv === null || zv === undefined) { out[zf] = null; continue; } var zcn = ""; try { zcn = String(zv.class.name); } catch (e) { } if (bpIsStr(zcn)) out[zf] = String(bpStr(zv)); else if (typeof zv === "number" || typeof zv === "bigint" || typeof zv === "boolean") out[zf] = Number(zv); else { try { if (zv.class.isEnum) out[zf] = bpNum(bpEnumNum(zv)); else out[zf] = "<" + zcn + ">"; } catch (e) { out[zf] = ""; } } } catch (e) { out[zf] = ""; } } } catch (e) { } return out; } // ---------- R16: 用 GetEnumerator 枚举 Dictionary(key/value 多名字兜底) ---------- BOWL_RPCS.push(bpRpc("dictdump", function (fieldName, maxOut, clsName) { var out = { rpc: "dictdump", cls: String(clsName || "LX6.Item.SceneItemBlob"), field: String(fieldName || ""), count: null, kvFields: [], valCls: null, valFields: [], rows: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "no-class"; return JSON.stringify(out); } var d; try { d = K.field(out.field).value; } catch (e) { out.err = "no-field:" + String(e.message).slice(0, 80); return JSON.stringify(out); } if (!d) { out.err = "null"; return JSON.stringify(out); } try { out.count = Number(d.field("_count").value); } catch (e) { } if (out.count === null) { try { out.count = Number(d.method("get_Count").invoke()); } catch (e) { } } var en = d.method("GetEnumerator").invoke(); // Enumerator 结构 var lim = parseInt(maxOut || "40", 10); var n = (out.count === null ? lim : out.count); for (var i = 0; i < n && out.rows.length < lim; i++) { var more = false; try { more = !!en.method("MoveNext").invoke(); } catch (e) { out.err = "MoveNext:" + String(e.message).slice(0, 60); break; } if (!more) break; var kv = null; try { kv = en.method("get_Current").invoke(); } catch (e) { out.err = "get_Current:" + String(e.message).slice(0, 60); break; } if (!kv) continue; if (out.kvFields.length === 0) { try { for (var q = 0; q < kv.class.fields.length; q++) { var qn = String(kv.class.fields[q].name); if (qn.indexOf("Native") === 0) continue; out.kvFields.push(qn); } } catch (e) { } } var kk = bpFieldTry(kv, ["key", "Key", "Item1"]); var vv = bpFieldTry(kv, ["value", "Value", "Item2"]); if (vv && out.valCls === null) { try { out.valCls = String(vv.class.fullName); for (var w = 0; w < vv.class.fields.length; w++) { var wn = String(vv.class.fields[w].name); if (wn.indexOf("Native") === 0) continue; out.valFields.push(wn); } } catch (e) { } } var row = { k: bpFlatten(kk, 4), v: bpFlatten(vv, 20) }; // 泛型字段枚举失败时:直接读装箱结构内存(header 0x10 之后是内联字段) if (out.kvFields.length === 0) { row.raw = {}; try { var hv = kv.handle; row.raw.at0 = hexOf(hv, 32); var b = hv.add(0x10); row.raw.u64b0 = b.readU64().toString(); row.raw.u32a = b.readU32(); row.raw.u32b = b.add(4).readU32(); row.raw.u32c = b.add(8).readU32(); row.raw.u32d = b.add(12).readU32(); row.raw.u32e = b.add(16).readU32(); row.raw.u32f = b.add(20).readU32(); row.raw.u32g = b.add(24).readU32(); row.raw.u32h = b.add(28).readU32(); } catch (e) { row.raw.err = String(e.message || e); } } out.rows.push(row); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R17: seekIndex 是否有数据(= 该网格有没有授权摆位) ---------- // 约定(实测):seekIndex = (indexX + 34) * 10000 + (indexZ + 75) BOWL_RPCS.push(bpRpc("seekat", function (listStr) { var out = { rpc: "seekat", rows: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemBlob"); if (!K) { out.err = "no-blob"; return JSON.stringify(out); } var d = K.field("seekIndexToDetailHeadListIndex").value; var ks = String(listStr || "").split(","); for (var i = 0; i < ks.length; i++) { var k = parseInt(ks[i], 10); if (isNaN(k)) continue; var rec = { seek: k, ix: Math.floor(k / 10000) - 34, iz: (k % 10000) - 75 }; try { rec.has = !!d.method("ContainsKey").invoke(k); if (rec.has) rec.head = Number(d.method("get_Item").invoke(k)); } catch (e) { rec.err = String(e.message || e).slice(0, 80); } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R18: 手动激活网格 AOI(让客户端把授权摆位实例化出来) ---------- BOWL_RPCS.push(bpRpc("aoiadd", function (ixList, izList, mode) { var out = { rpc: "aoiadd", grids: [], holdsBefore: 0, holdsAfter: 0, hits: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemBlobGrid"); var HK = bpClass("LX6.Item.SceneItemHold"); if (!K) { out.err = "no-grid-class"; return JSON.stringify(out); } var before = HK ? Il2Cpp.gc.choose(HK) : null; out.holdsBefore = before ? before.length : 0; var objs = Il2Cpp.gc.choose(K); var gmap = {}; for (var i = 0; i < (objs ? objs.length : 0); i++) { try { var x = Number(objs[i].field("indexX").value); var z = Number(objs[i].field("indexZ").value); gmap[x + "," + z] = objs[i]; } catch (e) { } } out.gridsTotal = objs ? objs.length : 0; var ixs = String(ixList || "").split(","), izs = String(izList || "").split(","); var useStatic = String(mode || "") === "static"; for (var a = 0; a < ixs.length; a++) { for (var b = 0; b < izs.length; b++) { var ix = parseInt(ixs[a], 10), iz = parseInt(izs[b], 10); if (isNaN(ix) || isNaN(iz)) continue; var g = gmap[ix + "," + iz]; if (!g) { out.grids.push({ ix: ix, iz: iz, r: "no-grid" }); continue; } var rr = []; for (var v = 0; v < 2; v++) { try { g.method("OnAoiAdd").invoke(v); rr.push("OnAoiAdd(" + v + ")=ok"); } catch (e) { rr.push("OnAoiAdd(" + v + ")=" + String(e.message || e).slice(0, 70)); } } if (useStatic) { for (var v2 = 0; v2 < 2; v2++) { try { K.method("LocalOnAOIAdd").invoke(g, v2); rr.push("LocalOnAOIAdd(" + v2 + ")=ok"); } catch (e) { rr.push("LocalOnAOIAdd(" + v2 + ")=" + String(e.message || e).slice(0, 70)); } } } out.grids.push({ ix: ix, iz: iz, r: rr.join(" | ") }); } } var after = HK ? Il2Cpp.gc.choose(HK) : null; out.holdsAfter = after ? after.length : 0; // 采样 hold 的 id + GameObject 名 for (var q = 0; q < (after ? after.length : 0) && out.hits.length < 40; q++) { var rec = {}; try { var iid = bpFieldTry(after[q], ["InstanceId", "instanceId", "SceneItemId", "sceneItemId"]); rec.id = String(bpFlatten(iid, 2)); } catch (e) { } try { var o = bpFieldTry(after[q], ["SceneItemObj", "sceneItemObj", "gameObject", "GameObject"]); if (o) { try { rec.go = String(bpStr(o.method("get_name").invoke())); } catch (e) { rec.go = ""; } } } catch (e) { } out.hits.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R19: 造 Vector3 / Quaternion 值类型 ---------- // 值类型参数:bundle 的 fromFridaValue 对「数组」按字段声明顺序写入 // Vector3 = {x,y,z}、Quaternion = {x,y,z,w} → 直接传 JS 数组 function bpVec3Arr(x, y, z) { return [Number(x), Number(y), Number(z)]; } function bpQuatArr(x, y, z, w) { return [Number(x), Number(y), Number(z), Number(w)]; } function bpVec3Obj(x, y, z) { var K = bpClass("UnityEngine.Vector3"); if (!K) return null; var o = null; try { o = K.alloc(); } catch (e) { try { o = K.new(); } catch (e2) { return null; } } try { o.field("x").value = x; o.field("y").value = y; o.field("z").value = z; } catch (e) { } return o; } function bpQuat(x, y, z, w) { var K = bpClass("UnityEngine.Quaternion"); if (!K) return null; var o = null; try { o = K.alloc(); } catch (e) { try { o = K.new(); } catch (e2) { return null; } } try { o.field("x").value = x; o.field("y").value = y; o.field("z").value = z; o.field("w").value = w; } catch (e) { } return o; } function bpHoldName(id) { try { var M = bpClass("LX6.Item.SceneItemMgr"); var inst = M.method("get_Instance").invoke(); var h = inst.method("GetSceneItemHold").invoke(Number(id)); if (!h) return null; try { if (h.isNull && h.isNull()) return null; } catch (e) { } var o = bpFieldTry(h, ["SceneItemObj", "sceneItemObj", "Obj", "obj", "Go", "go", "m_Obj"]); if (!o) { var names = ["get_SceneItemObj", "get_Obj", "get_Go", "get_GameObject", "get_SceneItemGo"]; for (var i = 0; i < names.length; i++) { try { var t = h.method(names[i]).invoke(); if (t) { o = t; break; } } catch (e) { } } } if (!o) return ""; try { if (o.isNull && o.isNull()) return ""; } catch (e) { } try { return String(bpStr(o.method("get_name").invoke())); } catch (e) { return ""; } } catch (e) { return ""; } } // ---------- R20: 直接生成保龄球(试探哪条 API 可用) ---------- BOWL_RPCS.push(bpRpc("spawnball", function (instId, idA, idB, x, y, z, yaw) { var out = { rpc: "spawnball", tries: [], err: null }; var M = bpClass("LX6.Item.SceneItemMgr"); if (!M) { out.err = "no-mgr"; return JSON.stringify(out); } var inst = M.method("get_Instance").invoke(); if (!inst) { out.err = "no-inst"; return JSON.stringify(out); } // 先确认 Vector3/Quaternion 能造 var v = bpVec3Arr(x, y, z); var q = bpQuatArr(0, 0, 0, 1); var sv = bpVec3Arr(1, 1, 1); out.vec = v; out.quat = q; // 试读回一个分量验证 var vobj = bpVec3Obj(x, y, z); try { out.vecRead = [Number(vobj.field("x").value), Number(vobj.field("y").value), Number(vobj.field("z").value)]; } catch (e) { out.vecReadErr = String(e.message); } var ii = Number(instId); var ids = [["pathIdA", Number(idA)], ["pathIdB", Number(idB)]]; // 姿势1: AddSceneItemBySceneItemIdSync for (var i = 0; i < ids.length; i++) { var rec = { api: "AddSceneItemBySceneItemIdSync", which: ids[i][0], id: ids[i][1] }; try { r = inst.method("AddSceneItemBySceneItemIdSync").invoke( Number(ii), Number(ids[i][1]), v, q, sv, Number(0), 0, 0, null, null, true, false); rec.r = "ok"; } catch (e) { rec.r = String(e.message || e).slice(0, 160); } rec.hold = bpHoldName(ii); out.tries.push(rec); } // 姿势2: AddSceneItemByPrefabNameSync for (var j = 0; j < ids.length; j++) { var rec2 = { api: "AddSceneItemByPrefabNameSync", which: ids[j][0], id: ids[j][1] }; try { inst.method("AddSceneItemByPrefabNameSync").invoke( Number(ii + 1), Number(ids[j][1]), v, q, sv, Number(0), 0, 0, null, null, true, false); rec2.r = "ok"; } catch (e) { rec2.r = String(e.message || e).slice(0, 160); } rec2.hold = bpHoldName(ii + 1); out.tries.push(rec2); } // 姿势3: AddSceneItemBySceneItemIdWithGameObject 变体 var rec3 = { api: "SyncAddSceneItemBySceneItemIdWithGameObject", id: Number(idA) }; try { inst.method("SyncAddSceneItemBySceneItemIdWithGameObject").invoke( Number(ii + 2), Number(Number(idA)), v, q, sv, 0, 0, Number(0), null, Number(0), true, 0.0, false, null, 0, Number(0), false); rec3.r = "ok"; } catch (e) { rec3.r = String(e.message || e).slice(0, 200); } rec3.hold = bpHoldName(ii + 2); out.tries.push(rec3); out.ok = true; return JSON.stringify(out); })); // ---------- R21: 保龄球模板 id 一览(从注册表按 cfgId 取) ---------- BOWL_RPCS.push(bpRpc("tpl", function () { var out = { rpc: "tpl", rows: [], err: null }; try { var K = bpClass("LX6.Item.SceneItemPathInfoRuntime"); var objs = Il2Cpp.gc.choose(K); for (var i = 0; i < (objs ? objs.length : 0); i++) { var cfg = null; try { cfg = Number(objs[i].field("sceneItemConfigId").value); } catch (e) { continue; } if (BP_BOWL_CFGIDS.indexOf(cfg) < 0) continue; var rec = { cfg: cfg }; try { rec.pathId = Number(objs[i].field("pathId").value); } catch (e) { } try { rec.modelId = Number(objs[i].field("modelId").value); } catch (e) { } try { rec.path = String(bpStr(objs[i].field("path").value)); } catch (e) { } try { rec.sizeType = bpNum(bpEnumNum(objs[i].field("sizeType").value)); } catch (e) { } try { rec.destruction = bpNum(bpEnumNum(objs[i].field("destructionType").value)); } catch (e) { } try { rec.canMove = objs[i].field("isConfigCanMove").value; } catch (e) { } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R22: 查单个 hold + Hold 总数 ---------- BOWL_RPCS.push(bpRpc("holdat", function (idsCsv) { var out = { rpc: "holdat", rows: [], err: null }; try { var list = String(idsCsv || "").split(","); for (var i = 0; i < list.length; i++) { var id = parseInt(list[i], 10); if (isNaN(id)) continue; out.rows.push({ id: id, go: bpHoldName(id) }); } var HK = bpClass("LX6.Item.SceneItemHold"); var objs = HK ? Il2Cpp.gc.choose(HK) : null; out.holdCount = objs ? objs.length : 0; out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R23: 方法签名内省(frida 签名 + 参数类型) ---------- BOWL_RPCS.push(bpRpc("msig", function (cls, name) { var out = { rpc: "msig", cls: String(cls || ""), name: String(name || ""), rows: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "no-class"; return JSON.stringify(out); } var ms = null; try { ms = K.methods; } catch (e) { out.err = "no-methods"; return JSON.stringify(out); } for (var i = 0; i < ms.length; i++) { var m = ms[i]; if (String(m.name) !== out.name) continue; var rec = { paramCount: null, ret: null, sig: null, params: [] }; try { rec.paramCount = Number(m.parameterCount); } catch (e) { } try { rec.ret = String(m.returnType.name); } catch (e) { } try { rec.sig = String(m.fridaSignature); } catch (e) { rec.sig = "<" + String(e.message).slice(0, 60) + ">"; } try { for (var p = 0; p < m.parameters.length; p++) { var pm = m.parameters[p]; var pt = pm.type; var one = String(pm.name) + " : " + String(pt.name); try { one += " | ev=" + pt.enumValue; } catch (e) { } try { one += " | byref=" + (!!pt.isByReference); } catch (e) { } try { one += " | vt=" + (!!(pt.class && pt.class.isValueType)); } catch (e) { } try { one += " | vsz=" + (pt.class ? pt.class.valueTypeSize : "?"); } catch (e) { } rec.params.push(one); } } catch (e) { rec.params.push(""); } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R24: 值类型参数封送方式正交试验(用 Vector3.Distance 静态方法,无害可观测) ---------- BOWL_RPCS.push(bpRpc("vectest", function () { var out = { rpc: "vectest", modes: [], err: null }; try { out.hasValueType = (typeof Il2Cpp.ValueType === "function"); out.hasArray = (typeof Il2Cpp.array === "function"); out.hasString = (typeof Il2Cpp.string === "function"); var V = bpClass("UnityEngine.Vector3"); if (!V) { out.err = "no-vector3"; return JSON.stringify(out); } // 静态方法 var M = null; try { M = V.method("Distance"); } catch (e) { out.err = "no-Distance:" + String(e.message).slice(0, 80); return JSON.stringify(out); } try { out.distSig = String(M.fridaSignature); } catch (e) { } try { out.distParamCount = Number(M.parameterCount); } catch (e) { } var A = [1, 0, 0], B = [4, 0, 0]; // 期望距离 3 // mode 0: 原生 JS 数组 (function () { var r = { mode: "js-array" }; try { r.res = String(M.invoke(A, B)); } catch (e) { r.err = String(e.message || e).slice(0, 140); } out.modes.push(r); })(); // mode 1: Il2Cpp.ValueType (function () { var r = { mode: "ValueType" }; if (typeof Il2Cpp.ValueType !== "function") { r.err = "no-Il2Cpp.ValueType"; out.modes.push(r); return; } function mk(a, b, c) { var vt = new Il2Cpp.ValueType(Memory.alloc(V.class.valueTypeSize), V.type); vt.field("x").value = a; vt.field("y").value = b; vt.field("z").value = c; return vt; } try { var va = mk(1, 0, 0), vb = mk(4, 0, 0); try { r.readback = [Number(va.field("x").value), Number(va.field("y").value), Number(va.field("z").value)]; } catch (e) { } r.res = String(M.invoke(va, vb)); } catch (e) { r.err = String(e.message || e).slice(0, 140); } out.modes.push(r); })(); // mode 2: 3 个扁平标量(若签名为扁平) (function () { var r = { mode: "flat-scalars" }; try { r.res = String(M.invoke(1, 0, 0, 4, 0, 0)); } catch (e) { r.err = String(e.message || e).slice(0, 140); } out.modes.push(r); })(); // mode 3: 指针(12 字节缓冲) (function () { var r = { mode: "pointer12" }; try { var pa = Memory.alloc(12); pa.writeFloat(1); pa.add(4).writeFloat(0); pa.add(8).writeFloat(0); var pb = Memory.alloc(12); pb.writeFloat(4); pb.add(4).writeFloat(0); pb.add(8).writeFloat(0); r.res = String(M.invoke(pa, pb)); } catch (e) { r.err = String(e.message || e).slice(0, 140); } out.modes.push(r); })(); out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R25: 列出一个类的全部方法(名字/参数数/返回/是否静态/frida 签名) ---------- BOWL_RPCS.push(bpRpc("mlist", function (cls, reStr, cap) { var out = { rpc: "mlist", cls: String(cls || ""), rows: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "no-class"; return JSON.stringify(out); } var rx = reStr ? new RegExp(String(reStr), "i") : null; var lim = cap ? Number(cap) : 400; var ms = K.methods; out.total = ms.length; for (var i = 0; i < ms.length && out.rows.length < lim; i++) { var m = ms[i], nm = ""; try { nm = String(m.name); } catch (e) { continue; } if (rx && !rx.test(nm)) continue; var rec = { n: nm }; try { rec.pc = Number(m.parameterCount); } catch (e) { } try { rec.ret = String(m.returnType.name); } catch (e) { } try { rec.st = !!m.isStatic; } catch (e) { } try { rec.sig = String(m.fridaSignature); } catch (e) { } out.rows.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R26: 通用强类型调用器 ---------- // spec = JSON 数组,元素 {"t":类型,"v":值} // t: u64|u32|i32|f|bool|null|ptr|str|v3|q|vt(按形参类型构造值类型) // instMode: "static" 静态 | "instance" 走 get_Instance | 其它 = 静态字段名(取其对象后实例调用) BOWL_RPCS.push(bpRpc("invk", function (cls, mname, pcStr, instMode, specStr) { var out = { rpc: "invk", cls: String(cls || ""), m: String(mname || ""), ts: [], err: null }; try { var K = bpClass(out.cls); if (!K) { out.err = "no-class"; return JSON.stringify(out); } var pc = (pcStr === "" || pcStr === null || pcStr === undefined) ? -1 : Number(pcStr); var spec = JSON.parse(String(specStr || "[]")); var target = null; if (String(instMode) === "static") { target = K.method(out.m, pc); } else { var holder = null; if (String(instMode) === "instance") { try { holder = K.method("get_Instance").invoke(); } catch (e) { } } else { try { holder = K.field(String(instMode)).value; } catch (e) { } } if (!holder) { out.err = "no-holder(" + instMode + ")"; return JSON.stringify(out); } try { if (holder.isNull && holder.isNull()) { out.err = "null-holder"; return JSON.stringify(out); } } catch (e) { } target = holder.method(out.m, pc); } try { out.pc = Number(target.parameterCount); } catch (e) { } try { out.ret = String(target.returnType.name); } catch (e) { } var args = []; for (var i = 0; i < spec.length; i++) { var a = spec[i], t = String(a.t); out.ts.push(t); if (t === "null") args.push(null); else if (t === "ptr") args.push(ptr(Number(a.v || 0))); else if (t === "bool") args.push(!!a.v); else if (t === "str") args.push(Il2Cpp.string(String(a.v))); else if (t === "v3") args.push([Number(a.v[0]), Number(a.v[1]), Number(a.v[2])]); else if (t === "q") args.push([Number(a.v[0]), Number(a.v[1]), Number(a.v[2]), Number(a.v[3])]); else if (t === "vt") { var pt = target.parameters[i].type; args.push(Il2Cpp.fromFridaValue(a.v, pt)); } // vtref: 按形参类型构造值类型,但传它的 handle(用于 ref/out 形参) else if (t === "vtref") { var pt2 = target.parameters[i].type; var vt2 = Il2Cpp.fromFridaValue(a.v, pt2); args.push(vt2.handle); } // new: 新建托管对象。v = [类名, 泛型参数类名...];泛型用反引号,如 HashSet`1 else if (t === "new") { var names = a.v; var K2 = null, nm0 = String(names[0]); K2 = bpClass(nm0); if (!K2) K2 = bpClass(nm0.replace("`1", "")); if (!K2) throw new Error("no-cls:" + nm0); if (names.length > 1) { var tas = []; for (var q = 1; q < names.length; q++) { var tc = bpClass(String(names[q])); if (!tc) throw new Error("no-arg-cls:" + names[q]); tas.push(tc); } K2 = K2.inflate.apply(K2, tas); } args.push(K2.new()); } else args.push(Number(a.v)); } var rr = target.invoke.apply(target, args); try { out.res = String(rr); } catch (e) { out.res = ""; } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R27: 封送正交试验 2(无害 API:null 指针 / 值类型+标量混合) ---------- BOWL_RPCS.push(bpRpc("ptest", function () { var out = { rpc: "ptest", rows: [], err: null }; function T(name, fn) { var r = { n: name }; try { r.res = String(fn()); } catch (e) { r.err = String(e && e.message ? e.message : e).slice(0, 190); } out.rows.push(r); } var O = bpClass("UnityEngine.Object"); out.hasObjectCls = !!O; if (O) { T("op_Equality(null,null)", function () { return O.method("op_Equality").invoke(null, null); }); T("op_Equality(inst,inst)", function () { var M = bpClass("LX6.Item.SceneItemMgr"); var inst = M.method("get_Instance").invoke(); return O.method("op_Equality").invoke(inst, inst); }); } var C = bpClass("UnityEngine.Color"); out.hasColorCls = !!C; if (C) { T("Color.Lerp([4],[4],f)", function () { return String(C.method("Lerp").invoke([1, 1, 1, 1], [0, 0, 0, 0], 0.5)); }); } var Q = bpClass("UnityEngine.Quaternion"); out.hasQuatCls = !!Q; if (Q) { T("Quaternion.Angle([q],[q])", function () { return String(Q.method("Angle").invoke([0, 0, 0, 1], [0, 0, 0.7071068, 0.7071068])); }); } return JSON.stringify(out); })); // ---------- R28: 打印一个 hold 的全部字段 + 关联 GameObject ---------- BOWL_RPCS.push(bpRpc("holdf", function (id) { var out = { rpc: "holdf", id: Number(id), fields: [], err: null }; try { var M = bpClass("LX6.Item.SceneItemMgr"); var inst = M.method("get_Instance").invoke(); var h = inst.method("GetSceneItemHold").invoke(Number(id)); if (!h) { out.err = "no-hold"; return JSON.stringify(out); } try { out.cls = String(h.class.name); } catch (e) { } try { var fs = h.class.fields; for (var i = 0; i < fs.length; i++) { var f = fs[i], nm = ""; try { nm = String(f.name); } catch (e) { continue; } var rec = { n: nm }; try { rec.st = !!f.isStatic; } catch (e) { } try { rec.t = String(f.type.name); } catch (e) { } if (!rec.st) { try { rec.v = bpFlatten(f.bind(h).value, 8); } catch (e) { rec.v = ""; } } out.fields.push(rec); } } catch (e) { out.fErr = String(e.message); } try { out.go = bpHoldName(Number(id)); } catch (e) { } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- R29: 按 instanceId 设置 hold 的字段(用于关掉 ECS 渲染等) ---------- BOWL_RPCS.push(bpRpc("holdset", function (id, specStr) { var out = { rpc: "holdset", id: Number(id), done: [], err: null }; try { var M = bpClass("LX6.Item.SceneItemMgr"); var inst = M.method("get_Instance").invoke(); var h = inst.method("GetSceneItemHold").invoke(Number(id)); if (!h) { out.err = "no-hold"; return JSON.stringify(out); } var spec = JSON.parse(String(specStr || "{}")); for (var k in spec) { var rec = { n: k }; try { var f = h.field(k); f.value = spec[k]; try { rec.back = String(f.value); } catch (e) { } rec.ok = true; } catch (e) { rec.err = String(e && e.message ? e.message : e).slice(0, 130); } out.done.push(rec); } out.ok = true; } catch (e) { out.err = String(e && e.message ? e.message : e); } return JSON.stringify(out); })); // ---------- 入口 ---------- // ★★ 重调用互斥闸(2026-09-24 08:17 现场事故之后加的,**这是防卡死的核心**): // 事故:自动快照里连着调 bowlst(内部要跑 7 次 Il2Cpp.gc.choose)+ 监视线程每 4 秒 // 调一次 holdsum(也要 gc.choose),把 GC 堆全量遍历叠在一起, // 游戏主线程被拖住 ~30 秒不动(Player.log 08:17:38 停写,08:18:09 才断开)。 // 现在三条规则: // ① 任何"重"RPC 之间**互斥**:重入直接返回 busy,不会叠成风暴。 // ② 每次耗时记进 BP_COST,`bpsCost()` 可查;>5s 计入 BP_SLOW,>8s 直接 send 一条告警。 // ③ 轻 RPC(只读 JS 变量 / 类元数据,不碰 gc)不受限,可以高频轮询 —— // `bfStatus` 就属于这一类,它**一个 IL2CPP 调用都不做**,所以心跳用它。 var BP_HEAVY_BUSY = false, BP_HEAVY_OWNER = null, BP_COST = {}, BP_SLOW = 0; var BP_LIGHT_RPCS = { pos: 1, clsfields: 1, enumv: 1 }; var BOWL_EXPORTS = {}; BOWL_RPCS.forEach(function (r) { var isLight = !!BP_LIGHT_RPCS[r.name]; BOWL_EXPORTS[r.name] = function () { var args = Array.prototype.slice.call(arguments); var res = null; if (!isLight && BP_HEAVY_BUSY) { return JSON.stringify({ rpc: r.name, err: "busy: heavy probe already running (" + BP_HEAVY_OWNER + ")" }); } if (!isLight) { BP_HEAVY_BUSY = true; BP_HEAVY_OWNER = r.name; } var t = Date.now(); try { res = r.fn.apply(null, args); } catch (e) { res = JSON.stringify({ rpc: r.name, err: String(e && e.message ? e.message : e) }); } finally { if (!isLight) { var ms = Date.now() - t; BP_COST[r.name] = ms; if (ms > 5000) BP_SLOW++; if (ms > 8000) { try { send(JSON.stringify({ bf: "[SLOW] " + r.name + " took " + ms + "ms" })); } catch (e) { } } BP_HEAVY_BUSY = false; BP_HEAVY_OWNER = null; } } // 只把小结果回送(避免几百 KB 的 dump 反复走通道 / 刷屏) if ("window" in globalThis && typeof globalThis.send === "function") { try { if (String(res).length <= 1200) send(String(res)); } catch (e) { } } return res; }; }); // ★ 代价查询(轻):看哪些 RPC 慢、有没有卡在 busy BOWL_EXPORTS.bpsCost = function () { return JSON.stringify({ rpc: "bpsCost", cost: BP_COST, slow: BP_SLOW, busy: BP_HEAVY_BUSY, owner: BP_HEAVY_OWNER }); }; Il2Cpp.perform(function () { rpc.exports = Object.assign(rpc.exports || {}, BOWL_EXPORTS); try { send(JSON.stringify({ ready: true, rpcs: BOWL_RPCS.length })); } catch (e) { } }); // ================== 保龄球客户端补丁 BF1(常驻版)================== // // 原理(v7..v12 实测闭环): // 客户端造 SceneItemHold 的**唯一**入口是 // LX6.Item.SceneItemBlob.TryGetSceneItemPlaceInfoDataByPlaceId( // UInt64 uniqueId, out MA_Ref data, out SceneItemBlobType& blobType) // 它是对「关卡烘焙摆位表」的一次查表。官方关卡里**没有烘焙任何球瓶/球** // (离线证据:StreamingAssets/Blocks/vfc_1286 里 cell(67,39) 的 blob 只有 19 个 // 椅子/长椅/垃圾桶/广告牌;BowlingConfig.json 记录数为 0), // 所以服务端推来的球瓶 id 查不到 → 返回 false → 不调 SyncAddSceneItemByLocalData // → 不建 hold → 「球瓶按下按钮后消失」、「投球时手上没球」、「pool[101] is nil」。 // // 本补丁让这个函数对「服务端的球瓶/球 id」返回 true,并把一个我们构造好的 // SceneItemPlaceInfoData 交出去 ⇒ 客户端原生流程全通: // SyncAddSceneItemByLocalData → SyncAddSceneItemByIdData → DoSceneItemHolderEnterWorld // // 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 // // 做法:先主动查一个真 placeId 拿「合法模板」,再克隆成我们的 15 个条目。 (function () { 'use strict'; function sendlog(s) { try { send(JSON.stringify({ bf: String(s) })); } catch (e) { } } // ★★ 2026-09-30 卡死修复 ①:热路径(hook 回调)**绝不能**直接 send()。 // send() 是给 Python 宿主的 IPC:宿主正忙 / 出站队列有背压时会**阻塞调用它的线程**, // 而 hook 回调跑在**客户端物件加载线程**上 ⇒ 整条加载流水线停摆 // (症状正是实测到的:CPU≈0.05 核、日志 0 增长的静默死锁)。 // ⇒ 热路径只往缓冲区塞一行,由 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); // 每次最多 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); // ---- 数据:与 GameRouter.GameGround.cs 的 BowlingLanes / 常量 完全一致 ---- // (laneZ, PinApexX, GameNodeX) 顺序 = C# BowlingLanes[](按 z 降序) var LANES = [ [1566.7732, 2710.2510, 2692.2559], [1564.7737, 2710.2056, 2692.2559], [1572.1193, 2710.3689, 2692.3738], [1570.1198, 2710.3235, 2692.3738], [1576.1143, 2710.4600, 2692.4648], [1574.1147, 2710.4146, 2692.4648], [1581.4462, 2710.5840, 2692.5889], [1579.4467, 2710.5386, 2692.5889] ]; var PIN_CFG = 80003145, PIN_PATH = 1959229003; var BALL_CFG = 80002967, BALL_PATH = 1838106492; var ID_BASE = 0xd3e03a0c01; // _bowlingSceneItemSeq 起点 910000000000 + 1 var BATCH = 15; // 10 瓶 + 5 球 // 模板候选(球馆格 (67,39) 的 5 个真实烘焙 placeId) var TPL_CANDIDATES = [ '0x482716fd0e9d2f30', '0x92204198eaef66de', '0x9bc102684b95d776', '0xce21103f6e9c1809', '0xebf189795fb8d298' ]; var TPL = null, TPLPTR = null, SLOTS = null; var ARMED = false, FAKED = 0, HITS = 0; var LANEIDX = -1, ITEMS = null; var PMGR = null, PCACHE = { t: 0, z: null }; var QUERY_VA = null; 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; } } // ★ 热路径优化(2026-09-24):isOurs 被 GetSceneItemHold / 查表 hook 的**每一次**调用触发, // 原来每次都 toString(16) + indexOf,字符串分配很贵。改成指针区间比较。 // 我们关心的 id 全在 0xD3E03A0000..0xD3E0400000(= 十进制 910000000000 起)这一段。 var OURS_LO = null, OURS_HI = null; function isOurs(p) { try { if (OURS_LO === null) { OURS_LO = ptr('0xd3e03a0000'); OURS_HI = ptr('0xd3e0400000'); } if (p && typeof p.compare === 'function') { if (p.compare(OURS_LO) < 0) return false; if (p.compare(OURS_HI) >= 0) return false; return true; } if (typeof p === 'number') return p >= 0xd3e03a0000 && p < 0xd3e0400000; return p.toString(16).indexOf('d3e03a') === 0; } catch (e) { return false; } } // ★ 紧急刹车:把三个 Interceptor 全部摘掉(游戏一卡就能立刻停手,不需要关客户端) var LISTENERS = []; // ★★ 停机标志:置上之后所有 hook 回调立刻短路,配合 detach 保证「卸载时没有任何 // JS 正在跑」—— 这是「关掉加载器 → 游戏卡死」那种事故的对症解法。 var DISARMED = false; // ---- 由玩家 z 选最近的道(与 C# BowlingPickLane 同规则)---- function pickLaneIdx(z) { if (typeof z !== 'number' || !isFinite(z)) return -1; var best = -1, bd = 1e9; for (var i = 0; i < LANES.length; i++) { var d = Math.abs(LANES[i][0] - z); if (d < bd) { bd = d; best = i; } } return best; } // ---- 15 个条目(10 瓶 1-2-3-4 三角 + 5 球一排),与 C# PushBowlingSceneItemsAsync 同公式 ---- function buildItems(laneIdx) { var L = LANES[laneIdx]; var laneZ = L[0], pinApexX = L[1], gameNodeX = L[2]; var laneHeadX = gameNodeX + 0.24; var out = []; for (var r = 0; r < 4; r++) { var n = r + 1; for (var c = 0; c < n; c++) { out.push({ cfg: PIN_CFG, path: PIN_PATH, x: pinApexX + r * 0.26, y: 0.02, z: laneZ + (c - (n - 1) * 0.5) * 0.30, s: 100 }); } } for (var b = 0; b < 5; b++) { out.push({ cfg: BALL_CFG, path: BALL_PATH, x: laneHeadX - 1.0, y: 0.12, z: laneZ + (b - 2) * 0.35, s: 100 }); } return out; } 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 * BATCH); function writeSlot(slot, idPtr, it) { if (TPL) cp(slot, TPL, 112); slot.add(0x10).writePointer(idPtr); slot.add(0x18).writeU32(it.cfg); slot.add(0x1C).writeS32(it.path); slot.add(0x24).writeFloat(it.x); slot.add(0x28).writeFloat(it.y); slot.add(0x2C).writeFloat(it.z); // ★★ 「最小覆写」原则 —— 2026-09-24 08:32 实测定案(bfTplDump 逐字段对比得出): // // 旧实现(v12)在拷完模板后还额外做了: // +0x20 groupId 写 0 → 模板本来就是 0,无影响 // +0x30 eulerX 写 0 → 模板本来就是 0,无影响 // +0x34 eulerY 写 0 → 模板是 359.963(≈整圈),与缩放无关 // +0x38 eulerZ 写 0 → 模板本来就是 0,无影响 // +0x3C iScale 写 100 → 模板是 **0** ← ★★★ 就是它 // // 症状:生成对象的 localScale 退化成 **[0, 0, 1]** ⇒ 尺寸压塌、画不出来 // (有碰撞、有音效,就是没画面)。改回沿用模板后 localScale = [1, 1, 1], // 用户实测「可以看到球了,也有击中的物理效果」。 // // `iScale` 很可能是**尺寸类型枚举/索引**(资源表里球瓶 sizeType=0、球 sizeType=1), // 0 = 默认尺寸;写 100 是非法值 ⇒ 退化。 // // ⇒ 教训:**含义没吃透的字段,"保持模板原值"永远比"写个我猜的值"安全。** // 想改什么就改什么,别顺手清零。诊断用 bfTplDump / bfSetField。 } 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); var ok = fn(uint64(TPL_CANDIDATES[i]), bA, bB); if (!ok) 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; sendlog('template captured from ' + TPL_CANDIDATES[i] + ' -> ' + p.toString(16) + ' cfg=' + TPL.add(0x18).readU32() + ' z=' + TPL.add(0x2C).readFloat().toFixed(2)); return true; } catch (e) { sendlog('tpl try err ' + e); } } return false; } // ★★ 2026-09-30 卡死修复 ②(**主犯**):热路径里绝不调用任何 IL2CPP API。 // // 原实现:onLeave → playerZ() → PMGR.method('get_PlayerUnit',0).invoke() // invoke() = il2cpp_runtime_invoke:要进 VM、可能触发**类加载**与 **GC**, // 还要拿 VM / 类加载器相关的锁。而我们的回调就跑在**客户端物件加载线程**上, // 那个线程此刻正在做类加载 + 反序列化 —— 两边互抢同一把锁 ⇒ **死锁**。 // // 修法: // · 解析动作搬到 Il2Cpp.perform 的安全上下文里做(启动时一次,且不在任何 // hook 回调内)⇒ 只拿到 PlayerUnit 句柄 + lastPosition 字段偏移; // · 热路径退化成 **纯原始内存读**(零 IL2CPP 调用、零锁、零 GC); // · 需要刷新时由 Il2Cpp.perform 在 frida 自己的线程上做,不碰游戏线程。 var PUNIT = null, POFF = -1, PKLASS = null; function refreshPlayer() { try { if (!PMGR) PMGR = bfClass('LX6.Game.MyPlayerManager'); if (!PMGR) return; var u = PMGR.method('get_PlayerUnit', 0).invoke(); if (!u || u.handle.isNull()) return; var v = u.field('lastPosition').value; if (!v || !v.handle) return; var off = v.handle.sub(u.handle).toInt32(); // 字段相对对象头的偏移 if (off < 0 || off > 0x4000) return; PUNIT = u.handle; POFF = off; try { PKLASS = u.handle.readPointer(); } catch (e) { PKLASS = null; } var zz = u.handle.add(off + 8).readFloat(); // Vector3: x=+0 y=+4 z=+8 if (isFinite(zz)) PCACHE = { t: Date.now(), z: zz }; } catch (e) { } } function playerZ() { if (PUNIT && POFF >= 0) { try { // 类指针不变 ⇒ 对象没被回收(IL2CPP 的 GC 是非移动式) if (PKLASS && !PUNIT.readPointer().equals(PKLASS)) return PCACHE.z; var zz = PUNIT.add(POFF + 8).readFloat(); if (isFinite(zz) && zz !== 0) { PCACHE = { t: Date.now(), z: zz }; return zz; } } catch (e) { } } return PCACHE.z; // 拿不到就用上一次的值 —— 永远不阻塞、永远不进 IL2CPP } // ★ 卡死修复 ③(配套):模板探测必须**在挂 hook 之前**做。 // 挂上之后再调 fetchTpl() 就是通过被 hook 的地址重入同一个函数(见 onLeave 里的说明)。 try { fetchTpl(); } catch (e) { } if (TPL) sendlog('template captured (pre-hook probe) -> ' + TPLPTR.toString(16)); // 启动时解析一次玩家句柄(此刻不在任何 hook 回调里 ⇒ 可以安全调用 IL2CPP API) try { refreshPlayer(); } catch (e) { } sendlog('player resolved: unit=' + (PUNIT ? PUNIT.toString(16) : 'null') + ' off=' + POFF + ' z=' + (PCACHE.z === null ? '?' : PCACHE.z.toFixed(2))); // ---------------- 核心 hook ---------------- LISTENERS.push(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) { 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; sendlogLater('template captured (hook) -> ' + p.toString(16)); } } } catch (e) { } } return; } if (!isOurs(this.id)) return; HITS++; if (!ARMED) return; // ★ 卡死修复 ③:这里原本是 `if (!TPL) { if (!fetchTpl()) return; }`。 // fetchTpl() 会通过 NativeFunction **直接调用 QUERY_VA**,而 QUERY_VA 此刻 // 已经被我们 hook 了 ⇒ 等于「在自己的 onLeave 里重入同一个被 hook 的函数」。 // 一旦该函数内部有非重入锁(或它自己也要进 VM)就会自锁 ⇒ 死锁。 // ⇒ 模板只留两个来源:① 挂 hook **之前**的探测;② 真实查表成功时的**被动** // 捕获(纯内存读,安全)。拿不到模板就这一次不伪造,等下一次。 if (!TPL) return; // 选道:按玩家 z;拿不到就退回上一次/第 0 条 var z = playerZ(); var li = pickLaneIdx(z); if (li >= 0 && li !== LANEIDX) { LANEIDX = li; ITEMS = buildItems(LANEIDX); sendlogLater('lane -> #' + LANEIDX + ' (laneZ=' + LANES[LANEIDX][0] + ', playerZ=' + (z === null ? '?' : z.toFixed(2)) + ')'); } if (LANEIDX < 0) { LANEIDX = 0; ITEMS = buildItems(0); } var n = 0; try { n = parseInt(this.id.toString(16), 16); } catch (e) { } var idx = (n - ID_BASE) % BATCH; if (idx < 0) idx += BATCH; var it = ITEMS[idx]; try { writeSlot(SLOTS.add(idx * 112), this.id, it); this.outA.writePointer(SLOTS.add(idx * 112)); retval.replace(ptr(1)); FAKED++; if (FAKED <= 20) { sendlogLater('FAKED id=' + this.id.toString(16) + ' idx=' + idx + ' cfg=' + it.cfg + ' pos=(' + it.x.toFixed(2) + ',' + it.y.toFixed(2) + ',' + it.z.toFixed(2) + ')'); } } catch (e) { sendlogLater('fake err ' + e); } } })); ARMED = true; sendlog('armed. waiting for template... (hint: stand inside the bowling alley so the AOI table loads)'); // ---- 观测 hook(★ 卡死修复 ④:默认**不挂**)---- // 这两个 hook 只统计/打日志,对伪造功能零贡献,却挂在 GetSceneItemHold / // DoSceneItemHolderEnterWorld 上 —— 与核心 hook 是同一条物件加载热路径。 // 每多一个 Interceptor,每次调用就多一次 trampoline 跳转 + 一次 JS 回调 // (而且另一个模块 balloon_fix.js 也 hook 了 QUERY_VA,热路径上本来就有两份)。 // ⇒ 默认关;要诊断时用 rpc `bfDiag(true)` 现挂,`bfDiag(false)` 摘掉。 var DIAG = false; var DIAG_LS = []; var HOLDS_OK = 0, HOLDS_NIL = 0; function attachDiag() { if (DIAG_LS.length) return; try { var mgrCls = bfClass('LX6.Item.SceneItemMgr'); var GET_VA = mgrCls.method('GetSceneItemHold').virtualAddress; DIAG_LS.push(Interceptor.attach(ptr(GET_VA), { onEnter: function (args) { if (DISARMED) return; // ★ 顺序反了会多跑一次 isOurs:这里先试更可能是 id 的那个实参 var a1 = isOurs(args[1]) ? args[1] : null; this.idp = a1 !== null ? a1 : (isOurs(args[0]) ? args[0] : null); }, onLeave: function (rv) { if (DISARMED || !this.idp) return; // ★ 停机后一个字节都不再碰 var s = '0x' + rv.toString(16); if (s === '0x0') { HOLDS_NIL++; if (HOLDS_NIL <= 20) sendlogLater('hold NIL for ' + this.idp.toString(16)); } else { HOLDS_OK++; if (HOLDS_OK <= 20) sendlogLater('hold OK for ' + this.idp.toString(16)); } } })); // DoSceneItemHolderEnterWorld:hold 真正进世界的时刻 try { var EW_VA = mgrCls.method('DoSceneItemHolderEnterWorld').virtualAddress; DIAG_LS.push(Interceptor.attach(ptr(EW_VA), { onEnter: function (args) { if (DISARMED) return; if (isOurs(args[1])) sendlogLater('holder entered world: ' + args[1].toString(16)); } })); } catch (e) { } } catch (e) { sendlog('watch hook err ' + e); } } if (DIAG) attachDiag(); // ★ 卡死修复 ③(配套):这个 tick 不再调 fetchTpl() —— 挂上 hook 之后再调它就是 // 重入被 hook 的函数。模板只靠「挂 hook 前的探测」+「真实查表成功时的被动捕获」。 // // ⛑⛑ 2026-09-30 11:0x 二次修正:这里**曾经**是 `Il2Cpp.perform(refreshPlayer)`, // 那是个错误 —— `Il2Cpp.perform` 要把任务投递到 IL2CPP 线程执行并等待结果, // 而与此同时「物件加载线程」可能正堵在这个 script 的运行时锁上等 hook 回调 ⇒ // 两边互等(定时器等游戏线程 / 游戏线程等锁)。这是**我自己引入的**新死锁面。 // ⇒ 干脆不再周期性刷新:启动时解析一次就够了(IL2CPP 用非移动 GC,句柄长期有效), // 解析失败时 playerZ() 会安全返回上一次的值,不会阻塞、不会进 IL2CPP。 // (真的要重新解析,用 rpc `bfRefetch` 或重启注入器。) var tick = setInterval(function () { if (DISARMED) return; }, 3000); // 供外部查询状态 rpc.exports = rpc.exports || {}; rpc.exports.bfStatus = function () { return JSON.stringify({ armed: ARMED, tpl: !!TPL, tplPtr: TPLPTR ? TPLPTR.toString(16) : null, tplCfg: TPL ? TPL.add(0x18).readU32() : null, lane: LANEIDX, hits: HITS, faked: FAKED, holdsOk: HOLDS_OK, holdsNil: HOLDS_NIL, playerZ: PCACHE.z, hooks: LISTENERS.length, punit: PUNIT ? PUNIT.toString(16) : null, poff: POFF, diag: DIAG, disarmed: DISARMED }); }; // ★ 卡死修复 ③:挂上 hook 之后**不能**再用 fetchTpl() 主动探测 // (= 通过被 hook 的地址重入同一个函数)⇒ 只清模板,交给被动捕获。 rpc.exports.bfRefetch = function () { TPL = null; if (LISTENERS.length > 0) return 'cleared — will re-capture passively on next real lookup'; return fetchTpl() ? 'ok' : 'fail'; }; // ★ 卡死修复 ④:观测 hook 的开关(默认关,见 attachDiag 的说明) rpc.exports.bfDiag = function (on) { DIAG = !!on; if (DIAG) { attachDiag(); return 'diag on (' + DIAG_LS.length + ' listener(s))'; } var n = 0; for (var i = 0; i < DIAG_LS.length; i++) { try { DIAG_LS[i].detach(); n++; } catch (e) { } } DIAG_LS = []; return 'diag off (detached ' + n + ')'; }; // ★★ 诊断 RPC:把 112 字节的 placeInfo 逐字段摊开。 // 目的:回答「+0x30/+0x34/+0x38 到底是 euler 角还是缩放分量」 // —— 拿**真实模板**(合法 scene item)的值做基准,一看便知。 function dump112(p, tag) { var o = { tag: tag, hex: null, dec: [], err: null }; try { var b = new Uint8Array(p.readByteArray(112)); var h = []; for (var i = 0; i < 112; i++) h.push(('0' + b[i].toString(16)).slice(-2)); o.hex = h.join(''); for (var off = 0; off < 112; off += 4) { var s = null, f = null; try { s = p.add(off).readS32(); } catch (e) { } try { f = p.add(off).readFloat(); } catch (e) { } o.dec.push('+0x' + ('0' + off.toString(16)).slice(-2) + ' i32=' + s + ' f32=' + (f === null ? '?' : (Math.abs(f) < 1e-7 ? '0' : f.toFixed(5)))); } } catch (e) { o.err = String(e); } return o; } rpc.exports.bfTplDump = function () { var out = { rpc: 'bfTplDump', tplCfg: null, template: null, slot0: null, err: null }; if (!TPL) { out.err = 'no-template-yet'; return JSON.stringify(out); } try { out.tplCfg = TPL.add(0x18).readU32(); } catch (e) { } out.template = dump112(TPL, 'template(real)'); if (SLOTS) { try { out.slot0 = dump112(SLOTS, 'slot0(what-we-write)'); } catch (e) { } } return JSON.stringify(out); }; // ★ 实验用:改「我们写入的 slot」里任意 4 字节字段。 // 例:bfSetField(0, 0x3c, 1) 把条 0 的 +0x3C 改成 1 // bfSetField(0, 0x3c, 0) 改回模板原值 // kind:'i'(默认,Int32)或 'f'(float)。 // ⚠ 只影响**之后**新建的 hold;已经进世界的对象不会变(要重开一局才生效)。 rpc.exports.bfSetField = function (idx, off, val, kind) { if (!SLOTS) return 'no-slots'; idx = parseInt(idx, 10); off = parseInt(off, 10); if (!(idx >= 0 && idx < BATCH) || off < 0 || off > 108) return 'bad-args'; var p = SLOTS.add(idx * 112 + off); if (kind === 'f') p.writeFloat(Number(val)); else p.writeS32(parseInt(val, 10)); return 'slot[' + idx + ']+0x' + off.toString(16) + ' = ' + val + ' (' + (kind || 'i32') + ')'; }; // ★ 紧急刹车:游戏一卡就调它,三个 Interceptor 全摘掉,立刻停止一切干扰。 // (不需要关客户端;Python 侧 Ctrl+C 也行,但钩子会留在进程里直到游戏退出。) rpc.exports.bfDisarm = function () { DISARMED = true; var n = 0; for (var i = 0; i < LISTENERS.length; i++) { try { LISTENERS[i].detach(); n++; } catch (e) { } } LISTENERS = []; for (var j = 0; j < DIAG_LS.length; j++) { try { DIAG_LS[j].detach(); n++; } catch (e) { } } DIAG_LS = []; ARMED = false; try { clearInterval(tick); } catch (e) { } try { clearInterval(LOGIV); } catch (e) { } try { flushLog(); } catch (e) { } // 停机前把缓冲区里剩的打出来 sendlog('DISARMED: detached ' + n + ' listener(s)'); return 'detached ' + n; }; // ★★ 停机协议(加载器关闭前**必调**,也是唯一该在 unload 脚本之前调的接口) // ① 置 DISARMED ⇒ 所有 hook 回调立刻短路(在途的那次也安全) // ② detach 全部 Interceptor // ③ 清掉 setInterval(补模板用的那个) // 返回后 agent 已彻底停手 —— 这时才允许 Python 侧 unload 脚本。 rpc.exports.bowlShutdown = 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 = []; for (var j = 0; j < DIAG_LS.length; j++) { try { DIAG_LS[j].detach(); n++; } catch (e) { } } DIAG_LS = []; ARMED = false; try { flushLog(); } catch (e) { } // 停机前把缓冲区里剩的打出来 try { sendlog('SHUTDOWN: disabled + detached ' + n + ' listener(s)'); } catch (e) { } return JSON.stringify({ ok: true, detached: n, faked: FAKED, holdsOk: HOLDS_OK }); }; }); })();