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
96 changes: 96 additions & 0 deletions docs/METODI-DI-TRADUZIONE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,102 @@ nulla — quel gioco non la spedisce — ma non è il caso generale.

---

## RPG Maker

### RPG Maker 2000/2003 non veniva riconosciuto affatto

**Il fatto.** GameStringer aveva un ramo dedicato che si presentava come
«RPG Maker classico (RPG_RT 2000/2003)» — con tanto di messaggio all'utente e,
da agosto 2026, l'aggancio al fallback a runtime. Quel ramo era
**irraggiungibile proprio per i giochi RPG_RT**: nessuno dei tre strati di
rilevamento sapeva riconoscerli.

**Come è emerso.** Installando Yume Nikki (RPG Maker 2003, gratuito su Steam,
app 650700) per provare il flusso completo. Interrogando i comandi direttamente
con `cargo run --example probe_rpgmaker -- "<percorso>"`:

```text
detect_rpgmaker_game -> ERRORE: Non sembra essere un gioco RPG Maker
```

…su una cartella che contiene `RPG_RT.exe`, `RPG_RT.ldb`, `RPG_RT.lmt`,
`RPG_RT.ini` e 60+ `Map####.lmu`.

**Perché.** Tre strati, tutti ciechi allo stesso modo:

1. `engine_detector::is_rpg_maker()` cercava `www/data/System.json` (MV/MZ),
`Game.rpgproject` e `rgss*.dll` (XP/VX/VXAce). Nessun controllo su `RPG_RT.*`.
2. `RpgMakerVersion` non aveva una variante per 2000/2003: si ricadeva in
`Unknown`, e `detect_rpgmaker_game` restituiva errore.
3. Nel frontend quell'errore finiva in
`} catch { /* detect fallito → prosegui col workflow file-based normale */ }`,
quindi il ramo «classico» veniva saltato **in silenzio**.

**La cura.** Variante `RpgMakerVersion::RT`, rilevamento sui file **dati** e
ricerca in profondità. Per RT `find_data_files` ritorna `Ok(vec![])`: zero file
dati **non è un errore**, è il fatto che manda questi giochi al runtime — se
tornasse `Err`, il rilevamento fallirebbe e il ramo resterebbe irraggiungibile
come prima.

**Due accortezze, entrambe già costate altrove.**

- **Mai cercare l'eseguibile.** Moltissimi giochi RPG_RT rinominano `RPG_RT.exe`
col titolo. `RPG_RT.ldb` e `RPG_RT.lmt` non si toccano mai: sono quelli il
marcatore. Una scansione della libreria che cerca `RPG_RT.exe` conclude «non
ne hai» anche quando ne hai.
- **La profondità va cercata.** Steam installa Yume Nikki in
`common/Yume Nikki/**yumenikki**/`: cercare solo nella radice non lo trova. È
identica alla trappola dei `Paks` di Unreal, in un altro punto del codice.
`find_rpg_rt_dir` cerca fino a 3 livelli e `detect_rpgmaker_game` **riporta la
cartella trovata**, non la radice, così chi legge il risultato non deve
rifare la ricerca.

**Dopo.** Dalla radice d'installazione, cioè il percorso che passa l'app:

```text
version: RT
path: …\Yume Nikki\yumenikki (risolto, non la radice)
title: Yume Nikki
data_files: [] estrazione: 0 stringhe
```

Che sono esattamente le condizioni del ramo classico (`isMvMz` falso, stringhe
`<= 0`), quindi ora scatta — e con esso il fallback a runtime della PR #87.

**Anche l'eseguibile va cercato in profondità.** `find_executables_in_folder`
leggeva solo la radice: su `common/Yume Nikki` non trovava nulla, perché
`RPG_RT.exe` sta in `yumenikki/`. L'esito era peggiore di «non trovato»: chi
chiama ripiega sul nome del gioco (`YumeNikki.exe`), che non esiste, e poi cerca
un processo che non esisterà mai — il fallback direbbe «avvia il gioco» a gioco
già avviato. Ora, **se e solo se** la radice è vuota, guarda un livello sotto.
Con questa, la stessa lezione è comparsa in quattro punti indipendenti del
codice: i `Paks` di Unreal, il rilevatore RPG Maker, l'engine detector e la
ricerca dell'eseguibile.

**Prova del flusso completo (21/08/2026).** Con Yume Nikki in esecuzione, premuto
«STRING IT!» nella scheda del gioco. L'app mostrava già la strategia corretta —
«RPG Maker · RT — 0 file, 0 stringhe» — e il flusso è arrivato in fondo:

