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