gkms-localify-ios/GakumasLocalify/il2cpp_dump/Il2cppJson.cpp

504 lines
16 KiB
C++

#include "Il2cppJson.hpp"
#include <nlohmann/json.hpp>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#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<std::string> 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<std::string> splitParams(const std::string& paramStr) {
std::vector<std::string> 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<std::string>& 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<std::string>& 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<int>(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<int>(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<uintptr_t>();
} else if (value.is_string()) {
addr = parseHexAddress(value.get<std::string>());
} else if (value.is_number_integer()) {
addr = static_cast<uintptr_t>(value.get<int64_t>());
}
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<size_t>(file.tellg());
if (fileSize < sizeof(BinHeader)) return false;
file.seekg(0);
std::vector<char> buf(fileSize);
file.read(buf.data(), static_cast<std::streamsize>(fileSize));
if (!file) return false;
const char* data = buf.data();
const auto* header = reinterpret_cast<const BinHeader*>(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<const BinMethodEntry*>(data + methodsOffset);
const auto* params = reinterpret_cast<const BinParamRef*>(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<uintptr_t>(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<int>(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<unsigned long>(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>();
std::string dotNetSig = entry["dotNetSignature"].get<std::string>();
std::string vaStr = entry["virtualAddress"].get<std::string>();
std::string assembly, nameSpace, clsName;
if (!parseGroup(group, assembly, nameSpace, clsName)) {
errorCount++;
continue;
}
std::string methodName;
std::vector<std::string> 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<int>(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<std::string>& 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