```text
[gs-hook] connesso a GameStringer via IPC
[gs-hook] dizionario pre-caricato: 0 voci da …\GameStringer\gs-hook-cache.gstc
[gs-hook] sorgente attiva: GDI (ExtTextOutW/DrawTextW) (livello 2)
[gs-hook] sorgente attiva: GDI/GetGlyphOutline (estrazione) (livello 2)
```

Tre conferme indipendenti: il log scritto nell'istante del clic; `RPG_RT.exe` è
a **32 bit**, quindi è stata iniettata la x86 e la selezione dual-arch ha
funzionato; ed esiste una finestra «GameStringer Overlay», che
`ensure_overlay_window()` crea **solo** dentro il ramo di successo
dell'iniezione. È la prima volta che il percorso dal pulsante al gioco viene
percorso per intero.

**La trappola.** Un ramo con un messaggio scritto bene, un commento circostanziato
e persino dei test a valle sembra codice vivo. Questo era morto da sempre, e non
per un difetto suo: per una condizione a monte che non poteva essere vera.
Prima di credere che un percorso funzioni, va provato con un input che lo
imbocchi davvero — qui è bastato installare un gioco.

## Traduzione in tempo reale (IPC)

### Stato della catena — aggiornato al 21/08/2026
Expand Down
38 changes: 38 additions & 0 deletions src-tauri/examples/probe_rpgmaker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! Interroga il rilevatore RPG Maker su un percorso, senza avviare l'app.
//!
//! Il ramo «RPG Maker classico» di `startAutoTranslate` scatta quando
//! `detect_rpgmaker_game` NON dice mv/mz **e** `extract_all_rpgmaker_strings`
//! ritorna 0. Prima di lanciare l'intero flusso conviene sapere se quelle due
//! condizioni sono davvero soddisfatte, e su QUALE percorso: i file RPG_RT
//! possono stare in una sottocartella, non nella radice d'installazione.
//!
//! ```text
//! cargo run --example probe_rpgmaker -- "<percorso>"
//! ```

fn main() {
let path = match std::env::args().nth(1) {
Some(p) => p,
None => {
eprintln!("uso: probe_rpgmaker <percorso del gioco>");
std::process::exit(2);
}
};

println!("percorso: {path}\n");

match gamestringer::commands::rpgmaker_patcher::detect_rpgmaker_game(path.clone()) {
Ok(game) => {
println!(" rilevato: {game:#?}");
}
Err(e) => {
println!(" detect_rpgmaker_game -> ERRORE: {e}");
return;
}
}

match gamestringer::commands::rpgmaker_patcher::extract_all_rpgmaker_strings(path) {
Ok(res) => println!("\n estrazione: {} stringhe", res.total_count),
Err(e) => println!("\n extract_all_rpgmaker_strings -> ERRORE: {e}"),
}
}
39 changes: 38 additions & 1 deletion src-tauri/src/commands/games.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,7 +1845,44 @@ pub async fn find_executables_in_folder(folder_path: String) -> Result<Vec<Strin
.then(a.2.to_lowercase().cmp(&b.2.to_lowercase()))
});

let executables: Vec<String> = candidates.into_iter().map(|(_, _, n)| n).collect();
let mut executables: Vec<String> = candidates.into_iter().map(|(_, _, n)| n).collect();

// Se la radice non ha eseguibili, guarda UN livello più sotto: molte
// installazioni Steam mettono il gioco in una sottocartella (Yume
// Nikki sta in `common/Yume Nikki/yumenikki/RPG_RT.exe`). Senza
// questo, l'esito è peggiore di «non trovato»: chi chiama ripiega
// sul nome del gioco (`YumeNikki.exe`), che non esiste, e cerca poi
// un processo che non esisterà mai.
//
// Un livello solo, e solo quando la radice è vuota: non è una
// scansione del disco, è il caso concreto che si incontra.
if executables.is_empty() {
if let Ok(mut dirs) = tokio::fs::read_dir(&folder_path).await {
while let Ok(Some(dir)) = dirs.next_entry().await {
let sub = dir.path();
if !sub.is_dir() {
continue;
}
if let Ok(mut inner) = tokio::fs::read_dir(&sub).await {
while let Ok(Some(e)) = inner.next_entry().await {
let p = e.path();
if p.extension().map(|x| x.to_string_lossy().to_lowercase())
== Some("exe".to_string())
{
if let Some(n) = p.file_name() {
executables.push(n.to_string_lossy().to_string());
}
}
}
}
if !executables.is_empty() {
log::info!("📁 Eseguibili trovati in sottocartella: {:?}", sub);
break;
}
}
}
}

