diff --git a/Cargo.toml b/Cargo.toml index c2ad45d..1ea6e2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = [ "cz", "pak_explorer", - "luca_pak", "utils", + "luca_pak", "utils", "luca_script", ] [workspace.package] diff --git a/luca_script/Cargo.toml b/luca_script/Cargo.toml new file mode 100644 index 0000000..8a16807 --- /dev/null +++ b/luca_script/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "luca_script" +version = "0.1.0" +edition = "2021" +authors.workspace = true + +[dependencies] +byteorder-lite = "0.1.0" +encoding_rs = "0.8.35" + +[lints] +workspace = true diff --git a/luca_script/src/LBEE_opcodes b/luca_script/src/LBEE_opcodes new file mode 100644 index 0000000..00aa4fb --- /dev/null +++ b/luca_script/src/LBEE_opcodes @@ -0,0 +1,107 @@ +EQU +EQUN +EQUV +ADD +SUB +MUL +DIV +MOD +AND +OR +RANDOM +VARSTR +SET +FLAGCLR +GOTO +ONGOTO +GOSUB +IFY +IFN +RETURN +JUMP +FARCALL +FARRETURN +JUMPPOINT +END +VARSTR_SET +TALKNAME_SET +ARFLAGSET +COLORBG_SET +SPLINE_SET +SHAKELIST_SET +MESSAGE +MESSAGE_CLEAR +SELECT +CLOSE_WINDOW +LOG +LOG_PAUSE +LOG_END +VOICE +WAIT_COUNT +WAIT_TIME +FFSTOP +INIT +STOP +IMAGELOAD +IMAGEUPADTE +ARC +MOVE +MOVE2 +ROT +PEND +FADE +SCALE +SHAKE +SHAKELIST +BASE +MCMOVE +MCARC +MCROT +MCSHAKE +MCFADE +WAIT +DRAW +WIPE +FRAMEON +FRAMEOFF +FW +SCISSOR +DELAY +RASTER +TONE +SCALECOSSIN +BMODE +SIZE +SPLINE +DISP +MASK +SG_QUAKE +BGM +BGM_WAITSTART +BGM_WAITFADE +SE +SE_STOP +SE_WAIT +VOLUME +MOVIE +SETCGFLAG +EX +TROPHY +SETBGMFLAG +TASK +BTFUNC +BATTLE +KOEP +BT_ACCESSORY_SELECT +UNDO_CLEAR +PTFUNC +PT +GMFUNC +GM +DEL_CALLSTACK +FULLQUAKE_ZOOM +LBFUNC +LBBG +HAIKEI_SET +SAYAVOICETEXT +UNKNOWN \ No newline at end of file diff --git a/luca_script/src/main.rs b/luca_script/src/main.rs new file mode 100644 index 0000000..5e3ce02 --- /dev/null +++ b/luca_script/src/main.rs @@ -0,0 +1,181 @@ +mod utils; + +use std::{cell::LazyCell, fs::{self, File}, io::Read, path::PathBuf}; + +use byteorder_lite::{ReadBytesExt, LE}; +use utils::Encoding; + +const OPCODES: LazyCell> = LazyCell::new(|| fs::read_to_string("LBEE_opcodes") + .unwrap() + .split("\n") + .map(|s| s.to_owned()) + .collect() +); + +fn main() { + let file_path = PathBuf::from("SEEN0513"); + + let mut script = File::open(&file_path).unwrap(); + + println!("Start parsing script"); + let script = parse_script( + &mut script, + file_path.file_name().unwrap().to_str().unwrap() + ); + println!("Parsing finished"); + + for c in script.opcodes { + let ascii_string = String::from_utf8_lossy(&c.param_bytes); + //println!("{:>4}: '{:>11}' — {}", c.index, c.string, ascii_string); + SpecificOpcode::decode(&c.string, c.param_bytes); + } +} + +fn parse_script(script_stream: &mut S, name: &str) -> Script { + let mut opcodes = Vec::new(); + let mut _offset = 0; + let mut i = 0; + let mut pos = 0; + loop { + // Read all base info + let (length, number, flag) = ( + script_stream.read_u16::().unwrap() as usize, + script_stream.read_u8().unwrap(), + script_stream.read_u8().unwrap() + ); + let string = OPCODES[number as usize].clone(); + + _offset += 4; + + let raw_len = length - 4; + let mut raw_bytes = vec![0u8; raw_len]; + script_stream.read_exact(&mut raw_bytes).unwrap(); + _offset += raw_len; + + // Read extra align byte if alignment needed + let align = if length % 2 != 0 { + _offset += 1; + Some(script_stream.read_u8().unwrap()) + } else { + None + }; + + let mut fixed_param = None; + let param_bytes = match flag { + 0 => raw_bytes.clone(), + f if f < 2 => { + fixed_param = Some(vec![ + u16::from_le_bytes(raw_bytes[..2].try_into().unwrap()), + ]); + raw_bytes[2..].to_vec() + } + _ => { + fixed_param = Some(vec![ + u16::from_le_bytes(raw_bytes[..2].try_into().unwrap()), + u16::from_le_bytes(raw_bytes[2..4].try_into().unwrap()), + ]); + raw_bytes[4..].to_vec() + } + }; + + opcodes.push(Opcode { + index: i, + position: pos, + length, + number, + string: string.clone(), + flag, + raw_bytes, + align, + fixed_param, + param_bytes + }); + + // Break if END opcode reached + if &string == "END" { + break; + } + + pos += (length + 1) & !1; + i += 1; + } + + Script { + name: name.to_string(), + code_count: opcodes.len(), + opcodes, + } +} + +#[derive(Debug, Clone)] +struct Script { + name: String, + opcodes: Vec, + code_count: usize, +} + +#[derive(Debug, Clone)] +struct Opcode { + index: usize, + position: usize, + length: usize, + number: u8, + string: String, + + flag: u8, + raw_bytes: Vec, + align: Option, + fixed_param: Option>, + param_bytes: Vec, +} + +#[derive(Debug, Clone)] +enum SpecificOpcode { + Message { + voice_id: u16, + messages: Vec, + end: Vec, + }, + Select, + Battle, + Task, + SayAVoiceText, + VarStrSet, + GoTo, + GoSub, + Jump, + FarCall, + IFN, + IFY, + Random, + ImageLoad, + Unknown, +} + +impl SpecificOpcode { + pub fn decode(opcode_str: &str, param_bytes: Vec) -> Self { + match opcode_str { + "MESSAGE" => Self::message(param_bytes), + _ => Self::Unknown + } + } + + fn message(param_bytes: Vec) -> Self { + let voice_id = u16::from_le_bytes(param_bytes[0..2].try_into().unwrap()); + + let mut messages = Vec::new(); + let mut offset = 2; + for _ in 0..2 { + let (o, string) = utils::get_string(¶m_bytes, offset, Encoding::UTF16, None).unwrap(); + messages.push(string); + offset = o; + } + dbg!(&messages); + + Self::Message { + voice_id, + messages, + end: param_bytes[offset..].to_vec() + } + } +} diff --git a/luca_script/src/utils.rs b/luca_script/src/utils.rs new file mode 100644 index 0000000..ee4ca3b --- /dev/null +++ b/luca_script/src/utils.rs @@ -0,0 +1,53 @@ +use std::error::Error; + +use encoding_rs::*; + +pub enum Encoding { + UTF8, + UTF16, + ShiftJIS, +} + +pub fn get_string( + bytes: &[u8], + offset: usize, + format: Encoding, + len: Option +) -> Result<(usize, String), Box> { + let slice = &bytes[offset..]; + + // Find the end of the string + let mut end = 0; + let mut char_width = 1; + if let Some(l) = len { + end = l; + } else { + match format { + Encoding::UTF8 | Encoding::ShiftJIS => { + while (end < slice.len()) && (slice[end] != 0) { + end += 1 + } + }, + Encoding::UTF16 => { + char_width = 2; + while (end + 1 < slice.len()) && !((slice[end] == 0) && (slice[end + 1] == 0)) { + end += 2 + } + }, + } + }; + + let string = match format { + Encoding::UTF8 => String::from_utf8(slice[..end].to_vec())?, + Encoding::UTF16 => { + String::from_utf16( + &slice[..end].chunks_exact(2) + .map(|e| u16::from_le_bytes(e.try_into().unwrap())) + .collect::>() + )? + } + Encoding::ShiftJIS => SHIFT_JIS.decode(&slice[..end]).0.to_string(), + }; + + Ok((offset + end + char_width, string)) +}