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. //