log::info!("✅ Trovati {} eseguibili (migliore: {:?})", executables.len(), executables.first());
Ok(executables)
}
Expand Down
120 changes: 120 additions & 0 deletions src-tauri/src/commands/rpgmaker_patcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use crate::commands::encoding_utils;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RpgMakerVersion {
/// 2000/2003 — RPG_RT.ldb/.lmt, formato binario proprietario.
/// Nessuna stringa estraibile dai file: la traduzione passa dal runtime.
RT,
XP, // .rxdata (Ruby Marshal)
VX, // .rvdata (Ruby Marshal)
VXAce, // .rvdata2 (Ruby Marshal)
Expand Down Expand Up @@ -103,6 +106,17 @@ pub fn detect_rpgmaker_game(game_path: String) -> Result<RpgMakerGame, String> {

log::info!("🎮 Rilevato RPG Maker {:?}: {} ({} file dati)", version, title, data_files.len());

// Per RT il percorso utile è la cartella che contiene davvero i dati, non
// la radice d'installazione: chi legge questo risultato deve poterci
// lavorare senza rifare la ricerca.
let game_path = if matches!(version, RpgMakerVersion::RT) {
find_rpg_rt_dir(path, RPG_RT_MAX_DEPTH)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or(game_path)
} else {
game_path
};

Ok(RpgMakerGame {
path: game_path,
version,
Expand All @@ -111,6 +125,39 @@ pub fn detect_rpgmaker_game(game_path: String) -> Result<RpgMakerGame, String> {
})
}

/// Cartella che contiene davvero i file dati di RPG Maker 2000/2003.
///
/// La profondità va cercata, mai assunta — è la stessa lezione dei `Paks` di
/// Unreal (vedi `find_all_paks_dirs`). Un'installazione Steam mette spesso il
/// gioco in una sottocartella: Yume Nikki sta in
/// `common/Yume Nikki/yumenikki/`, e cercare `RPG_RT.ldb` solo nella radice
/// non lo trova.
///
/// Si guardano i file DATI e non l'eseguibile: moltissimi giochi rinominano
/// `RPG_RT.exe` col titolo, ma `.ldb` e `.lmt` restano quelli.
fn find_rpg_rt_dir(root: &Path, max_depth: usize) -> Option<PathBuf> {
if root.join("RPG_RT.ldb").exists() || root.join("RPG_RT.lmt").exists() {
return Some(root.to_path_buf());
}
if max_depth == 0 {
return None;
}
let entries = fs::read_dir(root).ok()?;
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
if let Some(found) = find_rpg_rt_dir(&p, max_depth - 1) {
return Some(found);
}
}
}
None
}

/// Profondità massima di ricerca. Tre livelli coprono le installazioni Steam e
/// GOG viste finora senza trasformare il rilevamento in una scansione del disco.
const RPG_RT_MAX_DEPTH: usize = 3;

/// Rileva la versione di RPG Maker
fn detect_rpgmaker_version(game_path: &str) -> RpgMakerVersion {
let path = Path::new(game_path);
Expand Down Expand Up @@ -150,6 +197,11 @@ fn detect_rpgmaker_version(game_path: &str) -> RpgMakerVersion {
}
}

// 2000/2003: cercati in profondità, vedi find_rpg_rt_dir.
if find_rpg_rt_dir(path, RPG_RT_MAX_DEPTH).is_some() {
return RpgMakerVersion::RT;
}

RpgMakerVersion::Unknown
}

Expand All @@ -171,6 +223,12 @@ fn find_data_files(game_path: &str, version: &RpgMakerVersion) -> Result<Vec<Rpg
RpgMakerVersion::VXAce => (path.join("Data"), "rvdata2"),
RpgMakerVersion::VX => (path.join("Data"), "rvdata"),
RpgMakerVersion::XP => (path.join("Data"), "rxdata"),
// 2000/2003: i dati stanno in .ldb/.lmt, formato binario proprietario
// che non sappiamo (ancora) leggere. Zero file dati NON è un errore: è
// esattamente il fatto che manda questi giochi alla traduzione a
// runtime, e va riportato come zero, non come fallimento — altrimenti
// il rilevamento fallisce e il ramo che li gestisce non viene raggiunto.
RpgMakerVersion::RT => return Ok(Vec::new()),
RpgMakerVersion::Unknown => return Err("Versione non supportata".to_string()),
};

