添加贴图替换功能

This commit is contained in:
pm chihya 2026-05-09 20:37:22 +08:00
parent c7af3e41a5
commit 36a49ba4df
24 changed files with 2059 additions and 30 deletions

2
.gitignore vendored
View File

@ -18,3 +18,5 @@ local.properties
/.kotlin /.kotlin
/app/debug /app/debug
/app/release /app/release
app/src/main/assets/gakumas-local

View File

@ -6,6 +6,19 @@ plugins {
id("kotlin-parcelize") id("kotlin-parcelize")
} }
def sharedVersionFile = file("$projectDir/src/main/cpp/GakumasLocalify/VERSION")
if (!sharedVersionFile.exists()) {
throw new GradleException("Shared version file not found: ${sharedVersionFile}")
}
def sharedVersionName = sharedVersionFile.text.trim()
def sharedVersionMatcher = sharedVersionName =~ /^v?(\d+)\.(\d+)\.(\d+).*$/
if (!sharedVersionMatcher.matches()) {
throw new GradleException("Invalid shared version: ${sharedVersionName}")
}
def sharedVersionCode = sharedVersionMatcher[0][1].toInteger() * 10000 +
sharedVersionMatcher[0][2].toInteger() * 100 +
sharedVersionMatcher[0][3].toInteger()
android { android {
namespace 'io.github.chinosk.gakumas.localify' namespace 'io.github.chinosk.gakumas.localify'
compileSdk 34 compileSdk 34
@ -15,8 +28,8 @@ android {
applicationId "io.github.chinosk.gakumas.localify" applicationId "io.github.chinosk.gakumas.localify"
minSdk 29 minSdk 29
targetSdk 34 targetSdk 34
versionCode 12 versionCode sharedVersionCode
versionName "v3.2.0" versionName sharedVersionName
buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

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

View File

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

View File

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

View File

@ -2,8 +2,10 @@ package io.github.chinosk.gakumas.localify
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.core.content.FileProvider 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.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ProgramConfig 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()) { if (programConfig.useAPIAssetsURL.isEmpty()) {
programConfig.useAPIAssetsURL = getString(R.string.default_assets_check_api) 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 { fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
@ -105,7 +110,7 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
putExtra( putExtra(
"localData", "localData",
getProgramConfigContent(listOf("transRemoteZipUrl", "useAPIAssetsURL", getProgramConfigContent(listOf("transRemoteZipUrl", "useAPIAssetsURL",
"localAPIAssetsVersion", "p"), programConfig) "useAPITextureAssetsURL", "localAPIAssetsVersion", "p"), programConfig)
) )
putExtra("lVerName", version) putExtra("lVerName", version)
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
@ -141,5 +146,30 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) 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) startActivity(intent)
} }

View File

@ -21,6 +21,7 @@ interface ConfigListener {
fun onTextTestChanged(value: Boolean) fun onTextTestChanged(value: Boolean)
fun onUseMasterTransChanged(value: Boolean) fun onUseMasterTransChanged(value: Boolean)
fun onReplaceFontChanged(value: Boolean) fun onReplaceFontChanged(value: Boolean)
fun onReplaceTextureChanged(value: Boolean)
fun onLazyInitChanged(value: Boolean) fun onLazyInitChanged(value: Boolean)
fun onEnableFreeCameraChanged(value: Boolean) fun onEnableFreeCameraChanged(value: Boolean)
fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int) fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int)
@ -39,6 +40,7 @@ interface ConfigListener {
fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int) fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int)
fun onGameOrientationChanged(checkedId: Int) fun onGameOrientationChanged(checkedId: Int)
fun onDumpTextChanged(value: Boolean) fun onDumpTextChanged(value: Boolean)
fun onDumpRuntimeTextureChanged(value: Boolean)
fun onEnableBreastParamChanged(value: Boolean) fun onEnableBreastParamChanged(value: Boolean)
fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int) fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int)
@ -69,8 +71,15 @@ interface ConfigListener {
localResourceVersionState: String? = null, localResourceVersionState: String? = null,
errorString: String? = null, errorString: String? = null,
localAPIResourceVersion: String? = null) localAPIResourceVersion: String? = null)
fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
downloadProgressState: Float? = null,
localTextureResourceVersion: String? = null,
errorString: String? = null)
fun onPUseAPIAssetsChanged(value: Boolean) fun onPUseAPIAssetsChanged(value: Boolean)
fun onPUseAPIAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) 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, fun mainUIConfirmStatUpdate(isShow: Boolean? = null, title: String? = null,
content: String? = null, content: String? = null,
onConfirm: (() -> Unit)? = { mainUIConfirmStatUpdate(isShow = false) }, onConfirm: (() -> Unit)? = { mainUIConfirmStatUpdate(isShow = false) },
@ -129,6 +138,12 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
pushKeyEvent(KeyEvent(1145, 30)) pushKeyEvent(KeyEvent(1145, 30))
} }
override fun onReplaceTextureChanged(value: Boolean) {
config.replaceTexture = value
saveConfig()
pushKeyEvent(KeyEvent(1145, 30))
}
override fun onLazyInitChanged(value: Boolean) { override fun onLazyInitChanged(value: Boolean) {
config.lazyInit = value config.lazyInit = value
saveConfig() saveConfig()
@ -149,6 +164,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveConfig() saveConfig()
} }
override fun onDumpRuntimeTextureChanged(value: Boolean) {
config.dumpRuntimeTexture = value
saveConfig()
}
override fun onEnableFreeCameraChanged(value: Boolean) { override fun onEnableFreeCameraChanged(value: Boolean) {
config.enableFreeCamera = value config.enableFreeCamera = value
saveConfig() saveConfig()
@ -576,6 +596,14 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
localAPIResourceVersion?.let{ programConfigViewModel.localAPIResourceVersionState.value = it } 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) { override fun onPUseAPIAssetsChanged(value: Boolean) {
programConfig.useAPIAssets = value programConfig.useAPIAssets = value
if (value) { if (value) {
@ -591,6 +619,21 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
saveProgramConfig() 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?, override fun mainUIConfirmStatUpdate(isShow: Boolean?, title: String?, content: String?,
onConfirm: (() -> Unit)?, onCancel: (() -> Unit)? onConfirm: (() -> Unit)?, onCancel: (() -> Unit)?
) { ) {

View File

@ -35,6 +35,7 @@ import java.util.Locale
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir 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.mainUtils.json
import io.github.chinosk.gakumas.localify.models.NativeInitProgress import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import io.github.chinosk.gakumas.localify.models.ProgramConfig import io.github.chinosk.gakumas.localify.models.ProgramConfig
@ -53,6 +54,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
private var getConfigError: Exception? = null private var getConfigError: Exception? = null
private var externalFilesChecked: Boolean = false private var externalFilesChecked: Boolean = false
private var textureFilesChecked: Boolean = false
private var gameActivity: Activity? = null private var gameActivity: Activity? = null
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
@ -311,6 +313,25 @@ 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) loadConfig(gkmsData)
Log.d(TAG, "gkmsData: $gkmsData") Log.d(TAG, "gkmsData: $gkmsData")
} }

View File

@ -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.hookUtils.MainKeyEventDispatcher
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker
import io.github.chinosk.gakumas.localify.mainUtils.ShizukuApi 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.mainUtils.json
import io.github.chinosk.gakumas.localify.models.ConfirmStateModel import io.github.chinosk.gakumas.localify.models.ConfirmStateModel
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
@ -79,6 +80,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
fun getVersion(): List<String> { fun getVersion(): List<String> {
var versionText = "" var versionText = ""
var resVersionText = "unknown" var resVersionText = "unknown"
var textureVersionText = "unknown"
try { try {
val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt") val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt")
@ -87,6 +89,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
if (programConfig.useAPIAssets) { if (programConfig.useAPIAssets) {
RemoteAPIFilesChecker.getLocalVersion(this)?.let { resVersionText = it } RemoteAPIFilesChecker.getLocalVersion(this)?.let { resVersionText = it }
} }
TextureResourceUpdater.getLocalVersion(this)?.let { textureVersionText = it }
val packInfo = packageManager.getPackageInfo(packageName, 0) val packInfo = packageManager.getPackageInfo(packageName, 0)
val version = packInfo.versionName val version = packInfo.versionName
@ -95,7 +98,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
} }
catch (_: Exception) {} catch (_: Exception) {}
return listOf(versionText, resVersionText) return listOf(versionText, resVersionText, textureVersionText)
} }
fun openUrl(url: String) { fun openUrl(url: String) {
@ -130,7 +133,8 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java] viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java]
programConfigFactory = ProgramConfigViewModelFactory(programConfig, 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] 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 @Composable
fun getMainUIConfirmState(context: MainActivity?, previewData: ConfirmStateModel? = null): State<ConfirmStateModel> { fun getMainUIConfirmState(context: MainActivity?, previewData: ConfirmStateModel? = null): State<ConfirmStateModel> {
return if (context != null) { return if (context != null) {

View File

@ -45,6 +45,7 @@ object FilesChecker {
if (!pluginBasePath.exists()) { if (!pluginBasePath.exists()) {
pluginBasePath.mkdirs() pluginBasePath.mkdirs()
} }
val skipBuiltInTexture2d = File(filesDir, "$localizationFilesDir/texture2d").exists()
val assets = XModuleResources.createInstance(modulePath, null).assets val assets = XModuleResources.createInstance(modulePath, null).assets
fun forAllAssetFiles( fun forAllAssetFiles(
@ -65,6 +66,12 @@ object FilesChecker {
} }
} }
forAllAssetFiles(localizationFilesDir) { path, file -> forAllAssetFiles(localizationFilesDir) { path, file ->
if ((path == "$localizationFilesDir/texture2d" ||
path.startsWith("$localizationFilesDir/texture2d/")) &&
skipBuiltInTexture2d) {
return@forAllAssetFiles
}
val outFile = File(filesDir, path) val outFile = File(filesDir, path)
if (file == null) { if (file == null) {
outFile.mkdirs() outFile.mkdirs()

View File

@ -3,6 +3,8 @@ package io.github.chinosk.gakumas.localify.mainUtils
import okhttp3.* import okhttp3.*
import java.io.IOException import java.io.IOException
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object FileDownloader { 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() { fun cancel() {
call?.cancel() call?.cancel()
this@FileDownloader.call = null this@FileDownloader.call = null

View File

@ -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())
}
}
}

View File

@ -8,9 +8,11 @@ data class GakumasConfig (
var enabled: Boolean = true, var enabled: Boolean = true,
var lazyInit: Boolean = true, var lazyInit: Boolean = true,
var replaceFont: Boolean = true, var replaceFont: Boolean = true,
var replaceTexture: Boolean = true,
var textTest: Boolean = false, var textTest: Boolean = false,
var useMasterTrans: Boolean = true, var useMasterTrans: Boolean = true,
var dumpText: Boolean = false, var dumpText: Boolean = false,
var dumpRuntimeTexture: Boolean = false,
var gameOrientation: Int = 0, var gameOrientation: Int = 0,
var forceExportResource: Boolean = false, var forceExportResource: Boolean = false,
var enableFreeCamera: Boolean = false, var enableFreeCamera: Boolean = false,

View File

@ -19,6 +19,9 @@ data class ProgramConfig(
var useAPIAssets: Boolean = false, var useAPIAssets: Boolean = false,
var useAPIAssetsURL: String = "", var useAPIAssetsURL: String = "",
var delRemoteAfterUpdate: Boolean = true, var delRemoteAfterUpdate: Boolean = true,
var useAPITextureAssets: Boolean = false,
var useAPITextureAssetsURL: String = "",
var delTextureRemoteAfterUpdate: Boolean = true,
var cleanLocalAssets: Boolean = false, var cleanLocalAssets: Boolean = false,
// var localAPIAssetsVersion: String = "", // var localAPIAssetsVersion: String = "",

View File

@ -43,11 +43,12 @@ class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Bool
class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig, 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 { override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) { if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return ProgramConfigViewModel(initialValue, localResourceVersion) as T return ProgramConfigViewModel(initialValue, localResourceVersion, localTextureResourceVersion) as T
} }
throw IllegalArgumentException("Unknown ViewModel class") throw IllegalArgumentException("Unknown ViewModel class")
} }
@ -62,7 +63,8 @@ data class ConfirmStateModel(
var p: Boolean = false 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 configState = MutableStateFlow(initValue)
val config: StateFlow<ProgramConfig> = configState.asStateFlow() val config: StateFlow<ProgramConfig> = configState.asStateFlow()
@ -81,6 +83,18 @@ class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion:
val errorStringState = MutableStateFlow("") val errorStringState = MutableStateFlow("")
val errorString: StateFlow<String> = errorStringState.asStateFlow() 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 mainUIConfirmState = MutableStateFlow(ConfirmStateModel())
val mainUIConfirm: StateFlow<ConfirmStateModel> = mainUIConfirmState.asStateFlow() val mainUIConfirm: StateFlow<ConfirmStateModel> = mainUIConfirmState.asStateFlow()
} }

View File

@ -49,14 +49,14 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
previewData: GakumasConfig? = null) { previewData: GakumasConfig? = null) {
val imagePainter = painterResource(R.drawable.bg_pattern) val imagePainter = painterResource(R.drawable.bg_pattern)
var versionInfo by remember { var versionInfo by remember {
mutableStateOf(context?.getVersion() ?: listOf("", "Unknown")) mutableStateOf(context?.getVersion() ?: listOf("", "Unknown", "Unknown"))
} }
// val config = getConfigState(context, previewData) // val config = getConfigState(context, previewData)
val confirmState by getMainUIConfirmState(context, null) val confirmState by getMainUIConfirmState(context, null)
val programConfig by getProgramConfigState(context) val programConfig by getProgramConfigState(context)
LaunchedEffect(programConfig) { LaunchedEffect(programConfig) {
versionInfo = context?.getVersion() ?: listOf("", "Unknown") versionInfo = context?.getVersion() ?: listOf("", "Unknown", "Unknown")
} }
Box( Box(
@ -79,6 +79,7 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
) { ) {
Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp) Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp)
Text(text = "Assets version: ${versionInfo[1]}", fontSize = 13.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), SettingsTabs(modifier, listOf(stringResource(R.string.about), stringResource(R.string.home),
stringResource(R.string.advanced_settings)), stringResource(R.string.advanced_settings)),

View File

@ -87,6 +87,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
v -> context?.onDumpTextChanged(v) 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) { GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
v -> context?.onForceExportResourceChanged(v) v -> context?.onForceExportResourceChanged(v)
} }

View File

@ -43,11 +43,16 @@ import io.github.chinosk.gakumas.localify.getProgramConfigState
import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState
import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState
import io.github.chinosk.gakumas.localify.getProgramDownloadState 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.getProgramLocalResourceVersionState
import io.github.chinosk.gakumas.localify.getProgramLocalAPIResourceVersionState 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.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker 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.mainUtils.TimeUtils
import io.github.chinosk.gakumas.localify.models.GakumasConfig import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel
@ -75,6 +80,10 @@ fun HomePage(modifier: Modifier = Modifier,
val localResourceVersion by getProgramLocalResourceVersionState(context) val localResourceVersion by getProgramLocalResourceVersionState(context)
val localAPIResourceVersion by getProgramLocalAPIResourceVersionState(context) val localAPIResourceVersion by getProgramLocalAPIResourceVersionState(context)
val downloadErrorString by getProgramDownloadErrorStringState(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) } var isFirstTimeInThisPage by rememberSaveable { mutableStateOf(true) }
// val scrollState = rememberScrollState() // 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( context?.mainPageAssetsViewDataUpdate(
downloadAbleState = false, downloadAbleState = false,
errorString = "", errorString = "",
@ -139,6 +149,7 @@ fun HomePage(modifier: Modifier = Modifier,
) )
if (isZipResource) { if (isZipResource) {
zipResourceDownload() zipResourceDownload()
onFinished?.invoke()
} }
else { else {
RemoteAPIFilesChecker.checkUpdateLocalAssets(context!!, RemoteAPIFilesChecker.checkUpdateLocalAssets(context!!,
@ -150,6 +161,7 @@ fun HomePage(modifier: Modifier = Modifier,
downloadProgressState = -1f downloadProgressState = -1f
) )
context.mainUIConfirmStatUpdate(true, "Error", reason) context.mainUIConfirmStatUpdate(true, "Error", reason)
onFinished?.invoke()
}, },
onResult = { data, localVersion -> onResult = { data, localVersion ->
if (!isHumanClick) { if (!isHumanClick) {
@ -159,6 +171,7 @@ fun HomePage(modifier: Modifier = Modifier,
errorString = "", errorString = "",
downloadProgressState = -1f downloadProgressState = -1f
) )
onFinished?.invoke()
return@checkUpdateLocalAssets return@checkUpdateLocalAssets
} }
} }
@ -170,10 +183,13 @@ fun HomePage(modifier: Modifier = Modifier,
onDownload = { progress, _, _ -> onDownload = { progress, _, _ ->
context.mainPageAssetsViewDataUpdate(downloadProgressState = progress) context.mainPageAssetsViewDataUpdate(downloadProgressState = progress)
}, },
onFailed = { _, reason -> context.mainPageAssetsViewDataUpdate( onFailed = { _, reason ->
context.mainPageAssetsViewDataUpdate(
downloadAbleState = true, downloadAbleState = true,
errorString = reason, errorString = reason,
)}, )
onFinished?.invoke()
},
onSuccess = { saveFile, releaseVersion -> onSuccess = { saveFile, releaseVersion ->
context.mainPageAssetsViewDataUpdate( context.mainPageAssetsViewDataUpdate(
downloadAbleState = true, downloadAbleState = true,
@ -185,6 +201,7 @@ fun HomePage(modifier: Modifier = Modifier,
) )
context.saveProgramConfig() context.saveProgramConfig()
Log.d(TAG, "saved: $releaseVersion $saveFile") Log.d(TAG, "saved: $releaseVersion $saveFile")
onFinished?.invoke()
}) })
}, },
onCancel = { onCancel = {
@ -193,12 +210,92 @@ fun HomePage(modifier: Modifier = Modifier,
errorString = "", errorString = "",
downloadProgressState = -1f 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) { LaunchedEffect(Unit) {
try { try {
if (context == null) return@LaunchedEffect if (context == null) return@LaunchedEffect
@ -206,9 +303,25 @@ fun HomePage(modifier: Modifier = Modifier,
context.mainPageAssetsViewDataUpdate( context.mainPageAssetsViewDataUpdate(
localAPIResourceVersion = localAPIResVer localAPIResourceVersion = localAPIResVer
) )
context.mainPageTextureAssetsViewDataUpdate(
localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context)
)
if (isFirstTimeInThisPage) { if (isFirstTimeInThisPage) {
if (programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()) { val shouldCheckResource =
onClickDownload(false, false) 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) v -> context?.onReplaceFontChanged(v)
} }
GakuSwitch(modifier, stringResource(R.string.replace_texture), checked = config.value.replaceTexture) {
v -> context?.onReplaceTextureChanged(v)
}
} }
} }
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
@ -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))
}
}
}
}
}
} }
} }
} }

