#include "Il2cppJson.hpp" #include #include #include #include #include #include #include #include "../GakumasLocalify/Log.h" #include "../GakumasLocalify/Plugin.h" namespace Il2cppJson { // static ClassMap s_classMap{}; static bool s_initialized = false; ClassMap& GetClassMap() { // 这里的静态变量只有在第一次调用 GetClassMap() 时才会被初始化,完美避开顺序问题 static ClassMap s_classMap; return s_classMap; } // ─── helpers ──────────────────────────────────────────────────────── static std::string trim(const std::string& s) { auto start = s.find_first_not_of(" \t\r\n"); if (start == std::string::npos) return ""; auto end = s.find_last_not_of(" \t\r\n"); return s.substr(start, end - start + 1); } /// Parse `group` field into assembly / namespace / class. /// "Assembly-CSharp.dll/Campus/OutGame/SomePresenter" /// → assembly = "Assembly-CSharp.dll" /// → nameSpace = "Campus.OutGame" /// → className = "SomePresenter" static bool parseGroup(const std::string& group, std::string& assembly, std::string& nameSpace, std::string& className) { auto dllPos = group.find(".dll"); if (dllPos == std::string::npos) return false; assembly = group.substr(0, dllPos + 4); size_t restStart = dllPos + 4; if (restStart < group.size() && group[restStart] == '/') restStart++; if (restStart >= group.size()) return false; std::string rest = group.substr(restStart); std::vector parts; size_t pos = 0; while (pos < rest.size()) { auto next = rest.find('/', pos); if (next == std::string::npos) { parts.push_back(rest.substr(pos)); break; } parts.push_back(rest.substr(pos, next - pos)); pos = next + 1; } if (parts.empty()) return false; className = parts.back(); parts.pop_back(); nameSpace.clear(); for (size_t i = 0; i < parts.size(); i++) { if (i > 0) nameSpace += '.'; nameSpace += parts[i]; } return true; } /// Bracket-aware split of a parameter list string by comma. /// "IReadOnlyList`1[A,B], Int32" → ["IReadOnlyList`1[A,B]", "Int32"] static std::vector splitParams(const std::string& paramStr) { std::vector result; if (paramStr.empty()) return result; int depth = 0; std::string current; for (char c : paramStr) { if (c == '[' || c == '<' || c == '(') { depth++; current += c; } else if (c == ']' || c == '>' || c == ')') { depth--; current += c; } else if (c == ',' && depth == 0) { auto t = trim(current); if (!t.empty()) result.push_back(t); current.clear(); } else { current += c; } } auto t = trim(current); if (!t.empty()) result.push_back(t); return result; } /// Parse `dotNetSignature` into method name and parameter type list. /// "Void SetItemModels(IReadOnlyList`1[X])" /// → methodName = "SetItemModels", paramTypes = ["IReadOnlyList`1[X]"] /// /// "EmbeddedAttribute()" /// → methodName = "EmbeddedAttribute", paramTypes = [] static bool parseDotNetSignature(const std::string& sig, std::string& methodName, std::vector& paramTypes) { auto parenOpen = sig.find('('); if (parenOpen == std::string::npos) return false; std::string prefix = sig.substr(0, parenOpen); auto lastSpace = prefix.rfind(' '); methodName = (lastSpace != std::string::npos) ? prefix.substr(lastSpace + 1) : prefix; if (methodName.empty()) return false; auto parenClose = sig.rfind(')'); if (parenClose == std::string::npos || parenClose <= parenOpen) { paramTypes.clear(); return true; } std::string paramStr = trim(sig.substr(parenOpen + 1, parenClose - parenOpen - 1)); paramTypes = splitParams(paramStr); return true; } static uintptr_t parseHexAddress(const std::string& hexStr) { try { return std::stoull(hexStr, nullptr, 16); } catch (...) { return 0; } } /// Match a stored param type against a queried type name. /// Supports short name matching: stored "System.Int32" matches query "Int32". static bool typeMatches(const std::string& stored, const std::string& query) { if (query == "*") return true; if (stored == query) return true; auto dotPos = stored.rfind('.'); if (dotPos != std::string::npos && stored.substr(dotPos + 1) == query) return true; dotPos = query.rfind('.'); if (dotPos != std::string::npos && query.substr(dotPos + 1) == stored) return true; return false; } // ─── Class::GetMethod ─────────────────────────────────────────────── Method* Class::GetMethod(const std::string& methodName, const std::vector& args) { auto it = methods.find(methodName); if (it == methods.end()) return nullptr; auto& overloads = it->second; if (args.empty()) { return overloads.empty() ? nullptr : &overloads[0]; } // exact type match for (auto& m : overloads) { if (m.paramCount != static_cast(args.size())) continue; bool match = true; for (int i = 0; i < m.paramCount; i++) { if (!typeMatches(m.paramTypes[i], args[i])) { match = false; break; } } if (match) return &m; } // fallback: match by param count only for (auto& m : overloads) { if (m.paramCount == static_cast(args.size())) return &m; } return nullptr; } std::filesystem::path GetBasePath() { return GakumasLocal::Plugin::GetInstance().GetHookInstaller()->localizationFilesDir; } // ─── Flat binary format structs ───────────────────────────────────── // // Matches the output of convert_il2cpp_json_to_bin.py. // All string parsing (parseGroup, parseDotNetSignature) is done offline // by the Python script; the binary contains pre-processed data. #pragma pack(push, 1) struct BinHeader { char magic[4]; // "ILCB" uint32_t version; // 1 uint32_t methodCount; uint32_t totalParamCount; }; struct BinMethodEntry { uint32_t assemblyOff, assemblyLen; uint32_t namespaceOff, namespaceLen; uint32_t classnameOff, classnameLen; uint32_t methodnameOff, methodnameLen; uint32_t paramCount; uint32_t paramsStartIdx; uint64_t rva; }; struct BinParamRef { uint32_t strOff; uint32_t strLen; }; #pragma pack(pop) // ─── Public API ───────────────────────────────────────────────────── void LoadIl2cppAddress() { const std::string path = (GetBasePath() / "il2cpp_map.json").string(); std::ifstream file(path); if (!file.is_open()) return; nlohmann::json root; try { root = nlohmann::json::parse(file); } catch (const std::exception&) { return; } if (!root.is_object()) return; for (auto& [name, value] : root.items()) { uintptr_t addr = 0; if (value.is_number_unsigned()) { addr = value.get(); } else if (value.is_string()) { addr = parseHexAddress(value.get()); } else if (value.is_number_integer()) { addr = static_cast(value.get()); } if (addr != 0) { GetIl2cppAddressMap()[name] = addr; } } } static bool InitFromBin(const std::string& path, uintptr_t baseAddress) { std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file.is_open()) return false; auto fileSize = static_cast(file.tellg()); if (fileSize < sizeof(BinHeader)) return false; file.seekg(0); std::vector buf(fileSize); file.read(buf.data(), static_cast(fileSize)); if (!file) return false; const char* data = buf.data(); const auto* header = reinterpret_cast(data); if (std::memcmp(header->magic, "ILCB", 4) != 0 || header->version != 1) { GakumasLocal::Log::Error("Il2cppJson::InitFromBin: invalid header"); return false; } size_t methodsOffset = sizeof(BinHeader); size_t paramsOffset = methodsOffset + header->methodCount * sizeof(BinMethodEntry); size_t stringsOffset = paramsOffset + header->totalParamCount * sizeof(BinParamRef); if (stringsOffset > fileSize) { GakumasLocal::Log::Error("Il2cppJson::InitFromBin: file truncated"); return false; } const auto* methods = reinterpret_cast(data + methodsOffset); const auto* params = reinterpret_cast(data + paramsOffset); const char* strings = data + stringsOffset; size_t stringsSize = fileSize - stringsOffset; auto getString = [&](uint32_t off, uint32_t len) -> std::string { if (off + len > stringsSize) return ""; return {strings + off, len}; }; int parsedCount = 0; for (uint32_t i = 0; i < header->methodCount; i++) { const auto& me = methods[i]; std::string assembly = getString(me.assemblyOff, me.assemblyLen); std::string nameSpace = getString(me.namespaceOff, me.namespaceLen); std::string clsName = getString(me.classnameOff, me.classnameLen); std::string methName = getString(me.methodnameOff, me.methodnameLen); uintptr_t execAddr = baseAddress + static_cast(me.rva); Class& cls = GetClassMap()[assembly][nameSpace][clsName]; if (cls.assemblyName.empty()) { cls.assemblyName = assembly; cls.namespaceName = nameSpace; cls.className = clsName; } Method method; method.name = methName; method.paramCount = static_cast(me.paramCount); method.address = execAddr; method.paramTypes.reserve(me.paramCount); for (uint32_t j = 0; j < me.paramCount; j++) { const auto& pr = params[me.paramsStartIdx + j]; method.paramTypes.push_back(getString(pr.strOff, pr.strLen)); } cls.methods[methName].push_back(std::move(method)); parsedCount++; } s_initialized = true; GakumasLocal::Log::InfoFmt( "Il2cppJson::InitFromBin: loaded %d methods from %s", parsedCount, path.c_str()); return true; } static bool InitFromJson(uintptr_t baseAddress) { const std::string path = GetBasePath() / "il2cpp.json"; if (path.empty()) { GakumasLocal::Log::Error("Il2cppJson::InitFromJson: cannot determine JSON path"); return false; } GakumasLocal::Log::InfoFmt("Il2cppJson::InitFromJson: loading %s (base=0x%lx)", path.c_str(), static_cast(GetUnityBaseAddress())); std::ifstream file(path); if (!file.is_open()) { GakumasLocal::Log::ErrorFmt("Il2cppJson::InitFromJson: cannot open %s", path.c_str()); return false; } nlohmann::json root; try { root = nlohmann::json::parse(file); } catch (const std::exception& e) { GakumasLocal::Log::ErrorFmt("Il2cppJson::InitFromJson: JSON parse error: %s", e.what()); return false; } if (!root.contains("addressMap") || !root["addressMap"].contains("methodDefinitions")) { GakumasLocal::Log::Error("Il2cppJson::InitFromJson: missing addressMap.methodDefinitions"); return false; } const auto& defs = root["addressMap"]["methodDefinitions"]; int parsedCount = 0; int errorCount = 0; for (const auto& entry : defs) { if (!entry.contains("group") || !entry.contains("dotNetSignature") || !entry.contains("virtualAddress")) { errorCount++; continue; } std::string group = entry["group"].get(); std::string dotNetSig = entry["dotNetSignature"].get(); std::string vaStr = entry["virtualAddress"].get(); std::string assembly, nameSpace, clsName; if (!parseGroup(group, assembly, nameSpace, clsName)) { errorCount++; continue; } std::string methodName; std::vector paramTypes; if (!parseDotNetSignature(dotNetSig, methodName, paramTypes)) { errorCount++; continue; } uintptr_t rva = parseHexAddress(vaStr); uintptr_t execAddr = GetUnityBaseAddress() + rva; Class& cls = GetClassMap()[assembly][nameSpace][clsName]; if (cls.assemblyName.empty()) { cls.assemblyName = assembly; cls.namespaceName = nameSpace; cls.className = clsName; } Method method; method.name = methodName; method.paramTypes = std::move(paramTypes); method.paramCount = static_cast(method.paramTypes.size()); method.address = execAddr; cls.methods[methodName].push_back(std::move(method)); parsedCount++; } s_initialized = true; GakumasLocal::Log::InfoFmt( "Il2cppJson::InitFromJson: parsed %d methods (%d skipped) from %s", parsedCount, errorCount, path.c_str()); return true; } bool Init(uintptr_t baseAddress) { if (s_initialized) return true; GetUnityBaseAddress() = baseAddress; GakumasLocal::Log::InfoFmt("Set s_baseAddress to %p, now: %p", (void*)baseAddress, (void*)GetUnityBaseAddress()); LoadIl2cppAddress(); const std::string binPath = (GetBasePath() / "il2cpp.bin").string(); if (InitFromBin(binPath, baseAddress)) { return true; } GakumasLocal::Log::Info("Il2cppJson::Init: .bin not found or invalid"); return false; // return InitFromJson(baseAddress); } Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName, const std::string& className) { if (!s_initialized) return nullptr; auto asmIt = GetClassMap().find(assemblyName); if (asmIt == GetClassMap().end()) return nullptr; auto& nsMap = asmIt->second; // exact namespace lookup auto nsIt = nsMap.find(nameSpaceName); if (nsIt != nsMap.end()) { auto clsIt = nsIt->second.find(className); if (clsIt != nsIt->second.end()) return &clsIt->second; } // when namespace is empty and not found above, scan all namespaces if (nameSpaceName.empty()) { for (auto& [ns, clsMap] : nsMap) { auto clsIt = clsMap.find(className); if (clsIt != clsMap.end()) return &clsIt->second; } } return nullptr; } Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName, const std::string& className, const std::string& methodName, const std::vector& args) { auto* cls = GetClass(assemblyName, nameSpaceName, className); if (!cls) { GakumasLocal::Log::ErrorFmt("GetMethod failed: class not found. class: %s::%s, method: %s", nameSpaceName.c_str(), className.c_str(), methodName.c_str()); return nullptr; } auto* ret = cls->GetMethod(methodName, args); if (!ret) { if (methodName == ".ctor") { ret = cls->GetMethod(className, args); if (ret) return ret; } GakumasLocal::Log::ErrorFmt("GetMethod failed: method not found. class: %s::%s, method: %s", nameSpaceName.c_str(), className.c_str(), methodName.c_str()); } return ret; } } // namespace Il2cppJson