use std::fmt::Write as FmtWrite;
use std::fs;
use std::path::Path;
use anyhow::{anyhow, Context, Result};
use serde::Serialize;
use crate::scanner::S2sRefType;
use crate::util::relative_path;
#[derive(Serialize)]
pub struct InfRawSource {
#[serde(rename = "_parse_error")]
pub parse_error: String,
pub raw_content: String,
}
pub fn format_script_ref(ron_rel: &Path, seq_index: usize) -> String {
format!("{}.{}.lua", ron_rel.to_string_lossy(), seq_index)
}
pub fn write_script_lua(
inf_path: &Path,
input_root: &Path,
stage_root: &Path,
seq_index: usize,
script_s2s: &str,
debug: bool,
) -> Result<()> {
let ron_rel = relative_path(inf_path, input_root).with_extension("ron");
let lua_name = format!("{}.{}.lua", ron_rel.to_string_lossy(), seq_index);
let lua_path = stage_root.join(&lua_name);
if let Some(parent) = lua_path.parent() {
fs::create_dir_all(parent)?;
}
let lua = match s2s2lua::S2sParser::parse(script_s2s) {
Ok(script) => s2s2lua::LuaGenerator::generate(&script, s2s2lua::GenOptions::default())
.unwrap_or_else(|_| {
format!("--[[ s2s2lua generation failed\n{}\n]]", script_s2s)
}),
Err(e) => {
format!("--[[ s2s2lua parse error: {}\n{}\n]]", e, script_s2s)
}
};
fs::write(&lua_path, &lua)
.with_context(|| format!("writing {:?}", lua_path))?;
if debug {
eprintln!(" script {} → {:?}", seq_index, lua_path);
}
Ok(())
}
pub fn parse_dialogue_to_lua(content: &str) -> Result<String> {
let mut lua = String::from("-- Generated by openstranded-s2mod-tool\nreturn {\n pages = {\n");
let mut current_page: Option<String> = None;
let mut current_title = String::new();
let mut current_text = String::new();
let mut in_text = false;
let mut buttons: Vec<(String, String)> = Vec::new(); let mut trade_buy: Vec<(String, String)> = Vec::new();
let mut trade_sell: Vec<(String, String)> = Vec::new();
let mut in_trade = false;
let flush_page = |lua: &mut String,
page: &Option<String>,
title: &str,
text: &str,
buttons: &[(String, String)],
buy: &[(String, String)],
sell: &[(String, String)]|
-> Result<()> {
let page_name = page.as_ref().ok_or_else(|| anyhow!("page without name"))?;
writeln!(lua, " [\"{}\"] = {{", page_name)?;
if !title.is_empty() {
writeln!(lua, " title = \"{}\",", title.replace('\\', "\\\\").replace('"', "\\\""))?;
}
if !text.is_empty() {
writeln!(lua, " text = [[{}]],", text)?;
}
if !buy.is_empty() || !sell.is_empty() {
lua.push_str(" trade = {\n");
for (id, count) in buy {
writeln!(lua, " buy = {{ id = {}, count = {} }},", id, count)?;
}
for (id, count) in sell {
writeln!(lua, " sell = {{ id = {}, count = {} }},", id, count)?;
}
lua.push_str(" },\n");
}
if !buttons.is_empty() {
lua.push_str(" buttons = {\n");
for (target, label) in buttons {
writeln!(
lua,
" {{ target = \"{}\", label = \"{}\" }},",
target.replace('\\', "\\\\").replace('"', "\\\""),
label.replace('\\', "\\\\").replace('"', "\\\""),
)?;
}
lua.push_str(" },\n");
}
lua.push_str(" },\n");
Ok(())
};
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
if let Some(page_name) = trimmed.strip_prefix("page=") {
if current_page.is_some() {
flush_page(
&mut lua,
¤t_page,
¤t_title,
¤t_text,
&buttons,
&trade_buy,
&trade_sell,
)?;
}
current_page = Some(page_name.trim().to_string());
current_title.clear();
current_text.clear();
buttons.clear();
trade_buy.clear();
trade_sell.clear();
in_text = false;
in_trade = false;
continue;
}
if trimmed == "text=start" {
in_text = true;
continue;
}
if trimmed == "text=end" {
in_text = false;
continue;
}
if let Some(title_val) = trimmed.strip_prefix("title=") {
current_title = title_val.trim().to_string();
continue;
}
if trimmed == "trade=start" {
in_trade = true;
continue;
}
if trimmed == "trade=end" {
in_trade = false;
continue;
}
if in_text {
if !current_text.is_empty() {
current_text.push('\n');
}
current_text.push_str(line.trim_start());
continue;
}
if in_trade {
if let Some(sell_str) = trimmed.strip_prefix("sell=") {
let parts: Vec<&str> = sell_str.splitn(2, ',').collect();
let id = parts.first().unwrap_or(&"").trim().to_string();
let count = parts.get(1).unwrap_or(&"1").trim().to_string();
trade_sell.push((id, count));
} else if let Some(buy_str) = trimmed.strip_prefix("buy=") {
let parts: Vec<&str> = buy_str.splitn(2, ',').collect();
let id = parts.first().unwrap_or(&"").trim().to_string();
let count = parts.get(1).unwrap_or(&"1").trim().to_string();
trade_buy.push((id, count));
}
continue;
}
if let Some(btn_str) = trimmed.strip_prefix("button=") {
let parts: Vec<&str> = btn_str.splitn(2, ',').collect();
let target = parts.first().unwrap_or(&"").trim().to_string();
let label = parts.get(1).unwrap_or(&"").trim().to_string();
buttons.push((target, label));
continue;
}
}
if current_page.is_some() {
flush_page(
&mut lua,
¤t_page,
¤t_title,
¤t_text,
&buttons,
&trade_buy,
&trade_sell,
)?;
}
lua.push_str(" },\n}\n");
Ok(lua)
}
pub fn split_into_sections(content: &str) -> Vec<(String, String)> {
let mut sections: Vec<(String, String)> = Vec::new();
let mut current_name = String::new();
let mut current_lines: Vec<&str> = Vec::new();
for line in content.lines() {
if let Some(name) = line.strip_prefix("//~") {
if !current_lines.is_empty() || !current_name.is_empty() {
sections.push((current_name.clone(), current_lines.join("\n")));
}
current_name = name.trim().to_string();
current_lines.clear();
} else {
current_lines.push(line);
}
}
if !current_lines.is_empty() || !current_name.is_empty() {
sections.push((current_name, current_lines.join("\n")));
}
sections
}
pub fn convert_sectioned_file(
content: &str,
sections_refs: &std::collections::HashMap<String, Vec<S2sRefType>>,
) -> String {
let mut lua = String::from("-- Generated by openstranded-s2mod-tool\nreturn {\n");
let parts = split_into_sections(content);
for (name, body) in &parts {
let key = if name.is_empty() {
"[\"__preamble\"]".to_string()
} else {
format!("[\"{}\"]", name.replace('\\', "\\\\").replace('"', "\\\""))
};
let refs_for_section = if name.as_str().is_empty() {
vec![]
} else {
sections_refs
.get(name.as_str())
.cloned()
.unwrap_or_default()
};
let is_msgbox = refs_for_section
.iter()
.any(|r| matches!(r, S2sRefType::Msgbox { .. }));
let is_script = refs_for_section
.iter()
.any(|r| !matches!(r, S2sRefType::Msgbox { .. }));
if is_msgbox && !is_script {
writeln!(&mut lua, " {} = [[{}]],", key, body).unwrap();
} else {
match s2s2lua::S2sParser::parse(body) {
Ok(ast) => {
let func_body = s2s2lua::LuaGenerator::generate(&ast, s2s2lua::GenOptions::default())
.unwrap_or_else(|_| format!("--[[ generation failed ]]\n{}", body));
writeln!(&mut lua, " {} = function()\n{} end,", key, func_body).unwrap();
}
Err(_) => {
writeln!(&mut lua, " {} = [[{}]],", key, body).unwrap();
}
}
}
}
lua.push_str("}\n");
lua
}