diff --git a/src/components/inventory.rs b/src/components/inventory.rs index 777cff7..4139330 100644 --- a/src/components/inventory.rs +++ b/src/components/inventory.rs @@ -7,8 +7,8 @@ use crate::framework::error::GameResult; use crate::input::touch_controls::TouchControlType; use crate::inventory::Inventory; use crate::player::Player; -use crate::shared_game_state::SharedGameState; use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState}; +use crate::shared_game_state::SharedGameState; use crate::weapon::{WeaponLevel, WeaponType}; #[derive(Copy, Clone, PartialEq, Eq)] @@ -62,13 +62,12 @@ impl InventoryUI { inventory.get_item_idx(self.selected_item as usize).map(|i| i.0 + 6000).unwrap_or(6000) } - fn exit(&mut self, state: &mut SharedGameState, player: &mut Player, inventory: &mut Inventory) { + fn exit(&mut self, state: &mut SharedGameState, _player: &mut Player, inventory: &mut Inventory) { self.focus = InventoryFocus::None; inventory.current_item = 0; self.text_y_pos = 16; state.textscript_vm.reset(); state.textscript_vm.set_mode(ScriptMode::Map); - player.controller.update_trigger(); } } diff --git a/src/components/mod.rs b/src/components/mod.rs index 3beef8c..ad90e6c 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -10,6 +10,7 @@ pub mod inventory; pub mod map_system; pub mod nikumaru; pub mod number_popup; +pub mod replay; pub mod stage_select; pub mod text_boxes; pub mod tilemap; diff --git a/src/components/nikumaru.rs b/src/components/nikumaru.rs index 121a77a..17fe18a 100644 --- a/src/components/nikumaru.rs +++ b/src/components/nikumaru.rs @@ -25,7 +25,7 @@ impl NikumaruCounter { } fn load_time(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - if let Ok(mut data) = filesystem::user_open(ctx, state.get_290_filename()) { + if let Ok(mut data) = filesystem::user_open(ctx, [state.get_rec_filename(), ".rec".to_string()].join("")) { let mut ticks: [u32; 4] = [0; 4]; for iter in 0..=3 { @@ -54,9 +54,11 @@ impl NikumaruCounter { } fn save_time(&mut self, new_time: u32, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { - if let Ok(mut data) = - filesystem::open_options(ctx, state.get_290_filename(), OpenOptions::new().write(true).create(true)) - { + if let Ok(mut data) = filesystem::open_options( + ctx, + [state.get_rec_filename(), ".rec".to_string()].join(""), + OpenOptions::new().write(true).create(true), + ) { let mut ticks: [u32; 4] = [new_time; 4]; let mut random_list: [u8; 4] = [0; 4]; @@ -90,12 +92,13 @@ impl NikumaruCounter { Ok(()) } - pub fn save_counter(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + pub fn save_counter(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { let old_record = self.load_time(state, ctx)? as usize; if self.tick < old_record || old_record == 0 { self.save_time(self.tick as u32, state, ctx)?; + return Ok(true); } - Ok(()) + Ok(false) } } diff --git a/src/components/replay.rs b/src/components/replay.rs new file mode 100644 index 0000000..4e61235 --- /dev/null +++ b/src/components/replay.rs @@ -0,0 +1,165 @@ +use std::io::{Cursor, Read}; + +use byteorder::{ReadBytesExt, WriteBytesExt, LE}; + +use crate::entity::GameEntity; +use crate::frame::Frame; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::framework::filesystem; +use crate::framework::keyboard::ScanCode; +use crate::framework::vfs::OpenOptions; +use crate::input::replay_player_controller::{KeyState, ReplayController}; +use crate::player::Player; +use crate::shared_game_state::{ReplayState, SharedGameState}; + +#[derive(Clone)] +pub struct Replay { + replay_version: u16, + keylist: Vec, + last_input: KeyState, + rng_seed: u64, + pub controller: ReplayController, + tick: usize, + resume_tick: usize, +} + +impl Replay { + pub fn new() -> Replay { + Replay { + replay_version: 0, + keylist: Vec::new(), + last_input: KeyState(0), + rng_seed: 0, + controller: ReplayController::new(), + tick: 0, + resume_tick: 0, + } + } + + pub fn start_recording(&mut self, state: &mut SharedGameState) { + self.rng_seed = state.game_rng.dump_state(); + } + + pub fn stop_recording(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + state.replay_state = ReplayState::None; + self.write_replay(state, ctx)?; + Ok(()) + } + + pub fn start_playback(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + state.replay_state = ReplayState::Playback; + self.read_replay(state, ctx)?; + state.game_rng.load_state(self.rng_seed); + Ok(()) + } + + fn write_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if let Ok(mut file) = filesystem::open_options( + ctx, + [state.get_rec_filename(), ".rep".to_string()].join(""), + OpenOptions::new().write(true).create(true), + ) { + file.write_u16::(0)?; // Space for versioning replay files + file.write_u64::(self.rng_seed)?; + for input in &self.keylist { + file.write_u16::(*input)?; + } + } + Ok(()) + } + + fn read_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if let Ok(mut file) = filesystem::user_open(ctx, [state.get_rec_filename(), ".rep".to_string()].join("")) { + self.replay_version = file.read_u16::()?; + self.rng_seed = file.read_u64::()?; + + let mut data = Vec::new(); + file.read_to_end(&mut data)?; + + let count = data.len() / 2; + let mut inputs = Vec::new(); + let mut f = Cursor::new(data); + + for _ in 0..count { + inputs.push(f.read_u16::()?); + } + + self.keylist = inputs; + } + Ok(()) + } +} + +impl GameEntity<(&mut Context, &mut Player)> for Replay { + fn tick(&mut self, state: &mut SharedGameState, (ctx, player): (&mut Context, &mut Player)) -> GameResult { + match state.replay_state { + ReplayState::Recording => { + // This mimics the KeyState bitfield + let inputs = player.controller.move_left() as u16 + + ((player.controller.move_right() as u16) << 1) + + ((player.controller.move_up() as u16) << 2) + + ((player.controller.move_down() as u16) << 3) + + ((player.controller.trigger_map() as u16) << 4) + + ((player.controller.trigger_inventory() as u16) << 5) + + (((player.controller.jump() || player.controller.trigger_menu_ok()) as u16) << 6) + + (((player.controller.shoot() || player.controller.trigger_menu_back()) as u16) << 7) + + ((player.controller.next_weapon() as u16) << 8) + + ((player.controller.prev_weapon() as u16) << 9) + + ((player.controller.trigger_menu_ok() as u16) << 11) + + ((player.controller.skip() as u16) << 12) + + ((player.controller.strafe() as u16) << 13); + + self.keylist.push(inputs); + } + ReplayState::Playback => { + let pause = ctx.keyboard_context.is_key_pressed(ScanCode::Escape) && (self.tick - self.resume_tick > 3); + + let next_input = if pause { 1 << 10 } else { *self.keylist.get(self.tick).unwrap_or(&0) }; + + self.controller.state = KeyState(next_input); + self.controller.old_state = self.last_input; + player.controller = Box::new(self.controller); + + if !pause { + self.last_input = KeyState(next_input); + self.tick += 1; + } else { + self.resume_tick = self.tick; + }; + + if self.tick >= self.keylist.len() { + state.replay_state = ReplayState::None; + player.controller = state.settings.create_player1_controller(); + } + } + ReplayState::None => {} + } + + Ok(()) + } + + fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult { + let x = state.canvas_size.0 - 32.0; + let y = 8.0 + if state.settings.fps_counter { 12.0 } else { 0.0 }; + + match state.replay_state { + ReplayState::None => {} + ReplayState::Playback => { + state.font.draw_text_with_shadow( + "PLAY".chars(), + x, + y, + &state.constants, + &mut state.texture_set, + ctx, + )?; + } + ReplayState::Recording => { + state.font.draw_text_with_shadow("REC".chars(), x, y, &state.constants, &mut state.texture_set, ctx)?; + } + } + + Ok(()) + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index bf3d535..e0e62e3 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2,5 +2,6 @@ pub mod combined_menu_controller; pub mod dummy_player_controller; pub mod keyboard_player_controller; pub mod player_controller; +pub mod replay_player_controller; pub mod touch_controls; pub mod touch_player_controller; diff --git a/src/input/replay_player_controller.rs b/src/input/replay_player_controller.rs new file mode 100644 index 0000000..bfaa760 --- /dev/null +++ b/src/input/replay_player_controller.rs @@ -0,0 +1,207 @@ +use crate::bitfield; +use crate::framework::context::Context; +use crate::framework::error::GameResult; +use crate::input::player_controller::PlayerController; +use crate::shared_game_state::SharedGameState; + +bitfield! { + #[allow(unused)] + #[derive(Clone, Copy)] + pub struct KeyState(u16); + impl Debug; + + pub left, set_left: 0; + pub right, set_right: 1; + pub up, set_up: 2; + pub down, set_down: 3; + pub map, set_map: 4; + pub inventory, set_inventory: 5; + pub jump, set_jump: 6; + pub shoot, set_shoot: 7; + pub next_weapon, set_next_weapon: 8; + pub prev_weapon, set_prev_weapon: 9; + pub escape, set_escape: 10; + pub enter, set_enter: 11; + pub skip, set_skip: 12; + pub strafe, set_strafe: 13; +} + +#[derive(Copy, Clone)] +pub struct ReplayController { + //target: TargetPlayer, + pub state: KeyState, + pub old_state: KeyState, + trigger: KeyState, +} + +impl ReplayController { + pub fn new() -> ReplayController { + ReplayController { + //target, + state: KeyState(0), + old_state: KeyState(0), + trigger: KeyState(0), + } + } +} + +impl PlayerController for ReplayController { + fn update(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult { + Ok(()) + } + + fn update_trigger(&mut self) { + let mut trigger = self.state.0 ^ self.old_state.0; + trigger &= self.state.0; + self.old_state = self.state; + self.trigger = KeyState(trigger); + } + + fn move_up(&self) -> bool { + self.state.up() + } + + fn move_left(&self) -> bool { + self.state.left() + } + + fn move_down(&self) -> bool { + self.state.down() + } + + fn move_right(&self) -> bool { + self.state.right() + } + + fn prev_weapon(&self) -> bool { + self.state.prev_weapon() + } + + fn next_weapon(&self) -> bool { + self.state.next_weapon() + } + + fn map(&self) -> bool { + self.state.map() + } + + fn inventory(&self) -> bool { + self.state.inventory() + } + + fn jump(&self) -> bool { + self.state.jump() + } + + fn shoot(&self) -> bool { + self.state.shoot() + } + + fn skip(&self) -> bool { + self.state.skip() + } + + fn strafe(&self) -> bool { + self.state.strafe() + } + + fn trigger_up(&self) -> bool { + self.trigger.up() + } + + fn trigger_left(&self) -> bool { + self.trigger.left() + } + + fn trigger_down(&self) -> bool { + self.trigger.down() + } + + fn trigger_right(&self) -> bool { + self.trigger.right() + } + + fn trigger_prev_weapon(&self) -> bool { + self.trigger.prev_weapon() + } + + fn trigger_next_weapon(&self) -> bool { + self.trigger.next_weapon() + } + + fn trigger_map(&self) -> bool { + self.trigger.map() + } + + fn trigger_inventory(&self) -> bool { + self.trigger.inventory() + } + + fn trigger_jump(&self) -> bool { + self.trigger.jump() + } + + fn trigger_shoot(&self) -> bool { + self.trigger.shoot() + } + + fn trigger_skip(&self) -> bool { + self.trigger.skip() + } + + fn trigger_strafe(&self) -> bool { + self.trigger.strafe() + } + + fn trigger_menu_ok(&self) -> bool { + self.trigger.jump() || self.trigger.enter() + } + + fn trigger_menu_back(&self) -> bool { + self.trigger.shoot() || self.trigger.escape() + } + + fn trigger_menu_pause(&self) -> bool { + self.trigger.escape() + } + + fn look_up(&self) -> bool { + self.state.up() + } + + fn look_left(&self) -> bool { + self.state.left() + } + + fn look_down(&self) -> bool { + self.state.down() + } + + fn look_right(&self) -> bool { + self.state.right() + } + + fn move_analog_x(&self) -> f64 { + if self.state.left() && self.state.right() { + 0.0 + } else if self.state.left() { + -1.0 + } else if self.state.right() { + 1.0 + } else { + 0.0 + } + } + + fn move_analog_y(&self) -> f64 { + if self.state.up() && self.state.down() { + 0.0 + } else if self.state.up() { + -1.0 + } else if self.state.down() { + 1.0 + } else { + 0.0 + } + } +} diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 0341b3f..2779c5f 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -17,6 +17,7 @@ use crate::components::hud::HUD; use crate::components::inventory::InventoryUI; use crate::components::map_system::MapSystem; use crate::components::nikumaru::NikumaruCounter; +use crate::components::replay::Replay; use crate::components::stage_select::StageSelect; use crate::components::text_boxes::TextBoxes; use crate::components::tilemap::{TileLayer, Tilemap}; @@ -44,7 +45,7 @@ use crate::scene::title_scene::TitleScene; use crate::scene::Scene; use crate::scripting::tsc::credit_script::CreditScriptVM; use crate::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState, TextScriptVM}; -use crate::shared_game_state::{SharedGameState, TileSize}; +use crate::shared_game_state::{ReplayState, SharedGameState, TileSize}; use crate::stage::{BackgroundType, Stage, StageTexturePaths}; use crate::texture_set::SpriteBatch; use crate::weapon::bullet::BulletManager; @@ -83,6 +84,7 @@ pub struct GameScene { pub intro_mode: bool, pub pause_menu: PauseMenu, pub stage_textures: Rc>, + pub replay: Replay, map_name_counter: u16, skip_counter: u16, inventory_dim: f32, @@ -170,6 +172,7 @@ impl GameScene { map_name_counter: 0, skip_counter: 0, inventory_dim: 0.0, + replay: Replay::new(), }) } @@ -1621,10 +1624,22 @@ impl Scene for GameScene { self.pause_menu.init(state, ctx)?; self.whimsical_star.init(&self.player1); + if state.mod_path.is_some() && state.replay_state == ReplayState::Recording { + self.replay.start_recording(state); + } + + if state.mod_path.is_some() && state.replay_state == ReplayState::Playback { + self.replay.start_playback(state, ctx)?; + } + Ok(()) } fn tick(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult { + if !self.pause_menu.is_paused() && state.replay_state == ReplayState::Playback { + self.replay.tick(state, (ctx, &mut self.player1))?; + } + self.player1.controller.update(state, ctx)?; self.player1.controller.update_trigger(); self.player2.controller.update(state, ctx)?; @@ -1658,6 +1673,10 @@ impl Scene for GameScene { return Ok(()); } + if state.replay_state == ReplayState::Recording { + self.replay.tick(state, (ctx, &mut self.player1))?; + } + match state.textscript_vm.state { TextScriptExecutionState::Running(_, _) | TextScriptExecutionState::WaitTicks(_, _, _) @@ -1983,6 +2002,8 @@ impl Scene for GameScene { self.draw_debug_outlines(state, ctx)?; } + self.replay.draw(state, ctx, &self.frame)?; + self.pause_menu.draw(state, ctx)?; //draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?; diff --git a/src/scene/title_scene.rs b/src/scene/title_scene.rs index fe53c6c..219dbd4 100644 --- a/src/scene/title_scene.rs +++ b/src/scene/title_scene.rs @@ -13,7 +13,7 @@ use crate::menu::settings_menu::SettingsMenu; use crate::menu::{Menu, MenuEntry, MenuSelectionResult}; use crate::scene::jukebox_scene::JukeboxScene; use crate::scene::Scene; -use crate::shared_game_state::{GameDifficulty, MenuCharacter, SharedGameState, TileSize}; +use crate::shared_game_state::{GameDifficulty, MenuCharacter, ReplayState, SharedGameState, TileSize}; use crate::stage::{BackgroundType, NpcType, Stage, StageData, StageTexturePaths, Tileset}; #[derive(PartialEq, Eq, Copy, Clone)] @@ -168,6 +168,7 @@ impl Scene for TitleScene { self.confirm_menu.push_entry(MenuEntry::Disabled("".to_owned())); self.confirm_menu.push_entry(MenuEntry::Active("Start".to_owned())); + self.confirm_menu.push_entry(MenuEntry::Disabled("No Replay".to_owned())); self.confirm_menu.push_entry(MenuEntry::Active("< Back".to_owned())); self.confirm_menu.selected = 1; @@ -177,6 +178,8 @@ impl Scene for TitleScene { self.nikumaru_rec.load_counter(state, ctx)?; self.update_menu_cursor(state, ctx)?; + state.replay_state = ReplayState::None; + Ok(()) } @@ -272,6 +275,11 @@ impl Scene for TitleScene { self.confirm_menu.width = (state.font.text_width(mod_name.chars(), &state.constants).max(50.0) + 32.0) as u16; self.confirm_menu.entries[0] = MenuEntry::Disabled(mod_name); + self.confirm_menu.entries[2] = if state.has_replay_data(ctx) { + MenuEntry::Active("Replay Best".to_owned()) + } else { + MenuEntry::Disabled("No Replay".to_owned()) + }; self.nikumaru_rec.load_counter(state, ctx)?; self.current_menu = CurrentMenu::ChallengeConfirmMenu; } @@ -288,10 +296,17 @@ impl Scene for TitleScene { CurrentMenu::ChallengeConfirmMenu => match self.confirm_menu.tick(&mut self.controller, state) { MenuSelectionResult::Selected(1, _) => { state.difficulty = GameDifficulty::Normal; + state.replay_state = ReplayState::Recording; state.reload_resources(ctx)?; state.start_new_game(ctx)?; } - MenuSelectionResult::Selected(2, _) | MenuSelectionResult::Canceled => { + MenuSelectionResult::Selected(2, _) => { + state.difficulty = GameDifficulty::Normal; + state.replay_state = ReplayState::Playback; + state.reload_resources(ctx)?; + state.start_new_game(ctx)?; + } + MenuSelectionResult::Selected(3, _) | MenuSelectionResult::Canceled => { self.current_menu = CurrentMenu::ChallengesMenu; } _ => (), diff --git a/src/scripting/tsc/text_script.rs b/src/scripting/tsc/text_script.rs index c08512c..5ae5479 100644 --- a/src/scripting/tsc/text_script.rs +++ b/src/scripting/tsc/text_script.rs @@ -26,6 +26,7 @@ use crate::scene::title_scene::TitleScene; use crate::scripting::tsc::bytecode_utils::read_cur_varint; use crate::scripting::tsc::encryption::decrypt_tsc; use crate::scripting::tsc::opcodes::TSCOpCode; +use crate::shared_game_state::ReplayState; use crate::shared_game_state::SharedGameState; use crate::weapon::WeaponType; @@ -1121,6 +1122,7 @@ impl TextScriptVM { new_scene.player2.flags.set_hit_bottom_wall(false); new_scene.frame.wait = game_scene.frame.wait; new_scene.nikumaru = game_scene.nikumaru; + new_scene.replay = game_scene.replay.clone(); let skip = state.textscript_vm.flags.cutscene_skip(); state.control_flags.set_tick_world(true); @@ -1688,7 +1690,11 @@ impl TextScriptVM { ); } TSCOpCode::STC => { - game_scene.nikumaru.save_counter(state, ctx)?; + let new_record = game_scene.nikumaru.save_counter(state, ctx)?; + + if new_record && state.replay_state == ReplayState::Recording { + game_scene.replay.stop_recording(state, ctx)?; + } exec_state = TextScriptExecutionState::Running(event, cursor.position() as u32); } diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 3039ebb..43ed194 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -134,6 +134,13 @@ pub enum MenuCharacter { Sue, } +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum ReplayState { + None, + Recording, + Playback, +} + #[derive(PartialEq, Eq, Copy, Clone)] pub enum TileSize { Tile8x8, @@ -201,6 +208,7 @@ pub struct SharedGameState { pub settings: Settings, pub save_slot: usize, pub difficulty: GameDifficulty, + pub replay_state: ReplayState, pub shutdown: bool, } @@ -298,6 +306,7 @@ impl SharedGameState { settings, save_slot: 1, difficulty: GameDifficulty::Normal, + replay_state: ReplayState::None, shutdown: false, }) } @@ -591,15 +600,19 @@ impl SharedGameState { } } - pub fn get_290_filename(&self) -> String { + pub fn get_rec_filename(&self) -> String { if let Some(mod_path) = &self.mod_path { let name = self.mod_list.get_name_from_path(mod_path.to_string()); - return format!("/{}.rec", name); + return format!("/{}", name); } else { - return "/290.rec".to_string(); + return "/290".to_string(); } } + pub fn has_replay_data(&self, ctx: &mut Context) -> bool { + filesystem::user_exists(ctx, [self.get_rec_filename(), ".rep".to_string()].join("")) + } + pub fn get_damage(&self, hp: i32) -> i32 { match self.difficulty { GameDifficulty::Easy => cmp::max(hp / 2, 1),