From 4ed7ba66b8b734429cb40c638620f3f11c5d6a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sallai=20J=C3=B3zsef?= Date: Mon, 22 Aug 2022 01:10:33 +0300 Subject: [PATCH] add in-game debug command line --- src/framework/backend_sdl2.rs | 7 +- src/framework/error.rs | 13 +- src/live_debugger/command_line.rs | 293 ++++++++++++++++++ .../mod.rs} | 70 ++++- src/scene/game_scene.rs | 1 + src/shared_game_state.rs | 2 + 6 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 src/live_debugger/command_line.rs rename src/{live_debugger.rs => live_debugger/mod.rs} (87%) diff --git a/src/framework/backend_sdl2.rs b/src/framework/backend_sdl2.rs index 85dce41..025cc7b 100644 --- a/src/framework/backend_sdl2.rs +++ b/src/framework/backend_sdl2.rs @@ -8,6 +8,7 @@ use std::rc::Rc; use std::time::{Duration, Instant}; use imgui::internal::RawWrapper; +use imgui::sys::{ImGuiKey_Backspace, ImGuiKey_Delete, ImGuiKey_Enter}; use imgui::{ConfigFlags, DrawCmd, DrawData, DrawIdx, DrawVert, Key, MouseCursor, TextureId, Ui}; use sdl2::controller::GameController; use sdl2::event::{Event, WindowEvent}; @@ -424,7 +425,11 @@ impl BackendEventLoop for SDL2EventLoop { #[cfg(feature = "render-opengl")] if *self.opengl_available.borrow() { - let imgui = init_imgui()?; + let mut imgui = init_imgui()?; + let mut key_map = &mut imgui.io_mut().key_map; + key_map[ImGuiKey_Backspace as usize] = Scancode::Backspace as u32; + key_map[ImGuiKey_Delete as usize] = Scancode::Delete as u32; + key_map[ImGuiKey_Enter as usize] = Scancode::Return as u32; let refs = self.refs.clone(); diff --git a/src/framework/error.rs b/src/framework/error.rs index 3c5038b..3b3769c 100644 --- a/src/framework/error.rs +++ b/src/framework/error.rs @@ -1,11 +1,10 @@ //! Error types and conversion functions. - use std::error::Error; use std::fmt; use std::string::FromUtf8Error; -use std::sync::{Arc, PoisonError}; use std::sync::mpsc::SendError; +use std::sync::{Arc, PoisonError}; /// An enum containing all kinds of game framework errors. #[derive(Debug, Clone)] @@ -43,6 +42,8 @@ pub enum GameError { ParseError(String), /// Something went wrong while converting a value. InvalidValue(String), + /// Something went wrong while executing a debug command line command. + CommandLineError(String), } impl fmt::Display for GameError { @@ -50,11 +51,9 @@ impl fmt::Display for GameError { match *self { GameError::ConfigError(ref s) => write!(f, "Config error: {}", s), GameError::ResourceLoadError(ref s) => write!(f, "Error loading resource: {}", s), - GameError::ResourceNotFound(ref s, ref paths) => write!( - f, - "Resource not found: {}, searched in paths {:?}", - s, paths - ), + GameError::ResourceNotFound(ref s, ref paths) => { + write!(f, "Resource not found: {}, searched in paths {:?}", s, paths) + } GameError::WindowError(ref e) => write!(f, "Window creation error: {}", e), _ => write!(f, "GameError {:?}", self), } diff --git a/src/live_debugger/command_line.rs b/src/live_debugger/command_line.rs new file mode 100644 index 0000000..5eba1cd --- /dev/null +++ b/src/live_debugger/command_line.rs @@ -0,0 +1,293 @@ +use num_traits::FromPrimitive; + +use crate::framework::error::{GameError::CommandLineError, GameResult}; +use crate::scene::game_scene::GameScene; +use crate::shared_game_state::SharedGameState; +use crate::weapon::WeaponType; + +#[derive(Clone, Copy)] +pub enum CommandLineCommand { + AddItem(u16), + RemoveItem(u16), + AddWeapon(u16, u16), + RemoveWeapon(u16), + AddWeaponAmmo(u16), + SetWeaponMaxAmmo(u16), + RefillAmmo, + RefillHP, + AddXP(u16), + RemoveXP(u16), + SetMaxHP(u16), +} + +impl CommandLineCommand { + pub fn from_components(components: Vec<&str>) -> Option { + if components.len() == 0 { + return None; + } + + let command = components[0]; + + match command.replacen("/", "", 1).as_str() { + "add_item" => { + if components.len() < 2 { + return None; + } + + let item_id = components[1].parse::(); + if item_id.is_ok() { + return Some(CommandLineCommand::AddItem(item_id.unwrap())); + } + } + "remove_item" => { + if components.len() < 2 { + return None; + } + + let item_id = components[1].parse::(); + if item_id.is_ok() { + return Some(CommandLineCommand::RemoveItem(item_id.unwrap())); + } + } + "add_weapon" => { + if components.len() < 3 { + return None; + } + + let weapon_id = components[1].parse::(); + let ammo_count = components[2].parse::(); + + if weapon_id.is_ok() && ammo_count.is_ok() { + return Some(CommandLineCommand::AddWeapon(weapon_id.unwrap(), ammo_count.unwrap())); + } + } + "remove_weapon" => { + if components.len() < 2 { + return None; + } + + let weapon_id = components[1].parse::(); + if weapon_id.is_ok() { + return Some(CommandLineCommand::RemoveWeapon(weapon_id.unwrap())); + } + } + "add_weapon_ammo" => { + if components.len() < 2 { + return None; + } + + let ammo_count = components[1].parse::(); + if ammo_count.is_ok() { + return Some(CommandLineCommand::AddWeaponAmmo(ammo_count.unwrap())); + } + } + "set_weapon_max_ammo" => { + if components.len() < 2 { + return None; + } + + let max_ammo_count = components[1].parse::(); + if max_ammo_count.is_ok() { + return Some(CommandLineCommand::SetWeaponMaxAmmo(max_ammo_count.unwrap())); + } + } + "refill_ammo" => { + return Some(CommandLineCommand::RefillAmmo); + } + "refill_hp" => { + return Some(CommandLineCommand::RefillHP); + } + "add_xp" => { + if components.len() < 2 { + return None; + } + + let xp_count = components[1].parse::(); + if xp_count.is_ok() { + return Some(CommandLineCommand::AddXP(xp_count.unwrap())); + } + } + "remove_xp" => { + if components.len() < 2 { + return None; + } + + let xp_count = components[1].parse::(); + if xp_count.is_ok() { + return Some(CommandLineCommand::RemoveXP(xp_count.unwrap())); + } + } + "set_max_hp" => { + if components.len() < 2 { + return None; + } + + let hp_count = components[1].parse::(); + if hp_count.is_ok() { + return Some(CommandLineCommand::SetMaxHP(hp_count.unwrap())); + } + } + _ => return None, + } + + None + } + + pub fn execute(&mut self, game_scene: &mut GameScene, state: &mut SharedGameState) -> GameResult { + match *self { + CommandLineCommand::AddItem(item_id) => { + game_scene.inventory_player1.add_item(item_id); + } + CommandLineCommand::RemoveItem(item_id) => { + if !game_scene.inventory_player1.has_item(item_id) { + return Err(CommandLineError(format!("Player does not have item {}", item_id))); + } + + game_scene.inventory_player1.remove_item(item_id); + } + CommandLineCommand::AddWeapon(weapon_id, ammo_count) => { + let weapon_type: Option = FromPrimitive::from_u16(weapon_id); + match weapon_type { + Some(weapon_type) => game_scene.inventory_player1.add_weapon(weapon_type, ammo_count), + None => return Err(CommandLineError(format!("Invalid weapon id {}", weapon_id))), + } + } + CommandLineCommand::RemoveWeapon(weapon_id) => { + let weapon_type: Option = FromPrimitive::from_u16(weapon_id); + match weapon_type { + Some(weapon_type) => { + if !game_scene.inventory_player1.has_weapon(weapon_type) { + return Err(CommandLineError(format!("Player does not have weapon {:?}", weapon_type))); + } + + game_scene.inventory_player1.remove_weapon(weapon_type); + } + None => return Err(CommandLineError(format!("Invalid weapon id {}", weapon_id))), + }; + } + CommandLineCommand::AddWeaponAmmo(ammo_count) => { + let weapon = game_scene.inventory_player1.get_current_weapon_mut(); + match weapon { + Some(weapon) => weapon.ammo += ammo_count, + None => return Err(CommandLineError(format!("Player does not have an active weapon"))), + } + } + CommandLineCommand::SetWeaponMaxAmmo(max_ammo) => { + let weapon = game_scene.inventory_player1.get_current_weapon_mut(); + match weapon { + Some(weapon) => weapon.max_ammo = max_ammo, + None => return Err(CommandLineError(format!("Player does not have an active weapon"))), + } + } + CommandLineCommand::RefillAmmo => { + game_scene.inventory_player1.refill_all_ammo(); + } + CommandLineCommand::RefillHP => { + game_scene.player1.life = game_scene.player1.max_life; + } + CommandLineCommand::AddXP(xp_count) => { + game_scene.inventory_player1.add_xp(xp_count, &mut game_scene.player1, state); + } + CommandLineCommand::RemoveXP(xp_count) => { + game_scene.inventory_player1.take_xp(xp_count, state); + } + CommandLineCommand::SetMaxHP(hp_count) => { + game_scene.player1.max_life = hp_count; + game_scene.player1.life = hp_count; + } + } + + Ok(()) + } + + #[allow(dead_code)] + pub fn to_command(&self) -> String { + match self { + CommandLineCommand::AddItem(item_id) => format!("/add_item {}", item_id), + CommandLineCommand::RemoveItem(item_id) => format!("/remove_item {}", item_id), + CommandLineCommand::AddWeapon(weapon_id, ammo_count) => format!("/add_weapon {} {}", weapon_id, ammo_count), + CommandLineCommand::RemoveWeapon(weapon_id) => format!("/remove_weapon {}", weapon_id), + CommandLineCommand::AddWeaponAmmo(ammo_count) => format!("/add_weapon_ammo {}", ammo_count), + CommandLineCommand::SetWeaponMaxAmmo(max_ammo_count) => format!("/set_weapon_max_ammo {}", max_ammo_count), + CommandLineCommand::RefillAmmo => "/refill_ammo".to_string(), + CommandLineCommand::RefillHP => "/refill_hp".to_string(), + CommandLineCommand::AddXP(xp_count) => format!("/add_xp {}", xp_count), + CommandLineCommand::RemoveXP(xp_count) => format!("/remove_xp {}", xp_count), + CommandLineCommand::SetMaxHP(hp_count) => format!("/set_max_hp {}", hp_count), + } + } + + pub fn feedback_string(&self) -> String { + match self { + CommandLineCommand::AddItem(item_id) => format!("Added item with ID {}.", item_id), + CommandLineCommand::RemoveItem(item_id) => format!("Removed item with ID {}.", item_id), + CommandLineCommand::AddWeapon(weapon_id, ammo_count) => { + format!("Added weapon with ID {} and {} ammo.", weapon_id, ammo_count) + } + CommandLineCommand::RemoveWeapon(weapon_id) => format!("Removed weapon with ID {}.", weapon_id), + CommandLineCommand::AddWeaponAmmo(ammo_count) => format!("Added {} ammo to current weapon.", ammo_count), + CommandLineCommand::SetWeaponMaxAmmo(max_ammo_count) => { + format!("Set max ammo of current weapon to {}.", max_ammo_count) + } + CommandLineCommand::RefillAmmo => "Refilled ammo of all weapons.".to_string(), + CommandLineCommand::RefillHP => "Refilled HP of player.".to_string(), + CommandLineCommand::AddXP(xp_count) => format!("Added {} XP to current weapon.", xp_count), + CommandLineCommand::RemoveXP(xp_count) => format!("Removed {} XP from current weapon.", xp_count), + CommandLineCommand::SetMaxHP(hp_count) => format!("Set max HP of player to {}.", hp_count), + } + } +} + +pub struct CommandLineParser { + command_history: Vec, + cursor: usize, + pub last_feedback: String, + pub last_feedback_color: [f32; 4], + pub buffer: String, +} + +impl CommandLineParser { + pub fn new() -> CommandLineParser { + CommandLineParser { + command_history: Vec::new(), + last_feedback: "Awaiting command.".to_string(), + last_feedback_color: [1.0, 1.0, 1.0, 1.0], + cursor: 0, + buffer: String::new(), + } + } + + pub fn push(&mut self, command: String) -> Option { + let components = command.split_whitespace().collect::>(); + let command = CommandLineCommand::from_components(components); + + match command { + Some(command) => { + self.command_history.push(command); + self.cursor = self.command_history.len() - 1; + + Some(command) + } + None => None, + } + } + + #[allow(dead_code)] + pub fn traverse(&mut self, delta: i16) -> Option<&CommandLineCommand> { + if self.command_history.is_empty() { + return None; + } + + let command = self.command_history.get(self.cursor); + + if delta == -1 && self.cursor == 0 { + self.cursor = self.command_history.len() - 1; + } else if delta == 1 && self.cursor == self.command_history.len() - 1 { + self.cursor = 0; + } else { + self.cursor = (self.cursor as i16 + delta) as usize; + } + + command + } +} diff --git a/src/live_debugger.rs b/src/live_debugger/mod.rs similarity index 87% rename from src/live_debugger.rs rename to src/live_debugger/mod.rs index eb9b44e..4f9850e 100644 --- a/src/live_debugger.rs +++ b/src/live_debugger/mod.rs @@ -7,6 +7,10 @@ use crate::scene::game_scene::GameScene; use crate::scripting::tsc::text_script::TextScriptExecutionState; use crate::shared_game_state::SharedGameState; +use self::command_line::CommandLineParser; + +pub mod command_line; + #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[repr(u8)] pub enum ScriptType { @@ -22,6 +26,7 @@ pub struct LiveDebugger { flags_visible: bool, npc_inspector_visible: bool, hotkey_list_visible: bool, + command_line_parser: CommandLineParser, last_stage_id: usize, stages: Vec, selected_stage: i32, @@ -40,6 +45,7 @@ impl LiveDebugger { flags_visible: false, npc_inspector_visible: false, hotkey_list_visible: false, + command_line_parser: CommandLineParser::new(), last_stage_id: usize::MAX, stages: Vec::new(), selected_stage: -1, @@ -64,6 +70,60 @@ impl LiveDebugger { self.selected_event = -1; } + if state.command_line { + let width = state.screen_size.0; + let height = 85.0; + let x = 0.0 as f32; + let y = state.screen_size.1 - height; + + Window::new("Command Line") + .position([x, y], Condition::FirstUseEver) + .size([width, height], Condition::FirstUseEver) + .resizable(false) + .collapsible(false) + .movable(false) + .build(ui, || { + ui.text("Command:"); + ui.same_line(); + + ui.input_text("", &mut self.command_line_parser.buffer).build(); + + if ui.is_item_active() { + state.control_flags.set_tick_world(false); + } else { + state.control_flags.set_tick_world(true); + } + + ui.same_line(); + if ui.is_key_released(imgui::Key::Enter) || ui.button("Execute") { + log::info!("Executing command: {}", self.command_line_parser.buffer); + match self.command_line_parser.push(self.command_line_parser.buffer.clone()) { + Some(mut command) => match command.execute(game_scene, state) { + Ok(()) => { + self.command_line_parser.last_feedback = command.feedback_string(); + self.command_line_parser.last_feedback_color = [0.0, 1.0, 0.0, 1.0]; + state.sound_manager.play_sfx(5); + } + Err(e) => { + self.command_line_parser.last_feedback = e.to_string(); + self.command_line_parser.last_feedback_color = [1.0, 0.0, 0.0, 1.0]; + state.sound_manager.play_sfx(12); + } + }, + None => { + self.command_line_parser.last_feedback = "Invalid command".to_string(); + self.command_line_parser.last_feedback_color = [1.0, 0.0, 0.0, 1.0]; + state.sound_manager.play_sfx(12); + } + } + } + ui.text_colored( + self.command_line_parser.last_feedback_color, + self.command_line_parser.last_feedback.clone(), + ); + }); + } + if !state.debugger { return Ok(()); } @@ -72,7 +132,7 @@ impl LiveDebugger { .resizable(false) .collapsed(true, Condition::FirstUseEver) .position([5.0, 5.0], Condition::FirstUseEver) - .size([400.0, 235.0], Condition::FirstUseEver) + .size([400.0, 265.0], Condition::FirstUseEver) .build(ui, || { ui.text(format!( "Player position: ({:.1},{:.1}), velocity: ({:.1},{:.1})", @@ -164,6 +224,10 @@ impl LiveDebugger { self.hotkey_list_visible = !self.hotkey_list_visible; } + if ui.button("Command Line") { + state.command_line = !state.command_line; + } + ui.checkbox("noclip", &mut state.settings.noclip); ui.same_line(); ui.checkbox("more rust", &mut state.more_rust); @@ -405,7 +469,7 @@ impl LiveDebugger { if self.hotkey_list_visible { Window::new("Hotkeys") .position([400.0, 5.0], Condition::FirstUseEver) - .size([300.0, 280.0], Condition::FirstUseEver) + .size([300.0, 300.0], Condition::FirstUseEver) .resizable(false) .build(ui, || { let key = vec![ @@ -420,6 +484,7 @@ impl LiveDebugger { "F10 > Debug Overlay", "F11 > Toggle FPS Counter", "F12 > Toggle Debugger", + "` > Toggle Command Line", "Ctrl + F3 > Reload Sound Manager", "Ctrl + S > Quick Save", ]; @@ -433,6 +498,7 @@ impl LiveDebugger { } }); } + let mut remove = -1; for (idx, (_, title, contents)) in self.text_windows.iter().enumerate() { let mut opened = true; diff --git a/src/scene/game_scene.rs b/src/scene/game_scene.rs index 91c95b3..8d66cc5 100644 --- a/src/scene/game_scene.rs +++ b/src/scene/game_scene.rs @@ -2358,6 +2358,7 @@ impl Scene for GameScene { ScanCode::F10 => state.settings.debug_outlines = !state.settings.debug_outlines, ScanCode::F11 => state.settings.fps_counter = !state.settings.fps_counter, ScanCode::F12 => state.debugger = !state.debugger, + ScanCode::Grave => state.command_line = !state.command_line, _ => {} }; diff --git a/src/shared_game_state.rs b/src/shared_game_state.rs index 0444562..5601d2c 100644 --- a/src/shared_game_state.rs +++ b/src/shared_game_state.rs @@ -314,6 +314,7 @@ pub struct SharedGameState { pub stages: Vec, pub frame_time: f64, pub debugger: bool, + pub command_line: bool, pub scale: f32, pub canvas_size: (f32, f32), pub screen_size: (f32, f32), @@ -459,6 +460,7 @@ impl SharedGameState { stages: Vec::with_capacity(96), frame_time: 0.0, debugger: false, + command_line: false, scale: 2.0, screen_size: (640.0, 480.0), canvas_size: (320.0, 240.0),