diff --git a/README.md b/README.md index 8620847..6a07d47 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ A re-implementation of Cave Story (Doukutsu Monogatari) engine written in [Rust](https://www.rust-lang.org/), aiming for behavior accuracy and cleaner code. Later plans might involve turning it into a fully-featured modding tool with live debugging and stuff. +The engine also contains some (might be buggy and not accurate, everything was pure guess work on data files to avoid legal issues) implementation of Cave Story+ features from both PC and Switch versions. + +Note you have to ship the data files yourself if you want to play with those features, but nothing is stopping you from creating a modification of freeware files that uses those new TSC opcodes and features. I'd actually would like to see something cool created using this engine. + **The project is still incomplete and might not be playable. Expect lots of breaking changes and bugs** [Join the Discord server](https://discord.gg/fbRsNNB) @@ -15,7 +19,7 @@ Later plans might involve turning it into a fully-featured modding tool with liv This repo does not redistribute any copyrighted files. -The engine should work fine with [CSE2-Enhanced](https://github.com/Clownacy/CSE2) or [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) modified freeware data files and [Cave Story+](https://www.nicalis.com/games/cavestory+) (Nicalis commercial release, loading is supported, features are implemented in clean room way, using guess work or stuff already being inside the engine) data files. +The engine should work fine with [CSE2-Enhanced](https://github.com/Clownacy/CSE2) or [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) modified freeware data files and [Cave Story+](https://www.nicalis.com/games/cavestory+) data files. Vanilla Cave Story does not work yet because some important data files are embedded inside executable and we don't have an extractor yet. diff --git a/src/text_script.rs b/src/text_script.rs index 3b2a218..36880dd 100644 --- a/src/text_script.rs +++ b/src/text_script.rs @@ -1,3 +1,4 @@ +use std::cmp::Ordering; use std::collections::HashMap; use std::io; use std::io::Cursor; @@ -40,144 +41,242 @@ pub enum OpCode { /// internal: implicit END marker _END, - // ---- Official opcodes ---- - /// , pub flags: TextScriptFlags, /// Toggle for non-strict TSC parsing because English versions of CS+ (both AG and Nicalis release) /// modified the events carelessly and since original Pixel's engine hasn't enforced constraints @@ -303,6 +403,7 @@ impl TextScriptVM { scene_script: TextScript::new(), }, state: TextScriptExecutionState::Ended, + stack: Vec::with_capacity(6), strict_mode: false, suspend: true, flags: TextScriptFlags(0), @@ -530,6 +631,7 @@ impl TextScriptVM { state.textscript_vm.flags.set_render(false); state.textscript_vm.flags.set_background_visible(false); + state.textscript_vm.stack.clear(); game_scene.player.cond.set_interacted(false); game_scene.player.update_target = true; @@ -611,6 +713,18 @@ impl TextScriptVM { state.game_flags.set(flag_num, op == OpCode::FLp); 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 { + state.game_flags.set(flag, false); + } + } + + 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; @@ -630,6 +744,17 @@ impl TextScriptVM { 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.has_item_amount(item_id, Ordering::Equal, amount) { + 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; @@ -666,6 +791,22 @@ impl TextScriptVM { 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); + + 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.player.vel_x = 0; @@ -1003,14 +1144,26 @@ impl TextScriptVM { OpCode::ITp => { let item_id = read_cur_varint(&mut cursor)? as u16; - game_scene.inventory.add_item(item_id); + if !game_scene.inventory.has_item(item_id) { + game_scene.inventory.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.has_item_amount(item_id, Ordering::Less, amount) { + game_scene.inventory.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.remove_item(item_id); + game_scene.inventory.consume_item(item_id); exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } @@ -1106,7 +1259,7 @@ impl TextScriptVM { exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } // Three operand codes - OpCode::TAM | OpCode::INJ => { + OpCode::TAM => { let par_a = read_cur_varint(&mut cursor)?; let par_b = read_cur_varint(&mut cursor)?; let par_c = read_cur_varint(&mut cursor)?; @@ -1357,7 +1510,8 @@ impl TextScript { 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::STC | OpCode::SVP | OpCode::TUR | OpCode::WAS | OpCode::ZAM | OpCode::HM2 | + OpCode::POP => { TextScript::put_varint(instr as i32, out); } // One operand codes @@ -1367,14 +1521,15 @@ impl TextScript { 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::SSS | OpCode::ACH | OpCode::S2MV | OpCode::PSH => { let operand = TextScript::read_number(iter)?; TextScript::put_varint(instr as i32, out); TextScript::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::ITJ | OpCode::SKJ | OpCode::AMJ | OpCode::SMP | OpCode::PSp | OpCode::IpN | + OpCode::FFm => { let operand_a = TextScript::read_number(iter)?; if strict { TextScript::expect_char(b':', iter)?; } else { iter.next().ok_or_else(|| ParseError(str!("Script unexpectedly ended.")))?; } let operand_b = TextScript::read_number(iter)?; @@ -1412,9 +1567,8 @@ impl TextScript { TextScript::put_varint(operand_c as i32, out); TextScript::put_varint(operand_d as i32, out); } - _ => { - TextScript::put_varint(OpCode::_UNI as i32, out); - log::warn!("Unimplemented opcode: {:?}", instr); + OpCode::_NOP | OpCode::_UNI | OpCode::_STR | OpCode::_END => { + unreachable!() } }