From d4bc71e26f0a5b1cb9af61713431ca2c9eaf61c7 Mon Sep 17 00:00:00 2001 From: Shagnor Date: Mon, 26 Dec 2022 01:30:59 +1100 Subject: [PATCH] Start user registration work --- Cargo.lock | 220 +++++++++++++++++- blastmud_game/Cargo.toml | 2 + blastmud_game/src/db.rs | 29 ++- .../src/message_handler/new_session.rs | 7 +- .../src/message_handler/user_commands.rs | 37 ++- .../src/message_handler/user_commands/help.rs | 2 +- .../message_handler/user_commands/ignore.rs | 2 +- .../user_commands/less_explicit_mode.rs | 16 ++ .../message_handler/user_commands/parsing.rs | 108 ++++++++- .../src/message_handler/user_commands/quit.rs | 2 +- .../message_handler/user_commands/register.rs | 18 ++ blastmud_game/src/models.rs | 2 + blastmud_game/src/models/item.rs | 56 +++++ blastmud_game/src/models/user.rs | 119 ++++++++++ schema/schema.sql | 26 +-- 15 files changed, 607 insertions(+), 39 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/less_explicit_mode.rs create mode 100644 blastmud_game/src/message_handler/user_commands/register.rs create mode 100644 blastmud_game/src/models/item.rs create mode 100644 blastmud_game/src/models/user.rs diff --git a/Cargo.lock b/Cargo.lock index f11575ff..5ccc5c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_macro" version = "0.1.0" @@ -63,6 +72,18 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" +[[package]] +name = "bcrypt" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641" +dependencies = [ + "base64 0.13.1", + "blowfish", + "getrandom", + "zeroize", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -76,7 +97,9 @@ dependencies = [ "ansi_macro", "async-trait", "base64 0.20.0", + "bcrypt", "blastmud_interfaces", + "chrono", "deadpool", "deadpool-postgres", "futures", @@ -134,6 +157,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "buf_redux" version = "0.8.4" @@ -174,6 +207,42 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "colored" version = "2.0.0" @@ -185,6 +254,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.5" @@ -204,6 +279,50 @@ dependencies = [ "typenum", ] +[[package]] +name = "cxx" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deadpool" version = "0.9.5" @@ -414,7 +533,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -549,6 +668,30 @@ dependencies = [ "want", ] +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "idna" version = "0.3.0" @@ -569,6 +712,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -605,6 +757,15 @@ version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + [[package]] name = "lock_api" version = "0.4.9" @@ -678,7 +839,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1102,6 +1263,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + [[package]] name = "semver" version = "1.0.14" @@ -1225,7 +1392,7 @@ dependencies = [ "atty", "colored", "log", - "time", + "time 0.3.17", "windows-sys", ] @@ -1313,6 +1480,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.38" @@ -1333,6 +1509,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "time" version = "0.3.17" @@ -1580,6 +1767,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "unsafe-libyaml" version = "0.2.4" @@ -1666,6 +1859,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1752,6 +1951,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1814,3 +2022,9 @@ name = "windows_x86_64_msvc" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/blastmud_game/Cargo.toml b/blastmud_game/Cargo.toml index 34687eab..7236e116 100644 --- a/blastmud_game/Cargo.toml +++ b/blastmud_game/Cargo.toml @@ -29,3 +29,5 @@ phf = { version = "0.11.1", features = ["macros"] } async-trait = "0.1.60" nom = "7.1.1" ouroboros = "0.15.5" +chrono = { version = "0.4.23", features = ["serde"] } +bcrypt = "0.13.0" diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index f0047de1..b061107c 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -9,6 +9,7 @@ use tokio_postgres::NoTls; use crate::message_handler::ListenerSession; use crate::DResult; use crate::models::session::Session; +use crate::models::user::User; use serde_json; use futures::FutureExt; @@ -164,18 +165,34 @@ impl DBTrans { Ok(()) } - pub async fn get_session_model(self: &Self, session: &ListenerSession) -> DResult> { + pub async fn get_session_user_model(self: &Self, session: &ListenerSession) -> DResult)>> { match self.pg_trans()? - .query_opt("SELECT details FROM sessions WHERE session = $1", &[&session.session]) + .query_opt("SELECT s.details AS sess_details, \ + u.details AS user_details FROM sessions s \ + LEFT JOIN users u ON u.current_session = s.session \ + WHERE s.session = $1", &[&session.session]) .await? { None => Ok(None), Some(row) => - Ok(Some(serde_json::from_value( - row.get("details") - )?)) + Ok(Some( + (serde_json::from_value( + row.get("sess_details"))?, + match row.get::<&str, Option>("user_details") { + None => None, + Some(v) => serde_json::from_value(v)? + }) + )) } } - + + pub async fn save_session_model(self: &Self, session: &ListenerSession, details: &Session) + -> DResult<()> { + self.pg_trans()? + .execute("UPDATE sessions SET details = $1 WHERE session = $2", + &[&serde_json::to_value(details)?, &session.session]).await?; + Ok(()) + } + pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); for trans in trans_opt { diff --git a/blastmud_game/src/message_handler/new_session.rs b/blastmud_game/src/message_handler/new_session.rs index 6c4c3264..b37fb855 100644 --- a/blastmud_game/src/message_handler/new_session.rs +++ b/blastmud_game/src/message_handler/new_session.rs @@ -11,8 +11,11 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) -> Welcome to BlastMud - a text-based post-apocalyptic \ game restricted to adults (18+)\r\n\ Some commands to get you started:\r\n\ - \tregister username> password> email> to register as a new user.\r\n\ + \tregister username> password> to register as a new user.\r\n\ \tconnect username> password> to log in as an existing user.\r\n\ - \thelp to learn more.\r\n"))).await?; + \thelp to learn more.\r\n\ + [Please note BlastMud is still under development. You are welcome to play as we \ + develop it, but note it might still have bugs, unimplemented features, and \ + unbalanced gameplay aspects].\r\n"))).await?; Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 614560f8..fd7f6077 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -4,17 +4,20 @@ use crate::db::{DBTrans, DBPool}; use ansi_macro::ansi; use phf::phf_map; use async_trait::async_trait; -use crate::models::session::Session; +use crate::models::{session::Session, user::User}; use log::warn; mod parsing; mod ignore; mod help; mod quit; +mod less_explicit_mode; +mod register; pub struct VerbContext<'l> { session: &'l ListenerSession, session_dat: &'l mut Session, + user_dat: &'l mut Option, trans: &'l DBTrans } @@ -26,10 +29,9 @@ use CommandHandlingError::*; #[async_trait] pub trait UserVerb { - async fn handle(self: &Self, ctx: &VerbContext, verb: &str, remaining: &str) -> UResult<()>; + async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()>; } -pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send); pub type UResult = Result; impl From> for CommandHandlingError { @@ -42,6 +44,9 @@ pub fn user_error(msg: String) -> UResult { Err(UserError(msg)) } + +/* Verb registries list types of commands available in different circumstances. */ +pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send); type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>; static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! { @@ -50,10 +55,26 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! { "quit" => quit::VERB, }; +static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! { + "less_explicit_mode" => less_explicit_mode::VERB, + "register" => register::VERB, +}; + +fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> { + let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd); + + match ctx.user_dat { + None => { result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd)); } + Some(_) => {} + } + + result +} + pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> { let (cmd, params) = parsing::parse_command_name(msg); let trans = pool.start_transaction().await?; - let mut session_dat = match trans.get_session_model(session).await? { + let (mut session_dat, mut user_dat) = match trans.get_session_user_model(session).await? { None => { // If the session has been cleaned up from the database, there is // nowhere to go from here, so just ignore it. @@ -62,8 +83,10 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes } Some(v) => v }; - let handler_opt = ALWAYS_AVAILABLE_COMMANDS.get(cmd); - let ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat }; + + let mut ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat, + user_dat: &mut user_dat }; + let handler_opt = resolve_handler(&ctx, cmd); match handler_opt { None => { @@ -74,7 +97,7 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes ).await?; } Some(handler) => { - match handler.handle(&ctx, cmd, params).await { + match handler.handle(&mut ctx, cmd, params).await { Ok(()) => {} Err(UserError(err_msg)) => { trans.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?; diff --git a/blastmud_game/src/message_handler/user_commands/help.rs b/blastmud_game/src/message_handler/user_commands/help.rs index 8b0d0507..eb3be1ff 100644 --- a/blastmud_game/src/message_handler/user_commands/help.rs +++ b/blastmud_game/src/message_handler/user_commands/help.rs @@ -28,7 +28,7 @@ static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { pub struct Verb; #[async_trait] impl UserVerb for Verb { - async fn handle(self: &Self, ctx: &VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { let mut help = None; if !ctx.session_dat.less_explicit_mode { help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining)) diff --git a/blastmud_game/src/message_handler/user_commands/ignore.rs b/blastmud_game/src/message_handler/user_commands/ignore.rs index 5491adc7..a614072e 100644 --- a/blastmud_game/src/message_handler/user_commands/ignore.rs +++ b/blastmud_game/src/message_handler/user_commands/ignore.rs @@ -4,7 +4,7 @@ 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<()> { + async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/less_explicit_mode.rs b/blastmud_game/src/message_handler/user_commands/less_explicit_mode.rs new file mode 100644 index 00000000..30fb0bf4 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/less_explicit_mode.rs @@ -0,0 +1,16 @@ +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: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + (*ctx.session_dat).less_explicit_mode = true; + ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?; + 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 index 2a83e36d..05cb417d 100644 --- a/blastmud_game/src/message_handler/user_commands/parsing.rs +++ b/blastmud_game/src/message_handler/user_commands/parsing.rs @@ -1,13 +1,17 @@ use nom::{ - bytes::complete::{take_till1}, - character::complete::space0, + bytes::complete::{take_till1, take_while}, + character::{complete::{space0, space1, alpha1}}, + combinator::{recognize, fail, eof}, + sequence::terminated, + branch::alt, + error::{context, VerboseError, VerboseErrorKind}, 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, cmd) = take_till1(|c| c == ' ' || c == '\t')(input)?; let (input, _) = space0(input)?; Ok((input, cmd)) } @@ -17,3 +21,101 @@ pub fn parse_command_name(input: &str) -> (&str, &str) { Ok((rest, command)) => (command, rest) } } + +pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> { + const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _"; + fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> { + let (input, l1) = context("Must start with a letter", alpha1)(input)?; + let (input, l2) = context(CATCHALL_ERROR, + take_while(|c: char| c.is_alphanumeric() || c == '_'))(input)?; + if l1.len() + l2.len() > 20 { + context("Limit of 20 characters", fail::<&str, &str, VerboseError<&str>>)(input)?; + } + Ok((input, ())) + } + match terminated(recognize(parse_valid), alt((space1, eof)))(input) { + Ok((input, username)) => Ok((username, input)), + Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => + Err(e.errors.into_iter().find_map(|k| match k.1 { + VerboseErrorKind::Context(s) => Some(s), + _ => None + }).unwrap_or(CATCHALL_ERROR)), + Err(_) => Err(CATCHALL_ERROR) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_parses_normal_command() { + assert_eq!(parse_command_name("help"), + ("help", "")); + } + + #[test] + fn it_parses_normal_command_with_arg() { + assert_eq!(parse_command_name("help \t testing stuff"), + ("help", "testing stuff")); + } + + #[test] + fn it_parses_commands_with_leading_whitespace() { + assert_eq!(parse_command_name(" \t \thelp \t testing stuff"), + ("help", "testing stuff")); + } + + #[test] + fn it_parses_empty_command_names() { + assert_eq!(parse_command_name(""), + ("", "")); + assert_eq!(parse_command_name(" \t "), + ("", "")); + } + + #[test] + fn it_parses_usernames() { + assert_eq!(parse_username("Wizard123"), Ok(("Wizard123", ""))); + } + + #[test] + fn it_parses_usernames_with_further_args() { + assert_eq!(parse_username("Wizard_123 with cat"), Ok(("Wizard_123", "with cat"))); + } + + #[test] + fn it_parses_alpha_only_usernames() { + assert_eq!(parse_username("W"), Ok(("W", ""))); + } + + #[test] + fn it_fails_on_empty_usernames() { + assert_eq!(parse_username(""), Err("Must start with a letter")); + } + + #[test] + fn it_fails_on_usernames_with_invalid_start() { + assert_eq!(parse_username("#hack"), Err("Must start with a letter")); + } + + #[test] + fn it_fails_on_usernames_with_underscore_start() { + assert_eq!(parse_username("_hack"), Err("Must start with a letter")); + } + + #[test] + fn it_fails_on_usernames_with_number_start() { + assert_eq!(parse_username("31337 #"), Err("Must start with a letter")); + } + + #[test] + fn it_fails_on_usernames_with_bad_characters() { + assert_eq!(parse_username("Wizard!"), Err("Must only contain alphanumeric characters or _")); + } + + #[test] + fn it_fails_on_long_usernames() { + assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters")); + } +} diff --git a/blastmud_game/src/message_handler/user_commands/quit.rs b/blastmud_game/src/message_handler/user_commands/quit.rs index 72142b7a..0dab4614 100644 --- a/blastmud_game/src/message_handler/user_commands/quit.rs +++ b/blastmud_game/src/message_handler/user_commands/quit.rs @@ -7,7 +7,7 @@ use ansi_macro::ansi; pub struct Verb; #[async_trait] impl UserVerb for Verb { - async fn handle(self: &Self, ctx: &VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { ctx.trans.queue_for_session(ctx.session, Some(ansi!("Bye!\r\n"))).await?; ctx.trans.queue_for_session(ctx.session, None).await?; diff --git a/blastmud_game/src/message_handler/user_commands/register.rs b/blastmud_game/src/message_handler/user_commands/register.rs new file mode 100644 index 00000000..4f4bc4e3 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/register.rs @@ -0,0 +1,18 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult}; +use async_trait::async_trait; +use super::{user_error, parsing::parse_username}; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + let (username, password) = match parse_username(remaining) { + Err(e) => user_error("Invalid username: ".to_owned() + e)?, + Ok(r) => r + }; + let _pwhash = bcrypt::hash(password, 10); + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models.rs b/blastmud_game/src/models.rs index f52f1c4a..d42dd766 100644 --- a/blastmud_game/src/models.rs +++ b/blastmud_game/src/models.rs @@ -1 +1,3 @@ pub mod session; +pub mod user; +pub mod item; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs new file mode 100644 index 00000000..632925f6 --- /dev/null +++ b/blastmud_game/src/models/item.rs @@ -0,0 +1,56 @@ +use serde::{Serialize, Deserialize}; +use std::collections::BTreeMap; +use super::user::{SkillType, StatType}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum BuffCause { + WaitingTask { task_code: String, task_type: String }, + ByItem { item_code: String, item_type: String } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum BuffImpact { + ChangeStat { stat: StatType, magnitude: i16 }, + ChangeSkill { stat: StatType, magnitude: i16 } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Buff { + description: String, + cause: BuffCause, + impacts: Vec +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum Subattack { + Normal, + Powerattacking, + Feinting, + Grabbing, + Wrestling +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum LocationActionType { + Normal, + Sitting, + Reclining, + Worn, // Clothing etc... + Wielded, + Attacking(Subattack), +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Item { + pub item_code: String, + pub item_type: String, + pub location: String, // Item reference as item_type/item_code. + pub action_type: LocationActionType, + pub presence_target: Option, // e.g. what are they sitting on. + pub is_static: bool, + + pub total_xp: u64, + pub total_stats: BTreeMap, + pub total_skills: BTreeMap, + pub temporary_buffs: Vec, +} diff --git a/blastmud_game/src/models/user.rs b/blastmud_game/src/models/user.rs new file mode 100644 index 00000000..d7556b6b --- /dev/null +++ b/blastmud_game/src/models/user.rs @@ -0,0 +1,119 @@ +use serde::{Serialize, Deserialize}; +use chrono::{DateTime, Utc}; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct UserTermData { + pub accepted_terms: BTreeMap>, + pub last_presented_term: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct UserExperienceData { + pub spent_xp: u64, // Since last chargen complete. + pub completed_journals: BTreeMap>, + pub xp_change_for_this_reroll: i64, + pub crafted_items: BTreeMap +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum SkillType { + Apraise, + Blades, + Bombs, + Chemistry, + Climb, + Clubs, + Craft, + Fish, + Fists, + Flails, + Fuck, + Hack, + Locksmith, + Medic, + Persuade, + Pilot, + Pistols, + Quickdraw, + Repair, + Ride, + Rifles, + Scavenge, + Science, + Sneak, + Spears, + Swim, + Teach, + Throw, + Track, + Wrestle, + Whips +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum StatType { + Brains, + Senses, + Brawn, + Reflexes, + Endurance, + Cool +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct User { + pub username: String, + pub password_hash: String, // bcrypted. + pub player_item_id: u64, + pub registered_at: Option>, + pub banned_until: Option>, + pub abandoned_at: Option>, + pub chargen_last_completed_at: Option>, + + pub terms: UserTermData, + pub experience: UserExperienceData, + pub raw_skills: BTreeMap, + pub raw_stats: BTreeMap, + // 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 UserTermData { + fn default() -> Self { + UserTermData { + accepted_terms: BTreeMap::new(), + last_presented_term: None + } + } +} + +impl Default for UserExperienceData { + fn default() -> Self { + UserExperienceData { + spent_xp: 0, + completed_journals: BTreeMap::new(), + xp_change_for_this_reroll: 0, + crafted_items: BTreeMap::new(), + } + } +} + +impl Default for User { + fn default() -> Self { + User { + username: "unknown".to_owned(), + password_hash: "unknown".to_owned(), + player_item_id: 0, + registered_at: None, + banned_until: None, + abandoned_at: None, + chargen_last_completed_at: None, + + terms: UserTermData::default(), + experience: UserExperienceData::default(), + raw_skills: BTreeMap::new(), + raw_stats: BTreeMap::new(), + } + } +} diff --git a/schema/schema.sql b/schema/schema.sql index 331a0803..7334867a 100644 --- a/schema/schema.sql +++ b/schema/schema.sql @@ -18,15 +18,12 @@ CREATE TABLE sessions ( CREATE INDEX session_by_listener ON sessions(listener); CREATE TABLE items ( - item_id BIGINT NOT NULL PRIMARY KEY, - item_code TEXT NOT NULL, - item_type TEXT NOT NULL, - location BIGINT REFERENCES items(item_id), - details JSONB NOT NULL, - UNIQUE (item_code, item_type) + item_id BIGSERIAL NOT NULL PRIMARY KEY, + details JSONB NOT NULL ); -CREATE INDEX item_index ON items (item_code, item_type); -CREATE INDEX item_by_loc ON items (location); +CREATE UNIQUE INDEX item_index ON items ((details->>'item_code'), (details->>'item_type')); +CREATE INDEX item_by_loc ON items ((details->>'location')); +CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean))); CREATE TABLE users ( username TEXT NOT NULL PRIMARY KEY, @@ -35,6 +32,7 @@ CREATE TABLE users ( details JSONB NOT NULL ); CREATE INDEX user_by_listener ON users(current_listener); +CREATE INDEX user_by_session ON users(current_session); CREATE UNLOGGED TABLE sendqueue ( item BIGSERIAL NOT NULL PRIMARY KEY, @@ -44,11 +42,9 @@ CREATE UNLOGGED TABLE sendqueue ( ); CREATE TABLE tasks ( - task_code TEXT NOT NULL, - task_type TEXT NOT NULL, - is_static BOOL NOT NULL, - next_scheduled TIMESTAMP WITH TIME ZONE NOT NULL, - details JSONB NOT NULL, - PRIMARY KEY (task_code, task_type) + task_id BIGSERIAL NOT NULL PRIMARY KEY, + details JSONB NOT NULL ); -CREATE INDEX task_by_next_scheduled ON tasks(next_scheduled); +CREATE UNIQUE INDEX tasks_by_code_type ON tasks((details->>'task_code'), (details->>'task_type')); +CREATE INDEX tasks_by_static ON tasks((cast(details->>'is_static' as boolean))); +CREATE INDEX tasks_by_scheduled ON tasks((details->>'next_scheduled'));