Compare commits
2 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
64809af01c | |
|
|
8a0e332292 |
|
|
@ -157,3 +157,5 @@ resources/text_dumper/bin
|
||||||
.vscode/
|
.vscode/
|
||||||
/backend
|
/backend
|
||||||
/utils/events.br
|
/utils/events.br
|
||||||
|
|
||||||
|
gakumas-texture-TL
|
||||||
|
|
|
||||||
|
|
@ -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. 查看游戏目录权限设置
|
||||||
|
|
@ -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 文件夹可以保留作为参考,但不再需要编译使用
|
||||||
|
|
@ -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 即可
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"gameOrientation": 0,
|
"gameOrientation": 0,
|
||||||
"forceExportResource": false,
|
"forceExportResource": false,
|
||||||
"enableFreeCamera": false,
|
"enableFreeCamera": false,
|
||||||
|
"enableTextureReplace": false,
|
||||||
"targetFrameRate": 0,
|
"targetFrameRate": 0,
|
||||||
"unlockAllLive": true,
|
"unlockAllLive": true,
|
||||||
"unlockAllLiveCostume": true,
|
"unlockAllLiveCostume": true,
|
||||||
|
|
@ -16,6 +17,7 @@
|
||||||
"liveCustomeHeadId": "costume_head_fktn-cstm-0001",
|
"liveCustomeHeadId": "costume_head_fktn-cstm-0001",
|
||||||
"liveCustomeCostumeId": "hume-othr-0000",
|
"liveCustomeCostumeId": "hume-othr-0000",
|
||||||
"loginAsIOS": false,
|
"loginAsIOS": false,
|
||||||
|
"skipPurchaseInit": true,
|
||||||
"useCustomeGraphicSettings": false,
|
"useCustomeGraphicSettings": false,
|
||||||
"renderScale": 0.77,
|
"renderScale": 0.77,
|
||||||
"qualitySettingsLevel": 3,
|
"qualitySettingsLevel": 3,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
#include "Il2cppUtils.hpp"
|
#include "Il2cppUtils.hpp"
|
||||||
#include "Local.h"
|
#include "Local.h"
|
||||||
#include "MasterLocal.h"
|
#include "MasterLocal.h"
|
||||||
|
#include "TextureReplace.h"
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include "camera/camera.hpp"
|
#include "camera/camera.hpp"
|
||||||
#include "config/Config.hpp"
|
#include "config/Config.hpp"
|
||||||
|
|
@ -265,10 +266,62 @@ namespace GakumasLocal::HookMain {
|
||||||
// Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str());
|
// Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str());
|
||||||
auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type);
|
auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type);
|
||||||
loadHistory.emplace(ret, name->ToString());
|
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;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_HOOK(void*, AssetBundleRequest_GetResult, (void* self)) {
|
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);
|
auto result = AssetBundleRequest_GetResult_Orig(self);
|
||||||
if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) {
|
if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) {
|
||||||
const auto name = iter->second;
|
const auto name = iter->second;
|
||||||
|
|
@ -1873,6 +1926,12 @@ namespace GakumasLocal::HookMain {
|
||||||
|
|
||||||
Local::LoadData();
|
Local::LoadData();
|
||||||
MasterLocal::LoadData();
|
MasterLocal::LoadData();
|
||||||
|
|
||||||
|
// 初始化纹理替换系统
|
||||||
|
if (Config::enableTextureReplace) {
|
||||||
|
Log::Info("Initializing texture replacement system...");
|
||||||
|
TextureReplace::Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
UnityResolveProgress::startInit = false;
|
UnityResolveProgress::startInit = false;
|
||||||
|
|
||||||
|
|
@ -1897,5 +1956,11 @@ namespace GakumasLocal::Hook {
|
||||||
|
|
||||||
|
|
||||||
Log::Info("Hook installed");
|
Log::Info("Hook installed");
|
||||||
|
|
||||||
|
// 延迟启动自动资源更新检查
|
||||||
|
std::thread([]() {
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||||
|
GkmsResourceUpdate::AutoCheckUpdateFromAPI();
|
||||||
|
}).detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,16 @@
|
||||||
#define GAKUMAS_LOCALIFY_HOOK_H
|
#define GAKUMAS_LOCALIFY_HOOK_H
|
||||||
|
|
||||||
#include <string>
|
#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
|
namespace GakumasLocal::Hook
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -17,6 +17,7 @@ namespace GakumasLocal::Config {
|
||||||
int gameOrientation = 0;
|
int gameOrientation = 0;
|
||||||
bool dumpText = false;
|
bool dumpText = false;
|
||||||
bool enableFreeCamera = false;
|
bool enableFreeCamera = false;
|
||||||
|
bool enableTextureReplace = false;
|
||||||
int targetFrameRate = 0;
|
int targetFrameRate = 0;
|
||||||
bool unlockAllLive = false;
|
bool unlockAllLive = false;
|
||||||
bool unlockAllLiveCostume = false;
|
bool unlockAllLiveCostume = false;
|
||||||
|
|
@ -26,6 +27,7 @@ namespace GakumasLocal::Config {
|
||||||
std::string liveCustomeCostumeId = "";
|
std::string liveCustomeCostumeId = "";
|
||||||
|
|
||||||
bool loginAsIOS = false;
|
bool loginAsIOS = false;
|
||||||
|
bool skipPurchaseInit = true;
|
||||||
|
|
||||||
bool useCustomeGraphicSettings = false;
|
bool useCustomeGraphicSettings = false;
|
||||||
float renderScale = 0.77f;
|
float renderScale = 0.77f;
|
||||||
|
|
@ -73,12 +75,14 @@ namespace GakumasLocal::Config {
|
||||||
GetConfigItem(dumpText);
|
GetConfigItem(dumpText);
|
||||||
GetConfigItem(targetFrameRate);
|
GetConfigItem(targetFrameRate);
|
||||||
GetConfigItem(enableFreeCamera);
|
GetConfigItem(enableFreeCamera);
|
||||||
|
GetConfigItem(enableTextureReplace);
|
||||||
GetConfigItem(unlockAllLive);
|
GetConfigItem(unlockAllLive);
|
||||||
GetConfigItem(unlockAllLiveCostume);
|
GetConfigItem(unlockAllLiveCostume);
|
||||||
GetConfigItem(enableLiveCustomeDress);
|
GetConfigItem(enableLiveCustomeDress);
|
||||||
GetConfigItem(liveCustomeHeadId);
|
GetConfigItem(liveCustomeHeadId);
|
||||||
GetConfigItem(liveCustomeCostumeId);
|
GetConfigItem(liveCustomeCostumeId);
|
||||||
GetConfigItem(loginAsIOS);
|
GetConfigItem(loginAsIOS);
|
||||||
|
GetConfigItem(skipPurchaseInit);
|
||||||
GetConfigItem(useCustomeGraphicSettings);
|
GetConfigItem(useCustomeGraphicSettings);
|
||||||
GetConfigItem(renderScale);
|
GetConfigItem(renderScale);
|
||||||
GetConfigItem(qualitySettingsLevel);
|
GetConfigItem(qualitySettingsLevel);
|
||||||
|
|
@ -129,12 +133,14 @@ namespace GakumasLocal::Config {
|
||||||
SetConfigItem(dumpText);
|
SetConfigItem(dumpText);
|
||||||
SetConfigItem(targetFrameRate);
|
SetConfigItem(targetFrameRate);
|
||||||
SetConfigItem(enableFreeCamera);
|
SetConfigItem(enableFreeCamera);
|
||||||
|
SetConfigItem(enableTextureReplace);
|
||||||
SetConfigItem(unlockAllLive);
|
SetConfigItem(unlockAllLive);
|
||||||
SetConfigItem(unlockAllLiveCostume);
|
SetConfigItem(unlockAllLiveCostume);
|
||||||
SetConfigItem(enableLiveCustomeDress);
|
SetConfigItem(enableLiveCustomeDress);
|
||||||
SetConfigItem(liveCustomeHeadId);
|
SetConfigItem(liveCustomeHeadId);
|
||||||
SetConfigItem(liveCustomeCostumeId);
|
SetConfigItem(liveCustomeCostumeId);
|
||||||
SetConfigItem(loginAsIOS);
|
SetConfigItem(loginAsIOS);
|
||||||
|
SetConfigItem(skipPurchaseInit);
|
||||||
SetConfigItem(useCustomeGraphicSettings);
|
SetConfigItem(useCustomeGraphicSettings);
|
||||||
SetConfigItem(renderScale);
|
SetConfigItem(renderScale);
|
||||||
SetConfigItem(qualitySettingsLevel);
|
SetConfigItem(qualitySettingsLevel);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ namespace GakumasLocal::Config {
|
||||||
extern bool useMasterTrans;
|
extern bool useMasterTrans;
|
||||||
extern bool dumpText;
|
extern bool dumpText;
|
||||||
extern bool enableFreeCamera;
|
extern bool enableFreeCamera;
|
||||||
|
extern bool enableTextureReplace;
|
||||||
extern int targetFrameRate;
|
extern int targetFrameRate;
|
||||||
extern bool unlockAllLive;
|
extern bool unlockAllLive;
|
||||||
extern bool unlockAllLiveCostume;
|
extern bool unlockAllLiveCostume;
|
||||||
|
|
@ -22,6 +23,7 @@ namespace GakumasLocal::Config {
|
||||||
extern std::string liveCustomeCostumeId;
|
extern std::string liveCustomeCostumeId;
|
||||||
|
|
||||||
extern bool loginAsIOS;
|
extern bool loginAsIOS;
|
||||||
|
extern bool skipPurchaseInit;
|
||||||
|
|
||||||
extern bool useCustomeGraphicSettings;
|
extern bool useCustomeGraphicSettings;
|
||||||
extern float renderScale;
|
extern float renderScale;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <concepts>
|
#include <concepts>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
// UnityEngine.Color
|
// UnityEngine.Color
|
||||||
struct Color_t
|
struct Color_t
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,18 @@ namespace GkmsResourceUpdate {
|
||||||
using namespace concurrency::streams;
|
using namespace concurrency::streams;
|
||||||
|
|
||||||
try {
|
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));
|
auto outTask = fstream::open_ostream(conversions::to_string_t(outputPath));
|
||||||
outTask.wait();
|
outTask.wait();
|
||||||
auto fileStream = outTask.get();
|
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));
|
http_client client(conversions::to_string_t(url));
|
||||||
|
|
||||||
downloading = true;
|
downloading = true;
|
||||||
downloadProgress = 0.0f;
|
downloadProgress = 0.0f;
|
||||||
|
|
||||||
// 发起 GET 请求
|
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD> GET <20><><EFBFBD><EFBFBD>
|
||||||
auto responseTask = client.request(methods::GET);
|
auto responseTask = client.request(methods::GET);
|
||||||
responseTask.wait();
|
responseTask.wait();
|
||||||
http_response response = responseTask.get();
|
http_response response = responseTask.get();
|
||||||
|
|
@ -68,12 +68,12 @@ namespace GkmsResourceUpdate {
|
||||||
return false;
|
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;
|
uint64_t contentLength = 0;
|
||||||
if (response.headers().has(L"Content-Length"))
|
if (response.headers().has(L"Content-Length"))
|
||||||
contentLength = std::stoull(conversions::to_utf8string(response.headers().find(L"Content-Length")->second));
|
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();
|
auto inStream = response.body();
|
||||||
const size_t bufferSize = 8192;
|
const size_t bufferSize = 8192;
|
||||||
// std::vector<unsigned char> buffer(bufferSize);
|
// std::vector<unsigned char> buffer(bufferSize);
|
||||||
|
|
@ -118,13 +118,13 @@ namespace GkmsResourceUpdate {
|
||||||
buffer << file.rdbuf();
|
buffer << file.rdbuf();
|
||||||
std::string content = buffer.str();
|
std::string content = buffer.str();
|
||||||
|
|
||||||
// 去除首尾空格和换行符
|
// ȥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>β<EFBFBD>ո<EFBFBD>ͻ<EFBFBD><EFBFBD>з<EFBFBD>
|
||||||
auto is_not_space = [](unsigned char ch) {
|
auto is_not_space = [](unsigned char ch) {
|
||||||
return !std::isspace(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));
|
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());
|
content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end());
|
||||||
resourceVersionCache = content;
|
resourceVersionCache = content;
|
||||||
return content;
|
return content;
|
||||||
|
|
@ -204,7 +204,7 @@ namespace GkmsResourceUpdate {
|
||||||
g_reload_all_data();
|
g_reload_all_data();
|
||||||
GakumasLocal::Log::Info("Update completed.");
|
GakumasLocal::Log::Info("Update completed.");
|
||||||
}
|
}
|
||||||
// 仅解压一个文件
|
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹһ<EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -217,6 +217,66 @@ namespace GkmsResourceUpdate {
|
||||||
}).detach();
|
}).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) {
|
void checkUpdateFromURL(const std::string& downloadUrl) {
|
||||||
std::thread([downloadUrl]() {
|
std::thread([downloadUrl]() {
|
||||||
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) {
|
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) {
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,6 @@ namespace GkmsResourceUpdate {
|
||||||
void saveProgramConfig();
|
void saveProgramConfig();
|
||||||
std::string GetCurrentResourceVersion(bool useCache);
|
std::string GetCurrentResourceVersion(bool useCache);
|
||||||
void CheckUpdateFromAPI(bool isManual);
|
void CheckUpdateFromAPI(bool isManual);
|
||||||
|
void AutoCheckUpdateFromAPI();
|
||||||
void checkUpdateFromURL(const std::string& downloadUrl);
|
void checkUpdateFromURL(const std::string& downloadUrl);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue