From d8ef0826454d557207375c9efdbe0320bb5d4807 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 26 Dec 2025 17:29:05 +0800 Subject: [PATCH] Add new game support for escude --- src/args.rs | 4 + src/main.rs | 2 + src/scripts/escude/ops/hanaou.rs | 294 +++++++++++++++++++++++++++++++ src/scripts/escude/ops/mod.rs | 1 + src/scripts/escude/script.rs | 110 +++++++++--- src/types.rs | 3 + 6 files changed, 391 insertions(+), 23 deletions(-) create mode 100644 src/scripts/escude/ops/hanaou.rs diff --git a/src/args.rs b/src/args.rs index a5043ea..98d889a 100644 --- a/src/args.rs +++ b/src/args.rs @@ -189,6 +189,10 @@ pub struct Arg { #[arg(long, global = true)] /// The path to the Escude enum script file (enum_scr.bin) pub escude_enum_scr: Option, + #[cfg(feature = "escude")] + #[arg(long, global = true)] + /// Escude game title + pub escude_op: Option, #[cfg(feature = "bgi")] #[arg(long, action = ArgAction::SetTrue, global = true)] /// Duplicate same strings when importing into BGI scripts. diff --git a/src/main.rs b/src/main.rs index 363804b..bf9ecf8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3057,6 +3057,8 @@ fn main() { xp3_no_adler: arg.xp3_no_adler, #[cfg(feature = "bgi")] bgi_add_space: arg.bgi_add_space, + #[cfg(feature = "escude")] + escude_op: arg.escude_op, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/escude/ops/hanaou.rs b/src/scripts/escude/ops/hanaou.rs new file mode 100644 index 0000000..e138b00 --- /dev/null +++ b/src/scripts/escude/ops/hanaou.rs @@ -0,0 +1,294 @@ +use super::super::script::ReadParam; +use super::base::CustomOps; +use crate::ext::io::*; +use anyhow::Result; +use int_enum::IntEnum; +use std::collections::HashMap; +use std::io::Seek; + +#[repr(u8)] +#[derive(Debug, IntEnum)] +enum HanaouOp { + End = 0x22, + Jump, + Call, + AutoPlay, + Frame, + Text, + Clear, + Gap, + Mes, + Tlk, + Menu, + Select, + LsfInit, + LsfSet, + Cg, + Em, + Clr, + Disp, + Path, + Trans, + BgmPlay, + BgmStop, + BgmVolume, + BgmFx, + AmbPlay, + AmbStop, + AmbVolume, + AmbFx, + SePlay, + SeStop, + SeWait, + SeVolume, + SeFx, + VocPlay, + VocStop, + VocWait, + VocVolume, + VocFx, + Quake, + Flash, + Filter, + Effect, + Sync, + Wait, + Movie, + Credit, + Event, + Scene, + Title, + Notice, + Info, + SetPass, + IsPass, + AutoSave, + Place, + OpenName, + Name, + LogNew, + LogOut, + ElapsedDays, + Date, + TimeTable, + Lesson, + LessonExp, + QuestAdd, + QuestDel, + QuestMenu, + QuestExp, + MasterExp, + Battle, + Status, + Tutorial, + SetMaster, + GetMaster, + SetPlayer, + GetPlayer, + SetQuest, + GetQuest, + AddSkill, + HasSkill, + SetDesk, + GetLesson, + Impact, +} + +#[derive(Debug)] +pub struct HanaouOps { + prev_name: Option, + menus: HashMap, + last_select: usize, +} + +impl HanaouOps { + pub fn new() -> Self { + Self { + prev_name: None, + menus: HashMap::new(), + last_select: 0, + } + } +} + +use HanaouOp::*; + +impl CustomOps for HanaouOps +where + T: std::fmt::Debug + TryInto + std::hash::Hash, +{ + fn run<'a>(&mut self, vm: &mut super::super::script::VM<'a, T>, op: u8) -> Result + where + MemReaderRef<'a>: ReadParam, + T: TryInto + + Default + + Eq + + Ord + + Copy + + std::fmt::Debug + + std::fmt::Display + + std::hash::Hash + + From + + std::ops::Neg + + std::ops::Add + + std::ops::Sub + + std::ops::Mul + + std::ops::Div + + std::ops::Rem + + std::ops::Not + + std::ops::BitAnd + + std::ops::BitOr + + std::ops::BitXor + + std::ops::Shr + + std::ops::Shl, + anyhow::Error: From<>::Error>, + { + if let Ok(op) = HanaouOp::try_from(op) { + match op { + End => vm.skip_n_params(1, false), + Jump => vm.skip_n_params(1, false), + Call => vm.skip_n_params(1, false), + AutoPlay => vm.skip_n_params(1, false), + Frame => vm.skip_n_params(1, false), + Text => vm.skip_n_params(2, false), + Clear => vm.skip_n_params(1, false), + Gap => vm.skip_n_params(2, false), + // Handle concat name + Mes => { + let params = vm.read_params(Some(1))?; + let mes = params[0]; + vm.mess.insert(mes); + if let Some(name) = self.prev_name.take() { + vm.names.insert(mes, name); + } + Ok(false) + } + Tlk => { + let params = vm.read_params(None)?; + let name = params + .get(0) + .cloned() + .ok_or(anyhow::anyhow!("Missing name parameter"))?; + self.prev_name = Some(name); + Ok(false) + } + Menu => { + let params = vm.read_params(Some(3))?; + let id = params[0]; + let mes = params[1]; + vm.mess.insert(mes); + self.menus.insert(id, mes); + Ok(false) + } + Select => { + let param = vm.read_params(Some(1))?; + println!("Select param: {:?}", param); + if let Some(var) = vm.vars.get_mut(&T::from(131)) { + *var = *var + T::from(1); + return Ok(false); + } + let offset = vm.reader.stream_position()? - 1; + for _ in self.last_select + 1..self.menus.len() { + vm.stack.push(offset); + // println!("Pushing offset: {offset:#x} to stack"); + } + vm.vars.insert(T::from(131), T::from(0)); + Ok(false) + } + LsfInit => vm.skip_n_params(1, false), + LsfSet => vm.skip_params(false), + Cg => vm.skip_params(false), + Em => vm.skip_n_params(5, false), + Clr => vm.skip_n_params(1, false), + Disp => vm.skip_n_params(3, false), + Path => vm.skip_params(false), + Trans => Ok(false), + BgmPlay => vm.skip_n_params(3, false), + BgmStop => vm.skip_n_params(1, false), + BgmVolume => vm.skip_n_params(2, false), + BgmFx => vm.skip_n_params(1, false), + AmbPlay => vm.skip_n_params(3, false), + AmbStop => vm.skip_n_params(1, false), + AmbVolume => vm.skip_n_params(2, false), + AmbFx => vm.skip_n_params(1, false), + SePlay => vm.skip_n_params(5, false), + SeStop => vm.skip_n_params(2, false), + SeWait => vm.skip_n_params(1, false), + SeVolume => vm.skip_n_params(3, false), + SeFx => vm.skip_n_params(1, false), + VocPlay => vm.skip_n_params(4, false), + VocStop => vm.skip_n_params(2, false), + VocWait => vm.skip_n_params(1, false), + VocVolume => vm.skip_n_params(3, false), + VocFx => vm.skip_n_params(1, false), + Quake => vm.skip_n_params(4, false), + Flash => vm.skip_n_params(2, false), + Filter => vm.skip_n_params(2, false), + Effect => vm.skip_n_params(1, false), + Sync => vm.skip_n_params(2, false), + Wait => vm.skip_n_params(1, false), + Movie => vm.skip_n_params(1, false), + Credit => vm.skip_n_params(1, false), + Event => vm.skip_n_params(1, false), + Scene => vm.skip_n_params(1, false), + Title => { + let title = vm.read_params(Some(1))?; + vm.mess.insert(title[0]); + Ok(false) + } + Notice => { + let notices = vm.read_params(Some(3))?; + vm.mess.insert(notices[0]); + Ok(false) + } + Info => { + let infos = vm.read_params(Some(2))?; + vm.mess.insert(infos[0]); + Ok(false) + } + SetPass => vm.skip_n_params(2, false), + IsPass => vm.skip_n_params(1, false), + AutoSave => Ok(false), + Place => vm.skip_n_params(1, false), + OpenName => vm.skip_n_params(1, false), + Name => { + let params = vm.read_params(Some(2))?; + let name = params[0]; + let mes = params[1]; + vm.mess.insert(mes); + vm.names.insert(mes, name); + Ok(false) + } + LogNew => vm.skip_n_params(1, false), + LogOut => vm.skip_params(false), + ElapsedDays => vm.skip_n_params(1, false), + Date => Ok(false), + TimeTable => vm.skip_n_params(2, false), + Lesson => vm.skip_n_params(1, false), + LessonExp => vm.skip_n_params(2, false), + QuestAdd => vm.skip_n_params(1, false), + QuestDel => vm.skip_n_params(1, false), + QuestMenu => Ok(false), + QuestExp => vm.skip_n_params(2, false), + MasterExp => vm.skip_n_params(2, false), + Battle => vm.skip_n_params(3, false), + Status => Ok(false), + Tutorial => vm.skip_n_params(1, false), + SetMaster => vm.skip_n_params(3, false), + GetMaster => vm.skip_n_params(2, false), + SetPlayer => vm.skip_n_params(2, false), + GetPlayer => vm.skip_n_params(1, false), + SetQuest => vm.skip_n_params(3, false), + GetQuest => vm.skip_n_params(2, false), + AddSkill => vm.skip_n_params(2, false), + HasSkill => vm.skip_n_params(1, false), + SetDesk => vm.skip_n_params(2, false), + GetLesson => vm.skip_n_params(3, false), + Impact => Ok(false), + } + } else { + // return Err(anyhow::anyhow!("Unknown Panicon operation: {op:#02x}")); + Ok(false) + } + } +} diff --git a/src/scripts/escude/ops/mod.rs b/src/scripts/escude/ops/mod.rs index 39ffcaf..f1a9937 100644 --- a/src/scripts/escude/ops/mod.rs +++ b/src/scripts/escude/ops/mod.rs @@ -1,2 +1,3 @@ pub mod base; +pub mod hanaou; pub mod panicon; diff --git a/src/scripts/escude/script.rs b/src/scripts/escude/script.rs index b58f450..f8e2b37 100644 --- a/src/scripts/escude/script.rs +++ b/src/scripts/escude/script.rs @@ -1,5 +1,5 @@ //! Escu:de Script File (.bin) -use super::list::{EnumScr, EscudeBinList, ListData, NameT}; +use super::list::{EnumScr, EscudeBinList, ListData, NameT, VarT}; use super::ops::base::CustomOps; use crate::ext::io::*; use crate::scripts::base::*; @@ -7,12 +7,22 @@ use crate::types::*; use crate::utils::encoding::{decode_to_string, encode_string}; use crate::utils::struct_pack::StructPack; use anyhow::Result; +use clap::ValueEnum; use int_enum::IntEnum; use std::collections::{BTreeSet, HashMap}; use std::ffi::CString; use std::io::{Read, Seek, SeekFrom}; use unicode_segmentation::UnicodeSegmentation; +#[derive(Debug, ValueEnum, Clone, Copy)] +/// Game title +pub enum EscudeOp { + /// パニカルコンフュージョン + Panicon, + /// 花嫁と魔王 ~王室のハーレムは下克上~ + Hanaou, +} + #[derive(Debug)] /// Builder for Escu:de binary script files pub struct EscudeBinScriptBuilder {} @@ -70,23 +80,47 @@ fn load_enum_script( filename: &str, encoding: Encoding, config: &ExtraConfig, -) -> Result> { +) -> Result<(Vec, Vec)> { let buf = crate::utils::files::read_file(filename)?; let scr = EscudeBinList::new(buf, filename, encoding, config)?; + let mut names = None; + let mut vars = None; for scr in scr.entries { match scr.data { ListData::Scr(scr) => match scr { - EnumScr::Names(names) => return Ok(names), + EnumScr::Names(name) => { + names = Some(name); + } + EnumScr::Vars(var) => { + vars = Some(var); + } _ => {} }, _ => {} } } - Err(anyhow::anyhow!( - "Failed to find name table in Escude enum script", + Ok(( + names.ok_or_else(|| anyhow::anyhow!("No names data in enum script"))?, + vars.ok_or_else(|| anyhow::anyhow!("No vars data in enum script"))?, )) } +// fn check_messages_in_vms<'a, T: TryInto + Copy + 'a, I>(messages: &[String], mes: I) +// where +// I: Iterator, +// { +// let mess = mes.filter_map(|m| (*m).try_into().ok()).collect::>(); +// for (i, str) in messages.iter().enumerate() { +// if str.is_empty() { +// continue; +// } +// if !mess.contains(&i) { +// eprintln!("WARN: Message at index {i} not referenced in VMs: {}", str); +// crate::COUNTER.inc_warning(); +// } +// } +// } + impl EscudeBinScript { /// Creates a new `EscudeBinScript` /// @@ -129,26 +163,53 @@ impl EscudeBinScript { } let names = match &config.escude_enum_scr { Some(loc) => match load_enum_script(loc, encoding, config) { - Ok(list) => { - let mut names = HashMap::new(); - let mut vm = VM::new(&vms); - vm.vars.insert(1, 1); - vm.vars.insert(132, 0); - vm.vars.insert(133, 0); - vm.vars.insert(134, 0); - vm.vars.insert(1001, 0); - vm.vars.insert(1003, 0); - for i in 135..140 { - vm.vars.insert(i, 1); - } - let _ = vm.run(Some(Box::new(super::ops::panicon::PaniconOps::new()))); - for (index, name) in vm.names.iter() { - if let Some(name) = list.get(*name as usize) { - names.insert(*index as usize, name.text.clone()); + Ok((list, vars)) => match config.escude_op { + Some(EscudeOp::Panicon) => { + let mut names = HashMap::new(); + let mut vm = VM::new(&vms); + for var in vars { + vm.vars.insert(var.value as i32, var.flag as i32); } + let _ = vm.run(Some(Box::new(super::ops::panicon::PaniconOps::new()))); + for (index, name) in vm.names.iter() { + if let Some(name) = list.get(*name as usize) { + names.insert(*index as usize, name.text.clone()); + } + } + // check_messages_in_vms(&strings, vm.mess.iter()); + Some(names) } - Some(names) - } + Some(EscudeOp::Hanaou) => { + let mut names = HashMap::new(); + let mut vm = VM::new(&vms); + for var in vars { + vm.vars.insert(var.value as i32, var.flag as i32); + } + let _ = vm.run(Some(Box::new(super::ops::hanaou::HanaouOps::new()))); + for (index, name) in vm.names.iter() { + if let Some(name) = list.get(*name as usize) { + names.insert(*index as usize, name.text.clone()); + } + } + // check_messages_in_vms(&strings, vm.mess.iter()); + Some(names) + } + None => { + let mut names = HashMap::new(); + let mut vm = VM::new(&vms); + for var in vars { + vm.vars.insert(var.value as i32, var.flag as i32); + } + let _ = vm.run(None); + for (index, name) in vm.names.iter() { + if let Some(name) = list.get(*name as usize) { + names.insert(*index as usize, name.text.clone()); + } + } + // check_messages_in_vms(&strings, vm.mess.iter()); + Some(names) + } + }, Err(e) => { eprintln!( "WARN: Failed to load Escude enum script from {}: {}", @@ -636,6 +697,9 @@ where } pub fn skip_n_params(&mut self, n: u64, nbreak: bool) -> Result { + if (self.data.len() as u64) < n { + println!("{:?}", self.data); + } for _ in 0..n { self.pop_data()?; } diff --git a/src/types.rs b/src/types.rs index cc22969..0e062c4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -523,6 +523,9 @@ pub struct ExtraConfig { /// Add an additional space at the end of message in BGI scripts when importing. /// This may help BGI engine to display the message correctly in save/load screen for some games. pub bgi_add_space: bool, + #[cfg(feature = "escude")] + /// Escude game title + pub escude_op: Option, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]