Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/METODI-DI-TRADUZIONE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions gs-hook/src/sources/source_unreal_ftext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "utils.h" // GSTranslator::Utils::PatternScanUnique
#include "gs_log.h" // log unificato gs-hook (%TEMP%\gs-hook.log)
#include <Windows.h>
#include <TlHelp32.h>
#include <MinHook.h>
#include <string>
#include <cwctype>
Expand Down Expand Up @@ -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<HMODULE>(me.hModule), name);
if (!p) continue;
found = reinterpret_cast<uintptr_t>(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"; }
Expand Down Expand Up @@ -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<LPVOID>(sym),
reinterpret_cast<LPVOID>(&Hook_FText_ToString),
reinterpret_cast<LPVOID*>(&Original_FText_ToString)) == MH_OK &&
MH_EnableHook(reinterpret_cast<LPVOID>(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.
//
Expand Down