mirror of
https://git.chinosk6.cn/chinosk/gkms-local.git
synced 2026-06-14 21:53:19 +07:00
Compatible with Unity 6
This commit is contained in:
+1
-1
@@ -16,7 +16,7 @@ android {
|
|||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 34
|
targetSdk 34
|
||||||
versionCode 12
|
versionCode 12
|
||||||
versionName "v3.3.1"
|
versionName "v3.4.0"
|
||||||
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
|
buildConfigField "String", "VERSION_NAME", "\"${versionName}\""
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ void UnHookAll() {
|
|||||||
|
|
||||||
namespace GakumasLocal::HookMain {
|
namespace GakumasLocal::HookMain {
|
||||||
using Il2cppString = UnityResolve::UnityType::String;
|
using Il2cppString = UnityResolve::UnityType::String;
|
||||||
|
using Il2CppGCHandle = void*;
|
||||||
|
|
||||||
UnityResolve::UnityType::String* environment_get_stacktrace() {
|
UnityResolve::UnityType::String* environment_get_stacktrace() {
|
||||||
/*
|
/*
|
||||||
@@ -71,23 +72,46 @@ namespace GakumasLocal::HookMain {
|
|||||||
return toString_mtd->Invoke<Il2cppString*>(klassInstance);
|
return toString_mtd->Invoke<Il2cppString*>(klassInstance);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_HOOK(void, Internal_LogException, (void* ex, void* obj)) {
|
// Unity 6: DebugLogHandler.Internal_Log* is resolved as a managed method
|
||||||
Internal_LogException_Orig(ex, obj);
|
// through GetMethod(), so the generated IL2CPP function includes the
|
||||||
|
// trailing MethodInfo* argument.
|
||||||
|
DEFINE_HOOK(void, Internal_LogException, (void* ex, void* obj, void* mtd)) {
|
||||||
|
Internal_LogException_Orig(ex, obj, mtd);
|
||||||
static auto Exception_ToString = Il2cppUtils::GetMethod("mscorlib.dll", "System", "Exception", "ToString");
|
static auto Exception_ToString = Il2cppUtils::GetMethod("mscorlib.dll", "System", "Exception", "ToString");
|
||||||
Log::LogUnityLog(ANDROID_LOG_ERROR, "UnityLog - Internal_LogException:\n%s", Exception_ToString->Invoke<Il2cppString*>(ex)->ToString().c_str());
|
Log::LogUnityLog(ANDROID_LOG_ERROR, "UnityLog - Internal_LogException:\n%s", Exception_ToString->Invoke<Il2cppString*>(ex)->ToString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_HOOK(void, Internal_Log, (int logType, int logOption, UnityResolve::UnityType::String* content, void* context)) {
|
DEFINE_HOOK(void, Internal_Log, (int logType, int logOption, UnityResolve::UnityType::String* content, void* context, void* mtd)) {
|
||||||
Internal_Log_Orig(logType, logOption, content, context);
|
Internal_Log_Orig(logType, logOption, content, context, mtd);
|
||||||
// 2022.3.21f1
|
|
||||||
Log::LogUnityLog(ANDROID_LOG_VERBOSE, "Internal_Log:\n%s", content->ToString().c_str());
|
Log::LogUnityLog(ANDROID_LOG_VERBOSE, "Internal_Log:\n%s", content->ToString().c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IsNativeObjectAlive(void* obj) {
|
bool IsNativeObjectAlive(void* obj) {
|
||||||
static UnityResolve::Method* IsNativeObjectAliveMtd = nullptr;
|
if (!obj) {
|
||||||
if (!IsNativeObjectAliveMtd) IsNativeObjectAliveMtd = Il2cppUtils::GetMethod("UnityEngine.CoreModule.dll", "UnityEngine",
|
return false;
|
||||||
"Object", "IsNativeObjectAlive");
|
}
|
||||||
return IsNativeObjectAliveMtd->Invoke<bool>(obj);
|
|
||||||
|
static const auto IsNativeObjectAliveMtd = Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.CoreModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"Object",
|
||||||
|
"IsNativeObjectAlive",
|
||||||
|
{ "UnityEngine.Object" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!IsNativeObjectAliveMtd || !IsNativeObjectAliveMtd->function) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using IsNativeObjectAliveFn = bool (*)(void* obj, void* method);
|
||||||
|
const auto isNativeObjectAlive = reinterpret_cast<IsNativeObjectAliveFn>(
|
||||||
|
IsNativeObjectAliveMtd->function
|
||||||
|
);
|
||||||
|
|
||||||
|
return isNativeObjectAlive(
|
||||||
|
obj,
|
||||||
|
IsNativeObjectAliveMtd->address
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
UnityResolve::UnityType::Camera* mainCameraCache = nullptr;
|
UnityResolve::UnityType::Camera* mainCameraCache = nullptr;
|
||||||
@@ -343,7 +367,10 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_HOOK(void, CanvasRenderer_SetTexture, (void* self, void* texture)) {
|
DEFINE_HOOK(void, CanvasRenderer_SetTexture, (void* self, void* texture)) {
|
||||||
CanvasRenderer_SetTexture_Orig(self, ReplaceTextureOrSpriteByObjectName(texture));
|
CanvasRenderer_SetTexture_Orig(
|
||||||
|
self,
|
||||||
|
ReplaceTextureOrSpriteByObjectName(texture)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFINE_HOOK(void, SpriteRenderer_set_sprite, (void* self, void* sprite)) {
|
DEFINE_HOOK(void, SpriteRenderer_set_sprite, (void* self, void* sprite)) {
|
||||||
@@ -372,7 +399,6 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#ifdef GKMS_WINDOWS
|
#ifdef GKMS_WINDOWS
|
||||||
struct TransparentStringHash : std::hash<std::wstring>, std::hash<std::wstring_view>
|
struct TransparentStringHash : std::hash<std::wstring>, std::hash<std::wstring_view>
|
||||||
{
|
{
|
||||||
@@ -381,7 +407,7 @@ namespace GakumasLocal::HookMain {
|
|||||||
|
|
||||||
typedef std::unordered_set<std::wstring, TransparentStringHash, std::equal_to<void>> AssetPathsType;
|
typedef std::unordered_set<std::wstring, TransparentStringHash, std::equal_to<void>> AssetPathsType;
|
||||||
std::map<std::string, AssetPathsType> CustomAssetBundleAssetPaths;
|
std::map<std::string, AssetPathsType> CustomAssetBundleAssetPaths;
|
||||||
std::unordered_map<std::string, uint32_t> CustomAssetBundleHandleMap{};
|
std::unordered_map<std::string, Il2CppGCHandle> CustomAssetBundleHandleMap{};
|
||||||
std::list<std::string> g_extra_assetbundle_paths{};
|
std::list<std::string> g_extra_assetbundle_paths{};
|
||||||
|
|
||||||
void LoadExtraAssetBundle() {
|
void LoadExtraAssetBundle() {
|
||||||
@@ -394,9 +420,12 @@ namespace GakumasLocal::HookMain {
|
|||||||
// CustomAssetBundleAssetPaths.clear();
|
// CustomAssetBundleAssetPaths.clear();
|
||||||
// assert(!ExtraAssetBundleHandle && ExtraAssetBundleAssetPaths.empty());
|
// assert(!ExtraAssetBundleHandle && ExtraAssetBundleAssetPaths.empty());
|
||||||
|
|
||||||
static auto AssetBundle_GetAllAssetNames = reinterpret_cast<void* (*)(void*)>(
|
static auto AssetBundle_GetAllAssetNames = Il2cppUtils::GetMethod(
|
||||||
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundle::GetAllAssetNames()")
|
"UnityEngine.AssetBundleModule.dll",
|
||||||
);
|
"UnityEngine",
|
||||||
|
"AssetBundle",
|
||||||
|
"GetAllAssetNames"
|
||||||
|
);
|
||||||
|
|
||||||
for (const auto& i : g_extra_assetbundle_paths) {
|
for (const auto& i : g_extra_assetbundle_paths) {
|
||||||
if (CustomAssetBundleHandleMap.contains(i)) continue;
|
if (CustomAssetBundleHandleMap.contains(i)) continue;
|
||||||
@@ -404,7 +433,7 @@ namespace GakumasLocal::HookMain {
|
|||||||
const auto extraAssetBundle = WinHooks::LoadAssetBundle(i);
|
const auto extraAssetBundle = WinHooks::LoadAssetBundle(i);
|
||||||
if (extraAssetBundle)
|
if (extraAssetBundle)
|
||||||
{
|
{
|
||||||
const auto allAssetPaths = AssetBundle_GetAllAssetNames(extraAssetBundle);
|
const auto allAssetPaths = AssetBundle_GetAllAssetNames->Invoke<void*>(extraAssetBundle);
|
||||||
AssetPathsType assetPath{};
|
AssetPathsType assetPath{};
|
||||||
Il2cppUtils::iterate_IEnumerable<Il2CppString*>(allAssetPaths, [&assetPath](Il2CppString* path)
|
Il2cppUtils::iterate_IEnumerable<Il2CppString*>(allAssetPaths, [&assetPath](Il2CppString* path)
|
||||||
{
|
{
|
||||||
@@ -413,7 +442,8 @@ namespace GakumasLocal::HookMain {
|
|||||||
assetPath.emplace(path->start_char);
|
assetPath.emplace(path->start_char);
|
||||||
});
|
});
|
||||||
CustomAssetBundleAssetPaths.emplace(i, assetPath);
|
CustomAssetBundleAssetPaths.emplace(i, assetPath);
|
||||||
CustomAssetBundleHandleMap.emplace(i, UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", extraAssetBundle, false));
|
const auto bundleHandle = UnityResolve::Invoke<Il2CppGCHandle>("il2cpp_gchandle_new", extraAssetBundle, false);
|
||||||
|
CustomAssetBundleHandleMap.emplace(i, bundleHandle);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -422,7 +452,7 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t GetBundleHandleByAssetName(std::wstring assetName) {
|
Il2CppGCHandle GetBundleHandleByAssetName(std::wstring assetName) {
|
||||||
for (const auto& i : CustomAssetBundleAssetPaths) {
|
for (const auto& i : CustomAssetBundleAssetPaths) {
|
||||||
for (const auto& m : i.second) {
|
for (const auto& m : i.second) {
|
||||||
if (std::equal(m.begin(), m.end(), assetName.begin(), assetName.end(),
|
if (std::equal(m.begin(), m.end(), assetName.begin(), assetName.end(),
|
||||||
@@ -433,14 +463,14 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NULL;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t GetBundleHandleByAssetName(std::string assetName) {
|
Il2CppGCHandle GetBundleHandleByAssetName(std::string assetName) {
|
||||||
return GetBundleHandleByAssetName(utility::conversions::to_string_t(assetName));
|
return GetBundleHandleByAssetName(utility::conversions::to_string_t(assetName));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t ReplaceFontHandle;
|
Il2CppGCHandle ReplaceFontHandle = nullptr;
|
||||||
|
|
||||||
void* GetReplaceFont() {
|
void* GetReplaceFont() {
|
||||||
static auto FontClass = Il2cppUtils::GetClass("UnityEngine.TextRenderingModule.dll", "UnityEngine", "Font");
|
static auto FontClass = Il2cppUtils::GetClass("UnityEngine.TextRenderingModule.dll", "UnityEngine", "Font");
|
||||||
@@ -451,7 +481,7 @@ namespace GakumasLocal::HookMain {
|
|||||||
const auto fontPath = "assets/fonts/gkamszhfontmix.otf";
|
const auto fontPath = "assets/fonts/gkamszhfontmix.otf";
|
||||||
|
|
||||||
void* replaceFont{};
|
void* replaceFont{};
|
||||||
const auto& bundleHandle = GetBundleHandleByAssetName(fontPath);
|
const auto bundleHandle = GetBundleHandleByAssetName(fontPath);
|
||||||
if (bundleHandle)
|
if (bundleHandle)
|
||||||
{
|
{
|
||||||
if (ReplaceFontHandle)
|
if (ReplaceFontHandle)
|
||||||
@@ -466,19 +496,23 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UnityResolve::Invoke<void>("il2cpp_gchandle_free", std::exchange(ReplaceFontHandle, 0));
|
UnityResolve::Invoke<void>("il2cpp_gchandle_free", std::exchange(ReplaceFontHandle, nullptr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto extraAssetBundle = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", bundleHandle);
|
const auto extraAssetBundle = UnityResolve::Invoke<void*>("il2cpp_gchandle_get_target", bundleHandle);
|
||||||
static auto AssetBundle_LoadAsset = reinterpret_cast<void* (*)(void* _this, Il2CppString* name, Il2cppUtils::Il2CppReflectionType* type)>(
|
static auto AssetBundle_LoadAsset = Il2cppUtils::GetMethod(
|
||||||
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.AssetBundle::LoadAsset_Internal(System.String,System.Type)")
|
"UnityEngine.AssetBundleModule.dll",
|
||||||
);;
|
"UnityEngine",
|
||||||
|
"AssetBundle",
|
||||||
|
"LoadAsset_Internal",
|
||||||
|
{ "System.String", "System.Type" }
|
||||||
|
);
|
||||||
|
|
||||||
replaceFont = AssetBundle_LoadAsset(extraAssetBundle, Il2cppString::New(fontPath), Font_Type);
|
replaceFont = AssetBundle_LoadAsset->Invoke<void*>(extraAssetBundle, Il2cppString::New(fontPath), Font_Type);
|
||||||
if (replaceFont)
|
if (replaceFont)
|
||||||
{
|
{
|
||||||
ReplaceFontHandle = UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", replaceFont, false);
|
ReplaceFontHandle = UnityResolve::Invoke<Il2CppGCHandle>("il2cpp_gchandle_new", replaceFont, false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -493,19 +527,72 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
void* fontCache = nullptr;
|
void* fontCache = nullptr;
|
||||||
|
bool CreateFontFromPath(void* font, const std::filesystem::path& fontName) {
|
||||||
|
if (!font) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto fontPath = Il2cppString::New(fontName.string());
|
||||||
|
if (!fontPath) {
|
||||||
|
Log::Error("CreateFontFromPath failed: cannot create path string");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto CreateFontFromPathIcall = reinterpret_cast<void (*)(void* self, Il2cppString* path)>(
|
||||||
|
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Font::Internal_CreateFontFromPath(UnityEngine.Font,System.String)")
|
||||||
|
);
|
||||||
|
if (CreateFontFromPathIcall) {
|
||||||
|
CreateFontFromPathIcall(font, fontPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto CreateFontFromPathMethod = [] {
|
||||||
|
auto method = Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.TextRenderingModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"Font",
|
||||||
|
"Internal_CreateFontFromPath",
|
||||||
|
{ "UnityEngine.Font", "System.String" }
|
||||||
|
);
|
||||||
|
if (method) {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.TextRenderingModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"Font",
|
||||||
|
"Internal_CreateFontFromPath"
|
||||||
|
);
|
||||||
|
}();
|
||||||
|
if (!CreateFontFromPathMethod || !CreateFontFromPathMethod->function || !CreateFontFromPathMethod->address) {
|
||||||
|
Log::Error("CreateFontFromPath failed: method not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using CreateFontFromPathManagedFn = void (*)(void* font, Il2cppString* path, void* method);
|
||||||
|
const auto createFontFromPath = reinterpret_cast<CreateFontFromPathManagedFn>(
|
||||||
|
CreateFontFromPathMethod->function
|
||||||
|
);
|
||||||
|
createFontFromPath(font, fontPath, CreateFontFromPathMethod->address);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void* GetReplaceFont() {
|
void* GetReplaceFont() {
|
||||||
static auto fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
|
static auto fontName = Local::GetBasePath() / "local-files" / "gkamsZHFontMIX.otf";
|
||||||
if (!std::filesystem::exists(fontName)) {
|
if (!std::filesystem::exists(fontName)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static auto CreateFontFromPath = reinterpret_cast<void (*)(void* self, Il2cppString* path)>(
|
|
||||||
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Font::Internal_CreateFontFromPath(UnityEngine.Font,System.String)")
|
|
||||||
);
|
|
||||||
static auto Font_klass = Il2cppUtils::GetClass("UnityEngine.TextRenderingModule.dll",
|
static auto Font_klass = Il2cppUtils::GetClass("UnityEngine.TextRenderingModule.dll",
|
||||||
"UnityEngine", "Font");
|
"UnityEngine", "Font");
|
||||||
static auto Font_ctor = Il2cppUtils::GetMethod("UnityEngine.TextRenderingModule.dll",
|
static auto Font_ctor = Il2cppUtils::GetMethod("UnityEngine.TextRenderingModule.dll",
|
||||||
"UnityEngine", "Font", ".ctor");
|
"UnityEngine", "Font", ".ctor");
|
||||||
|
if (!Font_klass || !Font_ctor) {
|
||||||
|
Log::Error("GetReplaceFont failed: Font class or constructor not found");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
if (fontCache) {
|
if (fontCache) {
|
||||||
if (IsNativeObjectAlive(fontCache)) {
|
if (IsNativeObjectAlive(fontCache)) {
|
||||||
return fontCache;
|
return fontCache;
|
||||||
@@ -513,9 +600,16 @@ namespace GakumasLocal::HookMain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auto newFont = Font_klass->New<void*>();
|
const auto newFont = Font_klass->New<void*>();
|
||||||
|
if (!newFont) {
|
||||||
|
Log::Error("GetReplaceFont failed: cannot create Font instance");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
Font_ctor->Invoke<void>(newFont);
|
Font_ctor->Invoke<void>(newFont);
|
||||||
|
|
||||||
CreateFontFromPath(newFont, Il2cppString::New(fontName.string()));
|
if (!CreateFontFromPath(newFont, fontName)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
fontCache = newFont;
|
fontCache = newFont;
|
||||||
return newFont;
|
return newFont;
|
||||||
}
|
}
|
||||||
@@ -1753,7 +1847,18 @@ namespace GakumasLocal::HookMain {
|
|||||||
UnityResolve::Mode::Il2Cpp, Config::lazyInit);
|
UnityResolve::Mode::Il2Cpp, Config::lazyInit);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Temporarily isolate texture replacement to CanvasRenderer.SetTexture only.
|
// Texture replacement is currently isolated to CanvasRenderer.SetTexture.
|
||||||
|
//
|
||||||
|
// Unity 6 compatibility warning:
|
||||||
|
// The resolver helpers below may return either a native icall address or
|
||||||
|
// a managed IL2CPP methodPointer fallback. These two targets do not share
|
||||||
|
// the same ABI: a managed methodPointer normally has a trailing MethodInfo*
|
||||||
|
// argument, while the current hook declarations use the icall-style
|
||||||
|
// signatures. Do not simply uncomment these ADD_HOOK calls on Unity 6.
|
||||||
|
// Before restoring them, either:
|
||||||
|
// 1. make each resolver return only a verified icall address; or
|
||||||
|
// 2. return ABI metadata and install a separate managed hook signature
|
||||||
|
// that preserves and forwards the trailing MethodInfo* argument.
|
||||||
// ADD_HOOK(AssetBundle_LoadAsset, ResolveAssetBundleLoadAssetHookAddress());
|
// ADD_HOOK(AssetBundle_LoadAsset, ResolveAssetBundleLoadAssetHookAddress());
|
||||||
// ADD_HOOK(AssetBundle_LoadAssetAsync, ResolveAssetBundleLoadAssetAsyncHookAddress());
|
// ADD_HOOK(AssetBundle_LoadAssetAsync, ResolveAssetBundleLoadAssetAsyncHookAddress());
|
||||||
// ADD_HOOK(AssetBundleRequest_GetResult, ResolveAssetBundleRequestResultHookAddress());
|
// ADD_HOOK(AssetBundleRequest_GetResult, ResolveAssetBundleRequestResultHookAddress());
|
||||||
@@ -1992,10 +2097,41 @@ namespace GakumasLocal::HookMain {
|
|||||||
Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common",
|
Il2cppUtils::GetMethodPointer("campus-submodule.Runtime.dll", "Campus.Common",
|
||||||
"CampusQualityManager", "set_TargetFrameRate"));
|
"CampusQualityManager", "set_TargetFrameRate"));
|
||||||
|
|
||||||
ADD_HOOK(Internal_LogException, Il2cppUtils::il2cpp_resolve_icall(
|
// Unity 6 no longer exposes these two methods through the old icall
|
||||||
"UnityEngine.DebugLogHandler::Internal_LogException(System.Exception,UnityEngine.Object)"));
|
// names in this player. Resolve their managed IL2CPP methodPointer
|
||||||
ADD_HOOK(Internal_Log, Il2cppUtils::il2cpp_resolve_icall(
|
// through GetMethod() and use hook signatures with a trailing MethodInfo*.
|
||||||
"UnityEngine.DebugLogHandler::Internal_Log(UnityEngine.LogType,UnityEngine.LogOption,System.String,UnityEngine.Object)"));
|
const auto Internal_LogException_Method = Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.CoreModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"DebugLogHandler",
|
||||||
|
"Internal_LogException",
|
||||||
|
{ "System.Exception", "UnityEngine.Object" }
|
||||||
|
);
|
||||||
|
ADD_HOOK(
|
||||||
|
Internal_LogException,
|
||||||
|
Internal_LogException_Method
|
||||||
|
? Internal_LogException_Method->function
|
||||||
|
: nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto Internal_Log_Method = Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.CoreModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"DebugLogHandler",
|
||||||
|
"Internal_Log",
|
||||||
|
{
|
||||||
|
"UnityEngine.LogType",
|
||||||
|
"UnityEngine.LogOption",
|
||||||
|
"System.String",
|
||||||
|
"UnityEngine.Object"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
ADD_HOOK(
|
||||||
|
Internal_Log,
|
||||||
|
Internal_Log_Method
|
||||||
|
? Internal_Log_Method->function
|
||||||
|
: nullptr
|
||||||
|
);
|
||||||
|
|
||||||
// 双端
|
// 双端
|
||||||
ADD_HOOK(InternalSetOrientationAsync,
|
ADD_HOOK(InternalSetOrientationAsync,
|
||||||
@@ -2032,14 +2168,102 @@ namespace GakumasLocal::HookMain {
|
|||||||
// "AspectRatioHandler", "WindowProc"));
|
// "AspectRatioHandler", "WindowProc"));
|
||||||
|
|
||||||
if (GakumasLocal::Config::dmmUnlockSize) {
|
if (GakumasLocal::Config::dmmUnlockSize) {
|
||||||
std::thread([]() {
|
std::thread([]() {
|
||||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||||
auto hWnd = FindWindowW(L"UnityWndClass", L"gakumas");
|
|
||||||
// 添加可调整大小的边框和最大化按钮
|
const auto currentProcessId = GetCurrentProcessId();
|
||||||
LONG style = GetWindowLong(hWnd, GWL_STYLE);
|
HWND hWnd = nullptr;
|
||||||
style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
|
HWND candidate = nullptr;
|
||||||
SetWindowLong(hWnd, GWL_STYLE, style);
|
|
||||||
}).detach();
|
while ((candidate = FindWindowExW(
|
||||||
|
nullptr,
|
||||||
|
candidate,
|
||||||
|
L"UnityWndClass",
|
||||||
|
nullptr
|
||||||
|
)) != nullptr) {
|
||||||
|
DWORD windowProcessId = 0;
|
||||||
|
GetWindowThreadProcessId(
|
||||||
|
candidate,
|
||||||
|
&windowProcessId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (windowProcessId == currentProcessId) {
|
||||||
|
hWnd = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hWnd) {
|
||||||
|
Log::Error(
|
||||||
|
"DMM unlock size failed: Unity window not found."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetLastError(ERROR_SUCCESS);
|
||||||
|
|
||||||
|
auto style = GetWindowLongPtrW(
|
||||||
|
hWnd,
|
||||||
|
GWL_STYLE
|
||||||
|
);
|
||||||
|
|
||||||
|
if (style == 0 &&
|
||||||
|
GetLastError() != ERROR_SUCCESS) {
|
||||||
|
Log::ErrorFmt(
|
||||||
|
"DMM unlock size failed: "
|
||||||
|
"GetWindowLongPtrW error=%lu",
|
||||||
|
GetLastError()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
style |= WS_THICKFRAME |
|
||||||
|
WS_MAXIMIZEBOX;
|
||||||
|
|
||||||
|
SetLastError(ERROR_SUCCESS);
|
||||||
|
|
||||||
|
const auto previousStyle =
|
||||||
|
SetWindowLongPtrW(
|
||||||
|
hWnd,
|
||||||
|
GWL_STYLE,
|
||||||
|
style
|
||||||
|
);
|
||||||
|
|
||||||
|
if (previousStyle == 0 &&
|
||||||
|
GetLastError() != ERROR_SUCCESS) {
|
||||||
|
Log::ErrorFmt(
|
||||||
|
"DMM unlock size failed: "
|
||||||
|
"SetWindowLongPtrW error=%lu",
|
||||||
|
GetLastError()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SetWindowPos(
|
||||||
|
hWnd,
|
||||||
|
nullptr,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
SWP_NOMOVE |
|
||||||
|
SWP_NOSIZE |
|
||||||
|
SWP_NOZORDER |
|
||||||
|
SWP_NOACTIVATE |
|
||||||
|
SWP_FRAMECHANGED
|
||||||
|
)) {
|
||||||
|
Log::ErrorFmt(
|
||||||
|
"DMM unlock size failed: "
|
||||||
|
"SetWindowPos error=%lu",
|
||||||
|
GetLastError()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::Info(
|
||||||
|
"DMM window size unlocked."
|
||||||
|
);
|
||||||
|
}).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
g_extra_assetbundle_paths.push_back((gakumasLocalPath / "local-files/gakumasassets").string());
|
g_extra_assetbundle_paths.push_back((gakumasLocalPath / "local-files/gakumasassets").string());
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
namespace GakumasLocal::HookMain
|
namespace GakumasLocal::HookMain
|
||||||
{
|
{
|
||||||
using Il2cppString = UnityResolve::UnityType::String;
|
using Il2cppString = UnityResolve::UnityType::String;
|
||||||
|
using Il2CppGCHandle = void*;
|
||||||
|
|
||||||
extern void* (*Sprite_get_texture_Orig)(void* self);
|
extern void* (*Sprite_get_texture_Orig)(void* self);
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ namespace GakumasLocal::HookMain
|
|||||||
|
|
||||||
Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr;
|
Il2cppUtils::Il2CppClassHead* Texture2DClass = nullptr;
|
||||||
Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr;
|
Il2cppUtils::Il2CppClassHead* SpriteClass = nullptr;
|
||||||
std::unordered_map<std::string, uint32_t> LoadedLocalTextureHandles{};
|
std::unordered_map<std::string, Il2CppGCHandle> LoadedLocalTextureHandles{};
|
||||||
std::unordered_set<std::string> AppliedLocalTextureKeys{};
|
std::unordered_set<std::string> AppliedLocalTextureKeys{};
|
||||||
|
|
||||||
Il2cppUtils::Il2CppClassHead* GetTexture2DClass() {
|
Il2cppUtils::Il2CppClassHead* GetTexture2DClass() {
|
||||||
@@ -73,9 +74,35 @@ namespace GakumasLocal::HookMain
|
|||||||
Il2cppString* GetObjectName(void* obj) {
|
Il2cppString* GetObjectName(void* obj) {
|
||||||
if (!obj) return nullptr;
|
if (!obj) return nullptr;
|
||||||
|
|
||||||
static auto Object_GetName = reinterpret_cast<Il2cppString * (*)(void*)>(
|
static const auto Object_get_name =
|
||||||
Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Object::GetName(UnityEngine.Object)"));
|
Il2cppUtils::GetMethod(
|
||||||
return Object_GetName ? Object_GetName(obj) : nullptr;
|
"UnityEngine.CoreModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"Object",
|
||||||
|
"get_name"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Object_get_name ||
|
||||||
|
!Object_get_name->function) {
|
||||||
|
Log::Error("Cannot find UnityEngine.Object.get_name");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
using ObjectGetNameFn =
|
||||||
|
Il2cppString* (*)(
|
||||||
|
void* self,
|
||||||
|
void* method
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto getName =
|
||||||
|
reinterpret_cast<ObjectGetNameFn>(
|
||||||
|
Object_get_name->function
|
||||||
|
);
|
||||||
|
|
||||||
|
return getName(
|
||||||
|
obj,
|
||||||
|
Object_get_name->address
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetDontUnloadUnusedAsset(void* obj) {
|
void SetDontUnloadUnusedAsset(void* obj) {
|
||||||
@@ -494,6 +521,57 @@ namespace GakumasLocal::HookMain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool LoadImageFromByteArray(
|
||||||
|
void* texture,
|
||||||
|
void* byteArray,
|
||||||
|
bool markNonReadable
|
||||||
|
) {
|
||||||
|
if (!texture || !byteArray) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const auto LoadImageMethod =
|
||||||
|
Il2cppUtils::GetMethod(
|
||||||
|
"UnityEngine.ImageConversionModule.dll",
|
||||||
|
"UnityEngine",
|
||||||
|
"ImageConversion",
|
||||||
|
"LoadImage",
|
||||||
|
{
|
||||||
|
"UnityEngine.Texture2D",
|
||||||
|
"System.Byte[]",
|
||||||
|
"System.Boolean"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!LoadImageMethod ||
|
||||||
|
!LoadImageMethod->function) {
|
||||||
|
Log::Error(
|
||||||
|
"Cannot find ImageConversion.LoadImage(Texture2D, Byte[], Boolean)"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using LoadImageFn =
|
||||||
|
bool (*)(
|
||||||
|
void* texture,
|
||||||
|
void* byteArray,
|
||||||
|
bool markNonReadable,
|
||||||
|
void* method
|
||||||
|
);
|
||||||
|
|
||||||
|
const auto loadImage =
|
||||||
|
reinterpret_cast<LoadImageFn>(
|
||||||
|
LoadImageMethod->function
|
||||||
|
);
|
||||||
|
|
||||||
|
return loadImage(
|
||||||
|
texture,
|
||||||
|
byteArray,
|
||||||
|
markNonReadable,
|
||||||
|
LoadImageMethod->address
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void* LoadLocalTexture2D(const std::filesystem::path& path) {
|
void* LoadLocalTexture2D(const std::filesystem::path& path) {
|
||||||
if (!std::filesystem::is_regular_file(path)) return nullptr;
|
if (!std::filesystem::is_regular_file(path)) return nullptr;
|
||||||
|
|
||||||
@@ -516,33 +594,14 @@ namespace GakumasLocal::HookMain
|
|||||||
const auto ctor = textureClass ? Il2cppUtils::il2cpp_class_get_method_from_name(textureClass, ".ctor", 2) : nullptr;
|
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;
|
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 = [] {
|
static auto File_ReadAllBytes = [] {
|
||||||
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
|
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;
|
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;
|
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
if (!Texture2D_ctor || !ImageConversion_LoadImage || !File_ReadAllBytes) {
|
if (!Texture2D_ctor || !File_ReadAllBytes) {
|
||||||
Log::Error("LoadLocalTexture2D failed: Unity Texture2D/ImageConversion/File API not found.");
|
Log::Error("LoadLocalTexture2D failed: Unity Texture2D/File API not found.");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,19 +610,28 @@ namespace GakumasLocal::HookMain
|
|||||||
|
|
||||||
const auto texture = UnityResolve::Invoke<void*>("il2cpp_object_new", textureClass);
|
const auto texture = UnityResolve::Invoke<void*>("il2cpp_object_new", textureClass);
|
||||||
Texture2D_ctor(texture, 2, 2);
|
Texture2D_ctor(texture, 2, 2);
|
||||||
if (!ImageConversion_LoadImage(texture, fileBytes, false)) {
|
if (!LoadImageFromByteArray(texture, fileBytes, false)) {
|
||||||
Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str());
|
Log::ErrorFmt("LoadLocalTexture2D failed: %s", path.string().c_str());
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetDontUnloadUnusedAsset(texture);
|
SetDontUnloadUnusedAsset(texture);
|
||||||
LoadedLocalTextureHandles.emplace(cacheKey, UnityResolve::Invoke<uint32_t>("il2cpp_gchandle_new", texture, false));
|
const auto textureHandle = UnityResolve::Invoke<Il2CppGCHandle>("il2cpp_gchandle_new", texture, false);
|
||||||
|
if (!textureHandle) {
|
||||||
|
Log::ErrorFmt("Cannot create texture GCHandle: %s", path.string().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
LoadedLocalTextureHandles.emplace(cacheKey, textureHandle);
|
||||||
Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str());
|
Log::InfoFmt("Texture replaced from local file: %s", path.string().c_str());
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
void* LoadLocalTexture2DFromCandidates(const std::vector<std::filesystem::path>& candidates) {
|
void* LoadLocalTexture2DFromCandidates(const std::vector<std::filesystem::path>& candidates) {
|
||||||
for (const auto& candidate : candidates) {
|
for (const auto& candidate : candidates) {
|
||||||
|
if (!std::filesystem::is_regular_file(candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (auto texture = LoadLocalTexture2D(candidate)) {
|
if (auto texture = LoadLocalTexture2D(candidate)) {
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
@@ -578,40 +646,21 @@ namespace GakumasLocal::HookMain
|
|||||||
+ "|" + std::to_string(reinterpret_cast<std::uintptr_t>(texture2D));
|
+ "|" + std::to_string(reinterpret_cast<std::uintptr_t>(texture2D));
|
||||||
if (AppliedLocalTextureKeys.contains(cacheKey)) return true;
|
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 = [] {
|
static auto File_ReadAllBytes = [] {
|
||||||
const auto fileClass = Il2cppUtils::GetClass("mscorlib.dll", "System.IO", "File");
|
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;
|
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;
|
return method ? reinterpret_cast<void* (*)(Il2cppString*)>(method->methodPointer) : nullptr;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
if (!ImageConversion_LoadImage || !File_ReadAllBytes) {
|
if (!File_ReadAllBytes) {
|
||||||
Log::Error("ApplyLocalImageToTexture2D failed: Unity ImageConversion/File API not found.");
|
Log::Error("ApplyLocalImageToTexture2D failed: Unity File API not found.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
|
const auto fileBytes = File_ReadAllBytes(Il2cppString::New(path.string()));
|
||||||
if (!fileBytes) return false;
|
if (!fileBytes) return false;
|
||||||
|
|
||||||
if (!ImageConversion_LoadImage(texture2D, fileBytes, false)) {
|
if (!LoadImageFromByteArray(texture2D, fileBytes, false)) {
|
||||||
Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str());
|
Log::ErrorFmt("ApplyLocalImageToTexture2D failed: %s", path.string().c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -736,6 +785,16 @@ namespace GakumasLocal::HookMain
|
|||||||
return texture2D;
|
return texture2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unity 6 compatibility warning:
|
||||||
|
// The resolver functions below currently mix two possible target ABIs:
|
||||||
|
// - il2cpp_resolve_icall(): native icall signature
|
||||||
|
// - GetMethodPointer(): managed IL2CPP methodPointer, normally with a
|
||||||
|
// trailing MethodInfo* argument
|
||||||
|
//
|
||||||
|
// The corresponding AssetBundle / Resources / Sprite hooks in Hook.cpp are
|
||||||
|
// intentionally disabled. They must not be restored until the resolver and
|
||||||
|
// hook declarations distinguish these ABI forms, or until the managed
|
||||||
|
// fallback is removed and a verified icall is required.
|
||||||
void* ResolveSpriteGetTextureHookAddress() {
|
void* ResolveSpriteGetTextureHookAddress() {
|
||||||
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) {
|
if (const auto addr = Il2cppUtils::il2cpp_resolve_icall("UnityEngine.Sprite::get_texture(UnityEngine.Sprite)")) {
|
||||||
return addr;
|
return addr;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="gakumas_localify">Gakumas Localify</string>
|
<string name="gakumas_localify">Gakumas Localify</string>
|
||||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||||
<string name="replace_font">替換字體</string>
|
<string name="replace_font">替換字體</string>
|
||||||
|
<string name="replace_texture">替換貼圖</string>
|
||||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="gakumas_localify">Gakumas Localify</string>
|
<string name="gakumas_localify">Gakumas Localify</string>
|
||||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||||
<string name="replace_font">替換字體</string>
|
<string name="replace_font">替換字體</string>
|
||||||
|
<string name="replace_texture">替換貼圖</string>
|
||||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<string name="gakumas_localify">Gakumas Localify</string>
|
<string name="gakumas_localify">Gakumas Localify</string>
|
||||||
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
<string name="enable_plugin">啟用插件 (不可熱重載)</string>
|
||||||
<string name="replace_font">替換字體</string>
|
<string name="replace_font">替換字體</string>
|
||||||
|
<string name="replace_texture">替換貼圖</string>
|
||||||
<string name="lazy_init">快速初始化(懶人設定)</string>
|
<string name="lazy_init">快速初始化(懶人設定)</string>
|
||||||
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
<string name="enable_free_camera">啟用自由視角(可熱重載; 需使用實體鍵盤)</string>
|
||||||
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
<string name="start_game">以上述設定啟動遊戲/重載設定</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user