Compare commits

...

2 Commits
main ... main

Author SHA1 Message Date
pm chihya 64809af01c dmm版添加自动更新功能 2025-12-14 03:38:26 +08:00
pm chihya 8a0e332292 添加texture替换功能 2025-12-14 03:20:24 +08:00
15 changed files with 1007 additions and 9 deletions

2
.gitignore vendored
View File

@ -157,3 +157,5 @@ resources/text_dumper/bin
.vscode/
/backend
/utils/events.br
gakumas-texture-TL

View File

@ -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. 查看游戏目录权限设置

116
TEXTURE_REPLACE_MERGED.md Normal file
View File

@ -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 文件夹可以保留作为参考,但不再需要编译使用

View File

@ -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 即可

View File

@ -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

View File

@ -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,

View File

@ -5,6 +5,7 @@
#include "Il2cppUtils.hpp"
#include "Local.h"
#include "MasterLocal.h"
#include "TextureReplace.h"
#include <unordered_set>
#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;
@ -1874,6 +1927,12 @@ namespace GakumasLocal::HookMain {
Local::LoadData();
MasterLocal::LoadData();
// 初始化纹理替换系统
if (Config::enableTextureReplace) {
Log::Info("Initializing texture replacement system...");
TextureReplace::Initialize();
}
UnityResolveProgress::startInit = false;
Log::Info("Plugin init finished.");
@ -1897,5 +1956,11 @@ namespace GakumasLocal::Hook {
Log::Info("Hook installed");
// 延迟启动自动资源更新检查
std::thread([]() {
std::this_thread::sleep_for(std::chrono::seconds(5));
GkmsResourceUpdate::AutoCheckUpdateFromAPI();
}).detach();
}
}

View File

@ -2,6 +2,16 @@
#define GAKUMAS_LOCALIFY_HOOK_H
#include <string>
#include <cstdint>
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
{

View File

@ -0,0 +1,372 @@
#include "TextureReplace.h"
#include "Log.h"
#include "Il2cppUtils.hpp"
#include "../il2cpp/il2cpp_symbols.hpp"
#include <fstream>
#include <filesystem>
#include <windows.h>
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<std::string, std::string> g_assetMappings;
// 待替换的请求映射表
std::map<void*, PendingReplacement> 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<unsigned char>& 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<unsigned char>& 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<unsigned char> 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;
}
}
}
}

View File

@ -0,0 +1,40 @@
#ifndef GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H
#define GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H
#include <string>
#include <map>
#include <vector>
namespace GakumasLocal::TextureReplace {
// 初始化纹理替换系统
bool Initialize();
// 加载配置映射
bool LoadAssetMappings();
// 检查是否需要替换指定资源
bool ShouldReplaceAsset(const std::string& assetName, std::string& replacementPath);
// 从文件加载图片数据
bool LoadImageFile(const std::string& filePath, std::vector<unsigned char>& imageData);
// 创建Unity Texture2D对象从图片数据
void* CreateUnityTexture2DFromImage(const std::vector<unsigned char>& imageData, const std::string& fileName);
// 加载自定义纹理
void* LoadCustomTexture(const std::string& filePath);
// 清理待处理的替换请求
void CleanupPendingReplacements();
// 待替换请求结构
struct PendingReplacement {
void* texture;
unsigned long timestamp;
};
// 待替换的请求映射表
extern std::map<void*, PendingReplacement> g_pendingReplacements;
}
#endif // GAKUMAS_LOCALIFY_TEXTURE_REPLACE_H

View File

@ -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);

View File

@ -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;

View File

@ -1,6 +1,7 @@
#pragma once
#include <concepts>
#include <functional>
// UnityEngine.Color
struct Color_t

View File

