504 lines
16 KiB
C++
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
|