Skip to main content

siglus_scene_vm/runtime/
game_title.rs

1//! Game title resolution shared by runtime and platform UI integration.
2//!
3//! The title follows the engine configuration first and falls back to the game
4//! directory name. `Gameexe.dat` decoding uses `key.toml` from the project
5//! directory through `GameexeDecodeOptions::from_project_dir`.
6
7use std::path::Path;
8
9use siglus_assets::gameexe::{decode_gameexe_dat_bytes, GameexeConfig, GameexeDecodeOptions};
10
11const GAMEEXE_CANDIDATES: &[&str] = &[
12    "Gameexe.dat",
13    "Gameexe.ini",
14    "gameexe.dat",
15    "gameexe.ini",
16    "GameexeEN.dat",
17    "GameexeEN.ini",
18    "GameexeZH.dat",
19    "GameexeZH.ini",
20    "GameexeZHTW.dat",
21    "GameexeZHTW.ini",
22    "GameexeDE.dat",
23    "GameexeDE.ini",
24    "GameexeES.dat",
25    "GameexeES.ini",
26    "GameexeFR.dat",
27    "GameexeFR.ini",
28    "GameexeID.dat",
29    "GameexeID.ini",
30];
31
32/// Return the display title for a game directory.
33///
34/// This is intended for platform/bundle UI code as well as runtime dialogs. It
35/// reads `GAMENAME` from `Gameexe.ini` or `Gameexe.dat` when possible. If the
36/// Gameexe file is missing, cannot be decoded, or does not contain a non-empty
37/// `GAMENAME`, the returned value is the project directory name. As a final
38/// fallback it returns `Siglus`.
39pub fn resolve_game_title_from_project_dir(project_dir: impl AsRef<Path>) -> String {
40    let project_dir = project_dir.as_ref();
41    load_gameexe_config(project_dir)
42        .as_ref()
43        .and_then(game_title_from_config)
44        .unwrap_or_else(|| fallback_title_from_project_dir(project_dir))
45}
46
47/// Return the runtime title using an already loaded Gameexe config first.
48pub fn resolve_game_title(
49    gameexe: Option<&GameexeConfig>,
50    project_dir: impl AsRef<Path>,
51) -> String {
52    let project_dir = project_dir.as_ref();
53    gameexe
54        .and_then(game_title_from_config)
55        .unwrap_or_else(|| fallback_title_from_project_dir(project_dir))
56}
57
58/// Extract `GAMENAME` from a parsed Gameexe config.
59pub fn game_title_from_config(cfg: &GameexeConfig) -> Option<String> {
60    if let Some(v) = cfg.get_unquoted("GAMENAME") {
61        let s = normalize_game_title(v);
62        if !s.is_empty() {
63            return Some(s);
64        }
65    }
66    for entry in cfg.entries.iter().rev() {
67        if matches!(entry.key_parts.last().map(|s| s.as_str()), Some("GAMENAME")) {
68            let s = normalize_game_title(entry.scalar_unquoted());
69            if !s.is_empty() {
70                return Some(s);
71            }
72        }
73    }
74    None
75}
76
77fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
78    let gameexe_path = find_gameexe_path(project_dir)?;
79    let raw = std::fs::read(&gameexe_path).ok()?;
80    let text = if gameexe_path
81        .extension()
82        .and_then(|s| s.to_str())
83        .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
84    {
85        String::from_utf8(raw).ok()?
86    } else {
87        let opt = GameexeDecodeOptions::from_project_dir(project_dir).ok()?;
88        decode_gameexe_dat_bytes(&raw, &opt).ok()?.0
89    };
90    Some(GameexeConfig::from_text(&text))
91}
92
93fn find_gameexe_path(project_dir: &Path) -> Option<std::path::PathBuf> {
94    for name in GAMEEXE_CANDIDATES {
95        let p = project_dir.join(name);
96        if p.is_file() {
97            return Some(p);
98        }
99    }
100    None
101}
102
103fn normalize_game_title(raw: &str) -> String {
104    raw.trim().trim_matches('"').trim().to_string()
105}
106
107fn fallback_title_from_project_dir(project_dir: &Path) -> String {
108    project_dir
109        .file_name()
110        .and_then(|s| s.to_str())
111        .filter(|s| !s.is_empty())
112        .unwrap_or("Siglus")
113        .to_string()
114}