4320 lines
182 KiB
JavaScript
4320 lines
182 KiB
JavaScript
// 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; };
|
|||
|
|
});
|
|||
|
|
})();
|