siglus_scene_vm/runtime/forms/
file.rs1use 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 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}