use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::util::read_file_lossy;
#[derive(Debug, Clone, PartialEq)]
pub enum S2sRefType {
Dialogue { start_page: String },
Msgbox { title: String },
Button,
AddScript,
ExtendScript,
DefExtend,
DefOverride,
}
#[derive(Debug, Clone)]
pub struct S2sReference {
pub target: PathBuf,
pub ref_type: S2sRefType,
pub _section: Option<String>,
}
pub type S2sRefMap = HashMap<PathBuf, Vec<S2sRefType>>;
pub type S2sSectionMap = HashMap<PathBuf, HashMap<String, Vec<S2sRefType>>>;
#[derive(Debug, Clone, PartialEq)]
pub enum S2sClass {
Script,
Dialogue { start_page: String },
Msgbox { title: String },
Unknown,
}
pub fn scan_references(
content: &str,
_source_path: &Path,
input_root: &Path,
refs: &mut Vec<S2sReference>,
) {
let text = content.replace("\r\n", "\n");
let extract_quoted = |text: &str, start: usize| -> Option<(String, usize)> {
let bytes = text.as_bytes();
if start >= bytes.len() || bytes[start] != b'"' {
return None;
}
let mut i = start + 1;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 2; continue;
}
if bytes[i] == b'"' {
let s = text[start + 1..i].to_string();
return Some((s, i + 1));
}
i += 1;
}
None
};
let extract_token = |text: &str, start: usize| -> Option<(String, usize)> {
let bytes = text.as_bytes();
if start >= bytes.len() {
return None;
}
let mut s = start;
while s < bytes.len() && (bytes[s] == b' ' || bytes[s] == b'\t') {
s += 1;
}
if s >= bytes.len() {
return None;
}
if bytes[s] == b'$' || bytes[s].is_ascii_digit() || bytes[s] == b'-' {
let mut e = s + 1;
while e < bytes.len()
&& (bytes[e].is_ascii_alphanumeric() || bytes[e] == b'_' || bytes[e] == b'.')
{
e += 1;
}
let tok = text[s..e].to_string();
Some((tok, e))
} else if bytes[s].is_ascii_alphabetic() {
let mut e = s + 1;
while e < bytes.len()
&& (bytes[e].is_ascii_alphanumeric() || bytes[e] == b'_')
{
e += 1;
}
let tok = text[s..e].to_string();
Some((tok, e))
} else {
Some((text[s..s + 1].to_string(), s + 1))
}
};
let skip_comma = |text: &str, pos: usize| -> Option<usize> {
let bytes = text.as_bytes();
let mut i = pos;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
if i < bytes.len() && bytes[i] == b',' {
i += 1;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
Some(i)
} else {
None
}
};
let resolve_path = |rel: &str| -> Option<PathBuf> {
let p = Path::new(rel);
if p.is_absolute() {
return Some(p.to_path_buf());
}
let cleaned = rel.trim_start_matches(['/', '\\', '.']);
let abs = input_root.join(cleaned);
let normalised: PathBuf = abs.components().collect();
Some(normalised)
};
let lower = text.to_lowercase();
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find("dialogue ") {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + "dialogue ".len();
if let Some((start_page, after_page)) = extract_quoted(&text, after_cmd)
&& let Some(after_comma1) = skip_comma(&text, after_page)
&& let Some((path, after_path)) = extract_quoted(&text, after_comma1) {
let section = skip_comma(&text, after_path)
.and_then(|p| extract_quoted(&text, p))
.map(|(s, _)| s);
if let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: S2sRefType::Dialogue { start_page },
_section: section,
});
}
}
search_pos = abs_pos + 1;
}
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find("msgbox ") {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + "msgbox ".len();
if let Some((_title, after_title)) = extract_quoted(&text, after_cmd)
&& let Some(after_comma) = skip_comma(&text, after_title)
&& let Some((path, _)) = extract_quoted(&text, after_comma)
&& let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: S2sRefType::Msgbox { title: _title },
_section: None,
});
}
search_pos = abs_pos + 1;
}
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find("button ") {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + "button ".len();
if let Some((_, after_id)) = extract_token(&text, after_cmd)
&& let Some(after_comma1) = skip_comma(&text, after_id) {
if let Some((_, after_text)) = extract_quoted(&text, after_comma1)
&& let Some(after_comma2) = skip_comma(&text, after_text) {
if let Some((_, after_font)) = extract_token(&text, after_comma2)
&& let Some(after_comma3) = skip_comma(&text, after_font)
&& let Some((path, _)) = extract_quoted(&text, after_comma3)
&& let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: S2sRefType::Button,
_section: None,
});
}
}
}
search_pos = abs_pos + 1;
}
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find("addscript ") {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + "addscript ".len();
if let Some((_class, after_class)) = extract_quoted(&text, after_cmd)
&& let Some(after_comma1) = skip_comma(&text, after_class)
&& let Some((_, after_id)) = extract_token(&text, after_comma1)
&& let Some(after_comma2) = skip_comma(&text, after_id)
&& let Some((path, after_path)) = extract_quoted(&text, after_comma2) {
let section = skip_comma(&text, after_path)
.and_then(|p| extract_quoted(&text, p))
.map(|(s, _)| s);
if let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: S2sRefType::AddScript,
_section: section,
});
}
}
search_pos = abs_pos + 1;
}
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find("extendscript ") {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + "extendscript ".len();
if let Some((_, after_class)) = extract_token(&text, after_cmd)
&& let Some(after_comma1) = skip_comma(&text, after_class)
&& let Some((_, after_id)) = extract_token(&text, after_comma1)
&& let Some(after_comma2) = skip_comma(&text, after_id)
&& let Some((path, _)) = extract_quoted(&text, after_comma2)
&& let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: S2sRefType::ExtendScript,
_section: None,
});
}
search_pos = abs_pos + 1;
}
for keyword in &["def_extend ", "def_override "] {
let mut search_pos = 0;
while let Some(pos) = lower[search_pos..].find(keyword) {
let abs_pos = search_pos + pos;
let after_cmd = abs_pos + keyword.len();
let ref_type = if *keyword == "def_extend " {
S2sRefType::DefExtend
} else {
S2sRefType::DefOverride
};
if let Some((_class, after_class)) = extract_quoted(&text, after_cmd)
&& let Some(after_comma1) = skip_comma(&text, after_class)
&& let Some((_, after_id)) = extract_token(&text, after_comma1)
&& let Some(after_comma2) = skip_comma(&text, after_id)
&& let Some((path, after_path)) =
extract_quoted(&text, after_comma2)
{
let section = skip_comma(&text, after_path)
.and_then(|p| extract_quoted(&text, p))
.map(|(s, _)| s);
if let Some(target) = resolve_path(&path) {
refs.push(S2sReference {
target,
ref_type: ref_type.clone(),
_section: section,
});
}
}
search_pos = abs_pos + 1;
}
}
}
pub fn build_reference_map(
inf_entries: &std::collections::HashMap<PathBuf, Vec<inf2ron::InfEntry>>,
s2s_files: &[PathBuf],
input_root: &Path,
) -> (S2sRefMap, S2sSectionMap) {
let mut all_refs: Vec<S2sReference> = Vec::new();
for (inf_path, entries) in inf_entries {
for entry in entries {
if let Some(script_blocks) = entry.blocks.get("script") {
for block in script_blocks {
if let inf2ron::BlockContent::Text(content) = &block.content {
scan_references(content, inf_path, input_root, &mut all_refs);
}
}
}
}
}
for script_path in s2s_files {
if let Ok(content) = read_file_lossy(script_path) {
scan_references(&content, script_path, input_root, &mut all_refs);
}
}
let mut file_map: S2sRefMap = HashMap::new();
let mut section_map: S2sSectionMap = HashMap::new();
for r in &all_refs {
file_map
.entry(r.target.clone())
.or_default()
.push(r.ref_type.clone());
if let Some(ref section_name) = r._section {
section_map
.entry(r.target.clone())
.or_default()
.entry(section_name.clone())
.or_default()
.push(r.ref_type.clone());
}
}
if !all_refs.is_empty() {
eprintln!(" {} cross-references found", all_refs.len());
}
(file_map, section_map)
}
pub fn resolve_missing_script_refs(
ref_map: &mut S2sRefMap,
section_map: &mut S2sSectionMap,
) -> Vec<PathBuf> {
let mut extra_files: Vec<PathBuf> = Vec::new();
let mut redirects: Vec<(PathBuf, PathBuf)> = Vec::new();
let keys: Vec<PathBuf> = ref_map.keys().cloned().collect();
for key in &keys {
if key.exists() {
continue;
}
if let Some(ext) = key.extension().and_then(|s| s.to_str()) {
let alt_ext = match ext {
"s2s" => "txt",
"txt" => "s2s",
_ => {
eprintln!(" Warning: referenced script not found: {:?}", key);
continue;
}
};
let alt = key.with_extension(alt_ext);
if alt.exists() {
redirects.push((key.clone(), alt.clone()));
extra_files.push(alt);
} else {
eprintln!(" Warning: referenced script not found: {:?}", key);
}
} else {
eprintln!(" Warning: referenced script has no extension: {:?}", key);
}
}
for (old_key, new_key) in &redirects {
if let Some(refs) = ref_map.remove(old_key) {
ref_map.entry(new_key.clone()).or_insert(refs);
}
if let Some(sections) = section_map.remove(old_key) {
section_map.entry(new_key.clone()).or_insert(sections);
}
}
for key in ref_map.keys() {
let is_s2s = key.extension().is_some_and(|e| e == "s2s");
if !is_s2s && key.exists() {
extra_files.push(key.clone());
}
}
extra_files.sort();
extra_files.dedup();
extra_files
}
pub fn classify_s2s(path: &Path, ref_map: &S2sRefMap) -> S2sClass {
let refs = match ref_map.get(path) {
Some(r) => r,
None => return S2sClass::Unknown,
};
let mut dialogue_ref = None;
let mut msgbox_ref = None;
let mut is_script = false;
for r in refs {
match r {
S2sRefType::Dialogue { start_page } => {
dialogue_ref = Some(start_page.clone());
}
S2sRefType::Msgbox { title } => {
msgbox_ref = Some(title.clone());
}
_ => {
is_script = true;
}
}
}
if let Some(start_page) = dialogue_ref {
S2sClass::Dialogue { start_page }
} else if is_script && msgbox_ref.is_some() {
S2sClass::Script
} else if let Some(title) = msgbox_ref {
S2sClass::Msgbox { title }
} else if is_script {
S2sClass::Script
} else {
S2sClass::Unknown
}
}