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 ctx.push(Value::Str(chihaya_spec_info(ctx)));
408 return Ok(true);
409 }
410 OPEN_DIALOG_FOR_CHIHAYA_BENCH => {
411 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}