Merge pull request '分割hook.cpp文件' (#2) from chihya72/gkms-localify-dmm:main into main

Reviewed-on: chinosk/gkms-localify-dmm#2
This commit is contained in:
chinosk 2026-05-13 14:44:59 +08:00
commit 5b3f736e62
19 changed files with 1406 additions and 54 deletions

View File

@ -5,7 +5,10 @@
"useRemoteAssets": true, "useRemoteAssets": true,
"useAPIAssets": false, "useAPIAssets": false,
"useAPIAssetsURL": "https://uma.chinosk6.cn/api/gkms_trans_data", "useAPIAssetsURL": "https://uma.chinosk6.cn/api/gkms_trans_data",
"useAPITextureAssets": false,
"useAPITextureAssetsURL": "https://texture.gakumas.cn/api/gkms_texture_data",
"delTextureRemoteAfterUpdate": true,
"delRemoteAfterUpdate": false, "delRemoteAfterUpdate": false,
"cleanLocalAssets": false, "cleanLocalAssets": false,
"p": false "p": false
} }

View File

@ -3,9 +3,11 @@
"enabled": true, "enabled": true,
"lazyInit": true, "lazyInit": true,
"replaceFont": true, "replaceFont": true,
"replaceTexture": true,
"textTest": false, "textTest": false,
"useMasterTrans": true, "useMasterTrans": true,
"dumpText": false, "dumpText": false,
"dumpRuntimeTexture": false,
"gameOrientation": 0, "gameOrientation": 0,
"forceExportResource": false, "forceExportResource": false,
"enableFreeCamera": false, "enableFreeCamera": false,
@ -42,4 +44,4 @@
"bLimitZx": 1.5, "bLimitZx": 1.5,
"bLimitZy": 1.5, "bLimitZy": 1.5,
"pf": false "pf": false
} }

View File

@ -1,4 +1,5 @@
#include "Hook.h" #include "Hook.h"
#include "HookTexture.h"
#include "Plugin.h" #include "Plugin.h"
#include "Log.h" #include "Log.h"
#include "../deps/UnityResolve/UnityResolve.hpp" #include "../deps/UnityResolve/UnityResolve.hpp"
@ -13,6 +14,11 @@
#include <thread> #include <thread>
#include <map> #include <map>
#include <set> #include <set>
#include <list>
#include <vector>
#include <string_view>
#include <utility>
#include <cctype>
#include "../platformDefine.hpp" #include "../platformDefine.hpp"
#ifdef GKMS_WINDOWS #ifdef GKMS_WINDOWS
@ -262,10 +268,21 @@ namespace GakumasLocal::HookMain {
std::unordered_map<void*, std::string> loadHistory{}; std::unordered_map<void*, std::string> loadHistory{};
DEFINE_HOOK(void*, AssetBundle_LoadAsset, (void* self, Il2cppString* name, void* type)) {
auto result = AssetBundle_LoadAsset_Orig(self, name, type);
if (name) {
result = ReplaceTextureOrSpriteAsset(result, name->ToString());
}
return result;
}
DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) { DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) {
// 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()); if (ret && name) {
loadHistory.emplace(ret, name->ToString());
}
return ret; return ret;
} }
@ -277,18 +294,62 @@ namespace GakumasLocal::HookMain {
// const auto assetClass = Il2cppUtils::get_class_from_instance(result); // const auto assetClass = Il2cppUtils::get_class_from_instance(result);
// Log::InfoFmt("AssetBundleRequest_GetResult: %s, type: %s", name.c_str(), static_cast<Il2CppClassHead*>(assetClass)->name); // Log::InfoFmt("AssetBundleRequest_GetResult: %s, type: %s", name.c_str(), static_cast<Il2CppClassHead*>(assetClass)->name);
result = ReplaceTextureOrSpriteAsset(result, name);
} }
return result; return result;
} }
DEFINE_HOOK(void*, AssetBundleRequest_get_asset, (void* self)) {
std::string name;
if (const auto iter = loadHistory.find(self); iter != loadHistory.end()) {
name = iter->second;
loadHistory.erase(iter);
}
auto result = AssetBundleRequest_get_asset_Orig(self);
if (!name.empty()) {
result = ReplaceTextureOrSpriteAsset(result, name);
}
return result;
}
DEFINE_HOOK(void*, AssetBundleRequest_get_allAssets, (void* self)) {
auto result = AssetBundleRequest_get_allAssets_Orig(self);
ReplaceAllAssetTextures(result);
return result;
}
DEFINE_HOOK(void*, Resources_Load, (Il2cppString* path, void* systemTypeInstance)) { DEFINE_HOOK(void*, Resources_Load, (Il2cppString* path, void* systemTypeInstance)) {
auto ret = Resources_Load_Orig(path, systemTypeInstance); auto ret = Resources_Load_Orig(path, systemTypeInstance);
// if (ret) Log::DebugFmt("Resources_Load: %s, type: %s", path->ToString().c_str(), Il2cppUtils::get_class_from_instance(ret)->name); // if (ret) Log::DebugFmt("Resources_Load: %s, type: %s", path->ToString().c_str(), Il2cppUtils::get_class_from_instance(ret)->name);
if (path) {
ret = ReplaceTextureOrSpriteAsset(ret, path->ToString());
}
return ret; return ret;
} }
DEFINE_HOOK(void*, Sprite_get_texture, (void* self)) {
return ReplaceSpriteTexture(Sprite_get_texture_Orig(self));
}
DEFINE_HOOK(void, Image_set_sprite, (void* self, void* sprite)) {
Image_set_sprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite));
}
DEFINE_HOOK(void, Image_set_overrideSprite, (void* self, void* sprite)) {
Image_set_overrideSprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite));
}
DEFINE_HOOK(void, CanvasRenderer_SetTexture, (void* self, void* texture)) {
CanvasRenderer_SetTexture_Orig(self, ReplaceTextureOrSpriteByObjectName(texture));
}
DEFINE_HOOK(void, SpriteRenderer_set_sprite, (void* self, void* sprite)) {
SpriteRenderer_set_sprite_Orig(self, ReplaceSpriteAssetByTextureName(sprite));
}
DEFINE_HOOK(void, I18nHelper_SetUpI18n, (void* self, Il2cppString* lang, Il2cppString* localizationText, int keyComparison)) { DEFINE_HOOK(void, I18nHelper_SetUpI18n, (void* self, Il2cppString* lang, Il2cppString* localizationText, int keyComparison)) {
// Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str()); // Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str());
// TODO 此处为 dump 原文 csv // TODO 此处为 dump 原文 csv
@ -1688,12 +1749,18 @@ namespace GakumasLocal::HookMain {
UnityResolve::Mode::Il2Cpp, Config::lazyInit); UnityResolve::Mode::Il2Cpp, Config::lazyInit);
#endif #endif
ADD_HOOK(AssetBundle_LoadAssetAsync, Il2cppUtils::il2cpp_resolve_icall( // Temporarily isolate texture replacement to CanvasRenderer.SetTexture only.
"UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")); // ADD_HOOK(AssetBundle_LoadAsset, ResolveAssetBundleLoadAssetHookAddress());
ADD_HOOK(AssetBundleRequest_GetResult, Il2cppUtils::il2cpp_resolve_icall( // ADD_HOOK(AssetBundle_LoadAssetAsync, ResolveAssetBundleLoadAssetAsyncHookAddress());
"UnityEngine.AssetBundleRequest::GetResult()")); // ADD_HOOK(AssetBundleRequest_GetResult, ResolveAssetBundleRequestResultHookAddress());
ADD_HOOK(Resources_Load, Il2cppUtils::il2cpp_resolve_icall( // ADD_HOOK(AssetBundleRequest_get_asset, ResolveAssetBundleRequestAssetHookAddress());
"UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")); // ADD_HOOK(AssetBundleRequest_get_allAssets, ResolveAssetBundleRequestAllAssetsHookAddress());
// ADD_HOOK(Resources_Load, ResolveResourcesLoadHookAddress());
// ADD_HOOK(Sprite_get_texture, ResolveSpriteGetTextureHookAddress());
// ADD_HOOK(Image_set_sprite, Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI", "Image", "set_sprite"));
// ADD_HOOK(Image_set_overrideSprite, Il2cppUtils::GetMethodPointer("UnityEngine.UI.dll", "UnityEngine.UI", "Image", "set_overrideSprite"));
ADD_HOOK(CanvasRenderer_SetTexture, Il2cppUtils::GetMethodPointer("UnityEngine.UIModule.dll", "UnityEngine", "CanvasRenderer", "SetTexture", {"UnityEngine.Texture"}));
// ADD_HOOK(SpriteRenderer_set_sprite, Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "SpriteRenderer", "set_sprite"));
ADD_HOOK(I18nHelper_SetUpI18n, Il2cppUtils::GetMethodPointer("quaunity-ui.Runtime.dll", "Qua.UI", ADD_HOOK(I18nHelper_SetUpI18n, Il2cppUtils::GetMethodPointer("quaunity-ui.Runtime.dll", "Qua.UI",
"I18nHelper", "SetUpI18n")); "I18nHelper", "SetUpI18n"));
@ -1987,7 +2054,7 @@ namespace GakumasLocal::HookMain {
UnityResolveProgress::startInit = true; UnityResolveProgress::startInit = true;
UnityResolveProgress::assembliesProgress.total = 2; UnityResolveProgress::assembliesProgress.total = 2;
UnityResolveProgress::assembliesProgress.current = 1; UnityResolveProgress::assembliesProgress.current = 1;
UnityResolveProgress::classProgress.total = 36; UnityResolveProgress::classProgress.total = 43;
UnityResolveProgress::classProgress.current = 0; UnityResolveProgress::classProgress.current = 0;
} }

View File

