Skip to main content

siglus_scene_vm/runtime/forms/
system.rs

1use anyhow::Result;
2use std::fs::{self, OpenOptions};
3use std::io::Write;
4use std::path::{Path, PathBuf};
5
6use crate::runtime::{CommandContext, Value};
7
8use super::prop_access;
9use super::syscom;
10
11const CHECK_ACTIVE: i32 = 0;
12const SHELL_OPEN_FILE: i32 = 1;
13const CHECK_DUMMY_FILE_ONCE: i32 = 2;
14const OPEN_DIALOG_FOR_CHIHAYA_BENCH: i32 = 3;
15const GET_SPEC_INFO_FOR_CHIHAYA_BENCH: i32 = 4;
16const SHELL_OPEN_WEB: i32 = 5;
17const CHECK_FILE_EXIST: i32 = 6;
18const DEBUG_MESSAGEBOX_OK: i32 = 7;
19const DEBUG_MESSAGEBOX_OKCANCEL: i32 = 8;
20const DEBUG_MESSAGEBOX_YESNO: i32 = 9;
21const DEBUG_MESSAGEBOX_YESNOCANCEL: i32 = 10;
22const DEBUG_WRITE_LOG: i32 = 11;
23const CHECK_FILE_EXIST_SAVE_DIR: i32 = 12;
24const CHECK_DEBUG_FLAG: i32 = 13;
25const GET_CALENDAR: i32 = 14;
26const GET_UNIX_TIME: i32 = 15;
27const GET_LANGUAGE: i32 = 16;
28const MESSAGEBOX_OK: i32 = 17;
29const MESSAGEBOX_OKCANCEL: i32 = 18;
30const MESSAGEBOX_YESNO: i32 = 19;
31const MESSAGEBOX_YESNOCANCEL: i32 = 20;
32const CLEAR_DUMMY_FILE: i32 = 21;
33
34struct Call<'a> {
35    op: i32,
36    params: &'a [Value],
37}
38
39fn parse_call<'a>(ctx: &CommandContext, form_id: u32, args: &'a [Value]) -> Option<Call<'a>> {
40    let (chain_pos, chain) = prop_access::parse_element_chain_ctx(ctx, form_id, args)?;
41    if chain.len() < 2 {
42        return None;
43    }
44    let params = prop_access::script_args(args, chain_pos);
45    Some(Call {
46        op: chain[1],
47        params,
48    })
49}
50
51fn p_str(params: &[Value], idx: usize) -> &str {
52    params.get(idx).and_then(|v| v.as_str()).unwrap_or("")
53}
54
55fn join_game_path(base: &Path, raw: &str) -> PathBuf {
56    if raw.is_empty() {
57        return base.to_path_buf();
58    }
59    let norm = raw.replace('\\', "/");
60    let p = Path::new(&norm);
61    if p.is_absolute() {
62        p.to_path_buf()
63    } else {
64        base.join(p)
65    }
66}
67
68#[cfg(any(target_os = "windows", target_os = "macos"))]
69fn command_output(program: &str, args: &[&str]) -> Option<String> {
70    let output = std::process::Command::new(program)
71        .args(args)
72        .output()
73        .ok()?;
74    if !output.status.success() {
75        return None;
76    }
77    let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
78    (!text.is_empty()).then_some(text)
79}
80
81fn squash_spaces(text: &str) -> String {
82    text.split_whitespace().collect::<Vec<_>>().join(" ")
83}
84
85#[cfg(target_os = "windows")]
86fn chihaya_os_name() -> String {
87    command_output(
88        "powershell.exe",
89        &[
90            "-NoProfile",
91            "-NonInteractive",
92            "-Command",
93            "$o=Get-CimInstance Win32_OperatingSystem; $a=@($o.Caption,$o.CSDVersion,$o.OSArchitecture) | Where-Object { $_ -and $_.Trim() }; $a -join ' '",
94        ],
95    )
96    .map(|s| {
97        squash_spaces(&s)
98            .replace('™', "(TM)")
99            .replace('®', "(R)")
100    })
101    .unwrap_or_else(|| format!("Windows {}", std::env::consts::ARCH))
102}
103
104#[cfg(target_os = "windows")]
105fn chihaya_cpu_name() -> String {
106    command_output(
107        "powershell.exe",
108        &[
109            "-NoProfile",
110            "-NonInteractive",
111            "-Command",
112            "Get-CimInstance Win32_Processor | Select-Object -First 1 -ExpandProperty Name",
113        ],
114    )
115    .map(|s| squash_spaces(&s))
116    .filter(|s| !s.is_empty())
117    .unwrap_or_else(|| "不明な CPU".to_string())
118}
119
120#[cfg(target_os = "macos")]
121fn chihaya_os_name() -> String {
122    let product =
123        command_output("sw_vers", &["-productName"]).unwrap_or_else(|| "macOS".to_string());
124    let version = command_output("sw_vers", &["-productVersion"]).unwrap_or_default();
125    let build = command_output("sw_vers", &["-buildVersion"]).unwrap_or_default();
126    let mut fields = vec![product];
127    if !version.is_empty() {
128        fields.push(version);
129    }
130    if !build.is_empty() {
131        fields.push(format!("({build})"));
132    }
133    fields.push(std::env::consts::ARCH.to_string());
134    fields.join(" ")
135}
136
137#[cfg(target_os = "macos")]
138fn chihaya_cpu_name() -> String {
139    if let Some(name) = command_output("sysctl", &["-n", "machdep.cpu.brand_string"]) {
140        let name = squash_spaces(&name);
141        if !name.is_empty() {
142            return name;
143        }
144    }
145    if let Some(info) = command_output("system_profiler", &["SPHardwareDataType"]) {
146        for line in info.lines() {
147            let line = line.trim();
148            for key in ["Chip:", "Processor Name:"] {
149                if let Some(value) = line.strip_prefix(key) {
150                    let value = squash_spaces(value.trim());
151                    if !value.is_empty() {
152                        return value;
153                    }
154                }
155            }
156        }
157    }
158    command_output("sysctl", &["-n", "hw.model"])
159        .map(|s| squash_spaces(&s))
160        .filter(|s| !s.is_empty())
161        .unwrap_or_else(|| "不明な CPU".to_string())
162}
163
164#[cfg(target_os = "linux")]
165fn chihaya_os_name() -> String {
166    let pretty = fs::read_to_string("/etc/os-release").ok().and_then(|text| {
167        text.lines().find_map(|line| {
168            let raw = line.strip_prefix("PRETTY_NAME=")?;
169            let raw = raw.trim();
170            let value = raw
171                .strip_prefix('"')
172                .and_then(|s| s.strip_suffix('"'))
173                .unwrap_or(raw)
174                .replace("\\\"", "\"")
175                .replace("\\\\", "\\");
176            (!value.is_empty()).then_some(value)
177        })
178    });
179    format!(
180        "{} {}",
181        pretty.unwrap_or_else(|| "Linux".to_string()),
182        std::env::consts::ARCH
183    )
184}
185
186#[cfg(target_os = "linux")]
187fn chihaya_cpu_name() -> String {
188    if let Ok(text) = fs::read_to_string("/proc/cpuinfo") {
189        for key in ["model name", "Hardware", "Processor"] {
190            if let Some(value) = text.lines().find_map(|line| {
191                let (name, value) = line.split_once(':')?;
192                (name.trim() == key).then(|| squash_spaces(value.trim()))
193            }) && !value.is_empty()
194            {
195                return value;
196            }
197        }
198    }
199    "不明な CPU".to_string()
200}
201
202#[cfg(target_os = "android")]
203fn chihaya_os_name() -> String {
204    format!("Android {}", std::env::consts::ARCH)
205}
206
207#[cfg(target_os = "android")]
208fn chihaya_cpu_name() -> String {
209    fs::read_to_string("/proc/cpuinfo")
210        .ok()
211        .and_then(|text| {
212            text.lines().find_map(|line| {
213                let (name, value) = line.split_once(':')?;
214                matches!(name.trim(), "model name" | "Hardware" | "Processor")
215                    .then(|| squash_spaces(value.trim()))
216            })
217        })
218        .filter(|s| !s.is_empty())
219        .unwrap_or_else(|| std::env::consts::ARCH.to_string())
220}
221
222#[cfg(target_os = "ios")]
223fn chihaya_os_name() -> String {
224    format!("iOS {}", std::env::consts::ARCH)
225}
226
227#[cfg(target_os = "ios")]
228fn chihaya_cpu_name() -> String {
229    std::env::consts::ARCH.to_string()
230}
231
232#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
233fn chihaya_os_name() -> String {
234    "WebAssembly".to_string()
235}
236
237#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
238fn chihaya_cpu_name() -> String {
239    "wasm32".to_string()
240}
241
242#[cfg(not(any(
243    target_os = "windows",
244    target_os = "macos",
245    target_os = "linux",
246    target_os = "android",
247    target_os = "ios",
248    all(target_arch = "wasm32", target_os = "unknown")
249)))]
250fn chihaya_os_name() -> String {
251    format!("{} {}", std::env::consts::OS, std::env::consts::ARCH)
252}
253
254#[cfg(not(any(
255    target_os = "windows",
256    target_os = "macos",
257    target_os = "linux",
258    target_os = "android",
259    target_os = "ios",
260    all(target_arch = "wasm32", target_os = "unknown")
261)))]
262fn chihaya_cpu_name() -> String {
263    std::env::consts::ARCH.to_string()
264}
265
266fn chihaya_spec_info(ctx: &CommandContext) -> String {
267    let mut result = format!("OS = {}\nCPU = {}", chihaya_os_name(), chihaya_cpu_name());
268    let adapter = ctx.globals.system.chihaya_display_adapter_name.trim();
269    if !adapter.is_empty() {
270        result.push_str("\nビデオカード = ");
271        result.push_str(adapter);
272    }
273    result
274}
275
276pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
277    let Some(call) = parse_call(ctx, form_id, args) else {
278        return Ok(false);
279    };
280
281    match call.op {
282        GET_CALENDAR => {
283            let tm = local_time_fields();
284            let vals = [tm.0, tm.1, tm.2, tm.3, tm.4, tm.5, tm.6, tm.7];
285            for (idx, value) in vals.iter().enumerate() {
286                if let Some(Value::Element(chain)) = call.params.get(idx) {
287                    prop_access::assign_to_chain(ctx, chain, Value::Int(*value));
288                }
289            }
290            return Ok(true);
291        }
292        GET_UNIX_TIME => {
293            let t = crate::platform_time::unix_time_secs() as i64;
294            ctx.push(Value::Int(t));
295            return Ok(true);
296        }
297        CHECK_ACTIVE => {
298            ctx.push(Value::Int(if ctx.globals.system.active_flag {
299                1
300            } else {
301                0
302            }));
303            return Ok(true);
304        }
305        CHECK_DEBUG_FLAG => {
306            ctx.push(Value::Int(if ctx.globals.system.debug_flag {
307                1
308            } else {
309                0
310            }));
311            return Ok(true);
312        }
313        SHELL_OPEN_FILE => {
314            let requested = join_game_path(&ctx.project_dir, p_str(call.params, 0));
315            let resolved = crate::resource::resolve_game_path(&requested)
316                .ok()
317                .flatten();
318            if let Some(path) = resolved.as_ref() {
319                let _ = ctx.net.open_file(path);
320            }
321            ctx.globals.system.debug_logs.push(format!(
322                "shell_open_file:{}",
323                resolved.as_deref().unwrap_or(&requested).display()
324            ));
325            return Ok(true);
326        }
327        SHELL_OPEN_WEB => {
328            let url = p_str(call.params, 0);
329            let _ = ctx.net.open_url(url);
330            ctx.globals
331                .system
332                .debug_logs
333                .push(format!("shell_open_web:{url}"));
334            return Ok(true);
335        }
336        CHECK_FILE_EXIST => {
337            let path = join_game_path(&ctx.project_dir, p_str(call.params, 0));
338            ctx.push(Value::Int(if crate::resource::game_path_exists(&path) {
339                1
340            } else {
341                0
342            }));
343            return Ok(true);
344        }
345        CHECK_FILE_EXIST_SAVE_DIR => {
346            let save_dir = syscom::save_dir(&ctx.project_dir);
347            let path = join_game_path(&save_dir, p_str(call.params, 0));
348            ctx.push(Value::Int(if crate::resource::game_path_exists(&path) {
349                1
350            } else {
351                0
352            }));
353            return Ok(true);
354        }
355        CHECK_DUMMY_FILE_ONCE => {
356            let name = p_str(call.params, 0);
357            let key = call.params.get(1).and_then(|v| v.as_i64()).unwrap_or(0);
358            let code = p_str(call.params, 2);
359            let sig = format!("{name}:{key}:{code}");
360            ctx.globals.system.dummy_checks.insert(sig);
361            return Ok(true);
362        }
363        CLEAR_DUMMY_FILE => {
364            ctx.globals.system.dummy_checks.clear();
365            return Ok(true);
366        }
367        MESSAGEBOX_OK | MESSAGEBOX_OKCANCEL | MESSAGEBOX_YESNO | MESSAGEBOX_YESNOCANCEL => {
368            let text = messagebox_text(ctx, call.params);
369            if let Some(ret) = handle_messagebox(ctx, call.op, false, text) {
370                ctx.push(Value::Int(ret));
371            }
372            return Ok(true);
373        }
374        DEBUG_MESSAGEBOX_OK
375        | DEBUG_MESSAGEBOX_OKCANCEL
376        | DEBUG_MESSAGEBOX_YESNO
377        | DEBUG_MESSAGEBOX_YESNOCANCEL => {
378            let text = messagebox_text(ctx, call.params);
379            if ctx.globals.system.debug_flag {
380                if let Some(ret) = handle_messagebox(ctx, call.op, true, text) {
381                    ctx.push(Value::Int(ret));
382                }
383            } else {
384                ctx.push(Value::Int(0));
385            }
386            return Ok(true);
387        }
388        DEBUG_WRITE_LOG => {
389            if ctx.globals.system.debug_flag {
390                let s = match call.params.first() {
391                    Some(Value::Int(v)) => v.to_string(),
392                    Some(Value::Str(s)) => s.clone(),
393                    _ => String::new(),
394                };
395                write_debug_log(
396                    &ctx.project_dir,
397                    &s,
398                    ctx.current_scene_name.as_deref(),
399                    ctx.current_line_no,
400                );
401                ctx.globals.system.debug_logs.push(s);
402            }
403            return Ok(true);
404        }
405        GET_SPEC_INFO_FOR_CHIHAYA_BENCH => {
406            // eng_chihaya.cpp::tnm_get_spec_info_for_chihaya_bench().
407            ctx.push(Value::Str(chihaya_spec_info(ctx)));
408            return Ok(true);
409        }
410        OPEN_DIALOG_FOR_CHIHAYA_BENCH => {
411            // eng_chihaya.cpp::tnm_open_chihaya_bench_dialog().  Keep a
412            // history entry for diagnostics, but unlike the old placeholder
413            // this is a modal operation and script execution stops here.
414            let text = p_str(call.params, 0).to_string();
415            ctx.globals.system.bench_dialogs.push(text.clone());
416            ctx.request_chihaya_bench_dialog(text);
417            return Ok(true);
418        }
419        GET_LANGUAGE => {
420            ctx.push(Value::Str(ctx.globals.system.language_code.clone()));
421            return Ok(true);
422        }
423        _ => {}
424    }
425
426    Ok(false)
427}
428
429fn messagebox_text(ctx: &CommandContext, params: &[Value]) -> String {
430    match params.first() {
431        Some(Value::Int(v)) => v.to_string(),
432        Some(Value::Str(s)) => s.clone(),
433        Some(v) => v.as_str().unwrap_or("").to_string(),
434        None => {
435            if let Some(name) = ctx.current_scene_name.as_deref() {
436                format!("{name}:{}", ctx.current_line_no)
437            } else {
438                String::new()
439            }
440        }
441    }
442}
443
444fn handle_messagebox(
445    ctx: &mut CommandContext,
446    kind: i32,
447    debug_only: bool,
448    text: String,
449) -> Option<i64> {
450    ctx.globals
451        .system
452        .messagebox_history
453        .push(crate::runtime::globals::SystemMessageBoxRecord {
454            kind,
455            text: text.clone(),
456            debug_only,
457        });
458
459    let buttons = messagebox_buttons(kind);
460    let max_value = buttons.iter().map(|b| b.value).max().unwrap_or(0);
461    if !ctx.globals.system.messagebox_response_queue.is_empty() {
462        let v = ctx.globals.system.messagebox_response_queue.remove(0);
463        return Some(v.clamp(0, max_value));
464    }
465
466    ctx.request_system_messagebox(kind, debug_only, text, buttons);
467    None
468}
469
470fn messagebox_buttons(kind: i32) -> Vec<crate::runtime::globals::SystemMessageBoxButton> {
471    let raw: &[(&str, i64)] = match kind {
472        MESSAGEBOX_OK | DEBUG_MESSAGEBOX_OK => &[("OK", 0)],
473        MESSAGEBOX_OKCANCEL | DEBUG_MESSAGEBOX_OKCANCEL => &[("OK", 0), ("CANCEL", 1)],
474        MESSAGEBOX_YESNO | DEBUG_MESSAGEBOX_YESNO => &[("YES", 0), ("NO", 1)],
475        MESSAGEBOX_YESNOCANCEL | DEBUG_MESSAGEBOX_YESNOCANCEL => {
476            &[("YES", 0), ("NO", 1), ("CANCEL", 2)]
477        }
478        _ => &[("OK", 0)],
479    };
480    raw.iter()
481        .map(
482            |(label, value)| crate::runtime::globals::SystemMessageBoxButton {
483                label: (*label).to_string(),
484                value: *value,
485            },
486        )
487        .collect()
488}
489
490fn local_time_fields() -> (i64, i64, i64, i64, i64, i64, i64, i64) {
491    let now = crate::platform_time::local_time_fields();
492    (
493        now.year as i64,
494        now.month as i64,
495        now.day as i64,
496        now.weekday_sunday0 as i64,
497        now.hour as i64,
498        now.minute as i64,
499        now.second as i64,
500        now.millisecond as i64,
501    )
502}
503
504fn write_debug_log(project_dir: &Path, msg: &str, scene_name: Option<&str>, line_no: i64) {
505    if msg.is_empty() {
506        return;
507    }
508    let dir = project_dir.join("__DEBUG_LOG");
509    let _ = fs::create_dir_all(&dir);
510    let path = dir.join("debug_log.txt");
511    let stamp = crate::platform_time::local_log_timestamp();
512    if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&path) {
513        if let Some(scene) = scene_name {
514            let _ = writeln!(f, "{}\t({}.ss line={})\t{}", stamp, scene, line_no, msg);
515        } else {
516            let _ = writeln!(f, "{}\t{}", stamp, msg);
517        }
518    }
519}