View File

@ -91,6 +91,7 @@
<string name="error_a11y_label">エラー: 無効</string> <string name="error_a11y_label">エラー: 無効</string>
<string name="error_icon_content_description">エラー</string> <string name="error_icon_content_description">エラー</string>
<string name="export_text">テキストをエクスポート</string> <string name="export_text">テキストをエクスポート</string>
<string name="dump_runtime_texture">実行時テクスチャをダンプ</string>
<string name="exposed_dropdown_menu_content_description">ドロップダウンメニューを表示</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_scrim_behavior">com.google.android.material.transformation.FabTransformationScrimBehavior</string>
<string name="fab_transformation_sheet_behavior">com.google.android.material.transformation.FabTransformationSheetBehavior</string> <string name="fab_transformation_sheet_behavior">com.google.android.material.transformation.FabTransformationSheetBehavior</string>
@ -311,6 +312,7 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
<string name="range_start">範囲の開始</string> <string name="range_start">範囲の開始</string>
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string> <string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
<string name="replace_font">フォントを置換する</string> <string name="replace_font">フォントを置換する</string>
<string name="replace_texture">テクスチャを置換する</string>
<string name="reserve_patched">パッチ済みの APK を予約する</string> <string name="reserve_patched">パッチ済みの APK を予約する</string>
<string name="resource_settings">リソース設定</string> <string name="resource_settings">リソース設定</string>
<string name="resource_url">リソース URL</string> <string name="resource_url">リソース URL</string>
@ -352,4 +354,9 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
<string name="usescale">胸の大きさを使用する</string> <string name="usescale">胸の大きさを使用する</string>
<string name="very_high">最高</string> <string name="very_high">最高</string>
<string name="warning">警告</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">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources> </resources>

