diff --git a/src/components/credits.rs b/src/components/credits.rs new file mode 100644 index 0000000..b91f60f --- /dev/null +++ b/src/components/credits.rs @@ -0,0 +1,49 @@ +use crate::common::Rect; +use crate::entity::GameEntity; +use crate::frame::Frame; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::shared_game_state::SharedGameState; + +pub struct Credits {} + +impl Credits { + pub fn new() -> Credits { + Credits {} + } +} + +impl GameEntity<()> for Credits { + fn tick(&mut self, state: &mut SharedGameState, custom: ()) -> GameResult { + Ok(()) + } + + fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult { + if state.creditscript_vm.lines.is_empty() { + return Ok(()); + } + + let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Casts")?; + for line in state.creditscript_vm.lines.iter() { + let x = (line.cast_id % 13) * 24; + let y = ((line.cast_id / 13) & 0xff) * 24; + let rect = Rect::new_size(x, y, 24, 24); + + batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect); + } + batch.draw(ctx)?; + + for line in state.creditscript_vm.lines.iter() { + state.font.draw_text_with_shadow( + line.text.chars(), + line.pos_x, + line.pos_y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + } + + Ok(()) + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 803ec9d..9e48a63 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,4 +1,5 @@ pub mod boss_life_bar; +pub mod credits; pub mod draw_common; pub mod flash; pub mod hud; diff --git a/src/live_debugger.rs b/src/live_debugger.rs index a537d63..6aa2ded 100644 --- a/src/live_debugger.rs +++ b/src/live_debugger.rs @@ -64,7 +64,7 @@ impl LiveDebugger { .resizable(false) .collapsed(true, Condition::FirstUseEver) .position([5.0, 5.0], Condition::FirstUseEver) - .size([400.0, 170.0], Condition::FirstUseEver) + .size([400.0, 190.0], Condition::FirstUseEver) .build(ui, || { ui.text(format!( "Player position: ({:.1},{:.1}), velocity: ({:.1},{:.1})", @@ -112,7 +112,7 @@ impl LiveDebugger { } ui.same_line(0.0); - if ui.button(im_str!("Events"), [0.0, 0.0]) { + if ui.button(im_str!("TSC Scripts"), [0.0, 0.0]) { self.events_visible = !self.events_visible; } @@ -124,7 +124,7 @@ impl LiveDebugger { #[cfg(feature = "scripting-lua")] { ui.same_line(0.0); - if ui.button(im_str!("Reload Scripts"), [0.0, 0.0]) { + if ui.button(im_str!("Reload Lua Scripts"), [0.0, 0.0]) { if let Err(err) = state.lua.reload_scripts(ctx) { log::error!("Error reloading scripts: {:?}", err); self.error = Some(ImString::new(err.to_string())); @@ -204,39 +204,40 @@ impl LiveDebugger { } if self.events_visible { - Window::new(im_str!("Events")) + Window::new(im_str!("TSC Scripts")) .resizable(false) .position([80.0, 80.0], Condition::Appearing) - .size([300.0, 300.0], Condition::Appearing) + .size([300.0, 320.0], Condition::Appearing) .build(ui, || { if self.events.is_empty() { self.event_ids.clear(); - let vm = &state.textscript_vm; + let scripts = state.textscript_vm.scripts.borrow(); - for event in vm.scripts.scene_script.get_event_ids() { + for event in scripts.scene_script.get_event_ids() { self.events.push(ImString::new(format!("Scene: #{:04}", event))); self.event_ids.push((ScriptType::Scene, event)); } - for event in vm.scripts.global_script.get_event_ids() { + for event in scripts.global_script.get_event_ids() { self.events.push(ImString::new(format!("Global: #{:04}", event))); self.event_ids.push((ScriptType::Global, event)); } - for event in vm.scripts.inventory_script.get_event_ids() { + for event in scripts.inventory_script.get_event_ids() { self.events.push(ImString::new(format!("Inventory: #{:04}", event))); self.event_ids.push((ScriptType::Inventory, event)); } - for event in vm.scripts.stage_select_script.get_event_ids() { + for event in scripts.stage_select_script.get_event_ids() { self.events.push(ImString::new(format!("Stage Select: #{:04}", event))); self.event_ids.push((ScriptType::StageSelect, event)); } } let events: Vec<&ImStr> = self.events.iter().map(|e| e.as_ref()).collect(); - ui.text_wrapped(&ImString::new(format!("Execution state: {:?}", state.textscript_vm.state))); + ui.text_wrapped(&ImString::new(format!("TextScript execution state: {:?}", state.textscript_vm.state))); + ui.text_wrapped(&ImString::new(format!("CreditScript execution state: {:?}", state.creditscript_vm.state))); ui.push_item_width(-1.0); ui.list_box(im_str!(""), &mut self.selected_event, &events, 10); @@ -256,11 +257,12 @@ impl LiveDebugger { if let Some((stype, event_num)) = self.event_ids.get(self.selected_event as usize) { let id = ((*stype as u32) << 16) | (*event_num as u32); if !self.text_windows.iter().any(|(e, _, _)| *e == id) { + let scripts = state.textscript_vm.scripts.borrow(); let script = match stype { - ScriptType::Scene => &state.textscript_vm.scripts.scene_script, - ScriptType::Global => &state.textscript_vm.scripts.global_script, - ScriptType::Inventory => &state.textscript_vm.scripts.inventory_script, - ScriptType::StageSelect => &state.textscript_vm.scripts.stage_select_script, + ScriptType::Scene => &scripts.scene_script, + ScriptType::Global => &scripts.global_script, + ScriptType::Inventory => &scripts.inventory_script, + ScriptType::StageSelect => &scripts.stage_select_script, }; match script.decompile_event(*event_num) { diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index ab08b28..813b1f5 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -5,6 +5,7 @@ use log::info; use crate::caret::CaretType; use crate::common::{interpolate_fix9_scale, Color, Direction, FadeDirection, FadeState, Rect}; use crate::components::boss_life_bar::BossLifeBar; +use crate::components::credits::Credits; use crate::components::draw_common::Alignment; use crate::components::flash::Flash; use crate::components::hud::HUD; @@ -30,6 +31,7 @@ use crate::player::{Player, TargetPlayer}; use crate::rng::XorShift; use crate::scene::title_scene::TitleScene; use crate::scene::Scene; +use crate::scripting::tsc::credit_script::CreditScriptVM; use crate::shared_game_state::{SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage}; use crate::scripting::tsc::text_script::{ConfirmSelection, ScriptMode, TextScriptExecutionState, TextScriptLine, TextScriptVM}; @@ -45,6 +47,7 @@ pub struct GameScene { pub boss_life_bar: BossLifeBar, pub stage_select: StageSelect, pub flash: Flash, + pub credits: Credits, pub inventory_ui: InventoryUI, pub hud_player1: HUD, pub hud_player2: HUD, @@ -132,6 +135,7 @@ impl GameScene { boss_life_bar: BossLifeBar::new(), stage_select: StageSelect::new(), flash: Flash::new(), + credits: Credits::new(), inventory_ui: InventoryUI::new(), hud_player1: HUD::new(Alignment::Left), hud_player2: HUD::new(Alignment::Right), @@ -1930,6 +1934,11 @@ impl Scene for GameScene { } } + if state.control_flags.credits_running() { + self.skip_counter = 0; + CreditScriptVM::run(state, ctx)?; + } + match state.fade_state { FadeState::FadeOut(tick, direction) if tick < 15 => { state.fade_state = FadeState::FadeOut(tick + 1, direction); @@ -2134,7 +2143,12 @@ impl Scene for GameScene { )?; } + if state.control_flags.credits_running() { + self.credits.draw(state, ctx, &self.frame)?; + } + self.draw_text_boxes(state, ctx)?; + if self.skip_counter > 0 { let text = format!("Hold {:?} to skip the cutscene", state.settings.player1_key_map.inventory); let width = state.font.text_width(text.chars(), &state.constants); diff --git a/src/scene/loading_scene.rs b/src/scene/loading_scene.rs index 575daf1..f6e763d 100644 --- a/src/scene/loading_scene.rs +++ b/src/scene/loading_scene.rs @@ -4,6 +4,7 @@ use crate::framework::filesystem; use crate::npc::NPCTable; use crate::scene::no_data_scene::NoDataScene; use crate::scene::Scene; +use crate::scripting::tsc::credit_script::CreditScript; use crate::shared_game_state::SharedGameState; use crate::stage::StageData; use crate::scripting::tsc::text_script::TextScript; @@ -37,6 +38,10 @@ impl LoadingScene { let stage_select_script = TextScript::load_from(stage_select_tsc, &state.constants)?; state.textscript_vm.set_stage_select_script(stage_select_script); + let credit_tsc = filesystem::open(ctx, [&state.base_path, "/Credit.tsc"].join(""))?; + let credit_script = CreditScript::load_from(credit_tsc, &state.constants)?; + state.creditscript_vm.set_script(credit_script); + if ctx.headless { log::info!("Headless mode detected, skipping intro and loading last saved game."); state.load_or_start_game(ctx)?; diff --git a/src/scripting/tsc/bytecode_utils.rs b/src/scripting/tsc/bytecode_utils.rs index 0373435..7575fca 100644 --- a/src/scripting/tsc/bytecode_utils.rs +++ b/src/scripting/tsc/bytecode_utils.rs @@ -3,7 +3,6 @@ use std::io::{Cursor, Read}; use crate::encoding::{read_cur_shift_jis, read_cur_wtf8}; use crate::framework::error::GameError::ParseError; use crate::framework::error::GameResult; -use crate::scripting::tsc::opcodes::OpCode; use crate::scripting::tsc::text_script::TextScriptEncoding; pub fn put_varint(val: i32, out: &mut Vec) { @@ -83,7 +82,6 @@ pub fn put_string(buffer: &mut Vec, out: &mut Vec, encoding: TextScriptE buffer.clear(); - put_varint(OpCode::_STR as i32, out); put_varint(chars, out); out.append(&mut tmp_buf); } diff --git a/src/scripting/tsc/compiler.rs b/src/scripting/tsc/compiler.rs index a05d008..8005bca 100644 --- a/src/scripting/tsc/compiler.rs +++ b/src/scripting/tsc/compiler.rs @@ -4,11 +4,11 @@ use std::str::FromStr; use itertools::Itertools; -use crate::encoding::read_cur_wtf8; use crate::framework::error::GameError::ParseError; use crate::framework::error::GameResult; use crate::scripting::tsc::bytecode_utils::{put_string, put_varint}; -use crate::scripting::tsc::opcodes::OpCode; +use crate::scripting::tsc::credit_script::CreditScript; +use crate::scripting::tsc::opcodes::{CreditOpCode, TSCOpCode}; use crate::scripting::tsc::parse_utils::{expect_char, read_number, skip_until}; use crate::scripting::tsc::text_script::{TextScript, TextScriptEncoding}; @@ -80,11 +80,12 @@ impl TextScript { match chr { b'#' if allow_next_event => { if !char_buf.is_empty() { + put_varint(TSCOpCode::_STR as i32, &mut bytecode); put_string(&mut char_buf, &mut bytecode, encoding); } // some events end without { @@ -98,6 +99,7 @@ impl TextScript { } if !char_buf.is_empty() { + put_varint(TSCOpCode::_STR as i32, &mut bytecode); put_string(&mut char_buf, &mut bytecode, encoding); } @@ -138,111 +140,111 @@ impl TextScript { iter: &mut Peekable, out: &mut Vec, ) -> GameResult { - let instr = OpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?; + let instr = TSCOpCode::from_str(code).map_err(|_| ParseError(format!("Unknown opcode: {}", code)))?; match instr { // Zero operand codes - OpCode::AEp - | OpCode::CAT - | OpCode::CIL - | OpCode::CLO - | OpCode::CLR - | OpCode::CPS - | OpCode::CRE - | OpCode::CSS - | OpCode::END - | OpCode::ESC - | OpCode::FLA - | OpCode::FMU - | OpCode::FRE - | OpCode::HMC - | OpCode::INI - | OpCode::KEY - | OpCode::LDP - | OpCode::MLP - | OpCode::MM0 - | OpCode::MNA - | OpCode::MS2 - | OpCode::MS3 - | OpCode::MSG - | OpCode::NOD - | OpCode::PRI - | OpCode::RMU - | OpCode::SAT - | OpCode::SLP - | OpCode::SMC - | OpCode::SPS - | OpCode::STC - | OpCode::SVP - | OpCode::TUR - | OpCode::WAS - | OpCode::ZAM - | OpCode::HM2 - | OpCode::POP - | OpCode::KE2 - | OpCode::FR2 => { + TSCOpCode::AEp + | TSCOpCode::CAT + | TSCOpCode::CIL + | TSCOpCode::CLO + | TSCOpCode::CLR + | TSCOpCode::CPS + | TSCOpCode::CRE + | TSCOpCode::CSS + | TSCOpCode::END + | TSCOpCode::ESC + | TSCOpCode::FLA + | TSCOpCode::FMU + | TSCOpCode::FRE + | TSCOpCode::HMC + | TSCOpCode::INI + | TSCOpCode::KEY + | TSCOpCode::LDP + | TSCOpCode::MLP + | TSCOpCode::MM0 + | TSCOpCode::MNA + | TSCOpCode::MS2 + | TSCOpCode::MS3 + | TSCOpCode::MSG + | TSCOpCode::NOD + | TSCOpCode::PRI + | TSCOpCode::RMU + | TSCOpCode::SAT + | TSCOpCode::SLP + | TSCOpCode::SMC + | TSCOpCode::SPS + | TSCOpCode::STC + | TSCOpCode::SVP + | TSCOpCode::TUR + | TSCOpCode::WAS + | TSCOpCode::ZAM + | TSCOpCode::HM2 + | TSCOpCode::POP + | TSCOpCode::KE2 + | TSCOpCode::FR2 => { put_varint(instr as i32, out); } // One operand codes - OpCode::BOA - | OpCode::BSL - | OpCode::FOB - | OpCode::FOM - | OpCode::QUA - | OpCode::UNI - | OpCode::MYB - | OpCode::MYD - | OpCode::FAI - | OpCode::FAO - | OpCode::WAI - | OpCode::FAC - | OpCode::GIT - | OpCode::NUM - | OpCode::DNA - | OpCode::DNP - | OpCode::FLm - | OpCode::FLp - | OpCode::MPp - | OpCode::SKm - | OpCode::SKp - | OpCode::EQp - | OpCode::EQm - | OpCode::MLp - | OpCode::ITp - | OpCode::ITm - | OpCode::AMm - | OpCode::UNJ - | OpCode::MPJ - | OpCode::YNJ - | OpCode::EVE - | OpCode::XX1 - | OpCode::SIL - | OpCode::LIp - | OpCode::SOU - | OpCode::CMU - | OpCode::SSS - | OpCode::ACH - | OpCode::S2MV - | OpCode::S2PJ - | OpCode::PSH => { + TSCOpCode::BOA + | TSCOpCode::BSL + | TSCOpCode::FOB + | TSCOpCode::FOM + | TSCOpCode::QUA + | TSCOpCode::UNI + | TSCOpCode::MYB + | TSCOpCode::MYD + | TSCOpCode::FAI + | TSCOpCode::FAO + | TSCOpCode::WAI + | TSCOpCode::FAC + | TSCOpCode::GIT + | TSCOpCode::NUM + | TSCOpCode::DNA + | TSCOpCode::DNP + | TSCOpCode::FLm + | TSCOpCode::FLp + | TSCOpCode::MPp + | TSCOpCode::SKm + | TSCOpCode::SKp + | TSCOpCode::EQp + | TSCOpCode::EQm + | TSCOpCode::MLp + | TSCOpCode::ITp + | TSCOpCode::ITm + | TSCOpCode::AMm + | TSCOpCode::UNJ + | TSCOpCode::MPJ + | TSCOpCode::YNJ + | TSCOpCode::EVE + | TSCOpCode::XX1 + | TSCOpCode::SIL + | TSCOpCode::LIp + | TSCOpCode::SOU + | TSCOpCode::CMU + | TSCOpCode::SSS + | TSCOpCode::ACH + | TSCOpCode::S2MV + | TSCOpCode::S2PJ + | TSCOpCode::PSH => { let operand = read_number(iter)?; put_varint(instr as i32, out); put_varint(operand as i32, out); } // Two operand codes - OpCode::FON - | OpCode::MOV - | OpCode::AMp - | OpCode::NCJ - | OpCode::ECJ - | OpCode::FLJ - | OpCode::ITJ - | OpCode::SKJ - | OpCode::AMJ - | OpCode::SMP - | OpCode::PSp - | OpCode::IpN - | OpCode::FFm => { + TSCOpCode::FON + | TSCOpCode::MOV + | TSCOpCode::AMp + | TSCOpCode::NCJ + | TSCOpCode::ECJ + | TSCOpCode::FLJ + | TSCOpCode::ITJ + | TSCOpCode::SKJ + | TSCOpCode::AMJ + | TSCOpCode::SMP + | TSCOpCode::PSp + | TSCOpCode::IpN + | TSCOpCode::FFm => { let operand_a = read_number(iter)?; if strict { expect_char(b':', iter)?; @@ -256,7 +258,7 @@ impl TextScript { put_varint(operand_b as i32, out); } // Three operand codes - OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => { + TSCOpCode::ANP | TSCOpCode::CNP | TSCOpCode::INP | TSCOpCode::TAM | TSCOpCode::CMP | TSCOpCode::INJ => { let operand_a = read_number(iter)?; if strict { expect_char(b':', iter)?; @@ -277,7 +279,7 @@ impl TextScript { put_varint(operand_c as i32, out); } // Four operand codes - OpCode::TRA | OpCode::MNP | OpCode::SNP => { + TSCOpCode::TRA | TSCOpCode::MNP | TSCOpCode::SNP => { let operand_a = read_number(iter)?; if strict { expect_char(b':', iter)?; @@ -304,7 +306,7 @@ impl TextScript { put_varint(operand_c as i32, out); put_varint(operand_d as i32, out); } - OpCode::_NOP | OpCode::_UNI | OpCode::_STR | OpCode::_END => { + TSCOpCode::_NOP | TSCOpCode::_UNI | TSCOpCode::_STR | TSCOpCode::_END => { unreachable!() } } @@ -312,3 +314,102 @@ impl TextScript { Ok(()) } } + +impl CreditScript { + pub fn compile(data: &[u8], strict: bool, encoding: TextScriptEncoding) -> GameResult { + println!("data: {}", String::from_utf8_lossy(data)); + let mut labels = HashMap::new(); + let mut bytecode = Vec::new(); + let mut iter = data.iter().copied().peekable(); + + while let Some(chr) = iter.next() { + match chr { + b'/' => { + put_varint(CreditOpCode::StopCredits as i32, &mut bytecode); + } + b'[' => { + let mut char_buf = Vec::new(); + + while let Some(&chr) = iter.peek() { + if chr == b']' { + iter.next(); + break; + } + + char_buf.push(chr); + iter.next(); + } + + if let Ok(cast_tile) = read_number(&mut iter) { + put_varint(CreditOpCode::PushLine as i32, &mut bytecode); + put_varint((cast_tile as u16) as i32, &mut bytecode); + put_string(&mut char_buf, &mut bytecode, encoding); + } + } + b'-' => { + let ticks = read_number(&mut iter)? as u16; + + put_varint(CreditOpCode::Wait as i32, &mut bytecode); + put_varint(ticks as i32, &mut bytecode); + } + b'+' => { + let offset = read_number(&mut iter)?; + + put_varint(CreditOpCode::ChangeXOffset as i32, &mut bytecode); + put_varint(offset, &mut bytecode); + } + b'!' => { + let music = read_number(&mut iter)? as u16; + + put_varint(CreditOpCode::ChangeMusic as i32, &mut bytecode); + put_varint(music as i32, &mut bytecode); + } + b'~' => { + put_varint(CreditOpCode::FadeMusic as i32, &mut bytecode); + } + b'l' => { + let label = read_number(&mut iter)? as u16; + let pos = bytecode.len() as u32; + + labels.insert(label, pos); + } + b'j' => { + let label = read_number(&mut iter)? as u16; + + put_varint(CreditOpCode::JumpLabel as i32, &mut bytecode); + put_varint(label as i32, &mut bytecode); + } + b'f' => { + let flag = read_number(&mut iter)? as u16; + if strict { + expect_char(b':', &mut iter)?; + } else { + iter.next().ok_or_else(|| ParseError("Script unexpectedly ended.".to_owned()))?; + } + let label = read_number(&mut iter)? as u16; + + put_varint(CreditOpCode::JumpFlag as i32, &mut bytecode); + put_varint(flag as i32, &mut bytecode); + put_varint(label as i32, &mut bytecode); + } + b'p' => { + iter.next(); // idfk what's that for, in cs+ Credits.tsc it's '2'. + + if strict { + expect_char(b':', &mut iter)?; + } else { + iter.next().ok_or_else(|| ParseError("Script unexpectedly ended.".to_owned()))?; + } + + let label = read_number(&mut iter)? as u16; + + put_varint(CreditOpCode::JumpPlayer2 as i32, &mut bytecode); + put_varint(label as i32, &mut bytecode); + } + _ => (), + } + } + + Ok(CreditScript { labels, bytecode }) + } +} diff --git a/src/scripting/tsc/credit_script.rs b/src/scripting/tsc/credit_script.rs index 6b2b80f..633b9f7 100644 --- a/src/scripting/tsc/credit_script.rs +++ b/src/scripting/tsc/credit_script.rs @@ -1,3 +1,216 @@ -pub struct CreditScript { +use std::collections::HashMap; +use std::io; +use std::io::{Cursor, Seek, SeekFrom}; +use num_traits::FromPrimitive; + +use crate::engine_constants::EngineConstants; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::scripting::tsc::bytecode_utils::{put_varint, read_cur_varint}; +use crate::scripting::tsc::encryption::decrypt_tsc; +use crate::scripting::tsc::opcodes::CreditOpCode; +use crate::shared_game_state::SharedGameState; + +pub struct CreditScript { + pub(in crate::scripting::tsc) labels: HashMap, + pub(in crate::scripting::tsc) bytecode: Vec, +} + +impl Default for CreditScript { + fn default() -> Self { + let mut bytecode = Vec::new(); + put_varint(CreditOpCode::StopCredits as i32, &mut bytecode); + + CreditScript { labels: HashMap::new(), bytecode } + } +} + +impl CreditScript { + /// Loads, decrypts and compiles a credit script from specified stream. + pub fn load_from(mut data: R, constants: &EngineConstants) -> GameResult { + let mut buf = Vec::new(); + data.read_to_end(&mut buf)?; + + if constants.textscript.encrypted { + decrypt_tsc(&mut buf); + } + + CreditScript::compile(&buf, false, constants.textscript.encoding) + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum CreditScriptExecutionState { + Ended, + Running(u32), + WaitTicks(u32, u16), +} + +pub struct CreditScriptLine { + pub pos_x: f32, + pub pos_y: f32, + pub cast_id: u16, + pub text: String, +} + +pub struct CreditScriptVM { + pub state: CreditScriptExecutionState, + pub lines: Vec, + pub text_offset: f32, + script: CreditScript, +} + +impl CreditScriptVM { + pub fn new() -> CreditScriptVM { + CreditScriptVM { + state: CreditScriptExecutionState::Ended, + lines: Vec::new(), + text_offset: 0.0, + script: CreditScript::default(), + } + } + + pub fn set_script(&mut self, script: CreditScript) { + self.reset(); + self.script = script; + } + + pub fn start(&mut self) { + self.reset(); + self.state = CreditScriptExecutionState::Running(0); + } + + pub fn reset(&mut self) { + self.lines.clear(); + self.text_offset = 0.0; + self.state = CreditScriptExecutionState::Ended; + } + + pub fn run(state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if state.creditscript_vm.state != CreditScriptExecutionState::Ended { + for line in state.creditscript_vm.lines.iter_mut() { + line.pos_y -= 0.5; + } + } + + state.creditscript_vm.lines.retain(|l| l.pos_y > -16.0); + + loop { + match state.creditscript_vm.state { + CreditScriptExecutionState::Ended => { + break; + } + CreditScriptExecutionState::Running(ip) => { + let mut cursor = Cursor::new(&state.creditscript_vm.script.bytecode); + cursor.seek(SeekFrom::Start(ip as u64))?; + + let op: CreditOpCode = if let Some(op) = FromPrimitive::from_i32( + read_cur_varint(&mut cursor).unwrap_or_else(|_| CreditOpCode::StopCredits as i32), + ) { + op + } else { + state.creditscript_vm.reset(); + return Ok(()); + }; + + match op { + CreditOpCode::_NOP => { + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::StopCredits => { + state.creditscript_vm.state = CreditScriptExecutionState::Ended; + } + CreditOpCode::PushLine => { + let cast_id = read_cur_varint(&mut cursor)? as u16; + let text_len = read_cur_varint(&mut cursor)?; + let mut text = String::new(); + text.reserve(text_len as usize); + + for _ in 0..text_len { + let chr = + std::char::from_u32(read_cur_varint(&mut cursor)? as u32).unwrap_or('\u{fffd}'); + text.push(chr); + } + + let line = CreditScriptLine { + pos_x: state.creditscript_vm.text_offset, + pos_y: 256.0, + cast_id, + text, + }; + + state.creditscript_vm.lines.push(line); + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::Wait => { + let ticks = read_cur_varint(&mut cursor)? as u16; + + state.creditscript_vm.state = + CreditScriptExecutionState::WaitTicks(cursor.position() as u32, ticks); + } + CreditOpCode::ChangeXOffset => { + let offset = read_cur_varint(&mut cursor)?; + + state.creditscript_vm.text_offset = offset as f32; + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::ChangeMusic => { + let song = read_cur_varint(&mut cursor)? as u16; + + state.sound_manager.play_song(song as usize, &state.constants, &state.settings, ctx)?; + + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::FadeMusic => { + // todo + + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::JumpLabel => { + let label = read_cur_varint(&mut cursor)? as u16; + + if let Some(target) = state.creditscript_vm.script.labels.get(&label) { + state.creditscript_vm.state = CreditScriptExecutionState::Running(*target); + continue; + } + + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::JumpFlag => { + let flag = read_cur_varint(&mut cursor)? as u16; + let label = read_cur_varint(&mut cursor)? as u16; + + if state.get_flag(flag as usize) { + if let Some(target) = state.creditscript_vm.script.labels.get(&label) { + state.creditscript_vm.state = CreditScriptExecutionState::Running(*target); + continue; + } + } + + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + CreditOpCode::JumpPlayer2 => { + let _label = read_cur_varint(&mut cursor)? as u16; + // todo + + state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32); + } + } + } + CreditScriptExecutionState::WaitTicks(ip, ticks) => { + if ticks == 0 { + state.creditscript_vm.state = CreditScriptExecutionState::Running(ip); + } else if ticks != 9999 { + state.creditscript_vm.state = CreditScriptExecutionState::WaitTicks(ip, ticks - 1); + break; + } else { + break; + } + } + } + } + + Ok(()) + } } diff --git a/src/scripting/tsc/decompiler.rs b/src/scripting/tsc/decompiler.rs index 687200b..9ee096f 100644 --- a/src/scripting/tsc/decompiler.rs +++ b/src/scripting/tsc/decompiler.rs @@ -5,7 +5,7 @@ use num_traits::FromPrimitive; use crate::framework::error::GameError::InvalidValue; use crate::framework::error::GameResult; use crate::scripting::tsc::bytecode_utils::read_cur_varint; -use crate::scripting::tsc::opcodes::OpCode; +use crate::scripting::tsc::opcodes::TSCOpCode; use crate::scripting::tsc::text_script::TextScript; impl TextScript { @@ -15,119 +15,119 @@ impl TextScript { let mut cursor = Cursor::new(bytecode); while let Ok(op_num) = read_cur_varint(&mut cursor) { - let op_maybe: Option = FromPrimitive::from_i32(op_num); + let op_maybe: Option = FromPrimitive::from_i32(op_num); if let Some(op) = op_maybe { match op { // Zero operand codes - OpCode::AEp - | OpCode::CAT - | OpCode::CIL - | OpCode::CLO - | OpCode::CLR - | OpCode::CPS - | OpCode::CRE - | OpCode::CSS - | OpCode::END - | OpCode::ESC - | OpCode::FLA - | OpCode::FMU - | OpCode::FRE - | OpCode::HMC - | OpCode::INI - | OpCode::KEY - | OpCode::LDP - | OpCode::MLP - | OpCode::MM0 - | OpCode::MNA - | OpCode::MS2 - | OpCode::MS3 - | OpCode::MSG - | OpCode::NOD - | OpCode::PRI - | OpCode::RMU - | OpCode::SAT - | OpCode::SLP - | OpCode::SMC - | OpCode::SPS - | OpCode::STC - | OpCode::SVP - | OpCode::TUR - | OpCode::WAS - | OpCode::ZAM - | OpCode::HM2 - | OpCode::POP - | OpCode::KE2 - | OpCode::FR2 => { + TSCOpCode::AEp + | TSCOpCode::CAT + | TSCOpCode::CIL + | TSCOpCode::CLO + | TSCOpCode::CLR + | TSCOpCode::CPS + | TSCOpCode::CRE + | TSCOpCode::CSS + | TSCOpCode::END + | TSCOpCode::ESC + | TSCOpCode::FLA + | TSCOpCode::FMU + | TSCOpCode::FRE + | TSCOpCode::HMC + | TSCOpCode::INI + | TSCOpCode::KEY + | TSCOpCode::LDP + | TSCOpCode::MLP + | TSCOpCode::MM0 + | TSCOpCode::MNA + | TSCOpCode::MS2 + | TSCOpCode::MS3 + | TSCOpCode::MSG + | TSCOpCode::NOD + | TSCOpCode::PRI + | TSCOpCode::RMU + | TSCOpCode::SAT + | TSCOpCode::SLP + | TSCOpCode::SMC + | TSCOpCode::SPS + | TSCOpCode::STC + | TSCOpCode::SVP + | TSCOpCode::TUR + | TSCOpCode::WAS + | TSCOpCode::ZAM + | TSCOpCode::HM2 + | TSCOpCode::POP + | TSCOpCode::KE2 + | TSCOpCode::FR2 => { result.push_str(format!("{:?}()\n", op).as_str()); } // One operand codes - OpCode::BOA - | OpCode::BSL - | OpCode::FOB - | OpCode::FOM - | OpCode::QUA - | OpCode::UNI - | OpCode::MYB - | OpCode::MYD - | OpCode::FAI - | OpCode::FAO - | OpCode::WAI - | OpCode::FAC - | OpCode::GIT - | OpCode::NUM - | OpCode::DNA - | OpCode::DNP - | OpCode::FLm - | OpCode::FLp - | OpCode::MPp - | OpCode::SKm - | OpCode::SKp - | OpCode::EQp - | OpCode::EQm - | OpCode::MLp - | OpCode::ITp - | OpCode::ITm - | OpCode::AMm - | OpCode::UNJ - | OpCode::MPJ - | OpCode::YNJ - | OpCode::EVE - | OpCode::XX1 - | OpCode::SIL - | OpCode::LIp - | OpCode::SOU - | OpCode::CMU - | OpCode::SSS - | OpCode::ACH - | OpCode::S2MV - | OpCode::S2PJ - | OpCode::PSH => { + TSCOpCode::BOA + | TSCOpCode::BSL + | TSCOpCode::FOB + | TSCOpCode::FOM + | TSCOpCode::QUA + | TSCOpCode::UNI + | TSCOpCode::MYB + | TSCOpCode::MYD + | TSCOpCode::FAI + | TSCOpCode::FAO + | TSCOpCode::WAI + | TSCOpCode::FAC + | TSCOpCode::GIT + | TSCOpCode::NUM + | TSCOpCode::DNA + | TSCOpCode::DNP + | TSCOpCode::FLm + | TSCOpCode::FLp + | TSCOpCode::MPp + | TSCOpCode::SKm + | TSCOpCode::SKp + | TSCOpCode::EQp + | TSCOpCode::EQm + | TSCOpCode::MLp + | TSCOpCode::ITp + | TSCOpCode::ITm + | TSCOpCode::AMm + | TSCOpCode::UNJ + | TSCOpCode::MPJ + | TSCOpCode::YNJ + | TSCOpCode::EVE + | TSCOpCode::XX1 + | TSCOpCode::SIL + | TSCOpCode::LIp + | TSCOpCode::SOU + | TSCOpCode::CMU + | TSCOpCode::SSS + | TSCOpCode::ACH + | TSCOpCode::S2MV + | TSCOpCode::S2PJ + | TSCOpCode::PSH => { let par_a = read_cur_varint(&mut cursor)?; result.push_str(format!("{:?}({})\n", op, par_a).as_str()); } // Two operand codes - OpCode::FON - | OpCode::MOV - | OpCode::AMp - | OpCode::NCJ - | OpCode::ECJ - | OpCode::FLJ - | OpCode::ITJ - | OpCode::SKJ - | OpCode::AMJ - | OpCode::SMP - | OpCode::PSp - | OpCode::IpN - | OpCode::FFm => { + TSCOpCode::FON + | TSCOpCode::MOV + | TSCOpCode::AMp + | TSCOpCode::NCJ + | TSCOpCode::ECJ + | TSCOpCode::FLJ + | TSCOpCode::ITJ + | TSCOpCode::SKJ + | TSCOpCode::AMJ + | TSCOpCode::SMP + | TSCOpCode::PSp + | TSCOpCode::IpN + | TSCOpCode::FFm => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; result.push_str(format!("{:?}({}, {})\n", op, par_a, par_b).as_str()); } // Three operand codes - OpCode::ANP | OpCode::CNP | OpCode::INP | OpCode::TAM | OpCode::CMP | OpCode::INJ => { + TSCOpCode::ANP | TSCOpCode::CNP | TSCOpCode::INP | TSCOpCode::TAM | TSCOpCode::CMP | TSCOpCode::INJ => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; let par_c = read_cur_varint(&mut cursor)?; @@ -135,7 +135,7 @@ impl TextScript { result.push_str(format!("{:?}({}, {}, {})\n", op, par_a, par_b, par_c).as_str()); } // Four operand codes - OpCode::TRA | OpCode::MNP | OpCode::SNP => { + TSCOpCode::TRA | TSCOpCode::MNP | TSCOpCode::SNP => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; let par_c = read_cur_varint(&mut cursor)?; @@ -143,7 +143,7 @@ impl TextScript { result.push_str(format!("{:?}({}, {}, {}, {})\n", op, par_a, par_b, par_c, par_d).as_str()); } - OpCode::_STR => { + TSCOpCode::_STR => { let len = read_cur_varint(&mut cursor)?; result.push_str(format!("%string(len = {}, value = \"", len).as_str()); @@ -169,9 +169,9 @@ impl TextScript { } result.push_str("\")\n"); } - OpCode::_NOP => result.push_str("%no_op()\n"), - OpCode::_UNI => result.push_str("%unimplemented()\n"), - OpCode::_END => result.push_str("%end_marker()\n"), + TSCOpCode::_NOP => result.push_str("%no_op()\n"), + TSCOpCode::_UNI => result.push_str("%unimplemented()\n"), + TSCOpCode::_END => result.push_str("%end_marker()\n"), } } else { break; diff --git a/src/scripting/tsc/encryption.rs b/src/scripting/tsc/encryption.rs new file mode 100644 index 0000000..8c0ec5c --- /dev/null +++ b/src/scripting/tsc/encryption.rs @@ -0,0 +1,13 @@ +pub fn decrypt_tsc(buf: &mut [u8]) { + let half = buf.len() / 2; + let key = if let Some(0) = buf.get(half) { 0xf9 } else { (-(*buf.get(half).unwrap() as isize)) as u8 }; + log::info!("Decrypting TSC using key {:#x}", key); + + for (idx, byte) in buf.iter_mut().enumerate() { + if idx == half { + continue; + } + + *byte = byte.wrapping_add(key); + } +} diff --git a/src/scripting/tsc/mod.rs b/src/scripting/tsc/mod.rs index ee034a0..f8984e7 100644 --- a/src/scripting/tsc/mod.rs +++ b/src/scripting/tsc/mod.rs @@ -2,6 +2,7 @@ mod bytecode_utils; mod compiler; pub mod credit_script; mod decompiler; +mod encryption; mod opcodes; mod parse_utils; pub mod text_script; diff --git a/src/scripting/tsc/opcodes.rs b/src/scripting/tsc/opcodes.rs index 15c0092..2d27fa6 100644 --- a/src/scripting/tsc/opcodes.rs +++ b/src/scripting/tsc/opcodes.rs @@ -1,9 +1,8 @@ use num_derive::FromPrimitive; /// Engine's text script VM operation codes. -#[derive(EnumString, Debug, FromPrimitive, PartialEq)] -#[repr(i32)] -pub enum OpCode { +#[derive(EnumString, Debug, FromPrimitive, PartialEq, Copy, Clone)] +pub enum TSCOpCode { // ---- Internal opcodes (used by bytecode, no TSC representation) /// internal: no operation _NOP = 0, @@ -267,3 +266,54 @@ pub enum OpCode { FR2, // ---- Custom opcodes, for use by modders ---- } + +#[derive(FromPrimitive, PartialEq, Copy, Clone)] +pub enum CreditOpCode { + /// Internal, no operation + _NOP = 0, + /// `/` + /// + /// Arguments: `()` + StopCredits, + + /// `[{text: string}]{cast_tile: number}` + /// + /// Arguments: `(cast_tile: varint, text_len: varint, text: [varint; text_len])` + PushLine, + + /// `-{ticks: number}` + /// + /// Arguments: `(ticks: varint)` + Wait, + + /// `+{offset: number}` + /// + /// Arguments: `(offset: varint)` + ChangeXOffset, + + /// `!{music_id: number}` + /// + /// Arguments: `(music_id: varint)` + ChangeMusic, + + /// `~` + /// + /// Arguments: `()` + FadeMusic, + + /// `j{label: number}` + /// + /// Arguments: `(label: varint)` + JumpLabel, + + /// `f{flag: number}:{label: number}` + /// + /// Arguments: `(flag: varint, label: varint)` + JumpFlag, + + // ---- Cave Story+ (Switch) specific opcodes ---- + /// `p2:{label: number}` + /// + /// Arguments: `(label: varint)` + JumpPlayer2, +} diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index 35d17f0..3e9e390 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::cmp::Ordering; use std::collections::HashMap; use std::io; @@ -5,6 +6,7 @@ use std::io::Cursor; use std::io::Seek; use std::io::SeekFrom; use std::ops::Not; +use std::rc::Rc; use num_traits::{clamp, FromPrimitive}; @@ -22,7 +24,8 @@ use crate::player::{ControlMode, TargetPlayer}; use crate::scene::game_scene::GameScene; use crate::scene::title_scene::TitleScene; use crate::scripting::tsc::bytecode_utils::read_cur_varint; -use crate::scripting::tsc::opcodes::OpCode; +use crate::scripting::tsc::encryption::decrypt_tsc; +use crate::scripting::tsc::opcodes::TSCOpCode; use crate::shared_game_state::SharedGameState; use crate::weapon::WeaponType; @@ -80,7 +83,6 @@ impl Not for ConfirmSelection { } #[derive(Debug, PartialEq, Copy, Clone)] -#[repr(u8)] pub enum TextScriptExecutionState { Ended, Running(u16, u32), @@ -96,7 +98,7 @@ pub enum TextScriptExecutionState { } pub struct TextScriptVM { - pub scripts: Scripts, + pub scripts: Rc>, pub state: TextScriptExecutionState, pub stack: Vec, pub flags: TextScriptFlags, @@ -158,12 +160,12 @@ impl Scripts { impl TextScriptVM { pub fn new() -> Self { Self { - scripts: Scripts { + scripts: Rc::new(RefCell::new(Scripts { global_script: TextScript::new(), scene_script: TextScript::new(), inventory_script: TextScript::new(), stage_select_script: TextScript::new(), - }, + })), state: TextScriptExecutionState::Ended, stack: Vec::with_capacity(6), flags: TextScriptFlags(0), @@ -183,25 +185,35 @@ impl TextScriptVM { } pub fn set_global_script(&mut self, script: TextScript) { - self.scripts.global_script = script; + { + let mut scripts = self.scripts.borrow_mut(); + scripts.global_script = script; + } + if !self.suspend { self.reset(); } } pub fn set_scene_script(&mut self, script: TextScript) { - self.scripts.scene_script = script; + { + let mut scripts = self.scripts.borrow_mut(); + scripts.scene_script = script; + } + if !self.suspend { self.reset(); } } pub fn set_inventory_script(&mut self, script: TextScript) { - self.scripts.inventory_script = script; + let mut scripts = self.scripts.borrow_mut(); + scripts.inventory_script = script; } pub fn set_stage_select_script(&mut self, script: TextScript) { - self.scripts.stage_select_script = script; + let mut scripts = self.scripts.borrow_mut(); + scripts.stage_select_script = script; } pub fn reset(&mut self) { @@ -232,6 +244,10 @@ impl TextScriptVM { } pub fn run(state: &mut SharedGameState, game_scene: &mut GameScene, ctx: &mut Context) -> GameResult { + let scripts_ref = state.textscript_vm.scripts.clone(); + let scripts = scripts_ref.borrow_mut(); + let mut cached_event: Option<(u16, &Vec)> = None; + loop { if state.textscript_vm.suspend { break; @@ -244,7 +260,24 @@ impl TextScriptVM { } TextScriptExecutionState::Running(event, ip) => { state.control_flags.set_interactions_disabled(true); - state.textscript_vm.state = TextScriptVM::execute(event, ip, state, game_scene, ctx)?; + + // The `!event` case gets optimized out on None match + match (cached_event, !event) { + (None, bevent) | (Some((bevent, _)), _) if bevent != event => { + if let Some(bytecode) = scripts.find_script(state.textscript_vm.mode, event) { + cached_event = Some((event, bytecode)); + } else { + cached_event = None; + } + } + _ => (), + } + + state.textscript_vm.state = if let Some((_, bytecode)) = cached_event { + TextScriptVM::execute(bytecode, event, ip, state, game_scene, ctx)? + } else { + TextScriptExecutionState::Ended + }; if state.textscript_vm.state == TextScriptExecutionState::Ended { state.textscript_vm.reset(); @@ -260,7 +293,18 @@ impl TextScriptVM { state.touch_controls.control_type = TouchControlType::Dialog; } - if let Some(bytecode) = state.textscript_vm.scripts.find_script(state.textscript_vm.mode, event) { + match (cached_event, !event) { + (None, bevent) | (Some((bevent, _)), _) if bevent != event => { + if let Some(bytecode) = scripts.find_script(state.textscript_vm.mode, event) { + cached_event = Some((event, bytecode)); + } else { + cached_event = None; + } + } + _ => (), + } + + if let Some((_, bytecode)) = cached_event { let mut cursor = Cursor::new(bytecode); cursor.seek(SeekFrom::Start(ip as u64))?; @@ -481,6 +525,7 @@ impl TextScriptVM { } pub fn execute( + bytecode: &Vec, event: u16, ip: u32, state: &mut SharedGameState, @@ -489,826 +534,801 @@ impl TextScriptVM { ) -> GameResult { let mut exec_state = state.textscript_vm.state; - let state_ref = state as *mut SharedGameState; - let scripts = unsafe { &(*state_ref).textscript_vm.scripts }; + // let state_ref = state as *mut SharedGameState; + // let scripts = unsafe { &(*state_ref).textscript_vm.scripts }; - if let Some(bytecode) = scripts.find_script(state.textscript_vm.mode, event) { - let mut cursor = Cursor::new(bytecode); - cursor.seek(SeekFrom::Start(ip as u64))?; + let mut cursor = Cursor::new(bytecode); + cursor.seek(SeekFrom::Start(ip as u64))?; - let op_maybe: Option = - FromPrimitive::from_i32(read_cur_varint(&mut cursor).unwrap_or_else(|_| OpCode::END as i32)); + let op: TSCOpCode = if let Some(op) = + FromPrimitive::from_i32(read_cur_varint(&mut cursor).unwrap_or_else(|_| TSCOpCode::END as i32)) + { + op + } else { + return Ok(TextScriptExecutionState::Ended); + }; - if let Some(op) = op_maybe { - match op { - OpCode::_NOP => { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + match op { + TSCOpCode::_NOP => { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::_UNI => {} + TSCOpCode::_STR => { + let mut len = read_cur_varint(&mut cursor)? as u32; + if state.textscript_vm.flags.render() { + state.textscript_vm.prev_char = '\x00'; + exec_state = TextScriptExecutionState::Msg(event, cursor.position() as u32, len, 4); + } else { + while len > 0 { + len -= 1; + let _ = read_cur_varint(&mut cursor)?; } - OpCode::_UNI => {} - OpCode::_STR => { - let mut len = read_cur_varint(&mut cursor)? as u32; - if state.textscript_vm.flags.render() { - state.textscript_vm.prev_char = '\x00'; - exec_state = TextScriptExecutionState::Msg(event, cursor.position() as u32, len, 4); - } else { - while len > 0 { - len -= 1; - let _ = read_cur_varint(&mut cursor)?; - } - // simply skip the text if we aren't in message mode. - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + // simply skip the text if we aren't in message mode. + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::_END => { + state.textscript_vm.flags.set_cutscene_skip(false); + exec_state = TextScriptExecutionState::Ended; + } + TSCOpCode::END => { + state.textscript_vm.flags.set_cutscene_skip(false); + state.control_flags.set_tick_world(true); + state.control_flags.set_control_enabled(true); + + state.textscript_vm.flags.set_render(false); + state.textscript_vm.flags.set_background_visible(false); + state.textscript_vm.stack.clear(); + + game_scene.player1.cond.set_interacted(false); + game_scene.player2.cond.set_interacted(false); + + exec_state = TextScriptExecutionState::Ended; + } + TSCOpCode::SLP => { + state.textscript_vm.set_mode(ScriptMode::StageSelect); + + let event_num = if let Some(slot) = + state.teleporter_slots.get(game_scene.stage_select.current_teleport_slot as usize) + { + 1000 + slot.0 + } else { + 1000 + }; + + exec_state = TextScriptExecutionState::Running(event_num, 0); + } + TSCOpCode::PSp => { + let index = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if let Some(slot) = state.teleporter_slots.iter_mut().find(|s| s.0 == index) { + slot.1 = event_num; + } else { + state.teleporter_slots.push((index, event_num)); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::PRI => { + state.control_flags.set_tick_world(false); + state.control_flags.set_control_enabled(false); + + game_scene.player1.shock_counter = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::KEY => { + state.control_flags.set_tick_world(true); + state.control_flags.set_control_enabled(false); + + game_scene.player1.up = false; + game_scene.player1.shock_counter = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FRE => { + state.control_flags.set_tick_world(true); + state.control_flags.set_control_enabled(true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MYD => { + let new_direction = read_cur_varint(&mut cursor)? as usize; + if let Some(direction) = Direction::from_int(new_direction) { + game_scene.player1.direction = direction; + game_scene.player2.direction = direction; + } + game_scene.player1.cond.set_interacted(new_direction == 3); + game_scene.player2.cond.set_interacted(new_direction == 3); + + game_scene.player1.vel_x = 0; + game_scene.player2.vel_x = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MYB => { + let new_direction = read_cur_varint(&mut cursor)? as usize; + + game_scene.player1.vel_y = -0x200; + game_scene.player2.vel_y = -0x200; + + // Reset interaction condition, needed for places like talking to Toroko in shack + game_scene.player1.cond.set_interacted(false); + game_scene.player2.cond.set_interacted(false); + + if let Some(direction) = Direction::from_int_facing(new_direction) { + match direction { + Direction::Left => { + game_scene.player1.direction = Left; + game_scene.player2.direction = Left; + game_scene.player1.vel_x = 0x200; + game_scene.player2.vel_x = 0x200; } - } - OpCode::_END => { - state.textscript_vm.flags.set_cutscene_skip(false); - exec_state = TextScriptExecutionState::Ended; - } - OpCode::END => { - state.textscript_vm.flags.set_cutscene_skip(false); - state.control_flags.set_tick_world(true); - state.control_flags.set_control_enabled(true); - - state.textscript_vm.flags.set_render(false); - state.textscript_vm.flags.set_background_visible(false); - state.textscript_vm.stack.clear(); - - game_scene.player1.cond.set_interacted(false); - game_scene.player2.cond.set_interacted(false); - - exec_state = TextScriptExecutionState::Ended; - } - OpCode::SLP => { - state.textscript_vm.set_mode(ScriptMode::StageSelect); - - let event_num = if let Some(slot) = - state.teleporter_slots.get(game_scene.stage_select.current_teleport_slot as usize) - { - 1000 + slot.0 - } else { - 1000 - }; - - exec_state = TextScriptExecutionState::Running(event_num, 0); - } - OpCode::PSp => { - let index = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if let Some(slot) = state.teleporter_slots.iter_mut().find(|s| s.0 == index) { - slot.1 = event_num; - } else { - state.teleporter_slots.push((index, event_num)); + Direction::Up => { + game_scene.player1.vel_y = -0x200; + game_scene.player2.vel_y = -0x200; } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::PRI => { - state.control_flags.set_tick_world(false); - state.control_flags.set_control_enabled(false); - - game_scene.player1.shock_counter = 0; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::KEY => { - state.control_flags.set_tick_world(true); - state.control_flags.set_control_enabled(false); - - game_scene.player1.up = false; - game_scene.player1.shock_counter = 0; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FRE => { - state.control_flags.set_tick_world(true); - state.control_flags.set_control_enabled(true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MYD => { - let new_direction = read_cur_varint(&mut cursor)? as usize; - if let Some(direction) = Direction::from_int(new_direction) { - game_scene.player1.direction = direction; - game_scene.player2.direction = direction; + Direction::Right => { + game_scene.player1.direction = Right; + game_scene.player2.direction = Right; + game_scene.player1.vel_x = -0x200; + game_scene.player2.vel_x = -0x200; } - game_scene.player1.cond.set_interacted(new_direction == 3); - game_scene.player2.cond.set_interacted(new_direction == 3); - - game_scene.player1.vel_x = 0; - game_scene.player2.vel_x = 0; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MYB => { - let new_direction = read_cur_varint(&mut cursor)? as usize; - - game_scene.player1.vel_y = -0x200; - game_scene.player2.vel_y = -0x200; - - // Reset interaction condition, needed for places like talking to Toroko in shack - game_scene.player1.cond.set_interacted(false); - game_scene.player2.cond.set_interacted(false); - - if let Some(direction) = Direction::from_int_facing(new_direction) { - match direction { - Direction::Left => { - game_scene.player1.direction = Left; - game_scene.player2.direction = Left; - game_scene.player1.vel_x = 0x200; - game_scene.player2.vel_x = 0x200; - } - Direction::Up => { - game_scene.player1.vel_y = -0x200; - game_scene.player2.vel_y = -0x200; - } - Direction::Right => { - game_scene.player1.direction = Right; - game_scene.player2.direction = Right; - game_scene.player1.vel_x = -0x200; - game_scene.player2.vel_x = -0x200; - } - Direction::Bottom => { - game_scene.player1.vel_y = 0x200; - game_scene.player2.vel_y = 0x200; - } - Direction::FacingPlayer => { - for npc in game_scene.npc_list.iter_alive() { - if npc.event_num == new_direction as u16 { - if game_scene.player1.x >= npc.x { - game_scene.player1.direction = Left; - game_scene.player1.vel_x = 0x200; - } else { - game_scene.player1.direction = Right; - game_scene.player1.vel_x = -0x200; - } - - if game_scene.player2.x >= npc.x { - game_scene.player2.direction = Left; - game_scene.player2.vel_x = 0x200; - } else { - game_scene.player2.direction = Right; - game_scene.player2.vel_x = -0x200; - } - break; - } - } - } - } + Direction::Bottom => { + game_scene.player1.vel_y = 0x200; + game_scene.player2.vel_y = 0x200; } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SMC => { - game_scene.player1.cond.set_hidden(false); - game_scene.player2.cond.set_hidden(false); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::HMC => { - game_scene.player1.cond.set_hidden(true); - game_scene.player2.cond.set_hidden(true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::WAI => { - let ticks = read_cur_varint(&mut cursor)? as u16; - - exec_state = TextScriptExecutionState::WaitTicks(event, cursor.position() as u32, ticks); - } - OpCode::WAS => { - exec_state = TextScriptExecutionState::WaitStanding(event, cursor.position() as u32); - } - OpCode::NOD => { - exec_state = TextScriptExecutionState::WaitInput(event, cursor.position() as u32, 0); - } - OpCode::FLp | OpCode::FLm => { - let flag_num = read_cur_varint(&mut cursor)? as u16; - state.set_flag(flag_num as usize, op == OpCode::FLp); - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SKp | OpCode::SKm => { - let flag_num = read_cur_varint(&mut cursor)? as u16; - state.set_skip_flag(flag_num as usize, op == OpCode::SKp); - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FFm => { - let flag_from = read_cur_varint(&mut cursor)? as usize; - let flag_to = read_cur_varint(&mut cursor)? as usize; - - if flag_to >= flag_from { - for flag in flag_from..=flag_to { - if state.get_flag(flag) { - state.game_flags.set(flag, false); - break; - } - } - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FLJ => { - let flag_num = read_cur_varint(&mut cursor)? as usize; - let event_num = read_cur_varint(&mut cursor)? as u16; - if state.get_flag(flag_num) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::MPJ => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - if state.get_map_flag(game_scene.stage_id) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::ITJ => { - let item_id = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if game_scene.inventory_player1.has_item(item_id) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::INJ => { - let item_id = read_cur_varint(&mut cursor)? as u16; - let amount = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if game_scene.inventory_player1.has_item_amount(item_id, Ordering::Equal, amount) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::AMJ => { - let weapon = read_cur_varint(&mut cursor)? as u8; - let event_num = read_cur_varint(&mut cursor)? as u16; - let weapon_type: Option = FromPrimitive::from_u8(weapon); - - if weapon_type.is_some() && game_scene.inventory_player1.has_weapon(weapon_type.unwrap()) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::NCJ => { - let npc_type = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if game_scene.npc_list.is_alive_by_type(npc_type) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::ECJ => { - let npc_event_num = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if game_scene.npc_list.is_alive_by_event(npc_event_num) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - OpCode::SKJ => { - let flag_id = read_cur_varint(&mut cursor)? as u16; - let event_num = read_cur_varint(&mut cursor)? as u16; - - if state.get_skip_flag(flag_id as usize) { - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } else { - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - } - - OpCode::S2PJ => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - exec_state = if game_scene.player2.cond.alive() { - TextScriptExecutionState::Running(event_num, 0) - } else { - TextScriptExecutionState::Running(event, cursor.position() as u32) - } - } - OpCode::EVE => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } - OpCode::PSH => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - let saved_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - state.textscript_vm.stack.push(saved_state); - - state.textscript_vm.clear_text_box(); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } - OpCode::POP => { - if let Some(saved_state) = state.textscript_vm.stack.pop() { - exec_state = saved_state; - } else { - log::warn!("Tried to { - game_scene.player1.vel_x = 0; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SMP => { - let pos_x = read_cur_varint(&mut cursor)? as usize; - let pos_y = read_cur_varint(&mut cursor)? as usize; - - let tile_type = game_scene.stage.tile_at(pos_x, pos_y); - game_scene.stage.change_tile(pos_x, pos_y, tile_type.wrapping_sub(1)); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::CMP => { - let pos_x = read_cur_varint(&mut cursor)? as usize; - let pos_y = read_cur_varint(&mut cursor)? as usize; - let tile_type = read_cur_varint(&mut cursor)? as u8; - - if game_scene.stage.change_tile(pos_x, pos_y, tile_type) { - let mut npc = NPC::create(4, &state.npc_table); - npc.cond.set_alive(true); - npc.x = pos_x as i32 * 0x2000; - npc.y = pos_y as i32 * 0x2000; - - let _ = game_scene.npc_list.spawn(0x100, npc.clone()); - let _ = game_scene.npc_list.spawn(0x100, npc); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MLp => { - let life = read_cur_varint(&mut cursor)? as u16; - game_scene.player1.life += life; - game_scene.player1.max_life += life; - game_scene.player2.life += life; - game_scene.player2.max_life += life; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FAC => { - let face = read_cur_varint(&mut cursor)? as u16; - state.textscript_vm.face = face; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::CLR => { - state.textscript_vm.current_line = TextScriptLine::Line1; - state.textscript_vm.line_1.clear(); - state.textscript_vm.line_2.clear(); - state.textscript_vm.line_3.clear(); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MSG | OpCode::MS2 | OpCode::MS3 => { - state.textscript_vm.current_line = TextScriptLine::Line1; - state.textscript_vm.line_1.clear(); - state.textscript_vm.line_2.clear(); - state.textscript_vm.line_3.clear(); - state.textscript_vm.flags.set_render(true); - state.textscript_vm.flags.set_background_visible(op != OpCode::MS2); - state.textscript_vm.flags.set_fast(state.textscript_vm.flags.perma_fast()); - state.textscript_vm.flags.set_position_top(op != OpCode::MSG); - if op == OpCode::MS2 { - state.textscript_vm.face = 0; - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SAT | OpCode::CAT => { - state.textscript_vm.flags.set_perma_fast(true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - - OpCode::TUR => { - state.textscript_vm.flags.set_fast(true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::CLO => { - state.textscript_vm.flags.set_render(false); - state.textscript_vm.flags.set_background_visible(false); - state.textscript_vm.flags.set_fast(false); - state.textscript_vm.flags.set_position_top(false); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::YNJ => { - let event_no = read_cur_varint(&mut cursor)? as u16; - - state.sound_manager.play_sfx(5); - - exec_state = TextScriptExecutionState::WaitConfirmation( - event, - cursor.position() as u32, - event_no, - 16, - ConfirmSelection::Yes, - ); - } - OpCode::NUM => { - let index = read_cur_varint(&mut cursor)? as usize; - - if let Some(num) = state.textscript_vm.numbers.get(index) { - let mut str = num.to_string().chars().collect(); - - match state.textscript_vm.current_line { - TextScriptLine::Line1 => state.textscript_vm.line_1.append(&mut str), - TextScriptLine::Line2 => state.textscript_vm.line_2.append(&mut str), - TextScriptLine::Line3 => state.textscript_vm.line_3.append(&mut str), - } - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::GIT => { - let item = read_cur_varint(&mut cursor)? as u16; - state.textscript_vm.item = item; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::TRA => { - let map_id = read_cur_varint(&mut cursor)? as usize; - let event_num = read_cur_varint(&mut cursor)? as u16; - - let mut new_scene = GameScene::new(state, ctx, map_id)?; - - let block_size = new_scene.stage.map.tile_size.as_int() * 0x200; - let pos_x = read_cur_varint(&mut cursor)? as i32 * block_size; - let pos_y = read_cur_varint(&mut cursor)? as i32 * block_size; - - new_scene.intro_mode = game_scene.intro_mode; - new_scene.inventory_player1 = game_scene.inventory_player1.clone(); - new_scene.inventory_player2 = game_scene.inventory_player2.clone(); - new_scene.player1 = game_scene.player1.clone(); - new_scene.player1.vel_x = 0; - new_scene.player1.vel_y = 0; - new_scene.player1.x = pos_x; - new_scene.player1.y = pos_y; - new_scene.player2 = game_scene.player2.clone(); - new_scene.player2.vel_x = 0; - new_scene.player2.vel_y = 0; - new_scene.player2.x = pos_x; - new_scene.player2.y = pos_y; - // Reset player interaction flag upon TRA - new_scene.player1.cond.set_interacted(false); - new_scene.player2.cond.set_interacted(false); - new_scene.frame.wait = game_scene.frame.wait; - - let skip = state.textscript_vm.flags.cutscene_skip(); - state.control_flags.set_tick_world(true); - state.control_flags.set_interactions_disabled(true); - state.textscript_vm.flags.0 = 0; - state.textscript_vm.flags.set_cutscene_skip(skip); - state.textscript_vm.face = 0; - state.textscript_vm.item = 0; - state.textscript_vm.current_line = TextScriptLine::Line1; - state.textscript_vm.line_1.clear(); - state.textscript_vm.line_2.clear(); - state.textscript_vm.line_3.clear(); - state.textscript_vm.suspend = true; - state.next_scene = Some(Box::new(new_scene)); - - log::info!("Transitioning to stage {}, with script #{:04}", map_id, event_num); - exec_state = TextScriptExecutionState::Running(event_num, 0); - } - OpCode::MOV => { - let block_size = state.tile_size.as_int() * 0x200; - - let pos_x = read_cur_varint(&mut cursor)? as i32 * block_size; - let pos_y = read_cur_varint(&mut cursor)? as i32 * block_size; - - game_scene.player1.cond.set_interacted(false); - game_scene.player2.cond.set_interacted(false); - - for player in [&mut game_scene.player1, &mut game_scene.player2].iter_mut() { - player.vel_x = 0; - player.vel_y = 0; - player.x = pos_x; - player.y = pos_y; - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::S2MV => { - let param = read_cur_varint(&mut cursor)? as usize; - - let (executor, partner) = match state.textscript_vm.executor_player { - TargetPlayer::Player1 => (&game_scene.player1, &mut game_scene.player2), - TargetPlayer::Player2 => (&game_scene.player2, &mut game_scene.player1), - }; - - match param { - 0 | 1 => { - partner.vel_x = 0; - partner.vel_y = 0; - partner.x = executor.x + if param == 0 { -0x2000 } else { 0x2000 }; - partner.y = executor.y; - } - 2..=10 => { - log::warn!("<2MV unknown param"); - } - // what the fuck - i => { - let distance = i as i32 / 10; - - partner.vel_x = 0; - partner.vel_y = 0; - partner.x = - executor.x + if (param % 10) == 1 { distance * 0x200 } else { -distance * 0x200 }; - partner.y = executor.y; - } - } - - if partner.cond.alive() && !partner.cond.hidden() { - let mut npc = NPC::create(4, &state.npc_table); - npc.cond.set_alive(true); - npc.x = partner.x; - npc.y = partner.y; - - let _ = game_scene.npc_list.spawn(0x100, npc.clone()); - let _ = game_scene.npc_list.spawn(0x100, npc.clone()); - let _ = game_scene.npc_list.spawn(0x100, npc.clone()); - let _ = game_scene.npc_list.spawn(0x100, npc); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::UNI => { - let control_mode = read_cur_varint(&mut cursor)? as u8; - - let mode: Option = FromPrimitive::from_u8(control_mode); - if let Some(mode) = mode { - game_scene.player1.control_mode = mode; - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FAI => { - let fade_type = read_cur_varint(&mut cursor)? as usize; - - if let Some(direction) = FadeDirection::from_int(fade_type) { - state.fade_state = FadeState::FadeIn(15, direction); - } - - exec_state = TextScriptExecutionState::WaitFade(event, cursor.position() as u32); - } - OpCode::FAO => { - let fade_type = read_cur_varint(&mut cursor)? as usize; - - if let Some(direction) = FadeDirection::from_int(fade_type) { - state.fade_state = FadeState::FadeOut(-15, direction.opposite()); - } - - exec_state = TextScriptExecutionState::WaitFade(event, cursor.position() as u32); - } - OpCode::QUA => { - let count = read_cur_varint(&mut cursor)? as u16; - - state.quake_counter = count; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MNA => { - game_scene.display_map_name(160); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::CMU => { - let song_id = read_cur_varint(&mut cursor)? as usize; - state.sound_manager.play_song(song_id, &state.constants, &state.settings, ctx)?; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FMU => { - state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::RMU => { - state.sound_manager.restore_state()?; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SOU => { - let sound = read_cur_varint(&mut cursor)? as u8; - - state.sound_manager.play_sfx(sound); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::DNP => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - game_scene.npc_list.remove_by_event(event_num, state); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::DNA => { - let npc_remove_type = read_cur_varint(&mut cursor)? as u16; - - game_scene.npc_list.remove_by_type(npc_remove_type, state); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FOB => { - let part_id = read_cur_varint(&mut cursor)? as u16; - let ticks = read_cur_varint(&mut cursor)? as i32; - - game_scene.frame.wait = ticks; - game_scene.frame.update_target = UpdateTarget::Boss(part_id); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FOM => { - let ticks = read_cur_varint(&mut cursor)? as i32; - game_scene.frame.wait = ticks; - game_scene.frame.update_target = UpdateTarget::Player; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FON => { - let event_num = read_cur_varint(&mut cursor)? as u16; - let ticks = read_cur_varint(&mut cursor)? as i32; - game_scene.frame.wait = ticks; - - for npc in game_scene.npc_list.iter() { - if event_num == npc.event_num { - game_scene.frame.update_target = UpdateTarget::NPC(npc.id); - break; - } - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::BSL => { - let event_num = read_cur_varint(&mut cursor)? as u16; - - if event_num == 0 { - game_scene.boss_life_bar.set_boss_target(&game_scene.boss); - } else { + Direction::FacingPlayer => { for npc in game_scene.npc_list.iter_alive() { - if event_num == npc.event_num { - game_scene.boss_life_bar.set_npc_target(npc.id, &game_scene.npc_list); + if npc.event_num == new_direction as u16 { + if game_scene.player1.x >= npc.x { + game_scene.player1.direction = Left; + game_scene.player1.vel_x = 0x200; + } else { + game_scene.player1.direction = Right; + game_scene.player1.vel_x = -0x200; + } + + if game_scene.player2.x >= npc.x { + game_scene.player2.direction = Left; + game_scene.player2.vel_x = 0x200; + } else { + game_scene.player2.direction = Right; + game_scene.player2.vel_x = -0x200; + } break; } } } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } - OpCode::BOA => { - let action_num = read_cur_varint(&mut cursor)? as u16; + } - game_scene.boss.parts[0].action_num = action_num; + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SMC => { + game_scene.player1.cond.set_hidden(false); + game_scene.player2.cond.set_hidden(false); - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::HMC => { + game_scene.player1.cond.set_hidden(true); + game_scene.player2.cond.set_hidden(true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::WAI => { + let ticks = read_cur_varint(&mut cursor)? as u16; + + exec_state = TextScriptExecutionState::WaitTicks(event, cursor.position() as u32, ticks); + } + TSCOpCode::WAS => { + exec_state = TextScriptExecutionState::WaitStanding(event, cursor.position() as u32); + } + TSCOpCode::NOD => { + exec_state = TextScriptExecutionState::WaitInput(event, cursor.position() as u32, 0); + } + TSCOpCode::FLp | TSCOpCode::FLm => { + let flag_num = read_cur_varint(&mut cursor)? as u16; + state.set_flag(flag_num as usize, op == TSCOpCode::FLp); + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SKp | TSCOpCode::SKm => { + let flag_num = read_cur_varint(&mut cursor)? as u16; + state.set_skip_flag(flag_num as usize, op == TSCOpCode::SKp); + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FFm => { + let flag_from = read_cur_varint(&mut cursor)? as usize; + let flag_to = read_cur_varint(&mut cursor)? as usize; + + if flag_to >= flag_from { + for flag in flag_from..=flag_to { + if state.get_flag(flag) { + state.game_flags.set(flag, false); + break; + } } - OpCode::ANP => { - let event_num = read_cur_varint(&mut cursor)? as u16; - let action_num = read_cur_varint(&mut cursor)? as u16; - let tsc_direction = read_cur_varint(&mut cursor)? as usize; - let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + } - for npc in game_scene.npc_list.iter_alive() { - if npc.event_num == event_num { - npc.action_num = action_num; - npc.tsc_direction = tsc_direction as u16; + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FLJ => { + let flag_num = read_cur_varint(&mut cursor)? as usize; + let event_num = read_cur_varint(&mut cursor)? as u16; + if state.get_flag(flag_num) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::MPJ => { + let event_num = read_cur_varint(&mut cursor)? as u16; - if direction == Direction::FacingPlayer { - npc.direction = - if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; - } else { - npc.direction = direction; - } - } + if state.get_map_flag(game_scene.stage_id) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::ITJ => { + let item_id = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if game_scene.inventory_player1.has_item(item_id) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::INJ => { + let item_id = read_cur_varint(&mut cursor)? as u16; + let amount = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if game_scene.inventory_player1.has_item_amount(item_id, Ordering::Equal, amount) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::AMJ => { + let weapon = read_cur_varint(&mut cursor)? as u8; + let event_num = read_cur_varint(&mut cursor)? as u16; + let weapon_type: Option = FromPrimitive::from_u8(weapon); + + if weapon_type.is_some() && game_scene.inventory_player1.has_weapon(weapon_type.unwrap()) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::NCJ => { + let npc_type = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if game_scene.npc_list.is_alive_by_type(npc_type) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::ECJ => { + let npc_event_num = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if game_scene.npc_list.is_alive_by_event(npc_event_num) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + TSCOpCode::SKJ => { + let flag_id = read_cur_varint(&mut cursor)? as u16; + let event_num = read_cur_varint(&mut cursor)? as u16; + + if state.get_skip_flag(flag_id as usize) { + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } else { + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + } + + TSCOpCode::S2PJ => { + let event_num = read_cur_varint(&mut cursor)? as u16; + + exec_state = if game_scene.player2.cond.alive() { + TextScriptExecutionState::Running(event_num, 0) + } else { + TextScriptExecutionState::Running(event, cursor.position() as u32) + } + } + TSCOpCode::EVE => { + let event_num = read_cur_varint(&mut cursor)? as u16; + + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } + TSCOpCode::PSH => { + let event_num = read_cur_varint(&mut cursor)? as u16; + + let saved_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + state.textscript_vm.stack.push(saved_state); + + state.textscript_vm.clear_text_box(); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } + TSCOpCode::POP => { + if let Some(saved_state) = state.textscript_vm.stack.pop() { + exec_state = saved_state; + } else { + log::warn!("Tried to { + game_scene.player1.vel_x = 0; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SMP => { + let pos_x = read_cur_varint(&mut cursor)? as usize; + let pos_y = read_cur_varint(&mut cursor)? as usize; + + let tile_type = game_scene.stage.tile_at(pos_x, pos_y); + game_scene.stage.change_tile(pos_x, pos_y, tile_type.wrapping_sub(1)); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CMP => { + let pos_x = read_cur_varint(&mut cursor)? as usize; + let pos_y = read_cur_varint(&mut cursor)? as usize; + let tile_type = read_cur_varint(&mut cursor)? as u8; + + if game_scene.stage.change_tile(pos_x, pos_y, tile_type) { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = pos_x as i32 * 0x2000; + npc.y = pos_y as i32 * 0x2000; + + let _ = game_scene.npc_list.spawn(0x100, npc.clone()); + let _ = game_scene.npc_list.spawn(0x100, npc); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MLp => { + let life = read_cur_varint(&mut cursor)? as u16; + game_scene.player1.life += life; + game_scene.player1.max_life += life; + game_scene.player2.life += life; + game_scene.player2.max_life += life; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FAC => { + let face = read_cur_varint(&mut cursor)? as u16; + state.textscript_vm.face = face; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CLR => { + state.textscript_vm.current_line = TextScriptLine::Line1; + state.textscript_vm.line_1.clear(); + state.textscript_vm.line_2.clear(); + state.textscript_vm.line_3.clear(); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MSG | TSCOpCode::MS2 | TSCOpCode::MS3 => { + state.textscript_vm.current_line = TextScriptLine::Line1; + state.textscript_vm.line_1.clear(); + state.textscript_vm.line_2.clear(); + state.textscript_vm.line_3.clear(); + state.textscript_vm.flags.set_render(true); + state.textscript_vm.flags.set_background_visible(op != TSCOpCode::MS2); + state.textscript_vm.flags.set_fast(state.textscript_vm.flags.perma_fast()); + state.textscript_vm.flags.set_position_top(op != TSCOpCode::MSG); + if op == TSCOpCode::MS2 { + state.textscript_vm.face = 0; + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SAT | TSCOpCode::CAT => { + state.textscript_vm.flags.set_perma_fast(true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + + TSCOpCode::TUR => { + state.textscript_vm.flags.set_fast(true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CLO => { + state.textscript_vm.flags.set_render(false); + state.textscript_vm.flags.set_background_visible(false); + state.textscript_vm.flags.set_fast(false); + state.textscript_vm.flags.set_position_top(false); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::YNJ => { + let event_no = read_cur_varint(&mut cursor)? as u16; + + state.sound_manager.play_sfx(5); + + exec_state = TextScriptExecutionState::WaitConfirmation( + event, + cursor.position() as u32, + event_no, + 16, + ConfirmSelection::Yes, + ); + } + TSCOpCode::NUM => { + let index = read_cur_varint(&mut cursor)? as usize; + + if let Some(num) = state.textscript_vm.numbers.get(index) { + let mut str = num.to_string().chars().collect(); + + match state.textscript_vm.current_line { + TextScriptLine::Line1 => state.textscript_vm.line_1.append(&mut str), + TextScriptLine::Line2 => state.textscript_vm.line_2.append(&mut str), + TextScriptLine::Line3 => state.textscript_vm.line_3.append(&mut str), + } + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::GIT => { + let item = read_cur_varint(&mut cursor)? as u16; + state.textscript_vm.item = item; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::TRA => { + let map_id = read_cur_varint(&mut cursor)? as usize; + let event_num = read_cur_varint(&mut cursor)? as u16; + + let mut new_scene = GameScene::new(state, ctx, map_id)?; + + let block_size = new_scene.stage.map.tile_size.as_int() * 0x200; + let pos_x = read_cur_varint(&mut cursor)? as i32 * block_size; + let pos_y = read_cur_varint(&mut cursor)? as i32 * block_size; + + new_scene.intro_mode = game_scene.intro_mode; + new_scene.inventory_player1 = game_scene.inventory_player1.clone(); + new_scene.inventory_player2 = game_scene.inventory_player2.clone(); + new_scene.player1 = game_scene.player1.clone(); + new_scene.player1.vel_x = 0; + new_scene.player1.vel_y = 0; + new_scene.player1.x = pos_x; + new_scene.player1.y = pos_y; + new_scene.player2 = game_scene.player2.clone(); + new_scene.player2.vel_x = 0; + new_scene.player2.vel_y = 0; + new_scene.player2.x = pos_x; + new_scene.player2.y = pos_y; + // Reset player interaction flag upon TRA + new_scene.player1.cond.set_interacted(false); + new_scene.player2.cond.set_interacted(false); + new_scene.frame.wait = game_scene.frame.wait; + + let skip = state.textscript_vm.flags.cutscene_skip(); + state.control_flags.set_tick_world(true); + state.control_flags.set_interactions_disabled(true); + state.textscript_vm.flags.0 = 0; + state.textscript_vm.flags.set_cutscene_skip(skip); + state.textscript_vm.face = 0; + state.textscript_vm.item = 0; + state.textscript_vm.current_line = TextScriptLine::Line1; + state.textscript_vm.line_1.clear(); + state.textscript_vm.line_2.clear(); + state.textscript_vm.line_3.clear(); + state.textscript_vm.suspend = true; + state.next_scene = Some(Box::new(new_scene)); + + log::info!("Transitioning to stage {}, with script #{:04}", map_id, event_num); + exec_state = TextScriptExecutionState::Running(event_num, 0); + } + TSCOpCode::MOV => { + let block_size = state.tile_size.as_int() * 0x200; + + let pos_x = read_cur_varint(&mut cursor)? as i32 * block_size; + let pos_y = read_cur_varint(&mut cursor)? as i32 * block_size; + + game_scene.player1.cond.set_interacted(false); + game_scene.player2.cond.set_interacted(false); + + for player in [&mut game_scene.player1, &mut game_scene.player2].iter_mut() { + player.vel_x = 0; + player.vel_y = 0; + player.x = pos_x; + player.y = pos_y; + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::S2MV => { + let param = read_cur_varint(&mut cursor)? as usize; + + let (executor, partner) = match state.textscript_vm.executor_player { + TargetPlayer::Player1 => (&game_scene.player1, &mut game_scene.player2), + TargetPlayer::Player2 => (&game_scene.player2, &mut game_scene.player1), + }; + + match param { + 0 | 1 => { + partner.vel_x = 0; + partner.vel_y = 0; + partner.x = executor.x + if param == 0 { -0x2000 } else { 0x2000 }; + partner.y = executor.y; + } + 2..=10 => { + log::warn!("<2MV unknown param"); + } + // what the fuck + i => { + let distance = i as i32 / 10; + + partner.vel_x = 0; + partner.vel_y = 0; + partner.x = executor.x + if (param % 10) == 1 { distance * 0x200 } else { -distance * 0x200 }; + partner.y = executor.y; + } + } + + if partner.cond.alive() && !partner.cond.hidden() { + let mut npc = NPC::create(4, &state.npc_table); + npc.cond.set_alive(true); + npc.x = partner.x; + npc.y = partner.y; + + let _ = game_scene.npc_list.spawn(0x100, npc.clone()); + let _ = game_scene.npc_list.spawn(0x100, npc.clone()); + let _ = game_scene.npc_list.spawn(0x100, npc.clone()); + let _ = game_scene.npc_list.spawn(0x100, npc); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::UNI => { + let control_mode = read_cur_varint(&mut cursor)? as u8; + + let mode: Option = FromPrimitive::from_u8(control_mode); + if let Some(mode) = mode { + game_scene.player1.control_mode = mode; + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FAI => { + let fade_type = read_cur_varint(&mut cursor)? as usize; + + if let Some(direction) = FadeDirection::from_int(fade_type) { + state.fade_state = FadeState::FadeIn(15, direction); + } + + exec_state = TextScriptExecutionState::WaitFade(event, cursor.position() as u32); + } + TSCOpCode::FAO => { + let fade_type = read_cur_varint(&mut cursor)? as usize; + + if let Some(direction) = FadeDirection::from_int(fade_type) { + state.fade_state = FadeState::FadeOut(-15, direction.opposite()); + } + + exec_state = TextScriptExecutionState::WaitFade(event, cursor.position() as u32); + } + TSCOpCode::QUA => { + let count = read_cur_varint(&mut cursor)? as u16; + + state.quake_counter = count; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MNA => { + game_scene.display_map_name(160); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CMU => { + let song_id = read_cur_varint(&mut cursor)? as usize; + state.sound_manager.play_song(song_id, &state.constants, &state.settings, ctx)?; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FMU => { + state.sound_manager.play_song(0, &state.constants, &state.settings, ctx)?; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::RMU => { + state.sound_manager.restore_state()?; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SOU => { + let sound = read_cur_varint(&mut cursor)? as u8; + + state.sound_manager.play_sfx(sound); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::DNP => { + let event_num = read_cur_varint(&mut cursor)? as u16; + + game_scene.npc_list.remove_by_event(event_num, state); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::DNA => { + let npc_remove_type = read_cur_varint(&mut cursor)? as u16; + + game_scene.npc_list.remove_by_type(npc_remove_type, state); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FOB => { + let part_id = read_cur_varint(&mut cursor)? as u16; + let ticks = read_cur_varint(&mut cursor)? as i32; + + game_scene.frame.wait = ticks; + game_scene.frame.update_target = UpdateTarget::Boss(part_id); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FOM => { + let ticks = read_cur_varint(&mut cursor)? as i32; + game_scene.frame.wait = ticks; + game_scene.frame.update_target = UpdateTarget::Player; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FON => { + let event_num = read_cur_varint(&mut cursor)? as u16; + let ticks = read_cur_varint(&mut cursor)? as i32; + game_scene.frame.wait = ticks; + + for npc in game_scene.npc_list.iter() { + if event_num == npc.event_num { + game_scene.frame.update_target = UpdateTarget::NPC(npc.id); + break; + } + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::BSL => { + let event_num = read_cur_varint(&mut cursor)? as u16; + + if event_num == 0 { + game_scene.boss_life_bar.set_boss_target(&game_scene.boss); + } else { + for npc in game_scene.npc_list.iter_alive() { + if event_num == npc.event_num { + game_scene.boss_life_bar.set_npc_target(npc.id, &game_scene.npc_list); + break; + } + } + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::BOA => { + let action_num = read_cur_varint(&mut cursor)? as u16; + + game_scene.boss.parts[0].action_num = action_num; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::ANP => { + let event_num = read_cur_varint(&mut cursor)? as u16; + let action_num = read_cur_varint(&mut cursor)? as u16; + let tsc_direction = read_cur_varint(&mut cursor)? as usize; + let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + + for npc in game_scene.npc_list.iter_alive() { + if npc.event_num == event_num { + npc.action_num = action_num; + npc.tsc_direction = tsc_direction as u16; + + if direction == Direction::FacingPlayer { + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; + } else { + npc.direction = direction; + } + } + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CNP | TSCOpCode::INP => { + let event_num = read_cur_varint(&mut cursor)? as u16; + let new_type = read_cur_varint(&mut cursor)? as u16; + let tsc_direction = read_cur_varint(&mut cursor)? as usize; + let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + + for npc in game_scene.npc_list.iter_alive() { + if npc.event_num == event_num { + npc.npc_flags.set_solid_soft(false); + npc.npc_flags.set_ignore_tile_44(false); + npc.npc_flags.set_invulnerable(false); + npc.npc_flags.set_ignore_solidity(false); + npc.npc_flags.set_bouncy(false); + npc.npc_flags.set_shootable(false); + npc.npc_flags.set_solid_hard(false); + npc.npc_flags.set_rear_and_top_not_hurt(false); + npc.npc_flags.set_show_damage(false); + + if op == TSCOpCode::INP { + npc.npc_flags.set_event_when_touched(true); } - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::CNP | OpCode::INP => { - let event_num = read_cur_varint(&mut cursor)? as u16; - let new_type = read_cur_varint(&mut cursor)? as u16; - let tsc_direction = read_cur_varint(&mut cursor)? as usize; - let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + npc.npc_type = new_type; + npc.display_bounds = state.npc_table.get_display_bounds(new_type); + npc.hit_bounds = state.npc_table.get_hit_bounds(new_type); + let entry = state.npc_table.get_entry(new_type).unwrap().to_owned(); + npc.npc_flags.0 |= entry.npc_flags.0; + npc.life = entry.life; + npc.size = entry.size; + npc.exp = entry.experience as u16; + npc.damage = entry.damage as u16; + npc.spritesheet_id = entry.spritesheet_id as u16; - for npc in game_scene.npc_list.iter_alive() { - if npc.event_num == event_num { - npc.npc_flags.set_solid_soft(false); - npc.npc_flags.set_ignore_tile_44(false); - npc.npc_flags.set_invulnerable(false); - npc.npc_flags.set_ignore_solidity(false); - npc.npc_flags.set_bouncy(false); - npc.npc_flags.set_shootable(false); - npc.npc_flags.set_solid_hard(false); - npc.npc_flags.set_rear_and_top_not_hurt(false); - npc.npc_flags.set_show_damage(false); - - if op == OpCode::INP { - npc.npc_flags.set_event_when_touched(true); - } - - npc.npc_type = new_type; - npc.display_bounds = state.npc_table.get_display_bounds(new_type); - npc.hit_bounds = state.npc_table.get_hit_bounds(new_type); - let entry = state.npc_table.get_entry(new_type).unwrap().to_owned(); - npc.npc_flags.0 |= entry.npc_flags.0; - npc.life = entry.life; - npc.size = entry.size; - npc.exp = entry.experience as u16; - npc.damage = entry.damage as u16; - npc.spritesheet_id = entry.spritesheet_id as u16; - - npc.cond.set_alive(true); - npc.action_num = 0; - npc.action_counter = 0; - npc.anim_num = 0; - npc.anim_counter = 0; - npc.vel_x = 0; - npc.vel_y = 0; - npc.tsc_direction = tsc_direction as u16; - - if direction == Direction::FacingPlayer { - npc.direction = - if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; - } else { - npc.direction = direction; - } - - npc.tick( - state, - ( - [&mut game_scene.player1, &mut game_scene.player2], - &game_scene.npc_list, - &mut game_scene.stage, - &mut game_scene.bullet_manager, - &mut game_scene.flash, - ), - )?; - } - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::MNP => { - let event_num = read_cur_varint(&mut cursor)? as u16; - let x = read_cur_varint(&mut cursor)? as i32; - let y = read_cur_varint(&mut cursor)? as i32; - let tsc_direction = read_cur_varint(&mut cursor)? as usize; - let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); - let block_size = state.tile_size.as_int() * 0x200; - - for npc in game_scene.npc_list.iter_alive() { - if npc.event_num == event_num { - npc.x = x * block_size; - npc.y = y * block_size; - npc.tsc_direction = tsc_direction as u16; - - if direction == Direction::FacingPlayer { - npc.direction = - if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; - } else { - npc.direction = direction; - } - - break; - } - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SNP => { - let npc_type = read_cur_varint(&mut cursor)? as u16; - let x = read_cur_varint(&mut cursor)? as i32; - let y = read_cur_varint(&mut cursor)? as i32; - let tsc_direction = read_cur_varint(&mut cursor)? as usize; - let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); - let block_size = state.tile_size.as_int() * 0x200; - - let mut npc = NPC::create(npc_type, &state.npc_table); npc.cond.set_alive(true); + npc.action_num = 0; + npc.action_counter = 0; + npc.anim_num = 0; + npc.anim_counter = 0; + npc.vel_x = 0; + npc.vel_y = 0; + npc.tsc_direction = tsc_direction as u16; + + if direction == Direction::FacingPlayer { + npc.direction = + if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; + } else { + npc.direction = direction; + } + + npc.tick( + state, + ( + [&mut game_scene.player1, &mut game_scene.player2], + &game_scene.npc_list, + &mut game_scene.stage, + &mut game_scene.bullet_manager, + &mut game_scene.flash, + ), + )?; + } + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::MNP => { + let event_num = read_cur_varint(&mut cursor)? as u16; + let x = read_cur_varint(&mut cursor)? as i32; + let y = read_cur_varint(&mut cursor)? as i32; + let tsc_direction = read_cur_varint(&mut cursor)? as usize; + let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + let block_size = state.tile_size.as_int() * 0x200; + + for npc in game_scene.npc_list.iter_alive() { + if npc.event_num == event_num { npc.x = x * block_size; npc.y = y * block_size; npc.tsc_direction = tsc_direction as u16; @@ -1320,183 +1340,209 @@ impl TextScriptVM { npc.direction = direction; } - let _ = game_scene.npc_list.spawn(0x100, npc); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::LIp => { - let life = read_cur_varint(&mut cursor)? as u16; - - game_scene.player1.life = clamp(game_scene.player1.life + life, 0, game_scene.player1.max_life); - game_scene.player2.life = clamp(game_scene.player2.life + life, 0, game_scene.player2.max_life); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::ITp => { - let item_id = read_cur_varint(&mut cursor)? as u16; - - state.sound_manager.play_sfx(38); - - if !game_scene.inventory_player1.has_item(item_id) { - game_scene.inventory_player1.add_item(item_id); - } - - if !game_scene.inventory_player2.has_item(item_id) { - game_scene.inventory_player2.add_item(item_id); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::IpN => { - let item_id = read_cur_varint(&mut cursor)? as u16; - let amount = read_cur_varint(&mut cursor)? as u16; - - if game_scene.inventory_player1.has_item_amount(item_id, Ordering::Less, amount) { - game_scene.inventory_player1.add_item(item_id); - } - - if game_scene.inventory_player2.has_item_amount(item_id, Ordering::Less, amount) { - game_scene.inventory_player2.add_item(item_id); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::ITm => { - let item_id = read_cur_varint(&mut cursor)? as u16; - - game_scene.inventory_player1.consume_item(item_id); - game_scene.inventory_player2.consume_item(item_id); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::AMp => { - let weapon_id = read_cur_varint(&mut cursor)? as u8; - let max_ammo = read_cur_varint(&mut cursor)? as u16; - let weapon_type: Option = FromPrimitive::from_u8(weapon_id); - - state.textscript_vm.numbers[0] = max_ammo; - - if let Some(wtype) = weapon_type { - game_scene.inventory_player1.add_weapon(wtype, max_ammo); - game_scene.inventory_player2.add_weapon(wtype, max_ammo); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::AMm => { - let weapon_id = read_cur_varint(&mut cursor)? as u8; - let weapon_type: Option = FromPrimitive::from_u8(weapon_id); - - if let Some(wtype) = weapon_type { - game_scene.inventory_player1.remove_weapon(wtype); - game_scene.inventory_player2.remove_weapon(wtype); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::AEp => { - game_scene.inventory_player1.refill_all_ammo(); - game_scene.inventory_player2.refill_all_ammo(); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::TAM => { - let old_weapon_id = read_cur_varint(&mut cursor)? as u8; - let new_weapon_id = read_cur_varint(&mut cursor)? as u8; - let max_ammo = read_cur_varint(&mut cursor)? as u16; - let old_weapon_type: Option = FromPrimitive::from_u8(old_weapon_id); - let new_weapon_type: Option = FromPrimitive::from_u8(new_weapon_id); - - if let Some(wtype) = new_weapon_type { - game_scene.inventory_player1.trade_weapon(old_weapon_type, wtype, max_ammo); - game_scene.inventory_player2.trade_weapon(old_weapon_type, wtype, max_ammo); - } - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::ZAM => { - game_scene.inventory_player1.reset_all_weapon_xp(); - game_scene.inventory_player2.reset_all_weapon_xp(); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::EQp => { - let mask = read_cur_varint(&mut cursor)? as u16; - - game_scene.player1.equip.0 |= mask; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::EQm => { - let mask = read_cur_varint(&mut cursor)? as u16; - - game_scene.player1.equip.0 &= !mask; - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::FLA => { - game_scene.flash.set_blink(); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::INI => { - exec_state = TextScriptExecutionState::Reset; - } - OpCode::ESC => { - state.next_scene = Some(Box::new(TitleScene::new())); - state.control_flags.set_tick_world(false); - state.control_flags.set_control_enabled(false); - state.control_flags.set_interactions_disabled(true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - OpCode::SVP => { - exec_state = TextScriptExecutionState::SaveProfile(event, cursor.position() as u32); - } - OpCode::LDP => { - state.control_flags.set_tick_world(false); - state.control_flags.set_control_enabled(false); - state.control_flags.set_interactions_disabled(true); - - exec_state = TextScriptExecutionState::LoadProfile; - } - OpCode::MPp => { - let stage_id = read_cur_varint(&mut cursor)? as u16; - - state.set_map_flag(stage_id as usize, true); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - // unimplemented opcodes - // Zero operands - OpCode::CIL - | OpCode::CPS - | OpCode::KE2 - | OpCode::CRE - | OpCode::CSS - | OpCode::MLP - | OpCode::SPS - | OpCode::FR2 - | OpCode::STC - | OpCode::HM2 => { - log::warn!("unimplemented opcode: {:?}", op); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); - } - // One operand codes - OpCode::UNJ | OpCode::XX1 | OpCode::SIL | OpCode::SSS | OpCode::ACH => { - let par_a = read_cur_varint(&mut cursor)?; - - log::warn!("unimplemented opcode: {:?} {}", op, par_a); - - exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + break; } } - } else { - exec_state = TextScriptExecutionState::Ended; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SNP => { + let npc_type = read_cur_varint(&mut cursor)? as u16; + let x = read_cur_varint(&mut cursor)? as i32; + let y = read_cur_varint(&mut cursor)? as i32; + let tsc_direction = read_cur_varint(&mut cursor)? as usize; + let direction = Direction::from_int_facing(tsc_direction).unwrap_or(Direction::Left); + let block_size = state.tile_size.as_int() * 0x200; + + let mut npc = NPC::create(npc_type, &state.npc_table); + npc.cond.set_alive(true); + npc.x = x * block_size; + npc.y = y * block_size; + npc.tsc_direction = tsc_direction as u16; + + if direction == Direction::FacingPlayer { + npc.direction = if game_scene.player1.x < npc.x { Direction::Right } else { Direction::Left }; + } else { + npc.direction = direction; + } + + let _ = game_scene.npc_list.spawn(0x100, npc); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::LIp => { + let life = read_cur_varint(&mut cursor)? as u16; + + game_scene.player1.life = clamp(game_scene.player1.life + life, 0, game_scene.player1.max_life); + game_scene.player2.life = clamp(game_scene.player2.life + life, 0, game_scene.player2.max_life); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::ITp => { + let item_id = read_cur_varint(&mut cursor)? as u16; + + state.sound_manager.play_sfx(38); + + if !game_scene.inventory_player1.has_item(item_id) { + game_scene.inventory_player1.add_item(item_id); + } + + if !game_scene.inventory_player2.has_item(item_id) { + game_scene.inventory_player2.add_item(item_id); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::IpN => { + let item_id = read_cur_varint(&mut cursor)? as u16; + let amount = read_cur_varint(&mut cursor)? as u16; + + if game_scene.inventory_player1.has_item_amount(item_id, Ordering::Less, amount) { + game_scene.inventory_player1.add_item(item_id); + } + + if game_scene.inventory_player2.has_item_amount(item_id, Ordering::Less, amount) { + game_scene.inventory_player2.add_item(item_id); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::ITm => { + let item_id = read_cur_varint(&mut cursor)? as u16; + + game_scene.inventory_player1.consume_item(item_id); + game_scene.inventory_player2.consume_item(item_id); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::AMp => { + let weapon_id = read_cur_varint(&mut cursor)? as u8; + let max_ammo = read_cur_varint(&mut cursor)? as u16; + let weapon_type: Option = FromPrimitive::from_u8(weapon_id); + + state.textscript_vm.numbers[0] = max_ammo; + + if let Some(wtype) = weapon_type { + game_scene.inventory_player1.add_weapon(wtype, max_ammo); + game_scene.inventory_player2.add_weapon(wtype, max_ammo); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::AMm => { + let weapon_id = read_cur_varint(&mut cursor)? as u8; + let weapon_type: Option = FromPrimitive::from_u8(weapon_id); + + if let Some(wtype) = weapon_type { + game_scene.inventory_player1.remove_weapon(wtype); + game_scene.inventory_player2.remove_weapon(wtype); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::AEp => { + game_scene.inventory_player1.refill_all_ammo(); + game_scene.inventory_player2.refill_all_ammo(); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::TAM => { + let old_weapon_id = read_cur_varint(&mut cursor)? as u8; + let new_weapon_id = read_cur_varint(&mut cursor)? as u8; + let max_ammo = read_cur_varint(&mut cursor)? as u16; + let old_weapon_type: Option = FromPrimitive::from_u8(old_weapon_id); + let new_weapon_type: Option = FromPrimitive::from_u8(new_weapon_id); + + if let Some(wtype) = new_weapon_type { + game_scene.inventory_player1.trade_weapon(old_weapon_type, wtype, max_ammo); + game_scene.inventory_player2.trade_weapon(old_weapon_type, wtype, max_ammo); + } + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::ZAM => { + game_scene.inventory_player1.reset_all_weapon_xp(); + game_scene.inventory_player2.reset_all_weapon_xp(); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::EQp => { + let mask = read_cur_varint(&mut cursor)? as u16; + + game_scene.player1.equip.0 |= mask; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::EQm => { + let mask = read_cur_varint(&mut cursor)? as u16; + + game_scene.player1.equip.0 &= !mask; + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::FLA => { + game_scene.flash.set_blink(); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::INI => { + exec_state = TextScriptExecutionState::Reset; + } + TSCOpCode::ESC => { + state.next_scene = Some(Box::new(TitleScene::new())); + state.control_flags.set_tick_world(false); + state.control_flags.set_control_enabled(false); + state.control_flags.set_interactions_disabled(true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::SVP => { + exec_state = TextScriptExecutionState::SaveProfile(event, cursor.position() as u32); + } + TSCOpCode::LDP => { + state.control_flags.set_tick_world(false); + state.control_flags.set_control_enabled(false); + state.control_flags.set_interactions_disabled(true); + + exec_state = TextScriptExecutionState::LoadProfile; + } + TSCOpCode::MPp => { + let stage_id = read_cur_varint(&mut cursor)? as u16; + + state.set_map_flag(stage_id as usize, true); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + TSCOpCode::CRE => { + state.textscript_vm.flags.set_cutscene_skip(false); + state.control_flags.set_credits_running(true); + state.creditscript_vm.start(); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + // unimplemented opcodes + // Zero operands + TSCOpCode::CIL + | TSCOpCode::CPS + | TSCOpCode::KE2 + | TSCOpCode::CSS + | TSCOpCode::MLP + | TSCOpCode::SPS + | TSCOpCode::FR2 + | TSCOpCode::STC + | TSCOpCode::HM2 => { + log::warn!("unimplemented opcode: {:?}", op); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); + } + // One operand codes + TSCOpCode::UNJ | TSCOpCode::XX1 | TSCOpCode::SIL | TSCOpCode::SSS | TSCOpCode::ACH => { + let par_a = read_cur_varint(&mut cursor)?; + + log::warn!("unimplemented opcode: {:?} {}", op, par_a); + + exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } - } else { - return Ok(TextScriptExecutionState::Ended); } Ok(exec_state) @@ -1530,17 +1576,7 @@ impl TextScript { data.read_to_end(&mut buf)?; if constants.textscript.encrypted { - let half = buf.len() / 2; - let key = if let Some(0) = buf.get(half) { 0xf9 } else { (-(*buf.get(half).unwrap() as isize)) as u8 }; - log::info!("Decrypting TSC using key {:#x}", key); - - for (idx, byte) in buf.iter_mut().enumerate() { - if idx == half { - continue; - } - - *byte = byte.wrapping_add(key); - } + decrypt_tsc(&mut buf); } TextScript::compile(&buf, false, constants.textscript.encoding) diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 7b6150e..2c76ea1 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -25,6 +25,7 @@ use crate::scene::title_scene::TitleScene; use crate::scene::Scene; #[cfg(feature = "scripting-lua")] use crate::scripting::lua::LuaScriptingState; +use crate::scripting::tsc::credit_script::CreditScriptVM; use crate::settings::Settings; use crate::sound::SoundManager; use crate::stage::StageData; @@ -137,6 +138,7 @@ pub struct SharedGameState { pub preferred_viewport_size: (f32, f32), pub next_scene: Option>, pub textscript_vm: TextScriptVM, + pub creditscript_vm: CreditScriptVM, pub lightmap_canvas: Option>, pub season: Season, pub constants: EngineConstants, @@ -237,6 +239,7 @@ impl SharedGameState { preferred_viewport_size: (320.0, 240.0), next_scene: None, textscript_vm: TextScriptVM::new(), + creditscript_vm: CreditScriptVM::new(), lightmap_canvas: None, season, constants,