mirror of
https://codeberg.org/schrottkatze/mgd2-tram-championships.git
synced 2025-07-03 10:37:39 +00:00
add debug console initial implementation
This commit is contained in:
parent
cf63dd6e41
commit
cf05050c95
13 changed files with 709 additions and 51 deletions
|
@ -1,19 +1,14 @@
|
|||
//! Seperate out debugging uis/plugins in it's own module for cleanliness.
|
||||
use bevy::prelude::*;
|
||||
use bevy_inspector_egui::{
|
||||
bevy_egui::EguiPlugin,
|
||||
quick::{StateInspectorPlugin, WorldInspectorPlugin},
|
||||
};
|
||||
|
||||
use crate::{AppState, menus};
|
||||
use bevy_inspector_egui::bevy_egui::EguiPlugin;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(EguiPlugin {
|
||||
enable_multipass_for_primary_context: true,
|
||||
})
|
||||
.add_plugins((
|
||||
WorldInspectorPlugin::new(),
|
||||
StateInspectorPlugin::<AppState>::new(),
|
||||
StateInspectorPlugin::<menus::CurrentMenu>::new(),
|
||||
// WorldInspectorPlugin::new(),
|
||||
// StateInspectorPlugin::<AppState>::new(),
|
||||
// StateInspectorPlugin::<menus::CurrentMenu>::new(),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
};
|
||||
|
||||
mod camera;
|
||||
mod debug;
|
||||
mod scene;
|
||||
mod tram {
|
||||
use bevy::prelude::*;
|
||||
|
@ -28,6 +29,6 @@ pub fn plugin(app: &mut App) {
|
|||
OnExit(AppState::Ingame),
|
||||
despawn::<cleanup::Scene>.in_set(GameplaySet),
|
||||
)
|
||||
.add_plugins(camera::plugin);
|
||||
.add_plugins((debug::plugin, camera::plugin));
|
||||
app.configure_sets(Update, GameplaySet.run_if(in_state(AppState::Ingame)));
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use bevy::prelude::*;
|
||||
use bevy_third_person_camera::*;
|
||||
|
||||
use crate::{AppState, TPCTarget};
|
||||
use crate::AppState;
|
||||
|
||||
use super::GameplaySet;
|
||||
|
||||
|
|
13
src/game/debug.rs
Normal file
13
src/game/debug.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
mod console;
|
||||
|
||||
#[derive(Event)]
|
||||
enum DebugEvent {
|
||||
CloseDebugConsole,
|
||||
PrintToConsole(String),
|
||||
}
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.add_plugins(console::plugin).add_event::<DebugEvent>();
|
||||
}
|
105
src/game/debug/console.rs
Normal file
105
src/game/debug/console.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use bevy::prelude::*;
|
||||
use ui::{
|
||||
DebugConsole, autofocus, autoscroll, handle_open_console, process_prompt, update_content,
|
||||
update_scroll_position,
|
||||
};
|
||||
|
||||
use crate::{AppState, cleanup::despawn, game::GameplaySet};
|
||||
|
||||
use super::DebugEvent;
|
||||
|
||||
mod cli;
|
||||
mod ui;
|
||||
|
||||
pub(super) fn plugin(app: &mut App) {
|
||||
app.init_state::<ConsoleState>()
|
||||
.add_systems(
|
||||
OnEnter(ConsoleState::Open),
|
||||
(open_console, (autofocus, autoscroll).after(open_console)),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
handle_open_console
|
||||
.in_set(GameplaySet)
|
||||
.run_if(in_state(ConsoleState::Closed).and(in_state(AppState::Ingame))),
|
||||
update_content.run_if(resource_changed::<ConsoleLog>),
|
||||
(
|
||||
update_scroll_position,
|
||||
process_prompt,
|
||||
execute_console_events,
|
||||
)
|
||||
.run_if(in_state(ConsoleState::Open)),
|
||||
),
|
||||
)
|
||||
.add_systems(OnExit(ConsoleState::Open), despawn::<DebugConsole>)
|
||||
.insert_resource::<ConsoleLog>(ConsoleLog::new());
|
||||
}
|
||||
|
||||
/// the usize is a read index
|
||||
#[derive(Resource, Clone)]
|
||||
struct ConsoleLog(Vec<ConsoleEvent>, usize);
|
||||
|
||||
impl ConsoleLog {
|
||||
fn new() -> Self {
|
||||
Self(
|
||||
vec![ConsoleEvent::Output(String::from(
|
||||
"Welcome to the dev console :3\n (i should probably put some proper info into this some time)",
|
||||
))],
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
fn unread(&mut self) -> &[ConsoleEvent] {
|
||||
let res = &self.0[self.1..];
|
||||
self.1 = self.0.len();
|
||||
res
|
||||
}
|
||||
|
||||
pub fn input(&mut self, content: &str) {
|
||||
self.0.push(ConsoleEvent::Input(content.to_string()))
|
||||
}
|
||||
|
||||
pub fn output(&mut self, content: &str) {
|
||||
self.0.push(ConsoleEvent::Output(content.to_string()))
|
||||
}
|
||||
|
||||
pub fn error(&mut self, content: &str) {
|
||||
self.0.push(ConsoleEvent::Error(content.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ConsoleEvent {
|
||||
Input(String),
|
||||
Output(String),
|
||||
Error(String),
|
||||
}
|
||||
|
||||
const OPEN_CONSOLE_DEFAULT: KeyCode = KeyCode::KeyC;
|
||||
|
||||
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash, Reflect)]
|
||||
enum ConsoleState {
|
||||
#[default]
|
||||
Closed,
|
||||
Open,
|
||||
}
|
||||
|
||||
fn open_console(mut c: Commands, console_data: Res<ConsoleLog>, asset_server: Res<AssetServer>) {
|
||||
log::info!("opening debug console...");
|
||||
c.spawn(ui::console(console_data.clone(), asset_server.clone()));
|
||||
}
|
||||
|
||||
fn execute_console_events(
|
||||
mut ev_reader: EventReader<DebugEvent>,
|
||||
mut log: ResMut<ConsoleLog>,
|
||||
mut next_state: ResMut<NextState<ConsoleState>>,
|
||||
) {
|
||||
for ev in ev_reader.read() {
|
||||
match ev {
|
||||
DebugEvent::CloseDebugConsole => next_state.set(ConsoleState::Closed),
|
||||
DebugEvent::PrintToConsole(s) => log.output(s),
|
||||
_ => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
32
src/game/debug/console/cli.rs
Normal file
32
src/game/debug/console/cli.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::game::debug::DebugEvent;
|
||||
|
||||
pub(super) fn respond(line: &str) -> Result<DebugEvent, String> {
|
||||
let might_be_help_call = line.contains("help") || line.contains("-h");
|
||||
let args = shlex::split(line).ok_or("Invalid Quoting")?;
|
||||
let cli = Cli::try_parse_from(args).map_err(|e| e.to_string());
|
||||
|
||||
if might_be_help_call && cli.as_ref().is_err_and(|item| item.starts_with("Usage: ")) {
|
||||
let Err(s) = cli else { unreachable!() };
|
||||
return Ok(DebugEvent::PrintToConsole(s));
|
||||
}
|
||||
|
||||
Ok(match cli?.cmd {
|
||||
Commands::Close => DebugEvent::CloseDebugConsole,
|
||||
Commands::Echo { text } => DebugEvent::PrintToConsole(text),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(multicall = true)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
Close,
|
||||
Echo { text: String },
|
||||
}
|
112
src/game/debug/console/ui.rs
Normal file
112
src/game/debug/console/ui.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
use bevy::{
|
||||
input::mouse::{MouseScrollUnit, MouseWheel},
|
||||
input_focus::InputFocus,
|
||||
picking::hover::HoverMap,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
mod components;
|
||||
|
||||
#[derive(Component)]
|
||||
pub(super) struct DebugConsole;
|
||||
#[derive(Component)]
|
||||
struct TextInput;
|
||||
|
||||
use bevy_ui_text_input::TextSubmitEvent;
|
||||
pub(super) use components::console;
|
||||
use components::{Content, Prompt};
|
||||
|
||||
use crate::game::debug::DebugEvent;
|
||||
|
||||
use super::{ConsoleEvent, ConsoleLog, ConsoleState, OPEN_CONSOLE_DEFAULT, cli::respond};
|
||||
|
||||
// stolen from scrolling example
|
||||
pub fn update_scroll_position(
|
||||
mut mouse_wheel_events: EventReader<MouseWheel>,
|
||||
hover_map: Res<HoverMap>,
|
||||
mut scrolled_node_query: Query<&mut ScrollPosition>,
|
||||
) {
|
||||
for mouse_wheel_event in mouse_wheel_events.read() {
|
||||
let (dx, dy) = match mouse_wheel_event.unit {
|
||||
MouseScrollUnit::Line => (mouse_wheel_event.x * 18., mouse_wheel_event.y * 18.),
|
||||
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
|
||||
};
|
||||
|
||||
for (_pointer, pointer_map) in hover_map.iter() {
|
||||
for (entity, _hit) in pointer_map.iter() {
|
||||
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
|
||||
scroll_position.offset_x -= dx;
|
||||
scroll_position.offset_y -= dy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn autofocus(mut input_focus: ResMut<InputFocus>, prompt: Single<Entity, With<Prompt>>) {
|
||||
input_focus.set(*prompt);
|
||||
}
|
||||
|
||||
pub fn autoscroll(mut scrollables: Single<&mut ScrollPosition, With<Content>>) {
|
||||
scrollables.offset_y = f32::MAX;
|
||||
}
|
||||
|
||||
pub fn handle_open_console(
|
||||
kb_input: Res<ButtonInput<KeyCode>>,
|
||||
mut console_open_state: ResMut<NextState<ConsoleState>>,
|
||||
) {
|
||||
if kb_input.pressed(OPEN_CONSOLE_DEFAULT) {
|
||||
console_open_state.set(ConsoleState::Open);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_prompt(
|
||||
mut log: ResMut<ConsoleLog>,
|
||||
mut submit_reader: EventReader<TextSubmitEvent>,
|
||||
mut debug_event_writer: EventWriter<DebugEvent>,
|
||||
prompt: Single<Entity, With<Prompt>>,
|
||||
) {
|
||||
for submit in submit_reader.read() {
|
||||
if submit.entity == *prompt && !submit.text.trim().is_empty() {
|
||||
if submit.text.trim() != "close" {
|
||||
log.input(&submit.text);
|
||||
}
|
||||
match respond(&submit.text) {
|
||||
Ok(debug_ev) => {
|
||||
debug_event_writer.write(debug_ev);
|
||||
}
|
||||
Err(e) => log.error(&e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_content(
|
||||
mut c: Commands,
|
||||
content: Single<Entity, With<Content>>,
|
||||
mut log: ResMut<ConsoleLog>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut scrollables: Single<&mut ScrollPosition, With<Content>>,
|
||||
) {
|
||||
c.entity(*content).with_children(|parent| {
|
||||
for item in log.unread() {
|
||||
match dbg!(item) {
|
||||
ConsoleEvent::Input(s) => {
|
||||
parent.spawn(components::input(s.to_string(), &asset_server));
|
||||
}
|
||||
ConsoleEvent::Output(s) => {
|
||||
parent.spawn(components::output(s.to_string(), &asset_server));
|
||||
}
|
||||
ConsoleEvent::Error(s) => {
|
||||
parent.spawn(components::error(
|
||||
// dirty hack so they're not called subcommands
|
||||
s.replace("subcommand", "command").to_string(),
|
||||
&asset_server,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
});
|
||||
scrollables.offset_y = f32::MAX;
|
||||
}
|
180
src/game/debug/console/ui/components.rs
Normal file
180
src/game/debug/console/ui/components.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
use bevy::{ecs::spawn::SpawnWith, prelude::*};
|
||||
use bevy_ui_text_input::{TextInputMode, TextInputNode, TextInputPrompt};
|
||||
|
||||
use crate::game::debug::console::{ConsoleEvent, ConsoleLog};
|
||||
|
||||
use super::DebugConsole;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Prompt;
|
||||
#[derive(Component)]
|
||||
pub struct Content;
|
||||
|
||||
pub fn console(data: ConsoleLog, asset_server: AssetServer) -> impl Bundle {
|
||||
(
|
||||
DebugConsole,
|
||||
Node {
|
||||
justify_self: JustifySelf::Center,
|
||||
width: Val::Percent(85.),
|
||||
max_height: Val::Percent(100. / 3.),
|
||||
flex_direction: FlexDirection::Column,
|
||||
border: UiRect {
|
||||
left: Val::Px(2.),
|
||||
right: Val::Px(2.),
|
||||
top: Val::Px(0.),
|
||||
bottom: Val::Px(2.),
|
||||
},
|
||||
..default()
|
||||
},
|
||||
BorderColor(Color::BLACK),
|
||||
ZIndex(i32::MAX),
|
||||
BackgroundColor(Color::Srgba(Srgba::BLACK.with_alpha(0.2))),
|
||||
children![
|
||||
(
|
||||
Content,
|
||||
Node {
|
||||
padding: UiRect::all(Val::Px(10.)),
|
||||
overflow: Overflow::scroll_y(),
|
||||
flex_direction: FlexDirection::Column,
|
||||
..default()
|
||||
},
|
||||
Children::spawn(SpawnWith({
|
||||
let asset_server = asset_server.clone();
|
||||
move |parent: &mut ChildSpawner| {
|
||||
for item in data.0 {
|
||||
match item {
|
||||
ConsoleEvent::Input(s) => {
|
||||
parent.spawn(input(s.to_string(), &asset_server))
|
||||
}
|
||||
ConsoleEvent::Output(s) => {
|
||||
parent.spawn(output(s.to_string(), &asset_server))
|
||||
}
|
||||
ConsoleEvent::Error(s) => {
|
||||
parent.spawn(error(s.to_string(), &asset_server))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}))
|
||||
),
|
||||
(
|
||||
Node {
|
||||
width: Val::Percent(100.),
|
||||
padding: UiRect {
|
||||
left: Val::Px(10.),
|
||||
right: Val::Px(10.),
|
||||
top: Val::Px(10.),
|
||||
bottom: Val::Px(10.)
|
||||
},
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(Color::Srgba(Srgba::gray(0.2).with_alpha(0.3))),
|
||||
children![
|
||||
(
|
||||
Text::new("> "),
|
||||
TextFont {
|
||||
font: asset_server
|
||||
.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
|
||||
font_size: 14.,
|
||||
..default()
|
||||
}
|
||||
),
|
||||
(
|
||||
Prompt,
|
||||
Node {
|
||||
width: Val::Auto,
|
||||
flex_grow: 1.,
|
||||
..default()
|
||||
},
|
||||
TextInputNode {
|
||||
clear_on_submit: true,
|
||||
mode: TextInputMode::SingleLine,
|
||||
is_enabled: true,
|
||||
focus_on_pointer_down: true,
|
||||
..default()
|
||||
},
|
||||
TextInputPrompt {
|
||||
text: String::new(),
|
||||
..default()
|
||||
},
|
||||
TextFont {
|
||||
font: asset_server
|
||||
.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
|
||||
font_size: 14.,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
]
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn input(content: String, asset_server: &AssetServer) -> impl Bundle {
|
||||
console_output(
|
||||
Srgba::GREEN.with_alpha(0.05),
|
||||
UiRect::all(Val::Px(1.)),
|
||||
Srgba::GREEN,
|
||||
format!("> {content}"),
|
||||
asset_server,
|
||||
)
|
||||
}
|
||||
pub fn output(content: String, asset_server: &AssetServer) -> impl Bundle {
|
||||
console_output(
|
||||
Srgba::WHITE.with_alpha(0.05),
|
||||
UiRect::all(Val::ZERO),
|
||||
Srgba::NONE,
|
||||
content,
|
||||
asset_server,
|
||||
)
|
||||
}
|
||||
pub fn error(content: String, asset_server: &AssetServer) -> impl Bundle {
|
||||
console_output(
|
||||
Srgba::RED.with_alpha(0.025),
|
||||
UiRect::all(Val::Px(2.)),
|
||||
Srgba::RED.with_alpha(0.75),
|
||||
content,
|
||||
asset_server,
|
||||
)
|
||||
}
|
||||
|
||||
fn console_output(
|
||||
background_color: Srgba,
|
||||
border_width: UiRect,
|
||||
border_color: Srgba,
|
||||
content: String,
|
||||
asset_server: &AssetServer,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
margin: UiRect {
|
||||
left: Val::Px(0.),
|
||||
right: Val::Px(0.),
|
||||
top: Val::Px(0.),
|
||||
bottom: Val::Px(10.),
|
||||
},
|
||||
padding: UiRect::all(Val::Px(7.5)),
|
||||
border: border_width,
|
||||
..default()
|
||||
},
|
||||
BorderRadius::all(Val::Px(7.5)),
|
||||
BorderColor(Color::Srgba(border_color)),
|
||||
BackgroundColor(Color::Srgba(background_color)),
|
||||
Pickable {
|
||||
should_block_lower: false,
|
||||
..default()
|
||||
},
|
||||
children![(
|
||||
Text(content),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/DepartureMono-1.500/DepartureMono-Regular.otf"),
|
||||
font_size: 14.,
|
||||
..default()
|
||||
},
|
||||
Pickable {
|
||||
should_block_lower: false,
|
||||
..default()
|
||||
},
|
||||
)],
|
||||
)
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
#![feature(iter_collect_into)]
|
||||
use bevy::prelude::*;
|
||||
use bevy_skein::SkeinPlugin;
|
||||
use bevy_ui_text_input::TextInputPlugin;
|
||||
|
||||
mod camera;
|
||||
mod cleanup;
|
||||
|
@ -21,7 +22,7 @@ fn main() {
|
|||
App::new()
|
||||
.register_type::<TPCTarget>()
|
||||
.add_systems(Startup, camera::setup)
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_plugins((DefaultPlugins, TextInputPlugin))
|
||||
.add_plugins((game::plugin, menus::plugin, debugging::plugin))
|
||||
.add_plugins(SkeinPlugin::default())
|
||||
.init_state::<AppState>()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue