Files

4320 lines
182 KiB
JavaScript
Raw Permalink Normal View History

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