Initial Commit

This commit is contained in:
AviiNL
2023-12-22 04:50:17 +01:00
parent 15f764aae8
commit f79b6b5dfb
41 changed files with 4379 additions and 270 deletions

View File

@@ -0,0 +1,19 @@
# This packages is loosely(or not) based on https://github.com/RichoDemus/bevy-console
[package]
name = "guardian_commands"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap.workspace = true
bevy.workspace = true
shlex.workspace = true
nom = "7.1"
[lints]
workspace = true

View File

@@ -0,0 +1,336 @@
use std::fmt::Display;
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case};
use nom::character::complete::{alpha1, alphanumeric1, char, multispace0};
use nom::combinator::{map, opt, recognize};
use nom::error::ErrorKind;
use nom::sequence::tuple;
use nom::{Err as NomError, IResult};
#[derive(Debug)]
pub enum Awacs {
Overlord,
Magic,
Wizard,
Focus,
Darkstar,
}
#[derive(Debug)]
pub enum Receiver {
Awacs(Awacs),
Atc(String), //airfield name
}
impl Display for Receiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Receiver::Awacs(Awacs::Overlord) => write!(f, "Overlord"),
Receiver::Awacs(Awacs::Magic) => write!(f, "Magic"),
Receiver::Awacs(Awacs::Wizard) => write!(f, "Wizard"),
Receiver::Awacs(Awacs::Focus) => write!(f, "Focus"),
Receiver::Awacs(Awacs::Darkstar) => write!(f, "Darkstar"),
Receiver::Atc(airfield) => write!(f, "{}", airfield.to_owned()),
}
}
}
// impl ToString for Receiver {
// fn to_string(&self) -> String {
// match self {
// Receiver::Awacs(Awacs::Overlord) => "Overlord".to_string(),
// Receiver::Awacs(Awacs::Darkstar) => "Darkstar".to_string(),
// Receiver::Atc(airfield) => airfield.to_owned(),
// }
// }
// }
fn parse_receiver(input: &str) -> IResult<&str, Receiver> {
alt((
map(tag_no_case("overlord"), |_| {
Receiver::Awacs(Awacs::Overlord)
}),
map(tag_no_case("magic"), |_| Receiver::Awacs(Awacs::Magic)),
map(tag_no_case("wizard"), |_| Receiver::Awacs(Awacs::Wizard)),
map(tag_no_case("focus"), |_| Receiver::Awacs(Awacs::Focus)),
map(tag_no_case("darkstar"), |_| {
Receiver::Awacs(Awacs::Darkstar)
}),
map(alphanumeric1, |e: &str| Receiver::Atc(e.to_owned())), // anything else = airfield? airfields _can_ contain spaces though.. soo w... hmm... parsing that is a bitch
))(input)
}
#[derive(Debug)]
pub enum Squadron {
Enfield,
Springfield,
Uzi,
Colt,
Dodge,
Ford,
Chevy,
Pontiac,
Viper,
Venom,
Lobo,
Cowboy,
Python,
Rattler,
Panther,
Wolf,
Weasel,
Wild,
Ninja,
Jedi,
}
impl ToString for Squadron {
fn to_string(&self) -> String {
match self {
Squadron::Enfield => "Enfield".to_string(),
Squadron::Springfield => "Springfield".to_string(),
Squadron::Uzi => "Uzi".to_string(),
Squadron::Colt => "Colt".to_string(),
Squadron::Dodge => "Dodge".to_string(),
Squadron::Ford => "Ford".to_string(),
Squadron::Chevy => "Chevy".to_string(),
Squadron::Pontiac => "Pontiac".to_string(),
Squadron::Viper => "Viper".to_string(),
Squadron::Venom => "Venom".to_string(),
Squadron::Lobo => "Lobo".to_string(),
Squadron::Cowboy => "Cowboy".to_string(),
Squadron::Python => "Python".to_string(),
Squadron::Rattler => "Rattler".to_string(),
Squadron::Panther => "Panther".to_string(),
Squadron::Wolf => "Wolf".to_string(),
Squadron::Weasel => "Weasel".to_string(),
Squadron::Wild => "Wild".to_string(),
Squadron::Ninja => "Ninja".to_string(),
Squadron::Jedi => "Jedi".to_string(),
}
}
}
impl std::str::FromStr for Squadron {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Enfield" => Ok(Squadron::Enfield),
"Springfield" => Ok(Squadron::Springfield),
"Uzi" => Ok(Squadron::Uzi),
"Colt" => Ok(Squadron::Colt),
"Dodge" => Ok(Squadron::Dodge),
"Ford" => Ok(Squadron::Ford),
"Chevy" => Ok(Squadron::Chevy),
"Pontiac" => Ok(Squadron::Pontiac),
"Viper" => Ok(Squadron::Viper),
"Venom" => Ok(Squadron::Venom),
"Lobo" => Ok(Squadron::Lobo),
"Cowboy" => Ok(Squadron::Cowboy),
"Python" => Ok(Squadron::Python),
"Rattler" => Ok(Squadron::Rattler),
"Panther" => Ok(Squadron::Panther),
"Wolf" => Ok(Squadron::Wolf),
"Weasel" => Ok(Squadron::Weasel),
"Wild" => Ok(Squadron::Wild),
"Ninja" => Ok(Squadron::Ninja),
"Jedi" => Ok(Squadron::Jedi),
_ => Err("Invalid squadron".into()),
}
}
}
#[derive(Debug)]
pub struct Element {
pub squadron: Squadron,
pub group: u8,
pub unit: u8,
}
impl Display for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {}",
self.squadron.to_string(),
self.group,
self.unit
)
}
}
fn add_spaces_around_numbers(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
result.push(c);
if let Some(next_char) = chars.peek() {
if next_char.is_numeric() {
result.push(' ');
}
}
}
result
}
impl std::str::FromStr for Element {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let d = add_spaces_around_numbers(s);
let mut d = d.split(' ');
let squadron: Squadron = d.next().unwrap().parse()?;
let group: u8 = d.next().unwrap().parse()?;
let unit: u8 = d.next().unwrap().parse()?;
Ok(Element {
squadron,
group,
unit,
})
}
}
fn parse_element(input: &str) -> IResult<&str, Element> {
let mut parser = tuple((
alpha1, // Squadron name
opt(multispace0), // Optional whitespace
opt(tuple((
// Optional group
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
opt(char('-')),
opt(tuple((
// Optional unit
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
));
let (remainder, (squadron_str, _, group_str, _, unit_str)) = parser(input)?;
let squadron = match squadron_str.to_lowercase().as_str() {
"enfield" => Squadron::Enfield,
"springfield" => Squadron::Springfield,
"uzi" => Squadron::Uzi,
"colt" => Squadron::Colt,
"dodge" => Squadron::Dodge,
"ford" => Squadron::Ford,
"chevy" => Squadron::Chevy,
"pontiac" => Squadron::Pontiac,
"viper" => Squadron::Viper,
"venom" => Squadron::Venom,
"lobo" => Squadron::Lobo,
"cowboy" => Squadron::Cowboy,
"python" => Squadron::Python,
"rattler" => Squadron::Rattler,
"panther" => Squadron::Panther,
"wolf" => Squadron::Wolf,
"weasel" => Squadron::Weasel,
"wild" => Squadron::Wild,
"ninja" => Squadron::Ninja,
"jedi" => Squadron::Jedi,
_ => {
return Err(NomError::Error(nom::error::Error {
input: squadron_str,
code: ErrorKind::Alpha,
}))
}
};
let group = group_str.unwrap_or((None, 0, None)).1;
let unit = unit_str.unwrap_or((None, 0, None)).1;
Ok((
remainder,
Element {
squadron,
group,
unit,
},
))
}
fn parse_group_unit(input: &str) -> IResult<&str, u8> {
let parse_digit = map(
recognize(alt((
char('0'),
char('1'),
char('2'),
char('3'),
char('4'),
char('5'),
char('6'),
char('7'),
char('8'),
char('9'),
))),
|s: &str| s.parse::<u8>().unwrap(),
);
let parse_word = alt((
map(tag("one"), |_| 1),
map(tag("two"), |_| 2),
map(tag("three"), |_| 3),
map(tag("four"), |_| 4),
map(tag("five"), |_| 5),
map(tag("six"), |_| 6),
map(tag("seven"), |_| 7),
map(tag("eight"), |_| 8),
map(tag("nine"), |_| 9),
));
alt((parse_digit, parse_word))(input)
}
// #[derive(Debug)]
// pub enum Unit {
// NauticalMiles,
// Kilometers,
// }
// #[derive(Debug)]
// pub enum Call {
// RadioCheck(Awacs, Element),
// Shopping(Awacs, Element),
// LocateAirfield(Awacs, Element, String),
// LocateFriendly(Awacs, Element, Element),
// Tripwire(Awacs, Element, Option<i32>, Unit),
// }
#[derive(Debug)]
pub struct Call {
pub receiver: Receiver,
pub element: Element,
pub command: String,
}
pub fn parse_call(input: &str) -> IResult<&str, Call> {
let mut parser = tuple((parse_receiver, multispace0, parse_element));
let (command, (receiver, _, element)) = parser(input)?;
if element.group == 0 || element.unit == 0 {
return Err(NomError::Error(nom::error::Error {
input: command,
code: ErrorKind::Digit,
}));
}
Ok((
"",
Call {
receiver,
element,
command: command.to_string(),
},
))
}

View File

@@ -0,0 +1,554 @@
use bevy::{
app::{App, Startup, Update},
ecs::{
component::Tick,
entity::Entity,
event::{Event, EventReader},
schedule::IntoSystemConfigs,
system::{ResMut, Resource, SystemMeta, SystemParam},
world::{unsafe_world_cell::UnsafeWorldCell, World},
},
log::{debug, warn},
};
// use bevy::{input::keyboard::KeyboardInput, prelude::*};
// use bevy_egui::egui::{self, Align, ScrollArea, TextEdit};
// use bevy_egui::egui::{text::LayoutJob, text_edit::CCursorRange};
// use bevy_egui::egui::{Context, Id};
// use bevy_egui::{
// egui::{epaint::text::cursor::CCursor, Color32, FontId, TextFormat},
// EguiContexts,
// };
use clap::{builder::StyledStr, CommandFactory, FromArgMatches};
use std::collections::BTreeMap;
use std::marker::PhantomData;
use std::mem;
use crate::{parser, ConsoleSet};
type ConsoleCommandEnteredReaderSystemParam = EventReader<'static, 'static, ConsoleCommandEntered>;
/// A super-trait for command like structures
pub trait Command: NamedCommand + CommandFactory + FromArgMatches + Sized + Resource {}
impl<T: NamedCommand + CommandFactory + FromArgMatches + Sized + Resource> Command for T {}
/// Trait used to allow uniquely identifying commands at compile time
pub trait NamedCommand {
/// Return the unique command identifier (same as the command "executable")
fn name() -> &'static str;
}
/// Executed parsed console command.
///
/// Used to capture console commands which implement [`CommandName`], [`CommandArgs`] & [`CommandHelp`].
/// These can be easily implemented with the [`ConsoleCommand`](bevy_console_derive::ConsoleCommand) derive macro.
///
/// # Example
///
/// ```
/// # use bevy_console::ConsoleCommand;
/// # use clap::Parser;
/// /// Prints given arguments to the console.
/// #[derive(Parser, ConsoleCommand)]
/// #[command(name = "log")]
/// struct LogCommand {
/// /// Message to print
/// msg: String,
/// /// Number of times to print message
/// num: Option<i64>,
/// }
///
/// fn log_command(mut log: ConsoleCommand<LogCommand>) {
/// if let Some(Ok(LogCommand { msg, num })) = log.take() {
/// log.ok();
/// }
/// }
/// ```
pub struct ConsoleCommand<T> {
command: Option<Result<T, clap::Error>>,
pub raw: Option<String>,
pub pilot: Option<Entity>,
pub operator: Option<Entity>,
}
impl<T> ConsoleCommand<T> {
/// Returns Some(T) if the command was executed and arguments were valid.
///
/// This method should only be called once.
/// Consecutive calls will return None regardless if the command occurred.
pub fn take(&mut self) -> Option<Result<T, clap::Error>> {
mem::take(&mut self.command)
}
}
pub struct ConsoleCommandState<T> {
#[allow(clippy::type_complexity)]
event_reader: <ConsoleCommandEnteredReaderSystemParam as SystemParam>::State,
marker: PhantomData<T>,
}
unsafe impl<T: Command> SystemParam for ConsoleCommand<T> {
type State = ConsoleCommandState<T>;
type Item<'w, 's> = ConsoleCommand<T>;
fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
let event_reader = ConsoleCommandEnteredReaderSystemParam::init_state(world, system_meta);
ConsoleCommandState {
event_reader,
marker: PhantomData,
}
}
#[inline]
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &SystemMeta,
world: UnsafeWorldCell<'w>,
change_tick: Tick,
) -> Self::Item<'w, 's> {
let mut event_reader = ConsoleCommandEnteredReaderSystemParam::get_param(
&mut state.event_reader,
system_meta,
world,
change_tick,
);
let command = event_reader.read().find_map(|command| {
for name in parser::parse(T::name()) {
if name == command.command_name {
let clap_command = T::command().no_binary_name(true);
let arg_matches = clap_command.try_get_matches_from(command.args.iter());
debug!(
"Trying to parse as `{}`. Result: {arg_matches:?}",
command.command_name
);
match arg_matches {
Ok(matches) => match T::from_arg_matches(&matches) {
Ok(from_arg_matches) => {
return Some(Ok((
from_arg_matches,
command.raw.clone(),
command.pilot,
command.operator,
)));
}
Err(err) => return Some(Err(err)),
},
Err(err) => {
return Some(Err(err));
}
}
}
}
None
});
if let Some(Ok(command)) = command {
return ConsoleCommand {
command: Some(Ok(command.0)),
raw: Some(command.1),
pilot: Some(command.2),
operator: Some(command.3),
};
}
ConsoleCommand {
command: None,
raw: None,
pilot: None,
operator: None,
}
}
}
/// Parsed raw console command into `command` and `args`.
#[derive(Clone, Debug, Event)]
pub struct ConsoleCommandEntered {
/// the command definition
pub command_name: String,
/// the raw input string
pub raw: String,
/// Raw parsed arguments
pub args: Vec<String>,
/// Pilot making the request
pub pilot: Entity,
/// Operator responding
pub operator: Entity,
}
/// Events to print to the console.
#[derive(Clone, Debug, Eq, Event, PartialEq)]
pub struct PrintConsoleLine {
/// Console line
pub line: StyledStr,
}
impl PrintConsoleLine {
/// Creates a new console line to print.
pub const fn new(line: StyledStr) -> Self {
Self { line }
}
}
/// Console configuration
#[derive(Clone, Resource, Default)]
pub struct ConsoleConfiguration {
/// Registered console commands
pub commands: BTreeMap<String, clap::Command>,
}
/// Add a console commands to Bevy app.
pub trait AddConsoleCommand {
/// Add a console command with a given system.
///
/// This registers the console command so it will print with the built-in `help` console command.
///
/// # Example
///
/// ```
/// # use bevy::prelude::*;
/// # use bevy_console::{AddConsoleCommand, ConsoleCommand};
/// # use clap::Parser;
/// App::new()
/// .add_console_command::<LogCommand, _>(log_command);
/// #
/// # /// Prints given arguments to the console.
/// # #[derive(Parser, ConsoleCommand)]
/// # #[command(name = "log")]
/// # struct LogCommand;
/// #
/// # fn log_command(mut log: ConsoleCommand<LogCommand>) {}
/// ```
fn add_console_command<T: Command, Params>(
&mut self,
system: impl IntoSystemConfigs<Params>,
) -> &mut Self;
}
impl AddConsoleCommand for App {
fn add_console_command<T: Command, Params>(
&mut self,
system: impl IntoSystemConfigs<Params>,
) -> &mut Self {
let sys = move |mut config: ResMut<ConsoleConfiguration>| {
for name in crate::parser::parse(T::name()) {
let command = T::command().no_binary_name(true);
if config.commands.contains_key(&name) {
warn!(
"console command '{}' already registered and was overwritten",
name
);
}
config.commands.insert(name.clone(), command);
}
};
self.add_systems(Startup, sys)
.add_systems(Update, system.in_set(ConsoleSet::Commands))
}
}
// /// Console open state
// #[derive(Default, Resource)]
// pub struct ConsoleOpen {
// /// Console open
// pub open: bool,
// }
// #[derive(Resource)]
// pub(crate) struct ConsoleState {
// pub(crate) buf: String,
// pub(crate) scrollback: Vec<StyledStr>,
// pub(crate) history: VecDeque<StyledStr>,
// pub(crate) history_index: usize,
// }
// impl Default for ConsoleState {
// fn default() -> Self {
// ConsoleState {
// buf: String::default(),
// scrollback: Vec::new(),
// history: VecDeque::from([StyledStr::new()]),
// history_index: 0,
// }
// }
// }
// pub(crate) fn console_ui(
// mut egui_context: EguiContexts,
// config: Res<ConsoleConfiguration>,
// mut keyboard_input_events: EventReader<KeyboardInput>,
// keys: Res<Input<KeyCode>>,
// mut state: ResMut<ConsoleState>,
// mut command_entered: EventWriter<ConsoleCommandEntered>,
// mut console_open: ResMut<ConsoleOpen>,
// ) {
// let keyboard_input_events = keyboard_input_events.iter().collect::<Vec<_>>();
// let ctx = egui_context.ctx_mut();
// let pressed = keyboard_input_events
// .iter()
// .any(|code| console_key_pressed(code, &config.keys));
// // always close if console open
// // avoid opening console if typing in another text input
// if pressed && (console_open.open || !ctx.wants_keyboard_input()) {
// console_open.open = !console_open.open;
// }
// if console_open.open {
// egui::Window::new("Console")
// .collapsible(false)
// .default_pos([config.left_pos, config.top_pos])
// .default_size([config.width, config.height])
// .resizable(true)
// .show(ctx, |ui| {
// ui.vertical(|ui| {
// let scroll_height = ui.available_height() - 30.0;
// // Scroll area
// ScrollArea::vertical()
// .auto_shrink([false, false])
// .stick_to_bottom(true)
// .max_height(scroll_height)
// .show(ui, |ui| {
// ui.vertical(|ui| {
// for line in &state.scrollback {
// let mut text = LayoutJob::default();
// text.append(
// &line.to_string(), //TOOD: once clap supports custom styling use it here
// 0f32,
// TextFormat::simple(FontId::monospace(14f32), Color32::GRAY),
// );
// ui.label(text);
// }
// });
// // Scroll to bottom if console just opened
// if console_open.is_changed() {
// ui.scroll_to_cursor(Some(Align::BOTTOM));
// }
// });
// // Separator
// ui.separator();
// // Input
// let text_edit = TextEdit::singleline(&mut state.buf)
// .desired_width(f32::INFINITY)
// .lock_focus(true)
// .font(egui::TextStyle::Monospace);
// // Handle enter
// let text_edit_response = ui.add(text_edit);
// if text_edit_response.lost_focus()
// && ui.input(|i| i.key_pressed(egui::Key::Enter))
// {
// if state.buf.trim().is_empty() {
// state.scrollback.push(StyledStr::new());
// } else {
// let msg = format!("{}{}", config.symbol, state.buf);
// state.scrollback.push(msg.into());
// let cmd_string = state.buf.clone();
// state.history.insert(1, cmd_string.into());
// if state.history.len() > config.history_size + 1 {
// state.history.pop_back();
// }
// let mut args = Shlex::new(&state.buf).collect::<Vec<_>>();
// if !args.is_empty() {
// let command_name = args.remove(0);
// debug!("Command entered: `{command_name}`, with args: `{args:?}`");
// let command = config.commands.get(command_name.as_str());
// if command.is_some() {
// command_entered
// .send(ConsoleCommandEntered { command_name, args });
// } else {
// debug!(
// "Command not recognized, recognized commands: `{:?}`",
// config.commands.keys().collect::<Vec<_>>()
// );
// state.scrollback.push("error: Invalid command".into());
// }
// }
// state.buf.clear();
// }
// }
// // Clear on ctrl+l
// if keyboard_input_events
// .iter()
// .any(|&k| k.state.is_pressed() && k.key_code == Some(KeyCode::L))
// && (keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]))
// {
// state.scrollback.clear();
// }
// // Handle up and down through history
// if text_edit_response.has_focus()
// && ui.input(|i| i.key_pressed(egui::Key::ArrowUp))
// && state.history.len() > 1
// && state.history_index < state.history.len() - 1
// {
// if state.history_index == 0 && !state.buf.trim().is_empty() {
// *state.history.get_mut(0).unwrap() = state.buf.clone().into();
// }
// state.history_index += 1;
// let previous_item = state.history.get(state.history_index).unwrap().clone();
// state.buf = previous_item.to_string();
// set_cursor_pos(ui.ctx(), text_edit_response.id, state.buf.len());
// } else if text_edit_response.has_focus()
// && ui.input(|i| i.key_pressed(egui::Key::ArrowDown))
// && state.history_index > 0
// {
// state.history_index -= 1;
// let next_item = state.history.get(state.history_index).unwrap().clone();
// state.buf = next_item.to_string();
// set_cursor_pos(ui.ctx(), text_edit_response.id, state.buf.len());
// }
// // Focus on input
// ui.memory_mut(|m| m.request_focus(text_edit_response.id));
// });
// });
// }
// }
// pub(crate) fn receive_console_line(
// mut console_state: ResMut<ConsoleState>,
// mut events: EventReader<PrintConsoleLine>,
// ) {
// for event in events.iter() {
// let event: &PrintConsoleLine = event;
// console_state.scrollback.push(event.line.clone());
// }
// }
// fn console_key_pressed(
// keyboard_input: &KeyboardInput,
// configured_keys: &[ToggleConsoleKey],
// ) -> bool {
// if !keyboard_input.state.is_pressed() {
// return false;
// }
// for configured_key in configured_keys {
// match configured_key {
// ToggleConsoleKey::KeyCode(configured_key_code) => match keyboard_input.key_code {
// None => continue,
// Some(pressed_key) => {
// if configured_key_code == &pressed_key {
// return true;
// }
// }
// },
// ToggleConsoleKey::ScanCode(configured_scan_code) => {
// if &keyboard_input.scan_code == configured_scan_code {
// return true;
// }
// }
// }
// }
// false
// }
// fn set_cursor_pos(ctx: &Context, id: Id, pos: usize) {
// if let Some(mut state) = TextEdit::load_state(ctx, id) {
// state.set_ccursor_range(Some(CCursorRange::one(CCursor::new(pos))));
// state.store(ctx, id);
// }
// }
// #[cfg(test)]
// mod tests {
// use bevy::input::ButtonState;
// use super::*;
// #[test]
// fn test_console_key_pressed_scan_code() {
// let input = KeyboardInput {
// scan_code: 41,
// key_code: None,
// state: ButtonState::Pressed,
// window: Entity::PLACEHOLDER,
// };
// let config = vec![ToggleConsoleKey::ScanCode(41)];
// let result = console_key_pressed(&input, &config);
// assert!(result);
// }
// #[test]
// fn test_console_wrong_key_pressed_scan_code() {
// let input = KeyboardInput {
// scan_code: 42,
// key_code: None,
// state: ButtonState::Pressed,
// window: Entity::PLACEHOLDER,
// };
// let config = vec![ToggleConsoleKey::ScanCode(41)];
// let result = console_key_pressed(&input, &config);
// assert!(!result);
// }
// #[test]
// fn test_console_key_pressed_key_code() {
// let input = KeyboardInput {
// scan_code: 0,
// key_code: Some(KeyCode::Grave),
// state: ButtonState::Pressed,
// window: Entity::PLACEHOLDER,
// };
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
// let result = console_key_pressed(&input, &config);
// assert!(result);
// }
// #[test]
// fn test_console_wrong_key_pressed_key_code() {
// let input = KeyboardInput {
// scan_code: 0,
// key_code: Some(KeyCode::A),
// state: ButtonState::Pressed,
// window: Entity::PLACEHOLDER,
// };
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
// let result = console_key_pressed(&input, &config);
// assert!(!result);
// }
// #[test]
// fn test_console_key_right_key_but_not_pressed() {
// let input = KeyboardInput {
// scan_code: 0,
// key_code: Some(KeyCode::Grave),
// state: ButtonState::Released,
// window: Entity::PLACEHOLDER,
// };
// let config = vec![ToggleConsoleKey::KeyCode(KeyCode::Grave)];
// let result = console_key_pressed(&input, &config);
// assert!(!result);
// }
// }

View File

@@ -0,0 +1,62 @@
pub mod call;
use bevy::prelude::*;
// pub use bevy_console_derive::ConsoleCommand;
// use bevy_egui::EguiPlugin;
// use crate::commands::clear::{clear_command, ClearCommand};
// use crate::commands::exit::{exit_command, ExitCommand};
// use crate::commands::help::{help_command, HelpCommand};
pub use crate::console::{
AddConsoleCommand, Command, ConsoleCommand, ConsoleCommandEntered, ConsoleConfiguration,
NamedCommand, PrintConsoleLine,
};
// pub use color::{Style, StyledStr};
// use crate::console::ConsoleState;
// mod color;
// mod commands;
mod console;
mod parser;
// mod macros;
/// Console plugin.
pub struct CommandsPlugin;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
/// The SystemSet for console/command related systems
pub enum ConsoleSet {
/// Systems operating the console UI (the input layer)
ConsoleIO,
/// Systems executing console commands (the functionality layer).
/// All command handler systems are added to this set
Commands,
/// Systems running after command systems, which depend on the fact commands have executed beforehand (the output layer).
/// For example a system which makes use of [`PrintConsoleLine`] events should be placed in this set to be able to receive
/// New lines to print in the same frame
PostCommands,
}
/// Run condition which does not run any command systems if no command was entered
fn have_commands(commands: EventReader<ConsoleCommandEntered>) -> bool {
!commands.is_empty()
}
impl Plugin for CommandsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ConsoleConfiguration>()
.add_event::<ConsoleCommandEntered>()
.configure_sets(
Update,
(
ConsoleSet::Commands
.after(ConsoleSet::ConsoleIO)
.run_if(have_commands),
ConsoleSet::PostCommands.after(ConsoleSet::Commands),
),
);
}
}

View File

@@ -0,0 +1,194 @@
use nom::{
bytes::complete::tag,
error::{Error, ErrorKind, ParseError},
sequence::delimited,
IResult,
};
// from https://github.com/getreu/parse-hyperlinks/blob/5af034d14aa72ffb9e705da13bf557a564b1bebf/parse-hyperlinks/src/lib.rs#L41
fn take_until_unbalanced(
opening_bracket: char,
closing_bracket: char,
) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
let mut index = 0;
let mut bracket_counter = 0;
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
index += n;
let mut it = i[index..].chars();
match it.next() {
Some('\\') => {
// Skip the escape char `\`.
index += '\\'.len_utf8();
// Skip also the following char.
if let Some(c) = it.next() {
index += c.len_utf8();
}
}
Some(c) if c == opening_bracket => {
bracket_counter += 1;
index += opening_bracket.len_utf8();
}
Some(c) if c == closing_bracket => {
// Closing bracket.
bracket_counter -= 1;
index += closing_bracket.len_utf8();
}
// Can not happen.
_ => unreachable!(),
};
// We found the unmatched closing bracket.
if bracket_counter == -1 {
// We do not consume it.
index -= closing_bracket.len_utf8();
return Ok((&i[index..], &i[0..index]));
};
}
if bracket_counter == 0 {
Ok(("", i))
} else {
Err(nom::Err::Error(Error::from_error_kind(
i,
ErrorKind::TakeUntil,
)))
}
}
}
fn split(input: &str) -> IResult<&str, &str> {
let mut parser = delimited(tag("["), take_until_unbalanced('[', ']'), tag("]"));
parser(input)
}
fn inner_parse(input: &str, depth: usize) -> Vec<String> {
let mut output = vec![];
let start_idx = input.find('[').unwrap_or(input.len());
if start_idx > 0 && depth == 0 {
output.push(input[..start_idx].to_string());
}
if start_idx == input.len() {
return output;
}
let input = &input[start_idx..];
let splitted = split(input).unwrap();
let variants: Vec<String> = splitted
.1
.split('|')
.map(|v| v.to_string())
.collect::<Vec<_>>();
let mut temp = vec![];
let mut temp_str = String::new();
for v in &variants {
if v.contains('[') || v.contains(']') {
temp_str.push_str(v);
temp_str.push('|');
} else {
let len = temp_str.len();
if len > 0 {
temp.push(temp_str[..len - 1].to_string());
temp.push(v.to_string());
}
temp_str.clear();
}
}
let variants = if temp.is_empty() { variants } else { temp };
// check if variants need expanding
let mut temp = vec![];
for v in variants {
if v.contains('[') {
let index = v.find('[').unwrap_or(0);
let prepend = &v[..index];
let remainder = inner_parse(&v, depth + 1);
for r in &remainder {
let l = format!("{}{}", prepend, r);
temp.push(l);
}
} else {
temp.push(v);
}
}
let remainder = splitted.0;
let backup = output.clone();
output.clear();
for variant in temp {
if remainder.contains('[') {
let index = remainder.find('[').unwrap_or(0);
let prepend = &remainder[..index];
let r2 = inner_parse(remainder, depth + 1);
if !backup.is_empty() {
for o in &backup {
for r in &r2 {
let l = format!("{}{}{}{}", o, variant, prepend, r);
output.push(l);
}
}
} else {
for r in &r2 {
let l = format!("{}{}{}", variant, prepend, r);
output.push(l);
}
}
} else if !backup.is_empty() {
for o in &backup {
output.push(format!("{}{}{}", o, variant, remainder));
}
} else {
output.push(format!("{}{}", variant, remainder));
}
}
output
}
pub fn parse(input: &str) -> Vec<String> {
inner_parse(input, 0)
}
#[test]
fn parse_test() {
// This works
assert_eq!(
parse("[Hello|Goodbye], World!"),
vec!["Hello, World!", "Goodbye, World!"]
);
assert_eq!(
parse("[Hello|Goodbye], [World|Moon]!"),
vec![
"Hello, World!",
"Hello, Moon!",
"Goodbye, World!",
"Goodbye, Moon!",
]
);
assert_eq!(
parse("[[Heading|Bearing] [to|for]|Where is] that"),
vec![
"Heading to that",
"Heading for that",
"Bearing to that",
"Bearing for that",
"Where is that",
]
);
assert_eq!(
parse("set [warning|tripwire]"),
vec!["set warning", "set tripwire"]
);
}