分割hook.cpp文件 #4
|
|
@ -18,3 +18,5 @@ local.properties
|
|||
/.kotlin
|
||||
/app/debug
|
||||
/app/release
|
||||
|
||||
app/src/main/assets/gakumas-local
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ android {
|
|||
minSdk 29
|
||||
targetSdk 34
|
||||
versionCode 12
|
||||
versionName "v3.2.0"
|
||||
versionName "v3.3.1"
|
||||
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||
libMarryKotone.cpp
|
||||
GakumasLocalify/Plugin.cpp
|
||||
GakumasLocalify/Hook.cpp
|
||||
GakumasLocalify/HookTexture.cpp
|
||||
GakumasLocalify/Log.cpp
|
||||
GakumasLocalify/Misc.cpp
|
||||
GakumasLocalify/Local.cpp
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "Hook.h"
|
||||
#include "HookTexture.h"
|
||||
#include "Plugin.h"
|
||||
#include "Log.h"
|
||||
#include "../deps/UnityResolve/UnityResolve.hpp"
|
||||
|
|
@ -13,6 +14,11 @@
|
|||
#include <thread>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <cctype>
|
||||
#include "../platformDefine.hpp"
|
||||
|
||||
#ifdef GKMS_WINDOWS
|
||||
|
|
@ -262,10 +268,21 @@ namespace GakumasLocal::HookMain {
|
|||
|
||||
std::unordered_map<void*, std::string> loadHistory{};
|
||||
|
||||
|
||||
DEFINE_HOOK(void*, AssetBundle_LoadAsset, (void* self, Il2cppString* name, void* type)) {
|
||||
auto result = AssetBundle_LoadAsset_Orig(self, name, type);
|
||||
if (name) {
|
||||
result = ReplaceTextureOrSpriteAsset(result, name->ToString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
DEFINE_HOOK(void*, AssetBundle_LoadAssetAsync, (void* self, Il2cppString* name, void* type)) {
|
||||
// Log::InfoFmt("AssetBundle_LoadAssetAsync: %s, type: %s", name->ToString().c_str());
|
||||
auto ret = AssetBundle_LoadAssetAsync_Orig(self, name, type);
|
||||
if (ret && name) {
|
||||
loadHistory.emplace(ret, name->ToString());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -277,18 +294,62 @@ namespace GakumasLocal::HookMain {
|
|||
|
||||
// const auto assetClass = Il2cppUtils::get_class_from_instance(result);
|
||||
// Log::InfoFmt("AssetBundleRequest_GetResult: %s, type: %s", name.c_str(), static_cast<Il2CppClassHead*>(assetClass)->name);
|
||||
result = ReplaceTextureOrSpriteAsset(result, name);
|
||||
}
|
||||
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)) {
|
||||
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 (path) {
|
||||
ret = ReplaceTextureOrSpriteAsset(ret, path->ToString());
|
||||
}
|
||||
|
||||
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)) {
|
||||
// Log::InfoFmt("SetUpI18n lang: %s, key: %d text: %s", lang->ToString().c_str(), keyComparison, localizationText->ToString().c_str());
|
||||
// TODO 此处为 dump 原文 csv
|
||||
|
|
@ -1688,12 +1749,18 @@ namespace GakumasLocal::HookMain {
|
|||
UnityResolve::Mode::Il2Cpp, Config::lazyInit);
|
||||
#endif
|
||||
|
||||
ADD_HOOK(AssetBundle_LoadAssetAsync, Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)"));
|
||||
ADD_HOOK(AssetBundleRequest_GetResult, Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.AssetBundleRequest::GetResult()"));
|
||||
ADD_HOOK(Resources_Load, Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)"));
|
||||
// Temporarily isolate texture replacement to CanvasRenderer.SetTexture only.
|
||||
// ADD_HOOK(AssetBundle_LoadAsset, ResolveAssetBundleLoadAssetHookAddress());
|
||||
// ADD_HOOK(AssetBundle_LoadAssetAsync, ResolveAssetBundleLoadAssetAsyncHookAddress());
|
||||
// ADD_HOOK(AssetBundleRequest_GetResult, ResolveAssetBundleRequestResultHookAddress());
|
||||
// ADD_HOOK(AssetBundleRequest_get_asset, ResolveAssetBundleRequestAssetHookAddress());
|
||||
// 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",
|
||||
"I18nHelper", "SetUpI18n"));
|
||||
|
|
@ -1987,7 +2054,7 @@ namespace GakumasLocal::HookMain {
|
|||
UnityResolveProgress::startInit = true;
|
||||
UnityResolveProgress::assembliesProgress.total = 2;
|
||||
UnityResolveProgress::assembliesProgress.current = 1;
|
||||
UnityResolveProgress::classProgress.total = 36;
|
||||
UnityResolveProgress::classProgress.total = 43;
|
||||
UnityResolveProgress::classProgress.current = 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,797 @@
|
|||
#include "HookTexture.h"
|
||||
|
||||
#include "Log.h"
|
||||
#include "Il2cppUtils.hpp"
|
||||
#include "Local.h"
|
||||
#include "config/Config.hpp"
|
||||
#include "../deps/UnityResolve/UnityResolve.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace GakumasLocal::HookMain
|
||||
{
|
||||
using Il2cppString = UnityResolve::UnityType::String;
|
||||
|
||||
extern void* (*Sprite_get_texture_Orig)(void* self);
|
||||
|
||||
bool IsNativeObjectAlive(void* obj);
|
||||
|
||||
Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr;
|
||||
Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr;
|
||||
std::unordered_map<std::string, uint32_t> LoadedLocalTextureHandles{};
|
||||
std::unordered_set<std::string> AppliedLocalTextureKeys{};
|
||||
|
||||
Il2cppUtils::Il2CppClassHead* GetTexture2DClass() {
|
||||
if (!Texture2DClass) {
|
||||
const auto textureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Texture2D");
|
||||
if (textureClass) {
|
||||
Texture2DClass = static_cast<Il2cppUtils::Il2CppClassHead*>(textureClass->address);
|
||||
}
|
||||
}
|
||||
return Texture2DClass;
|
||||
}
|
||||
|
||||
Il2cppUtils::Il2CppClassHead* GetSpriteClass() {
|
||||
if (!SpriteClass) {
|
||||
const auto spriteClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite");
|
||||
if (spriteClass) {
|
||||
SpriteClass = static_cast<Il2cppUtils::Il2CppClassHead*>(spriteClass->address);
|
||||
}
|
||||
}
|
||||
return SpriteClass;
|
||||
}
|
||||
|
||||
bool IsTexture2D(void* obj) {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
if (!obj || !textureClass) return false;
|
||||
|
||||
const auto objClass = Il2cppUtils::get_class_from_instance(obj);
|
||||
if (objClass == textureClass) return true;
|
||||
|
||||
return UnityResolve::Invoke<bool>("il2cpp_class_is_assignable_from", textureClass, objClass);
|
||||
}
|
||||
|
||||
bool IsSprite(void* obj) {
|
||||
const auto spriteClass = GetSpriteClass();
|
||||
if (!obj || !spriteClass) return false;
|
||||
|
||||
const auto objClass = Il2cppUtils::get_class_from_instance(obj);
|
||||
if (objClass == spriteClass) return true;
|
||||
|
||||
return UnityResolve::Invoke<bool>("il2cpp_class_is_assignable_from", spriteClass, objClass);
|
||||
}
|
||||
|
||||
Il2cppString* GetObjectName(void* obj) {
|
||||
if (!obj) return nullptr;
|
||||
|
||||
static auto Object_GetName = reinterpret_cast<Il2cppString * (*)(void*)>(
|
||||
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)"));
|
||||
return Object_GetName ? Object_GetName(obj) : nullptr;
|
||||
}
|
||||
|
||||
void SetDontUnloadUnusedAsset(void* obj) {
|
||||
if (!obj) return;
|
||||
|
||||
static auto Object_set_hideFlags = reinterpret_cast<void (*)(void*, int)>(
|
||||
Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Object", "set_hideFlags"));
|
||||
if (Object_set_hideFlags) {
|
||||
Object_set_hideFlags(obj, 32);
|
||||
}
|
||||
}
|
||||
|
||||
void AddTexturePathCandidate(std::vector<std::filesystem::path>& candidates, const std::filesystem::path& path) {
|
||||
if (path.empty()) return;
|
||||
if (std::find(candidates.begin(), candidates.end(), path) == candidates.end()) {
|
||||
candidates.emplace_back(path);
|
||||
}
|
||||
if (!path.has_extension()) {
|
||||
auto pngPath = path;
|
||||
pngPath += ".png";
|
||||
if (std::find(candidates.begin(), candidates.end(), pngPath) == candidates.end()) {
|
||||
candidates.emplace_back(std::move(pngPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TextureCategory {
|
||||
Image,
|
||||
Atlas,
|
||||
Others,
|
||||
};
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
TextureCategory GetTextureCategory(const std::string& textureName) {
|
||||
const auto lowerName = ToLowerAscii(std::filesystem::path(textureName).filename().generic_string());
|
||||
if (lowerName.rfind("img", 0) == 0) {
|
||||
return TextureCategory::Image;
|
||||
}
|
||||
if (lowerName.rfind("sactx", 0) == 0) {
|
||||
return TextureCategory::Atlas;
|
||||
}
|
||||
return TextureCategory::Others;
|
||||
}
|
||||
|
||||
std::filesystem::path GetTextureCategoryDirName(TextureCategory category) {
|
||||
switch (category) {
|
||||
case TextureCategory::Image:
|
||||
return "image";
|
||||
case TextureCategory::Atlas:
|
||||
return "atlas";
|
||||
default:
|
||||
return "others";
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path GetTextureReplaceRoot() {
|
||||
return Local::GetBasePath() / "texture2d";
|
||||
}
|
||||
|
||||
std::filesystem::path GetTextureDumpRoot() {
|
||||
return Local::GetBasePath() / "dump-files" / "texture2d";
|
||||
}
|
||||
|
||||
std::filesystem::path GetTextureReplaceBase(const std::string& textureName) {
|
||||
return GetTextureReplaceRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName));
|
||||
}
|
||||
|
||||
std::filesystem::path GetTextureDumpBase(const std::string& textureName) {
|
||||
return GetTextureDumpRoot() / GetTextureCategoryDirName(GetTextureCategory(textureName));
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitString(const std::string& value, char delimiter) {
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
while (start <= value.size()) {
|
||||
const auto end = value.find(delimiter, start);
|
||||
parts.emplace_back(value.substr(start, end == std::string::npos ? std::string::npos : end - start));
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
void AppendTextureCandidates(std::vector<std::filesystem::path>& target, std::vector<std::filesystem::path>&& source);
|
||||
std::string NormalizeLocalAssetKey(const std::filesystem::path& path);
|
||||
|
||||
bool IsHexHashPart(const std::string& value) {
|
||||
return value.size() == 8 && std::all_of(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F');
|
||||
});
|
||||
}
|
||||
|
||||
std::string GetPortableSactxTextureName(const std::string& objectName) {
|
||||
auto fileName = std::filesystem::path(objectName).filename().generic_string();
|
||||
if (fileName.ends_with(".png")) {
|
||||
fileName.resize(fileName.size() - 4);
|
||||
}
|
||||
|
||||
const auto parts = SplitString(fileName, '-');
|
||||
if (parts.size() < 5 || parts[0] != "sactx" || parts[2].find('x') == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto atlasEnd = IsHexHashPart(parts.back()) ? parts.size() - 1 : parts.size();
|
||||
if (atlasEnd <= 4) return {};
|
||||
|
||||
std::string portableName = parts[0] + "-" + parts[1] + "-" + parts[2];
|
||||
for (size_t i = 4; i < atlasEnd; ++i) {
|
||||
portableName += "-" + parts[i];
|
||||
}
|
||||
return portableName;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::unordered_map<std::string, std::vector<std::filesystem::path>>> RecursiveTexturePathIndex{};
|
||||
|
||||
std::vector<std::filesystem::path> GetRecursiveTextureCandidates(const std::filesystem::path& basePath,
|
||||
const std::string& lookupName) {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
if (lookupName.empty() || !std::filesystem::exists(basePath)) return candidates;
|
||||
|
||||
const auto baseKey = NormalizeLocalAssetKey(basePath);
|
||||
auto& index = RecursiveTexturePathIndex[baseKey];
|
||||
if (index.empty()) {
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(basePath)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
const auto& path = entry.path();
|
||||
if (ToLowerAscii(path.extension().generic_string()) != ".png") continue;
|
||||
|
||||
const auto fileName = path.filename().generic_string();
|
||||
const auto stemName = path.stem().generic_string();
|
||||
index[fileName].emplace_back(path);
|
||||
if (stemName != fileName) {
|
||||
index[stemName].emplace_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (const auto iter = index.find(lookupName); iter != index.end()) {
|
||||
candidates.insert(candidates.end(), iter->second.begin(), iter->second.end());
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetNamedTextureCandidates(const std::filesystem::path& assetName) {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
if (assetName.empty()) return candidates;
|
||||
|
||||
const auto basePath = GetTextureReplaceBase(assetName.filename().generic_string());
|
||||
AddTexturePathCandidate(candidates, basePath / assetName);
|
||||
if (assetName.has_parent_path()) {
|
||||
AddTexturePathCandidate(candidates, basePath / assetName.filename());
|
||||
}
|
||||
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, assetName.filename().generic_string()));
|
||||
|
||||
const auto portableAssetName = GetPortableSactxTextureName(assetName.filename().generic_string());
|
||||
if (!portableAssetName.empty()) {
|
||||
AddTexturePathCandidate(candidates, basePath / portableAssetName);
|
||||
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableAssetName));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetSpriteTextureCandidates(const std::string& objectName) {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
if (objectName.empty()) return candidates;
|
||||
|
||||
auto safeObjectName = objectName;
|
||||
std::replace(safeObjectName.begin(), safeObjectName.end(), '|', '_');
|
||||
|
||||
const auto basePath = GetTextureReplaceBase(safeObjectName);
|
||||
AddTexturePathCandidate(candidates, basePath / objectName);
|
||||
if (safeObjectName != objectName) {
|
||||
AddTexturePathCandidate(candidates, basePath / safeObjectName);
|
||||
}
|
||||
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, safeObjectName));
|
||||
const auto portableObjectName = GetPortableSactxTextureName(safeObjectName);
|
||||
if (!portableObjectName.empty() && portableObjectName != objectName && portableObjectName != safeObjectName) {
|
||||
AddTexturePathCandidate(candidates, basePath / portableObjectName);
|
||||
AppendTextureCandidates(candidates, GetRecursiveTextureCandidates(basePath, portableObjectName));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
void AppendTextureCandidates(std::vector<std::filesystem::path>& target, std::vector<std::filesystem::path>&& source) {
|
||||
target.insert(target.end(),
|
||||
std::make_move_iterator(source.begin()),
|
||||
std::make_move_iterator(source.end()));
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetSpriteAssetTextureCandidates(void* sprite, const std::string& assetName) {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
|
||||
const auto assetPath = std::filesystem::path(assetName);
|
||||
if (!assetName.empty()) {
|
||||
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(assetPath.filename().generic_string()));
|
||||
}
|
||||
|
||||
if (sprite && Sprite_get_texture_Orig) {
|
||||
if (const auto texture = Sprite_get_texture_Orig(sprite)) {
|
||||
if (const auto textureName = GetObjectName(texture)) {
|
||||
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(textureName->ToString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
std::string NormalizeLocalAssetKey(const std::filesystem::path& path) {
|
||||
auto key = path.lexically_normal().generic_string();
|
||||
std::replace(key.begin(), key.end(), '\\', '/');
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string SanitizeDumpPathPart(std::string part) {
|
||||
constexpr std::string_view invalidChars = "<>:\"/\\|?*";
|
||||
if (part.empty() || part == "." || part == "..") return "_";
|
||||
|
||||
for (auto& ch : part) {
|
||||
if (static_cast<unsigned char>(ch) < 32 || invalidChars.find(ch) != std::string_view::npos) {
|
||||
ch = '_';
|
||||
}
|
||||
}
|
||||
|
||||
while (!part.empty() && (part.back() == '.' || part.back() == ' ')) {
|
||||
part.back() = '_';
|
||||
}
|
||||
return part.empty() ? "_" : part;
|
||||
}
|
||||
|
||||
std::filesystem::path SanitizeDumpSubPath(const std::filesystem::path& dumpSubDir) {
|
||||
std::filesystem::path safePath;
|
||||
for (const auto& part : dumpSubDir) {
|
||||
const auto partString = part.generic_string();
|
||||
if (partString.empty() || partString == "." || partString == ".."
|
||||
|| part == part.root_name() || part == part.root_directory()) {
|
||||
continue;
|
||||
}
|
||||
safePath /= SanitizeDumpPathPart(partString);
|
||||
}
|
||||
return safePath;
|
||||
}
|
||||
|
||||
bool DumpTexture2D(void* texture2D) {
|
||||
if (!IsTexture2D(texture2D)) return false;
|
||||
|
||||
const auto objectName = GetObjectName(texture2D);
|
||||
const auto textureName = objectName ? objectName->ToString() : std::string("texture");
|
||||
const auto dumpDir = GetTextureDumpBase(textureName);
|
||||
const auto dumpPath = dumpDir / (SanitizeDumpPathPart(textureName) + ".png");
|
||||
|
||||
if (std::filesystem::exists(dumpPath)) return true;
|
||||
|
||||
static auto Texture2D_get_width = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_width", 0) : nullptr;
|
||||
return method ? reinterpret_cast<int (*)(void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto Texture2D_get_height = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "get_height", 0) : nullptr;
|
||||
return method ? reinterpret_cast<int (*)(void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto Texture2D_ctor = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr;
|
||||
return ctor ? reinterpret_cast<void (*)(void*, int, int)>(ctor->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto Texture2D_ReadPixels = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "ReadPixels", 3) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(void*, UnityResolve::UnityType::Rect, int, int)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto Texture2D_Apply = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto method = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, "Apply", 0) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto RenderTexture_GetTemporary = [] {
|
||||
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
|
||||
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "GetTemporary", 3) : nullptr;
|
||||
return method ? reinterpret_cast<void* (*)(int, int, int)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto RenderTexture_ReleaseTemporary = [] {
|
||||
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
|
||||
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "ReleaseTemporary", 1) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto RenderTexture_get_active = [] {
|
||||
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
|
||||
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "get_active", 0) : nullptr;
|
||||
return method ? reinterpret_cast<void* (*)()>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto RenderTexture_set_active = [] {
|
||||
const auto renderTextureClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "RenderTexture");
|
||||
const auto method = renderTextureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(renderTextureClass->address, "set_active", 1) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto Graphics_Blit = [] {
|
||||
const auto graphicsClass = Il2cppUtils::GetClass("UnityEngine.CoreModule.dll", "UnityEngine", "Graphics");
|
||||
const auto method = graphicsClass ? Il2cppUtils::il2cpp_class_get_method_from_name(graphicsClass->address, "Blit", 2) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(void*, void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto ImageConversion_EncodeToPNG = [] {
|
||||
using EncodeToPNGFn = void* (*)(void*);
|
||||
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.ImageConversion::EncodeToPNG(UnityEngine.Texture2D)")) {
|
||||
return reinterpret_cast<EncodeToPNGFn>(icall);
|
||||
}
|
||||
|
||||
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
|
||||
const auto assembly = UnityResolve::Get(assemblyName);
|
||||
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
|
||||
const auto method = imageConversionClass
|
||||
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "EncodeToPNG", 1)
|
||||
: nullptr;
|
||||
if (method) {
|
||||
return reinterpret_cast<EncodeToPNGFn>(method->methodPointer);
|
||||
}
|
||||
}
|
||||
return static_cast<EncodeToPNGFn>(nullptr);
|
||||
}();
|
||||
static auto File_WriteAllBytes = [] {
|
||||
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
|
||||
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "WriteAllBytes", 2) : nullptr;
|
||||
return method ? reinterpret_cast<void (*)(Il2cppString*, void*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
|
||||
if (!Texture2D_get_width || !Texture2D_get_height || !Texture2D_ctor || !Texture2D_ReadPixels
|
||||
|| !Texture2D_Apply || !RenderTexture_GetTemporary || !RenderTexture_ReleaseTemporary
|
||||
|| !RenderTexture_get_active || !RenderTexture_set_active || !Graphics_Blit
|
||||
|| !ImageConversion_EncodeToPNG || !File_WriteAllBytes) {
|
||||
Log::Error("DumpTexture2D failed: Unity texture dump API not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto width = Texture2D_get_width(texture2D);
|
||||
const auto height = Texture2D_get_height(texture2D);
|
||||
if (width <= 0 || height <= 0) return false;
|
||||
|
||||
void* renderTexture = nullptr;
|
||||
void* readableTexture = nullptr;
|
||||
void* previousActive = nullptr;
|
||||
const auto cleanup = [&] {
|
||||
if (RenderTexture_get_active && RenderTexture_set_active
|
||||
&& (previousActive || RenderTexture_get_active() == renderTexture)) {
|
||||
RenderTexture_set_active(previousActive);
|
||||
}
|
||||
if (renderTexture && RenderTexture_ReleaseTemporary) {
|
||||
RenderTexture_ReleaseTemporary(renderTexture);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
std::filesystem::create_directories(dumpDir);
|
||||
|
||||
renderTexture = RenderTexture_GetTemporary(width, height, 0);
|
||||
if (!renderTexture) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
Graphics_Blit(texture2D, renderTexture);
|
||||
previousActive = RenderTexture_get_active();
|
||||
RenderTexture_set_active(renderTexture);
|
||||
|
||||
readableTexture = UnityResolve::Invoke<void*>("il2cpp_object_new", GetTexture2DClass());
|
||||
if (!readableTexture) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
Texture2D_ctor(readableTexture, width, height);
|
||||
Texture2D_ReadPixels(readableTexture, UnityResolve::UnityType::Rect(0, 0, static_cast<float>(width), static_cast<float>(height)), 0, 0);
|
||||
Texture2D_Apply(readableTexture);
|
||||
|
||||
const auto pngBytes = ImageConversion_EncodeToPNG(readableTexture);
|
||||
if (!pngBytes) {
|
||||
cleanup();
|
||||
return false;
|
||||
}
|
||||
|
||||
File_WriteAllBytes(Il2cppString::New(dumpPath.string()), pngBytes);
|
||||
Log::InfoFmt("Texture dumped: %s", dumpPath.string().c_str());
|
||||
cleanup();
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex) {
|
||||
cleanup();
|
||||
Log::ErrorFmt("DumpTexture2D failed: %s", ex.what());
|
||||
return false;
|
||||
}
|
||||
catch (...) {
|
||||
cleanup();
|
||||
Log::Error("DumpTexture2D failed: unknown error.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void DumpTextureOrSpriteAsset(void* result) {
|
||||
if (!result) return;
|
||||
|
||||
if (IsTexture2D(result)) {
|
||||
DumpTexture2D(result);
|
||||
return;
|
||||
}
|
||||
if (IsSprite(result) && Sprite_get_texture_Orig) {
|
||||
if (const auto texture = Sprite_get_texture_Orig(result)) {
|
||||
DumpTexture2D(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void* LoadLocalTexture2D(const std::filesystem::path& path) {
|
||||
if (!std::filesystem::is_regular_file(path)) return nullptr;
|
||||
|
||||
const auto cacheKey = NormalizeLocalAssetKey(path);
|
||||
if (const auto iter = LoadedLocalTextureHandles.find(cacheKey); iter != LoadedLocalTextureHandles.end()) {
|
||||
const auto cachedTexture = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", iter->second);
|
||||
if (cachedTexture && IsNativeObjectAlive(cachedTexture)) {
|
||||
return cachedTexture;
|
||||
}
|
||||
|
||||
UnityResolve::Invoke<void>("il2cpp_gchandle_free", iter->second);
|
||||
LoadedLocalTextureHandles.erase(iter);
|
||||
}
|
||||
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
if (!textureClass) return nullptr;
|
||||
|
||||
static auto Texture2D_ctor = [] {
|
||||
const auto textureClass = GetTexture2DClass();
|
||||
const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr;
|
||||
return ctor ? reinterpret_cast<void (*)(void*, int, int)>(ctor->methodPointer) : nullptr;
|
||||
}();
|
||||
static auto ImageConversion_LoadImage = [] {
|
||||
using LoadImageFn = bool (*)(void*, void*, bool);
|
||||
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) {
|
||||
return reinterpret_cast<LoadImageFn>(icall);
|
||||
}
|
||||
|
||||
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
|
||||
const auto assembly = UnityResolve::Get(assemblyName);
|
||||
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
|
||||
const auto method = imageConversionClass
|
||||
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3)
|
||||
: nullptr;
|
||||
if (method) {
|
||||
return reinterpret_cast<LoadImageFn>(method->methodPointer);
|
||||
}
|
||||
}
|
||||
return static_cast<LoadImageFn>(nullptr);
|
||||
}();
|
||||
static auto File_ReadAllBytes = [] {
|
||||
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
|
||||
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr;
|
||||
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
|
||||
if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) {
|
||||
Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
|
||||
if (!fileBytes) return nullptr;
|
||||
|
||||
const auto texture = UnityResolve::Invoke<void*>("il2cpp_object_new", textureClass);
|
||||
Texture2D_ctor(texture, 2, 2);
|
||||
if (!ImageConversion_LoadImage(texture, fileBytes, false)) {
|
||||
Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SetDontUnloadUnusedAsset(texture);
|
||||
LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", texture, false));
|
||||
Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str());
|
||||
return texture;
|
||||
}
|
||||
|
||||
void* LoadLocalTexture2DFromCandidates(const std::vector<std::filesystem::path>& candidates) {
|
||||
for (const auto& candidate : candidates) {
|
||||
if (auto texture = LoadLocalTexture2D(candidate)) {
|
||||
return texture;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ApplyLocalImageToTexture2D(void* texture2D, const std::filesystem::path& path) {
|
||||
if (!IsTexture2D(texture2D) || !std::filesystem::is_regular_file(path)) return false;
|
||||
|
||||
auto cacheKey = NormalizeLocalAssetKey(path)
|
||||
+ "|" + std::to_string(reinterpret_cast<std::uintptr_t>(texture2D));
|
||||
if (AppliedLocalTextureKeys.contains(cacheKey)) return true;
|
||||
|
||||
static auto ImageConversion_LoadImage = [] {
|
||||
using LoadImageFn = bool (*)(void*, void*, bool);
|
||||
if (const auto icall = Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.ImageConversion::LoadImage(UnityEngine.Texture2D,System.Byte[],System.Boolean)")) {
|
||||
return reinterpret_cast<LoadImageFn>(icall);
|
||||
}
|
||||
|
||||
for (const auto& assemblyName : {"UnityEngine.ImageConversionModule.dll", "UnityEngine.CoreModule.dll"}) {
|
||||
const auto assembly = UnityResolve::Get(assemblyName);
|
||||
const auto imageConversionClass = assembly ? assembly->Get("ImageConversion", "UnityEngine") : nullptr;
|
||||
const auto method = imageConversionClass
|
||||
? Il2cppUtils::il2cpp_class_get_method_from_name(imageConversionClass->address, "LoadImage", 3)
|
||||
: nullptr;
|
||||
if (method) {
|
||||
return reinterpret_cast<LoadImageFn>(method->methodPointer);
|
||||
}
|
||||
}
|
||||
return static_cast<LoadImageFn>(nullptr);
|
||||
}();
|
||||
static auto File_ReadAllBytes = [] {
|
||||
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
|
||||
const auto method = fileClass ? Il2cppUtils::il2cpp_class_get_method_from_name(fileClass->address, "ReadAllBytes", 1) : nullptr;
|
||||
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
|
||||
}();
|
||||
|
||||
if (!ImageConversion_LoadImage || !File_ReadAllBytes) {
|
||||
Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
|
||||
if (!fileBytes) return false;
|
||||
|
||||
if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) {
|
||||
Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
SetDontUnloadUnusedAsset(texture2D);
|
||||
AppliedLocalTextureKeys.emplace(std::move(cacheKey));
|
||||
Log::InfoFmt("Texture replaced in-place from local file: %s", path.string().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApplyLocalImageToTexture2DFromCandidates(void* texture2D, const std::vector<std::filesystem::path>& candidates) {
|
||||
for (const auto& candidate : candidates) {
|
||||
if (ApplyLocalImageToTexture2D(texture2D, candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ReplaceSpriteTextureInPlace(void* sprite, const std::vector<std::filesystem::path>& candidates) {
|
||||
if (!sprite || !Sprite_get_texture_Orig) return false;
|
||||
|
||||
const auto texture = Sprite_get_texture_Orig(sprite);
|
||||
if (!IsTexture2D(texture)) return false;
|
||||
|
||||
return ApplyLocalImageToTexture2DFromCandidates(texture, candidates);
|
||||
}
|
||||
|
||||
void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName) {
|
||||
if (!Config::replaceTexture && !Config::dumpRuntimeTexture) return result;
|
||||
|
||||
if (Config::dumpRuntimeTexture) {
|
||||
DumpTextureOrSpriteAsset(result);
|
||||
}
|
||||
if (!Config::replaceTexture) return result;
|
||||
|
||||
if (IsSprite(result)) {
|
||||
if (ReplaceSpriteTextureInPlace(result, GetSpriteAssetTextureCandidates(result, assetName))) {
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result && !IsTexture2D(result)) return result;
|
||||
|
||||
if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(std::filesystem::path(assetName)))) {
|
||||
return localTexture;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void* ReplaceTextureOrSpriteByObjectName(void* result) {
|
||||
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !result) return result;
|
||||
|
||||
const auto objectName = GetObjectName(result);
|
||||
if (!objectName) return result;
|
||||
|
||||
const auto assetPath = std::filesystem::path(objectName->ToString());
|
||||
if (Config::dumpRuntimeTexture) {
|
||||
DumpTextureOrSpriteAsset(result);
|
||||
}
|
||||
if (!Config::replaceTexture) return result;
|
||||
|
||||
if (IsSprite(result)) {
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
AppendTextureCandidates(candidates, GetSpriteTextureCandidates(objectName->ToString()));
|
||||
if (ReplaceSpriteTextureInPlace(result, candidates)) {
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (IsTexture2D(result)) {
|
||||
if (auto localTexture = LoadLocalTexture2DFromCandidates(GetNamedTextureCandidates(assetPath))) {
|
||||
return localTexture;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void ReplaceAllAssetTextures(void* allAssets) {
|
||||
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !allAssets) return;
|
||||
|
||||
auto assets = reinterpret_cast<UnityResolve::UnityType::Array<void*>*>(allAssets);
|
||||
for (std::uintptr_t i = 0; i < assets->max_length; ++i) {
|
||||
auto asset = assets->At(static_cast<unsigned int>(i));
|
||||
auto replacedAsset = ReplaceTextureOrSpriteByObjectName(asset);
|
||||
if (replacedAsset != asset) {
|
||||
assets->At(static_cast<unsigned int>(i)) = replacedAsset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void* ReplaceSpriteAssetByTextureName(void* sprite) {
|
||||
if (!Config::replaceTexture || !sprite) return sprite;
|
||||
|
||||
if (!IsSprite(sprite)) {
|
||||
return sprite;
|
||||
}
|
||||
|
||||
if (ReplaceSpriteTextureInPlace(sprite, GetSpriteAssetTextureCandidates(sprite, ""))) {
|
||||
return sprite;
|
||||
}
|
||||
return sprite;
|
||||
}
|
||||
|
||||
void* ReplaceSpriteTexture(void* texture2D) {
|
||||
if ((!Config::replaceTexture && !Config::dumpRuntimeTexture) || !IsTexture2D(texture2D)) return texture2D;
|
||||
|
||||
const auto objectName = GetObjectName(texture2D);
|
||||
if (!objectName) return texture2D;
|
||||
|
||||
if (Config::dumpRuntimeTexture) {
|
||||
DumpTexture2D(texture2D);
|
||||
}
|
||||
if (!Config::replaceTexture) return texture2D;
|
||||
|
||||
if (ApplyLocalImageToTexture2DFromCandidates(texture2D, GetSpriteTextureCandidates(objectName->ToString()))) {
|
||||
return texture2D;
|
||||
}
|
||||
return texture2D;
|
||||
}
|
||||
|
||||
void* ResolveSpriteGetTextureHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) {
|
||||
return addr;
|
||||
}
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture()")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "Sprite", "get_texture");
|
||||
}
|
||||
|
||||
void* ResolveAssetBundleLoadAssetHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle",
|
||||
"LoadAsset_Internal", {"System.String", "System.Type"});
|
||||
}
|
||||
|
||||
void* ResolveAssetBundleLoadAssetAsyncHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.AssetBundle::LoadAssetAsync_Internal(System.String,System.Type)")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundle",
|
||||
"LoadAssetAsync_Internal", {"System.String", "System.Type"});
|
||||
}
|
||||
|
||||
void* ResolveAssetBundleRequestResultHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::GetResult()")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "GetResult");
|
||||
}
|
||||
|
||||
void* ResolveAssetBundleRequestAssetHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_asset()")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_asset");
|
||||
}
|
||||
|
||||
void* ResolveAssetBundleRequestAllAssetsHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundleRequest::get_allAssets()")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.AssetBundleModule.dll", "UnityEngine", "AssetBundleRequest", "get_allAssets");
|
||||
}
|
||||
|
||||
void* ResolveResourcesLoadHookAddress() {
|
||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall(
|
||||
"UnityEngine.ResourcesAPIInternal::Load(System.String,System.Type)")) {
|
||||
return addr;
|
||||
}
|
||||
return Il2cppUtils::GetMethodPointer("UnityEngine.CoreModule.dll", "UnityEngine", "ResourcesAPIInternal",
|
||||
"Load", {"System.String", "System.Type"});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
#ifndef GAKUMAS_LOCALIFY_HOOK_TEXTURE_H
|
||||
#define GAKUMAS_LOCALIFY_HOOK_TEXTURE_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace GakumasLocal::HookMain
|
||||
{
|
||||
void* ReplaceTextureOrSpriteAsset(void* result, const std::string& assetName);
|
||||
void* ReplaceTextureOrSpriteByObjectName(void* result);
|
||||
void ReplaceAllAssetTextures(void* allAssets);
|
||||
void* ReplaceSpriteAssetByTextureName(void* sprite);
|
||||
void* ReplaceSpriteTexture(void* texture2D);
|
||||
|
||||
void* ResolveSpriteGetTextureHookAddress();
|
||||
void* ResolveAssetBundleLoadAssetHookAddress();
|
||||
void* ResolveAssetBundleLoadAssetAsyncHookAddress();
|
||||
void* ResolveAssetBundleRequestResultHookAddress();
|
||||
void* ResolveAssetBundleRequestAssetHookAddress();
|
||||
void* ResolveAssetBundleRequestAllAssetsHookAddress();
|
||||
void* ResolveResourcesLoadHookAddress();
|
||||
}
|
||||
|
||||
#endif // GAKUMAS_LOCALIFY_HOOK_TEXTURE_H
|
||||
|
|
@ -11,11 +11,13 @@ namespace GakumasLocal::Config {
|
|||
bool enabled = true;
|
||||
bool lazyInit = true;
|
||||
bool replaceFont = true;
|
||||
bool replaceTexture = true;
|
||||
bool forceExportResource = true;
|
||||
bool textTest = false;
|
||||
bool useMasterTrans = true;
|
||||
int gameOrientation = 0;
|
||||
bool dumpText = false;
|
||||
bool dumpRuntimeTexture = false;
|
||||
bool enableFreeCamera = false;
|
||||
int targetFrameRate = 0;
|
||||
bool unlockAllLive = false;
|
||||
|
|
@ -66,11 +68,13 @@ namespace GakumasLocal::Config {
|
|||
GetConfigItem(enabled);
|
||||
GetConfigItem(lazyInit);
|
||||
GetConfigItem(replaceFont);
|
||||
GetConfigItem(replaceTexture);
|
||||
GetConfigItem(forceExportResource);
|
||||
GetConfigItem(gameOrientation);
|
||||
GetConfigItem(textTest);
|
||||
GetConfigItem(useMasterTrans);
|
||||
GetConfigItem(dumpText);
|
||||
GetConfigItem(dumpRuntimeTexture);
|
||||
GetConfigItem(targetFrameRate);
|
||||
GetConfigItem(enableFreeCamera);
|
||||
GetConfigItem(unlockAllLive);
|
||||
|
|
@ -122,11 +126,13 @@ namespace GakumasLocal::Config {
|
|||
SetConfigItem(enabled);
|
||||
SetConfigItem(lazyInit);
|
||||
SetConfigItem(replaceFont);
|
||||
SetConfigItem(replaceTexture);
|
||||
SetConfigItem(forceExportResource);
|
||||
SetConfigItem(gameOrientation);
|
||||
SetConfigItem(textTest);
|
||||
SetConfigItem(useMasterTrans);
|
||||
SetConfigItem(dumpText);
|
||||
SetConfigItem(dumpRuntimeTexture);
|
||||
SetConfigItem(targetFrameRate);
|
||||
SetConfigItem(enableFreeCamera);
|
||||
SetConfigItem(unlockAllLive);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ namespace GakumasLocal::Config {
|
|||
extern bool enabled;
|
||||
extern bool lazyInit;
|
||||
extern bool replaceFont;
|
||||
extern bool replaceTexture;
|
||||
extern bool forceExportResource;
|
||||
extern int gameOrientation;
|
||||
extern bool textTest;
|
||||
extern bool useMasterTrans;
|
||||
extern bool dumpText;
|
||||
extern bool dumpRuntimeTexture;
|
||||
extern bool enableFreeCamera;
|
||||
extern int targetFrameRate;
|
||||
extern bool unlockAllLive;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ package io.github.chinosk.gakumas.localify
|
|||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
|
|
@ -77,6 +79,9 @@ fun <T> T.loadConfig() where T : Activity, T : IHasConfigItems {
|
|||
if (programConfig.useAPIAssetsURL.isEmpty()) {
|
||||
programConfig.useAPIAssetsURL = getString(R.string.default_assets_check_api)
|
||||
}
|
||||
if (programConfig.useAPITextureAssetsURL.isEmpty()) {
|
||||
programConfig.useAPITextureAssetsURL = getString(R.string.default_texture_assets_check_api)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
|
||||
|
|
@ -105,7 +110,7 @@ fun <T> T.onClickStartGame() where T : Activity, T : IHasConfigItems {
|
|||
putExtra(
|
||||
"localData",
|
||||
getProgramConfigContent(listOf("transRemoteZipUrl", "useAPIAssetsURL",
|
||||
"localAPIAssetsVersion", "p"), programConfig)
|
||||
"useAPITextureAssetsURL", "localAPIAssetsVersion", "p"), programConfig)
|
||||
)
|
||||
putExtra("lVerName", version)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ interface ConfigListener {
|
|||
fun onTextTestChanged(value: Boolean)
|
||||
fun onUseMasterTransChanged(value: Boolean)
|
||||
fun onReplaceFontChanged(value: Boolean)
|
||||
fun onReplaceTextureChanged(value: Boolean)
|
||||
fun onLazyInitChanged(value: Boolean)
|
||||
fun onEnableFreeCameraChanged(value: Boolean)
|
||||
fun onTargetFpsChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
|
|
@ -39,6 +40,7 @@ interface ConfigListener {
|
|||
fun onLodQualityLevelChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
fun onGameOrientationChanged(checkedId: Int)
|
||||
fun onDumpTextChanged(value: Boolean)
|
||||
fun onDumpRuntimeTextureChanged(value: Boolean)
|
||||
|
||||
fun onEnableBreastParamChanged(value: Boolean)
|
||||
fun onBDampingChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
|
|
@ -69,8 +71,15 @@ interface ConfigListener {
|
|||
localResourceVersionState: String? = null,
|
||||
errorString: String? = null,
|
||||
localAPIResourceVersion: String? = null)
|
||||
fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean? = null,
|
||||
downloadProgressState: Float? = null,
|
||||
localTextureResourceVersion: String? = null,
|
||||
errorString: String? = null)
|
||||
fun onPUseAPIAssetsChanged(value: Boolean)
|
||||
fun onPUseAPIAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
fun onPUseAPITextureAssetsChanged(value: Boolean)
|
||||
fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int)
|
||||
fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean)
|
||||
fun mainUIConfirmStatUpdate(isShow: Boolean? = null, title: String? = null,
|
||||
content: String? = null,
|
||||
onConfirm: (() -> Unit)? = { mainUIConfirmStatUpdate(isShow = false) },
|
||||
|
|
@ -129,6 +138,12 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
|||
pushKeyEvent(KeyEvent(1145, 30))
|
||||
}
|
||||
|
||||
override fun onReplaceTextureChanged(value: Boolean) {
|
||||
config.replaceTexture = value
|
||||
saveConfig()
|
||||
pushKeyEvent(KeyEvent(1145, 30))
|
||||
}
|
||||
|
||||
override fun onLazyInitChanged(value: Boolean) {
|
||||
config.lazyInit = value
|
||||
saveConfig()
|
||||
|
|
@ -149,6 +164,11 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
|||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onDumpRuntimeTextureChanged(value: Boolean) {
|
||||
config.dumpRuntimeTexture = value
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
override fun onEnableFreeCameraChanged(value: Boolean) {
|
||||
config.enableFreeCamera = value
|
||||
saveConfig()
|
||||
|
|
@ -576,6 +596,14 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
|||
localAPIResourceVersion?.let{ programConfigViewModel.localAPIResourceVersionState.value = it }
|
||||
}
|
||||
|
||||
override fun mainPageTextureAssetsViewDataUpdate(downloadAbleState: Boolean?, downloadProgressState: Float?,
|
||||
localTextureResourceVersion: String?, errorString: String?) {
|
||||
downloadAbleState?.let { programConfigViewModel.textureDownloadAbleState.value = it }
|
||||
downloadProgressState?.let{ programConfigViewModel.textureDownloadProgressState.value = it }
|
||||
localTextureResourceVersion?.let{ programConfigViewModel.localTextureResourceVersionState.value = it }
|
||||
errorString?.let{ programConfigViewModel.textureErrorStringState.value = it }
|
||||
}
|
||||
|
||||
override fun onPUseAPIAssetsChanged(value: Boolean) {
|
||||
programConfig.useAPIAssets = value
|
||||
if (value) {
|
||||
|
|
@ -591,6 +619,21 @@ interface ConfigUpdateListener: ConfigListener, IHasConfigItems {
|
|||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPUseAPITextureAssetsChanged(value: Boolean) {
|
||||
programConfig.useAPITextureAssets = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPUseAPITextureAssetsURLChanged(s: CharSequence, start: Int, before: Int, count: Int) {
|
||||
programConfig.useAPITextureAssetsURL = s.toString()
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun onPDelTextureRemoteAfterUpdateChanged(value: Boolean) {
|
||||
programConfig.delTextureRemoteAfterUpdate = value
|
||||
saveProgramConfig()
|
||||
}
|
||||
|
||||
override fun mainUIConfirmStatUpdate(isShow: Boolean?, title: String?, content: String?,
|
||||
onConfirm: (() -> Unit)?, onCancel: (() -> Unit)?
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import java.util.Locale
|
|||
import kotlin.system.measureTimeMillis
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
|
||||
import io.github.chinosk.gakumas.localify.models.ProgramConfig
|
||||
|
|
@ -53,6 +54,7 @@ class GakumasHookMain : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
|||
|
||||
private var getConfigError: Exception? = null
|
||||
private var externalFilesChecked: Boolean = false
|
||||
private var textureFilesChecked: Boolean = false
|
||||
private var gameActivity: Activity? = null
|
||||
|
||||
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
|
||||
|
|
@ -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)
|
||||
Log.d(TAG, "gkmsData: $gkmsData")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
|
|||
import io.github.chinosk.gakumas.localify.hookUtils.MainKeyEventDispatcher
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.ShizukuApi
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.json
|
||||
import io.github.chinosk.gakumas.localify.models.ConfirmStateModel
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
|
|
@ -79,6 +80,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
|
|||
fun getVersion(): List<String> {
|
||||
var versionText = ""
|
||||
var resVersionText = "unknown"
|
||||
var textureVersionText = "unknown"
|
||||
|
||||
try {
|
||||
val stream = assets.open("${FilesChecker.localizationFilesDir}/version.txt")
|
||||
|
|
@ -87,6 +89,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
|
|||
if (programConfig.useAPIAssets) {
|
||||
RemoteAPIFilesChecker.getLocalVersion(this)?.let { resVersionText = it }
|
||||
}
|
||||
TextureResourceUpdater.getLocalVersion(this)?.let { textureVersionText = it }
|
||||
|
||||
val packInfo = packageManager.getPackageInfo(packageName, 0)
|
||||
val version = packInfo.versionName
|
||||
|
|
@ -95,7 +98,7 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
|
|||
}
|
||||
catch (_: Exception) {}
|
||||
|
||||
return listOf(versionText, resVersionText)
|
||||
return listOf(versionText, resVersionText, textureVersionText)
|
||||
}
|
||||
|
||||
fun openUrl(url: String) {
|
||||
|
|
@ -130,7 +133,8 @@ class MainActivity : ComponentActivity(), ConfigUpdateListener, IConfigurableAct
|
|||
viewModel = ViewModelProvider(this, factory)[UserConfigViewModel::class.java]
|
||||
|
||||
programConfigFactory = ProgramConfigViewModelFactory(programConfig,
|
||||
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString()
|
||||
FileHotUpdater.getZipResourceVersion(File(filesDir, "update_trans.zip").absolutePath).toString(),
|
||||
TextureResourceUpdater.getLocalVersion(this).toString()
|
||||
)
|
||||
programConfigViewModel = ViewModelProvider(this, programConfigFactory)[ProgramConfigViewModel::class.java]
|
||||
|
||||
|
|
@ -222,6 +226,50 @@ fun getProgramDownloadErrorStringState(context: MainActivity?): State<String> {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramTextureDownloadState(context: MainActivity?): State<Float> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.textureDownloadProgress.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(0f)
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramTextureDownloadAbleState(context: MainActivity?): State<Boolean> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.textureDownloadAble.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow(true)
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramLocalTextureResourceVersionState(context: MainActivity?): State<String> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.localTextureResourceVersion.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow("null")
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getProgramTextureDownloadErrorStringState(context: MainActivity?): State<String> {
|
||||
return if (context != null) {
|
||||
context.programConfigViewModel.textureErrorString.collectAsState()
|
||||
}
|
||||
else {
|
||||
val configMSF = MutableStateFlow("")
|
||||
configMSF.asStateFlow().collectAsState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getMainUIConfirmState(context: MainActivity?, previewData: ConfirmStateModel? = null): State<ConfirmStateModel> {
|
||||
return if (context != null) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ object FilesChecker {
|
|||
if (!pluginBasePath.exists()) {
|
||||
pluginBasePath.mkdirs()
|
||||
}
|
||||
val skipBuiltInTexture2d = File(filesDir, "$localizationFilesDir/texture2d").exists()
|
||||
|
||||
val assets = XModuleResources.createInstance(modulePath, null).assets
|
||||
fun forAllAssetFiles(
|
||||
|
|
@ -65,6 +66,12 @@ object FilesChecker {
|
|||
}
|
||||
}
|
||||
forAllAssetFiles(localizationFilesDir) { path, file ->
|
||||
if ((path == "$localizationFilesDir/texture2d" ||
|
||||
path.startsWith("$localizationFilesDir/texture2d/")) &&
|
||||
skipBuiltInTexture2d) {
|
||||
return@forAllAssetFiles
|
||||
}
|
||||
|
||||
val outFile = File(filesDir, path)
|
||||
if (file == null) {
|
||||
outFile.mkdirs()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package io.github.chinosk.gakumas.localify.mainUtils
|
|||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object FileDownloader {
|
||||
|
|
@ -111,6 +113,99 @@ object FileDownloader {
|
|||
|
||||
}
|
||||
|
||||
fun downloadFileToPath(
|
||||
url: String,
|
||||
outputFile: File,
|
||||
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
|
||||
onSuccess: (File) -> Unit,
|
||||
onFailed: (Int, String) -> Unit,
|
||||
checkContentTypes: List<String>? = null
|
||||
) {
|
||||
try {
|
||||
if (call != null) {
|
||||
onFailed(-1, "Another file is downloading.")
|
||||
return
|
||||
}
|
||||
outputFile.parentFile?.mkdirs()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
call = client.newCall(request)
|
||||
call?.enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
this@FileDownloader.call = null
|
||||
if (call.isCanceled()) {
|
||||
onFailed(-1, "Download canceled")
|
||||
} else {
|
||||
onFailed(-1, e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
this@FileDownloader.call = null
|
||||
onFailed(response.code, response.message)
|
||||
return
|
||||
}
|
||||
|
||||
if (checkContentTypes != null) {
|
||||
val contentType = response.header("Content-Type")
|
||||
if (!checkContentTypes.contains(contentType)) {
|
||||
onFailed(-1, "Unexpected content type: $contentType")
|
||||
this@FileDownloader.call = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.body?.let { responseBody ->
|
||||
val contentLength = responseBody.contentLength()
|
||||
val inputStream = responseBody.byteStream()
|
||||
val buffer = ByteArray(8 * 1024)
|
||||
var downloadedBytes = 0L
|
||||
var read: Int
|
||||
|
||||
try {
|
||||
FileOutputStream(outputFile).use { outputStream ->
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
downloadedBytes += read
|
||||
val progress = if (contentLength < 0) {
|
||||
0f
|
||||
}
|
||||
else {
|
||||
downloadedBytes.toFloat() / contentLength
|
||||
}
|
||||
onDownload(progress, downloadedBytes, contentLength)
|
||||
}
|
||||
outputStream.flush()
|
||||
}
|
||||
onSuccess(outputFile)
|
||||
} catch (e: IOException) {
|
||||
outputFile.delete()
|
||||
if (call.isCanceled()) {
|
||||
onFailed(-1, "Download canceled")
|
||||
} else {
|
||||
onFailed(-1, e.message ?: "Error reading stream")
|
||||
}
|
||||
} finally {
|
||||
this@FileDownloader.call = null
|
||||
inputStream.close()
|
||||
}
|
||||
} ?: run {
|
||||
this@FileDownloader.call = null
|
||||
onFailed(-1, "Response body is null")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (e: Exception) {
|
||||
onFailed(-1, e.toString())
|
||||
call = null
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
call?.cancel()
|
||||
this@FileDownloader.call = null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,322 @@
|
|||
package io.github.chinosk.gakumas.localify.mainUtils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import io.github.chinosk.gakumas.localify.GakumasHookMain
|
||||
import io.github.chinosk.gakumas.localify.TAG
|
||||
import io.github.chinosk.gakumas.localify.models.Asset
|
||||
import io.github.chinosk.gakumas.localify.models.GithubReleaseModel
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
object TextureResourceUpdater {
|
||||
private const val CACHE_DIR = "remote_texture_files"
|
||||
private const val CACHE_ZIP_NAME = "texture.zip"
|
||||
private const val CACHE_VERSION_NAME = "texture_version.txt"
|
||||
private const val TEXTURE_DIR = "gakumas-local/texture2d"
|
||||
private const val VERSION_FILE_NAME = "texture_version.txt"
|
||||
|
||||
private data class TextureZipRoot(val prefix: String, val containsTexture2dDir: Boolean)
|
||||
|
||||
private fun textureDir(context: Context): File {
|
||||
return File(context.filesDir, TEXTURE_DIR)
|
||||
}
|
||||
|
||||
fun getCachedZipFile(context: Context): File {
|
||||
return File(context.filesDir, "$CACHE_DIR/$CACHE_ZIP_NAME")
|
||||
}
|
||||
|
||||
fun getLocalVersion(context: Context): String? {
|
||||
val versionFile = File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME")
|
||||
if (versionFile.exists()) {
|
||||
versionFile.readText().trim().takeIf { it.isNotEmpty() }?.let { return it }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getInstalledVersion(context: Context): String? {
|
||||
val versionFile = File(textureDir(context), VERSION_FILE_NAME)
|
||||
if (!versionFile.exists()) {
|
||||
return null
|
||||
}
|
||||
return versionFile.readText().trim()
|
||||
}
|
||||
|
||||
private fun ensureCacheDir(context: Context): File {
|
||||
val basePath = File(context.filesDir, CACHE_DIR)
|
||||
if (!basePath.exists()) {
|
||||
basePath.mkdirs()
|
||||
}
|
||||
return basePath
|
||||
}
|
||||
|
||||
private fun saveCachedVersion(context: Context, version: String) {
|
||||
val versionFile = File(ensureCacheDir(context), CACHE_VERSION_NAME)
|
||||
versionFile.writeText(version)
|
||||
}
|
||||
|
||||
private fun findTextureZipRoot(zipFile: ZipFile): TextureZipRoot? {
|
||||
val entries = zipFile.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement()
|
||||
if (entry.isDirectory) continue
|
||||
|
||||
val name = entry.name.replace('\\', '/').trimStart('/')
|
||||
val textureVersionMarker = "texture2d/$VERSION_FILE_NAME"
|
||||
if (name.endsWith(textureVersionMarker)) {
|
||||
return TextureZipRoot(name.substring(0, name.length - textureVersionMarker.length), true)
|
||||
}
|
||||
if (name == VERSION_FILE_NAME || name.endsWith("/$VERSION_FILE_NAME")) {
|
||||
val prefix = name.substring(0, name.length - VERSION_FILE_NAME.length)
|
||||
return TextureZipRoot(prefix, false)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun readTextureVersion(zipFile: ZipFile, root: TextureZipRoot): String? {
|
||||
val versionPath = if (root.containsTexture2dDir) {
|
||||
"${root.prefix}texture2d/$VERSION_FILE_NAME"
|
||||
} else {
|
||||
"${root.prefix}$VERSION_FILE_NAME"
|
||||
}
|
||||
val entry = zipFile.getEntry(versionPath) ?: return null
|
||||
return zipFile.getInputStream(entry).bufferedReader().use { it.readText().trim() }
|
||||
}
|
||||
|
||||
private fun validateTextureZip(zipFile: File): String {
|
||||
ZipFile(zipFile).use { zip ->
|
||||
val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip")
|
||||
return readTextureVersion(zip, root) ?: throw IOException("texture_version.txt is empty")
|
||||
}
|
||||
}
|
||||
|
||||
private fun findTextureZipAsset(releaseData: GithubReleaseModel): Asset? {
|
||||
val zipAssets = releaseData.assets.filter { it.name.endsWith(".zip", ignoreCase = true) }
|
||||
return zipAssets.firstOrNull { it.name.equals("texture2d.zip", ignoreCase = true) }
|
||||
?: zipAssets.firstOrNull()
|
||||
}
|
||||
|
||||
private fun safeOutputFile(baseDir: File, relativePath: String): File? {
|
||||
val targetFile = File(baseDir, relativePath)
|
||||
val baseCanonical = baseDir.canonicalFile
|
||||
val targetCanonical = targetFile.canonicalFile
|
||||
val basePath = baseCanonical.path + File.separator
|
||||
if (targetCanonical.path != baseCanonical.path && !targetCanonical.path.startsWith(basePath)) {
|
||||
return null
|
||||
}
|
||||
return targetFile
|
||||
}
|
||||
|
||||
private fun installTextureZip(context: Context, zipFile: File, version: String? = null) {
|
||||
ZipFile(zipFile).use { zip ->
|
||||
val root = findTextureZipRoot(zip) ?: throw IOException("texture_version.txt not found in texture zip")
|
||||
val installedVersion = version ?: readTextureVersion(zip, root)
|
||||
?: throw IOException("texture_version.txt is empty")
|
||||
val targetDir = textureDir(context)
|
||||
val tempDir = File(context.filesDir, "$TEXTURE_DIR.tmp")
|
||||
|
||||
if (tempDir.exists()) {
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
tempDir.mkdirs()
|
||||
|
||||
val entries = zip.entries()
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement()
|
||||
val name = entry.name.replace('\\', '/').trimStart('/')
|
||||
|
||||
val relativePath = if (root.containsTexture2dDir) {
|
||||
val rootPath = "${root.prefix}texture2d/"
|
||||
if (!name.startsWith(rootPath)) continue
|
||||
name.substring(rootPath.length)
|
||||
} else {
|
||||
if (!name.startsWith(root.prefix)) continue
|
||||
name.substring(root.prefix.length)
|
||||
}
|
||||
|
||||
if (relativePath.isEmpty()) continue
|
||||
val targetFile = safeOutputFile(tempDir, relativePath) ?: continue
|
||||
|
||||
if (entry.isDirectory) {
|
||||
targetFile.mkdirs()
|
||||
continue
|
||||
}
|
||||
|
||||
targetFile.parentFile?.mkdirs()
|
||||
zip.getInputStream(entry).use { input ->
|
||||
FileOutputStream(targetFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File(tempDir, VERSION_FILE_NAME).writeText(installedVersion)
|
||||
|
||||
if (targetDir.exists()) {
|
||||
targetDir.deleteRecursively()
|
||||
}
|
||||
if (!tempDir.renameTo(targetDir)) {
|
||||
tempDir.copyRecursively(targetDir, overwrite = true)
|
||||
tempDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTextureFilesFromZip(activity: Activity, zipFileUri: Uri, filesDir: File,
|
||||
deleteAfterUpdate: Boolean) {
|
||||
try {
|
||||
GakumasHookMain.showToast("Updating texture files from zip...")
|
||||
|
||||
val tempZipFile = File(filesDir, "texture_update.zip")
|
||||
activity.contentResolver.openInputStream(zipFileUri).use { input ->
|
||||
if (input == null) {
|
||||
Log.e(TAG, "texture zip openInputStream failed.")
|
||||
GakumasHookMain.showToast("Texture update file not found.")
|
||||
return
|
||||
}
|
||||
tempZipFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
installTextureZip(activity, tempZipFile)
|
||||
Log.i(TAG, "Texture zip installed into ${File(filesDir, TEXTURE_DIR).absolutePath}, " +
|
||||
"version=${getInstalledVersion(activity)}")
|
||||
tempZipFile.delete()
|
||||
|
||||
if (deleteAfterUpdate) {
|
||||
activity.contentResolver.delete(zipFileUri, null, null)
|
||||
}
|
||||
GakumasHookMain.showToast("Texture update success.")
|
||||
}
|
||||
catch (e: java.io.FileNotFoundException) {
|
||||
Log.i(TAG, "updateTextureFilesFromZip - file not found: $e")
|
||||
GakumasHookMain.showToast("Texture update file not found.")
|
||||
}
|
||||
catch (e: Exception) {
|
||||
Log.e(TAG, "updateTextureFilesFromZip failed: $e")
|
||||
GakumasHookMain.showToast("Texture update failed: $e")
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdateTextureAssets(context: Context, apiURL: String,
|
||||
onFailed: (Int, String) -> Unit,
|
||||
onResult: (data: GithubReleaseModel, localVersion: String?) -> Unit) {
|
||||
runCatching {
|
||||
val request = Request.Builder()
|
||||
.url(apiURL)
|
||||
.build()
|
||||
FileDownloader.requestGet(request, object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
runCatching {
|
||||
response.use {
|
||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
||||
|
||||
val responseBody = response.body?.string()
|
||||
if (responseBody != null) {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val releaseData = json.decodeFromString<GithubReleaseModel>(responseBody)
|
||||
onResult(releaseData, getLocalVersion(context))
|
||||
} else {
|
||||
onFailed(-1, "Response body is null")
|
||||
}
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "checkUpdateTextureAssets failed", e)
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "checkUpdateTextureAssets failed", e)
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTextureAssets(context: Context, apiURL: String,
|
||||
deleteAfterUpdate: Boolean,
|
||||
onDownload: (Float, downloaded: Long, size: Long) -> Unit,
|
||||
onFailed: (Int, String) -> Unit,
|
||||
onSuccess: (version: String, changed: Boolean) -> Unit) {
|
||||
runCatching {
|
||||
val request = Request.Builder()
|
||||
.url(apiURL)
|
||||
.build()
|
||||
FileDownloader.requestGet(request, object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
runCatching {
|
||||
response.use {
|
||||
if (!response.isSuccessful) throw IOException("Unexpected code $response")
|
||||
|
||||
val responseBody = response.body?.string()
|
||||
if (responseBody != null) {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val releaseData = json.decodeFromString<GithubReleaseModel>(responseBody)
|
||||
val releaseVersion = releaseData.tag_name
|
||||
val localVersion = getLocalVersion(context)
|
||||
if (releaseVersion == localVersion) {
|
||||
onSuccess(releaseVersion, false)
|
||||
return
|
||||
}
|
||||
|
||||
val zipAsset = findTextureZipAsset(releaseData)
|
||||
if (zipAsset == null) {
|
||||
onFailed(-1, "No zip asset found")
|
||||
return
|
||||
}
|
||||
|
||||
val cacheFile = getCachedZipFile(context)
|
||||
FileDownloader.downloadFileToPath(zipAsset.browser_download_url,
|
||||
cacheFile,
|
||||
onDownload, {
|
||||
runCatching {
|
||||
saveCachedVersion(context, releaseVersion)
|
||||
val zipVersion = validateTextureZip(cacheFile)
|
||||
if (zipVersion != releaseVersion) {
|
||||
cacheFile.delete()
|
||||
File(context.filesDir, "$CACHE_DIR/$CACHE_VERSION_NAME").delete()
|
||||
throw IOException("texture_version.txt ($zipVersion) differs from release tag ($releaseVersion)")
|
||||
}
|
||||
Log.i(TAG, "Texture zip cached: ${cacheFile.absolutePath}, " +
|
||||
"size=${cacheFile.length()}, version=$releaseVersion")
|
||||
onSuccess(releaseVersion, true)
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "save texture zip failed", e)
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
},
|
||||
onFailed)
|
||||
} else {
|
||||
onFailed(-1, "Response body is null")
|
||||
}
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "updateTextureAssets failed", e)
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
}
|
||||
})
|
||||
}.onFailure { e ->
|
||||
Log.e(TAG, "updateTextureAssets failed", e)
|
||||
onFailed(-1, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,11 @@ data class GakumasConfig (
|
|||
var enabled: Boolean = true,
|
||||
var lazyInit: Boolean = true,
|
||||
var replaceFont: Boolean = true,
|
||||
var replaceTexture: Boolean = true,
|
||||
var textTest: Boolean = false,
|
||||
var useMasterTrans: Boolean = true,
|
||||
var dumpText: Boolean = false,
|
||||
var dumpRuntimeTexture: Boolean = false,
|
||||
var gameOrientation: Int = 0,
|
||||
var forceExportResource: Boolean = false,
|
||||
var enableFreeCamera: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ data class ProgramConfig(
|
|||
var useAPIAssets: Boolean = false,
|
||||
var useAPIAssetsURL: String = "",
|
||||
var delRemoteAfterUpdate: Boolean = true,
|
||||
var useAPITextureAssets: Boolean = false,
|
||||
var useAPITextureAssetsURL: String = "",
|
||||
var delTextureRemoteAfterUpdate: Boolean = true,
|
||||
var cleanLocalAssets: Boolean = false,
|
||||
|
||||
// var localAPIAssetsVersion: String = "",
|
||||
|
|
|
|||
|
|
@ -43,11 +43,12 @@ class ResourceCollapsibleBoxViewModelFactory(private val initiallyExpanded: Bool
|
|||
|
||||
|
||||
class ProgramConfigViewModelFactory(private val initialValue: ProgramConfig,
|
||||
private val localResourceVersion: String) : ViewModelProvider.Factory {
|
||||
private val localResourceVersion: String,
|
||||
private val localTextureResourceVersion: String) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(ProgramConfigViewModel::class.java)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return ProgramConfigViewModel(initialValue, localResourceVersion) as T
|
||||
return ProgramConfigViewModel(initialValue, localResourceVersion, localTextureResourceVersion) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
}
|
||||
|
|
@ -62,7 +63,8 @@ data class ConfirmStateModel(
|
|||
var p: Boolean = false
|
||||
)
|
||||
|
||||
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String) : ViewModel() {
|
||||
class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion: String,
|
||||
initLocalTextureResourceVersion: String) : ViewModel() {
|
||||
val configState = MutableStateFlow(initValue)
|
||||
val config: StateFlow<ProgramConfig> = configState.asStateFlow()
|
||||
|
||||
|
|
@ -81,6 +83,18 @@ class ProgramConfigViewModel(initValue: ProgramConfig, initLocalResourceVersion:
|
|||
val errorStringState = MutableStateFlow("")
|
||||
val errorString: StateFlow<String> = errorStringState.asStateFlow()
|
||||
|
||||
val textureDownloadProgressState = MutableStateFlow(-1f)
|
||||
val textureDownloadProgress: StateFlow<Float> = textureDownloadProgressState.asStateFlow()
|
||||
|
||||
val textureDownloadAbleState = MutableStateFlow(true)
|
||||
val textureDownloadAble: StateFlow<Boolean> = textureDownloadAbleState.asStateFlow()
|
||||
|
||||
val localTextureResourceVersionState = MutableStateFlow(initLocalTextureResourceVersion)
|
||||
val localTextureResourceVersion: StateFlow<String> = localTextureResourceVersionState.asStateFlow()
|
||||
|
||||
val textureErrorStringState = MutableStateFlow("")
|
||||
val textureErrorString: StateFlow<String> = textureErrorStringState.asStateFlow()
|
||||
|
||||
val mainUIConfirmState = MutableStateFlow(ConfirmStateModel())
|
||||
val mainUIConfirm: StateFlow<ConfirmStateModel> = mainUIConfirmState.asStateFlow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,14 +49,14 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
|
|||
previewData: GakumasConfig? = null) {
|
||||
val imagePainter = painterResource(R.drawable.bg_pattern)
|
||||
var versionInfo by remember {
|
||||
mutableStateOf(context?.getVersion() ?: listOf("", "Unknown"))
|
||||
mutableStateOf(context?.getVersion() ?: listOf("", "Unknown", "Unknown"))
|
||||
}
|
||||
// val config = getConfigState(context, previewData)
|
||||
val confirmState by getMainUIConfirmState(context, null)
|
||||
val programConfig by getProgramConfigState(context)
|
||||
|
||||
LaunchedEffect(programConfig) {
|
||||
versionInfo = context?.getVersion() ?: listOf("", "Unknown")
|
||||
versionInfo = context?.getVersion() ?: listOf("", "Unknown", "Unknown")
|
||||
}
|
||||
|
||||
Box(
|
||||
|
|
@ -79,6 +79,7 @@ fun MainUI(modifier: Modifier = Modifier, context: MainActivity? = null,
|
|||
) {
|
||||
Text(text = "Gakumas Localify ${versionInfo[0]}", fontSize = 18.sp)
|
||||
Text(text = "Assets version: ${versionInfo[1]}", fontSize = 13.sp)
|
||||
Text(text = "Texture version: ${versionInfo[2]}", fontSize = 13.sp)
|
||||
|
||||
SettingsTabs(modifier, listOf(stringResource(R.string.about), stringResource(R.string.home),
|
||||
stringResource(R.string.advanced_settings)),
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ fun AdvanceSettingsPage(modifier: Modifier = Modifier,
|
|||
v -> context?.onDumpTextChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.dump_runtime_texture), checked = config.value.dumpRuntimeTexture) {
|
||||
v -> context?.onDumpRuntimeTextureChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.force_export_resource), checked = config.value.forceExportResource) {
|
||||
v -> context?.onForceExportResourceChanged(v)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,11 +43,16 @@ import io.github.chinosk.gakumas.localify.getProgramConfigState
|
|||
import io.github.chinosk.gakumas.localify.getProgramDownloadAbleState
|
||||
import io.github.chinosk.gakumas.localify.getProgramDownloadErrorStringState
|
||||
import io.github.chinosk.gakumas.localify.getProgramDownloadState
|
||||
import io.github.chinosk.gakumas.localify.getProgramLocalTextureResourceVersionState
|
||||
import io.github.chinosk.gakumas.localify.getProgramLocalResourceVersionState
|
||||
import io.github.chinosk.gakumas.localify.getProgramLocalAPIResourceVersionState
|
||||
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadAbleState
|
||||
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadErrorStringState
|
||||
import io.github.chinosk.gakumas.localify.getProgramTextureDownloadState
|
||||
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.FileDownloader
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.RemoteAPIFilesChecker
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.TextureResourceUpdater
|
||||
import io.github.chinosk.gakumas.localify.mainUtils.TimeUtils
|
||||
import io.github.chinosk.gakumas.localify.models.GakumasConfig
|
||||
import io.github.chinosk.gakumas.localify.models.ResourceCollapsibleBoxViewModel
|
||||
|
|
@ -75,6 +80,10 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
val localResourceVersion by getProgramLocalResourceVersionState(context)
|
||||
val localAPIResourceVersion by getProgramLocalAPIResourceVersionState(context)
|
||||
val downloadErrorString by getProgramDownloadErrorStringState(context)
|
||||
val textureDownloadProgress by getProgramTextureDownloadState(context)
|
||||
val textureDownloadAble by getProgramTextureDownloadAbleState(context)
|
||||
val localTextureResourceVersion by getProgramLocalTextureResourceVersionState(context)
|
||||
val textureDownloadErrorString by getProgramTextureDownloadErrorStringState(context)
|
||||
var isFirstTimeInThisPage by rememberSaveable { mutableStateOf(true) }
|
||||
|
||||
// val scrollState = rememberScrollState()
|
||||
|
|
@ -131,7 +140,8 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
})
|
||||
}
|
||||
|
||||
fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true) {
|
||||
fun onClickDownload(isZipResource: Boolean, isHumanClick: Boolean = true,
|
||||
onFinished: (() -> Unit)? = null) {
|
||||
context?.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = false,
|
||||
errorString = "",
|
||||
|
|
@ -139,6 +149,7 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
)
|
||||
if (isZipResource) {
|
||||
zipResourceDownload()
|
||||
onFinished?.invoke()
|
||||
}
|
||||
else {
|
||||
RemoteAPIFilesChecker.checkUpdateLocalAssets(context!!,
|
||||
|
|
@ -150,6 +161,7 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
downloadProgressState = -1f
|
||||
)
|
||||
context.mainUIConfirmStatUpdate(true, "Error", reason)
|
||||
onFinished?.invoke()
|
||||
},
|
||||
onResult = { data, localVersion ->
|
||||
if (!isHumanClick) {
|
||||
|
|
@ -159,6 +171,7 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
onFinished?.invoke()
|
||||
return@checkUpdateLocalAssets
|
||||
}
|
||||
}
|
||||
|
|
@ -170,10 +183,13 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
onDownload = { progress, _, _ ->
|
||||
context.mainPageAssetsViewDataUpdate(downloadProgressState = progress)
|
||||
},
|
||||
onFailed = { _, reason -> context.mainPageAssetsViewDataUpdate(
|
||||
onFailed = { _, reason ->
|
||||
context.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = reason,
|
||||
)},
|
||||
)
|
||||
onFinished?.invoke()
|
||||
},
|
||||
onSuccess = { saveFile, releaseVersion ->
|
||||
context.mainPageAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
|
|
@ -185,6 +201,7 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
)
|
||||
context.saveProgramConfig()
|
||||
Log.d(TAG, "saved: $releaseVersion $saveFile")
|
||||
onFinished?.invoke()
|
||||
})
|
||||
},
|
||||
onCancel = {
|
||||
|
|
@ -193,12 +210,92 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
onFinished?.invoke()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun startTextureResourceUpdate() {
|
||||
context?.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = false,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
TextureResourceUpdater.updateTextureAssets(context!!,
|
||||
programConfig.value.useAPITextureAssetsURL,
|
||||
programConfig.value.delTextureRemoteAfterUpdate,
|
||||
onDownload = { progress, _, _ ->
|
||||
context.mainPageTextureAssetsViewDataUpdate(downloadProgressState = progress)
|
||||
},
|
||||
onFailed = { _, reason ->
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = reason,
|
||||
)
|
||||
},
|
||||
onSuccess = { releaseVersion, changed ->
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f,
|
||||
localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context)
|
||||
?: releaseVersion
|
||||
)
|
||||
context.saveProgramConfig()
|
||||
Log.d(TAG, "texture resource update finished: $releaseVersion changed=$changed")
|
||||
})
|
||||
}
|
||||
|
||||
fun onClickTextureDownload(isHumanClick: Boolean = true) {
|
||||
context?.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = false,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
TextureResourceUpdater.checkUpdateTextureAssets(context!!,
|
||||
programConfig.value.useAPITextureAssetsURL,
|
||||
onFailed = { _, reason ->
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = reason,
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
if (isHumanClick) {
|
||||
context.mainUIConfirmStatUpdate(true, "Error", reason)
|
||||
}
|
||||
},
|
||||
onResult = { data, localVersion ->
|
||||
if (!isHumanClick) {
|
||||
if (data.tag_name == localVersion) {
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f,
|
||||
localTextureResourceVersion = localVersion
|
||||
)
|
||||
return@checkUpdateTextureAssets
|
||||
}
|
||||
}
|
||||
|
||||
context.mainUIConfirmStatUpdate(true, context.getString(R.string.texture_resource_update),
|
||||
"${data.name}\n$localVersion -> ${data.tag_name}\n${data.body}\n\n${TimeUtils.convertIsoToLocalTime(data.published_at)}",
|
||||
onConfirm = {
|
||||
resourceSettingsViewModel.expanded = true
|
||||
startTextureResourceUpdate()
|
||||
},
|
||||
onCancel = {
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
downloadAbleState = true,
|
||||
errorString = "",
|
||||
downloadProgressState = -1f
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
if (context == null) return@LaunchedEffect
|
||||
|
|
@ -206,9 +303,25 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
context.mainPageAssetsViewDataUpdate(
|
||||
localAPIResourceVersion = localAPIResVer
|
||||
)
|
||||
context.mainPageTextureAssetsViewDataUpdate(
|
||||
localTextureResourceVersion = TextureResourceUpdater.getLocalVersion(context)
|
||||
)
|
||||
if (isFirstTimeInThisPage) {
|
||||
if (programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()) {
|
||||
onClickDownload(false, false)
|
||||
val shouldCheckResource =
|
||||
programConfig.value.useAPIAssets && programConfig.value.useAPIAssetsURL.isNotEmpty()
|
||||
val shouldCheckTexture = config.value.replaceTexture &&
|
||||
programConfig.value.useAPITextureAssets &&
|
||||
programConfig.value.useAPITextureAssetsURL.isNotEmpty()
|
||||
|
||||
if (shouldCheckResource) {
|
||||
onClickDownload(false, false) {
|
||||
if (shouldCheckTexture) {
|
||||
onClickTextureDownload(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (shouldCheckTexture) {
|
||||
onClickTextureDownload(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,6 +353,10 @@ fun HomePage(modifier: Modifier = Modifier,
|
|||
v -> context?.onReplaceFontChanged(v)
|
||||
}
|
||||
|
||||
GakuSwitch(modifier, stringResource(R.string.replace_texture), checked = config.value.replaceTexture) {
|
||||
v -> context?.onReplaceTextureChanged(v)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@
|
|||
<string name="error_a11y_label">エラー: 無効</string>
|
||||
<string name="error_icon_content_description">エラー</string>
|
||||
<string name="export_text">テキストをエクスポート</string>
|
||||
<string name="dump_runtime_texture">実行時テクスチャをダンプ</string>
|
||||
<string name="exposed_dropdown_menu_content_description">ドロップダウンメニューを表示</string>
|
||||
<string name="fab_transformation_scrim_behavior">com.google.android.material.transformation.FabTransformationScrimBehavior</string>
|
||||
<string name="fab_transformation_sheet_behavior">com.google.android.material.transformation.FabTransformationSheetBehavior</string>
|
||||
|
|
@ -311,6 +312,7 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
|
|||
<string name="range_start">範囲の開始</string>
|
||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||
<string name="replace_font">フォントを置換する</string>
|
||||
<string name="replace_texture">テクスチャを置換する</string>
|
||||
<string name="reserve_patched">パッチ済みの APK を予約する</string>
|
||||
<string name="resource_settings">リソース設定</string>
|
||||
<string name="resource_url">リソース URL</string>
|
||||
|
|
@ -352,4 +354,9 @@ Xposed スコープは再パッチなしで動的に変更が可能です。
|
|||
<string name="usescale">胸の大きさを使用する</string>
|
||||
<string name="very_high">最高</string>
|
||||
<string name="warning">警告</string>
|
||||
<string name="check_texture_resource_from_api">API からテクスチャリソースの更新を確認</string>
|
||||
<string name="texture_api_addr">テクスチャ API アドレス (GitHub 最新リリース API)</string>
|
||||
<string name="texture_resource_update">テクスチャリソースを更新</string>
|
||||
<string name="downloaded_texture_resource_version">ダウンロード済みテクスチャバージョン</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="gakumas_localify">Gakumas Localify</string>
|
||||
<string name="enable_plugin">启用插件 (不可热重载)</string>
|
||||
<string name="replace_font">替换字体</string>
|
||||
<string name="replace_texture">替换贴图</string>
|
||||
<string name="lazy_init">快速初始化(懒加载配置)</string>
|
||||
<string name="enable_free_camera">启用自由视角(可热重载; 需使用实体键盘)</string>
|
||||
<string name="start_game">以上述配置启动游戏/重载配置</string>
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
<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>
|
||||
|
|
@ -86,6 +88,10 @@
|
|||
<string name="api_addr">API 地址(Github Latest Release API)</string>
|
||||
<string name="check_update">检查更新</string>
|
||||
<string name="translation_resource_update">翻译资源更新</string>
|
||||
<string name="check_texture_resource_from_api">从服务器检查贴图资源更新</string>
|
||||
<string name="texture_api_addr">贴图 API 地址(Github Latest Release API)</string>
|
||||
<string name="texture_resource_update">贴图资源更新</string>
|
||||
<string name="downloaded_texture_resource_version">已下载贴图资源版本</string>
|
||||
|
||||
<string name="game_patch">游戏修补</string>
|
||||
<string name="patch_mode">修补模式</string>
|
||||
|
|
@ -105,4 +111,5 @@
|
|||
|
||||
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
||||
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<resources>
|
||||
<string name="app_name">Gakumas Localify</string>
|
||||
<string name="gakumas_localify">Gakumas Localify</string>
|
||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||
<string name="replace_font">替換字體</string>
|
||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
|
||||
<string name="unlockAllLive">解鎖所有 Live</string>
|
||||
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
|
||||
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
|
||||
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
|
||||
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
|
||||
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
|
||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||
<string name="text_hook_test_mode">文本 hook 測試模式</string>
|
||||
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
|
||||
<string name="export_text">導出文本</string>
|
||||
<string name="dump_runtime_texture">導出運行時貼圖</string>
|
||||
<string name="force_export_resource">啟動後強制導出資源</string>
|
||||
<string name="login_as_ios">模擬以 iOS 登入</string>
|
||||
<string name="max_high">極高</string>
|
||||
<string name="very_high">超高</string>
|
||||
<string name="hign">高</string>
|
||||
<string name="middle">中</string>
|
||||
<string name="low">低</string>
|
||||
<string name="orientation_orig">原版</string>
|
||||
<string name="orientation_portrait">豎屏</string>
|
||||
<string name="orientation_landscape">橫屏</string>
|
||||
<string name="orientation_lock">方向鎖定</string>
|
||||
<string name="enable_breast_param">啟用胸部參數</string>
|
||||
<string name="damping">阻尼 (Damping)</string>
|
||||
<string name="stiffness">剛度 (Stiffness)</string>
|
||||
<string name="spring">彈簧係數 (Spring)</string>
|
||||
<string name="pendulum">鐘擺係數 (Pendulum)</string>
|
||||
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
|
||||
<string name="average">Average</string>
|
||||
<string name="rootweight">RootWeight</string>
|
||||
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
|
||||
<string name="usearmcorrection">使用手臂矯正</string>
|
||||
<string name="isdirty">IsDirty</string>
|
||||
<string name="usescale">應用縮放</string>
|
||||
<string name="breast_scale">胸部縮放倍率</string>
|
||||
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
|
||||
<string name="axisx_x">axisX.x</string>
|
||||
<string name="axisy_x">axisY.x</string>
|
||||
<string name="axisz_x">axisZ.x</string>
|
||||
<string name="axisx_y">axisX.y</string>
|
||||
<string name="axisy_y">axisY.y</string>
|
||||
<string name="axisz_y">axisZ.y</string>
|
||||
<string name="basic_settings">基本設定</string>
|
||||
<string name="graphic_settings">畫面設定</string>
|
||||
<string name="camera_settings">攝影機設定</string>
|
||||
<string name="test_mode_live">測試模式 - LIVE</string>
|
||||
<string name="debug_settings">調試設定</string>
|
||||
<string name="breast_param">胸部參數</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="home">主頁</string>
|
||||
<string name="advanced_settings">進階設定</string>
|
||||
<string name="about_warn_title">使用前警告</string>
|
||||
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
|
||||
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
|
||||
<string name="about_about_title">關於本插件</string>
|
||||
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
|
||||
<string name="about_about_p2">插件交流QQ群: 991990192</string>
|
||||
<string name="project_contribution">項目貢獻</string>
|
||||
<string name="plugin_code">插件本體</string>
|
||||
<string name="contributors">貢獻者列表</string>
|
||||
<string name="translation_repository">譯文倉庫</string>
|
||||
<string name="resource_settings">資源設定</string>
|
||||
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
|
||||
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
|
||||
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
|
||||
<string name="resource_url">資源地址</string>
|
||||
<string name="download">下載</string>
|
||||
<string name="invalid_zip_file">文件解析失敗</string>
|
||||
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="ok">確定</string>
|
||||
<string name="downloaded_resource_version">已下載資源版本</string>
|
||||
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
|
||||
<string name="warning">注意</string>
|
||||
<string name="install">安裝</string>
|
||||
<string name="installing">安裝中</string>
|
||||
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
|
||||
<string name="api_addr">API 地址(Github Latest Release API)</string>
|
||||
<string name="check_update">檢查更新</string>
|
||||
<string name="translation_resource_update">翻譯資源更新</string>
|
||||
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
|
||||
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API)</string>
|
||||
<string name="texture_resource_update">貼圖資源更新</string>
|
||||
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
|
||||
<string name="game_patch">遊戲修補</string>
|
||||
<string name="patch_mode">修補模式</string>
|
||||
<string name="patch_local">本地模式</string>
|
||||
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
|
||||
<string name="patch_integrated">集成模式</string>
|
||||
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
|
||||
<string name="shizuku_available">Shizuku 服務可用</string>
|
||||
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
|
||||
<string name="home_shizuku_warning">部分功能不可用</string>
|
||||
<string name="patch_debuggable">可調試</string>
|
||||
<string name="reserve_patched">安裝時保留修補包</string>
|
||||
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
|
||||
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
|
||||
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
|
||||
<string name="patch_finished">修補完成,是否開始安裝?</string>
|
||||
|
||||
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
||||
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<resources>
|
||||
<string name="app_name">Gakumas Localify</string>
|
||||
<string name="gakumas_localify">Gakumas Localify</string>
|
||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||
<string name="replace_font">替換字體</string>
|
||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
|
||||
<string name="unlockAllLive">解鎖所有 Live</string>
|
||||
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
|
||||
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
|
||||
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
|
||||
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
|
||||
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
|
||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||
<string name="text_hook_test_mode">文本 hook 測試模式</string>
|
||||
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
|
||||
<string name="export_text">導出文本</string>
|
||||
<string name="dump_runtime_texture">導出運行時貼圖</string>
|
||||
<string name="force_export_resource">啟動後強制導出資源</string>
|
||||
<string name="login_as_ios">模擬以 iOS 登入</string>
|
||||
<string name="max_high">極高</string>
|
||||
<string name="very_high">超高</string>
|
||||
<string name="hign">高</string>
|
||||
<string name="middle">中</string>
|
||||
<string name="low">低</string>
|
||||
<string name="orientation_orig">原版</string>
|
||||
<string name="orientation_portrait">豎屏</string>
|
||||
<string name="orientation_landscape">橫屏</string>
|
||||
<string name="orientation_lock">方向鎖定</string>
|
||||
<string name="enable_breast_param">啟用胸部參數</string>
|
||||
<string name="damping">阻尼 (Damping)</string>
|
||||
<string name="stiffness">剛度 (Stiffness)</string>
|
||||
<string name="spring">彈簧係數 (Spring)</string>
|
||||
<string name="pendulum">鐘擺係數 (Pendulum)</string>
|
||||
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
|
||||
<string name="average">Average</string>
|
||||
<string name="rootweight">RootWeight</string>
|
||||
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
|
||||
<string name="usearmcorrection">使用手臂矯正</string>
|
||||
<string name="isdirty">IsDirty</string>
|
||||
<string name="usescale">應用縮放</string>
|
||||
<string name="breast_scale">胸部縮放倍率</string>
|
||||
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
|
||||
<string name="axisx_x">axisX.x</string>
|
||||
<string name="axisy_x">axisY.x</string>
|
||||
<string name="axisz_x">axisZ.x</string>
|
||||
<string name="axisx_y">axisX.y</string>
|
||||
<string name="axisy_y">axisY.y</string>
|
||||
<string name="axisz_y">axisZ.y</string>
|
||||
<string name="basic_settings">基本設定</string>
|
||||
<string name="graphic_settings">畫面設定</string>
|
||||
<string name="camera_settings">攝影機設定</string>
|
||||
<string name="test_mode_live">測試模式 - LIVE</string>
|
||||
<string name="debug_settings">調試設定</string>
|
||||
<string name="breast_param">胸部參數</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="home">主頁</string>
|
||||
<string name="advanced_settings">進階設定</string>
|
||||
<string name="about_warn_title">使用前警告</string>
|
||||
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
|
||||
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
|
||||
<string name="about_about_title">關於本插件</string>
|
||||
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
|
||||
<string name="about_about_p2">插件交流QQ群: 991990192</string>
|
||||
<string name="project_contribution">項目貢獻</string>
|
||||
<string name="plugin_code">插件本體</string>
|
||||
<string name="contributors">貢獻者列表</string>
|
||||
<string name="translation_repository">譯文倉庫</string>
|
||||
<string name="resource_settings">資源設定</string>
|
||||
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
|
||||
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
|
||||
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
|
||||
<string name="resource_url">資源地址</string>
|
||||
<string name="download">下載</string>
|
||||
<string name="invalid_zip_file">文件解析失敗</string>
|
||||
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="ok">確定</string>
|
||||
<string name="downloaded_resource_version">已下載資源版本</string>
|
||||
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
|
||||
<string name="warning">注意</string>
|
||||
<string name="install">安裝</string>
|
||||
<string name="installing">安裝中</string>
|
||||
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
|
||||
<string name="api_addr">API 地址(Github Latest Release API)</string>
|
||||
<string name="check_update">檢查更新</string>
|
||||
<string name="translation_resource_update">翻譯資源更新</string>
|
||||
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
|
||||
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API)</string>
|
||||
<string name="texture_resource_update">貼圖資源更新</string>
|
||||
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
|
||||
<string name="game_patch">遊戲修補</string>
|
||||
<string name="patch_mode">修補模式</string>
|
||||
<string name="patch_local">本地模式</string>
|
||||
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
|
||||
<string name="patch_integrated">集成模式</string>
|
||||
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
|
||||
<string name="shizuku_available">Shizuku 服務可用</string>
|
||||
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
|
||||
<string name="home_shizuku_warning">部分功能不可用</string>
|
||||
<string name="patch_debuggable">可調試</string>
|
||||
<string name="reserve_patched">安裝時保留修補包</string>
|
||||
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
|
||||
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
|
||||
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
|
||||
<string name="patch_finished">修補完成,是否開始安裝?</string>
|
||||
|
||||
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
||||
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<resources>
|
||||
<string name="app_name">Gakumas Localify</string>
|
||||
<string name="gakumas_localify">Gakumas Localify</string>
|
||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||
<string name="replace_font">替換字體</string>
|
||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||
<string name="setFpsTitle">最大 FPS (0 為保持遊戲原設定)</string>
|
||||
<string name="unlockAllLive">解鎖所有 Live</string>
|
||||
<string name="unlockAllLiveCostume">解鎖所有 Live 服裝</string>
|
||||
<string name="liveUseCustomeDress">Live 使用自定義角色</string>
|
||||
<string name="live_costume_head_id">Live 自定義頭部 ID (例: costume_head_hski-cstm-0002)</string>
|
||||
<string name="live_custome_dress_id">Live 自定義服裝 ID (例: hski-cstm-0002)</string>
|
||||
<string name="useCustomeGraphicSettings">使用自定義畫質設定</string>
|
||||
<string name="renderscale">RenderScale (0.5/0.59/0.67/0.77/1.0)</string>
|
||||
<string name="text_hook_test_mode">文本 hook 測試模式</string>
|
||||
<string name="useMasterDBTrans">使用 MasterDB 翻譯</string>
|
||||
<string name="export_text">導出文本</string>
|
||||
<string name="dump_runtime_texture">導出運行時貼圖</string>
|
||||
<string name="force_export_resource">啟動後強制導出資源</string>
|
||||
<string name="login_as_ios">模擬以 iOS 登入</string>
|
||||
<string name="max_high">極高</string>
|
||||
<string name="very_high">超高</string>
|
||||
<string name="hign">高</string>
|
||||
<string name="middle">中</string>
|
||||
<string name="low">低</string>
|
||||
<string name="orientation_orig">原版</string>
|
||||
<string name="orientation_portrait">豎屏</string>
|
||||
<string name="orientation_landscape">橫屏</string>
|
||||
<string name="orientation_lock">方向鎖定</string>
|
||||
<string name="enable_breast_param">啟用胸部參數</string>
|
||||
<string name="damping">阻尼 (Damping)</string>
|
||||
<string name="stiffness">剛度 (Stiffness)</string>
|
||||
<string name="spring">彈簧係數 (Spring)</string>
|
||||
<string name="pendulum">鐘擺係數 (Pendulum)</string>
|
||||
<string name="pendulumrange">鐘擺範圍 (PendulumRange)</string>
|
||||
<string name="average">Average</string>
|
||||
<string name="rootweight">RootWeight</string>
|
||||
<string name="uselimit_0_1">範圍限制倍率 (0 為不限制, 1 為原版)</string>
|
||||
<string name="usearmcorrection">使用手臂矯正</string>
|
||||
<string name="isdirty">IsDirty</string>
|
||||
<string name="usescale">應用縮放</string>
|
||||
<string name="breast_scale">胸部縮放倍率</string>
|
||||
<string name="uselimitmultiplier">啟用範圍限制倍率</string>
|
||||
<string name="axisx_x">axisX.x</string>
|
||||
<string name="axisy_x">axisY.x</string>
|
||||
<string name="axisz_x">axisZ.x</string>
|
||||
<string name="axisx_y">axisX.y</string>
|
||||
<string name="axisy_y">axisY.y</string>
|
||||
<string name="axisz_y">axisZ.y</string>
|
||||
<string name="basic_settings">基本設定</string>
|
||||
<string name="graphic_settings">畫面設定</string>
|
||||
<string name="camera_settings">攝影機設定</string>
|
||||
<string name="test_mode_live">測試模式 - LIVE</string>
|
||||
<string name="debug_settings">調試設定</string>
|
||||
<string name="breast_param">胸部參數</string>
|
||||
<string name="about">關於</string>
|
||||
<string name="home">主頁</string>
|
||||
<string name="advanced_settings">進階設定</string>
|
||||
<string name="about_warn_title">使用前警告</string>
|
||||
<string name="about_warn_p1">本插件僅供學習和交流使用。</string>
|
||||
<string name="about_warn_p2">使用外部插件屬於違反遊戲條款的行為。若使用插件後帳號被封禁,造成的後果由用户自行承擔。</string>
|
||||
<string name="about_about_title">關於本插件</string>
|
||||
<string name="about_about_p1">本插件完全免費。若您付費購買了本插件,請檢舉店家。</string>
|
||||
<string name="about_about_p2">插件交流QQ群: 991990192</string>
|
||||
<string name="project_contribution">項目貢獻</string>
|
||||
<string name="plugin_code">插件本體</string>
|
||||
<string name="contributors">貢獻者列表</string>
|
||||
<string name="translation_repository">譯文倉庫</string>
|
||||
<string name="resource_settings">資源設定</string>
|
||||
<string name="check_built_in_resource">檢查內置翻譯資源更新</string>
|
||||
<string name="delete_plugin_resource">清除遊戲目錄內的插件翻譯資源</string>
|
||||
<string name="use_remote_zip_resource">使用雲端 ZIP 翻譯資源</string>
|
||||
<string name="resource_url">資源地址</string>
|
||||
<string name="download">下載</string>
|
||||
<string name="invalid_zip_file">文件解析失敗</string>
|
||||
<string name="invalid_zip_file_warn">此 ZIP 文件不是一個有效的翻譯資源包</string>
|
||||
<string name="cancel">取消</string>
|
||||
<string name="ok">確定</string>
|
||||
<string name="downloaded_resource_version">已下載資源版本</string>
|
||||
<string name="del_remote_after_update">替換文件後刪除下載緩存</string>
|
||||
<string name="warning">注意</string>
|
||||
<string name="install">安裝</string>
|
||||
<string name="installing">安裝中</string>
|
||||
<string name="check_resource_from_api">从伺服器檢查更新資源</string>
|
||||
<string name="api_addr">API 地址(Github Latest Release API)</string>
|
||||
<string name="check_update">檢查更新</string>
|
||||
<string name="translation_resource_update">翻譯資源更新</string>
|
||||
<string name="check_texture_resource_from_api">从伺服器檢查貼圖資源更新</string>
|
||||
<string name="texture_api_addr">貼圖 API 地址(Github Latest Release API)</string>
|
||||
<string name="texture_resource_update">貼圖資源更新</string>
|
||||
<string name="downloaded_texture_resource_version">已下載貼圖資源版本</string>
|
||||
<string name="game_patch">遊戲修補</string>
|
||||
<string name="patch_mode">修補模式</string>
|
||||
<string name="patch_local">本地模式</string>
|
||||
<string name="patch_local_desc">為未嵌入模塊的遊戲程式打補丁。\nXposed 範圍可動態更改,無需重新打補丁。\n以本地模式修補的遊戲程式只能在本地設備上執行。</string>
|
||||
<string name="patch_integrated">集成模式</string>
|
||||
<string name="patch_integrated_desc">修補遊戲程式並內置模塊。\n經集成模式修補的遊戲可以在沒有插件管理器的情况下執行,但不能動態管理設定。\n以集成模式修補的遊戲可在未安裝 LSPatch 管理器的設備上執行。</string>
|
||||
<string name="shizuku_available">Shizuku 服務可用</string>
|
||||
<string name="shizuku_unavailable">Shizuku 服務未連接</string>
|
||||
<string name="home_shizuku_warning">部分功能不可用</string>
|
||||
<string name="patch_debuggable">可調試</string>
|
||||
<string name="reserve_patched">安裝時保留修補包</string>
|
||||
<string name="support_file_types">支援文件類型:\n單/多選 apk\n單選 apks, xapk, zip</string>
|
||||
<string name="patch_uninstall_text">由於程式簽名不同,安裝修補版的遊戲前需要先刪除原版。\n請確保您已備份好個人資料。</string>
|
||||
<string name="patch_uninstall_confirm">您確定要刪除吗</string>
|
||||
<string name="patch_finished">修補完成,是否開始安裝?</string>
|
||||
|
||||
<string name="about_contributors_asset_file">about_contributors_zh_cn.json</string>
|
||||
<string name="default_assets_check_api">https://uma.chinosk6.cn/api/gkms_trans_data</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<string name="gakumas_localify">Gakumas Localify</string>
|
||||
<string name="enable_plugin">Enable Plugin (Not Hot Reloadable)</string>
|
||||
<string name="replace_font">Replace Font</string>
|
||||
<string name="replace_texture">Replace Texture</string>
|
||||
<string name="lazy_init">Fast Initialization (Lazy loading)</string>
|
||||
<string name="enable_free_camera">Enable Free Camera</string>
|
||||
<string name="start_game">Start Game / Hot Reload Config</string>
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
<string name="text_hook_test_mode">Text Hook Test Mode</string>
|
||||
<string name="useMasterDBTrans">Enable MasterDB Localization</string>
|
||||
<string name="export_text">Export Text</string>
|
||||
<string name="dump_runtime_texture">Dump Runtime Texture</string>
|
||||
<string name="force_export_resource">Force Update Resource</string>
|
||||
<string name="login_as_ios">Login as iOS</string>
|
||||
<string name="max_high">Ultra</string>
|
||||
|
|
@ -86,6 +88,10 @@
|
|||
<string name="api_addr">API Address(Github Latest Release API)</string>
|
||||
<string name="check_update">Check</string>
|
||||
<string name="translation_resource_update">Translation Resource Update</string>
|
||||
<string name="check_texture_resource_from_api">Check Texture Resource Update From API</string>
|
||||
<string name="texture_api_addr">Texture API Address (Github Latest Release API)</string>
|
||||
<string name="texture_resource_update">Texture Resource Update</string>
|
||||
<string name="downloaded_texture_resource_version">Downloaded Texture Version</string>
|
||||
|
||||
<string name="game_patch">Game Patch</string>
|
||||
<string name="patch_mode">Patch Mode</string>
|
||||
|
|
@ -105,4 +111,5 @@
|
|||
|
||||
<string name="about_contributors_asset_file">about_contributors_en.json</string>
|
||||
<string name="default_assets_check_api">https://api.github.com/repos/NatsumeLS/Gakumas-Translation-Data-EN/releases/latest</string>
|
||||
<string name="default_texture_assets_check_api">https://texture.gakumas.cn/api/gkms_texture_data</string>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue