Skip to main content

siglus_scene_vm/runtime/forms/
file.rs

1use anyhow::Result;
2use encoding_rs::{SHIFT_JIS, UTF_16BE, UTF_16LE};
3use std::path::{Path, PathBuf};
4
5use crate::runtime::forms::prop_access;
6use crate::runtime::{CommandContext, Value, constants};
7
8fn resolve_text_file_path(project_dir: &Path, append_dir: &str, raw: &str) -> Option<PathBuf> {
9    let raw_path = Path::new(raw);
10    let mut candidates = Vec::new();
11    if raw_path.is_absolute() {
12        candidates.push(raw_path.to_path_buf());
13    } else {
14        candidates.push(project_dir.join(raw_path));
15        candidates.push(project_dir.join("dat").join(raw_path));
16        candidates.push(project_dir.join("save").join(raw_path));
17        candidates.push(project_dir.join("savedata").join(raw_path));
18        if !append_dir.is_empty() {
19            let append = Path::new(append_dir);
20            if append.is_absolute() {
21                candidates.push(append.join(raw_path));
22            } else {
23                candidates.push(project_dir.join(append).join(raw_path));
24            }
25        }
26    }
27
28    let mut expanded = Vec::new();
29    for base in candidates {
30        expanded.push(base.clone());
31        if base.extension().is_none() {
32            expanded.push(base.with_extension("txt"));
33        }
34    }
35    expanded
36        .into_iter()
37        .find_map(|p| crate::resource::resolve_game_file(&p).ok().flatten())
38}
39
40fn decode_text(bytes: &[u8]) -> String {
41    if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
42        return String::from_utf8_lossy(&bytes[3..]).into_owned();
43    }
44    if bytes.starts_with(&[0xFF, 0xFE]) {
45        let (cow, _, _) = UTF_16LE.decode(&bytes[2..]);
46        return cow.into_owned();
47    }
48    if bytes.starts_with(&[0xFE, 0xFF]) {
49        let (cow, _, _) = UTF_16BE.decode(&bytes[2..]);
50        return cow.into_owned();
51    }
52    if let Ok(s) = std::str::from_utf8(bytes) {
53        return s.to_string();
54    }
55    let (cow, _, _) = SHIFT_JIS.decode(bytes);
56    cow.into_owned()
57}
58
59fn split_lines(text: String) -> Vec<String> {
60    text.replace("\r\n", "\n")
61        .replace('\r', "\n")
62        .split('\n')
63        .map(|s| s.to_string())
64        .collect()
65}
66
67fn strlist_key_from_value(value: &Value) -> Option<u32> {
68    match value.unwrap_named() {
69        Value::Element(chain) => chain.first().copied().map(|v| v as u32),
70        Value::Int(v @ 0..) => Some(*v as u32),
71        _ => None,
72    }
73}
74
75fn handle_load_txt(ctx: &mut CommandContext, params: &[Value]) -> Result<bool> {
76    let file_name = params.first().and_then(|v| v.as_str()).unwrap_or("");
77    let Some(target_key) = params.get(1).and_then(strlist_key_from_value) else {
78        ctx.unknown
79            .record_note("FILE.LOAD_TXT missing STRLIST target");
80        ctx.push(Value::Int(0));
81        return Ok(true);
82    };
83    if file_name.is_empty() {
84        ctx.unknown.record_note("FILE.LOAD_TXT empty file name");
85        ctx.push(Value::Int(0));
86        return Ok(true);
87    }
88    let Some(path) = resolve_text_file_path(&ctx.project_dir, &ctx.globals.append_dir, file_name)
89    else {
90        ctx.unknown
91            .record_note(&format!("FILE.LOAD_TXT missing file:{file_name}"));
92        ctx.push(Value::Int(0));
93        return Ok(true);
94    };
95    match crate::resource::read_file_bytes(&path) {
96        Ok(bytes) => {
97            let lines = split_lines(decode_text(&bytes));
98            let dst = ctx.globals.str_lists.entry(target_key).or_default();
99            dst.clear();
100            dst.extend(lines);
101            ctx.push(Value::Int(1));
102        }
103        Err(e) => {
104            ctx.unknown
105                .record_note(&format!("FILE.LOAD_TXT read failed:{}:{e}", path.display()));
106            ctx.push(Value::Int(0));
107        }
108    }
109    Ok(true)
110}
111
112fn preload_omv(ctx: &mut CommandContext, name: &str) {
113    if name.is_empty() {
114        return;
115    }
116    match crate::resource::find_omv_path_with_append_dir(
117        &ctx.project_dir,
118        &ctx.globals.append_dir,
119        name,
120    ) {
121        Ok(path) => {
122            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
123            match crate::resource::open_game_file(&path) {
124                Ok(mut file) => {
125                    use std::io::Read;
126                    let mut buf = vec![0u8; 1024 * 1024];
127                    let _ = file.read(&mut buf);
128                }
129                Err(e) => {
130                    ctx.unknown.record_note(&format!(
131                        "FILE.PRELOAD_OMV open failed:{}:{e}",
132                        path.display()
133                    ));
134                }
135            }
136
137            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
138            {
139                // Browser VFS files are already resident in memory. Resolution
140                // above is sufficient to mirror the preload existence check.
141                let _ = path;
142            }
143        }
144        Err(e) => {
145            ctx.unknown
146                .record_note(&format!("FILE.PRELOAD_OMV failed:{name}:{e}"));
147        }
148    }
149}
150
151pub fn dispatch(ctx: &mut CommandContext, form_id: u32, args: &[Value]) -> Result<bool> {
152    let parsed = prop_access::parse_element_chain_ctx(ctx, form_id, args);
153    let (chain_pos, chain) = match parsed {
154        Some((pos, ch @ [_, _, ..])) => (Some(pos), Some(ch)),
155        _ => (None, None),
156    };
157
158    if let Some(chain) = chain {
159        let op = chain[1];
160        let params = if let Some(pos) = chain_pos {
161            prop_access::script_args(args, pos)
162        } else {
163            &[]
164        };
165        let p_str = |i: usize| -> &str { params.get(i).and_then(|v| v.as_str()).unwrap_or("") };
166
167        if op == constants::elm_value::FILE_LOAD_TXT {
168            return handle_load_txt(ctx, params);
169        }
170
171        if op == constants::elm_value::FILE_PRELOAD_OMV {
172            preload_omv(ctx, p_str(0));
173            ctx.push(Value::Int(0));
174            return Ok(true);
175        }
176
177        return Ok(false);
178    }
179
180    if let Some(op) = args.first().and_then(|v| v.as_i64()) {
181        if op == constants::elm_value::FILE_LOAD_TXT as i64 {
182            return handle_load_txt(ctx, &args[1..]);
183        }
184        if op == constants::elm_value::FILE_PRELOAD_OMV as i64 {
185            let name = args.get(1).and_then(|v| v.as_str()).unwrap_or("");
186            preload_omv(ctx, name);
187            ctx.push(Value::Int(0));
188            return Ok(true);
189        }
190    }
191
192    Ok(false)
193}