From 4ce39f9421783754daa6084907663507b54162b6 Mon Sep 17 00:00:00 2001 From: Shagnor Date: Sat, 24 Dec 2022 21:16:23 +1100 Subject: [PATCH] Start work on command parsing. --- Cargo.lock | 34 ++++++++- blastmud_game/Cargo.toml | 6 +- blastmud_game/src/db.rs | 24 ++++--- blastmud_game/src/main.rs | 5 +- blastmud_game/src/message_handler.rs | 5 +- .../src/message_handler/new_session.rs | 6 +- .../src/message_handler/user_commands.rs | 70 ++++++++++++++++++- .../src/message_handler/user_commands/help.rs | 13 ++++ .../message_handler/user_commands/ignore.rs | 12 ++++ .../message_handler/user_commands/parsing.rs | 19 +++++ blastmud_game/src/models.rs | 1 + blastmud_game/src/models/session.rs | 15 ++++ blastmud_game/src/regular_tasks.rs | 12 ++-- 13 files changed, 194 insertions(+), 28 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/help.rs create mode 100644 blastmud_game/src/message_handler/user_commands/ignore.rs create mode 100644 blastmud_game/src/message_handler/user_commands/parsing.rs create mode 100644 blastmud_game/src/models.rs create mode 100644 blastmud_game/src/models/session.rs diff --git a/Cargo.lock b/Cargo.lock index da73674e..28977abe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" dependencies = [ "proc-macro2", "quote", @@ -62,6 +62,7 @@ name = "blastmud_game" version = "0.1.0" dependencies = [ "ansi_macro", + "async-trait", "base64 0.20.0", "blastmud_interfaces", "deadpool", @@ -69,8 +70,11 @@ dependencies = [ "futures", "log", "nix", + "nom", + "phf", "ring", "serde", + "serde_json", "serde_yaml", "simple_logger", "tokio", @@ -797,9 +801,33 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" dependencies = [ + "phf_macros", "phf_shared", ] +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf_shared" version = "0.11.1" @@ -868,6 +896,8 @@ dependencies = [ "bytes", "fallible-iterator", "postgres-protocol", + "serde", + "serde_json", "uuid", ] diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index 26f7778b..95c15d20 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -19,8 +19,12 @@ serde = { version = "1.0.150", features = ["derive", "serde_derive"] } serde_yaml = "0.9.14" simple_logger = "4.0.0" tokio = { version = "1.23.0", features = ["signal", "net", "macros", "rt-multi-thread", "rt", "tokio-macros", "time", "sync", "io-util"] } -tokio-postgres = { version = "0.7.7", features = ["with-uuid-1"] } +tokio-postgres = { version = "0.7.7", features = ["with-uuid-1", "with-serde_json-1"] } tokio-serde = { version = "0.8.0", features = ["serde", "serde_cbor", "cbor"] } tokio-stream = "0.1.11" tokio-util = { version = "0.7.4", features = ["codec"] } uuid = { version = "1.2.2", features = ["v4", "serde", "rng"] } +serde_json = "1.0.91" +phf = { version = "0.11.1", features = ["macros"] } +async-trait = "0.1.60" +nom = "7.1.1" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 43b88237..edd01af7 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -7,6 +7,8 @@ use uuid::Uuid; use tokio_postgres::NoTls; use crate::message_handler::ListenerSession; use crate::DResult; +use crate::models::session::Session; +use serde_json; #[derive(Clone, Debug)] pub struct DBPool { @@ -33,7 +35,7 @@ impl From for SendqueueItem { } impl DBPool { - pub async fn record_listener_ping(self: DBPool, listener: Uuid) -> DResult<()> { + pub async fn record_listener_ping(self: &DBPool, listener: Uuid) -> DResult<()> { self.get_conn().await?.execute( "INSERT INTO listeners (listener, last_seen) \ VALUES ($1, NOW()) \ @@ -42,14 +44,14 @@ impl DBPool { Ok(()) } - pub async fn get_dead_listeners(self: Self) -> DResult> { + pub async fn get_dead_listeners(self: &Self) -> DResult> { Ok(self.get_conn().await? .query("SELECT listener FROM listeners WHERE last_seen < NOW() - \ INTERVAL '2 minutes'", &[]) .await?.into_iter().map(|r| r.get(0)).collect()) } - pub async fn cleanup_listener(self: Self, listener: Uuid) -> DResult<()> { + pub async fn cleanup_listener(self: &Self, listener: Uuid) -> DResult<()> { let mut conn = self.get_conn().await?; let tx = conn.transaction().await?; tx.execute("UPDATE users SET current_session = NULL, \ @@ -65,16 +67,16 @@ impl DBPool { Ok(()) } - pub async fn start_session(self: Self, session: &ListenerSession) -> DResult<()> { + pub async fn start_session(self: &Self, session: &ListenerSession, details: &Session) -> DResult<()> { self.get_conn().await?.execute( "INSERT INTO sessions (session, listener, details) \ - VALUES ($1, $2, '{}') ON CONFLICT (session) DO NOTHING", - &[&session.session, &session.listener] + VALUES ($1, $2, $3) ON CONFLICT (session) DO NOTHING", + &[&session.session, &session.listener, &serde_json::to_value(details)?] ).await?; Ok(()) } - pub async fn end_session(self: Self, session: ListenerSession) -> DResult<()> { + pub async fn end_session(self: &Self, session: ListenerSession) -> DResult<()> { let mut conn = self.get_conn().await?; let tx = conn.transaction().await?; tx.execute("UPDATE users SET current_session = NULL, \ @@ -88,7 +90,7 @@ impl DBPool { Ok(()) } - pub async fn queue_for_session(self: Self, + pub async fn queue_for_session(self: &Self, session: &ListenerSession, message: &str) -> DResult<()> { let conn = self.get_conn().await?; @@ -97,7 +99,7 @@ impl DBPool { Ok(()) } - pub async fn get_from_sendqueue(self: Self) -> DResult> { + pub async fn get_from_sendqueue(self: &Self) -> DResult> { let conn = self.get_conn().await?; Ok(conn.query("SELECT item, session, listener, message FROM sendqueue ORDER BY item ASC LIMIT 10", &[]) @@ -105,13 +107,13 @@ impl DBPool { } - pub async fn delete_from_sendqueue(self: DBPool, item: &SendqueueItem) -> DResult<()> { + pub async fn delete_from_sendqueue(self: &DBPool, item: &SendqueueItem) -> DResult<()> { let conn = self.get_conn().await?; conn.execute("DELETE FROM sendqueue WHERE item=$1", &[&item.item]).await?; Ok(()) } - pub async fn get_conn(self: DBPool) -> + pub async fn get_conn(self: &DBPool) -> DResult { let conn = self.pool.get().await?; conn.execute("SET synchronous_commit=off", &[]).await?; diff --git a/blastmud_game/src/main.rs b/blastmud_game/src/main.rs index 76581813..78e4e1bf 100644 --- a/blastmud_game/src/main.rs +++ b/blastmud_game/src/main.rs @@ -12,6 +12,7 @@ mod message_handler; mod version_cutover; mod av; mod regular_tasks; +mod models; pub type DResult = Result>; @@ -39,7 +40,7 @@ async fn main() -> DResult<()> { let pool = DBPool::start(&config.database_conn_string)?; // Test the database connection string works so we quit early if not... - let _ = pool.clone().get_conn().await?.query("SELECT 1", &[]).await?; + let _ = pool.get_conn().await?.query("SELECT 1", &[]).await?; info!("Database pool initialised"); @@ -53,7 +54,7 @@ async fn main() -> DResult<()> { ).await?; version_cutover::replace_old_gameserver(&config.pidfile)?; - regular_tasks::start_regular_tasks(pool.clone(), listener_map)?; + regular_tasks::start_regular_tasks(&pool, listener_map)?; let mut sigusr1 = signal(SignalKind::user_defined1())?; sigusr1.recv().await; diff --git a/blastmud_game/src/message_handler.rs b/blastmud_game/src/message_handler.rs index fcf41c93..e5a70bd5 100644 --- a/blastmud_game/src/message_handler.rs +++ b/blastmud_game/src/message_handler.rs @@ -18,13 +18,14 @@ pub async fn handle(listener: Uuid, msg: MessageFromListener, pool: db::DBPool) match msg { ListenerPing { .. } => { pool.record_listener_ping(listener).await?; } SessionConnected { session, source } => { - new_session::handle(&ListenerSession { listener, session }, &source, pool).await?; + new_session::handle( + &ListenerSession { listener, session }, source, &pool).await?; } SessionDisconnected { session } => { pool.end_session(ListenerSession { listener, session }).await?; } SessionSentLine { session, msg } => { - user_commands::handle(&ListenerSession { listener, session }, &msg, pool).await?; + user_commands::handle(&ListenerSession { listener, session }, &msg, &pool).await?; } AcknowledgeMessage => {} } diff --git a/blastmud_game/src/message_handler/new_session.rs b/blastmud_game/src/message_handler/new_session.rs index 3892a8bc..c8d9ec44 100644 --- a/blastmud_game/src/message_handler/new_session.rs +++ b/blastmud_game/src/message_handler/new_session.rs @@ -2,9 +2,11 @@ use crate::message_handler::ListenerSession; use crate::DResult; use crate::db::DBPool; use ansi_macro::ansi; +use std::default::Default; +use crate::models::session::Session; -pub async fn handle(session: &ListenerSession, _source: &str, pool: DBPool) -> DResult<()> { - pool.clone().start_session(session).await?; +pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) -> DResult<()> { + pool.start_session(session, &Session { source, ..Default::default() }).await?; pool.queue_for_session(&session, &ansi!("\ Welcome to BlastMud - a text-based post-apocalyptic \ game restricted to adults (18+)\r\n\ diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 90d4b058..c3702a4d 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -1,12 +1,78 @@ -use crate::message_handler::ListenerSession; +use super::ListenerSession; use crate::DResult; use crate::db::DBPool; use ansi_macro::ansi; +use phf::phf_map; +use async_trait::async_trait; -pub async fn handle(session: &ListenerSession, msg: &str, pool: DBPool) -> DResult<()> { +mod parsing; +mod ignore; +mod help; + +#[derive(Debug)] +pub struct VerbContext<'l> { + session: &'l ListenerSession, + pool: &'l DBPool +} + +pub enum CommandHandlingError { + UserError(String), + SystemError(Box) +} +use CommandHandlingError::*; + +#[async_trait] +pub trait UserVerb { + async fn handle(self: &Self, ctx: &VerbContext, verb: &str, remaining: &str) -> UResult<()>; +} + +pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send); +pub type UResult = Result; + +impl From> for CommandHandlingError { + fn from(input: Box) -> CommandHandlingError { + SystemError(input) + } +} + +pub fn user_error(msg: String) -> UResult { + Err(UserError(msg)) +} + +type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>; + +static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! { + "" => ignore::VERB, + "help" => help::VERB +}; + +pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> { + let (cmd, params) = parsing::parse_command_name(msg); + let handler_opt = ALWAYS_AVAILABLE_COMMANDS.get(cmd); + + match handler_opt { + None => { + pool.queue_for_session(session, + ansi!( + "That's not a command I know. Try help\r\n" + ) + ).await?; + } + Some(handler) => { + match handler.handle(&VerbContext { session, pool }, cmd, params).await { + Ok(()) => {} + Err(UserError(err_msg)) => { + pool.queue_for_session(session, &err_msg).await?; + } + Err(SystemError(e)) => Err(e)? + } + } + } + /* pool.queue_for_session(session, &format!(ansi!( "You hear an echo saying: {}\r\n" ), msg)).await?; + */ Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/help.rs b/blastmud_game/src/message_handler/user_commands/help.rs new file mode 100644 index 00000000..b182cc54 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/help.rs @@ -0,0 +1,13 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error}; +use async_trait::async_trait; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + user_error("Not implemented yet\r\n".to_string())?; + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/ignore.rs b/blastmud_game/src/message_handler/user_commands/ignore.rs new file mode 100644 index 00000000..5491adc7 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/ignore.rs @@ -0,0 +1,12 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult}; +use async_trait::async_trait; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, _ctx: &VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/parsing.rs b/blastmud_game/src/message_handler/user_commands/parsing.rs new file mode 100644 index 00000000..2a83e36d --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/parsing.rs @@ -0,0 +1,19 @@ +use nom::{ + bytes::complete::{take_till1}, + character::complete::space0, + IResult, +}; + +pub fn parse_command_name(input: &str) -> (&str, &str) { + fn parse(input: &str) -> IResult<&str, &str> { + let (input, _) = space0(input)?; + let (input, cmd) = take_till1(|c| c == ' ' || c == '\n')(input)?; + let (input, _) = space0(input)?; + Ok((input, cmd)) + } + match parse(input) { + /* This parser only fails on empty / whitespace only strings. */ + Err(_) => ("", ""), + Ok((rest, command)) => (command, rest) + } +} diff --git a/blastmud_game/src/models.rs b/blastmud_game/src/models.rs new file mode 100644 index 00000000..f52f1c4a --- /dev/null +++ b/blastmud_game/src/models.rs @@ -0,0 +1 @@ +pub mod session; diff --git a/blastmud_game/src/models/session.rs b/blastmud_game/src/models/session.rs new file mode 100644 index 00000000..015f524b --- /dev/null +++ b/blastmud_game/src/models/session.rs @@ -0,0 +1,15 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Session { + pub source: String, + pub less_explicit_mode: bool, + // Reminder: Consider backwards compatibility when updating this. New fields should generally + // be an Option, or things will crash out for existing sessions. +} + +impl Default for Session { + fn default() -> Self { + Session { source: "unknown".to_owned(), less_explicit_mode: false } + } +} diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index 966cd049..ac40cacd 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -6,8 +6,8 @@ use blastmud_interfaces::MessageToListener; use log::warn; async fn cleanup_session_once(pool: db::DBPool) -> DResult<()> { - for listener in pool.clone().get_dead_listeners().await? { - pool.clone().cleanup_listener(listener).await?; + for listener in pool.get_dead_listeners().await? { + pool.cleanup_listener(listener).await?; } Ok(()) } @@ -27,7 +27,7 @@ fn start_session_cleanup_task(pool: db::DBPool) { } async fn process_sendqueue_once(pool: db::DBPool, listener_map: ListenerMap) -> DResult<()> { - for item in pool.clone().get_from_sendqueue().await? { + for item in pool.get_from_sendqueue().await? { match listener_map.lock().await.get(&item.session.listener).map(|l| l.clone()) { None => {} Some(listener_sender) => { @@ -42,7 +42,7 @@ async fn process_sendqueue_once(pool: db::DBPool, listener_map: ListenerMap) -> } ).await.unwrap_or(()); rx.await.unwrap_or(()); - pool.clone().delete_from_sendqueue(&item).await?; + pool.delete_from_sendqueue(&item).await?; } } } @@ -63,8 +63,8 @@ fn start_send_queue_task(pool: db::DBPool, listener_map: ListenerMap) { }); } -pub fn start_regular_tasks(pool: db::DBPool, listener_map: ListenerMap) -> DResult<()> { +pub fn start_regular_tasks(pool: &db::DBPool, listener_map: ListenerMap) -> DResult<()> { start_session_cleanup_task(pool.clone()); - start_send_queue_task(pool, listener_map); + start_send_queue_task(pool.clone(), listener_map); Ok(()) }