31 Commits

Author SHA1 Message Date
chinosk b9b1474414 Merge pull request '修复bug' (#5) from chihya72/gkms-local:main into main
Reviewed-on: https://git.chinosk6.cn/chinosk/gkms-local/pulls/5
2026-05-14 20:08:22 +08:00
pm chihya 786bc06db2 修复bug 2026-05-13 23:15:13 +08:00
chinosk 754fceb305 Merge pull request '分割hook.cpp文件' (#4) from chihya72/gkms-local:main into main
Reviewed-on: https://git.chinosk6.cn/chinosk/gkms-local/pulls/4
2026-05-13 14:44:45 +08:00
pm chihya b4955e94ee 分割hook.cpp文件 2026-05-10 16:25:38 +08:00
pm chihya 562b5850b5 修正贴图资源替换网址 2026-05-09 20:57:16 +08:00
pm chihya afcf1a3a7c 修改版本号 2026-05-09 20:55:31 +08:00
pm chihya 0200f20c73 添加贴图替换功能 2026-05-09 20:52:39 +08:00
pm chihya 36a49ba4df 添加贴图替换功能 2026-05-09 20:37:22 +08:00
chinosk c7af3e41a5 bump version 2026-03-19 17:36:35 +08:00
chinosk 87c4f366df Merge pull request '只替换普通字体、保留特殊数字' (#2) from chihya72/gkms-local:main into main
Reviewed-on: https://git.chinosk6.cn/chinosk/gkms-local/pulls/2
2026-03-19 16:31:29 +08:00
pm chihya a39b024b58 只替换普通字体、保留特殊数字
添加新genericTrans hook点
2026-03-13 16:48:43 +08:00
chinosk 3110cc7e49 Filtered unnecessary space. Updated LSPatch version. 2025-12-10 13:15:12 +08:00
chinosk b70376dcef bump version to 3.1.0 2025-06-06 03:11:20 +01:00
chinosk e859589125 fixed live 2025-04-27 15:50:38 +01:00
chinosk d09904643f fixed crash 2025-03-18 09:28:15 +00:00
chinosk d83854c755 Adapted for multi-platform compilation
Fixed URL input could only enter numbers
2025-03-17 22:26:52 +00:00
chinosk ade47131f9 add version consistency check 2025-01-22 16:03:10 +00:00
chinosk 0218543935 fixed LoginAsIOS not working in 1.8.0 2025-01-20 20:13:07 +00:00
chinosk 60c846b2f0 update README 2025-01-09 07:28:00 +00:00
chinosk 01591e61c0 Merge pull request #7 from imas-tools/feature-masterdb-localization
MasterDB Localization
2025-01-09 01:21:08 -06:00
chinosk 56c066bf42 bump version to v2.0.1 2025-01-09 06:54:23 +00:00
chinosk 35c2b9f489 MasterDB Localization 2025-01-05 22:36:12 +00:00
chinosk c50fdfd678 bump version to 1.6.8 2024-12-24 02:50:27 +00:00
chinosk 3c1d1f139a fix game crash 2024-12-24 02:49:30 +00:00
chinosk 6ac94178fa add ListEditor tool function 2024-12-24 02:33:56 +00:00
chinosk c27085772f bump version to 1.6.7 2024-12-24 00:52:55 +00:00
chinosk 361c48e2c9 fix: incomplete hooks 2024-12-24 00:52:11 +00:00
chinosk e9ba8b58fd bump version 2024-12-23 00:49:04 +00:00
chinosk e03736bd7d fix crash when using remote files 2024-12-23 00:44:10 +00:00
chinosk 06b552a097 Built-in patcher supports Android 15
Source: JingMatrix/LSPatch
2024-12-01 04:51:53 +00:00
chinosk bd9bcae01d Add login as ios.
Trim version string.
2024-12-01 03:49:58 +00:00
54 changed files with 5017 additions and 232 deletions
+2
View File
@@ -18,3 +18,5 @@ local.properties
/.kotlin
/app/debug
/app/release
app/src/main/assets/gakumas-local
+1 -10
View File
@@ -8,16 +8,7 @@
# Usage
- 这是一个 XPosed 插件,已 Root 用户可以使用 [LSPosed](https://github.com/LSPosed/LSPosed),未 Root 用户可以使用 [LSPatch](https://github.com/LSPosed/LSPatch)。
# TODO
- [x] 卡片信息、TIPS 等部分的文本 hook (`generic`)
- [ ] 更多类型的文件替换
- [x] LSPatch 集成模式无效
... and more
- 安卓 15 及以上的用户,请使用 [JingMatrix/LSPosed](https://github.com/JingMatrix/LSPosed) 或 [JingMatrix/LSPatch](https://github.com/JingMatrix/LSPatch)。因为原版已停止更新。
+3 -2
View File
@@ -15,8 +15,9 @@ android {
applicationId "io.github.chinosk.gakumas.localify"
minSdk 29
targetSdk 34
versionCode 4
versionName "v1.6.3"
versionCode 12
versionName "v3.3.1"
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Binary file not shown.
Binary file not shown.
+27 -27
View File
@@ -2,33 +2,33 @@ import os
logs = """
Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules listenablefuture-1.0.jar -> listenablefuture-1.0 (com.google.guava:listenablefuture:1.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.CanIgnoreReturnValue found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.CheckReturnValue found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.CompatibleWith found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.CompileTimeConstant found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.DoNotCall found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.DoNotMock found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.ForOverride found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.FormatMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.FormatString found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.Immutable found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.IncompatibleModifiers found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.InlineMe found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.InlineMeValidationDisabled found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.Keep found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.Modifier found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.MustBeClosed found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.NoAllocation found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.RequiredModifiers found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.RestrictedApi found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.SuppressPackageLocation found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.Var found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.concurrent.GuardedBy found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.concurrent.LazyInit found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.concurrent.LockMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.errorprone.annotations.concurrent.UnlockMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch.jar -> lspatch (lspatch.jar)
Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules listenablefuture-1.0.jar -> listenablefuture-1.0 (com.google.guava:listenablefuture:1.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.CanIgnoreReturnValue found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.CheckReturnValue found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.CompatibleWith found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.CompileTimeConstant found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.DoNotCall found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.DoNotMock found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.ForOverride found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.FormatMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.FormatString found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.Immutable found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.IncompatibleModifiers found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.InlineMe found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.InlineMeValidationDisabled found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.Keep found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.Modifier found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.MustBeClosed found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.NoAllocation found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.OverridingMethodsMustInvokeSuper found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.RequiredModifiers found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.RestrictedApi found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.SuppressPackageLocation found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.Var found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.concurrent.GuardedBy found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.concurrent.LazyInit found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.concurrent.LockMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
Duplicate class com.google.errorprone.annotations.concurrent.UnlockMethod found in modules error_prone_annotations-2.15.0.jar -> error_prone_annotations-2.15.0 (com.google.errorprone:error_prone_annotations:2.15.0) and lspatch_cleaned.jar -> lspatch_cleaned (lspatch_cleaned.jar)
"""
for i in logs.split("\n"):
Binary file not shown.
Binary file not shown.
+2
View File
@@ -39,9 +39,11 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
libMarryKotone.cpp
GakumasLocalify/Plugin.cpp
GakumasLocalify/Hook.cpp
GakumasLocalify/HookTexture.cpp
GakumasLocalify/Log.cpp
GakumasLocalify/Misc.cpp
GakumasLocalify/Local.cpp
GakumasLocalify/MasterLocal.cpp
GakumasLocalify/camera/baseCamera.cpp
GakumasLocalify/camera/camera.cpp
GakumasLocalify/config/Config.cpp
+54 -25
View File
@@ -1,29 +1,58 @@
#define KEY_W 51
#define KEY_S 47
#define KEY_A 29
#define KEY_D 32
#define KEY_R 46
#define KEY_Q 45
#define KEY_E 33
#define KEY_F 34
#define KEY_I 37
#define KEY_K 39
#define KEY_J 38
#define KEY_L 40
#define KEY_V 50
#define KEY_UP 19
#define KEY_DOWN 20
#define KEY_LEFT 21
#define KEY_RIGHT 22
#define KEY_CTRL 113
#define KEY_SHIFT 59
#define KEY_ALT 57
#define KEY_SPACE 62
#define KEY_ADD 70
#define KEY_SUB 69
#include "../platformDefine.hpp"
#define WM_KEYDOWN 0
#define WM_KEYUP 1
#ifndef GKMS_WINDOWS
#define KEY_W 51
#define KEY_S 47
#define KEY_A 29
#define KEY_D 32
#define KEY_R 46
#define KEY_Q 45
#define KEY_E 33
#define KEY_F 34
#define KEY_I 37
#define KEY_K 39
#define KEY_J 38
#define KEY_L 40
#define KEY_V 50
#define KEY_UP 19
#define KEY_DOWN 20
#define KEY_LEFT 21
#define KEY_RIGHT 22
#define KEY_CTRL 113
#define KEY_SHIFT 59
#define KEY_ALT 57
#define KEY_SPACE 62
#define KEY_ADD 70
#define KEY_SUB 69
#define WM_KEYDOWN 0
#define WM_KEYUP 1
#else
#define KEY_W 'W'
#define KEY_S 'S'
#define KEY_A 'A'
#define KEY_D 'D'
#define KEY_R 'R'
#define KEY_Q 'Q'
#define KEY_E 'E'
#define KEY_F 'F'
#define KEY_I 'I'
#define KEY_K 'K'
#define KEY_J 'J'
#define KEY_L 'L'
#define KEY_V 'V'
#define KEY_UP 38
#define KEY_DOWN 40
#define KEY_LEFT 37
#define KEY_RIGHT 39
#define KEY_CTRL 17
#define KEY_SHIFT 16
#define KEY_ALT 18
#define KEY_SPACE 32
#define KEY_ADD 187
#define KEY_SUB 189
#endif
#define BTN_A 96
#define BTN_B 97
File diff suppressed because it is too large Load Diff
@@ -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"});
}
}
@@ -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
+349 -20
View File
@@ -14,14 +14,96 @@ namespace Il2cppUtils {
const char* namespaze;
};
struct Il2CppObject
{
union
{
void* klass;
void* vtable;
};
void* monitor;
};
enum Il2CppTypeEnum
{
IL2CPP_TYPE_END = 0x00, /* End of List */
IL2CPP_TYPE_VOID = 0x01,
IL2CPP_TYPE_BOOLEAN = 0x02,
IL2CPP_TYPE_CHAR = 0x03,
IL2CPP_TYPE_I1 = 0x04,
IL2CPP_TYPE_U1 = 0x05,
IL2CPP_TYPE_I2 = 0x06,
IL2CPP_TYPE_U2 = 0x07,
IL2CPP_TYPE_I4 = 0x08,
IL2CPP_TYPE_U4 = 0x09,
IL2CPP_TYPE_I8 = 0x0a,
IL2CPP_TYPE_U8 = 0x0b,
IL2CPP_TYPE_R4 = 0x0c,
IL2CPP_TYPE_R8 = 0x0d,
IL2CPP_TYPE_STRING = 0x0e,
IL2CPP_TYPE_PTR = 0x0f,
IL2CPP_TYPE_BYREF = 0x10,
IL2CPP_TYPE_VALUETYPE = 0x11,
IL2CPP_TYPE_CLASS = 0x12,
IL2CPP_TYPE_VAR = 0x13,
IL2CPP_TYPE_ARRAY = 0x14,
IL2CPP_TYPE_GENERICINST = 0x15,
IL2CPP_TYPE_TYPEDBYREF = 0x16,
IL2CPP_TYPE_I = 0x18,
IL2CPP_TYPE_U = 0x19,
IL2CPP_TYPE_FNPTR = 0x1b,
IL2CPP_TYPE_OBJECT = 0x1c,
IL2CPP_TYPE_SZARRAY = 0x1d,
IL2CPP_TYPE_MVAR = 0x1e,
IL2CPP_TYPE_CMOD_REQD = 0x1f,
IL2CPP_TYPE_CMOD_OPT = 0x20,
IL2CPP_TYPE_INTERNAL = 0x21,
IL2CPP_TYPE_MODIFIER = 0x40,
IL2CPP_TYPE_SENTINEL = 0x41,
IL2CPP_TYPE_PINNED = 0x45,
IL2CPP_TYPE_ENUM = 0x55
};
typedef struct Il2CppType
{
void* dummy;
unsigned int attrs : 16;
Il2CppTypeEnum type : 8;
unsigned int num_mods : 6;
unsigned int byref : 1;
unsigned int pinned : 1;
} Il2CppType;
struct Il2CppReflectionType
{
Il2CppObject object;
const Il2CppType* type;
};
struct Resolution_t {
int width;
int height;
int herz;
};
struct FieldInfo {
const char* name;
const Il2CppType* type;
uintptr_t parent;
int32_t offset;
uint32_t token;
};
struct MethodInfo {
uintptr_t methodPointer;
uintptr_t invoker_method;
const char* name;
uintptr_t klass;
//const Il2CppType* return_type;
const Il2CppType* return_type;
//const ParameterInfo* parameters;
const void* return_type;
// const void* return_type;
const void* parameters;
uintptr_t methodDefinition;
uintptr_t genericContainer;
@@ -36,13 +118,7 @@ namespace Il2cppUtils {
uint8_t is_marshaled_from_native : 1;
};
struct Resolution_t {
int width;
int height;
int herz;
};
UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName,
static UnityResolve::Class* GetClass(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className) {
const auto assembly = UnityResolve::Get(assemblyName);
if (!assembly) {
@@ -81,7 +157,7 @@ namespace Il2cppUtils {
return ret;
}*/
UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName,
static UnityResolve::Method* GetMethod(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
const auto assembly = UnityResolve::Get(assemblyName);
if (!assembly) {
@@ -108,7 +184,7 @@ namespace Il2cppUtils {
return method;
}
void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName,
static void* GetMethodPointer(const std::string& assemblyName, const std::string& nameSpaceName,
const std::string& className, const std::string& methodName, const std::vector<std::string>& args = {}) {
auto method = GetMethod(assemblyName, nameSpaceName, className, methodName, args);
if (method) {
@@ -117,20 +193,28 @@ namespace Il2cppUtils {
return nullptr;
}
void* il2cpp_resolve_icall(const char* s) {
static void* il2cpp_resolve_icall(const char* s) {
return UnityResolve::Invoke<void*>("il2cpp_resolve_icall", s);
}
Il2CppClassHead* get_class_from_instance(const void* instance) {
static Il2CppClassHead* get_class_from_instance(const void* instance) {
return static_cast<Il2CppClassHead*>(*static_cast<void* const*>(std::assume_aligned<alignof(void*)>(instance)));
}
MethodInfo* il2cpp_class_get_method_from_name(void* klass, const char* name, int argsCount) {
static MethodInfo* il2cpp_class_get_method_from_name(void* klass, const char* name, int argsCount) {
return UnityResolve::Invoke<MethodInfo*>("il2cpp_class_get_method_from_name", klass, name, argsCount);
}
void* find_nested_class(void* klass, std::predicate<void*> auto&& predicate)
{
static uintptr_t il2cpp_class_get_method_pointer_from_name(void* klass, const char* name, int argsCount) {
auto findKlass = il2cpp_class_get_method_from_name(klass, name, argsCount);
if (findKlass) {
return findKlass->methodPointer;
}
Log::ErrorFmt("method: %s not found", name);
return 0;
}
static void* find_nested_class(void* klass, std::predicate<void*> auto&& predicate) {
void* iter{};
while (const auto curNestedClass = UnityResolve::Invoke<void*>("il2cpp_class_get_nested_types", klass, &iter))
{
@@ -143,22 +227,267 @@ namespace Il2cppUtils {
return nullptr;
}
void* find_nested_class_from_name(void* klass, const char* name)
{
static void* find_nested_class_from_name(void* klass, const char* name) {
return find_nested_class(klass, [name = std::string_view(name)](void* nestedClass) {
return static_cast<Il2CppClassHead*>(nestedClass)->name == name;
});
}
template <typename RType>
auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType {
static auto ClassGetFieldValue(void* obj, UnityResolve::Field* field) -> RType {
return *reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset);
}
template <typename RType>
auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, RType value) -> void {
static auto ClassGetFieldValue(void* obj, FieldInfo* field) -> RType {
return *reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset);
}
template <typename T>
static auto ClassSetFieldValue(void* obj, UnityResolve::Field* field, T value) -> void {
const auto fieldPtr = static_cast<std::byte*>(obj) + field->offset;
std::memcpy(fieldPtr, std::addressof(value), sizeof(T));
}
template <typename RType>
static auto ClassSetFieldValue(void* obj, FieldInfo* field, RType value) -> void {
*reinterpret_cast<RType*>(reinterpret_cast<uintptr_t>(obj) + field->offset) = value;
}
static void* get_system_class_from_reflection_type_str(const char* typeStr, const char* assemblyName = "mscorlib") {
using Il2CppString = UnityResolve::UnityType::String;
static auto assemblyLoad = reinterpret_cast<void* (*)(Il2CppString*)>(
GetMethodPointer("mscorlib.dll", "System.Reflection",
"Assembly", "Load", {"*"})
);
static auto assemblyGetType = reinterpret_cast<Il2CppReflectionType * (*)(void*, Il2CppString*)>(
GetMethodPointer("mscorlib.dll", "System.Reflection",
"Assembly", "GetType", {"*"})
);
static auto reflectionAssembly = assemblyLoad(Il2CppString::New(assemblyName));
auto reflectionType = assemblyGetType(reflectionAssembly, Il2CppString::New(typeStr));
return UnityResolve::Invoke<void*>("il2cpp_class_from_system_type", reflectionType);
}
static std::unordered_map<std::string, std::unordered_map<int, std::string>> enumToValueMapCache{};
static std::unordered_map<int, std::string> EnumToValueMap(Il2CppClassHead* enumClass, bool useCache) {
std::unordered_map<int, std::string> ret{};
auto isEnum = UnityResolve::Invoke<bool>("il2cpp_class_is_enum", enumClass);
if (isEnum) {
Il2cppUtils::FieldInfo* field = nullptr;
void* iter = nullptr;
std::string cacheName = std::string(enumClass->namespaze) + "::" + enumClass->name;
if (useCache) {
if (auto it = enumToValueMapCache.find(cacheName); it != enumToValueMapCache.end()) {
return it->second;
}
}
while ((field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>("il2cpp_class_get_fields", enumClass, &iter))) {
// Log::DebugFmt("field: %s, off: %d", field->name, field->offset);
if (field->offset > 0) continue; // 非 static
if (strcmp(field->name, "value__") == 0) {
continue;
}
int value;
UnityResolve::Invoke<void>("il2cpp_field_static_get_value", field, &value);
// Log::DebugFmt("returnClass: %s - %s: 0x%x", enumClass->name, field->name, value);
std::string itemName = std::string(enumClass->name) + "_" + field->name;
ret.emplace(value, std::move(itemName));
}
if (useCache) {
enumToValueMapCache.emplace(std::move(cacheName), ret);
}
}
return ret;
}
template <typename T = void*>
static void iterate_IEnumerable(const void* obj, std::invocable<T> auto&& receiver)
{
const auto klass = get_class_from_instance(obj);
const auto getEnumeratorMethod = reinterpret_cast<void* (*)(const void*)>(il2cpp_class_get_method_from_name(klass, "GetEnumerator", 0)->methodPointer);
const auto enumerator = getEnumeratorMethod(obj);
const auto enumeratorClass = get_class_from_instance(enumerator);
const auto getCurrentMethod = reinterpret_cast<T(*)(void*)>(il2cpp_class_get_method_from_name(enumeratorClass, "get_Current", 0)->methodPointer);
const auto moveNextMethod = reinterpret_cast<bool(*)(void*)>(il2cpp_class_get_method_from_name(enumeratorClass, "MoveNext", 0)->methodPointer);
while (moveNextMethod(enumerator))
{
static_cast<decltype(receiver)>(receiver)(getCurrentMethod(enumerator));
}
}
namespace Tools {
template <typename T = void*>
class CSListEditor {
public:
CSListEditor(void* list) {
list_klass = get_class_from_instance(list);
lst = list;
lst_get_Count_method = il2cpp_class_get_method_from_name(list_klass, "get_Count", 0);
lst_get_Item_method = il2cpp_class_get_method_from_name(list_klass, "get_Item", 1);
lst_set_Item_method = il2cpp_class_get_method_from_name(list_klass, "set_Item", 2);
lst_Add_method = il2cpp_class_get_method_from_name(list_klass, "Add", 1);
lst_Contains_method = il2cpp_class_get_method_from_name(list_klass, "Contains", 1);
lst_get_Count = reinterpret_cast<lst_get_Count_t>(lst_get_Count_method->methodPointer);
lst_get_Item = reinterpret_cast<lst_get_Item_t>(lst_get_Item_method->methodPointer);
lst_set_Item = reinterpret_cast<lst_set_Item_t>(lst_set_Item_method->methodPointer);
lst_Add = reinterpret_cast<lst_Add_t>(lst_Add_method->methodPointer);
lst_Contains = reinterpret_cast<lst_Contains_t>(lst_Contains_method->methodPointer);
}
void Add(T value) {
lst_Add(lst, value, lst_Add_method);
}
bool Contains(T value) {
return lst_Contains(lst, value, lst_Contains_method);
}
T get_Item(int index) {
return lst_get_Item(lst, index, lst_get_Item_method);
}
void set_Item(int index, T value) {
return lst_set_Item(lst, index, value, lst_set_Item_method);
}
int get_Count() {
return lst_get_Count(lst, lst_get_Count_method);
}
T operator[] (int key) {
return get_Item(key);
}
class Iterator {
public:
Iterator(CSListEditor<T>* editor, int index) : editor(editor), index(index) {}
T operator*() const {
return editor->get_Item(index);
}
Iterator& operator++() {
++index;
return *this;
}
bool operator!=(const Iterator& other) const {
return index != other.index;
}
private:
CSListEditor<T>* editor;
int index;
};
Iterator begin() {
return Iterator(this, 0);
}
Iterator end() {
return Iterator(this, get_Count());
}
void* lst;
void* list_klass;
private:
typedef T(*lst_get_Item_t)(void*, int, void* mtd);
typedef void(*lst_Add_t)(void*, T, void* mtd);
typedef void(*lst_set_Item_t)(void*, int, T, void* mtd);
typedef int(*lst_get_Count_t)(void*, void* mtd);
typedef bool(*lst_Contains_t)(void*, T, void* mtd);
MethodInfo* lst_get_Item_method;
MethodInfo* lst_Add_method;
MethodInfo* lst_get_Count_method;
MethodInfo* lst_set_Item_method;
MethodInfo* lst_Contains_method;
lst_get_Item_t lst_get_Item;
lst_set_Item_t lst_set_Item;
lst_Add_t lst_Add;
lst_get_Count_t lst_get_Count;
lst_Contains_t lst_Contains;
};
template <typename KT = void*, typename VT = void*>
class CSDictEditor {
public:
// @param dict: Dictionary instance.
// @param dictTypeStr: Reflection type. eg: "System.Collections.Generic.Dictionary`2[System.Int32, System.Int32]"
CSDictEditor(void* dict, const char* dictTypeStr) {
dic_klass = Il2cppUtils::get_system_class_from_reflection_type_str(dictTypeStr);
initDict(dict);
}
CSDictEditor(void* dict) {
dic_klass = get_class_from_instance(dict);
initDict(dict);
}
CSDictEditor(void* dict, void* dicClass) {
dic_klass = dicClass;
initDict(dict);
}
void Add(KT key, VT value) {
dic_Add(dict, key, value, Add_method);
}
bool ContainsKey(KT key) {
return dic_containsKey(dict, key, ContainsKey_method);
}
VT get_Item(KT key) {
return dic_get_Item(dict, key, get_Item_method);
}
VT operator[] (KT key) {
return get_Item(key);
}
void* dict;
void* dic_klass;
private:
void initDict(void* dict) {
// dic_klass = dicClass;
this->dict = dict;
get_Item_method = il2cpp_class_get_method_from_name(dic_klass, "get_Item", 1);
Add_method = il2cpp_class_get_method_from_name(dic_klass, "Add", 2);
ContainsKey_method = il2cpp_class_get_method_from_name(dic_klass, "ContainsKey", 1);
dic_get_Item = (dic_get_Item_t)get_Item_method->methodPointer;
dic_Add = (dic_Add_t)Add_method->methodPointer;
dic_containsKey = (dic_containsKey_t)ContainsKey_method->methodPointer;
}
typedef VT(*dic_get_Item_t)(void*, KT, void* mtd);
typedef VT(*dic_Add_t)(void*, KT, VT, void* mtd);
typedef VT(*dic_containsKey_t)(void*, KT, void* mtd);
CSDictEditor();
MethodInfo* get_Item_method;
MethodInfo* Add_method;
MethodInfo* ContainsKey_method;
dic_get_Item_t dic_get_Item;
dic_Add_t dic_Add;
dic_containsKey_t dic_containsKey;
};
}
}
+21 -4
View File
@@ -17,6 +17,8 @@
#include "BaseDefine.h"
#include "string_parser/StringParser.hpp"
// #include "cpprest/details/http_helpers.h"
namespace GakumasLocal::Local {
std::unordered_map<std::string, std::string> i18nData{};
@@ -37,6 +39,12 @@ namespace GakumasLocal::Local {
return Plugin::GetInstance().GetHookInstaller()->localizationFilesDir;
}
bool isAllSpace(const std::string& str) {
return std::all_of(str.begin(), str.end(), [](unsigned char c) {
return std::isspace(c);
});
}
std::string trim(const std::string& str) {
auto is_not_space = [](char ch) { return !std::isspace(ch); };
auto start = std::ranges::find_if(str, is_not_space);
@@ -90,7 +98,7 @@ namespace GakumasLocal::Local {
}
std::ifstream file(filePath);
if (!file.is_open()) {
Log::ErrorFmt("Load %s failed.\n", filePath.c_str());
Log::ErrorFmt("Load %s failed.\n", filePath.string().c_str());
return;
}
std::string fileContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
@@ -112,7 +120,7 @@ namespace GakumasLocal::Local {
}
}
catch (std::exception& e) {
Log::ErrorFmt("Load %s failed: %s\n", filePath.c_str(), e.what());
Log::ErrorFmt("Load %s failed: %s\n", filePath.string().c_str(), e.what());
}
}
@@ -249,7 +257,7 @@ namespace GakumasLocal::Local {
}
bool GetSplitTagsTranslation(const std::string& origText, std::string* newText, std::vector<std::string>& unTransResultRet) {
if (!origText.contains(L'<')) return false;
if (!origText.contains('<')) return false;
const auto splitResult = SplitByTags(origText);
if (splitResult.empty()) return false;
@@ -289,10 +297,18 @@ namespace GakumasLocal::Local {
std::u16string currentWaitingReplaceText;
#ifdef GKMS_WINDOWS
#define checkCurrentWaitingReplaceTextAndClear() \
if (!currentWaitingReplaceText.empty()) { \
auto trimmed = trim(Misc::ToUTF8(currentWaitingReplaceText)); \
waitingReplaceTexts.push_back(trimmed); \
currentWaitingReplaceText.clear(); }
#else
#define checkCurrentWaitingReplaceTextAndClear() \
if (!currentWaitingReplaceText.empty()) { \
waitingReplaceTexts.push_back(Misc::ToUTF8(currentWaitingReplaceText)); \
currentWaitingReplaceText.clear(); }
#endif
for (char16_t currChar : origText) {
if (currChar == u'<') {
@@ -333,6 +349,7 @@ namespace GakumasLocal::Local {
bool hasNotTrans = false;
if (!waitingReplaceTexts.empty()) {
for (const auto& i : waitingReplaceTexts) {
if (isAllSpace(i)) continue;
std::string searchResult = findInMapIgnoreSpace(i, genericSplitText);
if (!searchResult.empty()) {
ReplaceNumberComma(&searchResult);
@@ -447,7 +464,7 @@ namespace GakumasLocal::Local {
const auto targetFilePath = basePath / "local-files" / "resource" / name;
// Log::DebugFmt("GetResourceText: %s", targetFilePath.c_str());
if (exists(targetFilePath)) {
auto readStr = readFileToString(targetFilePath);
auto readStr = readFileToString(targetFilePath.string());
*ret = readStr;
return true;
}
+3
View File
@@ -3,8 +3,11 @@
#include <string>
#include <filesystem>
#include <unordered_set>
namespace GakumasLocal::Local {
extern std::unordered_set<std::string> translatedText;
std::filesystem::path GetBasePath();
void LoadData();
bool GetI18n(const std::string& key, std::string* ret);
+20 -7
View File
@@ -1,14 +1,19 @@
#include "Log.h"
#include <android/log.h>
#include <Misc.hpp>
#include "Misc.hpp"
#include <sstream>
#include <string>
#include <thread>
#include <queue>
#include <cstdarg>
#ifndef GKMS_WINDOWS
#include <android/log.h>
extern JavaVM* g_javaVM;
extern jclass g_gakumasHookMainClass;
extern jmethodID showToastMethodId;
#endif // GKMS_WINDOWS
extern JavaVM* g_javaVM;
extern jclass g_gakumasHookMainClass;
extern jmethodID showToastMethodId;
#define GetParamStringResult(name)\
va_list args;\
@@ -75,6 +80,7 @@ namespace GakumasLocal::Log {
__android_log_write(prio, "GakumasLog", result.c_str());
}
/*
void ShowToastJNI(const char* text) {
DebugFmt("Toast: %s", text);
@@ -99,15 +105,19 @@ namespace GakumasLocal::Log {
g_javaVM->DetachCurrentThread();
}).detach();
}
}*/
void ShowToast(const std::string& text) {
#ifndef GKMS_WINDOWS
showingToasts.push(text);
#else
InfoFmt("Toast: %s", text.c_str());
#endif
}
void ShowToast(const char* text) {
DebugFmt("Toast: %s", text);
// DebugFmt("Toast: %s", text);
return ShowToast(std::string(text));
}
@@ -125,6 +135,7 @@ namespace GakumasLocal::Log {
return ret;
}
#ifndef GKMS_WINDOWS
void ToastLoop(JNIEnv *env, jclass clazz) {
const auto toastString = GetQueuedToast();
if (toastString.empty()) return;
@@ -140,4 +151,6 @@ namespace GakumasLocal::Log {
_showToastMethodId = env->GetStaticMethodID(clazz, "showToast", "(Ljava/lang/String;)V");
}
}
#endif
}
+9 -1
View File
@@ -1,8 +1,14 @@
#ifndef GAKUMAS_LOCALIFY_LOG_H
#define GAKUMAS_LOCALIFY_LOG_H
#include "../platformDefine.hpp"
#include <string>
#include <jni.h>
#ifndef GKMS_WINDOWS
#include <jni.h>
#endif
namespace GakumasLocal::Log {
std::string StringFormat(const char* fmt, ...);
@@ -18,7 +24,9 @@ namespace GakumasLocal::Log {
void ShowToast(const char* text);
void ShowToastFmt(const char* fmt, ...);
#ifndef GKMS_WINDOWS
void ToastLoop(JNIEnv *env, jclass clazz);
#endif
}
#endif //GAKUMAS_LOCALIFY_LOG_H
@@ -0,0 +1,809 @@
#include "MasterLocal.h"
#include "Local.h"
#include "Il2cppUtils.hpp"
#include "config/Config.hpp"
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <regex>
#include <nlohmann/json.hpp>
namespace GakumasLocal::MasterLocal {
using Il2cppString = UnityResolve::UnityType::String;
static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> fieldSetCache;
static std::unordered_map<std::string, Il2cppUtils::MethodInfo*> fieldGetCache;
enum class JsonValueType {
JVT_String,
JVT_Int,
JVT_Object,
JVT_ArrayObject,
JVT_ArrayString,
JVT_Unsupported,
JVT_NeedMore_EmptyArray
};
struct ItemRule {
std::vector<std::string> mainPrimaryKey;
std::map<std::string, std::vector<std::string>> subPrimaryKey;
std::vector<std::string> mainLocalKey;
std::map<std::string, std::vector<std::string>> subLocalKey;
};
struct TableLocalData {
ItemRule itemRule;
std::unordered_map<std::string, JsonValueType> mainKeyType;
std::unordered_map<std::string, std::unordered_map<std::string, JsonValueType>> subKeyType;
std::unordered_map<std::string, std::string> transData;
std::unordered_map<std::string, std::vector<std::string>> transStrListData;
[[nodiscard]] JsonValueType GetMainKeyType(const std::string& mainKey) const {
if (auto it = mainKeyType.find(mainKey); it != mainKeyType.end()) {
return it->second;
}
return JsonValueType::JVT_Unsupported;
}
[[nodiscard]] JsonValueType GetSubKeyType(const std::string& parentKey, const std::string& subKey) const {
if (auto it = subKeyType.find(parentKey); it != subKeyType.end()) {
if (auto subIt = it->second.find(subKey); subIt != it->second.end()) {
return subIt->second;
}
}
return JsonValueType::JVT_Unsupported;
}
};
static std::unordered_map<std::string, TableLocalData> masterLocalData;
class FieldController {
void* self;
std::string self_klass_name;
static std::string capitalizeFirstLetter(const std::string& input) {
if (input.empty()) return input;
std::string result = input;
result[0] = static_cast<char>(std::toupper(result[0]));
return result;
}
Il2cppUtils::MethodInfo* GetGetSetMethodFromCache(const std::string& fieldName, int argsCount,
std::unordered_map<std::string, Il2cppUtils::MethodInfo*>& fromCache, const std::string& prefix = "set_") {
const std::string methodName = prefix + capitalizeFirstLetter(fieldName);
const std::string searchName = self_klass_name + "." + methodName;
if (auto it = fromCache.find(searchName); it != fromCache.end()) {
return it->second;
}
auto set_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(
self_klass,
methodName.c_str(),
argsCount
);
fromCache.emplace(searchName, set_mtd);
return set_mtd;
}
public:
Il2cppUtils::Il2CppClassHead* self_klass;
explicit FieldController(void* from) {
if (!from) {
self = nullptr;
return;
}
self = from;
self_klass = Il2cppUtils::get_class_from_instance(self);
if (self_klass) {
self_klass_name = self_klass->name;
}
}
template<typename T>
T ReadField(const std::string& fieldName) {
if (!self) return T();
auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_");
if (get_mtd) {
return reinterpret_cast<T (*)(void*, void*)>(get_mtd->methodPointer)(self, get_mtd);
}
auto field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>(
"il2cpp_class_get_field_from_name",
self_klass,
(fieldName + '_').c_str()
);
if (!field) {
return T();
}
return Il2cppUtils::ClassGetFieldValue<T>(self, field);
}
template<typename T>
void SetField(const std::string& fieldName, T value) {
if (!self) return;
auto set_mtd = GetGetSetMethodFromCache(fieldName, 1, fieldSetCache, "set_");
if (set_mtd) {
reinterpret_cast<void (*)(void*, T, void*)>(
set_mtd->methodPointer
)(self, value, set_mtd);
return;
}
auto field = UnityResolve::Invoke<Il2cppUtils::FieldInfo*>(
"il2cpp_class_get_field_from_name",
self_klass,
(fieldName + '_').c_str()
);
if (!field) return;
Il2cppUtils::ClassSetFieldValue(self, field, value);
}
int ReadIntField(const std::string& fieldName) {
return ReadField<int>(fieldName);
}
Il2cppString* ReadStringField(const std::string& fieldName) {
if (!self) return nullptr;
auto get_mtd = GetGetSetMethodFromCache(fieldName, 0, fieldGetCache, "get_");
if (!get_mtd) {
return ReadField<Il2cppString*>(fieldName);
}
auto returnClass = UnityResolve::Invoke<Il2cppUtils::Il2CppClassHead*>(
"il2cpp_class_from_type",
UnityResolve::Invoke<void*>("il2cpp_method_get_return_type", get_mtd)
);
if (!returnClass) {
return reinterpret_cast<Il2cppString* (*)(void*, void*)>(
get_mtd->methodPointer
)(self, get_mtd);
}
auto isEnum = UnityResolve::Invoke<bool>("il2cpp_class_is_enum", returnClass);
if (!isEnum) {
return reinterpret_cast<Il2cppString* (*)(void*, void*)>(
get_mtd->methodPointer
)(self, get_mtd);
}
auto enumMap = Il2cppUtils::EnumToValueMap(returnClass, true);
auto enumValue = reinterpret_cast<int (*)(void*, void*)>(
get_mtd->methodPointer
)(self, get_mtd);
if (auto it = enumMap.find(enumValue); it != enumMap.end()) {
return Il2cppString::New(it->second);
}
return nullptr;
}
void SetStringField(const std::string& fieldName, const std::string& value) {
if (!self) return;
auto newString = Il2cppString::New(value);
SetField(fieldName, newString);
}
void SetStringListField(const std::string& fieldName, const std::vector<std::string>& data) {
if (!self) return;
static auto List_String_klass = Il2cppUtils::get_system_class_from_reflection_type_str(
"System.Collections.Generic.List`1[System.String]"
);
static auto List_String_ctor_mtd = Il2cppUtils::il2cpp_class_get_method_from_name(
List_String_klass, ".ctor", 0
);
static auto List_String_ctor = reinterpret_cast<void (*)(void*, void*)>(
List_String_ctor_mtd->methodPointer
);
auto newList = UnityResolve::Invoke<void*>("il2cpp_object_new", List_String_klass);
List_String_ctor(newList, List_String_ctor_mtd);
Il2cppUtils::Tools::CSListEditor<Il2cppString*> newListEditor(newList);
for (auto& s : data) {
newListEditor.Add(Il2cppString::New(s));
}
SetField(fieldName, newList);
}
void* ReadObjectField(const std::string& fieldName) {
if (!self) return nullptr;
return ReadField<void*>(fieldName);
}
void* ReadObjectListField(const std::string& fieldName) {
if (!self) return nullptr;
return ReadField<void*>(fieldName);
}
static FieldController CreateSubFieldController(void* subObj) {
return FieldController(subObj);
}
FieldController CreateSubFieldController(const std::string& subObjName) {
auto field = ReadObjectField(subObjName);
return FieldController(field);
}
};
JsonValueType checkJsonValueType(const nlohmann::json& j) {
if (j.is_string()) return JsonValueType::JVT_String;
if (j.is_number_integer()) return JsonValueType::JVT_Int;
if (j.is_object()) return JsonValueType::JVT_Object;
if (j.is_array()) {
if (!j.empty()) {
if (j.begin()->is_object()) {
return JsonValueType::JVT_ArrayObject;
}
else if (j.begin()->is_string()) {
return JsonValueType::JVT_ArrayString;
}
}
else {
return JsonValueType::JVT_NeedMore_EmptyArray;
}
}
return JsonValueType::JVT_Unsupported;
}
std::string ReadFileToString(const std::filesystem::path& path) {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) return {};
std::stringstream buffer;
buffer << ifs.rdbuf();
return buffer.str();
}
namespace Load {
std::vector<std::string> ArrayStrJsonToVec(nlohmann::json& data) {
return data;
}
bool BuildObjectItemLocalRule(nlohmann::json& transData, ItemRule& itemRule) {
// transData: data[]
bool hasSuccess = false;
for (auto& data : transData) {
// data: {"id": "xxx", "produceDescriptions": [{"k", "v"}], "descriptions": {"k2", "v2"}}
if (!data.is_object()) continue;
for (auto& [key, value] : data.items()) {
// key: "id", value: "xxx"
// key: "produceDescriptions", value: [{"k", "v"}]
const auto valueType = checkJsonValueType(value);
switch (valueType) {
case JsonValueType::JVT_String:
// case JsonValueType::JVT_Int:
case JsonValueType::JVT_ArrayString: {
if (std::find(itemRule.mainPrimaryKey.begin(), itemRule.mainPrimaryKey.end(), key) != itemRule.mainPrimaryKey.end()) {
continue;
}
if (auto it = std::find(itemRule.mainLocalKey.begin(), itemRule.mainLocalKey.end(), key); it == itemRule.mainLocalKey.end()) {
itemRule.mainLocalKey.emplace_back(key);
}
hasSuccess = true;
} break;
case JsonValueType::JVT_Object: {
ItemRule currRule{ .mainPrimaryKey = itemRule.subPrimaryKey[key] };
auto vJson = nlohmann::json::array();
vJson.push_back(value);
if (BuildObjectItemLocalRule(vJson, currRule)) {
itemRule.subLocalKey.emplace(key, currRule.mainLocalKey);
hasSuccess = true;
}
} break;
case JsonValueType::JVT_ArrayObject: {
for (auto& obj : value) {
// obj: {"k", "v"}
ItemRule currRule{ .mainPrimaryKey = itemRule.subPrimaryKey[key] };
if (BuildObjectItemLocalRule(value, currRule)) {
itemRule.subLocalKey.emplace(key, currRule.mainLocalKey);
hasSuccess = true;
break;
}
}
} break;
case JsonValueType::JVT_Unsupported:
default:
break;
}
}
if (hasSuccess) break;
}
return hasSuccess;
}
bool GetItemRule(nlohmann::json& fullData, ItemRule& itemRule) {
auto& primaryKeys = fullData["rules"]["primaryKeys"];
auto& transData = fullData["data"];
if (!primaryKeys.is_array()) return false;
if (!transData.is_array()) return false;
// 首先构造 mainPrimaryKey 规则
for (auto& pkItem : primaryKeys) {
if (!pkItem.is_string()) {
return false;
}
std::string pk = pkItem;
auto dotCount = std::ranges::count(pk, '.');
if (dotCount == 0) {
itemRule.mainPrimaryKey.emplace_back(pk);
}
else if (dotCount == 1) {
auto [parentKey, subKey] = Misc::StringFormat::split_once(pk, ".");
if (itemRule.subPrimaryKey.contains(parentKey)) {
itemRule.subPrimaryKey[parentKey].emplace_back(subKey);
}
else {
itemRule.subPrimaryKey.emplace(parentKey, std::vector<std::string>{subKey});
}
}
else {
Log::ErrorFmt("Unsupported depth: %d", dotCount);
continue;
}
}
return BuildObjectItemLocalRule(transData, itemRule);
}
std::string BuildBaseMainUniqueKey(nlohmann::json& data, TableLocalData& tableLocalData) {
try {
std::string mainBaseUniqueKey;
for (auto& mainPrimaryKey : tableLocalData.itemRule.mainPrimaryKey) {
if (!data.contains(mainPrimaryKey)) {
return "";
}
auto& value = data[mainPrimaryKey];
if (value.is_number_integer()) {
mainBaseUniqueKey.append(std::to_string(value.get<int>()));
}
else {
mainBaseUniqueKey.append(value);
}
mainBaseUniqueKey.push_back('|');
}
return mainBaseUniqueKey;
}
catch (std::exception& e) {
Log::ErrorFmt("LoadData - BuildBaseMainUniqueKey failed: %s", e.what());
throw e;
}
}
void BuildBaseObjectSubUniqueKey(nlohmann::json& value, JsonValueType valueType, std::string& currLocalKey) {
switch (valueType) {
case JsonValueType::JVT_String:
currLocalKey.append(value.get<std::string>()); // p_card-00-acc-0_002|0|produceDescriptions|ProduceDescriptionType_Exam|
currLocalKey.push_back('|');
break;
case JsonValueType::JVT_Int:
currLocalKey.append(std::to_string(value.get<int>()));
currLocalKey.push_back('|');
break;
default:
break;
}
}
bool BuildUniqueKeyValue(nlohmann::json& data, TableLocalData& tableLocalData) {
// 首先处理 main 部分
const std::string mainBaseUniqueKey = BuildBaseMainUniqueKey(data, tableLocalData); // p_card-00-acc-0_002|0|
if (mainBaseUniqueKey.empty()) return false;
for (auto& mainLocalKey : tableLocalData.itemRule.mainLocalKey) {
if (!data.contains(mainLocalKey)) continue;
auto& currLocalValue = data[mainLocalKey];
auto currUniqueKey = mainBaseUniqueKey + mainLocalKey; // p_card-00-acc-0_002|0|name
if (tableLocalData.GetMainKeyType(mainLocalKey) == JsonValueType::JVT_ArrayString) {
tableLocalData.transStrListData.emplace(currUniqueKey, ArrayStrJsonToVec(currLocalValue));
}
else {
tableLocalData.transData.emplace(currUniqueKey, currLocalValue);
}
}
// 然后处理 sub 部分
/*
for (const auto& [subPrimaryParentKey, subPrimarySubKeys] : tableLocalData.itemRule.subPrimaryKey) {
if (!data.contains(subPrimaryParentKey)) continue;
const std::string subBaseUniqueKey = mainBaseUniqueKey + subPrimaryParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions|
auto subValueType = checkJsonValueType(data[subPrimaryParentKey]);
std::string currLocalKey = subBaseUniqueKey; // p_card-00-acc-0_002|0|produceDescriptions|
switch (subValueType) {
case JsonValueType::JVT_Object: {
for (auto& subPrimarySubKey : subPrimarySubKeys) {
if (!data[subPrimaryParentKey].contains(subPrimarySubKey)) continue;
auto& value = data[subPrimaryParentKey][subPrimarySubKey];
auto valueType = tableLocalData.GetSubKeyType(subPrimaryParentKey, subPrimarySubKey);
BuildBaseObjectSubUniqueKey(value, valueType, currLocalKey); // p_card-00-acc-0_002|0|produceDescriptions|ProduceDescriptionType_Exam|
}
} break;
case JsonValueType::JVT_ArrayObject: {
int currIndex = 0;
for (auto& obj : data[subPrimaryParentKey]) {
for (auto& subPrimarySubKey : subPrimarySubKeys) {
}
currIndex++;
}
} break;
default:
break;
}
}*/
for (const auto& [subLocalParentKey, subLocalSubKeys] : tableLocalData.itemRule.subLocalKey) {
if (!data.contains(subLocalParentKey)) continue;
const std::string subBaseUniqueKey = mainBaseUniqueKey + subLocalParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions|
auto subValueType = checkJsonValueType(data[subLocalParentKey]);
if (subValueType != JsonValueType::JVT_NeedMore_EmptyArray) {
tableLocalData.mainKeyType.emplace(subLocalParentKey, subValueType); // 在这里插入 subParent 的类型
}
switch (subValueType) {
case JsonValueType::JVT_Object: {
for (auto& localSubKey : subLocalSubKeys) {
const std::string currLocalUniqueKey = subBaseUniqueKey + localSubKey; // p_card-00-acc-0_002|0|produceDescriptions|text
if (tableLocalData.GetSubKeyType(subLocalParentKey, localSubKey) == JsonValueType::JVT_ArrayString) {
tableLocalData.transStrListData.emplace(currLocalUniqueKey, ArrayStrJsonToVec(data[subLocalParentKey][localSubKey]));
}
else {
tableLocalData.transData.emplace(currLocalUniqueKey, data[subLocalParentKey][localSubKey]);
}
}
} break;
case JsonValueType::JVT_ArrayObject: {
int currIndex = 0;
for (auto& obj : data[subLocalParentKey]) {
for (auto& localSubKey : subLocalSubKeys) {
std::string currLocalUniqueKey = subBaseUniqueKey; // p_card-00-acc-0_002|0|produceDescriptions|
currLocalUniqueKey.push_back('[');
currLocalUniqueKey.append(std::to_string(currIndex));
currLocalUniqueKey.append("]|");
currLocalUniqueKey.append(localSubKey); // p_card-00-acc-0_002|0|produceDescriptions|[0]|text
if (tableLocalData.GetSubKeyType(subLocalParentKey, localSubKey) == JsonValueType::JVT_ArrayString) {
// if (obj[localSubKey].is_array()) {
tableLocalData.transStrListData.emplace(currLocalUniqueKey, ArrayStrJsonToVec(obj[localSubKey]));
}
else if (obj[localSubKey].is_string()) {
tableLocalData.transData.emplace(currLocalUniqueKey, obj[localSubKey]);
}
}
currIndex++;
}
} break;
default:
break;
}
}
return true;
}
#define MainKeyTypeProcess() if (!data.contains(mainPrimaryKey)) { Log::ErrorFmt("mainPrimaryKey: %s not found", mainPrimaryKey.c_str()); isFailed = true; break; } \
auto currType = checkJsonValueType(data[mainPrimaryKey]); \
if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \
tableLocalData.mainKeyType[mainPrimaryKey] = currType
#define SubKeyTypeProcess() if (!data.contains(subKeyParent)) { Log::ErrorFmt("subKeyParent: %s not found", subKeyParent.c_str()); isFailed = true; break; } \
for (auto& subKey : subKeys) { \
auto& subKeyValue = data[subKeyParent]; \
if (subKeyValue.is_object()) { \
if (!subKeyValue.contains(subKey)) { \
Log::ErrorFmt("subKey: %s not in subKeyParent: %s", subKey.c_str(), subKeyParent.c_str()); isFailed = true; break; \
} \
auto currType = checkJsonValueType(subKeyValue[subKey]); \
if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \
tableLocalData.subKeyType[subKeyParent].emplace(subKey, currType); \
} \
else if (subKeyValue.is_array()) { \
if (subKeyValue.empty()) goto NextLoop; \
for (auto& i : subKeyValue) { \
if (!i.is_object()) continue; \
if (!i.contains(subKey)) continue; \
auto currType = checkJsonValueType(i[subKey]); \
if (currType == JsonValueType::JVT_NeedMore_EmptyArray) goto NextLoop; \
tableLocalData.subKeyType[subKeyParent].emplace(subKey, currType); \
break; \
} \
} \
else { \
goto NextLoop;\
} \
}
bool GetTableLocalData(nlohmann::json& fullData, TableLocalData& tableLocalData) {
bool isFailed = false;
// 首先 Build mainKeyType 和 subKeyType
for (auto& data : fullData["data"]) {
if (!data.is_object()) continue;
for (auto& mainPrimaryKey : tableLocalData.itemRule.mainPrimaryKey) {
MainKeyTypeProcess();
}
for (auto& mainPrimaryKey : tableLocalData.itemRule.mainLocalKey) {
MainKeyTypeProcess();
}
for (const auto& [subKeyParent, subKeys] : tableLocalData.itemRule.subPrimaryKey) {
SubKeyTypeProcess()
if (isFailed) break;
}
for (const auto& [subKeyParent, subKeys] : tableLocalData.itemRule.subLocalKey) {
SubKeyTypeProcess()
if (isFailed) break;
}
if (!isFailed) break;
NextLoop:
;
}
if (isFailed) return false;
bool hasSuccess = false;
// 然后构造 transData
for (auto& data : fullData["data"]) {
if (!data.is_object()) continue;
if (BuildUniqueKeyValue(data, tableLocalData)) {
hasSuccess = true;
}
}
if (!hasSuccess) {
Log::ErrorFmt("BuildUniqueKeyValue failed.");
}
return hasSuccess;
}
void LoadData() {
masterLocalData.clear();
static auto masterDir = Local::GetBasePath() / "local-files" / "masterTrans";
if (!std::filesystem::is_directory(masterDir)) {
Log::ErrorFmt("LoadData: not found: %s", masterDir.string().c_str());
return;
}
bool isFirstIteration = true;
for (auto& p : std::filesystem::directory_iterator(masterDir)) {
if (isFirstIteration) {
auto totalFileCount = std::distance(
std::filesystem::directory_iterator(masterDir),
std::filesystem::directory_iterator{}
);
UnityResolveProgress::classProgress.total = totalFileCount <= 0 ? 1 : totalFileCount;
isFirstIteration = false;
}
UnityResolveProgress::classProgress.current++;
if (!p.is_regular_file()) continue;
const auto& path = p.path();
if (path.extension() != ".json") continue;
std::string tableName = path.stem().string();
auto fileContent = ReadFileToString(path);
if (fileContent.empty()) continue;
try {
auto j = nlohmann::json::parse(fileContent);
if (!j.contains("rules") || !j["rules"].contains("primaryKeys")) {
continue;
}
ItemRule currRule;
if (!GetItemRule(j, currRule)) {
Log::ErrorFmt("GetItemRule failed: %s", path.string().c_str());
continue;
}
/*
if (tableName == "ProduceStepEventDetail") {
for (auto& i : currRule.mainLocalKey) {
Log::DebugFmt("currRule.mainLocalKey: %s", i.c_str());
}
for (auto& i : currRule.mainPrimaryKey) {
Log::DebugFmt("currRule.mainPrimaryKey: %s", i.c_str());
}
for (auto& i : currRule.subLocalKey) {
for (auto& m : i.second) {
Log::DebugFmt("currRule.subLocalKey: %s - %s", i.first.c_str(), m.c_str());
}
}
for (auto& i : currRule.subPrimaryKey) {
for (auto& m : i.second) {
Log::DebugFmt("currRule.subPrimaryKey: %s - %s", i.first.c_str(), m.c_str());
}
}
}*/
TableLocalData tableLocalData{ .itemRule = currRule };
if (GetTableLocalData(j, tableLocalData)) {
for (auto& i : tableLocalData.transData) {
// Log::DebugFmt("%s: %s -> %s", tableName.c_str(), i.first.c_str(), i.second.c_str());
Local::translatedText.emplace(i.second);
}
for (auto& i : tableLocalData.transStrListData) {
for (auto& str : i.second) {
// Log::DebugFmt("%s[]: %s -> %s", tableName.c_str(), i.first.c_str(), str.c_str());
Local::translatedText.emplace(str);
}
}
/*
if (tableName == "ProduceStepEventDetail") {
for (auto& i : tableLocalData.mainKeyType) {
Log::DebugFmt("mainKeyType: %s -> %d", i.first.c_str(), i.second);
}
for (auto& i : tableLocalData.subKeyType) {
for (auto& m : i.second) {
Log::DebugFmt("subKeyType: %s - %s -> %d", i.first.c_str(), m.first.c_str(), m.second);
}
}
}*/
// JVT_ArrayString in HelpCategory, ProduceStory, Tutorial
masterLocalData.emplace(tableName, std::move(tableLocalData));
}
else {
Log::ErrorFmt("GetTableLocalData failed: %s", path.string().c_str());
}
} catch (std::exception& e) {
Log::ErrorFmt("MasterLocal::LoadData: parse error in '%s': %s",
path.string().c_str(), e.what());
}
}
}
}
void LoadData() {
return Load::LoadData();
}
std::string GetTransString(const std::string& key, const TableLocalData& localData) {
if (auto it = localData.transData.find(key); it != localData.transData.end()) {
return it->second;
}
return {};
}
std::vector<std::string> GetTransArrayString(const std::string& key, const TableLocalData& localData) {
if (auto it = localData.transStrListData.find(key); it != localData.transStrListData.end()) {
return it->second;
}
return {};
}
void LocalizeMasterItem(FieldController& fc, const std::string& tableName) {
auto it = masterLocalData.find(tableName);
if (it == masterLocalData.end()) return;
const auto& localData = it->second;
// 首先拼 BasePrimaryKey
std::string baseDataKey; // p_card-00-acc-0_002|0|
for (auto& mainPk : localData.itemRule.mainPrimaryKey) {
auto mainPkType = localData.GetMainKeyType(mainPk);
switch (mainPkType) {
case JsonValueType::JVT_Int: {
auto readValue = std::to_string(fc.ReadIntField(mainPk));
baseDataKey.append(readValue);
baseDataKey.push_back('|');
} break;
case JsonValueType::JVT_String: {
auto readValue = fc.ReadStringField(mainPk);
if (!readValue) return;
baseDataKey.append(readValue->ToString());
baseDataKey.push_back('|');
} break;
default:
break;
}
}
// 然后本地化 mainLocal
for (auto& mainLocal : localData.itemRule.mainLocalKey) {
std::string currSearchKey = baseDataKey;
currSearchKey.append(mainLocal); // p_card-00-acc-0_002|0|name
auto localVType = localData.GetMainKeyType(mainLocal);
switch (localVType) {
case JsonValueType::JVT_String: {
auto localValue = GetTransString(currSearchKey, localData);
if (!localValue.empty()) {
fc.SetStringField(mainLocal, localValue);
}
} break;
case JsonValueType::JVT_ArrayString: {
auto localValue = GetTransArrayString(currSearchKey, localData);
if (!localValue.empty()) {
fc.SetStringListField(mainLocal, localValue);
}
} break;
default:
break;
}
}
// 处理 sub
for (const auto& [subParentKey, subLocalKeys] : localData.itemRule.subLocalKey) {
const auto subBaseSearchKey = baseDataKey + subParentKey + '|'; // p_card-00-acc-0_002|0|produceDescriptions|
const auto subParentType = localData.GetMainKeyType(subParentKey);
switch (subParentType) {
case JsonValueType::JVT_Object: {
auto subParentField = fc.CreateSubFieldController(subParentKey);
for (const auto& subLocalKey : subLocalKeys) {
const auto currSearchKey = subBaseSearchKey + subLocalKey; // p_card-00-acc-0_002|0|produceDescriptions|text
auto localKeyType = localData.GetSubKeyType(subParentKey, subLocalKey);
if (localKeyType == JsonValueType::JVT_String) {
auto setData = GetTransString(currSearchKey, localData);
if (!setData.empty()) {
subParentField.SetStringField(subLocalKey, setData);
}
}
else if (localKeyType == JsonValueType::JVT_ArrayString) {
auto setData = GetTransArrayString(currSearchKey, localData);
if (!setData.empty()) {
subParentField.SetStringListField(subLocalKey, setData);
}
}
}
} break;
case JsonValueType::JVT_ArrayObject: {
auto subArrField = fc.ReadObjectListField(subParentKey);
if (!subArrField) continue;
Il2cppUtils::Tools::CSListEditor<void*> subListEdit(subArrField);
auto count = subListEdit.get_Count();
for (int idx = 0; idx < count; idx++) {
auto currItem = subListEdit.get_Item(idx);
if (!currItem) continue;
auto currFc = FieldController::CreateSubFieldController(currItem);
std::string currSearchBaseKey = subBaseSearchKey; // p_card-00-acc-0_002|0|produceDescriptions|
currSearchBaseKey.push_back('[');
currSearchBaseKey.append(std::to_string(idx));
currSearchBaseKey.append("]|"); // p_card-00-acc-0_002|0|produceDescriptions|[0]|
for (const auto& subLocalKey : subLocalKeys) {
std::string currSearchKey = currSearchBaseKey + subLocalKey; // p_card-00-acc-0_002|0|produceDescriptions|[0]|text
auto localKeyType = localData.GetSubKeyType(subParentKey, subLocalKey);
/*
if (tableName == "ProduceStepEventDetail") {
Log::DebugFmt("localKeyType: %d currSearchKey: %s", localKeyType, currSearchKey.c_str());
}*/
if (localKeyType == JsonValueType::JVT_String) {
auto setData = GetTransString(currSearchKey, localData);
if (!setData.empty()) {
currFc.SetStringField(subLocalKey, setData);
}
}
else if (localKeyType == JsonValueType::JVT_ArrayString) {
auto setData = GetTransArrayString(currSearchKey, localData);
if (!setData.empty()) {
currFc.SetStringListField(subLocalKey, setData);
}
}
}
}
} break;
default:
break;
}
}
}
void LocalizeMasterItem(void* item, const std::string& tableName) {
if (!Config::useMasterTrans) return;
// Log::DebugFmt("LocalizeMasterItem: %s", tableName.c_str());
FieldController fc(item);
LocalizeMasterItem(fc, tableName);
}
} // namespace GakumasLocal::MasterLocal
@@ -0,0 +1,12 @@
#ifndef GAKUMAS_LOCALIFY_MASTERLOCAL_H
#define GAKUMAS_LOCALIFY_MASTERLOCAL_H
#include <string>
namespace GakumasLocal::MasterLocal {
void LoadData();
void LocalizeMasterItem(void* item, const std::string& tableName);
}
#endif //GAKUMAS_LOCALIFY_MASTERLOCAL_H
+55 -3
View File
@@ -2,14 +2,36 @@
#include <codecvt>
#include <locale>
#include <jni.h>
#include "fmt/core.h"
extern JavaVM* g_javaVM;
#ifndef GKMS_WINDOWS
#include <jni.h>
extern JavaVM* g_javaVM;
#else
#include "cpprest/details/http_helpers.h"
#endif
namespace GakumasLocal::Misc {
#ifdef GKMS_WINDOWS
std::string ToUTF8(const std::wstring_view& str) {
return utility::conversions::to_utf8string(str.data());
}
std::u16string ToUTF16(const std::string_view& str) {
std::string input(str);
std::wstring wstr = utility::conversions::utf8_to_utf16(input);
return std::u16string(wstr.begin(), wstr.end());
}
std::string ToUTF8(const std::u16string_view& str) {
std::u16string u16(str);
std::wstring wstr(u16.begin(), u16.end());
return utility::conversions::utf16_to_utf8(wstr);
}
#else
std::u16string ToUTF16(const std::string_view& str) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
return utf16conv.from_bytes(str.data(), str.data() + str.size());
@@ -19,7 +41,9 @@ namespace GakumasLocal::Misc {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> utf16conv;
return utf16conv.to_bytes(str.data(), str.data() + str.size());
}
#endif
#ifndef GKMS_WINDOWS
JNIEnv* GetJNIEnv() {
if (!g_javaVM) return nullptr;
JNIEnv* env = nullptr;
@@ -31,6 +55,7 @@ namespace GakumasLocal::Misc {
}
return env;
}
#endif
CSEnum::CSEnum(const std::string& name, const int value) {
this->Add(name, value);
@@ -168,6 +193,33 @@ namespace GakumasLocal::Misc {
return fmt;
}
}
std::vector<std::string> split(const std::string& str, char delimiter) {
std::vector<std::string> result;
std::string current;
for (char c : str) {
if (c == delimiter) {
if (!current.empty()) {
result.push_back(current);
}
current.clear();
} else {
current += c;
}
}
if (!current.empty()) {
result.push_back(current);
}
return result;
}
std::pair<std::string, std::string> split_once(const std::string& str, const std::string& delimiter) {
size_t pos = str.find(delimiter);
if (pos != std::string::npos) {
return {str.substr(0, pos), str.substr(pos + delimiter.size())};
}
return {str, ""};
}
}
}
+14 -1
View File
@@ -2,11 +2,16 @@
#include <string>
#include <string_view>
#include <jni.h>
#include <deque>
#include <numeric>
#include <vector>
#include "../platformDefine.hpp"
#ifndef GKMS_WINDOWS
#include <jni.h>
#endif
namespace GakumasLocal {
using OpaqueFunctionPointer = void (*)();
@@ -14,7 +19,13 @@ namespace GakumasLocal {
namespace Misc {
std::u16string ToUTF16(const std::string_view& str);
std::string ToUTF8(const std::u16string_view& str);
#ifdef GKMS_WINDOWS
std::string ToUTF8(const std::wstring_view& str);
#endif
#ifndef GKMS_WINDOWS
JNIEnv* GetJNIEnv();
#endif
class CSEnum {
public:
@@ -76,6 +87,8 @@ namespace GakumasLocal {
namespace StringFormat {
std::string stringFormatString(const std::string& fmt, const std::vector<std::string>& vec);
std::vector<std::string> split(const std::string& str, char delimiter);
std::pair<std::string, std::string> split_once(const std::string& str, const std::string& delimiter);
}
}
}
+7 -1
View File
@@ -4,7 +4,13 @@
#include "Misc.hpp"
#include <string>
#include <memory>
#include <jni.h>
#include "../platformDefine.hpp"
#ifndef GKMS_WINDOWS
#include <jni.h>
#endif // !GKMS_WINDOWS
namespace GakumasLocal {
struct HookInstaller
@@ -1,6 +1,12 @@
#include "baseCamera.hpp"
#include <thread>
#include "../../platformDefine.hpp"
#ifdef GKMS_WINDOWS
#include <corecrt_math_defines.h>
#endif // GKMS_WINDOWS
namespace BaseCamera {
using Vector3_t = UnityResolve::UnityType::Vector3;
@@ -1,6 +1,6 @@
#pragma once
#include "../deps/UnityResolve/UnityResolve.hpp"
#include "../../deps/UnityResolve/UnityResolve.hpp"
enum LonMoveHState {
LonMoveLeftAndRight,
@@ -1,8 +1,14 @@
#include "baseCamera.hpp"
#include "camera.hpp"
#include <thread>
#include "Misc.hpp"
#include "../Misc.hpp"
#include "../BaseDefine.h"
#include "../../platformDefine.hpp"
#ifdef GKMS_WINDOWS
#include <corecrt_math_defines.h>
#endif // GKMS_WINDOWS
namespace GKCamera {
@@ -1,6 +1,6 @@
#pragma once
#include "baseCamera.hpp"
#include "Joystick/JoystickEvent.h"
#include "../../deps/Joystick/JoystickEvent.h"
namespace GKCamera {
enum class CameraMode {
@@ -1,6 +1,8 @@
#include <string>
#include "nlohmann/json.hpp"
#include "../Log.h"
#include <thread>
#include <fstream>
namespace GakumasLocal::Config {
bool isConfigInit = false;
@@ -9,18 +11,24 @@ namespace GakumasLocal::Config {
bool enabled = true;
bool lazyInit = true;
bool replaceFont = true;
bool replaceTexture = true;
bool forceExportResource = true;
bool textTest = false;
bool useMasterTrans = true;
int gameOrientation = 0;
bool dumpText = false;
bool dumpRuntimeTexture = false;
bool enableFreeCamera = false;
int targetFrameRate = 0;
bool unlockAllLive = false;
bool unlockAllLiveCostume = false;
bool enableLiveCustomeDress = false;
std::string liveCustomeHeadId = "";
std::string liveCustomeCostumeId = "";
bool loginAsIOS = false;
bool useCustomeGraphicSettings = false;
float renderScale = 0.77f;
int qualitySettingsLevel = 3;
@@ -48,6 +56,8 @@ namespace GakumasLocal::Config {
float bLimitZx = 1.0f;
float bLimitZy = 1.0f;
bool dmmUnlockSize = false;
void LoadConfig(const std::string& configStr) {
try {
const auto config = nlohmann::json::parse(configStr);
@@ -58,16 +68,21 @@ namespace GakumasLocal::Config {
GetConfigItem(enabled);
GetConfigItem(lazyInit);
GetConfigItem(replaceFont);
GetConfigItem(replaceTexture);
GetConfigItem(forceExportResource);
GetConfigItem(gameOrientation);
GetConfigItem(textTest);
GetConfigItem(useMasterTrans);
GetConfigItem(dumpText);
GetConfigItem(dumpRuntimeTexture);
GetConfigItem(targetFrameRate);
GetConfigItem(enableFreeCamera);
GetConfigItem(unlockAllLive);
GetConfigItem(unlockAllLiveCostume);
GetConfigItem(enableLiveCustomeDress);
GetConfigItem(liveCustomeHeadId);
GetConfigItem(liveCustomeCostumeId);
GetConfigItem(loginAsIOS);
GetConfigItem(useCustomeGraphicSettings);
GetConfigItem(renderScale);
GetConfigItem(qualitySettingsLevel);
@@ -93,11 +108,76 @@ namespace GakumasLocal::Config {
GetConfigItem(bLimitYy);
GetConfigItem(bLimitZx);
GetConfigItem(bLimitZy);
GetConfigItem(dmmUnlockSize);
}
catch (std::exception& e) {
Log::ErrorFmt("LoadConfig error: %s", e.what());
}
isConfigInit = true;
}
void SaveConfig(const std::string& configPath) {
try {
nlohmann::json config;
#define SetConfigItem(name) config[#name] = name
SetConfigItem(dbgMode);
SetConfigItem(enabled);
SetConfigItem(lazyInit);
SetConfigItem(replaceFont);
SetConfigItem(replaceTexture);
SetConfigItem(forceExportResource);
SetConfigItem(gameOrientation);
SetConfigItem(textTest);
SetConfigItem(useMasterTrans);
SetConfigItem(dumpText);
SetConfigItem(dumpRuntimeTexture);
SetConfigItem(targetFrameRate);
SetConfigItem(enableFreeCamera);
SetConfigItem(unlockAllLive);
SetConfigItem(unlockAllLiveCostume);
SetConfigItem(enableLiveCustomeDress);
SetConfigItem(liveCustomeHeadId);
SetConfigItem(liveCustomeCostumeId);
SetConfigItem(loginAsIOS);
SetConfigItem(useCustomeGraphicSettings);
SetConfigItem(renderScale);
SetConfigItem(qualitySettingsLevel);
SetConfigItem(volumeIndex);
SetConfigItem(maxBufferPixel);
SetConfigItem(reflectionQualityLevel);
SetConfigItem(lodQualityLevel);
SetConfigItem(enableBreastParam);
SetConfigItem(bDamping);
SetConfigItem(bStiffness);
SetConfigItem(bSpring);
SetConfigItem(bPendulum);
SetConfigItem(bPendulumRange);
SetConfigItem(bAverage);
SetConfigItem(bRootWeight);
SetConfigItem(bUseArmCorrection);
SetConfigItem(bUseScale);
SetConfigItem(bScale);
SetConfigItem(bUseLimit);
SetConfigItem(bLimitXx);
SetConfigItem(bLimitXy);
SetConfigItem(bLimitYx);
SetConfigItem(bLimitYy);
SetConfigItem(bLimitZx);
SetConfigItem(bLimitZy);
SetConfigItem(dmmUnlockSize);
std::ofstream out(configPath);
if (!out) {
Log::ErrorFmt("SaveConfig error: Cannot open file: %s", configPath.c_str());
return;
}
out << config.dump(4);
Log::Info("SaveConfig success");
}
catch (std::exception& e) {
Log::ErrorFmt("SaveConfig error: %s", e.what());
}
}
}
@@ -7,18 +7,24 @@ namespace GakumasLocal::Config {
extern bool enabled;
extern bool lazyInit;
extern bool replaceFont;
extern bool replaceTexture;
extern bool forceExportResource;
extern int gameOrientation;
extern bool textTest;
extern bool useMasterTrans;
extern bool dumpText;
extern bool dumpRuntimeTexture;
extern bool enableFreeCamera;
extern int targetFrameRate;
extern bool unlockAllLive;
extern bool unlockAllLiveCostume;
extern bool enableLiveCustomeDress;
extern std::string liveCustomeHeadId;
extern std::string liveCustomeCostumeId;
extern bool loginAsIOS;
extern bool useCustomeGraphicSettings;
extern float renderScale;
extern int qualitySettingsLevel;
@@ -47,5 +53,8 @@ namespace GakumasLocal::Config {
extern float bLimitZx;
extern float bLimitZy;
extern bool dmmUnlockSize;
void LoadConfig(const std::string& configStr);
void SaveConfig(const std::string& configPath);
}
@@ -309,7 +309,7 @@ public:
pDomain = Invoke<void*>("il2cpp_domain_get");
Invoke<void*>("il2cpp_thread_attach", pDomain);
ForeachAssembly();
if (!lazyInit) UnityResolveProgress::startInit = false;
// if (!lazyInit) UnityResolveProgress::startInit = false;
}
else {
pDomain = Invoke<void*>("mono_get_root_domain");
+21
View File
@@ -0,0 +1,21 @@
#include "shadowhook.h"
#include <android/log.h>
#define ADD_HOOK(name, addr) \
name##_Addr = reinterpret_cast<name##_Type>(addr); \
if (addr) { \
auto stub = hookInstaller->InstallHook(reinterpret_cast<void*>(addr), \
reinterpret_cast<void*>(name##_Hook), \
reinterpret_cast<void**>(&name##_Orig)); \
if (stub == NULL) { \
int error_num = shadowhook_get_errno(); \
const char *error_msg = shadowhook_to_errmsg(error_num); \
Log::ErrorFmt("ADD_HOOK: %s at %p failed: %s", #name, addr, error_msg); \
} \
else { \
hookedStubs.emplace(stub); \
GakumasLocal::Log::InfoFmt("ADD_HOOK: %s at %p", #name, addr); \
} \
} \
else GakumasLocal::Log::ErrorFmt("Hook failed: %s is NULL", #name, addr); \
if (Config::lazyInit) UnityResolveProgress::classProgress.current++
@@ -2,8 +2,10 @@ package io.github.chinosk.gakumas.localify
import android.app.Activity
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig
@@ -77,6 +79,9 @@ fun <T> T.loadConfig() where T : Activity, T : IHasConfigItems {
if (programConfig.useAPIAssetsURL.isEmpty()) {
programConfig.useAPIAssetsURL = getString(R.string.default_assets_check_api)
}
if (programConfig.useAPITextureAssetsURL.isEmpty()) {
programConfig.useAPITextureAssetsURL = getString(R.string.default_texture_assets_check_api)
}
}
fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
@@ -105,8 +110,9 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
putExtra(
"localData",
getProgramConfigContent(listOf("transRemoteZipUrl", "useAPIAssetsURL",
"localAPIAssetsVersion", "p"), programConfig)
"useAPITextureAssetsURL", "localAPIAssetsVersion", "p"), programConfig)
)
putExtra("lVerName", version)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
@@ -128,9 +134,42 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
"io.github.chinosk.gakumas.localify.fileprovider",
File(targetFile.absolutePath)
)
intent.setDataAndType(dirUri, "resource/file")
// intent.setDataAndType(dirUri, "resource/file")
grantUriPermission(
"com.bandainamcoent.idolmaster_gakuen",
dirUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
intent.putExtra("resource_file", dirUri)
// intent.clipData = ClipData.newRawUri("resource_file", dirUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
val textureUpdateFile = TextureResourceUpdater.getCachedZipFile(this)
Log.i(TAG, "Texture cache before launch: replaceTexture=${config.replaceTexture}, " +
"useAPITextureAssets=${programConfig.useAPITextureAssets}, " +
"exists=${textureUpdateFile.exists()}, size=${if (textureUpdateFile.exists()) textureUpdateFile.length() else 0}, " +
"path=${textureUpdateFile.absolutePath}")
if (config.replaceTexture && textureUpdateFile.exists()) {
val textureUri = FileProvider.getUriForFile(
this,
"io.github.chinosk.gakumas.localify.fileprovider",
textureUpdateFile
)
grantUriPermission(
"com.bandainamcoent.idolmaster_gakuen",
textureUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
intent.putExtra("texture_resource_file", textureUri)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
Log.i(TAG, "Texture resource uri attached: $textureUri")
}
else {
Log.i(TAG, "Texture resource uri not attached.")
}
startActivity(intent)
}
@@ -17,12 +17,16 @@ import kotlinx.coroutines.runBlocking
interface ConfigListener {
fun onEnabledChanged(value: Boolean)
fun onForceExportResourceChanged(value: Boolean)
fun onLoginAsIOSChanged(value: Boolean)
fun onTextTestChanged(value: Boolean)
fun onUseMasterTransChanged(value: Boolean)
fun onReplaceFontChanged(value: Boolean)
fun onReplaceTextureChanged(value: Boolean)
fun onLazyInitChanged(value: Boolean)
fun onEnableFreeCameraChanged(value: Boolean)
fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onUnlockAllLiveChanged(value: Boolean)
fun onUnlockAllLiveCostumeChanged(value: Boolean)
fun onLiveCustomeDressChanged(value: Boolean)
fun onLiveCustomeHeadIdChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onLiveCustomeCostumeIdChanged(s: CharSequence, start: Int, before: Int, count: Int)
@@ -36,6 +40,7 @@ interface ConfigListener {
fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onGameOrientationChanged(checkedId: Int)
fun onDumpTextChanged(value: Boolean)
fun onDumpRuntimeTextureChanged(value: Boolean)
fun onEnableBreastParamChanged(value: Boolean)
fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int)
@@ -66,8 +71,15 @@ interface ConfigListener {
localResourceVersionState: String? = null,
errorString: String? = null,
localAPIResourceVersion: String? = null)
fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
downloadProgressState: Float? = null,
localTextureResourceVersion: String? = null,
errorString: String? = null)
fun onPUseAPIAssetsChanged(value: Boolean)
fun onPUseAPIAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onPUseAPITextureAssetsChanged(value: Boolean)
fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean)
fun mainUIConfirmStatUpdate(isShow: Boolean? = null, title: String? = null,
content: String? = null,
onConfirm: (() -> Unit)? = { mainUIConfirmStatUpdate(isShow = false) },
@@ -115,12 +127,23 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onLoginAsIOSChanged(value: Boolean) {
config.loginAsIOS = value
saveConfig()
}
override fun onReplaceFontChanged(value: Boolean) {
config.replaceFont = value
saveConfig()
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onReplaceTextureChanged(value: Boolean) {
config.replaceTexture = value
saveConfig()
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onLazyInitChanged(value: Boolean) {
config.lazyInit = value
saveConfig()
@@ -131,11 +154,21 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveConfig()
}
override fun onUseMasterTransChanged(value: Boolean) {
config.useMasterTrans = value
saveConfig()
}
override fun onDumpTextChanged(value: Boolean) {
config.dumpText = value
saveConfig()
}
override fun onDumpRuntimeTextureChanged(value: Boolean) {
config.dumpRuntimeTexture = value
saveConfig()
}
override fun onEnableFreeCameraChanged(value: Boolean) {
config.enableFreeCamera = value
saveConfig()
@@ -146,6 +179,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveConfig()
}
override fun onUnlockAllLiveCostumeChanged(value: Boolean) {
config.unlockAllLiveCostume = value
saveConfig()
}
override fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int) {
try {
val valueStr = s.toString()
@@ -558,6 +596,14 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
localAPIResourceVersion?.let{ programConfigViewModel.localAPIResourceVersionState.value = it }
}
override fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?,
localTextureResourceVersion: String?, errorString: String?) {
downloadAbleState?.let { programConfigViewModel.textureDownloadAbleState.value = it }
downloadProgressState?.let{ programConfigViewModel.textureDownloadProgressState.value = it }
localTextureResourceVersion?.let{ programConfigViewModel.localTextureResourceVersionState.value = it }
errorString?.let{ programConfigViewModel.textureErrorStringState.value = it }
}
override fun onPUseAPIAssetsChanged(value: Boolean) {
programConfig.useAPIAssets = value
if (value) {
@@ -573,6 +619,21 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveProgramConfig()
}
override fun onPUseAPITextureAssetsChanged(value: Boolean) {
programConfig.useAPITextureAssets = value
saveProgramConfig()
}
override fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) {
programConfig.useAPITextureAssetsURL = s.toString()
saveProgramConfig()
}
override fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean) {
programConfig.delTextureRemoteAfterUpdate = value
saveProgramConfig()
}
override fun mainUIConfirmStatUpdate(isShow: Boolean?, title: String?, content: String?,
onConfirm: (() -> Unit)?, onCancel: (() -> Unit)?
) {
@@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
@@ -34,6 +35,7 @@ import java.util.Locale
import kotlin.system.measureTimeMillis
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import io.github.chinosk.gakumas.localify.models.ProgramConfig
@@ -52,6 +54,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var getConfigError: Exception? = null
private var externalFilesChecked: Boolean = false
private var textureFilesChecked: Boolean = false
private var gameActivity: Activity? = null
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
@@ -247,6 +250,9 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
val gkmsData = intent.getStringExtra("gkmsData")
val programData = intent.getStringExtra("localData")
if (gkmsData != null) {
val readVersion = intent.getStringExtra("lVerName")
checkPluginVersion(activity, readVersion)
gkmsDataInited = true
val initConfig = try {
json.decodeFromString<GakumasConfig>(gkmsData)
@@ -282,7 +288,14 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
// 使用热更新文件
if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
val dataUri = intent.data
// val dataUri = intent.data
val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("resource_file", Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>("resource_file")
}
if (dataUri != null) {
if (!externalFilesChecked) {
externalFilesChecked = true
@@ -300,11 +313,64 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
}
}
if (initConfig?.replaceTexture == true && !textureFilesChecked) {
val textureDataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("texture_resource_file", Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>("texture_resource_file")
}
if (textureDataUri != null) {
Log.i(TAG, "Texture resource uri received: $textureDataUri")
textureFilesChecked = true
TextureResourceUpdater.updateTextureFilesFromZip(activity, textureDataUri,
activity.filesDir, programConfig?.delTextureRemoteAfterUpdate ?: true)
}
else {
Log.i(TAG, "Texture resource uri missing.")
}
}
loadConfig(gkmsData)
Log.d(TAG, "gkmsData: $gkmsData")
}
}
private fun checkPluginVersion(activity: Activity, readVersion: String?) {
val buildVersionName = BuildConfig.VERSION_NAME
Log.i(TAG, "Checking Plugin Version: Build: $buildVersionName, Request: $readVersion")
if (readVersion?.trim() == buildVersionName.trim()) {
return
}
val builder = AlertDialog.Builder(activity)
val infoBuilder = AlertDialog.Builder(activity)
builder.setTitle("Warning")
builder.setCancelable(false)
builder.setMessage(when (getCurrentLanguage(activity)) {
"zh" -> "检测到插件版本不一致\n内置版本: $buildVersionName\n请求版本: $readVersion\n\n这可能是使用了 LSPatch 的集成模式,仅更新了插件本体,未重新修补游戏导致的。请使用 $readVersion 版本的插件重新修补或使用本地模式。"
else -> "Detected plugin version mismatch\nBuilt-in version: $buildVersionName\nRequested version: $readVersion\n\nThis may be caused by using the LSPatch integration mode, where only the plugin itself was updated without re-patching the game. Please re-patch the game using the $readVersion version of the plugin or use the local mode."
})
builder.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
builder.setNegativeButton("Exit") { dialog, _ ->
dialog.dismiss()
activity.finishAffinity()
}
val dialog = builder.create()
infoBuilder.setOnCancelListener {
dialog.show()
}
dialog.show()
}
private fun showGetConfigFailedImpl(activity: Context, title: String, msg: String, infoButton: String, dlButton: String, okButton: String) {
if (getConfigError == null) return
val builder = AlertDialog.Builder(activity)
@@ -464,4 +530,4 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
false
}
}
}
}
@@ -18,6 +18,7 @@ import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker
import io.github.chinosk.gakumas.localify.mainUtils.ShizukuApi
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.ConfirmStateModel
import io.github.chinosk.gakumas.localify.models.GakumasConfig
@@ -79,6 +80,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
fun getVersion(): List<String> {
var versionText = ""
var resVersionText = "unknown"
var textureVersionText = "unknown"
try {
val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt")
@@ -87,6 +89,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
if (programConfig.useAPIAssets) {
RemoteAPIFilesChecker.getLocalVersion(this)?.let { resVersionText = it }
}
TextureResourceUpdater.getLocalVersion(this)?.let { textureVersionText = it }
val packInfo = packageManager.getPackageInfo(packageName, 0)
val version = packInfo.versionName
@@ -95,7 +98,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
}
catch (_: Exception) {}
return listOf(versionText, resVersionText)
return listOf(versionText, resVersionText, textureVersionText)
}
fun openUrl(url: String) {
@@ -130,7 +133,8 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java]
programConfigFactory = ProgramConfigViewModelFactory(programConfig,
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString()
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString(),
TextureResourceUpdater.getLocalVersion(this).toString()
)
programConfigViewModel = ViewModelProvider(this, programConfigFactory)[ProgramConfigViewModel::class.java]
@@ -222,6 +226,50 @@ fun getProgramDownloadErrorStringState(context: MainActivity?): State<String> {
}
}
@Composable
fun getProgramTextureDownloadState(context: MainActivity?): State<Float> {
return if (context != null) {
context.programConfigViewModel.textureDownloadProgress.collectAsState()
}
else {
val configMSF = MutableStateFlow(0f)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramTextureDownloadAbleState(context: MainActivity?): State<Boolean> {
return if (context != null) {
context.programConfigViewModel.textureDownloadAble.collectAsState()
}
else {
val configMSF = MutableStateFlow(true)
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramLocalTextureResourceVersionState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.localTextureResourceVersion.collectAsState()
}
else {
val configMSF = MutableStateFlow("null")
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getProgramTextureDownloadErrorStringState(context: MainActivity?): State<String> {
return if (context != null) {
context.programConfigViewModel.textureErrorString.collectAsState()
}
else {
val configMSF = MutableStateFlow("")
configMSF.asStateFlow().collectAsState()
}
}
@Composable
fun getMainUIConfirmState(context: MainActivity?, previewData: ConfirmStateModel? = null): State<ConfirmStateModel> {
return if (context != null) {
@@ -645,7 +645,7 @@ class PatchActivity : ComponentActivity() {
val copyFilesCmd: MutableList<String> = mutableListOf()
val movedFiles: MutableList<String> = mutableListOf()
savedFileNames.forEach { file ->
val movedFileName = "$installDS/${file}"
val movedFileName = "\"$installDS/${file}\""
movedFiles.add(movedFileName)
val dlSaveFileName = File(targetDirectory, file)
copyFilesCmd.add("$action ${dlSaveFileName.absolutePath} $movedFileName")
@@ -45,6 +45,7 @@ object FilesChecker {
if (!pluginBasePath.exists()) {
pluginBasePath.mkdirs()
}
val skipBuiltInTexture2d = File(filesDir, "$localizationFilesDir/texture2d").exists()
val assets = XModuleResources.createInstance(modulePath, null).assets
fun forAllAssetFiles(
@@ -65,6 +66,12 @@ object FilesChecker {
}
}
forAllAssetFiles(localizationFilesDir) { path, file ->
if ((path == "$localizationFilesDir/texture2d" ||
path.startsWith("$localizationFilesDir/texture2d/")) &&
skipBuiltInTexture2d) {
return@forAllAssetFiles
}
val outFile = File(filesDir, path)
if (file == null) {
outFile.mkdirs()
@@ -84,7 +91,7 @@ object FilesChecker {
for (i in assets.list(localizationFilesDir)!!) {
if (i.toString() == "version.txt") {
val stream = assets.open("$localizationFilesDir/$i")
return convertToString(stream)
return convertToString(stream).trim()
}
}
return "0.0"
@@ -96,7 +103,7 @@ object FilesChecker {
val versionFile = File(pluginFilesDir, "version.txt")
if (!versionFile.exists()) return "0.0"
return versionFile.readText()
return versionFile.readText().trim()
}
fun convertToString(inputStream: InputStream?): String {
@@ -146,6 +153,7 @@ object FilesChecker {
val genericTransDir = File(localFilesDir, "genericTrans")
val genericTransFile = File(localFilesDir, "generic.json")
val i18nFile = File(localFilesDir, "localization.json")
val masterTransDir = File(localFilesDir, "masterTrans")
if (fontFile.exists()) {
fontFile.delete()
@@ -156,6 +164,9 @@ object FilesChecker {
if (deleteRecursively(genericTransDir)) {
genericTransDir.mkdirs()
}
if (deleteRecursively(masterTransDir)) {
masterTransDir.mkdirs()
}
if (genericTransFile.exists()) {
genericTransFile.writeText("{}")
}
@@ -163,4 +174,4 @@ object FilesChecker {
i18nFile.writeText("{}")
}
}
}
}
@@ -3,6 +3,8 @@ package io.github.chinosk.gakumas.localify.mainUtils
import okhttp3.*
import java.io.IOException
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
object FileDownloader {
@@ -111,6 +113,99 @@ object FileDownloader {
}
fun downloadFileToPath(
url: String,
outputFile: File,
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
onSuccess: (File) -> Unit,
onFailed: (Int, String) -> Unit,
checkContentTypes: List<String>? = null
) {
try {
if (call != null) {
onFailed(-1, "Another file is downloading.")
return
}
outputFile.parentFile?.mkdirs()
val request = Request.Builder()
.url(url)
.build()
call = client.newCall(request)
call?.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
this@FileDownloader.call = null
if (call.isCanceled()) {
onFailed(-1, "Download canceled")
} else {
onFailed(-1, e.message ?: "Unknown error")
}
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
this@FileDownloader.call = null
onFailed(response.code, response.message)
return
}
if (checkContentTypes != null) {
val contentType = response.header("Content-Type")
if (!checkContentTypes.contains(contentType)) {
onFailed(-1, "Unexpected content type: $contentType")
this@FileDownloader.call = null
return
}
}
response.body?.let { responseBody ->
val contentLength = responseBody.contentLength()
val inputStream = responseBody.byteStream()
val buffer = ByteArray(8 * 1024)
var downloadedBytes = 0L
var read: Int
try {
FileOutputStream(outputFile).use { outputStream ->
while (inputStream.read(buffer).also { read = it } != -1) {
outputStream.write(buffer, 0, read)
downloadedBytes += read
val progress = if (contentLength < 0) {
0f
}
else {
downloadedBytes.toFloat() / contentLength
}
onDownload(progress, downloadedBytes, contentLength)
}
outputStream.flush()
}
onSuccess(outputFile)
} catch (e: IOException) {
outputFile.delete()
if (call.isCanceled()) {
onFailed(-1, "Download canceled")
} else {
onFailed(-1, e.message ?: "Error reading stream")
}
} finally {
this@FileDownloader.call = null
inputStream.close()
}
} ?: run {
this@FileDownloader.call = null
onFailed(-1, "Response body is null")
}
}
})
}
catch (e: Exception) {
onFailed(-1, e.toString())
call = null
}
}
fun cancel() {
call?.cancel()
this@FileDownloader.call = null
@@ -0,0 +1,322 @@
package io.github.chinosk.gakumas.localify.mainUtils
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.util.Log
import io.github.chinosk.gakumas.localify.GakumasHookMain
import io.github.chinosk.gakumas.localify.TAG
import io.github.chinosk.gakumas.localify.models.Asset
import io.github.chinosk.gakumas.localify.models.GithubReleaseModel
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Request
import okhttp3.Response
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.zip.ZipFile
object TextureResourceUpdater {
private const val CACHE_DIR = "remote_texture_files"
private const val CACHE_ZIP_NAME = "texture.zip"
private const val CACHE_VERSION_NAME = "texture_version.txt"
private const val TEXTURE_DIR = "gakumas-local/texture2d"
private const val VERSION_FILE_NAME = "texture_version.txt"
private data class TextureZipRoot(val prefix: String, val containsTexture2dDir: Boolean)
private fun textureDir(context: Context): File {
return File(context.filesDir, TEXTURE_DIR)
}
fun getCachedZipFile(context: Context): File {
return File(context.filesDir, "$CACHE_DIR/$CACHE_ZIP_NAME")
}
fun getLocalVersion(context: Context): String? {
val versionFile = File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME")
if (versionFile.exists()) {
versionFile.readText().trim().takeIf { it.isNotEmpty() }?.let { return it }
}
return null
}
fun getInstalledVersion(context: Context): String? {
val versionFile = File(textureDir(context), VERSION_FILE_NAME)
if (!versionFile.exists()) {
return null
}
return versionFile.readText().trim()
}
private fun ensureCacheDir(context: Context): File {
val basePath = File(context.filesDir, CACHE_DIR)
if (!basePath.exists()) {
basePath.mkdirs()
}
return basePath
}
private fun saveCachedVersion(context: Context, version: String) {
val versionFile = File(ensureCacheDir(context), CACHE_VERSION_NAME)
versionFile.writeText(version)
}
private fun findTextureZipRoot(zipFile: ZipFile): TextureZipRoot? {
val entries = zipFile.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.isDirectory) continue
val name = entry.name.replace('\\', '/').trimStart('/')
val textureVersionMarker = "texture2d/$VERSION_FILE_NAME"
if (name.endsWith(textureVersionMarker)) {
return TextureZipRoot(name.substring(0, name.length - textureVersionMarker.length), true)
}
if (name == VERSION_FILE_NAME || name.endsWith("/$VERSION_FILE_NAME")) {
val prefix = name.substring(0, name.length - VERSION_FILE_NAME.length)
return TextureZipRoot(prefix, false)
}
}
return null
}
private fun readTextureVersion(zipFile: ZipFile, root: TextureZipRoot): String? {
val versionPath = if (root.containsTexture2dDir) {
"${root.prefix}texture2d/$VERSION_FILE_NAME"
} else {
"${root.prefix}$VERSION_FILE_NAME"
}
val entry = zipFile.getEntry(versionPath) ?: return null
return zipFile.getInputStream(entry).bufferedReader().use { it.readText().trim() }
}
private fun validateTextureZip(zipFile: File): String {
ZipFile(zipFile).use { zip ->
val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip")
return readTextureVersion(zip, root) ?: throw IOException("texture_version.txt is empty")
}
}
private fun findTextureZipAsset(releaseData: GithubReleaseModel): Asset? {
val zipAssets = releaseData.assets.filter { it.name.endsWith(".zip", ignoreCase = true) }
return zipAssets.firstOrNull { it.name.equals("texture2d.zip", ignoreCase = true) }
?: zipAssets.firstOrNull()
}
private fun safeOutputFile(baseDir: File, relativePath: String): File? {
val targetFile = File(baseDir, relativePath)
val baseCanonical = baseDir.canonicalFile
val targetCanonical = targetFile.canonicalFile
val basePath = baseCanonical.path + File.separator
if (targetCanonical.path != baseCanonical.path && !targetCanonical.path.startsWith(basePath)) {
return null
}
return targetFile
}
private fun installTextureZip(context: Context, zipFile: File, version: String? = null) {
ZipFile(zipFile).use { zip ->
val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip")
val installedVersion = version ?: readTextureVersion(zip, root)
?: throw IOException("texture_version.txt is empty")
val targetDir = textureDir(context)
val tempDir = File(context.filesDir, "$TEXTURE_DIR.tmp")
if (tempDir.exists()) {
tempDir.deleteRecursively()
}
tempDir.mkdirs()
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
val name = entry.name.replace('\\', '/').trimStart('/')
val relativePath = if (root.containsTexture2dDir) {
val rootPath = "${root.prefix}texture2d/"
if (!name.startsWith(rootPath)) continue
name.substring(rootPath.length)
} else {
if (!name.startsWith(root.prefix)) continue
name.substring(root.prefix.length)
}
if (relativePath.isEmpty()) continue
val targetFile = safeOutputFile(tempDir, relativePath) ?: continue
if (entry.isDirectory) {
targetFile.mkdirs()
continue
}
targetFile.parentFile?.mkdirs()
zip.getInputStream(entry).use { input ->
FileOutputStream(targetFile).use { output ->
input.copyTo(output)
}
}
}
File(tempDir, VERSION_FILE_NAME).writeText(installedVersion)
if (targetDir.exists()) {
targetDir.deleteRecursively()
}
if (!tempDir.renameTo(targetDir)) {
tempDir.copyRecursively(targetDir, overwrite = true)
tempDir.deleteRecursively()
}
}
}
fun updateTextureFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File,
deleteAfterUpdate: Boolean) {
try {
GakumasHookMain.showToast("Updating texture files from zip...")
val tempZipFile = File(filesDir, "texture_update.zip")
activity.contentResolver.openInputStream(zipFileUri).use { input ->
if (input == null) {
Log.e(TAG, "texture zip openInputStream failed.")
GakumasHookMain.showToast("Texture update file not found.")
return
}
tempZipFile.outputStream().use { output ->
input.copyTo(output)
}
}
installTextureZip(activity, tempZipFile)
Log.i(TAG, "Texture zip installed into ${File(filesDir, TEXTURE_DIR).absolutePath}, " +
"version=${getInstalledVersion(activity)}")
tempZipFile.delete()
if (deleteAfterUpdate) {
activity.contentResolver.delete(zipFileUri, null, null)
}
GakumasHookMain.showToast("Texture update success.")
}
catch (e: java.io.FileNotFoundException) {
Log.i(TAG, "updateTextureFilesFromZip - file not found: $e")
GakumasHookMain.showToast("Texture update file not found.")
}
catch (e: Exception) {
Log.e(TAG, "updateTextureFilesFromZip failed: $e")
GakumasHookMain.showToast("Texture update failed: $e")
}
}
fun checkUpdateTextureAssets(context: Context, apiURL: String,
onFailed: (Int, String) -> Unit,
onResult: (data: GithubReleaseModel, localVersion: String?) -> Unit) {
runCatching {
val request = Request.Builder()
.url(apiURL)
.build()
FileDownloader.requestGet(request, object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailed(-1, e.toString())
}
override fun onResponse(call: Call, response: Response) {
runCatching {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val responseBody = response.body?.string()
if (responseBody != null) {
val json = Json { ignoreUnknownKeys = true }
val releaseData = json.decodeFromString<GithubReleaseModel>(responseBody)
onResult(releaseData, getLocalVersion(context))
} else {
onFailed(-1, "Response body is null")
}
}
}.onFailure { e ->
Log.e(TAG, "checkUpdateTextureAssets failed", e)
onFailed(-1, e.toString())
}
}
})
}.onFailure { e ->
Log.e(TAG, "checkUpdateTextureAssets failed", e)
onFailed(-1, e.toString())
}
}
fun updateTextureAssets(context: Context, apiURL: String,
deleteAfterUpdate: Boolean,
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
onFailed: (Int, String) -> Unit,
onSuccess: (version: String, changed: Boolean) -> Unit) {
runCatching {
val request = Request.Builder()
.url(apiURL)
.build()
FileDownloader.requestGet(request, object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailed(-1, e.toString())
}
override fun onResponse(call: Call, response: Response) {
runCatching {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val responseBody = response.body?.string()
if (responseBody != null) {
val json = Json { ignoreUnknownKeys = true }
val releaseData = json.decodeFromString<GithubReleaseModel>(responseBody)
val releaseVersion = releaseData.tag_name
val localVersion = getLocalVersion(context)
if (releaseVersion == localVersion) {
onSuccess(releaseVersion, false)
return
}
val zipAsset = findTextureZipAsset(releaseData)
if (zipAsset == null) {
onFailed(-1, "No zip asset found")
return
}
val cacheFile = getCachedZipFile(context)
FileDownloader.downloadFileToPath(zipAsset.browser_download_url,
cacheFile,
onDownload, {
runCatching {
saveCachedVersion(context, releaseVersion)
val zipVersion = validateTextureZip(cacheFile)
if (zipVersion != releaseVersion) {
cacheFile.delete()
File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME").delete()
throw IOException("texture_version.txt ($zipVersion) differs from release tag ($releaseVersion)")
}
Log.i(TAG, "Texture zip cached: ${cacheFile.absolutePath}, " +
"size=${cacheFile.length()}, version=$releaseVersion")
onSuccess(releaseVersion, true)
}.onFailure { e ->
Log.e(TAG, "save texture zip failed", e)
onFailed(-1, e.toString())
}
},
onFailed)
} else {
onFailed(-1, "Response body is null")
}
}
}.onFailure { e ->
Log.e(TAG, "updateTextureAssets failed", e)
onFailed(-1, e.toString())
}
}
})
}.onFailure { e ->
Log.e(TAG, "updateTextureAssets failed", e)
onFailed(-1, e.toString())
}
}
}
@@ -8,17 +8,23 @@ data class GakumasConfig (
var enabled: Boolean = true,
var lazyInit: Boolean = true,
var replaceFont: Boolean = true,
var replaceTexture: Boolean = true,
var textTest: Boolean = false,
var useMasterTrans: Boolean = true,
var dumpText: Boolean = false,
var dumpRuntimeTexture: Boolean = false,
var gameOrientation: Int = 0,
var forceExportResource: Boolean = false,
var enableFreeCamera: Boolean = false,
var targetFrameRate: Int = 0,
var unlockAllLive: Boolean = false,
var unlockAllLiveCostume: Boolean = false,
var enableLiveCustomeDress: Boolean = false,
var liveCustomeHeadId: String = "",
var liveCustomeCostumeId: String = "",
var loginAsIOS: Boolean = false,
var useCustomeGraphicSettings: Boolean = false,
var renderScale: Float = 0.77f,
var qualitySettingsLevel: Int = 3,
@@ -19,6 +19,9 @@ data class ProgramConfig(
var useAPIAssets: Boolean = false,
var useAPIAssetsURL: String = "",
var delRemoteAfterUpdate: Boolean = true,
var useAPITextureAssets: Boolean = false,
var useAPITextureAssetsURL: String = "",
var delTextureRemoteAfterUpdate: Boolean = true,
var cleanLocalAssets: Boolean = false,
// var localAPIAssetsVersion: String = "",
@@ -43,11 +43,12 @@ class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Bool
class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig,
private val localResourceVersion: String) : ViewModelProvider.Factory {
private val localResourceVersion: String,
private val localTextureResourceVersion: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ProgramConfigViewModel(initialValue, localResourceVersion) as T
return ProgramConfigViewModel(initialValue, localResourceVersion, localTextureResourceVersion) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
@@ -62,7 +63,8 @@ data class ConfirmStateModel(
var p: Boolean = false
)
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() {
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String,
initLocalTextureResourceVersion: String) : ViewModel() {
val configState = MutableStateFlow(initValue)
val config: StateFlow<ProgramConfig> = configState.asStateFlow()
@@ -81,6 +83,18 @@ class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion:
val errorStringState = MutableStateFlow("")
val errorString: StateFlow<String> = errorStringState.asStateFlow()
val textureDownloadProgressState = MutableStateFlow(-1f)
val textureDownloadProgress: StateFlow<Float> = textureDownloadProgressState.asStateFlow()
val textureDownloadAbleState = MutableStateFlow(true)
val textureDownloadAble: StateFlow<Boolean> = textureDownloadAbleState.asStateFlow()
val localTextureResourceVersionState = MutableStateFlow(initLocalTextureResourceVersion)
val localTextureResourceVersion: StateFlow<String> = localTextureResourceVersionState.asStateFlow()
val textureErrorStringState = MutableStateFlow("")
val textureErrorString: StateFlow<String> = textureErrorStringState.asStateFlow()
val mainUIConfirmState = MutableStateFlow(ConfirmStateModel())
val mainUIConfirm: StateFlow<ConfirmStateModel> = mainUIConfirmState.asStateFlow()
}
@@ -49,14 +49,14 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
previewData: GakumasConfig? = null) {
val imagePainter = painterResource(R.drawable.bg_pattern)
var versionInfo by remember {
mutableStateOf(context?.getVersion() ?: listOf("", "Unknown"))
mutableStateOf(context?.getVersion() ?: listOf("", "Unknown", "Unknown"))
}
// val config = getConfigState(context, previewData)
val confirmState by getMainUIConfirmState(context, null)
val programConfig by getProgramConfigState(context)
LaunchedEffect(programConfig) {
versionInfo = context?.getVersion() ?: listOf("", "Unknown")
versionInfo = context?.getVersion() ?: listOf("", "Unknown", "Unknown")
}
Box(
@@ -79,6 +79,7 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
) {
Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp)
Text(text = "Assets version: ${versionInfo[1]}", fontSize = 13.sp)
Text(text = "Texture version: ${versionInfo[2]}", fontSize = 13.sp)
SettingsTabs(modifier, listOf(stringResource(R.string.about), stringResource(R.string.home),
stringResource(R.string.advanced_settings)),
@@ -75,6 +75,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
item {
GakuGroupBox(modifier, stringResource(R.string.debug_settings)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
GakuSwitch(modifier, stringResource(R.string.useMasterDBTrans), checked = config.value.useMasterTrans) {
v -> context?.onUseMasterTransChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.text_hook_test_mode), checked = config.value.textTest) {
v -> context?.onTextTestChanged(v)
}
@@ -83,9 +87,17 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
v -> context?.onDumpTextChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.dump_runtime_texture), checked = config.value.dumpRuntimeTexture) {
v -> context?.onDumpRuntimeTextureChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
v -> context?.onForceExportResourceChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.login_as_ios), checked = config.value.loginAsIOS) {
v -> context?.onLoginAsIOSChanged(v)
}
}
}
@@ -353,6 +365,12 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
checked = config.value.unlockAllLive) {
v -> context?.onUnlockAllLiveChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.unlockAllLiveCostume),
checked = config.value.unlockAllLiveCostume) {
v -> context?.onUnlockAllLiveCostumeChanged(v)
}
/*
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
@@ -377,7 +395,7 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
value = config.value.liveCustomeCostumeId,
onValueChange = { c -> context?.onLiveCustomeCostumeIdChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.live_custome_dress_id)) }
)
)*/
}
}
}
@@ -43,11 +43,16 @@ import io.github.chinosk.gakumas.localify.getProgramConfigState
import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState
import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState
import io.github.chinosk.gakumas.localify.getProgramDownloadState
import io.github.chinosk.gakumas.localify.getProgramLocalTextureResourceVersionState
import io.github.chinosk.gakumas.localify.getProgramLocalResourceVersionState
import io.github.chinosk.gakumas.localify.getProgramLocalAPIResourceVersionState
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadAbleState
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadErrorStringState
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadState
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
import io.github.chinosk.gakumas.localify.mainUtils.TimeUtils
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel
@@ -75,6 +80,10 @@ fun HomePage(modifier: Modifier = Modifier,
val localResourceVersion by getProgramLocalResourceVersionState(context)
val localAPIResourceVersion by getProgramLocalAPIResourceVersionState(context)
val downloadErrorString by getProgramDownloadErrorStringState(context)
val textureDownloadProgress by getProgramTextureDownloadState(context)
val textureDownloadAble by getProgramTextureDownloadAbleState(context)
val localTextureResourceVersion by getProgramLocalTextureResourceVersionState(context)
val textureDownloadErrorString by getProgramTextureDownloadErrorStringState(context)
var isFirstTimeInThisPage by rememberSaveable { mutableStateOf(true) }
// val scrollState = rememberScrollState()
@@ -131,7 +140,8 @@ fun HomePage(modifier: Modifier = Modifier,
})
}
fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true) {
fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true,
onFinished: (() -> Unit)? = null) {
context?.mainPageAssetsViewDataUpdate(
downloadAbleState = false,
errorString = "",
@@ -139,6 +149,7 @@ fun HomePage(modifier: Modifier = Modifier,
)
if (isZipResource) {
zipResourceDownload()
onFinished?.invoke()
}
else {
RemoteAPIFilesChecker.checkUpdateLocalAssets(context!!,
@@ -150,6 +161,7 @@ fun HomePage(modifier: Modifier = Modifier,
downloadProgressState = -1f
)
context.mainUIConfirmStatUpdate(true, "Error", reason)
onFinished?.invoke()
},
onResult = { data, localVersion ->
if (!isHumanClick) {
@@ -159,6 +171,7 @@ fun HomePage(modifier: Modifier = Modifier,
errorString = "",
downloadProgressState = -1f
)
onFinished?.invoke()
return@checkUpdateLocalAssets
}
}
@@ -170,10 +183,13 @@ fun HomePage(modifier: Modifier = Modifier,
onDownload = { progress, _, _ ->
context.mainPageAssetsViewDataUpdate(downloadProgressState = progress)
},
onFailed = { _, reason -> context.mainPageAssetsViewDataUpdate(
downloadAbleState = true,
errorString = reason,
)},
onFailed = { _, reason ->
context.mainPageAssetsViewDataUpdate(
downloadAbleState = true,
errorString = reason,
)
onFinished?.invoke()
},
onSuccess = { saveFile, releaseVersion ->
context.mainPageAssetsViewDataUpdate(
downloadAbleState = true,
@@ -185,6 +201,7 @@ fun HomePage(modifier: Modifier = Modifier,
)
context.saveProgramConfig()
Log.d(TAG, "saved: $releaseVersion $saveFile")
onFinished?.invoke()
})
},
onCancel = {
@@ -193,12 +210,92 @@ fun HomePage(modifier: Modifier = Modifier,
errorString = "",
downloadProgressState = -1f
)
onFinished?.invoke()
}
)
})
}
}
fun startTextureResourceUpdate() {
context?.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = false,
errorString = "",
downloadProgressState = -1f
)
TextureResourceUpdater.updateTextureAssets(context!!,
programConfig.value.useAPITextureAssetsURL,
programConfig.value.delTextureRemoteAfterUpdate,
onDownload = { progress, _, _ ->
context.mainPageTextureAssetsViewDataUpdate(downloadProgressState = progress)
},
onFailed = { _, reason ->
context.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = true,
errorString = reason,
)
},
onSuccess = { releaseVersion, changed ->
context.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = true,
errorString = "",
downloadProgressState = -1f,
localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context)
?: releaseVersion
)
context.saveProgramConfig()
Log.d(TAG, "texture resource update finished: $releaseVersion changed=$changed")
})
}
fun onClickTextureDownload(isHumanClick: Boolean = true) {
context?.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = false,
errorString = "",
downloadProgressState = -1f
)
TextureResourceUpdater.checkUpdateTextureAssets(context!!,
programConfig.value.useAPITextureAssetsURL,
onFailed = { _, reason ->
context.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = true,
errorString = reason,
downloadProgressState = -1f
)
if (isHumanClick) {
context.mainUIConfirmStatUpdate(true, "Error", reason)
}
},
onResult = { data, localVersion ->
if (!isHumanClick) {
if (data.tag_name == localVersion) {
context.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = true,
errorString = "",
downloadProgressState = -1f,
localTextureResourceVersion = localVersion
)
return@checkUpdateTextureAssets
}
}
context.mainUIConfirmStatUpdate(true, context.getString(R.string.texture_resource_update),
"${data.name}\n$localVersion -> ${data.tag_name}\n${data.body}\n\n${TimeUtils.convertIsoToLocalTime(data.published_at)}",
onConfirm = {
resourceSettingsViewModel.expanded = true
startTextureResourceUpdate()
},
onCancel = {
context.mainPageTextureAssetsViewDataUpdate(
downloadAbleState = true,
errorString = "",
downloadProgressState = -1f
)
}
)
})
}
LaunchedEffect(Unit) {
try {
if (context == null) return@LaunchedEffect
@@ -206,9 +303,25 @@ fun HomePage(modifier: Modifier = Modifier,
context.mainPageAssetsViewDataUpdate(
localAPIResourceVersion = localAPIResVer
)
context.mainPageTextureAssetsViewDataUpdate(
localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context)
)
if (isFirstTimeInThisPage) {
if (programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()) {
onClickDownload(false, false)
val shouldCheckResource =
programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()
val shouldCheckTexture = config.value.replaceTexture &&
programConfig.value.useAPITextureAssets &&
programConfig.value.useAPITextureAssetsURL.isNotEmpty()
if (shouldCheckResource) {
onClickDownload(false, false) {
if (shouldCheckTexture) {
onClickTextureDownload(false)
}
}
}
else if (shouldCheckTexture) {
onClickTextureDownload(false)
}
}
}
@@ -240,6 +353,10 @@ fun HomePage(modifier: Modifier = Modifier,
v -> context?.onReplaceFontChanged(v)
}
GakuSwitch(modifier, stringResource(R.string.replace_texture), checked = config.value.replaceTexture) {
v -> context?.onReplaceTextureChanged(v)
}
}
}
Spacer(Modifier.height(6.dp))
@@ -314,8 +431,8 @@ fun HomePage(modifier: Modifier = Modifier,
fontSize = 14f,
value = programConfig.value.useAPIAssetsURL,
onValueChange = { c -> context?.onPUseAPIAssetsURLChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.api_addr)) },
keyboardOptions = keyboardOptionsNumber)
label = { Text(stringResource(R.string.api_addr)) }
)
if (downloadAble) {
GakuButton(modifier = modifier
@@ -411,8 +528,8 @@ fun HomePage(modifier: Modifier = Modifier,
fontSize = 14f,
value = programConfig.value.transRemoteZipUrl,
onValueChange = { c -> context?.onPTransRemoteZipUrlChanged(c, 0, 0, 0)},
label = { Text(stringResource(id = R.string.resource_url)) },
keyboardOptions = keyboardOptionsNumber)
label = { Text(stringResource(id = R.string.resource_url)) }
)
if (downloadAble) {
GakuButton(modifier = modifier
@@ -467,6 +584,104 @@ fun HomePage(modifier: Modifier = Modifier,
}
}
if (config.value.replaceTexture) {
item {
HorizontalDivider(
thickness = 1.dp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
}
item {
GakuSwitch(modifier = modifier.padding(start = 8.dp, end = 8.dp),
checked = programConfig.value.useAPITextureAssets,
text = stringResource(R.string.check_texture_resource_from_api)
) { v -> context?.onPUseAPITextureAssetsChanged(v) }
CollapsibleBox(modifier = modifier.graphicsLayer(clip = false),
expandState = programConfig.value.useAPITextureAssets,
collapsedHeight = 0.dp,
innerPaddingLeftRight = 8.dp,
showExpand = false
) {
GakuSwitch(modifier = modifier,
checked = programConfig.value.delTextureRemoteAfterUpdate,
text = stringResource(id = R.string.del_remote_after_update)
) { v -> context?.onPDelTextureRemoteAfterUpdateChanged(v) }
LazyColumn(modifier = modifier
.sizeIn(maxHeight = screenH),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Row(modifier = modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically) {
GakuTextInput(modifier = modifier
.height(45.dp)
.padding(end = 8.dp)
.fillMaxWidth()
.weight(1f),
fontSize = 14f,
value = programConfig.value.useAPITextureAssetsURL,
onValueChange = { c -> context?.onPUseAPITextureAssetsURLChanged(c, 0, 0, 0)},
label = { Text(stringResource(R.string.texture_api_addr)) }
)
if (textureDownloadAble) {
GakuButton(modifier = modifier
.height(40.dp)
.sizeIn(minWidth = 80.dp),
text = stringResource(R.string.check_update),
onClick = { onClickTextureDownload(true) })
}
else {
GakuButton(modifier = modifier
.height(40.dp)
.sizeIn(minWidth = 80.dp),
text = stringResource(id = R.string.cancel), onClick = {
FileDownloader.cancel()
})
}
}
}
if (textureDownloadProgress >= 0) {
item {
GakuProgressBar(progress = textureDownloadProgress,
isError = textureDownloadErrorString.isNotEmpty())
}
}
if (textureDownloadErrorString.isNotEmpty()) {
item {
Text(text = textureDownloadErrorString, color = Color(0xFFE2041B))
}
}
item {
Text(modifier = Modifier
.fillMaxWidth()
.clickable {
context?.let {
it.mainPageTextureAssetsViewDataUpdate(
localTextureResourceVersion = TextureResourceUpdater
.getLocalVersion(it)
)
}
}, text = "${stringResource(R.string.downloaded_texture_resource_version)}: $localTextureResourceVersion")
}
item {
Spacer(Modifier.height(0.dp))
}
}
}
}
}
}
}
}
+247 -13
View File
@@ -1,4 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="abc_action_bar_home_description">ホームに戻る</string>
<string name="abc_action_bar_up_description">前に戻る</string>
<string name="abc_action_menu_overflow_description">その他のオプション</string>
<string name="abc_action_mode_done">完了</string>
<string name="abc_activity_chooser_view_see_all">すべて表示</string>
<string name="abc_activitychooserview_choose_application">アプリの選択</string>
<string name="abc_capital_off">OFF</string>
<string name="abc_capital_on">ON</string>
<string name="abc_menu_alt_shortcut_label">Alt+</string>
<string name="abc_menu_ctrl_shortcut_label">Ctrl+</string>
<string name="abc_menu_delete_shortcut_label">Delete</string>
<string name="abc_menu_enter_shortcut_label">Enter</string>
<string name="abc_menu_function_shortcut_label">Function+</string>
<string name="abc_menu_meta_shortcut_label">Meta+</string>
<string name="abc_menu_shift_shortcut_label">Shift+</string>
<string name="abc_menu_space_shortcut_label">Space</string>
<string name="abc_menu_sym_shortcut_label">Sym+</string>
<string name="abc_prepend_shortcut_label">Menu+</string>
<string name="abc_search_hint">検索…</string>
<string name="abc_searchview_description_clear">検索キーワードを削除</string>
<string name="abc_searchview_description_query">検索キーワード</string>
<string name="abc_searchview_description_search">検索</string>
<string name="abc_searchview_description_submit">検索キーワードを送信</string>
<string name="abc_searchview_description_voice">音声検索</string>
<string name="abc_shareactionprovider_share_with">共有</string>
<string name="abc_shareactionprovider_share_with_application">%sと共有</string>
<string name="abc_toolbar_collapse_description">折りたたむ</string>
<string name="about">情報</string>
<string name="about_about_p1">このプラグインは完全に無料で提供されます。このプラグインで料金を支払ってしまった場合は、販売者に報告をしてください。</string>
<string name="about_about_p2">プラグインの QQ グループ: 975854705</string>
@@ -8,8 +36,10 @@
<string name="about_warn_p2">外部プラグインは関連する TOS に違反するため、自己責任でご使用ください。</string>
<string name="about_warn_title">警告</string>
<string name="advanced_settings">高度な設定</string>
<string name="api_addr">APIアドレス (GitHub の最新リリース API)</string>
<string name="androidx_startup">androidx.startup</string>
<string name="api_addr">API アドレス (GitHub 最新リリース API)</string>
<string name="app_name">Gakumas Localify</string>
<string name="appbar_scrolling_view_behavior">com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior</string>
<string name="average">平均</string>
<string name="axisx_x">X 軸.x</string>
<string name="axisx_y">X 軸.y</string>
@@ -18,15 +48,28 @@
<string name="axisz_x">Z 軸.x</string>
<string name="axisz_y">Z 軸.y</string>
<string name="basic_settings">基本設定</string>
<string name="bottom_sheet_behavior">com.google.android.material.bottomsheet.BottomSheetBehavior</string>
<string name="bottomsheet_action_collapse">ボトムシートを閉じる</string>
<string name="bottomsheet_action_expand">ボトムシートを開く</string>
<string name="bottomsheet_action_expand_halfway">下半分を展開</string>
<string name="bottomsheet_drag_handle_clicked">ハンドルをダブルタップしてドラッグ</string>
<string name="bottomsheet_drag_handle_content_description">ドラッグハンドル</string>
<string name="breast_param">胸のパラメーター</string>
<string name="breast_scale">胸の大きさ</string>
<string name="call_notification_answer_action">応答</string>
<string name="call_notification_answer_video_action">動画</string>
<string name="call_notification_decline_action">拒否</string>
<string name="call_notification_hang_up_action">通話終了</string>
<string name="call_notification_incoming_text">着信</string>
<string name="call_notification_ongoing_text">通話中</string>
<string name="call_notification_screening_text">着信をスクリーニング中</string>
<string name="camera_settings">カメラ設定</string>
<string name="cancel">キャンセル</string>
<string name="character_counter_content_description">%1$d の %2$d に入力された文字</string>
<string name="character_counter_overflowed_content_description">文字制限が %2$d 文字中、 %1$d 文字を超えています</string>
<string name="character_counter_pattern">%1$d/%2$d</string>
<string name="check_built_in_resource">内蔵アセットのアップデートを確認</string>
<string name="check_resource_from_api">リソースアップデートを API から確認</string>
<string name="check_built_in_resource">内蔵アセットの更新を確認</string>
<string name="check_resource_from_api">リソースの更新を API から確認</string>
<string name="check_update">確認</string>
<string name="clear_text_end_icon_content_description">テキストを消去</string>
<string name="close_drawer">ナビゲーションメニューを閉じる</string>
@@ -37,7 +80,7 @@
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
<string name="default_error_message">入力が無効です</string>
<string name="default_popup_window_title">ポップアップウィンドウ</string>
<string name="del_remote_after_update">キャッシュファイルをアップデート後に削除</string>
<string name="del_remote_after_update">キャッシュファイルを更新後に削除</string>
<string name="delete_plugin_resource">プラグインリソースを削除</string>
<string name="download">ダウンロード</string>
<string name="downloaded_resource_version">ダウンロードされたバージョン</string>
@@ -48,17 +91,21 @@
<string name="error_a11y_label">エラー: 無効</string>
<string name="error_icon_content_description">エラー</string>
<string name="export_text">テキストをエクスポート</string>
<string name="dump_runtime_texture">実行時テクスチャをダンプ</string>
<string name="exposed_dropdown_menu_content_description">ドロップダウンメニューを表示</string>
<string name="force_export_resource">リソースのアップデートを強制する</string>
<string name="fab_transformation_scrim_behavior">com.google.android.material.transformation.FabTransformationScrimBehavior</string>
<string name="fab_transformation_sheet_behavior">com.google.android.material.transformation.FabTransformationSheetBehavior</string>
<string name="force_export_resource">リソースの更新を強制する</string>
<string name="gakumas_localify">Gakumas Localify</string>
<string name="game_patch">ゲームパッチ</string>
<string name="graphic_settings">グラフィック設定</string>
<string name="hide_bottom_view_on_scroll_behavior">com.google.android.material.behavior.HideBottomViewOnScrollBehavior</string>
<string name="hign"></string>
<string name="home">ホーム</string>
<string name="home_shizuku_warning">一部の機能が使用できません</string>
<string name="icon_content_description">ダイアログアイコン</string>
<string name="in_progress">実行中</string>
<string name="indeterminate">部分的にチェック済み</string>
<string name="indeterminate">部分的に確認済み</string>
<string name="install">インストール</string>
<string name="installing">インストール中</string>
<string name="invalid_zip_file">無効なファイル</string>
@@ -69,9 +116,168 @@
<string name="liveUseCustomeDress">ライブのキャラクターをカスタム</string>
<string name="live_costume_head_id">ライブのカスタムヘッド ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">ライブ衣装のカスタム ID (例: hski-cstm-0002)</string>
<string name="login_as_ios">iOS としてログイン</string>
<string name="low"></string>
<string name="m3_exceed_max_badge_text_suffix">%1$s%2$s</string>
<string name="m3_ref_typeface_brand_medium">sans-serif-medium</string>
<string name="m3_ref_typeface_brand_regular">sans-serif</string>
<string name="m3_ref_typeface_plain_medium">sans-serif-medium</string>
<string name="m3_ref_typeface_plain_regular">sans-serif</string>
<string name="m3_sys_motion_easing_emphasized">path(M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1)</string>
<string name="m3_sys_motion_easing_emphasized_accelerate">cubic-bezier(0.3, 0, 0.8, 0.2)</string>
<string name="m3_sys_motion_easing_emphasized_decelerate">cubic-bezier(0.1, 0.7, 0.1, 1)</string>
<string name="m3_sys_motion_easing_emphasized_path_data">M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1</string>
<string name="m3_sys_motion_easing_legacy">cubic-bezier(0.4, 0, 0.2, 1)</string>
<string name="m3_sys_motion_easing_legacy_accelerate">cubic-bezier(0.4, 0, 1, 1)</string>
<string name="m3_sys_motion_easing_legacy_decelerate">cubic-bezier(0, 0, 0.2, 1)</string>
<string name="m3_sys_motion_easing_linear">cubic-bezier(0, 0, 1, 1)</string>
<string name="m3_sys_motion_easing_standard">cubic-bezier(0.2, 0, 0, 1)</string>
<string name="m3_sys_motion_easing_standard_accelerate">cubic-bezier(0.3, 0, 1, 1)</string>
<string name="m3_sys_motion_easing_standard_decelerate">cubic-bezier(0, 0, 0, 1)</string>
<string name="m3c_bottom_sheet_collapse_description">ボトムシートを折りたたみます</string>
<string name="m3c_bottom_sheet_dismiss_description">ボトムシートを閉じます</string>
<string name="m3c_bottom_sheet_drag_handle_description">ドラッグハンドル</string>
<string name="m3c_bottom_sheet_expand_description">ボトムシートを開きます</string>
<string name="m3c_bottom_sheet_pane_title">ボトムシート</string>
<string name="m3c_date_input_headline">入力された日付</string>
<string name="m3c_date_input_headline_description">入力された日付: %1$s</string>
<string name="m3c_date_input_invalid_for_pattern">想定パターンと一致しない日付: %1$s</string>
<string name="m3c_date_input_invalid_not_allowed">許可されない日付: %1$s</string>
<string name="m3c_date_input_invalid_year_range">想定される年の範囲(%1$s~%2$s)から日付が外れています</string>
<string name="m3c_date_input_label">日付</string>
<string name="m3c_date_input_no_input_description">なし</string>
<string name="m3c_date_input_title">日付の選択</string>
<string name="m3c_date_picker_headline">選択した日付</string>
<string name="m3c_date_picker_headline_description">現在の選択: %1$s</string>
<string name="m3c_date_picker_navigate_to_year_description">年に移動 %1$s</string>
<string name="m3c_date_picker_no_selection_description">なし</string>
<string name="m3c_date_picker_scroll_to_earlier_years">これより前の年を表示するにはスクロールしてください</string>
<string name="m3c_date_picker_scroll_to_later_years">これより後の年を表示するにはスクロールしてください</string>
<string name="m3c_date_picker_switch_to_calendar_mode">カレンダー入力モードに切り替え</string>
<string name="m3c_date_picker_switch_to_day_selection">スワイプして年を選択するか、タップして日付の選択に戻ります</string>
<string name="m3c_date_picker_switch_to_input_mode">テキスト入力モードに切り替え</string>
<string name="m3c_date_picker_switch_to_next_month">翌月に変更</string>
<string name="m3c_date_picker_switch_to_previous_month">前月に変更</string>
<string name="m3c_date_picker_switch_to_year_selection">年の選択に切り替え</string>
<string name="m3c_date_picker_title">日付の選択</string>
<string name="m3c_date_picker_today_description">今日</string>
<string name="m3c_date_picker_year_picker_pane_title">年の選択ツールの表示</string>
<string name="m3c_date_range_input_invalid_range_input">入力された期間は無効です</string>
<string name="m3c_date_range_input_title">日付の入力</string>
<string name="m3c_date_range_picker_day_in_range">範囲内</string>
<string name="m3c_date_range_picker_end_headline">終了日</string>
<string name="m3c_date_range_picker_scroll_to_next_month">翌月を表示するにはスクロールしてください</string>
<string name="m3c_date_range_picker_scroll_to_previous_month">前月を表示するにはスクロールしてください</string>
<string name="m3c_date_range_picker_start_headline">開始日</string>
<string name="m3c_date_range_picker_title">日付の選択</string>
<string name="m3c_dialog">ダイアログ</string>
<string name="m3c_dropdown_menu_collapsed">閉じています</string>
<string name="m3c_dropdown_menu_expanded">開いています</string>
<string name="m3c_search_bar_search">検索</string>
<string name="m3c_snackbar_dismiss">閉じる</string>
<string name="m3c_suggestions_available">検索候補は次のとおりです</string>
<string name="m3c_time_picker_am">AM</string>
<string name="m3c_time_picker_hour">時間</string>
<string name="m3c_time_picker_hour_24h_suffix">%1$d 時間</string>
<string name="m3c_time_picker_hour_selection">時刻を選択</string>
<string name="m3c_time_picker_hour_suffix">"%1$d 時"</string>
<string name="m3c_time_picker_hour_text_field">(時間単位)</string>
<string name="m3c_time_picker_minute"></string>
<string name="m3c_time_picker_minute_selection">分を選択</string>
<string name="m3c_time_picker_minute_suffix">%1$d 分</string>
<string name="m3c_time_picker_minute_text_field">(分単位)</string>
<string name="m3c_time_picker_period_toggle_description">午前または午後を選択</string>
<string name="m3c_time_picker_pm">PM</string>
<string name="m3c_tooltip_long_press_label">ツールチップを表示</string>
<string name="m3c_tooltip_pane_description">ツールチップ</string>
<string name="material_clock_display_divider">:</string>
<string name="material_clock_toggle_content_description">午前または午後を選択</string>
<string name="material_hour_24h_suffix">%1$s 時間</string>
<string name="material_hour_selection">時刻を選択してください</string>
<string name="material_hour_suffix">"%1$s時"</string>
<string name="material_minute_selection">分を選択</string>
<string name="material_minute_suffix">%1$s分</string>
<string name="material_motion_easing_accelerated">cubic-bezier(0.4, 0.0, 1.0, 1.0)</string>
<string name="material_motion_easing_decelerated">cubic-bezier(0.0, 0.0, 0.2, 1.0)</string>
<string name="material_motion_easing_emphasized">path(M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1)</string>
<string name="material_motion_easing_linear">cubic-bezier(0.0, 0.0, 1.0, 1.0)</string>
<string name="material_motion_easing_standard">cubic-bezier(0.4, 0.0, 0.2, 1.0)</string>
<string name="material_slider_range_end">Range end</string>
<string name="material_slider_range_start">Range start</string>
<string name="material_slider_value">Value</string>
<string name="material_timepicker_am">AM</string>
<string name="material_timepicker_clock_mode_description">時刻を時計で入力するモードに切り替えます。</string>
<string name="material_timepicker_hour">時間</string>
<string name="material_timepicker_minute"></string>
<string name="material_timepicker_pm">PM</string>
<string name="material_timepicker_select_time">時間を選択</string>
<string name="material_timepicker_text_input_mode_description">時刻をテキストで入力するモードに切り替えます。</string>
<string name="max_high">ウルトラ</string>
<string name="middle"></string>
<string name="mtrl_badge_numberless_content_description">新しい通知</string>
<string name="mtrl_checkbox_button_icon_path_checked">M14,18.2 11.4,15.6 10,17 14,21 22,13 20.6,11.6z</string>
<string name="mtrl_checkbox_button_icon_path_group_name">icon</string>
<string name="mtrl_checkbox_button_icon_path_indeterminate">M13.4,15 11,15 11,17 13.4,17 21,17 21,15z</string>
<string name="mtrl_checkbox_button_icon_path_name">icon path</string>
<string name="mtrl_checkbox_button_path_checked">M23,7H9C7.9,7,7,7.9,7,9v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9C25,7.9,24.1,7,23,7z</string>
<string name="mtrl_checkbox_button_path_group_name">button</string>
<string name="mtrl_checkbox_button_path_name">button path</string>
<string name="mtrl_checkbox_button_path_unchecked">M23,7H9C7.9,7,7,7.9,7,9v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9C25,7.9,24.1,7,23,7z M23,23H9V9h14V23z</string>
<string name="mtrl_checkbox_state_description_checked">オン</string>
<string name="mtrl_checkbox_state_description_indeterminate">一部オン</string>
<string name="mtrl_checkbox_state_description_unchecked">オフ</string>
<string name="mtrl_chip_close_icon_content_description">%1$s を削除します</string>
<string name="mtrl_exceed_max_badge_number_content_description">%1$d 件以上の新しい通知</string>
<string name="mtrl_exceed_max_badge_number_suffix">%1$d%2$s</string>
<string name="mtrl_picker_a11y_next_month">翌月に変更</string>
<string name="mtrl_picker_a11y_prev_month">前月に変更</string>
<string name="mtrl_picker_announce_current_range_selection">開始日の選択: %1$s – 終了日の選択: %2$s</string>
<string name="mtrl_picker_announce_current_selection">現在の選択: %1$s</string>
<string name="mtrl_picker_announce_current_selection_none">なし</string>
<string name="mtrl_picker_cancel">キャンセル</string>
<string name="mtrl_picker_confirm">OK</string>
<string name="mtrl_picker_date_header_selected">%1$s</string>
<string name="mtrl_picker_date_header_title">日付を選択してください</string>
<string name="mtrl_picker_date_header_unselected">選択した日付</string>
<string name="mtrl_picker_day_of_week_column_header">%1$s</string>
<string name="mtrl_picker_end_date_description">終了日 %1$s</string>
<string name="mtrl_picker_invalid_format">形式が無効です。</string>
<string name="mtrl_picker_invalid_format_example">例: %1$s</string>
<string name="mtrl_picker_invalid_format_use">使用: %1$s</string>
<string name="mtrl_picker_invalid_range">範囲が無効です。</string>
<string name="mtrl_picker_navigate_to_current_year_description">現在の年(%1$d)に移動</string>
<string name="mtrl_picker_navigate_to_year_description">%1$d 年に移動</string>
<string name="mtrl_picker_out_of_range">範囲外: %1$s</string>
<string name="mtrl_picker_range_header_only_end_selected">開始日~%1$s</string>
<string name="mtrl_picker_range_header_only_start_selected">%1$s~終了日</string>
<string name="mtrl_picker_range_header_selected">%1$s%2$s</string>
<string name="mtrl_picker_range_header_title">期間を選択してください</string>
<string name="mtrl_picker_range_header_unselected">開始日~終了日</string>
<string name="mtrl_picker_save">保存</string>
<string name="mtrl_picker_start_date_description">開始日 %1$s</string>
<string name="mtrl_picker_text_input_date_hint">日付</string>
<string name="mtrl_picker_text_input_date_range_end_hint">終了日</string>
<string name="mtrl_picker_text_input_date_range_start_hint">開始日</string>
<string name="mtrl_picker_text_input_day_abbr">d</string>
<string name="mtrl_picker_text_input_month_abbr">m</string>
<string name="mtrl_picker_text_input_year_abbr">y</string>
<string name="mtrl_picker_today_description">今日(%1$s</string>
<string name="mtrl_picker_toggle_to_calendar_input_mode">カレンダー入力モードに切り替え</string>
<string name="mtrl_picker_toggle_to_day_selection">タップするとカレンダー表示に切り替わります</string>
<string name="mtrl_picker_toggle_to_text_input_mode">テキスト入力モードに切り替え</string>
<string name="mtrl_picker_toggle_to_year_selection">タップすると年表示に切り替わります</string>
<string name="mtrl_switch_thumb_group_name">circle_group</string>
<string name="mtrl_switch_thumb_path_checked">M4,16 A12,12 0 0,1 16,4 H16 A12,12 0 0,1 16,28 H16 A12,12 0 0,1 4,16</string>
<string name="mtrl_switch_thumb_path_morphing">M0,16 A11,11 0 0,1 11,5 H21 A11,11 0 0,1 21,27 H11 A11,11 0 0,1 0,16</string>
<string name="mtrl_switch_thumb_path_name">circle</string>
<string name="mtrl_switch_thumb_path_pressed">M2,16 A14,14 0 0,1 16,2 H16 A14,14 0 0,1 16,30 H16 A14,14 0 0,1 2,16</string>
<string name="mtrl_switch_thumb_path_unchecked">M8,16 A8,8 0 0,1 16,8 H16 A8,8 0 0,1 16,24 H16 A8,8 0 0,1 8,16</string>
<string name="mtrl_switch_track_decoration_path">M1,16 A15,15 0 0,1 16,1 H36 A15,15 0 0,1 36,31 H16 A15,15 0 0,1 1,16</string>
<string name="mtrl_switch_track_path">M0,16 A16,16 0 0,1 16,0 H36 A16,16 0 0,1 36,32 H16 A16,16 0 0,1 0,16</string>
<string name="mtrl_timepicker_cancel">キャンセル</string>
<string name="mtrl_timepicker_confirm">OK</string>
<string name="navigation_menu">ナビゲーションメニュー</string>
<string name="not_selected">未選択</string>
<string name="off">OFF</string>
<string name="ok">OK</string>
<string name="on">ON</string>
@@ -83,12 +289,21 @@
<string name="patch_debuggable">デバッグを可能にする</string>
<string name="patch_finished">パッチが完了しました。インストールをしますか?</string>
<string name="patch_integrated">統合</string>
<string name="patch_integrated_desc">"モジュールを埋め込んだ状態なアプリでパッチを当てます。\nパッチを適用したアプリは LSPatch Manager なしで実行できますが、動的に管理はできません。\n統合パッチが適用されたアプリは、LSPatch Manager がインストールされていないデバイスでも使用が可能です。"</string>
<string name="patch_integrated_desc">"モジュールを埋め込んだ状態なアプリでパッチを当てます。
パッチを適用したアプリは LSPatch Manager なしで実行できますが、動的に管理はできません。
統合パッチが適用されたアプリは、LSPatch Manager がインストールされていないデバイスでも使用が可能です。"</string>
<string name="patch_local">ローカル</string>
<string name="patch_local_desc">"モジュールを埋め込まずにアプリにパッチを当てます。\nXposed スコープは再パッチなしで動的に変更が可能です。\nローカルでのパッチを当てたアプリは、ローカルのデバイスでのみ実行可能です。"</string>
<string name="patch_local_desc">"モジュールを埋め込まずにアプリにパッチを当てます。
Xposed スコープは再パッチなしで動的に変更が可能です。
ローカルでのパッチを当てたアプリは、ローカルのデバイスでのみ実行可能です。"</string>
<string name="patch_mode">パッチモード</string>
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</string>
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。\n個人データのバックアップを設定済みであることを確認してください。"</string>
<string name="patch_uninstall_text">"署名が異なるため、パッチをインストールする前に元となるアプリをアンインストールする必要があります。
個人データのバックアップを設定済みであることを確認してください。"</string>
<string name="path_password_eye">M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z</string>
<string name="path_password_eye_mask_strike_through">M2,4.27 L19.73,22 L22.27,19.46 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
<string name="path_password_eye_mask_visible">M2,4.27 L2,4.27 L4.54,1.73 L4.54,1.73 L4.54,1 L23,1 L23,23 L1,23 L1,4.27 Z</string>
<string name="path_password_strike_through">M3.27,4.27 L19.74,20.74</string>
<string name="pendulum">揺れ</string>
<string name="pendulumrange">揺れの範囲</string>
<string name="plugin_code">プラグインのコード</string>
@@ -97,27 +312,41 @@
<string name="range_start">範囲の開始</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="replace_font">フォントを置換する</string>
<string name="replace_texture">テクスチャを置換する</string>
<string name="reserve_patched">パッチ済みの APK を予約する</string>
<string name="resource_settings">リソース設定</string>
<string name="resource_url">リソース URL</string>
<string name="rootweight">ルートウイト</string>
<string name="rootweight">ルートウイト</string>
<string name="search_menu_title">検索</string>
<string name="searchbar_scrolling_view_behavior">com.google.android.material.search.SearchBar$ScrollingViewBehavior</string>
<string name="searchview_clear_text_content_description">テキストを消去</string>
<string name="searchview_navigation_content_description">戻る</string>
<string name="selected">選択済み</string>
<string name="setFpsTitle">最大 FPS (0 はオリジナルの設定を使用します)</string>
<string name="shizuku_available">Shizuku サービスが有効です</string>
<string name="shizuku_unavailable">Shizuku サービスが接続されていません</string>
<string name="side_sheet_accessibility_pane_title">サイドシート</string>
<string name="side_sheet_behavior">com.google.android.material.sidesheet.SideSheetBehavior</string>
<string name="spring">跳ね</string>
<string name="start_game">ゲーム開始 / ホットリロードの設定</string>
<string name="status_bar_notification_info_overflow">999+</string>
<string name="stiffness">剛性</string>
<string name="support_file_types">"対応ファイル:\n単一または複数選択: apk\n単一選択: apks、xapk、zip"</string>
<string name="support_file_types">"対応ファイル:
単一または複数選択: apk
単一選択: apks、xapk、zip"</string>
<string name="switch_role">切り替え</string>
<string name="tab">タブ</string>
<string name="template_percent">%1$d パーセント。</string>
<string name="test_mode_live">テストモード - ライブ</string>
<string name="text_hook_test_mode">テキストフックテストモード</string>
<string name="tooltip_description">ツールチップ</string>
<string name="tooltip_label">ツールチップを表示</string>
<string name="translation_repository">翻訳のリポジトリ</string>
<string name="translation_resource_update">翻訳リソースをアップデート</string>
<string name="translation_resource_update">翻訳リソースを更新</string>
<string name="unlockAllLive">すべてのライブを開放</string>
<string name="unlockAllLiveCostume">すべてのライブ衣装を開放</string>
<string name="useCustomeGraphicSettings">カスタムグラフィック設定を使用する</string>
<string name="useMasterDBTrans">MasterDB のローカライズを有効化</string>
<string name="use_remote_zip_resource">リモート ZIP リソースを使用する</string>
<string name="usearmcorrection">Arm コレクションを使用する</string>
<string name="uselimit_0_1">リミットレンジの倍率 (0 は無制限)</string>
@@ -125,4 +354,9 @@
<string name="usescale">胸の大きさを使用する</string>
<string name="very_high">最高</string>
<string name="warning">警告</string>
</resources>
<string name="check_texture_resource_from_api">API からテクスチャリソースの更新を確認</string>
<string name="texture_api_addr">テクスチャ API アドレス (GitHub 最新リリース API)</string>
<string name="texture_resource_update">テクスチャリソースを更新</string>
<string name="downloaded_texture_resource_version">ダウンロード済みテクスチャバージョン</string>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+11 -1
View File
@@ -3,19 +3,24 @@
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">启用插件 (不可热重载)</string>
<string name="replace_font">替换字体</string>
<string name="replace_texture">替换贴图</string>
<string name="lazy_init">快速初始化(懒加载配置)</string>
<string name="enable_free_camera">启用自由视角(可热重载; 需使用实体键盘)</string>
<string name="start_game">以上述配置启动游戏/重载配置</string>
<string name="setFpsTitle">最大 FPS (0 为保持游戏原设置)</string>
<string name="unlockAllLive">解锁所有 Live</string>
<string name="unlockAllLiveCostume">解锁所有 Live 服装</string>
<string name="liveUseCustomeDress">Live 使用自定义角色</string>
<string name="live_costume_head_id">Live 自定义头部 ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live 自定义服装 ID (例: hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">使用自定义画质设置</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="text_hook_test_mode">文本 hook 测试模式</string>
<string name="useMasterDBTrans">使用 MasterDB 本地化</string>
<string name="export_text">导出文本</string>
<string name="dump_runtime_texture">导出运行时贴图</string>
<string name="force_export_resource">启动后强制导出资源</string>
<string name="login_as_ios">以 iOS 登陆</string>
<string name="max_high">极高</string>
<string name="very_high">超高</string>
<string name="hign"></string>
@@ -83,6 +88,10 @@
<string name="api_addr">API 地址(Github Latest Release API</string>
<string name="check_update">检查更新</string>
<string name="translation_resource_update">翻译资源更新</string>
<string name="check_texture_resource_from_api">从服务器检查贴图资源更新</string>
<string name="texture_api_addr">贴图 API 地址(Github Latest Release API</string>
<string name="texture_resource_update">贴图资源更新</string>
<string name="downloaded_texture_resource_version">已下载贴图资源版本</string>
<string name="game_patch">游戏修补</string>
<string name="patch_mode">修补模式</string>
@@ -102,4 +111,5 @@
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
</resources>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+113
View File
@@ -0,0 +1,113 @@
<resources>
<string name="app_name">Gakumas Localify</string>
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
<string name="replace_font">替換字體</string>
<string name="lazy_init">快速初始化(懶人設定)</string>
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
<string name="unlockAllLive">解鎖所有 Live</string>
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="text_hook_test_mode">文本 hook 測試模式</string>
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
<string name="export_text">導出文本</string>
<string name="dump_runtime_texture">導出運行時貼圖</string>
<string name="force_export_resource">啟動後強制導出資源</string>
<string name="login_as_ios">模擬以 iOS 登入</string>
<string name="max_high">極高</string>
<string name="very_high">超高</string>
<string name="hign"></string>
<string name="middle"></string>
<string name="low"></string>
<string name="orientation_orig">原版</string>
<string name="orientation_portrait">豎屏</string>
<string name="orientation_landscape">橫屏</string>
<string name="orientation_lock">方向鎖定</string>
<string name="enable_breast_param">啟用胸部參數</string>
<string name="damping">阻尼 (Damping)</string>
<string name="stiffness">剛度 (Stiffness)</string>
<string name="spring">彈簧係數 (Spring)</string>
<string name="pendulum">鐘擺係數 (Pendulum)</string>
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
<string name="average">Average</string>
<string name="rootweight">RootWeight</string>
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
<string name="usearmcorrection">使用手臂矯正</string>
<string name="isdirty">IsDirty</string>
<string name="usescale">應用縮放</string>
<string name="breast_scale">胸部縮放倍率</string>
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
<string name="axisx_x">axisX.x</string>
<string name="axisy_x">axisY.x</string>
<string name="axisz_x">axisZ.x</string>
<string name="axisx_y">axisX.y</string>
<string name="axisy_y">axisY.y</string>
<string name="axisz_y">axisZ.y</string>
<string name="basic_settings">基本設定</string>
<string name="graphic_settings">畫面設定</string>
<string name="camera_settings">攝影機設定</string>
<string name="test_mode_live">測試模式 - LIVE</string>
<string name="debug_settings">調試設定</string>
<string name="breast_param">胸部參數</string>
<string name="about">關於</string>
<string name="home">主頁</string>
<string name="advanced_settings">進階設定</string>
<string name="about_warn_title">使用前警告</string>
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
<string name="about_about_title">關於本插件</string>
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
<string name="about_about_p2">插件交流QQ群: 991990192</string>
<string name="project_contribution">項目貢獻</string>
<string name="plugin_code">插件本體</string>
<string name="contributors">貢獻者列表</string>
<string name="translation_repository">譯文倉庫</string>
<string name="resource_settings">資源設定</string>
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
<string name="resource_url">資源地址</string>
<string name="download">下載</string>
<string name="invalid_zip_file">文件解析失敗</string>
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
<string name="cancel">取消</string>
<string name="ok">確定</string>
<string name="downloaded_resource_version">已下載資源版本</string>
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
<string name="warning">注意</string>
<string name="install">安裝</string>
<string name="installing">安裝中</string>
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
<string name="api_addr">API 地址(Github Latest Release API</string>
<string name="check_update">檢查更新</string>
<string name="translation_resource_update">翻譯資源更新</string>
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API</string>
<string name="texture_resource_update">貼圖資源更新</string>
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
<string name="game_patch">遊戲修補</string>
<string name="patch_mode">修補模式</string>
<string name="patch_local">本地模式</string>
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
<string name="patch_integrated">集成模式</string>
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
<string name="shizuku_available">Shizuku 服務可用</string>
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
<string name="home_shizuku_warning">部分功能不可用</string>
<string name="patch_debuggable">可調試</string>
<string name="reserve_patched">安裝時保留修補包</string>
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
<string name="patch_finished">修補完成,是否開始安裝?</string>
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+113
View File
@@ -0,0 +1,113 @@
<resources>
<string name="app_name">Gakumas Localify</string>
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
<string name="replace_font">替換字體</string>
<string name="lazy_init">快速初始化(懶人設定)</string>
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
<string name="unlockAllLive">解鎖所有 Live</string>
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="text_hook_test_mode">文本 hook 測試模式</string>
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
<string name="export_text">導出文本</string>
<string name="dump_runtime_texture">導出運行時貼圖</string>
<string name="force_export_resource">啟動後強制導出資源</string>
<string name="login_as_ios">模擬以 iOS 登入</string>
<string name="max_high">極高</string>
<string name="very_high">超高</string>
<string name="hign"></string>
<string name="middle"></string>
<string name="low"></string>
<string name="orientation_orig">原版</string>
<string name="orientation_portrait">豎屏</string>
<string name="orientation_landscape">橫屏</string>
<string name="orientation_lock">方向鎖定</string>
<string name="enable_breast_param">啟用胸部參數</string>
<string name="damping">阻尼 (Damping)</string>
<string name="stiffness">剛度 (Stiffness)</string>
<string name="spring">彈簧係數 (Spring)</string>
<string name="pendulum">鐘擺係數 (Pendulum)</string>
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
<string name="average">Average</string>
<string name="rootweight">RootWeight</string>
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
<string name="usearmcorrection">使用手臂矯正</string>
<string name="isdirty">IsDirty</string>
<string name="usescale">應用縮放</string>
<string name="breast_scale">胸部縮放倍率</string>
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
<string name="axisx_x">axisX.x</string>
<string name="axisy_x">axisY.x</string>
<string name="axisz_x">axisZ.x</string>
<string name="axisx_y">axisX.y</string>
<string name="axisy_y">axisY.y</string>
<string name="axisz_y">axisZ.y</string>
<string name="basic_settings">基本設定</string>
<string name="graphic_settings">畫面設定</string>
<string name="camera_settings">攝影機設定</string>
<string name="test_mode_live">測試模式 - LIVE</string>
<string name="debug_settings">調試設定</string>
<string name="breast_param">胸部參數</string>
<string name="about">關於</string>
<string name="home">主頁</string>
<string name="advanced_settings">進階設定</string>
<string name="about_warn_title">使用前警告</string>
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
<string name="about_about_title">關於本插件</string>
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
<string name="about_about_p2">插件交流QQ群: 991990192</string>
<string name="project_contribution">項目貢獻</string>
<string name="plugin_code">插件本體</string>
<string name="contributors">貢獻者列表</string>
<string name="translation_repository">譯文倉庫</string>
<string name="resource_settings">資源設定</string>
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
<string name="resource_url">資源地址</string>
<string name="download">下載</string>
<string name="invalid_zip_file">文件解析失敗</string>
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
<string name="cancel">取消</string>
<string name="ok">確定</string>
<string name="downloaded_resource_version">已下載資源版本</string>
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
<string name="warning">注意</string>
<string name="install">安裝</string>
<string name="installing">安裝中</string>
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
<string name="api_addr">API 地址(Github Latest Release API</string>
<string name="check_update">檢查更新</string>
<string name="translation_resource_update">翻譯資源更新</string>
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API</string>
<string name="texture_resource_update">貼圖資源更新</string>
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
<string name="game_patch">遊戲修補</string>
<string name="patch_mode">修補模式</string>
<string name="patch_local">本地模式</string>
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
<string name="patch_integrated">集成模式</string>
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
<string name="shizuku_available">Shizuku 服務可用</string>
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
<string name="home_shizuku_warning">部分功能不可用</string>
<string name="patch_debuggable">可調試</string>
<string name="reserve_patched">安裝時保留修補包</string>
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
<string name="patch_finished">修補完成,是否開始安裝?</string>
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+113
View File
@@ -0,0 +1,113 @@
<resources>
<string name="app_name">Gakumas Localify</string>
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
<string name="replace_font">替換字體</string>
<string name="lazy_init">快速初始化(懶人設定)</string>
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
<string name="unlockAllLive">解鎖所有 Live</string>
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="text_hook_test_mode">文本 hook 測試模式</string>
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
<string name="export_text">導出文本</string>
<string name="dump_runtime_texture">導出運行時貼圖</string>
<string name="force_export_resource">啟動後強制導出資源</string>
<string name="login_as_ios">模擬以 iOS 登入</string>
<string name="max_high">極高</string>
<string name="very_high">超高</string>
<string name="hign"></string>
<string name="middle"></string>
<string name="low"></string>
<string name="orientation_orig">原版</string>
<string name="orientation_portrait">豎屏</string>
<string name="orientation_landscape">橫屏</string>
<string name="orientation_lock">方向鎖定</string>
<string name="enable_breast_param">啟用胸部參數</string>
<string name="damping">阻尼 (Damping)</string>
<string name="stiffness">剛度 (Stiffness)</string>
<string name="spring">彈簧係數 (Spring)</string>
<string name="pendulum">鐘擺係數 (Pendulum)</string>
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
<string name="average">Average</string>
<string name="rootweight">RootWeight</string>
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
<string name="usearmcorrection">使用手臂矯正</string>
<string name="isdirty">IsDirty</string>
<string name="usescale">應用縮放</string>
<string name="breast_scale">胸部縮放倍率</string>
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
<string name="axisx_x">axisX.x</string>
<string name="axisy_x">axisY.x</string>
<string name="axisz_x">axisZ.x</string>
<string name="axisx_y">axisX.y</string>
<string name="axisy_y">axisY.y</string>
<string name="axisz_y">axisZ.y</string>
<string name="basic_settings">基本設定</string>
<string name="graphic_settings">畫面設定</string>
<string name="camera_settings">攝影機設定</string>
<string name="test_mode_live">測試模式 - LIVE</string>
<string name="debug_settings">調試設定</string>
<string name="breast_param">胸部參數</string>
<string name="about">關於</string>
<string name="home">主頁</string>
<string name="advanced_settings">進階設定</string>
<string name="about_warn_title">使用前警告</string>
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
<string name="about_about_title">關於本插件</string>
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
<string name="about_about_p2">插件交流QQ群: 991990192</string>
<string name="project_contribution">項目貢獻</string>
<string name="plugin_code">插件本體</string>
<string name="contributors">貢獻者列表</string>
<string name="translation_repository">譯文倉庫</string>
<string name="resource_settings">資源設定</string>
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
<string name="resource_url">資源地址</string>
<string name="download">下載</string>
<string name="invalid_zip_file">文件解析失敗</string>
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
<string name="cancel">取消</string>
<string name="ok">確定</string>
<string name="downloaded_resource_version">已下載資源版本</string>
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
<string name="warning">注意</string>
<string name="install">安裝</string>
<string name="installing">安裝中</string>
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
<string name="api_addr">API 地址(Github Latest Release API</string>
<string name="check_update">檢查更新</string>
<string name="translation_resource_update">翻譯資源更新</string>
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API</string>
<string name="texture_resource_update">貼圖資源更新</string>
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
<string name="game_patch">遊戲修補</string>
<string name="patch_mode">修補模式</string>
<string name="patch_local">本地模式</string>
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
<string name="patch_integrated">集成模式</string>
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
<string name="shizuku_available">Shizuku 服務可用</string>
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
<string name="home_shizuku_warning">部分功能不可用</string>
<string name="patch_debuggable">可調試</string>
<string name="reserve_patched">安裝時保留修補包</string>
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
<string name="patch_finished">修補完成,是否開始安裝?</string>
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+12 -2
View File
@@ -3,19 +3,24 @@
<string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">Enable Plugin (Not Hot Reloadable)</string>
<string name="replace_font">Replace Font</string>
<string name="replace_texture">Replace Texture</string>
<string name="lazy_init">Fast Initialization (Lazy loading)</string>
<string name="enable_free_camera">Enable Free Camera</string>
<string name="start_game">Start Game / Hot Reload Config</string>
<string name="setFpsTitle">Max FPS (0 is Use Original Settings)</string>
<string name="unlockAllLive">Unlock All Live</string>
<string name="unlockAllLiveCostume">Unlock All Live Costume</string>
<string name="liveUseCustomeDress">Live Custom Character</string>
<string name="live_costume_head_id">Live Custom Head ID (eg. costume_head_hski-cstm-0002)</string>
<string name="live_custome_dress_id">Live Custom Dress ID (eg. hski-cstm-0002)</string>
<string name="useCustomeGraphicSettings">Use Custom Graphics Settings</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="text_hook_test_mode">Text Hook Test Mode</string>
<string name="useMasterDBTrans">Enable MasterDB Localization</string>
<string name="export_text">Export Text</string>
<string name="dump_runtime_texture">Dump Runtime Texture</string>
<string name="force_export_resource">Force Update Resource</string>
<string name="login_as_ios">Login as iOS</string>
<string name="max_high">Ultra</string>
<string name="very_high">Very High</string>
<string name="hign">High</string>
@@ -45,7 +50,7 @@
<string name="axisx_y">axisX.y</string>
<string name="axisy_y">axisY.y</string>
<string name="axisz_y">axisZ.y</string>
<string name="basic_settings">Basic Ssettings</string>
<string name="basic_settings">Basic Settings</string>
<string name="graphic_settings">Graphic Settings</string>
<string name="camera_settings">Camera Settings</string>
<string name="test_mode_live">Test Mode - LIVE</string>
@@ -83,6 +88,10 @@
<string name="api_addr">API AddressGithub Latest Release API</string>
<string name="check_update">Check</string>
<string name="translation_resource_update">Translation Resource Update</string>
<string name="check_texture_resource_from_api">Check Texture Resource Update From API</string>
<string name="texture_api_addr">Texture API Address (Github Latest Release API)</string>
<string name="texture_resource_update">Texture Resource Update</string>
<string name="downloaded_texture_resource_version">Downloaded Texture Version</string>
<string name="game_patch">Game Patch</string>
<string name="patch_mode">Patch Mode</string>
@@ -102,4 +111,5 @@
<string name="about_contributors_asset_file">about_contributors_en.json</string>
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
</resources>
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>
+1 -1
View File
@@ -17,7 +17,7 @@ lifecycle = "2.8.2"
material = "1.12.0"
navigationCompose = "2.7.7"
xdl = "2.1.1"
shadowhook = "1.0.9"
shadowhook = "1.0.10"
serialization="1.7.1"
zip4j = "2.9.1"