Expand Down Expand Up @@ -1471,4 +1529,66 @@ mod tests {
assert_eq!(stats.untranslated, 1);
assert_eq!(stats.percentage, 66);
}

/// Crea una finta installazione RPG_RT sotto `sub` livelli di sottocartelle.
fn fixture_rt(nome: &str, sub: &[&str]) -> std::path::PathBuf {
let root = std::env::temp_dir().join(nome);
let _ = fs::remove_dir_all(&root);
let mut dir = root.clone();
for s in sub {
dir = dir.join(s);
}
fs::create_dir_all(&dir).unwrap();
// Solo i file DATI: l'eseguibile e' volutamente assente, perche' i
// giochi veri lo rinominano e cercarlo non funziona.
fs::write(dir.join("RPG_RT.ldb"), b"finto").unwrap();
fs::write(dir.join("RPG_RT.lmt"), b"finto").unwrap();
root
}

#[test]
fn rt_riconosciuto_nella_radice() {
let root = fixture_rt("gs_rt_root", &[]);
let g = detect_rpgmaker_game(root.to_string_lossy().into_owned()).unwrap();
assert!(matches!(g.version, RpgMakerVersion::RT));
assert!(g.data_files.is_empty(), "RPG_RT non espone file dati leggibili");
let _ = fs::remove_dir_all(&root);
}

#[test]
fn rt_riconosciuto_in_sottocartella() {
// Steam installa spesso il gioco un livello piu' in basso: Yume Nikki
// sta in `common/Yume Nikki/yumenikki/`. Cercare solo nella radice non
// lo trova, ed e' la stessa trappola dei `Paks` di Unreal.
let root = fixture_rt("gs_rt_sub", &["yumenikki"]);
let g = detect_rpgmaker_game(root.to_string_lossy().into_owned()).unwrap();
assert!(matches!(g.version, RpgMakerVersion::RT));
assert!(
g.path.ends_with("yumenikki"),
"il percorso deve puntare alla cartella dei dati, non alla radice: {}",
g.path
);
let _ = fs::remove_dir_all(&root);
}

#[test]
fn rt_estrazione_ritorna_zero_non_errore() {
// Zero stringhe NON e' un fallimento: e' il fatto che manda questi
// giochi alla traduzione a runtime. Se qui si tornasse un errore, il
// ramo che li gestisce non verrebbe mai raggiunto.
let root = fixture_rt("gs_rt_zero", &["game"]);
let res = extract_all_rpgmaker_strings(root.to_string_lossy().into_owned()).unwrap();
assert_eq!(res.total_count, 0);
let _ = fs::remove_dir_all(&root);
}

#[test]
fn cartella_qualsiasi_non_e_rpg_maker() {
let root = std::env::temp_dir().join("gs_rt_none");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(root.join("a").join("b")).unwrap();
fs::write(root.join("gioco.exe"), b"x").unwrap();
assert!(detect_rpgmaker_game(root.to_string_lossy().into_owned()).is_err());
let _ = fs::remove_dir_all(&root);
}
}
26 changes: 26 additions & 0 deletions src-tauri/src/engine_detector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,33 @@ fn is_hendrix(path: &Path) -> bool {
false
}

/// True se `root` (o una sottocartella entro `max_depth`) contiene i file dati
/// di RPG Maker 2000/2003. Steam installa spesso il gioco in una sottocartella,
/// quindi la profondita' va cercata. Vedi `find_rpg_rt_dir` nel patcher.
fn rpg_rt_dir_exists(root: &Path, max_depth: usize) -> bool {
if root.join("RPG_RT.ldb").exists() || root.join("RPG_RT.lmt").exists() {
return true;
}
if max_depth == 0 {
return false;
}
match std::fs::read_dir(root) {
Ok(entries) => entries.flatten().any(|e| {
let p = e.path();
p.is_dir() && rpg_rt_dir_exists(&p, max_depth - 1)
}),
Err(_) => false,
}
}

fn is_rpg_maker(path: &Path) -> bool {
// RPG Maker 2000/2003 (RPG_RT). Si guardano i file DATI, non l'eseguibile:
// moltissimi giochi rinominano RPG_RT.exe col titolo (Yume Nikki lo chiama
// RPG_RT.exe, Ib no), ma RPG_RT.ldb e RPG_RT.lmt non si toccano mai.
if rpg_rt_dir_exists(path, 3) {
return true;
}

// RPG Maker MV/MZ
if path.join("www").exists() && path.join("www/data/System.json").exists() {
return true;
Expand Down