View File

@ -3,6 +3,7 @@
<string name="gakumas_localify">Gakumas Localify</string> <string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">启用插件 (不可热重载)</string> <string name="enable_plugin">启用插件 (不可热重载)</string>
<string name="replace_font">替换字体</string> <string name="replace_font">替换字体</string>
<string name="replace_texture">替换贴图</string>
<string name="lazy_init">快速初始化(懒加载配置)</string> <string name="lazy_init">快速初始化(懒加载配置)</string>
<string name="enable_free_camera">启用自由视角(可热重载; 需使用实体键盘)</string> <string name="enable_free_camera">启用自由视角(可热重载; 需使用实体键盘)</string>
<string name="start_game">以上述配置启动游戏/重载配置</string> <string name="start_game">以上述配置启动游戏/重载配置</string>
@ -17,6 +18,7 @@
<string name="text_hook_test_mode">文本 hook 测试模式</string> <string name="text_hook_test_mode">文本 hook 测试模式</string>
<string name="useMasterDBTrans">使用 MasterDB 本地化</string> <string name="useMasterDBTrans">使用 MasterDB 本地化</string>
<string name="export_text">导出文本</string> <string name="export_text">导出文本</string>
<string name="dump_runtime_texture">导出运行时贴图</string>
<string name="force_export_resource">启动后强制导出资源</string> <string name="force_export_resource">启动后强制导出资源</string>
<string name="login_as_ios">以 iOS 登陆</string> <string name="login_as_ios">以 iOS 登陆</string>
<string name="max_high">极高</string> <string name="max_high">极高</string>
@ -86,6 +88,10 @@
<string name="api_addr">API 地址Github Latest Release API</string> <string name="api_addr">API 地址Github Latest Release API</string>
<string name="check_update">检查更新</string> <string name="check_update">检查更新</string>
<string name="translation_resource_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="game_patch">游戏修补</string>
<string name="patch_mode">修补模式</string> <string name="patch_mode">修补模式</string>
@ -105,4 +111,5 @@
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</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_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
<string name="default_texture_assets_check_api">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources> </resources>

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">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>

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">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>

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">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources>

