add in-game debug command line

This commit is contained in:
Sallai József 2022-08-22 01:10:33 +03:00
parent b1b3b131e2
commit 4ed7ba66b8
6 changed files with 376 additions and 10 deletions

View File

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

View File

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

View File

@ -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<CommandLineCommand> {
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::<u16>();
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::<u16>();
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::<u16>();
let ammo_count = components[2].parse::<u16>();
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::<u16>();
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::<u16>();
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::<u16>();
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::<u16>();
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::<u16>();
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::<u16>();
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<WeaponType> = 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<WeaponType> = 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<CommandLineCommand>,
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<CommandLineCommand> {
let components = command.split_whitespace().collect::<Vec<&str>>();
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
}
}

View File

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

View File

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

View File

@ -314,6 +314,7 @@ pub struct SharedGameState {
pub stages: Vec<StageData>,
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),