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