@ -0,0 +1,797 @@
#include "HookTexture.h"
#include "Log.h"
#include "Il2cppUtils.hpp"
#include "Local.h"
#include "config/Config.hpp"
#include "../deps/UnityResolve/UnityResolve.hpp"
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
namespace GakumasLocal::HookMain
{
using Il2cppString = UnityResolve::UnityType::String;
extern void* (*Sprite_get_texture_Orig)(void* self);
bool IsNativeObjectAlive(void* obj);
Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr;
Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr;
std::unordered_map<std::string, uint32_t> LoadedLocalTextureHandles{};
std::unordered_set<std::string> AppliedLocalTextureKeys{};
Il2cppUtils::Il2CppClassHead* GetTexture2DClass() {
if (!Texture2DClass) {
const auto textureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Texture2D");
if (textureClass) {
Texture2DClass = static_cast<Il2cppUtils::Il2CppClassHead*>(textureClass->address);
}
}
return Texture2DClass;
}
Il2cppUtils::Il2CppClassHead* GetSpriteClass() {
if (!SpriteClass) {
const auto spriteClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite");
if (spriteClass) {
SpriteClass = static_cast<Il2cppUtils::Il2CppClassHead*>(spriteClass->address);
}
}
return SpriteClass;
}
bool IsTexture2D(void* obj) {
const auto textureClass = GetTexture2DClass();
if (!obj || !textureClass) return false;
const auto objClass = Il2cppUtils::get_class_from_instance(obj);
if (objClass == textureClass) return true;
return UnityResolve::Invoke<bool>("il2cpp_class_is_assignable_from", textureClass, objClass);
}
bool IsSprite(void* obj) {
const auto spriteClass = GetSpriteClass();
if (!obj || !spriteClass) return false;
const auto objClass = Il2cppUtils::get_class_from_instance(obj);
if (objClass == spriteClass) return true;
return UnityResolve::Invoke<bool>("il2cpp_class_is_assignable_from", spriteClass, objClass);
}
Il2cppString* GetObjectName(void* obj) {
if (!obj) return nullptr;
static auto Object_GetName = reinterpret_cast<Il2cppString * (*)(void*)>(
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)"));
return Object_GetName ? Object_GetName(obj) : nullptr;
}
void SetDontUnloadUnusedAsset(void* obj) {
if (!obj) return;
static auto Object_set_hideFlags = reinterpret_cast<void (*)(void*, int)>(
Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Object", "set_hideFlags"));
if (Object_set_hideFlags) {
Object_set_hideFlags(obj, 32);
}
}
void AddTexturePathCandidate(std::vector<std::filesystem::path>& candidates, const std::filesystem::path& path) {
if (path.empty()) return;
if (std::find(candidates.begin(), candidates.end(), path) == candidates.end()) {
candidates.emplace_back(path);
}
if (!path.has_extension()) {
auto pngPath = path;
pngPath += ".png";
if (std::find(candidates.begin(), candidates.end(), pngPath) == candidates.end()) {
candidates.emplace_back(std::move(pngPath));
}
}
}
enum class TextureCategory {
Image,
Atlas,
Others,
};
std::string ToLowerAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return value;
}
TextureCategory GetTextureCategory(const std::string& textureName) {
const auto lowerName = ToLowerAscii(std::filesystem::path(textureName).filename().generic_string());
if (lowerName.rfind("img", 0) == 0) {
return TextureCategory::Image;
}
if (lowerName.rfind("sactx", 0) == 0) {
return TextureCategory::Atlas;
}
return TextureCategory::Others;
}
std::filesystem::path GetTextureCategoryDirName(TextureCategory category) {
switch (category) {
case TextureCategory::Image:
return "image";
case TextureCategory::Atlas:
return "atlas";
default:
return "others";
}
}
std::filesystem::path GetTextureReplaceRoot() {
return Local::GetBasePath() / "texture2d";
}
std::filesystem::path GetTextureDumpRoot() {
return Local::GetBasePath() / "dump-files" / "texture2d";
}
std::filesystem::path GetTextureReplaceBase(const std::string& textureName) {
return GetTextureReplaceRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName));
}
std::filesystem::path GetTextureDumpBase(const std::string& textureName) {
return GetTextureDumpRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName));
}
std::vector<std::string> SplitString(const std::string& value, char delimiter) {
std::vector<std::string> parts;
size_t start = 0;
while (start <= value.size()) {
const auto end = value.find(delimiter, start);
parts.emplace_back(value.substr(start, end == std::string::npos ? std::string::npos : end - start));
if (end == std::string::npos) break;
start = end + 1;
}
return parts;
}
void AppendTextureCandidates(std::vector<std::filesystem::path>& target, std::vector<std::filesystem::path>&& source);
std::string NormalizeLocalAssetKey(const std::filesystem::path& path);
bool IsHexHashPart(const std::string& value) {
return value.size() == 8 && std::all_of(value.begin(), value.end(), [](unsigned char ch) {
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
});
}
std::string GetPortableSactxTextureName(const std::string& objectName) {
auto fileName = std::filesystem::path(objectName).filename().generic_string();
if (fileName.ends_with(".png")) {
fileName.resize(fileName.size() - 4);
}
const auto parts = SplitString(fileName, '-');
if (parts.size() < 5 || parts[0] != "sactx" || parts[2].find('x') == std::string::npos) {
return {};
}
const auto atlasEnd = IsHexHashPart(parts.back()) ? parts.size() - 1 : parts.size();
if (atlasEnd <= 4) return {};
std::string portableName = parts[0] + "-" + parts[1] + "-" + parts[2];
for (size_t i = 4; i < atlasEnd; ++i) {
portableName += "-" + parts[i];
}
return portableName;
}
std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::filesystem::path>>> RecursiveTexturePathIndex{};
std::vector<std::filesystem::path> GetRecursiveTextureCandidates(const std::filesystem::path& basePath,
const std::string& lookupName) {
std::vector<std::filesystem::path> candidates;
if (lookupName.empty() || !std::filesystem::exists(basePath)) return candidates;
const auto baseKey = NormalizeLocalAssetKey(basePath);
auto& index = RecursiveTexturePathIndex[baseKey];
if (index.empty()) {
for (const auto& entry : std::filesystem::recursive_directory_iterator(basePath)) {
if (!entry.is_regular_file()) continue;
const auto& path = entry.path();
if (ToLowerAscii(path.extension().generic_string()) != ".png") continue;
const auto fileName = path.filename().generic_string();
const auto stemName = path.stem().generic_string();
index[fileName].emplace_back(path);
if (stemName != fileName) {
index[stemName].emplace_back(path);
}
}
}
if (const auto iter = index.find(lookupName); iter != index.end()) {
candidates.insert(candidates.end(), iter->second.begin(), iter->second.end());
}
return candidates;
}
std::vector<std::filesystem::path> GetNamedTextureCandidates(const std::filesystem::path& assetName) {
std::vector<std::filesystem::path> candidates;
if (assetName.empty()) return candidates;
const auto basePath = GetTextureReplaceBase(assetName.filename().generic_string());
AddTexturePathCandidate(candidates, basePath / assetName);
if (assetName.has_parent_path()) {
AddTexturePathCandidate(candidates, basePath / assetName.filename());
}
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, assetName.filename().generic_string()));
const auto portableAssetName = GetPortableSactxTextureName(assetName.filename().generic_string());
if (!portableAssetName.empty()) {
AddTexturePathCandidate(candidates, basePath / portableAssetName);
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableAssetName));
}
return candidates;
}
std::vector<std::filesystem::path> GetSpriteTextureCandidates(const std::string& objectName) {
std::vector<std::filesystem::path> candidates;
if (objectName.empty()) return candidates;
auto safeObjectName = objectName;
std::replace(safeObjectName.begin(), safeObjectName.end(), '|', '_');
const auto basePath = GetTextureReplaceBase(safeObjectName);
AddTexturePathCandidate(candidates, basePath / objectName);
if (safeObjectName != objectName) {
AddTexturePathCandidate(candidates, basePath / safeObjectName);
}
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, safeObjectName));
const auto portableObjectName = GetPortableSactxTextureName(safeObjectName);
if (!portableObjectName.empty() && portableObjectName != objectName && portableObjectName != safeObjectName) {
AddTexturePathCandidate(candidates, basePath / portableObjectName);
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableObjectName));
}
return candidates;
}
void AppendTextureCandidates(std::vector<std::filesystem::path>& target, std::vector<std::filesystem::path>&& source) {
target.insert(target.end(),
std::make_move_iterator(source.begin()),
std::make_move_iterator(source.end()));
}
std::vector<std::filesystem::path> GetSpriteAssetTextureCandidates(void* sprite, const std::string& assetName) {
std::vector<std::filesystem::path> candidates;
const auto assetPath = std::filesystem::path(assetName);
if (!assetName.empty()) {
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(assetPath.filename().generic_string()));
}
if (sprite && Sprite_get_texture_Orig) {
if (const auto texture = Sprite_get_texture_Orig(sprite)) {
if (const auto textureName = GetObjectName(texture)) {
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(textureName->ToString()));
}
}
}
return candidates;
}
std::string NormalizeLocalAssetKey(const std::filesystem::path& path) {
auto key = path.lexically_normal().generic_string();
std::replace(key.begin(), key.end(), '\\', '/');
return key;
}
std::string SanitizeDumpPathPart(std::string part) {
constexpr std::string_view invalidChars = "<>:\"/\\|?*";
if (part.empty() || part == "." || part == "..") return "_";
for (auto& ch : part) {
if (static_cast<unsigned char>(ch) < 32 || invalidChars.find(ch) != std::string_view::npos) {
ch = '_';
}
}
while (!part.empty() && (part.back() == '.' || part.back() == ' ')) {
part.back() = '_';
}
return part.empty() ? "_" : part;
}
std::filesystem::path SanitizeDumpSubPath(const std::filesystem::path& dumpSubDir) {
std::filesystem::path safePath;
for (const auto& part : dumpSubDir) {
const auto partString = part.generic_string();
if (partString.empty() || partString == "." || partString == ".."
|| part == part.root_name() || part == part.root_directory()) {
continue;
}
safePath /= SanitizeDumpPathPart(partString);
}
return safePath;
}
bool DumpTexture2D(void* texture2D) {
if (!IsTexture2D(texture2D)) return false;
const auto objectName = GetObjectName(texture2D);
const auto textureName = objectName ? objectName->ToString() : std::string("texture");
const auto dumpDir = GetTextureDumpBase(textureName);
const auto dumpPath = dumpDir / (SanitizeDumpPathPart(textureName) + ".png");
if (std::filesystem::exists(dumpPath)) return true;
static auto Texture2D_get_width = [] {
const auto textureClass = GetTexture2DClass();
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_width", 0) : nullptr;
return method ? reinterpret_cast<int (*)(void*)>(method->methodPointer) : nullptr;
}();
static auto Texture2D_get_height = [] {
const auto textureClass = GetTexture2DClass();
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_height", 0) : nullptr;
return method ? reinterpret_cast<int (*)(void*)>(method->methodPointer) : nullptr;
}();
static auto Texture2D_ctor = [] {
const auto textureClass = GetTexture2DClass();
const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr;
return ctor ? reinterpret_cast<void (*)(void*, int, int)>(ctor->methodPointer) : nullptr;
}();
static auto Texture2D_ReadPixels = [] {
const auto textureClass = GetTexture2DClass();
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "ReadPixels", 3) : nullptr;
return method ? reinterpret_cast<void (*)(void*, UnityResolve::UnityType::Rect, int, int)>(method->methodPointer) : nullptr;
}();
static auto Texture2D_Apply = [] {
const auto textureClass = GetTexture2DClass();
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "Apply", 0) : nullptr;
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
}();
static auto RenderTexture_GetTemporary = [] {
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "GetTemporary", 3) : nullptr;
return method ? reinterpret_cast<void* (*)(int, int, int)>(method->methodPointer) : nullptr;
}();
static auto RenderTexture_ReleaseTemporary = [] {
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "ReleaseTemporary", 1) : nullptr;
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
}();
static auto RenderTexture_get_active = [] {
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "get_active", 0) : nullptr;
return method ? reinterpret_cast<void* (*)()>(method->methodPointer) : nullptr;
}();
static auto RenderTexture_set_active = [] {
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "set_active", 1) : nullptr;
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
}();
static auto Graphics_Blit = [] {
const auto graphicsClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Graphics");
const auto method = graphicsClass ? Il2cppUtils::il2cpp_class_get_method_from_name(graphicsClass->address, "Blit", 2) : nullptr;
return method ? reinterpret_cast<void (*)(void*, void*)>(method->methodPointer) : nullptr;
}();
static auto ImageConversion_EncodeToPNG = [] {
using EncodeToPNGFn = void* (*)(void*);
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.ImageConversion::EncodeToPNG(UnityEngine.Texture2D)")) {
return reinterpret_cast<EncodeToPNGFn>(icall);
}
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
const auto assembly = UnityResolve::Get(assemblyName);
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
const auto method = imageConversionClass
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "EncodeToPNG", 1)
: nullptr;
if (method) {
return reinterpret_cast<EncodeToPNGFn>(method->methodPointer);
}
}
return static_cast<EncodeToPNGFn>(nullptr);
}();
static auto File_WriteAllBytes = [] {
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "WriteAllBytes", 2) : nullptr;
return method ? reinterpret_cast<void (*)(Il2cppString*, void*)>(method->methodPointer) : nullptr;
}();
if (!Texture2D_get_width || !Texture2D_get_height || !Texture2D_ctor || !Texture2D_ReadPixels
|| !Texture2D_Apply || !RenderTexture_GetTemporary || !RenderTexture_ReleaseTemporary
|| !RenderTexture_get_active || !RenderTexture_set_active || !Graphics_Blit
|| !ImageConversion_EncodeToPNG || !File_WriteAllBytes) {
Log::Error("DumpTexture2D failed: Unity texture dump API not found.");
return false;
}
const auto width = Texture2D_get_width(texture2D);
const auto height = Texture2D_get_height(texture2D);
if (width <= 0 || height <= 0) return false;
void* renderTexture = nullptr;
void* readableTexture = nullptr;
void* previousActive = nullptr;
const auto cleanup = [&] {
if (RenderTexture_get_active && RenderTexture_set_active
&& (previousActive || RenderTexture_get_active() == renderTexture)) {
RenderTexture_set_active(previousActive);
}
if (renderTexture && RenderTexture_ReleaseTemporary) {
RenderTexture_ReleaseTemporary(renderTexture);
}
};
try {
std::filesystem::create_directories(dumpDir);
renderTexture = RenderTexture_GetTemporary(width, height, 0);
if (!renderTexture) {
cleanup();
return false;
}
Graphics_Blit(texture2D, renderTexture);
previousActive = RenderTexture_get_active();
RenderTexture_set_active(renderTexture);
readableTexture = UnityResolve::Invoke<void*>("il2cpp_object_new", GetTexture2DClass());
if (!readableTexture) {
cleanup();
return false;
}
Texture2D_ctor(readableTexture, width, height);
Texture2D_ReadPixels(readableTexture, UnityResolve::UnityType::Rect(0, 0, static_cast<float>(width), static_cast<float>(height)), 0, 0);
Texture2D_Apply(readableTexture);
const auto pngBytes = ImageConversion_EncodeToPNG(readableTexture);
if (!pngBytes) {
cleanup();
return false;
}
File_WriteAllBytes(Il2cppString::New(dumpPath.string()), pngBytes);
Log::InfoFmt("Texture dumped: %s", dumpPath.string().c_str());
cleanup();
return true;
}
catch (const std::exception& ex) {
cleanup();
Log::ErrorFmt("DumpTexture2D failed: %s", ex.what());
return false;
}
catch (...) {
cleanup();
Log::Error("DumpTexture2D failed: unknown error.");
return false;
}
}
void DumpTextureOrSpriteAsset(void* result) {
if (!result) return;
if (IsTexture2D(result)) {
DumpTexture2D(result);
return;
}
if (IsSprite(result) && Sprite_get_texture_Orig) {
if (const auto texture = Sprite_get_texture_Orig(result)) {
DumpTexture2D(texture);
}
}
}
void* LoadLocalTexture2D(const std::filesystem::path& path) {
if (!std::filesystem::is_regular_file(path)) return nullptr;
const auto cacheKey = NormalizeLocalAssetKey(path);
if (const auto iter = LoadedLocalTextureHandles.find(cacheKey); iter != LoadedLocalTextureHandles.end()) {
const auto cachedTexture = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", iter->second);
if (cachedTexture && IsNativeObjectAlive(cachedTexture)) {
return cachedTexture;
}
UnityResolve::Invoke<void>("il2cpp_gchandle_free", iter->second);
LoadedLocalTextureHandles.erase(iter);
}
const auto textureClass = GetTexture2DClass();
if (!textureClass) return nullptr;
static auto Texture2D_ctor = [] {
const auto textureClass = GetTexture2DClass();
const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr;
return ctor ? reinterpret_cast<void (*)(void*, int, int)>(ctor->methodPointer) : nullptr;
}();
static auto ImageConversion_LoadImage = [] {
using LoadImageFn = bool (*)(void*, void*, bool);
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) {
return reinterpret_cast<LoadImageFn>(icall);
}
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
const auto assembly = UnityResolve::Get(assemblyName);
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
const auto method = imageConversionClass
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3)
: nullptr;
if (method) {
return reinterpret_cast<LoadImageFn>(method->methodPointer);
}
}
return static_cast<LoadImageFn>(nullptr);
}();
static auto File_ReadAllBytes = [] {
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr;
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
}();
if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) {
Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found.");
return nullptr;
}
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
if (!fileBytes) return nullptr;
const auto texture = UnityResolve::Invoke<void*>("il2cpp_object_new", textureClass);
Texture2D_ctor(texture, 2, 2);
if (!ImageConversion_LoadImage(texture, fileBytes, false)) {
Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str());
return nullptr;
}
SetDontUnloadUnusedAsset(texture);
LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", texture, false));
Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str());
return texture;
}
void* LoadLocalTexture2DFromCandidates(const std::vector<std::filesystem::path>& candidates) {
for (const auto& candidate : candidates) {
if (auto texture = LoadLocalTexture2D(candidate)) {
return texture;
}
}
return nullptr;
}
bool ApplyLocalImageToTexture2D(void* texture2D, const std::filesystem::path& path) {
if (!IsTexture2D(texture2D) || !std::filesystem::is_regular_file(path)) return false;
auto cacheKey = NormalizeLocalAssetKey(path)
+ "|" + std::to_string(reinterpret_cast<std::uintptr_t>(texture2D));
if (AppliedLocalTextureKeys.contains(cacheKey)) return true;
static auto ImageConversion_LoadImage = [] {
using LoadImageFn = bool (*)(void*, void*, bool);
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) {
return reinterpret_cast<LoadImageFn>(icall);
}
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
const auto assembly = UnityResolve::Get(assemblyName);
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
const auto method = imageConversionClass
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3)
: nullptr;
if (method) {
return reinterpret_cast<LoadImageFn>(method->methodPointer);
}
}
return static_cast<LoadImageFn>(nullptr);
}();
static auto File_ReadAllBytes = [] {
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr;
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
}();
if (!ImageConversion_LoadImage || !File_ReadAllBytes) {
Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found.");
return false;
}
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
if (!fileBytes) return false;
if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) {
Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str());
return false;
}
SetDontUnloadUnusedAsset(texture2D);
AppliedLocalTextureKeys.emplace(std::move(cacheKey));
Log::InfoFmt("Texture replaced in-place from local file: %s", path.string().c_str());
return true;
}
bool ApplyLocalImageToTexture2DFromCandidates(void* texture2D, const std::vector<std::filesystem::path>& candidates) {
for (const auto& candidate : candidates) {
if (ApplyLocalImageToTexture2D(texture2D, candidate)) {
return true;
}
}
return false;
}
bool ReplaceSpriteTextureInPlace(void* sprite, const std::vector<std::filesystem::path>& candidates) {
if (!sprite || !Sprite_get_texture_Orig) return false;
const auto texture = Sprite_get_texture_Orig(sprite);
if (!IsTexture2D(texture)) return false;
return ApplyLocalImageToTexture2DFromCandidates(texture, candidates);
}
void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName) {
if (!Config::replaceTexture && !Config::dumpRuntimeTexture) return result;
if (Config::dumpRuntimeTexture) {
DumpTextureOrSpriteAsset(result);
}
if (!Config::replaceTexture) return result;
if (IsSprite(result)) {
if (ReplaceSpriteTextureInPlace(result, GetSpriteAssetTextureCandidates(result, assetName))) {
return result;
}
return result;
}
if (result && !IsTexture2D(result)) return result;
if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(std::filesystem::path(assetName)))) {
return localTexture;
}
return result;
}
void* ReplaceTextureOrSpriteByObjectName(void* result) {
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !result) return result;
const auto objectName = GetObjectName(result);
if (!objectName) return result;
const auto assetPath = std::filesystem::path(objectName->ToString());
if (Config::dumpRuntimeTexture) {
DumpTextureOrSpriteAsset(result);
}
if (!Config::replaceTexture) return result;
if (IsSprite(result)) {
std::vector<std::filesystem::path> candidates;
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(objectName->ToString()));
if (ReplaceSpriteTextureInPlace(result, candidates)) {
return result;
}
return result;
}
if (IsTexture2D(result)) {
if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(assetPath))) {
return localTexture;
}
}
return result;
}
void ReplaceAllAssetTextures(void* allAssets) {
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !allAssets) return;
auto assets = reinterpret_cast<UnityResolve::UnityType::Array<void*>*>(allAssets);
for (std::uintptr_t i = 0; i < assets->max_length; ++i) {
auto asset = assets->At(static_cast<unsigned int>(i));
auto replacedAsset = ReplaceTextureOrSpriteByObjectName(asset);
if (replacedAsset != asset) {
assets->At(static_cast<unsigned int>(i)) = replacedAsset;
}
}
}
void* ReplaceSpriteAssetByTextureName(void* sprite) {
if (!Config::replaceTexture || !sprite) return sprite;
if (!IsSprite(sprite)) {
return sprite;
}
if (ReplaceSpriteTextureInPlace(sprite, GetSpriteAssetTextureCandidates(sprite, ""))) {
return sprite;
}
return sprite;
}
void* ReplaceSpriteTexture(void* texture2D) {
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !IsTexture2D(texture2D)) return texture2D;
const auto objectName = GetObjectName(texture2D);
if (!objectName) return texture2D;
if (Config::dumpRuntimeTexture) {
DumpTexture2D(texture2D);
}
if (!Config::replaceTexture) return texture2D;
if (ApplyLocalImageToTexture2DFromCandidates(texture2D, GetSpriteTextureCandidates(objectName->ToString()))) {
return texture2D;
}
return texture2D;
}
void* ResolveSpriteGetTextureHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) {
return addr;
}
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture()")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite", "get_texture");
}
void* ResolveAssetBundleLoadAssetHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle",
"LoadAsset_Internal", {"System.String", "System.Type"});
}
void* ResolveAssetBundleLoadAssetAsyncHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle",
"LoadAssetAsync_Internal", {"System.String", "System.Type"});
}
void* ResolveAssetBundleRequestResultHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::GetResult()")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "GetResult");
}
void* ResolveAssetBundleRequestAssetHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_asset()")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_asset");
}
void* ResolveAssetBundleRequestAllAssetsHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_allAssets()")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_allAssets");
}
void* ResolveResourcesLoadHookAddress() {
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
"UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")) {
return addr;
}
return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "ResourcesAPIInternal",
"Load", {"System.String", "System.Type"});
}
}

