40 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
chinosk 8c850ad7db update submodule 2024-11-23 15:19:41 +00:00
chinosk 7bf429336b fix build error, add card name suffixes match 2024-11-22 22:45:52 +00:00
chinosk c7e3d4f718 Add Japanese UI strings by @reindex-ot
Co-authored-by: Re*Index. (ot_inc) <32851879+reindex-ot@users.noreply.github.com>
2024-09-09 16:49:33 +08:00
chinosk b74713be78 update version 2024-09-05 20:04:14 +08:00
chinosk 67945c86dd update submodule 2024-09-05 19:28:06 +08:00
chinosk 06a96a450e update README 2024-09-05 19:15:00 +08:00
chinosk 6e512d9380 Fix game crash (#6) 2024-09-05 19:09:47 +08:00
chinosk f82e73845a delete cache 2024-08-09 20:36:58 +08:00
chinosk 8ddd6f53bc Compatible with Android 10 2024-08-09 20:15:21 +08:00
59 changed files with 5386 additions and 220 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`)
- [ ] 更多类型的文件替换
- [ ] 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"
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"):
+3
View File
@@ -0,0 +1,3 @@
<lint>
<issue id="ExtraTranslation" severity="ignore" />
</lint>
+3
View File
@@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
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;
};
}
}
+32 -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;
}
@@ -521,6 +538,17 @@ namespace GakumasLocal::Local {
return false;
}
// 匹配升级卡名
if (auto plusPos = origText.find_last_not_of('+'); plusPos != std::string::npos) {
const auto noPlusText = origText.substr(0, plusPos + 1);
if (const auto iter = genericText.find(noPlusText); iter != genericText.end()) {
size_t plusCount = origText.length() - (plusPos + 1);
*newStr = iter->second + std::string(plusCount, '+');
return true;
}
}
// fmt 文本
auto fmtText = StringParser::ParseItems::parse(origText, false);
if (fmtText.isValid) {
+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) {
@@ -1,19 +1,29 @@
package io.github.chinosk.gakumas.localify
import android.Manifest
import android.content.ContentValues
import android.content.Context
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import io.github.chinosk.gakumas.localify.mainUtils.IOnShell
import io.github.chinosk.gakumas.localify.mainUtils.LSPatchUtils
@@ -29,11 +39,13 @@ import kotlinx.coroutines.withContext
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermissions
import java.util.concurrent.CountDownLatch
interface PatchCallback {
@@ -99,6 +111,137 @@ class PatchActivity : ComponentActivity() {
private var reservePatchFiles: Boolean = false
var patchCallback: PatchCallback? = null
private val writePermissionLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != RESULT_OK) {
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
finish()
}
}
private val writePermissionLauncherQ = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (!isGranted) {
Toast.makeText(this, "Permission Request Failed.", Toast.LENGTH_SHORT).show()
finish()
}
}
private fun checkAndRequestWritePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
/*
// 针对 API 级别 30 及以上使用 MediaStore.createWriteRequest
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val intentSender = MediaStore.createWriteRequest(contentResolver, listOf(uri)).intentSender
writePermissionLauncher.launch(IntentSenderRequest.Builder(intentSender).build())*/
}
else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// 请求 WRITE_EXTERNAL_STORAGE 权限
writePermissionLauncherQ.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
private fun writeFileToDownloadFolder(
sourceFile: File,
targetFolder: String,
targetFileName: String
): Boolean {
val downloadDirectory = Environment.DIRECTORY_DOWNLOADS
val relativePath = "$downloadDirectory/$targetFolder/"
val resolver = contentResolver
// 检查文件是否已经存在
val existingUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val query = resolver.query(
existingUri,
arrayOf(MediaStore.Files.FileColumns._ID),
"${MediaStore.Files.FileColumns.RELATIVE_PATH}=? AND ${MediaStore.Files.FileColumns.DISPLAY_NAME}=?",
arrayOf(relativePath, targetFileName),
null
)
query?.use {
if (it.moveToFirst()) {
// 如果文件存在,则删除
val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
val deleteUri = MediaStore.Files.getContentUri("external", id)
resolver.delete(deleteUri, null, null)
Log.d(patchTag, "query delete: $deleteUri")
}
}
val contentValues = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, targetFileName)
put(MediaStore.Downloads.MIME_TYPE, "application/octet-stream")
put(MediaStore.Downloads.RELATIVE_PATH, relativePath)
}
var uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
Log.d(patchTag, "insert uri: $uri")
if (uri == null) {
val latch = CountDownLatch(1)
val downloadDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val downloadSaveDirectory = File(downloadDirectory, targetFolder)
val downloadSaveFile = File(downloadSaveDirectory, targetFileName)
MediaScannerConnection.scanFile(this, arrayOf(downloadSaveFile.absolutePath),
null
) { _, _ ->
Log.d(patchTag, "scanFile finished.")
latch.countDown()
}
latch.await()
uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri == null) {
Log.e(patchTag, "uri is still null")
return false
}
}
return try {
resolver.openOutputStream(uri)?.use { outputStream ->
FileInputStream(sourceFile).use { inputStream ->
inputStream.copyTo(outputStream)
}
}
contentValues.clear()
contentValues.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, contentValues, null, null)
true
} catch (e: Exception) {
resolver.delete(uri, null, null)
e.printStackTrace()
false
}
}
private fun deleteFileInDownloadFolder(targetFolder: String, targetFileName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
val selectionArgs =
arrayOf("${Environment.DIRECTORY_DOWNLOADS}/$targetFolder/", targetFileName)
val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
contentResolver.delete(uri, selection, selectionArgs)
}
else {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "$targetFolder/$targetFileName")
if (file.exists()) {
if (file.delete()) {
// Toast.makeText(this, "文件已删除", Toast.LENGTH_SHORT).show()
}
}
}
}
private fun handleSelectedFile(uri: Uri) {
val fileName = uri.path?.substringAfterLast('/')
if (fileName != null) {
@@ -110,6 +253,7 @@ class PatchActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
outputDir = "${filesDir.absolutePath}/output"
// ShizukuApi.init()
checkAndRequestWritePermission()
setContent {
GakumasLocalifyTheme(dynamicColor = false, darkTheme = false) {
@@ -414,7 +558,38 @@ class PatchActivity : ComponentActivity() {
return movedFiles
}
suspend fun installSplitApks(context: Context, apkFiles: List<File>, reservePatchFiles: Boolean,
private fun generateNonce(size: Int): String {
val nonceScope = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
val scopeSize = nonceScope.length
val nonceItem: (Int) -> Char = { nonceScope[(scopeSize * Math.random()).toInt()] }
return Array(size, nonceItem).joinToString("")
}
fun saveFilesToDownload(context: PatchActivity, apkFiles: List<File>, targetFolder: String,
isMove: Boolean): List<String>? {
val ret: MutableList<String> = mutableListOf()
apkFiles.forEach { f ->
val success = context.writeFileToDownloadFolder(f, targetFolder, f.name)
if (success) {
ret.add(f.name)
}
else {
val newName = "${generateNonce(6)}${f.name}"
val success2 = context.writeFileToDownloadFolder(f, targetFolder,
newName)
if (!success2) {
return null
}
ret.add(newName)
}
if (isMove) {
f.delete()
}
}
return ret
}
suspend fun installSplitApks(context: PatchActivity, apkFiles: List<File>, reservePatchFiles: Boolean,
patchCallback: PatchCallback?): Pair<Int, String?> {
Log.i(TAG, "Perform install patched apks")
var status = PackageInstaller.STATUS_FAILURE
@@ -424,13 +599,27 @@ class PatchActivity : ComponentActivity() {
runCatching {
val sdcardPath = Environment.getExternalStorageDirectory().path
val targetDirectory = File(sdcardPath, "Download/gkms_local_patch")
val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
patchCallback?.onLog("Patched files: $savedFiles")
// val savedFiles = saveFileTo(apkFiles, targetDirectory, true, false)
val savedFileNames = saveFilesToDownload(context, apkFiles, "gkms_local_patch", true)
if (savedFileNames == null) {
status = PackageInstaller.STATUS_FAILURE
message = "Save files failed."
return@runCatching
}
// patchCallback?.onLog("Patched files: $savedFiles")
patchCallback?.onLog("Patched files: $apkFiles")
if (!ShizukuApi.isPermissionGranted) {
status = PackageInstaller.STATUS_FAILURE
message = "Shizuku Not Ready."
if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
// if (!reservePatchFiles) savedFiles.forEach { file -> if (file.exists()) file.delete() }
if (!reservePatchFiles) {
savedFileNames.forEach { f ->
context.deleteFileInDownloadFolder("gkms_local_patch", f)
}
}
return@runCatching
}
@@ -455,16 +644,26 @@ class PatchActivity : ComponentActivity() {
val action = if (reservePatchFiles) "cp" else "mv"
val copyFilesCmd: MutableList<String> = mutableListOf()
val movedFiles: MutableList<String> = mutableListOf()
savedFileNames.forEach { file ->
val movedFileName = "\"$installDS/${file}\""
movedFiles.add(movedFileName)
val dlSaveFileName = File(targetDirectory, file)
copyFilesCmd.add("$action ${dlSaveFileName.absolutePath} $movedFileName")
}
/*
savedFiles.forEach { file ->
val movedFileName = "$installDS/${file.name}"
movedFiles.add(movedFileName)
copyFilesCmd.add("$action ${file.absolutePath} $movedFileName")
}
val moveFileCommand = "mkdir $installDS && " +
"chmod 777 $installDS && " +
*/
val createDirCommand = "mkdir $installDS"
val moveFileCommand = "chmod 777 $installDS && " +
copyFilesCmd.joinToString(" && ")
Log.d(TAG, "moveFileCommand: $moveFileCommand")
ShizukuShell(mutableListOf(), createDirCommand, ioShell).exec().destroy()
val cpFileShell = ShizukuShell(mutableListOf(), moveFileCommand, ioShell)
cpFileShell.exec()
cpFileShell.destroy()
@@ -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
@@ -23,7 +23,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
val isBusy: Boolean
get() = mOutput.size > 0 && mOutput[mOutput.size - 1] != "aShell: Finish"
fun exec() {
fun exec(): ShizukuShell {
try {
Log.i(shellTag, "Execute: $mCommand")
shellCallback?.onShellLine(mCommand)
@@ -66,6 +66,7 @@ class ShizukuShell(private var mOutput: MutableList<String>, private var mComman
mProcess!!.waitFor()
} catch (ignored: Exception) {
}
return this
}
fun destroy() {
@@ -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()
}
@@ -27,7 +27,7 @@ import java.io.File
@Composable
fun InstallDiag(context: Context?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
fun InstallDiag(context: PatchActivity?, apkFiles: List<File>, patchCallback: PatchCallback?, reservePatchFiles: Boolean,
onFinish: (Int, String?) -> Unit) {
// val scope = rememberCoroutineScope()
// var uninstallFirst by remember { mutableStateOf(ShizukuApi.isPackageInstalledWithoutPatch(patchApp.app.packageName)) }
@@ -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))
}
}
}
}
}
}
}
}
+362
View File
@@ -0,0 +1,362 @@
<?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>
<string name="about_about_title">このプラグインについて</string>
<string name="about_contributors_asset_file">about_contributors_en.json</string>
<string name="about_warn_p1">このプラグインは学習とコミュニケーションのみを目的としています。</string>
<string name="about_warn_p2">外部プラグインは関連する TOS に違反するため、自己責任でご使用ください。</string>
<string name="about_warn_title">警告</string>
<string name="advanced_settings">高度な設定</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>
<string name="axisy_x">Y 軸.x</string>
<string name="axisy_y">Y 軸.y</string>
<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_update">確認</string>
<string name="clear_text_end_icon_content_description">テキストを消去</string>
<string name="close_drawer">ナビゲーションメニューを閉じる</string>
<string name="close_sheet">シートを閉じる</string>
<string name="contributors">貢献者</string>
<string name="damping">ダンプ中</string>
<string name="debug_settings">デバッグ設定</string>
<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="delete_plugin_resource">プラグインリソースを削除</string>
<string name="download">ダウンロード</string>
<string name="downloaded_resource_version">ダウンロードされたバージョン</string>
<string name="dropdown_menu">ドロップダウンメニュー</string>
<string name="enable_breast_param">胸のパラメーターを有効化</string>
<string name="enable_free_camera">フリーカメラを有効化</string>
<string name="enable_plugin">プラグイン有効化 (ホットリロードなし)</string>
<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="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="install">インストール</string>
<string name="installing">インストール中</string>
<string name="invalid_zip_file">無効なファイル</string>
<string name="invalid_zip_file_warn">このファイルは有効な ZIP 翻訳リソースパックではありません。</string>
<string name="isdirty">IsDirty</string>
<string name="item_view_role_description">タブ</string>
<string name="lazy_init">高速な初期化 (読み込みを遅延)</string>
<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>
<string name="orientation_landscape">横画面</string>
<string name="orientation_lock">画面を固定</string>
<string name="orientation_orig">オリジナル</string>
<string name="orientation_portrait">縦画面</string>
<string name="password_toggle_content_description">パスワードを表示</string>
<string name="patch_debuggable">デバッグを可能にする</string>
<string name="patch_finished">パッチが完了しました。インストールをしますか?</string>
<string name="patch_integrated">統合</string>
<string name="patch_integrated_desc">"モジュールを埋め込んだ状態なアプリでパッチを当てます。
パッチを適用したアプリは LSPatch Manager なしで実行できますが、動的に管理はできません。
統合パッチが適用されたアプリは、LSPatch Manager がインストールされていないデバイスでも使用が可能です。"</string>
<string name="patch_local">ローカル</string>
<string name="patch_local_desc">"モジュールを埋め込まずにアプリにパッチを当てます。
Xposed スコープは再パッチなしで動的に変更が可能です。
ローカルでのパッチを当てたアプリは、ローカルのデバイスでのみ実行可能です。"</string>
<string name="patch_mode">パッチモード</string>
<string name="patch_uninstall_confirm">アンインストールをしてもよろしいですか?</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>
<string name="project_contribution">プロジェクトの貢献者</string>
<string name="range_end">範囲の終了</string>
<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="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">"対応ファイル:
単一または複数選択: 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="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>
<string name="uselimitmultiplier">乗数制限を使用する</string>
<string name="usescale">胸の大きさを使用する</string>
<string name="very_high">最高</string>
<string name="warning">警告</string>
<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"