1use std::fmt::Write as FmtWrite;
21use std::fs;
22use std::path::Path;
23
24use anyhow::{anyhow, Context, Result};
25use serde::Serialize;
26
27use crate::scanner::S2sRefType;
28use crate::util::relative_path;
29
30#[derive(Serialize)]
33pub struct InfRawSource {
34 #[serde(rename = "_parse_error")]
36 pub parse_error: String,
37 pub raw_content: String,
39}
40
41pub fn format_script_ref(ron_rel: &Path, seq_index: usize) -> String {
44 format!("{}.{}.lua", ron_rel.to_string_lossy(), seq_index)
45}
46
47pub fn write_script_lua(
53 inf_path: &Path,
54 input_root: &Path,
55 stage_root: &Path,
56 seq_index: usize,
57 script_s2s: &str,
58 debug: bool,
59) -> Result<()> {
60 let ron_rel = relative_path(inf_path, input_root).with_extension("ron");
61
62 let lua_name = format!("{}.{}.lua", ron_rel.to_string_lossy(), seq_index);
64 let lua_path = stage_root.join(&lua_name);
65
66 if let Some(parent) = lua_path.parent() {
67 fs::create_dir_all(parent)?;
68 }
69
70 let lua = match s2s2lua::S2sParser::parse(script_s2s) {
71 Ok(script) => s2s2lua::LuaGenerator::generate(&script, s2s2lua::GenOptions::default())
72 .unwrap_or_else(|_| {
73 format!("--[[ s2s2lua generation failed\n{}\n]]", script_s2s)
75 }),
76 Err(e) => {
77 format!("--[[ s2s2lua parse error: {}\n{}\n]]", e, script_s2s)
79 }
80 };
81
82 fs::write(&lua_path, &lua)
83 .with_context(|| format!("writing {:?}", lua_path))?;
84
85 if debug {
86 eprintln!(" script {} → {:?}", seq_index, lua_path);
87 }
88
89 Ok(())
90}
91
92pub fn parse_dialogue_to_lua(content: &str) -> Result<String> {
108 let mut lua = String::from("-- Generated by openstranded-s2mod-tool\nreturn {\n pages = {\n");
109 let mut current_page: Option<String> = None;
110 let mut current_title = String::new();
111 let mut current_text = String::new();
112 let mut in_text = false;
113 let mut buttons: Vec<(String, String)> = Vec::new(); let mut trade_buy: Vec<(String, String)> = Vec::new();
115 let mut trade_sell: Vec<(String, String)> = Vec::new();
116 let mut in_trade = false;
117
118 let flush_page = |lua: &mut String,
119 page: &Option<String>,
120 title: &str,
121 text: &str,
122 buttons: &[(String, String)],
123 buy: &[(String, String)],
124 sell: &[(String, String)]|
125 -> Result<()> {
126 let page_name = page.as_ref().ok_or_else(|| anyhow!("page without name"))?;
127 writeln!(lua, " [\"{}\"] = {{", page_name)?;
128 if !title.is_empty() {
129 writeln!(lua, " title = \"{}\",", title.replace('\\', "\\\\").replace('"', "\\\""))?;
130 }
131 if !text.is_empty() {
132 writeln!(lua, " text = [[{}]],", text)?;
133 }
134 if !buy.is_empty() || !sell.is_empty() {
135 lua.push_str(" trade = {\n");
136 for (id, count) in buy {
137 writeln!(lua, " buy = {{ id = {}, count = {} }},", id, count)?;
138 }
139 for (id, count) in sell {
140 writeln!(lua, " sell = {{ id = {}, count = {} }},", id, count)?;
141 }
142 lua.push_str(" },\n");
143 }
144 if !buttons.is_empty() {
145 lua.push_str(" buttons = {\n");
146 for (target, label) in buttons {
147 writeln!(
148 lua,
149 " {{ target = \"{}\", label = \"{}\" }},",
150 target.replace('\\', "\\\\").replace('"', "\\\""),
151 label.replace('\\', "\\\\").replace('"', "\\\""),
152 )?;
153 }
154 lua.push_str(" },\n");
155 }
156 lua.push_str(" },\n");
157 Ok(())
158 };
159
160 for line in content.lines() {
161 let trimmed = line.trim();
162 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
163 continue;
164 }
165
166 if let Some(page_name) = trimmed.strip_prefix("page=") {
167 if current_page.is_some() {
169 flush_page(
170 &mut lua,
171 ¤t_page,
172 ¤t_title,
173 ¤t_text,
174 &buttons,
175 &trade_buy,
176 &trade_sell,
177 )?;
178 }
179 current_page = Some(page_name.trim().to_string());
180 current_title.clear();
181 current_text.clear();
182 buttons.clear();
183 trade_buy.clear();
184 trade_sell.clear();
185 in_text = false;
186 in_trade = false;
187 continue;
188 }
189
190 if trimmed == "text=start" {
191 in_text = true;
192 continue;
193 }
194 if trimmed == "text=end" {
195 in_text = false;
196 continue;
197 }
198
199 if let Some(title_val) = trimmed.strip_prefix("title=") {
200 current_title = title_val.trim().to_string();
201 continue;
202 }
203
204 if trimmed == "trade=start" {
205 in_trade = true;
206 continue;
207 }
208 if trimmed == "trade=end" {
209 in_trade = false;
210 continue;
211 }
212
213 if in_text {
214 if !current_text.is_empty() {
215 current_text.push('\n');
216 }
217 current_text.push_str(line.trim_start());
218 continue;
219 }
220
221 if in_trade {
222 if let Some(sell_str) = trimmed.strip_prefix("sell=") {
223 let parts: Vec<&str> = sell_str.splitn(2, ',').collect();
224 let id = parts.first().unwrap_or(&"").trim().to_string();
225 let count = parts.get(1).unwrap_or(&"1").trim().to_string();
226 trade_sell.push((id, count));
227 } else if let Some(buy_str) = trimmed.strip_prefix("buy=") {
228 let parts: Vec<&str> = buy_str.splitn(2, ',').collect();
229 let id = parts.first().unwrap_or(&"").trim().to_string();
230 let count = parts.get(1).unwrap_or(&"1").trim().to_string();
231 trade_buy.push((id, count));
232 }
233 continue;
234 }
235
236 if let Some(btn_str) = trimmed.strip_prefix("button=") {
237 let parts: Vec<&str> = btn_str.splitn(2, ',').collect();
238 let target = parts.first().unwrap_or(&"").trim().to_string();
239 let label = parts.get(1).unwrap_or(&"").trim().to_string();
240 buttons.push((target, label));
241 continue;
242 }
243 }
244
245 if current_page.is_some() {
247 flush_page(
248 &mut lua,
249 ¤t_page,
250 ¤t_title,
251 ¤t_text,
252 &buttons,
253 &trade_buy,
254 &trade_sell,
255 )?;
256 }
257
258 lua.push_str(" },\n}\n");
259 Ok(lua)
260}
261
262pub fn split_into_sections(content: &str) -> Vec<(String, String)> {
269 let mut sections: Vec<(String, String)> = Vec::new();
270 let mut current_name = String::new();
271 let mut current_lines: Vec<&str> = Vec::new();
272
273 for line in content.lines() {
274 if let Some(name) = line.strip_prefix("//~") {
275 if !current_lines.is_empty() || !current_name.is_empty() {
277 sections.push((current_name.clone(), current_lines.join("\n")));
278 }
279 current_name = name.trim().to_string();
281 current_lines.clear();
282 } else {
283 current_lines.push(line);
284 }
285 }
286 if !current_lines.is_empty() || !current_name.is_empty() {
288 sections.push((current_name, current_lines.join("\n")));
289 }
290
291 sections
292}
293
294pub fn convert_sectioned_file(
302 content: &str,
303 sections_refs: &std::collections::HashMap<String, Vec<S2sRefType>>,
304) -> String {
305 let mut lua = String::from("-- Generated by openstranded-s2mod-tool\nreturn {\n");
306 let parts = split_into_sections(content);
307
308 for (name, body) in &parts {
310 let key = if name.is_empty() {
311 "[\"__preamble\"]".to_string()
312 } else {
313 format!("[\"{}\"]", name.replace('\\', "\\\\").replace('"', "\\\""))
314 };
315
316 let refs_for_section = if name.as_str().is_empty() {
318 vec![]
319 } else {
320 sections_refs
321 .get(name.as_str())
322 .cloned()
323 .unwrap_or_default()
324 };
325
326 let is_msgbox = refs_for_section
327 .iter()
328 .any(|r| matches!(r, S2sRefType::Msgbox { .. }));
329 let is_script = refs_for_section
330 .iter()
331 .any(|r| !matches!(r, S2sRefType::Msgbox { .. }));
332
333 if is_msgbox && !is_script {
334 writeln!(&mut lua, " {} = [[{}]],", key, body).unwrap();
336 } else {
337 match s2s2lua::S2sParser::parse(body) {
339 Ok(ast) => {
340 let func_body = s2s2lua::LuaGenerator::generate(&ast, s2s2lua::GenOptions::default())
341 .unwrap_or_else(|_| format!("--[[ generation failed ]]\n{}", body));
342 writeln!(&mut lua, " {} = function()\n{} end,", key, func_body).unwrap();
343 }
344 Err(_) => {
345 writeln!(&mut lua, " {} = [[{}]],", key, body).unwrap();
347 }
348 }
349 }
350 }
351
352 lua.push_str("}\n");
353 lua
354}