View File

@ -3,6 +3,7 @@
<string name="gakumas_localify">Gakumas Localify</string> <string name="gakumas_localify">Gakumas Localify</string>
<string name="enable_plugin">Enable Plugin (Not Hot Reloadable)</string> <string name="enable_plugin">Enable Plugin (Not Hot Reloadable)</string>
<string name="replace_font">Replace Font</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="lazy_init">Fast Initialization (Lazy loading)</string>
<string name="enable_free_camera">Enable Free Camera</string> <string name="enable_free_camera">Enable Free Camera</string>
<string name="start_game">Start Game / Hot Reload Config</string> <string name="start_game">Start Game / Hot Reload Config</string>
@ -17,6 +18,7 @@
<string name="text_hook_test_mode">Text Hook Test Mode</string> <string name="text_hook_test_mode">Text Hook Test Mode</string>
<string name="useMasterDBTrans">Enable MasterDB Localization</string> <string name="useMasterDBTrans">Enable MasterDB Localization</string>
<string name="export_text">Export Text</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="force_export_resource">Force Update Resource</string>
<string name="login_as_ios">Login as iOS</string> <string name="login_as_ios">Login as iOS</string>
<string name="max_high">Ultra</string> <string name="max_high">Ultra</string>
@ -86,6 +88,10 @@
<string name="api_addr">API AddressGithub Latest Release API</string> <string name="api_addr">API AddressGithub Latest Release API</string>
<string name="check_update">Check</string> <string name="check_update">Check</string>
<string name="translation_resource_update">Translation Resource Update</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="game_patch">Game Patch</string>
<string name="patch_mode">Patch Mode</string> <string name="patch_mode">Patch Mode</string>
@ -105,4 +111,5 @@
<string name="about_contributors_asset_file">about_contributors_en.json</string> <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> <string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
<string name="default_texture_assets_check_api">http://texture.gakumas.cn/api/gkms_texture_data</string>
</resources> </resources>