1
0
Fork 0
mirror of https://github.com/doukutsu-rs/doukutsu-rs synced 2025-03-24 19:09:22 +00:00

Initial challenge replay support

This commit is contained in:
dawnDus 2022-03-04 18:37:25 -05:00
parent 0387a450ce
commit 15010e54c2
No known key found for this signature in database
GPG key ID: 972AABDE81848F21
10 changed files with 447 additions and 16 deletions

View file

@ -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();
}
}

View file

@ -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;

View file

@ -25,7 +25,7 @@ impl NikumaruCounter {
}
fn load_time(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<u32> {
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<bool> {
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)
}
}

165
src/components/replay.rs Normal file
View file

@ -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<u16>,
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::<LE>(0)?; // Space for versioning replay files
file.write_u64::<LE>(self.rng_seed)?;
for input in &self.keylist {
file.write_u16::<LE>(*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::<LE>()?;
self.rng_seed = file.read_u64::<LE>()?;
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::<LE>()?);
}
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(())
}
}

View file

@ -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;

View file

@ -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
}
}
}

View file

@ -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<RefCell<StageTexturePaths>>,
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)?;

View file

@ -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;
}
_ => (),

View file

@ -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);
}

View file

@ -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),