View File

@ -0,0 +1,23 @@
#ifndef GAKUMAS_LOCALIFY_HOOK_TEXTURE_H
#define GAKUMAS_LOCALIFY_HOOK_TEXTURE_H
#include <string>
namespace GakumasLocal::HookMain
{
void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName);
void* ReplaceTextureOrSpriteByObjectName(void* result);
void ReplaceAllAssetTextures(void* allAssets);
void* ReplaceSpriteAssetByTextureName(void* sprite);
void* ReplaceSpriteTexture(void* texture2D);
void* ResolveSpriteGetTextureHookAddress();
void* ResolveAssetBundleLoadAssetHookAddress();
void* ResolveAssetBundleLoadAssetAsyncHookAddress();
void* ResolveAssetBundleRequestResultHookAddress();
void* ResolveAssetBundleRequestAssetHookAddress();
void* ResolveAssetBundleRequestAllAssetsHookAddress();
void* ResolveResourcesLoadHookAddress();
}
#endif // GAKUMAS_LOCALIFY_HOOK_TEXTURE_H

View File

@ -11,11 +11,13 @@ namespace GakumasLocal::Config {
bool enabled = true; bool enabled = true;
bool lazyInit = true; bool lazyInit = true;
bool replaceFont = true; bool replaceFont = true;
bool replaceTexture = true;
bool forceExportResource = true; bool forceExportResource = true;
bool textTest = false; bool textTest = false;
bool useMasterTrans = true; bool useMasterTrans = true;
int gameOrientation = 0; int gameOrientation = 0;
bool dumpText = false; bool dumpText = false;
bool dumpRuntimeTexture = false;
bool enableFreeCamera = false; bool enableFreeCamera = false;
int targetFrameRate = 0; int targetFrameRate = 0;
bool unlockAllLive = false; bool unlockAllLive = false;
@ -66,11 +68,13 @@ namespace GakumasLocal::Config {
GetConfigItem(enabled); GetConfigItem(enabled);
GetConfigItem(lazyInit); GetConfigItem(lazyInit);
GetConfigItem(replaceFont); GetConfigItem(replaceFont);
GetConfigItem(replaceTexture);
GetConfigItem(forceExportResource); GetConfigItem(forceExportResource);
GetConfigItem(gameOrientation); GetConfigItem(gameOrientation);
GetConfigItem(textTest); GetConfigItem(textTest);
GetConfigItem(useMasterTrans); GetConfigItem(useMasterTrans);
GetConfigItem(dumpText); GetConfigItem(dumpText);
GetConfigItem(dumpRuntimeTexture);
GetConfigItem(targetFrameRate); GetConfigItem(targetFrameRate);
GetConfigItem(enableFreeCamera); GetConfigItem(enableFreeCamera);
GetConfigItem(unlockAllLive); GetConfigItem(unlockAllLive);
@ -122,11 +126,13 @@ namespace GakumasLocal::Config {
SetConfigItem(enabled); SetConfigItem(enabled);
SetConfigItem(lazyInit); SetConfigItem(lazyInit);
SetConfigItem(replaceFont); SetConfigItem(replaceFont);
SetConfigItem(replaceTexture);
SetConfigItem(forceExportResource); SetConfigItem(forceExportResource);
SetConfigItem(gameOrientation); SetConfigItem(gameOrientation);
SetConfigItem(textTest); SetConfigItem(textTest);
SetConfigItem(useMasterTrans); SetConfigItem(useMasterTrans);
SetConfigItem(dumpText); SetConfigItem(dumpText);
SetConfigItem(dumpRuntimeTexture);
SetConfigItem(targetFrameRate); SetConfigItem(targetFrameRate);
SetConfigItem(enableFreeCamera); SetConfigItem(enableFreeCamera);
SetConfigItem(unlockAllLive); SetConfigItem(unlockAllLive);

View File

@ -7,11 +7,13 @@ namespace GakumasLocal::Config {
extern bool enabled; extern bool enabled;
extern bool lazyInit; extern bool lazyInit;
extern bool replaceFont; extern bool replaceFont;
extern bool replaceTexture;
extern bool forceExportResource; extern bool forceExportResource;
extern int gameOrientation; extern int gameOrientation;
extern bool textTest; extern bool textTest;
extern bool useMasterTrans; extern bool useMasterTrans;
extern bool dumpText; extern bool dumpText;
extern bool dumpRuntimeTexture;
extern bool enableFreeCamera; extern bool enableFreeCamera;
extern int targetFrameRate; extern int targetFrameRate;
extern bool unlockAllLive; extern bool unlockAllLive;

View File

@ -2,19 +2,23 @@
#include "i18nData/strings_en.hpp" #include "i18nData/strings_en.hpp"
#include "i18nData/strings_ja.hpp" #include "i18nData/strings_ja.hpp"
#include "i18nData/strings_zh-rCN.hpp" #include "i18nData/strings_zh-rCN.hpp"
#include "i18nData/strings_zh-rTW.hpp"
namespace GkmsGUII18n { namespace GkmsGUII18n {
const LANGID localLanguage = GetUserDefaultUILanguage(); const LANGID localLanguage = GetUserDefaultUILanguage();
std::unordered_set<LANGID> sChineseLangIds{ { 0x0004, 0x0804, 0x1004 } }; // zh-Hans, zh-CN, zh-SG std::unordered_set<LANGID> sChineseLangIds{ { 0x0004, 0x0804, 0x1004 } }; // zh-Hans, zh-CN, zh-SG
// std::unordered_set<LANGID> tChineseLangIds{ { 0x0404, 0x0c04, 0x1404, 0x048E } }; // zh-TW, zh-HK, zh-MO, zh-yue-HK std::unordered_set<LANGID> tChineseLangIds{ { 0x0404, 0x0c04, 0x1404, 0x048E } }; // zh-TW, zh-HK, zh-MO, zh-yue-HK
std::unordered_set<LANGID> jpnLangIds{ { 0x0011, 0x0411 } }; // ja, ja-JP std::unordered_set<LANGID> jpnLangIds{ { 0x0011, 0x0411 } }; // ja, ja-JP
std::unordered_map<std::string, std::string> GetI18nData() { std::unordered_map<std::string, std::string> GetI18nData() {
if (sChineseLangIds.contains(localLanguage)) { if (sChineseLangIds.contains(localLanguage)) {
return I18nData::i18nData_zh_rCN; return I18nData::i18nData_zh_rCN;
} }
else if (tChineseLangIds.contains(localLanguage)) {
return I18nData::i18nData_zh_rTW;
}
else if (jpnLangIds.contains(localLanguage)) { else if (jpnLangIds.contains(localLanguage)) {
return I18nData::i18nData_ja; return I18nData::i18nData_ja;
} }

View File

@ -153,6 +153,7 @@ namespace GkmsGUILoop {
if (ImGui::Begin("Gakumas Plugin Config")) { if (ImGui::Begin("Gakumas Plugin Config")) {
ImGui::Text("Plugin Version: %s", PLUGIN_VERSION); ImGui::Text("Plugin Version: %s", PLUGIN_VERSION);
ImGui::Text("Resource Version: %s", GkmsResourceUpdate::GetCurrentResourceVersion(true).c_str()); ImGui::Text("Resource Version: %s", GkmsResourceUpdate::GetCurrentResourceVersion(true).c_str());
ImGui::Text("%s: %s", ts("texture_version"), GkmsResourceUpdate::GetCurrentTextureVersion(true).c_str());
if (ImGui::Button("Reload Config And Translation Data")) { if (ImGui::Button("Reload Config And Translation Data")) {
g_reload_all_data(); g_reload_all_data();
@ -169,6 +170,7 @@ namespace GkmsGUILoop {
ImGui::Checkbox(ts("lazy_init"), &Config::lazyInit); ImGui::Checkbox(ts("lazy_init"), &Config::lazyInit);
ImGui::Checkbox(ts("replace_font"), &Config::replaceFont); ImGui::Checkbox(ts("replace_font"), &Config::replaceFont);
ImGui::Checkbox(ts("replace_texture"), &Config::replaceTexture);
ImGui::Unindent(indentWidth); ImGui::Unindent(indentWidth);
} }
@ -207,6 +209,24 @@ namespace GkmsGUILoop {
} }
} }
if (Config::replaceTexture) {
ImGui::Separator();
ImGui::Text("%s: %s", ts("downloaded_texture_version"), GkmsResourceUpdate::GetCurrentTextureVersion(true).c_str());
ImGui::Checkbox(ts("check_texture_from_api"), &g_useAPITextureAssets);
if (g_useAPITextureAssets) {
ImGui::Checkbox(ts("del_texture_remote_after_update"), &g_delTextureRemoteAfterUpdate);
InputTextString(ts("texture_api_addr"), &g_useAPITextureAssetsURL);
if (!downloading && ImGui::Button((std::string(ts("check_update")) + "##APITexture").c_str())) {
GkmsResourceUpdate::CheckTextureUpdateFromAPI(true);
}
if (downloading) {
ImGui::ProgressBar(downloadProgress);
ImGui::SameLine();
ImGui::Text("Downloading");
}
}
}
ImGui::Unindent(indentWidth); ImGui::Unindent(indentWidth);
} }
@ -308,6 +328,7 @@ namespace GkmsGUILoop {
ImGui::Checkbox(ts("useMasterDBTrans"), &Config::useMasterTrans); ImGui::Checkbox(ts("useMasterDBTrans"), &Config::useMasterTrans);
ImGui::Checkbox(ts("text_hook_test_mode"), &Config::textTest); ImGui::Checkbox(ts("text_hook_test_mode"), &Config::textTest);
ImGui::Checkbox(ts("export_text"), &Config::dumpText); ImGui::Checkbox(ts("export_text"), &Config::dumpText);
ImGui::Checkbox(ts("dump_runtime_texture"), &Config::dumpRuntimeTexture);
ImGui::Checkbox(ts("login_as_ios"), &Config::loginAsIOS); ImGui::Checkbox(ts("login_as_ios"), &Config::loginAsIOS);
ImGui::Unindent(indentWidth); ImGui::Unindent(indentWidth);

View File

@ -13,6 +13,7 @@ namespace I18nData {
{ "gakumas_localify", "Gakumas Localify" }, { "gakumas_localify", "Gakumas Localify" },
{ "enable_plugin", "Enable Plugin (Not Hot Reloadable)" }, { "enable_plugin", "Enable Plugin (Not Hot Reloadable)" },
{ "replace_font", "Replace Font" }, { "replace_font", "Replace Font" },
{ "replace_texture", "Replace Texture" },
{ "lazy_init", "Fast Initialization (Lazy loading)" }, { "lazy_init", "Fast Initialization (Lazy loading)" },
{ "enable_free_camera", "Enable Free Camera" }, { "enable_free_camera", "Enable Free Camera" },
{ "start_game", "Start Game / Hot Reload Config" }, { "start_game", "Start Game / Hot Reload Config" },
@ -27,6 +28,7 @@ namespace I18nData {
{ "text_hook_test_mode", "Text Hook Test Mode" }, { "text_hook_test_mode", "Text Hook Test Mode" },
{ "useMasterDBTrans", "Enable MasterDB Localization" }, { "useMasterDBTrans", "Enable MasterDB Localization" },
{ "export_text", "Export Text" }, { "export_text", "Export Text" },
{ "dump_runtime_texture", "Dump Runtime Texture" },
{ "force_export_resource", "Force Update Resource" }, { "force_export_resource", "Force Update Resource" },
{ "login_as_ios", "Login as iOS" }, { "login_as_ios", "Login as iOS" },
{ "max_high", "Ultra" }, { "max_high", "Ultra" },
@ -88,13 +90,18 @@ namespace I18nData {
{ "cancel", "Cancel" }, { "cancel", "Cancel" },
{ "ok", "OK" }, { "ok", "OK" },
{ "downloaded_resource_version", "Downloaded Version" }, { "downloaded_resource_version", "Downloaded Version" },
{ "downloaded_texture_version", "Downloaded Texture Version" },
{ "del_remote_after_update", "Delete Cache File After Update" }, { "del_remote_after_update", "Delete Cache File After Update" },
{ "del_texture_remote_after_update", "Delete Cache File After Update" },
{ "warning", "Warning" }, { "warning", "Warning" },
{ "install", "Install" }, { "install", "Install" },
{ "installing", "Installing" }, { "installing", "Installing" },
{ "check_resource_from_api", "Check Resource Update From API" }, { "check_resource_from_api", "Check Resource Update From API" },
{ "check_texture_from_api", "Check Texture Update From API" },
{ "api_addr", "API AddressGithub Latest Release API" }, { "api_addr", "API AddressGithub Latest Release API" },
{ "texture_api_addr", "Texture API AddressGithub Latest Release API" },
{ "check_update", "Check" }, { "check_update", "Check" },
{ "texture_version", "Texture Version" },
{ "translation_resource_update", "Translation Resource Update" }, { "translation_resource_update", "Translation Resource Update" },
{ "game_patch", "Game Patch" }, { "game_patch", "Game Patch" },
{ "patch_mode", "Patch Mode" }, { "patch_mode", "Patch Mode" },
@ -114,4 +121,4 @@ namespace I18nData {
{ "about_contributors_asset_file", "about_contributors_en.json" }, { "about_contributors_asset_file", "about_contributors_en.json" },
{ "default_assets_check_api", "https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest" }, { "default_assets_check_api", "https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest" },
}; };
} }

View File

@ -37,6 +37,7 @@ namespace I18nData {
{ "character_counter_pattern", "%1$d/%2$d" }, { "character_counter_pattern", "%1$d/%2$d" },
{ "check_built_in_resource", "内蔵アセットのアップデートを確認" }, { "check_built_in_resource", "内蔵アセットのアップデートを確認" },
{ "check_resource_from_api", "リソースアップデートを API から確認" }, { "check_resource_from_api", "リソースアップデートを API から確認" },
{ "check_texture_from_api", "テクスチャアップデートを API から確認" },
{ "check_update", "確認" }, { "check_update", "確認" },
{ "clear_text_end_icon_content_description", "テキストを消去" }, { "clear_text_end_icon_content_description", "テキストを消去" },
{ "close_drawer", "ナビゲーションメニューを閉じる" }, { "close_drawer", "ナビゲーションメニューを閉じる" },
@ -48,9 +49,11 @@ namespace I18nData {
{ "default_error_message", "入力が無効です" }, { "default_error_message", "入力が無効です" },
{ "default_popup_window_title", "ポップアップウィンドウ" }, { "default_popup_window_title", "ポップアップウィンドウ" },
{ "del_remote_after_update", "キャッシュファイルをアップデート後に削除" }, { "del_remote_after_update", "キャッシュファイルをアップデート後に削除" },
{ "del_texture_remote_after_update", "キャッシュファイルをアップデート後に削除" },
{ "delete_plugin_resource", "プラグインリソースを削除" }, { "delete_plugin_resource", "プラグインリソースを削除" },
{ "download", "ダウンロード" }, { "download", "ダウンロード" },
{ "downloaded_resource_version", "ダウンロードされたバージョン" }, { "downloaded_resource_version", "ダウンロードされたバージョン" },
{ "downloaded_texture_version", "ダウンロードされたテクスチャバージョン" },
{ "dropdown_menu", "ドロップダウンメニュー" }, { "dropdown_menu", "ドロップダウンメニュー" },
{ "enable_breast_param", "胸のパラメーターを有効化" }, { "enable_breast_param", "胸のパラメーターを有効化" },
{ "enable_free_camera", "フリーカメラを有効化" }, { "enable_free_camera", "フリーカメラを有効化" },
@ -58,6 +61,7 @@ namespace I18nData {
{ "error_a11y_label", "エラー: 無効" }, { "error_a11y_label", "エラー: 無効" },
{ "error_icon_content_description", "エラー" }, { "error_icon_content_description", "エラー" },
{ "export_text", "テキストをエクスポート" }, { "export_text", "テキストをエクスポート" },
{ "dump_runtime_texture", "ランタイムテクスチャをダンプ" },
{ "exposed_dropdown_menu_content_description", "ドロップダウンメニューを表示" }, { "exposed_dropdown_menu_content_description", "ドロップダウンメニューを表示" },
{ "force_export_resource", "リソースのアップデートを強制する" }, { "force_export_resource", "リソースのアップデートを強制する" },
{ "login_as_ios", "iOSとしてログイン" }, { "login_as_ios", "iOSとしてログイン" },
@ -108,6 +112,7 @@ namespace I18nData {
{ "range_start", "範囲の開始" }, { "range_start", "範囲の開始" },
{ "renderscale", "RenderScale (0.5/0.59/0.67/0.77/1.0)" }, { "renderscale", "RenderScale (0.5/0.59/0.67/0.77/1.0)" },
{ "replace_font", "フォントを置換する" }, { "replace_font", "フォントを置換する" },
{ "replace_texture", "テクスチャを置換する" },
{ "reserve_patched", "パッチ済みの APK を予約する" }, { "reserve_patched", "パッチ済みの APK を予約する" },
{ "resource_settings", "リソース設定" }, { "resource_settings", "リソース設定" },
{ "resource_url", "リソース URL" }, { "resource_url", "リソース URL" },
@ -128,6 +133,8 @@ namespace I18nData {
{ "useMasterDBTrans", "MasterDB をローカライズする" }, { "useMasterDBTrans", "MasterDB をローカライズする" },
{ "translation_repository", "翻訳のリポジトリ" }, { "translation_repository", "翻訳のリポジトリ" },
{ "translation_resource_update", "翻訳リソースをアップデート" }, { "translation_resource_update", "翻訳リソースをアップデート" },
{ "texture_api_addr", "テクスチャ API アドレス (GitHub の最新リリース API)" },
{ "texture_version", "Texture version" },
{ "unlockAllLive", "すべてのライブを開放" }, { "unlockAllLive", "すべてのライブを開放" },
{ "unlockAllLiveCostume", "すべてのライブ衣装を開放" }, { "unlockAllLiveCostume", "すべてのライブ衣装を開放" },
{ "useCustomeGraphicSettings", "カスタムグラフィック設定を使用する" }, { "useCustomeGraphicSettings", "カスタムグラフィック設定を使用する" },
@ -139,4 +146,4 @@ namespace I18nData {
{ "very_high", "最高" }, { "very_high", "最高" },
{ "warning", "警告" }, { "warning", "警告" },
}; };
} }

View File

@ -13,6 +13,7 @@ namespace I18nData {
{ "gakumas_localify", "Gakumas Localify" }, { "gakumas_localify", "Gakumas Localify" },
{ "enable_plugin", "启用插件 (不可热重载)" }, { "enable_plugin", "启用插件 (不可热重载)" },
{ "replace_font", "替换字体" }, { "replace_font", "替换字体" },
{ "replace_texture", "替换贴图" },
{ "lazy_init", "快速初始化(懒加载配置)" }, { "lazy_init", "快速初始化(懒加载配置)" },
{ "enable_free_camera", "启用自由视角(可热重载; 需使用实体键盘)" }, { "enable_free_camera", "启用自由视角(可热重载; 需使用实体键盘)" },
{ "start_game", "以上述配置启动游戏/重载配置" }, { "start_game", "以上述配置启动游戏/重载配置" },
@ -27,6 +28,7 @@ namespace I18nData {
{ "text_hook_test_mode", "文本 hook 测试模式" }, { "text_hook_test_mode", "文本 hook 测试模式" },
{ "useMasterDBTrans", "使用 MasterDB 本地化" }, { "useMasterDBTrans", "使用 MasterDB 本地化" },
{ "export_text", "导出文本" }, { "export_text", "导出文本" },
{ "dump_runtime_texture", "导出运行时贴图" },
{ "force_export_resource", "启动后强制导出资源" }, { "force_export_resource", "启动后强制导出资源" },
{ "login_as_ios", "以 iOS 登陆" }, { "login_as_ios", "以 iOS 登陆" },
{ "max_high", "极高" }, { "max_high", "极高" },
@ -88,13 +90,18 @@ namespace I18nData {
{ "cancel", "取消" }, { "cancel", "取消" },
{ "ok", "确定" }, { "ok", "确定" },
{ "downloaded_resource_version", "已下载资源版本" }, { "downloaded_resource_version", "已下载资源版本" },
{ "downloaded_texture_version", "已下载贴图资源版本" },
{ "del_remote_after_update", "替换文件后删除下载缓存" }, { "del_remote_after_update", "替换文件后删除下载缓存" },
{ "del_texture_remote_after_update", "替换文件后删除下载缓存" },
{ "warning", "注意" }, { "warning", "注意" },
{ "install", "安装" }, { "install", "安装" },
{ "installing", "安装中" }, { "installing", "安装中" },
{ "check_resource_from_api", "从服务器检查热更新资源" }, { "check_resource_from_api", "从服务器检查热更新资源" },
{ "check_texture_from_api", "从服务器检查贴图资源更新" },
{ "api_addr", "API 地址Github Latest Release API" }, { "api_addr", "API 地址Github Latest Release API" },
{ "texture_api_addr", "贴图 API 地址Github Latest Release API" },
{ "check_update", "检查更新" }, { "check_update", "检查更新" },
{ "texture_version", "Texture version" },
{ "translation_resource_update", "翻译资源更新" }, { "translation_resource_update", "翻译资源更新" },
{ "game_patch", "游戏修补" }, { "game_patch", "游戏修补" },
{ "patch_mode", "修补模式" }, { "patch_mode", "修补模式" },
@ -114,4 +121,4 @@ namespace I18nData {
{ "about_contributors_asset_file", "about_contributors_zh_cn.json" }, { "about_contributors_asset_file", "about_contributors_zh_cn.json" },
{ "default_assets_check_api", "https://uma.chinosk6.cn/api/gkms_trans_data" }, { "default_assets_check_api", "https://uma.chinosk6.cn/api/gkms_trans_data" },
}; };
} }

View File

@ -0,0 +1,124 @@
#pragma once
#include <unordered_map>
#include <string>
namespace I18nData {
static const std::unordered_map<std::string, std::string> i18nData_zh_rTW = {
{ "local_file_already_latest", "本機檔案已經是最新版本,是否繼續更新?" },
{ "dmmUnlockSize", "解鎖視窗大小" },
{ "dmmUnlockSizeHelp", "可隨意拖動視窗大小。使用 F11 切換全螢幕。" },
{ "app_name", "Gakumas Localify" },
{ "gakumas_localify", "Gakumas Localify" },
{ "enable_plugin", "啟用插件 (不可熱重載)" },
{ "replace_font", "替換字體" },
{ "replace_texture", "替換貼圖" },
{ "lazy_init", "快速初始化(懶人設定)" },
{ "enable_free_camera", "啟用自由視角(可熱重載; 需使用實體鍵盤)" },
{ "start_game", "以上述設定啟動遊戲/重載設定" },
{ "setFpsTitle", "最大 FPS (0 為保持遊戲原設定)" },
{ "unlockAllLive", "解鎖所有 Live" },
{ "unlockAllLiveCostume", "解鎖所有 Live 服裝" },
{ "liveUseCustomeDress", "Live 使用自定義角色" },
{ "live_costume_head_id", "Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)" },
{ "live_custome_dress_id", "Live 自定義服裝 ID (例: hski-cstm-0002)" },
{ "useCustomeGraphicSettings", "使用自定義畫質設定" },
{ "renderscale", "RenderScale (0.5/0.59/0.67/0.77/1.0)" },
{ "text_hook_test_mode", "文本 hook 測試模式" },
{ "useMasterDBTrans", "使用 MasterDB 翻譯" },
{ "export_text", "導出文本" },
{ "dump_runtime_texture", "導出運行時貼圖" },
{ "force_export_resource", "啟動後強制導出資源" },
{ "login_as_ios", "模擬以 iOS 登入" },
{ "max_high", "極高" },
{ "very_high", "超高" },
{ "hign", "" },
{ "middle", "" },
{ "low", "" },
{ "orientation_orig", "原版" },
{ "orientation_portrait", "豎屏" },
{ "orientation_landscape", "橫屏" },
{ "orientation_lock", "方向鎖定" },
{ "enable_breast_param", "啟用胸部參數" },
{ "damping", "阻尼 (Damping)" },
{ "stiffness", "剛度 (Stiffness)" },
{ "spring", "彈簧係數 (Spring)" },
{ "pendulum", "鐘擺係數 (Pendulum)" },
{ "pendulumrange", "鐘擺範圍 (PendulumRange)" },
{ "average", "Average" },
{ "rootweight", "RootWeight" },
{ "uselimit_0_1", "範圍限制倍率 (0 為不限制, 1 為原版)" },
{ "usearmcorrection", "使用手臂矯正" },
{ "isdirty", "IsDirty" },
{ "usescale", "應用縮放" },
{ "breast_scale", "胸部縮放倍率" },
{ "uselimitmultiplier", "啟用範圍限制倍率" },
{ "axisx_x", "axisX.x" },
{ "axisy_x", "axisY.x" },
{ "axisz_x", "axisZ.x" },
{ "axisx_y", "axisX.y" },
{ "axisy_y", "axisY.y" },
{ "axisz_y", "axisZ.y" },
{ "basic_settings", "基本設定" },
{ "graphic_settings", "畫面設定" },
{ "camera_settings", "攝影機設定" },
{ "test_mode_live", "測試模式 - LIVE" },
{ "debug_settings", "調試設定" },
{ "breast_param", "胸部參數" },
{ "about", "關於" },
{ "home", "主頁" },
{ "advanced_settings", "進階設定" },
{ "about_warn_title", "使用前警告" },
{ "about_warn_p1", "本插件僅供學習和交流使用。" },
{ "about_warn_p2", "使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。" },
{ "about_about_title", "關於本插件" },
{ "about_about_p1", "本插件完全免費。若您付費購買了本插件,請檢舉店家。" },
{ "about_about_p2", "插件交流QQ群: 991990192" },
{ "project_contribution", "項目貢獻" },
{ "plugin_code", "插件本體" },
{ "contributors", "貢獻者列表" },
{ "translation_repository", "譯文倉庫" },
{ "resource_settings", "資源設定" },
{ "check_built_in_resource", "檢查內置翻譯資源更新" },
{ "delete_plugin_resource", "清除遊戲目錄內的插件翻譯資源" },
{ "use_remote_zip_resource", "使用雲端 ZIP 翻譯資源" },
{ "resource_url", "資源地址" },
{ "download", "下載" },
{ "invalid_zip_file", "文件解析失敗" },
{ "invalid_zip_file_warn", "此 ZIP 文件不是一個有效的翻譯資源包" },
{ "cancel", "取消" },
{ "ok", "確定" },
{ "downloaded_resource_version", "已下載資源版本" },
{ "downloaded_texture_version", "已下載貼圖資源版本" },
{ "del_remote_after_update", "替換文件後刪除下載緩存" },
{ "del_texture_remote_after_update", "替換文件後刪除下載緩存" },
{ "warning", "注意" },
{ "install", "安裝" },
{ "installing", "安裝中" },
{ "check_resource_from_api", "从伺服器檢查更新資源" },
{ "check_texture_from_api", "从伺服器檢查貼圖資源更新" },
{ "api_addr", "API 地址Github Latest Release API" },
{ "texture_api_addr", "貼圖 API 地址Github Latest Release API" },
{ "check_update", "檢查更新" },
{ "texture_version", "Texture version" },
{ "translation_resource_update", "翻譯資源更新" },
{ "game_patch", "遊戲修補" },
{ "patch_mode", "修補模式" },
{ "patch_local", "本地模式" },
{ "patch_local_desc", "為未嵌入模塊的遊戲程式打補丁。\\nXposed 範圍可動態更改,無需重新打補丁。\\n以本地模式修補的遊戲程式只能在本地設備上執行。" },
{ "patch_integrated", "集成模式" },
{ "patch_integrated_desc", "修補遊戲程式並內置模塊。\\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行但不能動態管理設定。\\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。" },
{ "shizuku_available", "Shizuku 服務可用" },
{ "shizuku_unavailable", "Shizuku 服務未連接" },
{ "home_shizuku_warning", "部分功能不可用" },
{ "patch_debuggable", "可調試" },
{ "reserve_patched", "安裝時保留修補包" },
{ "support_file_types", "支援文件類型:\\n單/多選 apk\\n單選 apks, xapk, zip" },
{ "patch_uninstall_text", "由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\\n請確保您已備份好個人資料。" },
{ "patch_uninstall_confirm", "您確定要刪除吗" },
{ "patch_finished", "修補完成,是否開始安裝?" },
{ "about_contributors_asset_file", "about_contributors_zh_cn.json" },
{ "default_assets_check_api", "https://uma.chinosk6.cn/api/gkms_trans_data" },
};
}

View File

@ -25,8 +25,11 @@ bool g_has_config_file = false;
bool g_enable_console = true; bool g_enable_console = true;
bool g_useRemoteAssets = false; bool g_useRemoteAssets = false;
bool g_useAPIAssets = false; bool g_useAPIAssets = false;
bool g_useAPITextureAssets = false;
bool g_delTextureRemoteAfterUpdate = true;
std::string g_remoteResourceUrl = ""; std::string g_remoteResourceUrl = "";
std::string g_useAPIAssetsURL = ""; std::string g_useAPIAssetsURL = "";
std::string g_useAPITextureAssetsURL = "https://texture.gakumas.cn/api/gkms_texture_data";
namespace namespace
{ {
@ -84,6 +87,18 @@ void readProgramConfig() {
g_useAPIAssetsURL = document["useAPIAssetsURL"].GetString(); g_useAPIAssetsURL = document["useAPIAssetsURL"].GetString();
} }
if (document.HasMember("useAPITextureAssets")) {
g_useAPITextureAssets = document["useAPITextureAssets"].GetBool();
}
if (document.HasMember("useAPITextureAssetsURL")) {
g_useAPITextureAssetsURL = document["useAPITextureAssetsURL"].GetString();
}
if (document.HasMember("delTextureRemoteAfterUpdate")) {
g_delTextureRemoteAfterUpdate = document["delTextureRemoteAfterUpdate"].GetBool();
}
} }
config_stream.close(); config_stream.close();
} }
@ -142,7 +157,7 @@ int __stdcall DllMain(HINSTANCE dllModule, DWORD reason, LPVOID)
std::condition_variable cond; std::condition_variable cond;
std::atomic<bool> hookIsReady(false); std::atomic<bool> hookIsReady(false);
// 依赖检查游戏版本的指针加载,因此在 hook 完成后再加载翻译数据 // 渚濊禆妫€鏌ユ父鎴忕増鏈殑鎸囬拡鍔犺浇锛屽洜姝ゅ湪 hook 瀹屾垚鍚庡啀鍔犺浇缈昏瘧鏁版嵁
std::unique_lock lock(mutex); std::unique_lock lock(mutex);
cond.wait(lock, [&] { cond.wait(lock, [&] {
return hookIsReady.load(std::memory_order_acquire); return hookIsReady.load(std::memory_order_acquire);

View File

@ -11,7 +11,7 @@
#define LogMinVersion ANDROID_LOG_DEBUG #define LogMinVersion ANDROID_LOG_DEBUG
#define PLUGIN_VERSION "3.2.0" #define PLUGIN_VERSION "v3.3.1"
#define ADD_HOOK(name, addr) \ #define ADD_HOOK(name, addr) \
name##_Addr = reinterpret_cast<name##_Type>(addr); \ name##_Addr = reinterpret_cast<name##_Type>(addr); \

View File

@ -1,10 +1,14 @@
#include "stdinclude.hpp" #include "stdinclude.hpp"
#include "cpprest/http_client.h" #include "cpprest/http_client.h"
#include "cpprest/filestream.h" #include "cpprest/filestream.h"
#include "nlohmann/json.hpp" #include "nlohmann/json.hpp"
#include "GakumasLocalify/Log.h" #include "GakumasLocalify/Log.h"
#include "gkmsGUI/GUII18n.hpp" #include "gkmsGUI/GUII18n.hpp"
#include <atomic>
#include <algorithm>
#include <cctype>
#include <format> #include <format>
#include <sstream>
#include "unzip.hpp" #include "unzip.hpp"
extern std::filesystem::path gakumasLocalPath; extern std::filesystem::path gakumasLocalPath;
@ -13,8 +17,82 @@ extern bool downloading;
extern float downloadProgress; extern float downloadProgress;
extern std::function<void()> g_reload_all_data; extern std::function<void()> g_reload_all_data;
std::string resourceVersionCache = ""; std::string resourceVersionCache = "";
std::string textureVersionCache = "";
namespace GkmsResourceUpdate { namespace GkmsResourceUpdate {
std::atomic_bool updateJobRunning = false;
class UpdateJobGuard {
public:
explicit UpdateJobGuard(const char* jobName) : active(false) {
bool expected = false;
if (!updateJobRunning.compare_exchange_strong(expected, true)) {
GakumasLocal::Log::InfoFmt("Skip %s: another resource update job is running.", jobName);
return;
}
active = true;
downloading = true;
downloadProgress = 0.0f;
}
~UpdateJobGuard() {
if (!active) return;
downloading = false;
updateJobRunning = false;
}
explicit operator bool() const {
return active;
}
private:
bool active;
};
std::string trimString(std::string content) {
auto is_not_space = [](unsigned char ch) {
return !std::isspace(ch);
};
content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space));
content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end());
return content;
}
std::string readTrimmedFile(const std::filesystem::path& filePath) {
std::ifstream file(filePath);
if (!file) {
return "Unknown";
}
std::stringstream buffer;
buffer << file.rdbuf();
return trimString(buffer.str());
}
std::filesystem::path getTextureResourceRoot() {
return gakumasLocalPath / "texture2d";
}
std::filesystem::path findTextureZipSourceDir(const std::filesystem::path& extractDir) {
std::error_code ec;
std::filesystem::path fallback;
for (const auto& entry : std::filesystem::recursive_directory_iterator(
extractDir, std::filesystem::directory_options::skip_permission_denied, ec)) {
if (ec) break;
if (!entry.is_regular_file(ec)) continue;
if (entry.path().filename() != "texture_version.txt") continue;
const auto parent = entry.path().parent_path();
if (parent.filename() == "texture2d") {
return parent;
}
if (fallback.empty()) {
fallback = parent;
}
}
return fallback;
}
void saveProgramConfig() { void saveProgramConfig() {
nlohmann::json config; nlohmann::json config;
config["enableConsole"] = g_enable_console; config["enableConsole"] = g_enable_console;
@ -22,6 +100,9 @@ namespace GkmsResourceUpdate {
config["transRemoteZipUrl"] = g_remoteResourceUrl; config["transRemoteZipUrl"] = g_remoteResourceUrl;
config["useAPIAssets"] = g_useAPIAssets; config["useAPIAssets"] = g_useAPIAssets;
config["useAPIAssetsURL"] = g_useAPIAssetsURL; config["useAPIAssetsURL"] = g_useAPIAssetsURL;
config["useAPITextureAssets"] = g_useAPITextureAssets;
config["useAPITextureAssetsURL"] = g_useAPITextureAssetsURL;
config["delTextureRemoteAfterUpdate"] = g_delTextureRemoteAfterUpdate;
std::ofstream out(ProgramConfigJson); std::ofstream out(ProgramConfigJson);
if (!out) { if (!out) {
@ -47,18 +128,18 @@ namespace GkmsResourceUpdate {
using namespace concurrency::streams; using namespace concurrency::streams;
try { try {
// 打开输出文件流(同步方式) // 打开输出文件流(同步方式)
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 会自动解析 // 创建 HTTP 客户端,注意:如果 url 包含完整路径cpprestsdk 会自动解析
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 请求 // 发起 GET 请求
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 +149,12 @@ namespace GkmsResourceUpdate {
return false; return false;
} }
// 获取响应头中的文件大小(如果存在) // 获取响应头中的文件大小(如果存在)
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));
// 读取响应体,逐块写入文件,同时更新进度 // 读取响应体,逐块写入文件,同时更新进度
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);
@ -108,26 +189,20 @@ namespace GkmsResourceUpdate {
} }
auto resourceVersionFile = gakumasLocalPath / "version.txt"; auto resourceVersionFile = gakumasLocalPath / "version.txt";
std::ifstream file(resourceVersionFile); resourceVersionCache = readTrimmedFile(resourceVersionFile);
if (!file) { return resourceVersionCache;
// GakumasLocal::Log::ErrorFmt("Can't open file: %s", resourceVersionFile.string().c_str()); }
return "Unknown";
std::string GetCurrentTextureVersion(bool useCache) {
if (useCache) {
if (!textureVersionCache.empty()) {
return textureVersionCache;
}
} }
std::stringstream buffer; auto textureVersionFile = getTextureResourceRoot() / "texture_version.txt";
buffer << file.rdbuf(); textureVersionCache = readTrimmedFile(textureVersionFile);
std::string content = buffer.str(); return textureVersionCache;
// 去除首尾空格和换行符
auto is_not_space = [](unsigned char ch) {
return !std::isspace(ch);
};
// 去除前导空白
content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space));
// 去除尾部空白
content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end());
resourceVersionCache = content;
return content;
} }
bool unzipFileFromURL(std::string downloadUrl, const std::string& unzipPath, const std::string& targetDir = "") { bool unzipFileFromURL(std::string downloadUrl, const std::string& unzipPath, const std::string& targetDir = "") {
@ -146,12 +221,106 @@ namespace GkmsResourceUpdate {
return true; return true;
} }
bool installTextureZipFromURL(const std::string& downloadUrl, const std::string& expectedVersion) {
auto textureRoot = getTextureResourceRoot();
auto tempZipFile = gakumasLocalPath / "temp_texture_download.zip";
auto extractDir = gakumasLocalPath / "texture2d.extract";
auto installDir = gakumasLocalPath / "texture2d.tmp";
std::error_code ec;
std::filesystem::remove(tempZipFile, ec);
std::filesystem::remove_all(extractDir, ec);
std::filesystem::remove_all(installDir, ec);
std::filesystem::create_directories(gakumasLocalPath, ec);
if (!DownloadFile(downloadUrl, tempZipFile.string())) {
GakumasLocal::Log::Error("Download texture zip file failed.");
return false;
}
if (!UnzipFile(tempZipFile.string(), extractDir.string())) {
GakumasLocal::Log::Error("Unzip texture file failed.");
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
auto sourceDir = findTextureZipSourceDir(extractDir);
if (sourceDir.empty()) {
GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt not found.");
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
auto versionFile = sourceDir / "texture_version.txt";
const auto packageVersion = readTrimmedFile(versionFile);
if (packageVersion.empty() || packageVersion == "Unknown") {
GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt is empty or not found.");
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
if (!expectedVersion.empty() && packageVersion != expectedVersion) {
GakumasLocal::Log::ErrorFmt(
"Texture zip validation failed: texture_version.txt (%s) differs from release tag (%s).",
packageVersion.c_str(), expectedVersion.c_str());
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
std::filesystem::copy(sourceDir, installDir,
std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, ec);
if (ec) {
GakumasLocal::Log::ErrorFmt("Copy texture resource failed: %s", ec.message().c_str());
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
std::filesystem::remove_all(textureRoot, ec);
ec.clear();
std::filesystem::rename(installDir, textureRoot, ec);
if (ec) {
GakumasLocal::Log::ErrorFmt("Install texture resource failed: %s", ec.message().c_str());
std::filesystem::remove_all(installDir, ec);
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
return false;
}
std::filesystem::remove_all(extractDir, ec);
if (g_delTextureRemoteAfterUpdate) {
std::filesystem::remove(tempZipFile, ec);
}
textureVersionCache.clear();
const auto installedVersion = GetCurrentTextureVersion(false);
GakumasLocal::Log::InfoFmt("Texture zip installed into %s, version=%s",
textureRoot.string().c_str(), installedVersion.c_str());
return true;
}
void CheckUpdateFromAPI(bool isManual) { void CheckUpdateFromAPI(bool isManual) {
std::thread([isManual]() { std::thread([isManual]() {
try { try {
if (!g_useAPIAssets) { if (!g_useAPIAssets) {
return; return;
} }
UpdateJobGuard job("resource API update");
if (!job) return;
GakumasLocal::Log::Info("Checking update from API..."); GakumasLocal::Log::Info("Checking update from API...");
@ -204,7 +373,7 @@ namespace GkmsResourceUpdate {
g_reload_all_data(); g_reload_all_data();
GakumasLocal::Log::Info("Update completed."); GakumasLocal::Log::Info("Update completed.");
} }
// 仅解压一个文件 // 仅解压一个文件
return; return;
} }
} }
@ -217,8 +386,100 @@ namespace GkmsResourceUpdate {
}).detach(); }).detach();
} }
void CheckTextureUpdateFromAPI(bool isManual) {
std::thread([isManual]() {
try {
if (!g_useAPITextureAssets) {
return;
}
UpdateJobGuard job("texture API update");
if (!job) return;
GakumasLocal::Log::Info("Checking texture update from API...");
auto response = send_get(g_useAPITextureAssetsURL, 30);
if (response.status_code() != 200) {
GakumasLocal::Log::ErrorFmt("Failed to check texture update from API: %d\n", response.status_code());
return;
}
auto data = nlohmann::json::parse(response.extract_utf8string().get());
if (!data.contains("tag_name") || !data["tag_name"].is_string()) {
GakumasLocal::Log::Error("Texture API response doesn't contain tag_name.");
return;
}
std::string remoteVersion = data["tag_name"];
const auto localVersion = GetCurrentTextureVersion(false);
if (localVersion == remoteVersion) {
if (isManual) {
auto check = MessageBoxW(NULL, utility::conversions::to_string_t(GkmsGUII18n::ts("local_file_already_latest")).c_str(),
L"Texture Resource Update", MB_OKCANCEL);
if (check != IDOK) {
return;
}
}
else {
return;
}
}
std::string description = "";
if (data.contains("body") && data["body"].is_string()) {
description = data["body"];
}
auto check = MessageBoxW(NULL, std::format(L"{} -> {}\n\n{}", utility::conversions::to_string_t(localVersion),
utility::conversions::to_string_t(remoteVersion), utility::conversions::to_string_t(description)).c_str(),
L"Texture Resource Update", MB_OKCANCEL);
if (check != IDOK) {
return;
}
if (!data.contains("assets") || !data["assets"].is_array()) {
GakumasLocal::Log::Error("Texture API response doesn't contain assets array.");
return;
}
std::string downloadUrl = "";
for (const auto& asset : data["assets"]) {
if (!asset.contains("name") || !asset.contains("browser_download_url") || !asset["name"].is_string() || !asset["browser_download_url"].is_string()) {
continue;
}
std::string name = asset["name"];
std::string lowerName = name;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
if (lowerName.ends_with(".zip")) {
downloadUrl = asset["browser_download_url"];
if (lowerName == "texture2d.zip") {
break;
}
}
}
if (downloadUrl.empty()) {
GakumasLocal::Log::Error("No texture .zip file found.");
return;
}
if (installTextureZipFromURL(downloadUrl, remoteVersion)) {
g_reload_all_data();
GakumasLocal::Log::Info("Texture update completed.");
}
}
catch (std::exception& e) {
GakumasLocal::Log::ErrorFmt("Exception occurred in CheckTextureUpdateFromAPI: %s\n", e.what());
}
}).detach();
}
void checkUpdateFromURL(const std::string& downloadUrl) { void checkUpdateFromURL(const std::string& downloadUrl) {
std::thread([downloadUrl]() { std::thread([downloadUrl]() {
UpdateJobGuard job("remote zip update");
if (!job) return;
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) { if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) {
g_reload_all_data(); g_reload_all_data();
GakumasLocal::Log::Info("Update completed."); GakumasLocal::Log::Info("Update completed.");

View File

@ -1,10 +1,12 @@
#pragma once #pragma once
#include <string> #include <string>
namespace GkmsResourceUpdate { namespace GkmsResourceUpdate {
void saveProgramConfig(); void saveProgramConfig();
std::string GetCurrentResourceVersion(bool useCache); std::string GetCurrentResourceVersion(bool useCache);
std::string GetCurrentTextureVersion(bool useCache);
void CheckUpdateFromAPI(bool isManual); void CheckUpdateFromAPI(bool isManual);
void CheckTextureUpdateFromAPI(bool isManual);
void checkUpdateFromURL(const std::string& downloadUrl); void checkUpdateFromURL(const std::string& downloadUrl);
} }

View File

@ -1,4 +1,4 @@
#pragma once #pragma once
#define NOMINMAX #define NOMINMAX
@ -38,10 +38,13 @@
extern bool g_has_config_file; extern bool g_has_config_file;
// config 区 // config 区
extern bool g_enable_console; extern bool g_enable_console;
extern bool g_useRemoteAssets; extern bool g_useRemoteAssets;
extern bool g_useAPIAssets; extern bool g_useAPIAssets;
extern bool g_useAPITextureAssets;
extern bool g_delTextureRemoteAfterUpdate;
extern std::string g_remoteResourceUrl; extern std::string g_remoteResourceUrl;
extern std::string g_useAPIAssetsURL; extern std::string g_useAPIAssetsURL;
// config 区结束 extern std::string g_useAPITextureAssetsURL;
// config 区结束

View File

@ -1,4 +1,4 @@
#include "windowsPlatform.hpp" #include "windowsPlatform.hpp"
#include "GakumasLocalify/Plugin.h" #include "GakumasLocalify/Plugin.h"
#include "GakumasLocalify/Log.h" #include "GakumasLocalify/Log.h"
#include "GakumasLocalify/Local.h" #include "GakumasLocalify/Local.h"
@ -158,6 +158,7 @@ void reload_all_data() {
readProgramConfig(); readProgramConfig();
loadConfig(ConfigJson); loadConfig(ConfigJson);
GkmsResourceUpdate::GetCurrentResourceVersion(false); GkmsResourceUpdate::GetCurrentResourceVersion(false);
GkmsResourceUpdate::GetCurrentTextureVersion(false);
GakumasLocal::Local::LoadData(); GakumasLocal::Local::LoadData();
GakumasLocal::MasterLocal::LoadData(); GakumasLocal::MasterLocal::LoadData();
} }
@ -234,7 +235,7 @@ namespace GakumasLocal::WinHooks {
v8[0] = 0x100000000LL; v8[0] = 0x100000000LL;
if (currFullScreen) { if (currFullScreen) {
// 取消全屏 // 取消全屏
if (savedWidth == -1) { if (savedWidth == -1) {
savedWidth = 542; savedWidth = 542;
savedHeight = 990; savedHeight = 990;
@ -270,7 +271,7 @@ namespace GakumasLocal::WinHooks {
UINT size = sizeof(RAWINPUT); UINT size = sizeof(RAWINPUT);
if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawInput, &size, sizeof(RAWINPUTHEADER)) == size) { if (GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawInput, &size, sizeof(RAWINPUTHEADER)) == size) {
/* 鼠标事件,后面加上 /* 鼠标事件,后面加上
if (rawInput.header.dwType == RIM_TYPEMOUSE) if (rawInput.header.dwType == RIM_TYPEMOUSE)
{ {
switch (rawInput.data.mouse.ulButtons) { switch (rawInput.data.mouse.ulButtons) {
@ -373,7 +374,7 @@ namespace GakumasLocal::WinHooks {
ScreenToClient(hWnd, &pt); ScreenToClient(hWnd, &pt);
RECT rcClient; RECT rcClient;
GetClientRect(hWnd, &rcClient); GetClientRect(hWnd, &rcClient);
const int borderWidth = 8; // 根据需要调整边缘宽度 const int borderWidth = 8; // 根据需要调整边缘宽度
bool left = pt.x < borderWidth; bool left = pt.x < borderWidth;
bool right = pt.x >= rcClient.right - borderWidth; bool right = pt.x >= rcClient.right - borderWidth;
@ -396,10 +397,10 @@ namespace GakumasLocal::WinHooks {
{ {
if (GakumasLocal::Config::dmmUnlockSize) { if (GakumasLocal::Config::dmmUnlockSize) {
LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam; LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;
// 设置最大尺寸为屏幕分辨率,这样就不限制窗口的最大尺寸 // 设置最大尺寸为屏幕分辨率,这样就不限制窗口的最大尺寸
lpMMI->ptMaxTrackSize.x = GetSystemMetrics(SM_CXSCREEN) * 3; lpMMI->ptMaxTrackSize.x = GetSystemMetrics(SM_CXSCREEN) * 3;
lpMMI->ptMaxTrackSize.y = GetSystemMetrics(SM_CYSCREEN) * 3; lpMMI->ptMaxTrackSize.y = GetSystemMetrics(SM_CYSCREEN) * 3;
// 可选设置窗口最小尺寸例如200x200 // 可选设置窗口最小尺寸例如200x200
lpMMI->ptMinTrackSize.x = 200; lpMMI->ptMinTrackSize.x = 200;
lpMMI->ptMinTrackSize.y = 200; lpMMI->ptMinTrackSize.y = 200;
return 1; return 1;
@ -421,7 +422,7 @@ namespace GakumasLocal::WinHooks {
// printf("WM_NCPAINT: 0x%x\n", style); // printf("WM_NCPAINT: 0x%x\n", style);
if (!(style & WS_POPUP)) { if (!(style & WS_POPUP)) {
// 添加可调整大小的边框和最大化按钮 // 添加可调整大小的边框和最大化按钮
style |= WS_THICKFRAME | WS_MAXIMIZEBOX; style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
SetWindowLong(hWnd, GWL_STYLE, style); SetWindowLong(hWnd, GWL_STYLE, style);
} }
@ -439,7 +440,7 @@ namespace GakumasLocal::WinHooks {
auto hWnd = FindWindowW(L"UnityWndClass", L"gakumas"); auto hWnd = FindWindowW(L"UnityWndClass", L"gakumas");
g_pfnOldWndProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC); g_pfnOldWndProc = (WNDPROC)GetWindowLongPtr(hWnd, GWLP_WNDPROC);
SetWindowLongPtr(FindWindowW(L"UnityWndClass", L"gakumas"), GWLP_WNDPROC, (LONG_PTR)WndProcCallback); SetWindowLongPtr(FindWindowW(L"UnityWndClass", L"gakumas"), GWLP_WNDPROC, (LONG_PTR)WndProcCallback);
// 添加可调整大小的边框和最大化按钮 // 添加可调整大小的边框和最大化按钮
LONG style = GetWindowLong(hWnd, GWL_STYLE); LONG style = GetWindowLong(hWnd, GWL_STYLE);
style |= WS_THICKFRAME | WS_MAXIMIZEBOX; style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
SetWindowLong(hWnd, GWL_STYLE, style); SetWindowLong(hWnd, GWL_STYLE, style);