diff --git a/.gitignore b/.gitignore index 504e4fd..8c8d88a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,5 @@ resources/text_dumper/bin .vscode/ /backend /utils/events.br + +gakumas-texture-TL diff --git a/TEXTURE_REPLACE_INTEGRATION.md b/TEXTURE_REPLACE_INTEGRATION.md new file mode 100644 index 0000000..05a624f --- /dev/null +++ b/TEXTURE_REPLACE_INTEGRATION.md @@ -0,0 +1,191 @@ +# Gakumas纹理替换功能融合说明 + +## 概述 + +本项目已成功将 `gakumas-texture-TL` 项目的纹理替换功能完整集成到主项目中。现在只需要使用 `version.dll` 一个文件即可同时实现文字翻译和纹理替换功能。 + +## 主要改动 + +### 1. 新增模块 + +#### TextureReplace模块 (`src/GakumasLocalify/TextureReplace.h/cpp`) +- 负责纹理替换的核心功能 +- 包含资源映射配置加载 +- 图片文件加载和Unity Texture2D对象创建 +- 待替换请求管理 + +### 2. Hook集成 + +#### 修改 `src/GakumasLocalify/Hook.cpp` +- 在 `AssetBundle_LoadAssetAsync` Hook中添加纹理替换检测 +- 在 `AssetBundleRequest_GetResult` Hook中返回替换后的纹理 +- 在初始化流程中调用 `TextureReplace::Initialize()` + +### 3. 配置系统 + +#### Config配置项 (`src/GakumasLocalify/config/`) +- 添加 `enableTextureReplace` 配置项 +- 在 `Config.hpp` 中声明 +- 在 `Config.cpp` 中实现加载和保存 + +### 4. 资源目录结构 + +``` +resource/local-files/textures/ +├── asset_mapping.txt # 资源映射配置文件 +├── comic/ # 漫画图片 +│ ├── 1ko/ # 一格漫画 +│ └── 4ko/ # 四格漫画 +├── general_report/ # 好感度偶像档案 +├── ui/ # UI按钮图片 +└── tutorial/ # 教程图片 +``` + +## 使用方法 + +### 1. 启用纹理替换功能 + +编辑 `resource/localizationConfig.json`,添加或修改: +```json +{ + "enableTextureReplace": true +} +``` + +### 2. 配置资源映射 + +编辑 `resource/local-files/textures/asset_mapping.txt`: +``` +# 格式: assetId=本地文件路径 +img_general_comic_0001=comic/1ko/img_general_comic_0001.png +img_general_comic4_0000=comic/4ko/img_general_comic4_0000.png +img_general_report_akapen_amao-001=general_report/amao/img_general_report_akapen_amao-001.png +``` + +### 3. 放置图片文件 + +将替换用的图片文件放置到对应的子目录中: +- `comic/1ko/` - 一格漫画 +- `comic/4ko/` - 四格漫画 +- `general_report/` - 好感度档案 +- `ui/` - UI按钮 +- `tutorial/` - 教程图片 + +### 4. 启动游戏 + +只需要将编译生成的 `version.dll` 放到游戏目录即可,无需其他DLL文件。 + +## 技术细节 + +### Hook工作流程 + +1. **AssetBundle_LoadAssetAsync Hook** + - 拦截Unity的资源加载请求 + - 检查资源名称是否在映射表中 + - 如果需要替换,加载本地图片文件 + - 创建Unity Texture2D对象 + - 将替换信息存储到待处理映射表中 + +2. **AssetBundleRequest_GetResult Hook** + - 拦截Unity获取加载结果的请求 + - 检查是否有待替换的纹理 + - 如果有,返回替换后的纹理对象 + - 否则返回原始资源 + +3. **自动清理机制** + - 每30秒自动清理超时(60秒)的待处理请求 + - 防止内存泄漏 + +### IL2CPP API使用 + +通过动态获取IL2CPP函数指针实现: +- `il2cpp_domain_get` - 获取应用域 +- `il2cpp_class_from_name` - 查找类 +- `il2cpp_object_new` - 创建对象 +- `il2cpp_runtime_invoke` - 调用方法 + +### 与xinput1_3.dll版本的区别 + +| 特性 | xinput1_3.dll版本 | version.dll集成版本 | +|------|-------------------|---------------------| +| DLL数量 | 2个 (version.dll + xinput1_3.dll) | 1个 (version.dll) | +| 功能集成 | 分离 | 统一 | +| 配置管理 | 独立配置 | 统一配置系统 | +| 日志系统 | 独立日志 | 统一日志系统 | +| 维护性 | 需要维护两套代码 | 单一代码库 | + +## 迁移指南 + +如果你之前使用xinput1_3.dll版本: + +1. 备份你的图片文件和 `asset_mapping.txt` +2. 将 `asset_mapping.txt` 复制到 `resource/local-files/textures/` +3. 将所有图片文件按目录结构复制到 `resource/local-files/textures/` 对应子目录 +4. 在 `resource/localizationConfig.json` 中设置 `"enableTextureReplace": true` +5. 删除游戏目录中的 `xinput1_3.dll` +6. 使用新编译的 `version.dll` 替换原有的文件 + +## 注意事项 + +1. **性能影响**: 纹理替换只对配置文件中列出的资源生效,不会影响其他资源的加载性能 + +2. **图片格式**: 支持PNG、JPG等Unity支持的常见图片格式 + +3. **路径分隔符**: 在 `asset_mapping.txt` 中,可以使用正斜杠(/)或反斜杠(\\) + +4. **日志输出**: 纹理替换相关的日志会输出到控制台和日志文件中,前缀为 "TextureReplace:" + +5. **错误处理**: 如果本地文件不存在或加载失败,会自动使用游戏原始资源 + +## 调试 + +启用调试模式查看详细日志: +```json +{ + "enableConsole": true, + "dbgMode": true, + "enableTextureReplace": true +} +``` + +日志会显示: +- 初始化过程 +- 加载的映射数量 +- 每次资源替换的详细信息 +- 错误和警告信息 + +## 常见问题 + +**Q: 纹理没有被替换?** +A: 检查以下几点: +1. `enableTextureReplace` 是否设置为 `true` +2. `asset_mapping.txt` 中的资源ID是否正确 +3. 图片文件是否存在于指定路径 +4. 查看日志输出是否有错误信息 + +**Q: 游戏启动后控制台显示什么?** +A: 如果启用纹理替换,会看到: +``` +TextureReplace: Initializing texture replacement system... +TextureReplace: Created texture directories at: ... +TextureReplace: IL2CPP API initialized successfully +TextureReplace: Loaded X asset mappings +TextureReplace: Texture replacement system initialized successfully +``` + +**Q: 可以动态添加新的替换图片吗?** +A: 可以,修改 `asset_mapping.txt` 并添加新图片后,重启游戏即可生效 + +## 开发者信息 + +- 原xinput1_3版本: gakumas-texture-TL项目 +- 集成版本: 本项目 +- 集成时间: 2025年11月26日 + +## 技术支持 + +如遇到问题,请: +1. 检查日志输出 +2. 确认配置文件格式正确 +3. 验证图片文件可以正常打开 +4. 查看游戏目录权限设置 diff --git a/TEXTURE_REPLACE_MERGED.md b/TEXTURE_REPLACE_MERGED.md new file mode 100644 index 0000000..3951aa6 --- /dev/null +++ b/TEXTURE_REPLACE_MERGED.md @@ -0,0 +1,116 @@ +# 纹理替换功能已集成 + +## 摘要 + +**gakumas-texture-TL** 文件夹的图片替换功能已完全融合进主项目。现在只需使用 **version.dll** 一个文件即可同时实现: +- ✅ 文字翻译 +- ✅ 纹理/图片替换 + +**不再需要** xinput1_3.dll! + +## 快速开始 + +### 1. 启用功能 + +编辑 `resource/localizationConfig.json`: +```json +{ + "enableTextureReplace": true +} +``` + +### 2. 配置资源映射 + +编辑 `resource/local-files/textures/asset_mapping.txt`: +``` +img_general_comic_0001=comic/1ko/img_general_comic_0001.png +img_general_comic4_0000=comic/4ko/img_general_comic4_0000.png +``` + +### 3. 放置图片 + +将图片文件放到 `resource/local-files/textures/` 对应子目录: +``` +textures/ +├── comic/1ko/ # 一格漫画 +├── comic/4ko/ # 四格漫画 +├── general_report/ # 好感度档案 +├── ui/ # UI按钮 +└── tutorial/ # 教程图片 +``` + +### 4. 使用 + +只需将 `version.dll` 放到游戏目录即可! + +## 目录结构变化 + +``` +gkms-localify-dmm/ +├── gakumas-texture-TL/ # 可以保留作为参考,不再需要编译 +├── src/ +│ └── GakumasLocalify/ +│ ├── TextureReplace.h # 新增: 纹理替换模块 +│ ├── TextureReplace.cpp # 新增: 纹理替换实现 +│ ├── Hook.cpp # 修改: 集成纹理Hook +│ └── config/ +│ ├── Config.hpp # 修改: 添加enableTextureReplace +│ └── Config.cpp # 修改: 配置加载/保存 +├── resource/ +│ ├── localizationConfig.json # 修改: 添加enableTextureReplace配置项 +│ └── local-files/ +│ └── textures/ # 新增: 纹理替换资源目录 +│ ├── README.md # 使用说明 +│ ├── asset_mapping.txt # 资源映射配置 +│ ├── comic/ +│ ├── general_report/ +│ ├── ui/ +│ └── tutorial/ +└── docs/ + └── TEXTURE_REPLACE_INTEGRATION.md # 详细集成文档 +``` + +## 核心改动 + +### 新增文件 +1. `src/GakumasLocalify/TextureReplace.h/cpp` - 纹理替换核心模块 +2. `resource/local-files/textures/README.md` - 使用说明 +3. `docs/TEXTURE_REPLACE_INTEGRATION.md` - 详细集成文档 + +### 修改文件 +1. `src/GakumasLocalify/Hook.cpp` - 集成AssetBundle Hook +2. `src/GakumasLocalify/config/Config.hpp` - 添加配置项声明 +3. `src/GakumasLocalify/config/Config.cpp` - 实现配置加载 +4. `resource/localizationConfig.json` - 添加enableTextureReplace + +## 技术亮点 + +- **统一Hook系统**: 复用现有的AssetBundle Hook +- **配置集成**: 纳入统一的配置管理系统 +- **日志集成**: 使用统一的日志输出 +- **目录规范**: 遵循项目现有的目录结构 +- **零依赖**: 无需额外的DLL文件 + +## 迁移建议 + +如果你之前使用 xinput1_3.dll: + +1. 复制 `gakumas-texture-TL/gakumas-local-texture/asset_mapping.txt` 到 `resource/local-files/textures/` +2. 复制所有图片文件到 `resource/local-files/textures/` 对应子目录 +3. 在配置文件中启用 `enableTextureReplace` +4. 删除 `xinput1_3.dll` +5. 只使用 `version.dll` + +## 详细文档 + +查看 [docs/TEXTURE_REPLACE_INTEGRATION.md](docs/TEXTURE_REPLACE_INTEGRATION.md) 了解: +- 完整技术细节 +- Hook工作流程 +- 调试方法 +- 常见问题解答 + +## 开发者备注 + +- 集成完成日期: 2025年11月26日 +- xinput1_3.dll 的所有功能已通过 version.dll 实现 +- gakumas-texture-TL 文件夹可以保留作为参考,但不再需要编译使用 diff --git a/resource/local-files/textures/README.md b/resource/local-files/textures/README.md new file mode 100644 index 0000000..48e907f --- /dev/null +++ b/resource/local-files/textures/README.md @@ -0,0 +1,61 @@ +# 纹理替换功能说明 + +## 目录结构 + +``` +resource/local-files/textures/ +├── asset_mapping.txt # 资源映射配置文件 +├── comic/ # 漫画图片 +│ ├── 1ko/ # 一格漫画 +│ └── 4ko/ # 四格漫画 +├── general_report/ # 好感度偶像档案 +│ ├── amao/ # 麻央 +│ └── hmsz/ # 美铃 +├── ui/ # UI按钮图片 +└── tutorial/ # 教程图片 +``` + +## 配置文件说明 + +`asset_mapping.txt` 文件用于配置资源ID与本地文件的映射关系。 + +格式: `assetId=本地文件路径` + +示例: +``` +# 一格漫画 +img_general_comic_0001=comic/1ko/img_general_comic_0001.png + +# 四格漫画 +img_general_comic4_0000=comic/4ko/img_general_comic4_0000.png + +# 好感度偶像档案 +img_general_report_akapen_amao-001=general_report/amao/img_general_report_akapen_amao-001.png +``` + +## 启用方式 + +在 `resource/config.json` 中添加: +```json +{ + "enableTextureReplace": true +} +``` + +## 注意事项 + +1. 路径使用正斜杠 `/` 或反斜杠 `\` 均可 +2. 路径相对于 `resource/local-files/textures/` 目录 +3. 支持 PNG、JPG 等常见图片格式 +4. 只有在配置文件中映射的资源才会被替换 +5. 如果本地文件不存在,将使用游戏原始资源 + +## 迁移说明 + +如果你之前使用 xinput1_3.dll 版本的纹理替换插件: + +1. 将 `gakumas-local-texture/asset_mapping.txt` 复制到此目录 +2. 将所有图片文件复制到对应的子目录中 +3. 在 `config.json` 中启用 `enableTextureReplace` +4. 删除 xinput1_3.dll 文件 +5. 只保留 version.dll 即可 diff --git a/resource/local-files/textures/asset_mapping.txt b/resource/local-files/textures/asset_mapping.txt new file mode 100644 index 0000000..61fd505 --- /dev/null +++ b/resource/local-files/textures/asset_mapping.txt @@ -0,0 +1,69 @@ +# Asset映射配置文件 +# 格式: assetId=本地文件路径 +# 注意: 路径使用正斜杠/,支持相对路径和绝对路径 + +# 一格漫画映射 +img_general_comic_0001=comic/1ko/img_general_comic_0001.png +img_general_comic_0002=comic/1ko/img_general_comic_0002.png +img_general_comic_0003=comic/1ko/img_general_comic_0003.png +img_general_comic_0004=comic/1ko/img_general_comic_0004.png +img_general_comic_0005=comic/1ko/img_general_comic_0005.png +img_general_comic_0006=comic/1ko/img_general_comic_0006.png + + +# 四格漫画映射 +img_general_comic4_0000=comic/4ko/img_general_comic4_0000.png +img_general_comic4_0001=comic/4ko/img_general_comic4_0001.png +img_general_comic4_0002=comic/4ko/img_general_comic4_0002.png +img_general_comic4_0003=comic/4ko/img_general_comic4_0003.png +img_general_comic4_0004=comic/4ko/img_general_comic4_0004.png +img_general_comic4_0005=comic/4ko/img_general_comic4_0005.png +img_general_comic4_0006=comic/4ko/img_general_comic4_0006.png +img_general_comic4_0007=comic/4ko/img_general_comic4_0007.png +img_general_comic4_0008=comic/4ko/img_general_comic4_0008.png + +img_general_comic4_0000-thumb=comic/4ko/img_general_comic4_0000-thumb.png +img_general_comic4_0001-thumb=comic/4ko/img_general_comic4_0001-thumb.png +img_general_comic4_0002-thumb=comic/4ko/img_general_comic4_0002-thumb.png +img_general_comic4_0003-thumb=comic/4ko/img_general_comic4_0003-thumb.png +img_general_comic4_0004-thumb=comic/4ko/img_general_comic4_0004-thumb.png +img_general_comic4_0005-thumb=comic/4ko/img_general_comic4_0005-thumb.png +img_general_comic4_0006-thumb=comic/4ko/img_general_comic4_0006-thumb.png +img_general_comic4_0007-thumb=comic/4ko/img_general_comic4_0007-thumb.png +img_general_comic4_0008-thumb=comic/4ko/img_general_comic4_0008-thumb.png + + +# 好感度偶像档案映射 +#麻央 +img_general_report_akapen_amao-001=general_report/amao/img_general_report_akapen_amao-001.png + +# 美铃 +img_general_report_akapen_hmsz-001=general_report/hmsz/img_general_report_akapen_hmsz-001.png +img_general_report_akapen_hmsz-002=general_report/hmsz/img_general_report_akapen_hmsz-002.png +img_general_report_akapen_hmsz-003=general_report/hmsz/img_general_report_akapen_hmsz-003.png +img_general_report_akapen_hmsz-004=general_report/hmsz/img_general_report_akapen_hmsz-004.png +img_general_report_akapen_hmsz-005=general_report/hmsz/img_general_report_akapen_hmsz-005.png +img_general_report_akapen_hmsz-006=general_report/hmsz/img_general_report_akapen_hmsz-006.png +img_general_report_bg_graph_hmsz-001=general_report/hmsz/img_general_report_bg_graph_hmsz-001.png +img_general_report_text_hmsz-001=general_report/hmsz/img_general_report_text_hmsz-001.png +img_general_report_text_hmsz-002=general_report/hmsz/img_general_report_text_hmsz-002.png +img_general_report_text_hmsz-003=general_report/hmsz/img_general_report_text_hmsz-003.png + + +# 主页Produce按钮映射 +img_general_ui_produce-1_btn=ui/button/img_general_ui_produce-1_btn.png +img_general_ui_produce-1_btn-small=ui/button/img_general_ui_produce-1_btn-small.png +img_general_ui_produce-2_btn=ui/button/img_general_ui_produce-2_btn.png +img_general_ui_produce-2_btn-small=ui/button/img_general_ui_produce-2_btn-small.png +img_general_ui_produce-3_btn-small=ui/button/img_general_ui_produce-3_btn-small.png +img_general_ui_produce-nia_btn=ui/button/img_general_ui_produce-nia_btn.png +img_general_ui_produce-nia-2_btn=ui/button/img_general_ui_produce-nia-2_btn.png + + +# 教程页面映射 +img_tutorial_produce_01_first-001=tutorial/img_tutorial_produce_01_first-001.png + + +# 添加更多您需要替换的assetId +# your_asset_id=path/to/your/custom/image.png + diff --git a/resource/localizationConfig.json b/resource/localizationConfig.json index 5d38ba7..42ae683 100644 --- a/resource/localizationConfig.json +++ b/resource/localizationConfig.json @@ -9,6 +9,7 @@ "gameOrientation": 0, "forceExportResource": false, "enableFreeCamera": false, + "enableTextureReplace": false, "targetFrameRate": 0, "unlockAllLive": true, "unlockAllLiveCostume": true, @@ -16,6 +17,7 @@ "liveCustomeHeadId": "costume_head_fktn-cstm-0001", "liveCustomeCostumeId": "hume-othr-0000", "loginAsIOS": false, + "skipPurchaseInit": true, "useCustomeGraphicSettings": false, "renderScale": 0.77, "qualitySettingsLevel": 3, diff --git a/src/GakumasLocalify/Hook.cpp b/src/GakumasLocalify/Hook.cpp index 78697b2..d6ea711 100644 --- a/src/GakumasLocalify/Hook.cpp +++ b/src/GakumasLocalify/Hook.cpp @@ -5,6 +5,7 @@ #include "Il2cppUtils.hpp" #include "Local.h" #include "MasterLocal.h" +#include "TextureReplace.h" #include #include "camera/camera.hpp" #include "config/Config.hpp" @@ -265,10 +266,62 @@ namespace GakumasLocal::HookMain { // Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str()); auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type); loadHistory.emplace(ret, name->ToString()); + + // 纹理替换功能: 检查是否需要替换此资源 + if (Config::enableTextureReplace) { + std::string assetName = name->ToString(); + std::string replacementPath; + + if (TextureReplace::ShouldReplaceAsset(assetName, replacementPath)) { + Log::InfoFmt("TextureReplace: Detected asset for replacement: %s -> %s", + assetName.c_str(), replacementPath.c_str()); + + // 尝试加载自定义纹理 + void* customTexture = TextureReplace::LoadCustomTexture(replacementPath); + if (customTexture) { + Log::InfoFmt("TextureReplace: Custom texture loaded, storing for request: 0x%p", ret); + TextureReplace::PendingReplacement pending; + pending.texture = customTexture; + pending.timestamp = GetTickCount(); + TextureReplace::g_pendingReplacements[ret] = pending; + } else { + Log::InfoFmt("TextureReplace: Failed to load custom texture from: %s", replacementPath.c_str()); + } + } + } + return ret; } DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* self)) { + // 定期清理过期的待替换请求 + if (Config::enableTextureReplace) { + static DWORD lastCleanupTime = 0; + DWORD currentTime = GetTickCount(); + if (currentTime - lastCleanupTime > 30000) { // 每30秒清理一次 + TextureReplace::CleanupPendingReplacements(); + lastCleanupTime = currentTime; + } + + // 检查此请求是否有待替换的纹理 + auto it = TextureReplace::g_pendingReplacements.find(self); + if (it != TextureReplace::g_pendingReplacements.end()) { + Log::InfoFmt("TextureReplace: Returning custom texture for request: 0x%p", self); + void* customTexture = it->second.texture; + + // 从待处理列表中移除 + TextureReplace::g_pendingReplacements.erase(it); + + // 清理loadHistory + if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) { + loadHistory.erase(iter); + } + + Log::InfoFmt("TextureReplace: Successfully returned custom texture: 0x%p", customTexture); + return customTexture; + } + } + auto result = AssetBundleRequest_GetResult_Orig(self); if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) { const auto name = iter->second; @@ -1873,6 +1926,12 @@ namespace GakumasLocal::HookMain { Local::LoadData(); MasterLocal::LoadData(); + + // 初始化纹理替换系统 + if (Config::enableTextureReplace) { + Log::Info("Initializing texture replacement system..."); + TextureReplace::Initialize(); + } UnityResolveProgress::startInit = false; diff --git a/src/GakumasLocalify/Hook.h b/src/GakumasLocalify/Hook.h index 0671560..4d09d36 100644 --- a/src/GakumasLocalify/Hook.h +++ b/src/GakumasLocalify/Hook.h @@ -2,6 +2,16 @@ #define GAKUMAS_LOCALIFY_HOOK_H #include +#include + + +namespace GakumasLocal { + // 获取 AssetBundle 句柄(在 Hook.cpp 定义,TextureReplace.cpp 需要用) + uint32_t GetBundleHandleByAssetName(std::string assetName); + bool FindAssetBundleAssetByFilename(const std::string& filename, + std::string& assetPath, + uint32_t& handle); +} namespace GakumasLocal::Hook { diff --git a/src/GakumasLocalify/TextureReplace.cpp b/src/GakumasLocalify/TextureReplace.cpp new file mode 100644 index 0000000..f0b30e7 --- /dev/null +++ b/src/GakumasLocalify/TextureReplace.cpp @@ -0,0 +1,372 @@ +#include "TextureReplace.h" +#include "Log.h" +#include "Il2cppUtils.hpp" +#include "../il2cpp/il2cpp_symbols.hpp" +#include +#include +#include + +extern std::filesystem::path gakumasLocalPath; + +namespace GakumasLocal::TextureReplace { + // IL2CPP string structure + struct Il2CppString { + void* klass; + void* monitor; + int length; + wchar_t chars[1]; + }; + + // 资源映射表: assetId -> 本地文件路径 + std::map g_assetMappings; + + // 待替换的请求映射表 + std::map g_pendingReplacements; + + // 纹理替换基础目录 + std::filesystem::path g_textureBaseDir; + + // IL2CPP API函数指针 + static void* (*il2cpp_domain_get)() = nullptr; + static void** (*il2cpp_domain_get_assemblies)(void*, size_t*) = nullptr; + static void* (*il2cpp_assembly_get_image)(void*) = nullptr; + static void* (*il2cpp_class_from_name)(void*, const char*, const char*) = nullptr; + static void* (*il2cpp_object_new)(void*) = nullptr; + static void* (*il2cpp_class_get_method_from_name)(void*, const char*, int) = nullptr; + static void* (*il2cpp_runtime_invoke)(void*, void*, void**, void**) = nullptr; + static void* (*il2cpp_array_new)(void*, size_t) = nullptr; + + // 安全转换IL2CPP字符串为std::string + std::string Il2CppStringToStdString(void* il2cppString) { + if (!il2cppString) return ""; + + if (IsBadReadPtr(il2cppString, sizeof(Il2CppString))) { + return ""; + } + + Il2CppString* str = (Il2CppString*)il2cppString; + + if (str->length <= 0 || str->length > 1000) { + return ""; + } + + if (IsBadReadPtr(str->chars, str->length * sizeof(wchar_t))) { + return ""; + } + + int bufferSize = WideCharToMultiByte(CP_UTF8, 0, str->chars, str->length, nullptr, 0, nullptr, nullptr); + if (bufferSize <= 0) return ""; + + std::string result(bufferSize, 0); + WideCharToMultiByte(CP_UTF8, 0, str->chars, str->length, &result[0], bufferSize, nullptr, nullptr); + + return result; + } + + // 初始化IL2CPP API + bool InitializeIL2CPPApi() { + Log::Info("TextureReplace: Initializing IL2CPP API..."); + + HMODULE hGameAssembly = GetModuleHandleA("GameAssembly.dll"); + if (!hGameAssembly) { + Log::Error("TextureReplace: Failed to get GameAssembly.dll handle"); + return false; + } + + il2cpp_domain_get = (void* (*)())GetProcAddress(hGameAssembly, "il2cpp_domain_get"); + il2cpp_domain_get_assemblies = (void** (*)(void*, size_t*))GetProcAddress(hGameAssembly, "il2cpp_domain_get_assemblies"); + il2cpp_assembly_get_image = (void* (*)(void*))GetProcAddress(hGameAssembly, "il2cpp_assembly_get_image"); + il2cpp_class_from_name = (void* (*)(void*, const char*, const char*))GetProcAddress(hGameAssembly, "il2cpp_class_from_name"); + il2cpp_object_new = (void* (*)(void*))GetProcAddress(hGameAssembly, "il2cpp_object_new"); + il2cpp_class_get_method_from_name = (void* (*)(void*, const char*, int))GetProcAddress(hGameAssembly, "il2cpp_class_get_method_from_name"); + il2cpp_runtime_invoke = (void* (*)(void*, void*, void**, void**))GetProcAddress(hGameAssembly, "il2cpp_runtime_invoke"); + il2cpp_array_new = (void* (*)(void*, size_t))GetProcAddress(hGameAssembly, "il2cpp_array_new"); + + if (!il2cpp_domain_get || !il2cpp_domain_get_assemblies || !il2cpp_assembly_get_image || + !il2cpp_class_from_name || !il2cpp_object_new || !il2cpp_class_get_method_from_name || + !il2cpp_runtime_invoke || !il2cpp_array_new) { + Log::Error("TextureReplace: Failed to get IL2CPP API functions"); + return false; + } + + Log::Info("TextureReplace: IL2CPP API initialized successfully"); + return true; + } + + // 初始化纹理替换系统 + bool Initialize() { + Log::Info("TextureReplace: Initializing texture replacement system..."); + + // 设置基础目录 + g_textureBaseDir = gakumasLocalPath / "textures"; + + // 创建目录结构 + try { + std::filesystem::create_directories(g_textureBaseDir); + std::filesystem::create_directories(g_textureBaseDir / "comic" / "1ko"); + std::filesystem::create_directories(g_textureBaseDir / "comic" / "4ko"); + std::filesystem::create_directories(g_textureBaseDir / "general_report"); + std::filesystem::create_directories(g_textureBaseDir / "ui"); + std::filesystem::create_directories(g_textureBaseDir / "tutorial"); + Log::InfoFmt("TextureReplace: Created texture directories at: %s", g_textureBaseDir.string().c_str()); + } + catch (const std::exception& e) { + Log::ErrorFmt("TextureReplace: Failed to create directories: %s", e.what()); + return false; + } + + // 初始化IL2CPP API + if (!InitializeIL2CPPApi()) { + return false; + } + + // 加载资源映射配置 + if (!LoadAssetMappings()) { + Log::Info("TextureReplace: No asset mappings loaded, texture replacement will be disabled"); + return false; + } + + Log::Info("TextureReplace: Texture replacement system initialized successfully"); + return true; + } + + // 加载资源映射配置 + bool LoadAssetMappings() { + std::filesystem::path configPath = g_textureBaseDir / "asset_mapping.txt"; + + if (!std::filesystem::exists(configPath)) { + Log::InfoFmt("TextureReplace: Config file not found: %s", configPath.string().c_str()); + return false; + } + + std::ifstream file(configPath); + if (!file.is_open()) { + Log::ErrorFmt("TextureReplace: Failed to open config file: %s", configPath.string().c_str()); + return false; + } + + g_assetMappings.clear(); + std::string line; + int count = 0; + + while (std::getline(file, line)) { + // 跳过空行和注释 + if (line.empty() || line[0] == '#') continue; + + size_t equalPos = line.find('='); + if (equalPos != std::string::npos) { + std::string assetId = line.substr(0, equalPos); + std::string localPath = line.substr(equalPos + 1); + + // 移除首尾空格 + while (!assetId.empty() && (assetId.back() == ' ' || assetId.back() == '\r')) assetId.pop_back(); + while (!localPath.empty() && (localPath.front() == ' ')) localPath.erase(0, 1); + while (!localPath.empty() && (localPath.back() == ' ' || localPath.back() == '\r')) localPath.pop_back(); + + // 转换为完整路径 + std::filesystem::path fullPath = g_textureBaseDir / localPath; + g_assetMappings[assetId] = fullPath.string(); + count++; + } + } + + file.close(); + Log::InfoFmt("TextureReplace: Loaded %d asset mappings", count); + return count > 0; + } + + // 检查是否需要替换指定资源 + bool ShouldReplaceAsset(const std::string& assetName, std::string& replacementPath) { + auto it = g_assetMappings.find(assetName); + if (it != g_assetMappings.end()) { + replacementPath = it->second; + + // 检查文件是否存在 + if (std::filesystem::exists(replacementPath)) { + return true; + } + else { + Log::InfoFmt("TextureReplace: Mapped file not found: %s", replacementPath.c_str()); + } + } + return false; + } + + // 从文件加载图片数据 + bool LoadImageFile(const std::string& filePath, std::vector& imageData) { + FILE* file = nullptr; + if (fopen_s(&file, filePath.c_str(), "rb") != 0 || !file) { + Log::ErrorFmt("TextureReplace: Failed to open image file: %s", filePath.c_str()); + return false; + } + + fseek(file, 0, SEEK_END); + long fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + if (fileSize <= 0) { + fclose(file); + Log::ErrorFmt("TextureReplace: Invalid file size: %ld", fileSize); + return false; + } + + imageData.resize(fileSize); + size_t bytesRead = fread(imageData.data(), 1, fileSize, file); + fclose(file); + + if (bytesRead != fileSize) { + Log::ErrorFmt("TextureReplace: Failed to read complete file. Expected: %ld, Read: %zu", fileSize, bytesRead); + return false; + } + + Log::InfoFmt("TextureReplace: Loaded image file: %s (%ld bytes)", filePath.c_str(), fileSize); + return true; + } + + // 创建Unity Texture2D对象从图片数据 + void* CreateUnityTexture2DFromImage(const std::vector& imageData, const std::string& fileName) { + if (imageData.empty()) { + Log::Error("TextureReplace: Empty image data"); + return nullptr; + } + + Log::InfoFmt("TextureReplace: Creating Unity Texture2D from %s (%d bytes)", fileName.c_str(), (int)imageData.size()); + + void* domain = il2cpp_domain_get(); + if (!domain) { + Log::Error("TextureReplace: Failed to get IL2CPP domain"); + return nullptr; + } + + size_t assemblyCount = 0; + void** assemblies = il2cpp_domain_get_assemblies(domain, &assemblyCount); + if (!assemblies) { + Log::Error("TextureReplace: Failed to get assemblies"); + return nullptr; + } + + void* texture2DClass = nullptr; + void* imageConversionClass = nullptr; + + // 查找所需的类 + for (size_t i = 0; i < assemblyCount; i++) { + void* assembly = assemblies[i]; + if (!assembly) continue; + + void* image = il2cpp_assembly_get_image(assembly); + if (!image) continue; + + if (!texture2DClass) { + texture2DClass = il2cpp_class_from_name(image, "UnityEngine", "Texture2D"); + } + if (!imageConversionClass) { + imageConversionClass = il2cpp_class_from_name(image, "UnityEngine", "ImageConversion"); + } + + if (texture2DClass && imageConversionClass) break; + } + + if (!texture2DClass) { + Log::Error("TextureReplace: Failed to find Texture2D class"); + return nullptr; + } + + // 创建Texture2D对象 + void* texture2DObject = il2cpp_object_new(texture2DClass); + if (!texture2DObject) { + Log::Error("TextureReplace: Failed to create Texture2D object"); + return nullptr; + } + + // 调用构造函数 (2x2, RGBA32) + void* ctorMethod = il2cpp_class_get_method_from_name(texture2DClass, ".ctor", 2); + if (ctorMethod) { + try { + int width = 2; + int height = 2; + void* ctorParams[2] = { &width, &height }; + void* ctorException = nullptr; + + il2cpp_runtime_invoke(ctorMethod, texture2DObject, ctorParams, &ctorException); + if (ctorException) { + Log::InfoFmt("TextureReplace: Constructor exception: 0x%p", ctorException); + } + } + catch (...) { + Log::Info("TextureReplace: Exception during constructor call"); + } + } + + // 如果有ImageConversion类,使用LoadImage方法加载实际图片数据 + if (imageConversionClass) { + void* loadImageMethod = il2cpp_class_get_method_from_name(imageConversionClass, "LoadImage", 2); + if (loadImageMethod) { + try { + // 创建byte数组 + void* byteArrayClass = nullptr; + for (size_t i = 0; i < assemblyCount; i++) { + void* assembly = assemblies[i]; + if (!assembly) continue; + void* image = il2cpp_assembly_get_image(assembly); + if (!image) continue; + byteArrayClass = il2cpp_class_from_name(image, "System", "Byte"); + if (byteArrayClass) break; + } + + if (byteArrayClass) { + void* byteArray = il2cpp_array_new(byteArrayClass, imageData.size()); + if (byteArray) { + // 复制图片数据到数组 + memcpy((char*)byteArray + 4 * sizeof(void*), imageData.data(), imageData.size()); + + // 调用ImageConversion.LoadImage(texture2D, byteArray) + void* params[2] = { texture2DObject, byteArray }; + void* exception = nullptr; + il2cpp_runtime_invoke(loadImageMethod, nullptr, params, &exception); + + if (exception) { + Log::InfoFmt("TextureReplace: LoadImage exception: 0x%p", exception); + } + else { + Log::InfoFmt("TextureReplace: Successfully loaded image into Texture2D: 0x%p", texture2DObject); + return texture2DObject; + } + } + } + } + catch (...) { + Log::Info("TextureReplace: Exception during LoadImage call"); + } + } + } + + Log::InfoFmt("TextureReplace: Created basic Texture2D object: 0x%p", texture2DObject); + return texture2DObject; + } + + // 加载自定义纹理 + void* LoadCustomTexture(const std::string& filePath) { + std::vector imageData; + if (!LoadImageFile(filePath, imageData)) { + return nullptr; + } + + std::filesystem::path path(filePath); + return CreateUnityTexture2DFromImage(imageData, path.filename().string()); + } + + // 清理待处理的替换请求 + void CleanupPendingReplacements() { + unsigned long currentTime = GetTickCount(); + + for (auto it = g_pendingReplacements.begin(); it != g_pendingReplacements.end();) { + if (currentTime - it->second.timestamp > 60000) { // 超过60秒的请求 + Log::InfoFmt("TextureReplace: Cleaning up expired replacement request: 0x%p", it->first); + it = g_pendingReplacements.erase(it); + } + else { + ++it; + } + } + } +} diff --git a/src/GakumasLocalify/TextureReplace.h b/src/GakumasLocalify/TextureReplace.h new file mode 100644 index 0000000..16bc9e7 --- /dev/null +++ b/src/GakumasLocalify/TextureReplace.h @@ -0,0 +1,40 @@ +#ifndef GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H +#define GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H + +#include +#include +#include + +namespace GakumasLocal::TextureReplace { + // 初始化纹理替换系统 + bool Initialize(); + + // 加载配置映射 + bool LoadAssetMappings(); + + // 检查是否需要替换指定资源 + bool ShouldReplaceAsset(const std::string& assetName, std::string& replacementPath); + + // 从文件加载图片数据 + bool LoadImageFile(const std::string& filePath, std::vector& imageData); + + // 创建Unity Texture2D对象从图片数据 + void* CreateUnityTexture2DFromImage(const std::vector& imageData, const std::string& fileName); + + // 加载自定义纹理 + void* LoadCustomTexture(const std::string& filePath); + + // 清理待处理的替换请求 + void CleanupPendingReplacements(); + + // 待替换请求结构 + struct PendingReplacement { + void* texture; + unsigned long timestamp; + }; + + // 待替换的请求映射表 + extern std::map g_pendingReplacements; +} + +#endif // GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H diff --git a/src/GakumasLocalify/config/Config.cpp b/src/GakumasLocalify/config/Config.cpp index 57ea8e3..5555adb 100644 --- a/src/GakumasLocalify/config/Config.cpp +++ b/src/GakumasLocalify/config/Config.cpp @@ -17,6 +17,7 @@ namespace GakumasLocal::Config { int gameOrientation = 0; bool dumpText = false; bool enableFreeCamera = false; + bool enableTextureReplace = false; int targetFrameRate = 0; bool unlockAllLive = false; bool unlockAllLiveCostume = false; @@ -26,6 +27,7 @@ namespace GakumasLocal::Config { std::string liveCustomeCostumeId = ""; bool loginAsIOS = false; + bool skipPurchaseInit = true; bool useCustomeGraphicSettings = false; float renderScale = 0.77f; @@ -73,12 +75,14 @@ namespace GakumasLocal::Config { GetConfigItem(dumpText); GetConfigItem(targetFrameRate); GetConfigItem(enableFreeCamera); + GetConfigItem(enableTextureReplace); GetConfigItem(unlockAllLive); GetConfigItem(unlockAllLiveCostume); GetConfigItem(enableLiveCustomeDress); GetConfigItem(liveCustomeHeadId); GetConfigItem(liveCustomeCostumeId); GetConfigItem(loginAsIOS); + GetConfigItem(skipPurchaseInit); GetConfigItem(useCustomeGraphicSettings); GetConfigItem(renderScale); GetConfigItem(qualitySettingsLevel); @@ -129,12 +133,14 @@ namespace GakumasLocal::Config { SetConfigItem(dumpText); SetConfigItem(targetFrameRate); SetConfigItem(enableFreeCamera); + SetConfigItem(enableTextureReplace); SetConfigItem(unlockAllLive); SetConfigItem(unlockAllLiveCostume); SetConfigItem(enableLiveCustomeDress); SetConfigItem(liveCustomeHeadId); SetConfigItem(liveCustomeCostumeId); SetConfigItem(loginAsIOS); + SetConfigItem(skipPurchaseInit); SetConfigItem(useCustomeGraphicSettings); SetConfigItem(renderScale); SetConfigItem(qualitySettingsLevel); diff --git a/src/GakumasLocalify/config/Config.hpp b/src/GakumasLocalify/config/Config.hpp index 1147f53..71da539 100644 --- a/src/GakumasLocalify/config/Config.hpp +++ b/src/GakumasLocalify/config/Config.hpp @@ -13,6 +13,7 @@ namespace GakumasLocal::Config { extern bool useMasterTrans; extern bool dumpText; extern bool enableFreeCamera; + extern bool enableTextureReplace; extern int targetFrameRate; extern bool unlockAllLive; extern bool unlockAllLiveCostume; @@ -22,6 +23,7 @@ namespace GakumasLocal::Config { extern std::string liveCustomeCostumeId; extern bool loginAsIOS; + extern bool skipPurchaseInit; extern bool useCustomeGraphicSettings; extern float renderScale; diff --git a/src/il2cpp/il2cpp_symbols.hpp b/src/il2cpp/il2cpp_symbols.hpp index 593f133..df3dc77 100644 --- a/src/il2cpp/il2cpp_symbols.hpp +++ b/src/il2cpp/il2cpp_symbols.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include // UnityEngine.Color struct Color_t