From 2a75d1c5b9be5d4af6d08ff09e008260b64c8862 Mon Sep 17 00:00:00 2001 From: rouges78 Date: Fri, 21 Aug 2026 20:10:41 +0200 Subject: [PATCH 1/2] Detect RPG Maker 2000/2003, which was never recognised at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GameStringer had a branch that introduced itself as "RPG Maker classico (RPG_RT 2000/2003)", with a message for the user and, since #87, the hook into the runtime fallback. That branch was unreachable for exactly those games: none of the three detection layers could recognise them. It surfaced while installing Yume Nikki (RPG Maker 2003, free on Steam) to test the complete flow. Querying the commands directly returned "Non sembra essere un gioco RPG Maker" for a folder holding RPG_RT.exe, RPG_RT.ldb, RPG_RT.lmt, RPG_RT.ini and sixty-odd Map####.lmu files. Three layers, blind the same way. engine_detector::is_rpg_maker looked for www/data/System.json, Game.rpgproject and rgss*.dll, with no check for RPG_RT at all. RpgMakerVersion had no variant for 2000/2003, so detection fell to Unknown and errored. And in the frontend that error landed in a catch that comments itself "detect fallito → prosegui col workflow file-based normale", so the classic branch was skipped silently. Now there is a RpgMakerVersion::RT variant, detection on the data files, and a depth-limited search. For RT, find_data_files returns Ok(vec![]): zero data files is not an error, it is the fact that routes these games to runtime translation. Returning Err would fail detection and leave the branch as unreachable as before. Two things worth not relearning. Never look for the executable — plenty of RPG_RT games rename RPG_RT.exe to the game's title, while .ldb and .lmt are never touched; a library scan searching for RPG_RT.exe concludes you own none when you do. And the folder depth has to be searched: Steam installs Yume Nikki under common/Yume Nikki/yumenikki/, so looking only in the root misses it — the same trap as Unreal's Paks folders, in a different part of the codebase. detect_rpgmaker_game reports the folder it found rather than the root, so callers need not repeat the search. From the install root the app actually passes, detection now yields version RT, the resolved subfolder, and zero strings — precisely the conditions the classic branch tests for, so it fires, and with it the runtime fallback from #87. Four tests cover root and subfolder detection, zero-not-error extraction, and a non-RPG-Maker folder. Full Rust suite: 1560 passed. Co-Authored-By: Claude Opus 5 --- docs/METODI-DI-TRADUZIONE.md | 68 ++++++++++++ src-tauri/examples/probe_rpgmaker.rs | 38 +++++++ src-tauri/src/commands/rpgmaker_patcher.rs | 120 +++++++++++++++++++++ src-tauri/src/engine_detector.rs | 26 +++++ 4 files changed, 252 insertions(+) create mode 100644 src-tauri/examples/probe_rpgmaker.rs diff --git a/docs/METODI-DI-TRADUZIONE.md b/docs/METODI-DI-TRADUZIONE.md index a27387ace..dace42ae2 100644 --- a/docs/METODI-DI-TRADUZIONE.md +++ b/docs/METODI-DI-TRADUZIONE.md @@ -178,6 +178,74 @@ 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 -- ""`: + +```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. + +**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 diff --git a/src-tauri/examples/probe_rpgmaker.rs b/src-tauri/examples/probe_rpgmaker.rs new file mode 100644 index 000000000..b188686e9 --- /dev/null +++ b/src-tauri/examples/probe_rpgmaker.rs @@ -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 -- "" +//! ``` + +fn main() { + let path = match std::env::args().nth(1) { + Some(p) => p, + None => { + eprintln!("uso: probe_rpgmaker "); + 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}"), + } +} diff --git a/src-tauri/src/commands/rpgmaker_patcher.rs b/src-tauri/src/commands/rpgmaker_patcher.rs index cad7d8536..7246bc179 100644 --- a/src-tauri/src/commands/rpgmaker_patcher.rs +++ b/src-tauri/src/commands/rpgmaker_patcher.rs @@ -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) @@ -103,6 +106,17 @@ pub fn detect_rpgmaker_game(game_path: String) -> Result { 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, @@ -111,6 +125,39 @@ pub fn detect_rpgmaker_game(game_path: String) -> Result { }) } +/// 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 { + 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); @@ -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 } @@ -171,6 +223,12 @@ fn find_data_files(game_path: &str, version: &RpgMakerVersion) -> Result (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()), }; @@ -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); + } } diff --git a/src-tauri/src/engine_detector.rs b/src-tauri/src/engine_detector.rs index 2ddefaa10..1dc9b0f3a 100644 --- a/src-tauri/src/engine_detector.rs +++ b/src-tauri/src/engine_detector.rs @@ -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; From 0d6370f0c1ce8ac9c4ad5bae373d0a1940964c54 Mon Sep 17 00:00:00 2001 From: rouges78 Date: Fri, 21 Aug 2026 20:55:17 +0200 Subject: [PATCH 2/2] Search one level down for the game executable too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_executables_in_folder read only the root. On common/Yume Nikki it found nothing, because RPG_RT.exe lives in yumenikki/. The outcome was worse than "not found": callers fall back to the game's name (YumeNikki.exe), which does not exist, and then look for a process that never will — so the runtime fallback would say "launch the game" with the game already running. Now, if and only if the root is empty, it looks one level down. One level, and only in that case: this is not a disk scan, it is the case you actually meet. That makes four independent places in this codebase where the same lesson had to be learned: Unreal's Paks folders, the RPG Maker detector, the engine detector, and now the executable search. Also records the end-to-end run. With Yume Nikki running, pressing "STRING IT!" on its page took the whole path: the app already showed the right strategy ("RPG Maker · RT — 0 file, 0 stringhe"), and gs-hook came up with the GDI sources, the preloaded dictionary and an IPC connection. Three independent confirmations — the log written at the instant of the click, RPG_RT.exe being 32-bit so the x86 DLL was chosen by the dual-arch selection, and a "GameStringer Overlay" window, which ensure_overlay_window creates only inside the injection's success branch. First time the path from the button to the game has been walked end to end. Co-Authored-By: Claude Opus 5 --- docs/METODI-DI-TRADUZIONE.md | 28 +++++++++++++++++++++++ src-tauri/src/commands/games.rs | 39 ++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/docs/METODI-DI-TRADUZIONE.md b/docs/METODI-DI-TRADUZIONE.md index dace42ae2..248159739 100644 --- a/docs/METODI-DI-TRADUZIONE.md +++ b/docs/METODI-DI-TRADUZIONE.md @@ -240,6 +240,34 @@ 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. diff --git a/src-tauri/src/commands/games.rs b/src-tauri/src/commands/games.rs index ed53b7e34..54dbb3e62 100644 --- a/src-tauri/src/commands/games.rs +++ b/src-tauri/src/commands/games.rs @@ -1845,7 +1845,44 @@ pub async fn find_executables_in_folder(folder_path: String) -> Result = candidates.into_iter().map(|(_, _, n)| n).collect(); + let mut executables: Vec = 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) }