//! Softpal script (.src) mod disasm; use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; use anyhow::Result; use disasm::*; use std::collections::HashMap; use std::io::{Read, Write}; #[derive(Debug)] /// Softpal script builder pub struct SoftpalScriptBuilder {} impl SoftpalScriptBuilder { /// Create a new Softpal script builder pub fn new() -> Self { Self {} } } impl ScriptBuilder for SoftpalScriptBuilder { fn default_encoding(&self) -> Encoding { Encoding::Cp932 } fn build_script( &self, buf: Vec, filename: &str, encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, archive: Option<&Box>, ) -> Result> { Ok(Box::new(SoftpalScript::new( buf, filename, encoding, config, archive, )?)) } fn extensions(&self) -> &'static [&'static str] { &["src"] } fn script_type(&self) -> &'static ScriptType { &ScriptType::Softpal } fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { if buf_len >= 4 && buf.starts_with(b"Sv20") { return Some(10); } None } } #[derive(Debug)] /// Softpal SRC Script pub struct SoftpalScript { data: MemReader, strs: Vec, texts: MemReader, encoding: Encoding, label_offsets: Vec, add_message_index: bool, } impl SoftpalScript { /// Create a new Softpal script pub fn new( buf: Vec, filename: &str, encoding: Encoding, config: &ExtraConfig, archive: Option<&Box>, ) -> Result { let texts = Self::load_texts_data(Self::load_file(filename, archive, "TEXT.DAT")?)?; let points_data = MemReader::new(Self::load_file(filename, archive, "POINT.DAT")?); let label_offsets = Self::load_point_data(points_data)?; let strs = Disasm::new(&buf, &label_offsets)?.disassemble::(None)?; Ok(Self { data: MemReader::new(buf), strs, encoding, texts, label_offsets, add_message_index: config.softpal_add_message_index, }) } fn load_file(filename: &str, archive: Option<&Box>, name: &str) -> Result> { if let Some(archive) = archive { Ok(archive .open_file_by_name(name, true) .map_err(|e| anyhow::anyhow!("Failed to open file {} in archive: {}", name, e))? .data()?) } else { let mut path = std::path::PathBuf::from(filename); path.set_file_name(name); path = crate::utils::files::get_ignorecase_path(&path)?; std::fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", name, e)) } } fn load_texts_data(data: Vec) -> Result { let mut writer = MemWriter::from_vec(data); if writer.data.len() >= 0x14 { let ind = writer.cpeek_u32_at(0x10)?; writer.pos = 0x10; if ind != 0 { let mut shift = 4; for _ in 0..(writer.data.len() / 4 - 4) { let mut data = writer.cpeek_u32()?; let mut add = data.to_le_bytes(); add[0] = add[0].rotate_left(shift); shift = (shift + 1) % 8; data = u32::from_le_bytes(add); data ^= 0x084DF873 ^ 0xFF987DEE; writer.write_u32(data)?; } } } Ok(MemReader::new(writer.into_inner())) } fn load_point_data(mut data: MemReader) -> Result> { let mut magic = [0u8; 16]; data.read_exact(&mut magic)?; if magic != *b"$POINT_LIST_****" && magic != *b"_POINT_LIST_****" { return Err(anyhow::anyhow!("Invalid point list magic: {:?}", magic)); } let is_crypted = magic[0] == b'$' && { let first_offset = data.cpeek_u32()?; first_offset & 0xFF000000 != 0 }; let mut label_offsets = Vec::new(); let mut shift = 4; while !data.is_eof() { let mut val = data.read_u32()?; if is_crypted { let mut add = val.to_le_bytes(); add[0] = add[0].rotate_left(shift); shift = (shift + 1) % 8; val = u32::from_le_bytes(add); val ^= 0x084DF873 ^ 0xFF987DEE; } label_offsets.push(val + CODE_OFFSET); } label_offsets.reverse(); Ok(label_offsets) } } impl Script for SoftpalScript { fn default_output_script_type(&self) -> OutputScriptType { OutputScriptType::Json } fn default_format_type(&self) -> FormatOptions { FormatOptions::None } fn is_output_supported(&self, _: OutputScriptType) -> bool { true } fn multiple_message_files(&self) -> bool { true } fn extract_messages(&self) -> Result> { let mut messages = Vec::new(); let mut name = None; let max_len = self.texts.data.len() as u32; for str in &self.strs { let addr = self.data.cpeek_u32_at(str.offset as u64)?; if addr - 4 > max_len { continue; } let idx = self.texts.cpeek_u32_at(addr as u64)?; let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?; let text = decode_to_string(self.encoding, text.as_bytes(), false)?.replace("
", "\n"); let text = if self.add_message_index { format!("[{}]{}", idx, text) } else { text }; match str.typ { StringType::Name => { if text.is_empty() { continue; // Skip empty names } name = Some(text); } StringType::Message => messages.push(Message { name: name.take(), message: text, }), StringType::Hover => messages.push(Message::new(text, None)), StringType::Label => {} // Ignore labels } } Ok(messages) } fn extract_multiple_messages(&self) -> Result>> { let mut hovers = Vec::new(); let mut messages = Vec::new(); let mut label = None; let mut name = None; let mut result = HashMap::new(); let max_len = self.texts.data.len() as u32; for str in &self.strs { let addr = self.data.cpeek_u32_at(str.offset as u64)?; if addr - 4 > max_len { continue; } let idx = self.texts.cpeek_u32_at(addr as u64)?; let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?; let ptext = decode_to_string(self.encoding, text.as_bytes(), false)?.replace("
", "\n"); let text = if self.add_message_index { format!("[{}]{}", idx, ptext) } else { ptext.clone() }; match str.typ { StringType::Name => { if text.is_empty() { continue; // Skip empty names } name = Some(text); } StringType::Message => messages.push(Message::new(text, name.take())), StringType::Hover => hovers.push(Message::new(text, None)), StringType::Label => { if !messages.is_empty() { let key = label.take().unwrap_or_else(|| "default".to_string()); if result.contains_key(&key) { eprintln!( "Warning: Duplicate label '{}', overwriting previous messages.", key ); crate::COUNTER.inc_warning(); } result.insert(key, messages); messages = Vec::new(); } label = Some(ptext); } } } if !messages.is_empty() { let key = label.take().unwrap_or_else(|| "default".to_string()); result.insert(key, messages); } if !hovers.is_empty() { result.insert("hover".to_string(), hovers); } Ok(result) } fn import_messages<'a>( &'a self, messages: Vec, mut file: Box, filename: &str, encoding: Encoding, replacement: Option<&'a ReplacementTable>, ) -> Result<()> { let mut texts_filename = std::path::PathBuf::from(filename); texts_filename.set_file_name("TEXT.DAT"); let mut texts = Vec::new(); let mut reader = self.texts.to_ref(); reader.pos = 0x10; while !reader.is_eof() { reader.pos += 4; // Skip index texts.push(reader.read_cstring()?) } let mut texts_file = std::fs::File::create(&texts_filename) .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?; file.write_all(&self.data.data)?; let mut mes = messages.iter(); let mut mess = mes.next(); let texts_data_len = self.texts.data.len() as u32; let mut num_offset_map: HashMap = HashMap::new(); for str in &self.strs { let addr = self.data.cpeek_u32_at(str.offset as u64)?; if addr + 4 > texts_data_len { continue; } if str.typ.is_label() { continue; // Ignore labels } let m = match mess { Some(m) => m, None => return Err(anyhow::anyhow!("Not enough messages.")), }; let mut text = match str.typ { StringType::Name => match &m.name { Some(name) => name.clone(), None => return Err(anyhow::anyhow!("Missing name for message.")), }, StringType::Message => { let m = m.message.clone(); mess = mes.next(); m } StringType::Hover => { let m = m.message.clone(); mess = mes.next(); m } StringType::Label => continue, // Ignore labels }; if let Some(repl) = replacement { for (from, to) in repl.map.iter() { text = text.replace(from, to); } } text = text.replace("\n", "
"); let encoded = encode_string(encoding, &text, false)?; let s = std::ffi::CString::new(encoded)?; let num = texts.len() as u32; num_offset_map.insert(num, str.offset); texts.push(s); } if mess.is_some() || mes.next().is_some() { return Err(anyhow::anyhow!("Some messages were not processed.")); } texts_file.write_all(b"$TEXT_LIST__")?; texts_file.write_u32(texts.len() as u32)?; let mut nf = MemWriter::new(); for (num, text) in texts.into_iter().enumerate() { let num = num as u32; let newaddr = nf.pos as u32 + 0x10; if let Some(offset) = num_offset_map.get(&num) { file.write_u32_at(*offset as u64, newaddr)?; } nf.write_u32(num)?; nf.write_cstring(&text)?; } nf.pos = 0; let mut shift = 4; for _ in 0..(nf.data.len() / 4) { let mut data = nf.cpeek_u32()?; data ^= 0x084DF873 ^ 0xFF987DEE; let mut add = data.to_le_bytes(); add[0] = add[0].rotate_right(shift); shift = (shift + 1) % 8; data = u32::from_le_bytes(add); nf.write_u32(data)?; } texts_file.write_all(&nf.data)?; Ok(()) } fn import_multiple_messages<'a>( &'a self, messages: HashMap>, mut file: Box, filename: &str, encoding: Encoding, replacement: Option<&'a ReplacementTable>, ) -> Result<()> { let mut texts_filename = std::path::PathBuf::from(filename); texts_filename.set_file_name("TEXT.DAT"); let mut texts = Vec::new(); let mut reader = self.texts.to_ref(); reader.pos = 0x10; while !reader.is_eof() { reader.pos += 4; // Skip index texts.push(reader.read_cstring()?) } let mut texts_file = std::fs::File::create(&texts_filename) .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?; file.write_all(&self.data.data)?; let hover_messages = messages.get("hover").cloned().unwrap_or_default(); let mut hover_iter = hover_messages.iter(); let mut hover_mes = hover_iter.next(); let mut cur_label: Option = None; let mut cur_messages = messages .get(cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")) .cloned() .unwrap_or_default(); let mut cur_iter = cur_messages.iter(); let mut cur_mes = cur_iter.next(); let texts_data_len = self.texts.data.len() as u32; let mut num_offset_map: HashMap = HashMap::new(); for str in &self.strs { let addr = self.data.cpeek_u32_at(str.offset as u64)?; if addr + 4 > texts_data_len { continue; } let mut text = match str.typ { StringType::Label => { if cur_mes.is_some() || cur_iter.next().is_some() { return Err(anyhow::anyhow!( "Not all messages were used for label {}.", cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") )); } let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?; let text = decode_to_string(self.encoding, text.as_bytes(), false)? .replace("
", "\n"); cur_messages = messages.get(text.as_str()).cloned().unwrap_or_default(); cur_iter = cur_messages.iter(); cur_mes = cur_iter.next(); cur_label = Some(text); // We don't need update labels continue; } StringType::Hover => { let m = match hover_mes { Some(m) => m, None => return Err(anyhow::anyhow!("Not enough hover messages.")), }; let m = m.message.clone(); hover_mes = hover_iter.next(); m } StringType::Name => { let m = match cur_mes { Some(m) => m, None => { return Err(anyhow::anyhow!( "Not enough messages for label {}.", cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") )); } }; let name = match &m.name { Some(name) => name.clone(), None => return Err(anyhow::anyhow!("Missing name for message.")), }; name } StringType::Message => { let m = match cur_mes { Some(m) => m, None => { return Err(anyhow::anyhow!( "Not enough messages for label {}.", cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") )); } }; let m = m.message.clone(); cur_mes = cur_iter.next(); m } }; if let Some(repl) = replacement { for (from, to) in repl.map.iter() { text = text.replace(from, to); } } text = text.replace("\n", "
"); let encoded = encode_string(encoding, &text, false)?; let s = std::ffi::CString::new(encoded)?; let num = texts.len() as u32; num_offset_map.insert(num, str.offset); texts.push(s); } if cur_mes.is_some() || cur_iter.next().is_some() { return Err(anyhow::anyhow!( "Some messages were not processed for label {}.", cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") )); } if hover_mes.is_some() || hover_iter.next().is_some() { return Err(anyhow::anyhow!("Some hover messages were not processed.")); } texts_file.write_all(b"$TEXT_LIST__")?; texts_file.write_u32(texts.len() as u32)?; let mut nf = MemWriter::new(); for (num, text) in texts.into_iter().enumerate() { let num = num as u32; let newaddr = nf.pos as u32 + 0x10; if let Some(offset) = num_offset_map.get(&num) { file.write_u32_at(*offset as u64, newaddr)?; } nf.write_u32(num)?; nf.write_cstring(&text)?; } nf.pos = 0; let mut shift = 4; for _ in 0..(nf.data.len() / 4) { let mut data = nf.cpeek_u32()?; data ^= 0x084DF873 ^ 0xFF987DEE; let mut add = data.to_le_bytes(); add[0] = add[0].rotate_right(shift); shift = (shift + 1) % 8; data = u32::from_le_bytes(add); nf.write_u32(data)?; } texts_file.write_all(&nf.data)?; Ok(()) } fn custom_output_extension<'a>(&'a self) -> &'a str { "txt" } fn custom_export(&self, filename: &std::path::Path, _encoding: Encoding) -> Result<()> { let file = std::fs::File::create(filename) .map_err(|e| anyhow::anyhow!("Failed to create file {}: {}", filename.display(), e))?; let mut file = std::io::BufWriter::new(file); Disasm::new(&self.data.data, &self.label_offsets)?.disassemble(Some(&mut file))?; Ok(()) } }