fix: 汉化后特效大字随机缺字

游戏的 TMP_FontAsset 是全局共享的 Dynamic 资产,特效大字与普通 UI
共用同一张 2048² 图集,单页只装约 735 个字形,主页几屏就会溢出到
第 1 页。第 1 页字形拿到第二个材质进入 meshInfo[1],而特效文字组件
只提交 meshInfo[0],于是这些字消失(保留 advance,表现为空格)。

修法(UpdateFont):
- 替换字体时把图集抬到 4096²(约 2900 字形),开启多图集;
- 用 ClearFontAssetData(false) 代替 UpdateFontAssetData,后者会把
  原字符表全集重新加回来白占第 0 页;
- 清空后广播 ON_FONT_PROPERTY_CHANGED,让已生成网格的文字重排,
  否则 ruby 等先排版的文字会拿着失效的图集坐标显示错字或空白;
- 以 (fontAsset 地址, InstanceID, sourceFont InstanceID) 追踪资产,
  只有真正新资产才重建;Resources.UnloadUnusedAssets 回收替换字体
  后重新赋值即可,不再清空图集。
- 图集溢出到第 1 页时记录错误日志,便于观察天花板。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
chihya72andClaude Fable 5.1 committed 2026-09-08 16:17:05 +08:00
1 parent 5af7956318
commit 937c45ba8c
1 file changed
+97 -9
+97 -9
View File
@@ -869,7 +869,16 @@ namespace GakumasLocal::HookMain {
}
#endif
std::unordered_set<void*> 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<void*, UpdatedFontState> 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<void*>(TMP_Textself);
if (!fontAsset) {
@@ -910,13 +950,61 @@ namespace GakumasLocal::HookMain {
auto newFont = GetReplaceFont();
if (!newFont) return;
set_sourceFontFile->Invoke<void>(fontAsset, newFont);
if (!updatedFontPtrs.contains(fontAsset)) {
updatedFontPtrs.emplace(fontAsset);
UpdateFontAssetData->Invoke<void>(fontAsset);
const auto fontInstanceId = GetInstanceID->Invoke<int>(fontAsset);
const auto sourceFontInstanceId = GetInstanceID->Invoke<int>(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<void>(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<void>(fontAsset, 1); // AtlasPopulationMode.Dynamic
set_isMultiAtlasTexturesEnabled->Invoke<void>(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<int>(fontAsset) < 4096) {
set_atlasWidth->Invoke<void>(fontAsset, 4096);
set_atlasHeight->Invoke<void>(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<void>(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<void>(true, fontAsset);
}
static int reportedAtlasPages = 1;
const auto atlasPages = get_atlasTextureCount->Invoke<int>(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<void>(TMP_Textself, fontAsset);
// auto fontMaterial = get_material->Invoke<void*>(fontAsset);