diff --git a/src/GakumasLocalify/Hook.cpp b/src/GakumasLocalify/Hook.cpp index 060aee7..1fc283e 100644 --- a/src/GakumasLocalify/Hook.cpp +++ b/src/GakumasLocalify/Hook.cpp @@ -869,7 +869,16 @@ namespace GakumasLocal::HookMain { } #endif - std::unordered_set updatedFontPtrs{}; + // A managed object address can be reused after the original object is collected, and the + // replacement Font is dropped by Resources.UnloadUnusedAssets on scene loads. Pair the address + // with both InstanceIDs so a recycled address or a reloaded Font is set up again - and so the + // map is never cleared wholesale, which would re-initialize every live asset and reshuffle the + // atlas under text that is already on screen. + struct UpdatedFontState { + int fontAssetInstanceId; + int sourceFontInstanceId; + }; + std::unordered_map updatedFontStates{}; void UpdateFont(void* TMP_Textself) { if (!Config::replaceFont) return; static auto get_font = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", @@ -888,8 +897,39 @@ namespace GakumasLocal::HookMain { static auto set_sourceFontFile = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_sourceFontFile"); - static auto UpdateFontAssetData = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", - "TMP_FontAsset", "UpdateFontAssetData"); + static auto set_atlasPopulationMode = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasPopulationMode", + { "TMPro.AtlasPopulationMode" }); + static auto set_isMultiAtlasTexturesEnabled = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_isMultiAtlasTexturesEnabled", + { "System.Boolean" }); + static auto ClearFontAssetData = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "ClearFontAssetData", + { "System.Boolean" }); + static auto get_atlasWidth = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", + "TMP_FontAsset", "get_atlasWidth"); + static auto set_atlasWidth = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasWidth", + { "System.Int32" }); + static auto set_atlasHeight = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMP_FontAsset", "set_atlasHeight", + { "System.Int32" }); + static auto get_atlasTextureCount = Il2cppUtils::GetMethod("Unity.TextMeshPro.dll", "TMPro", + "TMP_FontAsset", "get_atlasTextureCount"); + static auto GetInstanceID = Il2cppUtils::GetMethod( + "UnityEngine.CoreModule.dll", "UnityEngine", "Object", "GetInstanceID"); + // TMP's own "this font asset changed, re-layout everything using it" broadcast. + static auto ON_FONT_PROPERTY_CHANGED = Il2cppUtils::GetMethod( + "Unity.TextMeshPro.dll", "TMPro", "TMPro_EventManager", "ON_FONT_PROPERTY_CHANGED", + { "System.Boolean", "UnityEngine.Object" }); + + if (!get_font || !set_font || !get_name || !set_sourceFontFile || !set_atlasPopulationMode || + !set_isMultiAtlasTexturesEnabled || !ClearFontAssetData || !get_atlasWidth || + !set_atlasWidth || !set_atlasHeight || !get_atlasTextureCount || !GetInstanceID || + !ON_FONT_PROPERTY_CHANGED) { + Log::Error("UpdateFont disabled: a TMP_FontAsset method could not be resolved"); + return; + } auto fontAsset = get_font->Invoke(TMP_Textself); if (!fontAsset) { @@ -910,13 +950,61 @@ namespace GakumasLocal::HookMain { auto newFont = GetReplaceFont(); if (!newFont) return; - set_sourceFontFile->Invoke(fontAsset, newFont); - if (!updatedFontPtrs.contains(fontAsset)) { - updatedFontPtrs.emplace(fontAsset); - UpdateFontAssetData->Invoke(fontAsset); + const auto fontInstanceId = GetInstanceID->Invoke(fontAsset); + const auto sourceFontInstanceId = GetInstanceID->Invoke(newFont); + const auto known = updatedFontStates.find(fontAsset); + const bool isNewAsset = known == updatedFontStates.end() || + known->second.fontAssetInstanceId != fontInstanceId; + const bool sourceFontReloaded = !isNewAsset && + known->second.sourceFontInstanceId != sourceFontInstanceId; + if (isNewAsset || sourceFontReloaded) { + // Mark the asset before rebuilding it: TMP can re-enter one of the text hooks below + // while the rebuild is in progress. + updatedFontStates.insert_or_assign( + fontAsset, UpdatedFontState{ fontInstanceId, sourceFontInstanceId }); + set_sourceFontFile->Invoke(fontAsset, newFont); } - if (updatedFontPtrs.size() > 200) updatedFontPtrs.clear(); - + // Only a genuinely new asset is rebuilt. Resources.UnloadUnusedAssets drops the replacement + // Font on most scene loads, and re-clearing on every reload throws away glyphs that text + // already on screen is still pointing at - it keeps its stale atlas coordinates and renders + // the wrong characters, or nothing at all once the atlas is no longer refilled. The glyphs + // already in the atlas came from an identical font file, so a reloaded Font can simply be + // re-assigned without touching them. + if (isNewAsset) { + set_atlasPopulationMode->Invoke(fontAsset, 1); // AtlasPopulationMode.Dynamic + set_isMultiAtlasTexturesEnabled->Invoke(fontAsset, true); + + // Glyphs that do not fit on atlas page 0 get a second material, which puts them in + // textInfo.meshInfo[1]. The game's stylized text only submits meshInfo[0], so those + // characters vanish - correct advance, no quad - while ordinary UI still draws them. + // Measured from a frame capture: shelf pitch 79px, ~735 glyphs per 2048x2048 page, and + // page 0 was completely full, so the spill was guaranteed rather than occasional. + // 4096x4096 holds ~2900 glyphs (16MB of A8); 20 minutes of play reaches about a third. + // ponytail: still a ceiling. The atlasTextureCount check below reports when it is hit; + // the real fix is making the stylized text submit its sub meshes. + if (get_atlasWidth->Invoke(fontAsset) < 4096) { + set_atlasWidth->Invoke(fontAsset, 4096); + set_atlasHeight->Invoke(fontAsset, 4096); + } + // Clear, not UpdateFontAssetData: the latter re-adds the asset's entire original + // character table first, spending page 0 on glyphs the translation never shows. + ClearFontAssetData->Invoke(fontAsset, false); + // Clearing invalidates the atlas coordinates baked into every mesh already generated + // from this asset. Without this broadcast those texts keep the stale coordinates: the + // game's ruby text objects, laid out before the rebuild, showed wrong glyphs when the + // old character table was re-added and nothing at all once it was not. + ON_FONT_PROPERTY_CHANGED->Invoke(true, fontAsset); + } + + static int reportedAtlasPages = 1; + const auto atlasPages = get_atlasTextureCount->Invoke(fontAsset); + if (atlasPages > reportedAtlasPages) { + reportedAtlasPages = atlasPages; + Log::ErrorFmt("Font atlas spilled to page %d on [%s] - stylized text will start " + "dropping characters", + atlasPages - 1, fontAssetName ? fontAssetName->ToString().c_str() : "?"); + } + set_font->Invoke(TMP_Textself, fontAsset); // auto fontMaterial = get_material->Invoke(fontAsset);