From afa6681e040fcaaf27c3c7d694f35453c0c580fc Mon Sep 17 00:00:00 2001 From: rouges78 Date: Fri, 21 Aug 2026 16:45:06 +0200 Subject: [PATCH] Try FText::ToString by symbol first, and record why it does not save us MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A symbol is deterministic: it is either there and correct, or absent. That is strictly better than a byte signature, so it now runs first, and the pattern scan stays as the fallback. ResolveFTextToStringBySymbol looks for ?ToString@FText@@QEBAAEBVFString@@XZ across every loaded module via Toolhelp. It does not fix Father's Day, and it cannot: symbol resolution needs the engine in DLLs, and a monolithic Shipping build has no UE symbols at all. Measured rather than assumed — the exe carries 312 exports and every one is a driver hint (NvOptimusEnablement, ags*), the PDB is named in the header but not shipped, and the only DLL beside it is turbojpeg. At runtime the resolver interrogates 142 modules and hits nothing. That module count is logged on purpose. Without it, a zero cannot separate "looked everywhere, not there" from "the snapshot failed and I looked at nothing" — the same trap as the "connesso via IPC" line, which reads like success and is half the chain. The check that confirms it is comparing against (Get-Process ...).Modules.Count: 142 against 142. So the value here is for non-monolithic and editor builds, plus a failure that now says what it did. Commercial games still fall through to the pattern. The four ambiguous candidates are measured and written down: four distinct functions sharing a prologue, diverging at byte 13 — inside the 17 the signature covers. Lengthening the signature would mean retuning it against the four games where it is already unique, with no ground truth to say which candidate is even the right one. The entry names the promising direction instead: anchoring on a string reference rather than a prologue. Verified on the real game: no regression, fallback chain intact, game responsive, both architectures rebuilt. Co-Authored-By: Claude Opus 5 --- docs/METODI-DI-TRADUZIONE.md | 38 +++++++++ gs-hook/src/sources/source_unreal_ftext.cpp | 85 +++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/docs/METODI-DI-TRADUZIONE.md b/docs/METODI-DI-TRADUZIONE.md index 0718fe71..23c226db 100644 --- a/docs/METODI-DI-TRADUZIONE.md +++ b/docs/METODI-DI-TRADUZIONE.md @@ -375,6 +375,44 @@ vivo, non che qualcuno gli stia parlando. Il numero che conta è quante richieste arrivano al server — se è zero, il problema sta a monte dell'IPC, e guardare la pipe non lo troverà mai. +**La risoluzione per simbolo non salva i build monolitici (21/08/2026).** +Un simbolo esportato sarebbe deterministico — o c'è ed è quello giusto, o non +c'è — ma richiede l'engine in DLL. Father's Day è un Shipping monolitico: + +| | | +|---|---| +| export nell'exe | 312, **tutti** hint per driver (`NvOptimusEnablement`, `ags*`) | +| simboli UE | nessuno | +| PDB | dichiarato nell'header (`Fathers_Day-Win64-Shipping.pdb`), non spedito | +| DLL accanto all'exe | solo `turbojpeg.dll` | +| moduli interrogati a runtime | **142**, zero hit | + +`ResolveFTextToStringBySymbol()` in `source_unreal_ftext.cpp` prova comunque +per primo (`?ToString@FText@@QEBAAEBVFString@@XZ`, cercato in tutti i moduli via +Toolhelp) e cade sul pattern quando non trova. Serve ai build non monolitici e +all'editor; sui giochi commerciali no. Il numero di moduli finisce nel log +apposta: senza, uno zero non distingue «guardato ovunque, non c'è» da «lo +snapshot è fallito e non ho guardato niente». Il controllo che lo conferma è +confrontarlo con `(Get-Process ...).Modules.Count` — qui 142 contro 142. + +**I quattro candidati, misurati.** Non sono un pattern che sfarfalla: sono +quattro funzioni diverse con lo stesso prologo, che divergono al **byte 13**, +cioè dentro i 17 che la firma copre. + +| # | RVA | dopo il prologo | +|---|---|---| +| 1 | `0x1CADD20` | `call [rax+0x28]`, ritorna `rax+0x68` | +| 2 | `0x1D04720` | `call [rax+0x48]`, ritorna `[rbx+0x60]` (int32) | +| 3 | `0x1D047B0` | identica a #2 ma `[rbx+0x64]` — accessori gemelli | +| 4 | `0x3074940` | `mov rcx,[rax+0x90]`, chiama via `r8` | + +Allungare la firma per isolarne una significa ritararla sui quattro giochi dove +oggi è già unica, senza avere una verità di riferimento con cui dire *quale* +sia quella giusta: qui non ci sono simboli da confrontare. La strada promettente +non è una firma più lunga, è un **ancoraggio a un riferimento di stringa** +(trovare un dato noto in `.rdata` e risalire al codice che lo usa), che non +dipende da come il compilatore ha disposto i byte del prologo. + **Corollario sul fallback a runtime.** Agganciare il fallback al vicolo cieco «Unreal senza `.locres`» (PR #87) è giusto in linea di principio, ma finché il pattern di `FText::ToString` resta ambiguo **su UE il runtime non ha testo diff --git a/gs-hook/src/sources/source_unreal_ftext.cpp b/gs-hook/src/sources/source_unreal_ftext.cpp index ea2e4f46..da013a52 100644 --- a/gs-hook/src/sources/source_unreal_ftext.cpp +++ b/gs-hook/src/sources/source_unreal_ftext.cpp @@ -27,6 +27,7 @@ #include "utils.h" // GSTranslator::Utils::PatternScanUnique #include "gs_log.h" // log unificato gs-hook (%TEMP%\gs-hook.log) #include +#include #include #include #include @@ -74,6 +75,65 @@ UE::FString* __fastcall Hook_FText_ToString(const UE::FText* self, UE::FString* return result; } +// ─── Risoluzione per SIMBOLO ───────────────────────────────────────────────── +// Un simbolo esportato e' deterministico: o c'e', ed e' quello giusto, o non +// c'e'. Nessuna ambiguita' possibile, a differenza di una firma di byte. +// +// QUANDO FUNZIONA E QUANDO NO — misurato, non supposto (21/08/2026): +// serve un build **non monolitico**, con l'engine in DLL (editor, o giochi +// compilati modulari). I build Shipping monolitici — la norma commerciale — non +// esportano nulla di UE: Father's Day ha 312 export e sono tutti hint per i +// driver AMD/NVIDIA (`NvOptimusEnablement`, `ags*`), il PDB e' dichiarato +// nell'header ma non spedito. Li' questa strada non puo' funzionare, e si +// scende al pattern. +// +// Nome mangled MSVC di `const FString& FText::ToString() const`: +// ?ToString@FText@@QEBAAEBVFString@@XZ +// (`Q`=metodo pubblico, `EBA`=const __cdecl x64, `AEBVFString@@`=ritorna +// const FString&, `XZ`=nessun parametro) +// +// La firma dell'hook resta a due argomenti anche qui: usa il VALORE DI RITORNO, +// non `out`, quindi funziona sia con il ritorno per riferimento sia con quello +// per valore (dove rdx e' il buffer di ritorno e rax lo ripete). +const char* const kFTextToStringSymbols[] = { + "?ToString@FText@@QEBAAEBVFString@@XZ", +}; + +/// Cerca il simbolo in tutti i moduli caricati. `moduleOut` riceve il nome del +/// modulo che l'ha fornito; `scanned` quanti moduli sono stati interrogati. +/// +/// `scanned` non e' decorativo: senza, un ritorno a zero non distingue «ho +/// guardato ovunque e non c'e'» da «lo snapshot e' fallito e non ho guardato +/// niente». Sono due diagnosi diverse e portano a due indagini diverse. +uintptr_t ResolveFTextToStringBySymbol(std::string& moduleOut, size_t& scanned) { + scanned = 0; + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) return 0; + + MODULEENTRY32W me{}; + me.dwSize = sizeof(me); + uintptr_t found = 0; + + if (Module32FirstW(snap, &me)) { + do { + ++scanned; + for (const char* name : kFTextToStringSymbols) { + FARPROC p = GetProcAddress(reinterpret_cast(me.hModule), name); + if (!p) continue; + found = reinterpret_cast(p); + char buf[MAX_MODULE_NAME32 + 1] = {}; + WideCharToMultiByte(CP_UTF8, 0, me.szModule, -1, buf, sizeof(buf) - 1, + nullptr, nullptr); + moduleOut = buf; + break; + } + } while (!found && Module32NextW(snap, &me)); + } + + CloseHandle(snap); + return found; +} + class UnrealFTextSource : public ITextSource { public: const char* Name() const override { return "Unreal/FText"; } @@ -106,6 +166,31 @@ class UnrealFTextSource : public ITextSource { HMODULE game = GetModuleHandleA(nullptr); if (!game) return Activation::Failed; + // 1) SIMBOLO. Se l'engine è in DLL, l'indirizzo è certo: nessun pattern + // da validare, nessun rischio di agganciare la funzione sbagliata. + std::string symModule; + size_t symScanned = 0; + const uintptr_t sym = ResolveFTextToStringBySymbol(symModule, symScanned); + if (!sym) { + LogLineA(("[gs-hook/UE] nessun simbolo FText::ToString in " + + std::to_string(symScanned) + + " moduli (build monolitico?), provo il pattern\n").c_str()); + } + if (sym) { + if (MH_CreateHook(reinterpret_cast(sym), + reinterpret_cast(&Hook_FText_ToString), + reinterpret_cast(&Original_FText_ToString)) == MH_OK && + MH_EnableHook(reinterpret_cast(sym)) == MH_OK) { + hookedAddr_ = sym; + LogLineA(("[gs-hook/UE] FText::ToString risolto per simbolo in " + + symModule + " — hook installato\n").c_str()); + return Activation::Activated; + } + LogLineA("[gs-hook/UE] simbolo trovato ma hook fallito (MinHook), provo il pattern\n"); + } + + // 2) PATTERN. Nei build Shipping monolitici non c'è nessun simbolo da + // risolvere, quindi si ricade qui — ed è il caso normale. // Pattern-scan di FText::ToString. UNICITÀ OBBLIGATORIA: PatternScanUnique // ritorna un indirizzo solo se il pattern compare ESATTAMENTE una volta. //