@ -47,18 +47,18 @@ namespace GkmsResourceUpdate {
using namespace concurrency::streams;
try {
// 打开输出文件流(同步方式)
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD>
auto outTask = fstream::open_ostream(conversions::to_string_t(outputPath));
outTask.wait();
auto fileStream = outTask.get();
// 创建 HTTP 客户端,注意:如果 url 包含完整路径cpprestsdk 会自动解析
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTTP <20>ͻ<EFBFBD><CDBB>ˣ<EFBFBD>ע<EFBFBD><EFBFBD><E2A3BA><EFBFBD> url <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>·<EFBFBD><C2B7><EFBFBD><EFBFBD>cpprestsdk <20><><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD>
http_client client(conversions::to_string_t(url));
downloading = true;
downloadProgress = 0.0f;
// 发起 GET 请求
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> GET <20><><EFBFBD><EFBFBD>
auto responseTask = client.request(methods::GET);
responseTask.wait();
http_response response = responseTask.get();
@ -68,12 +68,12 @@ namespace GkmsResourceUpdate {
return false;
}
// 获取响应头中的文件大小(如果存在)
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>Ӧͷ<EFBFBD>е<EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD>
uint64_t contentLength = 0;
if (response.headers().has(L"Content-Length"))
contentLength = std::stoull(conversions::to_utf8string(response.headers().find(L"Content-Length")->second));
// 读取响应体,逐块写入文件,同时更新进度
// <EFBFBD><EFBFBD>ȡ<EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>д<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>ͬʱ<EFBFBD><EFBFBD><EFBFBD>½<EFBFBD><EFBFBD><EFBFBD>
auto inStream = response.body();
const size_t bufferSize = 8192;
// std::vector<unsigned char> buffer(bufferSize);
@ -118,13 +118,13 @@ namespace GkmsResourceUpdate {
buffer << file.rdbuf();
std::string content = buffer.str();
// 去除首尾空格和换行符
// ȥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>ͻ<EFBFBD><EFBFBD>з<EFBFBD>
auto is_not_space = [](unsigned char ch) {
return !std::isspace(ch);
};
// 去除前导空白
// ȥ<EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD>հ<EFBFBD>
content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space));
// 去除尾部空白
// ȥ<EFBFBD><EFBFBD>β<EFBFBD><EFBFBD><EFBFBD>հ<EFBFBD>
content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end());
resourceVersionCache = content;
return content;
@ -204,7 +204,7 @@ namespace GkmsResourceUpdate {
g_reload_all_data();
GakumasLocal::Log::Info("Update completed.");
}
// 仅解压一个文件
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹһ<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
return;
}
}
@ -217,6 +217,66 @@ namespace GkmsResourceUpdate {
}).detach();
}
void AutoCheckUpdateFromAPI() {
std::thread([]() {
try {
if (!g_useAPIAssets) {
GakumasLocal::Log::Info("Auto update check skipped: API assets disabled.");
return;
}
GakumasLocal::Log::Info("Auto checking update from API...");
auto response = send_get(g_useAPIAssetsURL, 30);
if (response.status_code() != 200) {
GakumasLocal::Log::ErrorFmt("Auto update check failed: HTTP %d\n", response.status_code());
return;
}
auto data = nlohmann::json::parse(response.extract_utf8string().get());
std::string remoteVersion = data["tag_name"];
const auto localVersion = GetCurrentResourceVersion(false);
if (localVersion == remoteVersion) {
GakumasLocal::Log::InfoFmt("Local resource is already up to date (v%s).", localVersion.c_str());
return;
}
GakumasLocal::Log::InfoFmt("New resource version found: %s -> %s", localVersion.c_str(), remoteVersion.c_str());
GakumasLocal::Log::Info("Starting automatic download...");
if (!data.contains("assets") || !data["assets"].is_array()) {
GakumasLocal::Log::Error("API response doesn't contain assets array.");
return;
}
for (const auto& asset : data["assets"]) {
if (!asset.contains("name") || !asset.contains("browser_download_url"))
continue;
std::string name = asset["name"];
if (name.ends_with(".zip")) {
std::string downloadUrl = asset["browser_download_url"];
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string())) {
g_reload_all_data();
GakumasLocal::Log::Info("Auto update completed successfully.");
}
else {
GakumasLocal::Log::Error("Auto update failed during download or extraction.");
}
return;
}
}
GakumasLocal::Log::Error("No .zip file found in assets.");
}
catch (std::exception& e) {
GakumasLocal::Log::ErrorFmt("Exception occurred in AutoCheckUpdateFromAPI: %s\n", e.what());
}
}).detach();
}
void checkUpdateFromURL(const std::string& downloadUrl) {
std::thread([downloadUrl]() {
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) {

View File

@ -6,5 +6,6 @@ namespace GkmsResourceUpdate {
void saveProgramConfig();
std::string GetCurrentResourceVersion(bool useCache);
void CheckUpdateFromAPI(bool isManual);
void AutoCheckUpdateFromAPI();
void checkUpdateFromURL(const std::string& downloadUrl);
}