blastmud/blastmud_game/src/message_handler/user_commands.rs

431 lines
10 KiB
Rust

use super::ListenerSession;
#[double]
use crate::db::DBTrans;
use crate::db::{DBPool, ItemSearchParams};
use crate::models::user::UserFlag;
use crate::models::{item::Item, session::Session, user::User};
use crate::DResult;
#[cfg(not(test))]
use ansi::ansi;
use async_trait::async_trait;
#[cfg(not(test))]
use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell;
use phf::phf_map;
use std::sync::Arc;
mod agree;
mod allow;
pub mod attack;
mod butcher;
mod buy;
mod c;
pub mod close;
pub mod corp;
pub mod cut;
pub mod delete;
mod describe;
pub mod drink;
pub mod drop;
pub mod eat;
pub mod fill;
mod fire;
pub mod follow;
mod gear;
pub mod get;
mod help;
pub mod hire;
mod ignore;
pub mod improvise;
mod install;
mod inventory;
mod less_explicit_mode;
mod list;
pub mod load;
mod login;
mod look;
pub mod make;
mod map;
pub mod movement;
pub mod open;
mod page;
pub mod parsing;
pub mod put;
mod quit;
pub mod recline;
pub mod register;
pub mod remove;
pub mod rent;
mod report;
mod reset_spawns;
pub mod say;
mod score;
mod sign;
pub mod sit;
pub mod stand;
mod status;
mod uninstall;
pub mod use_cmd;
mod vacate;
pub mod wear;
mod whisper;
mod who;
pub mod wield;
mod write;
pub struct VerbContext<'l> {
pub session: &'l ListenerSession,
pub session_dat: &'l mut Session,
pub user_dat: &'l mut Option<User>,
pub trans: &'l DBTrans,
}
pub enum CommandHandlingError {
UserError(String),
SystemError(Box<dyn std::error::Error + Send + Sync>),
}
pub use CommandHandlingError::*;
#[async_trait]
pub trait UserVerb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str)
-> UResult<()>;
}
pub type UResult<A> = Result<A, CommandHandlingError>;
impl<T> From<T> for CommandHandlingError
where
T: Into<Box<dyn std::error::Error + Send + Sync>>,
{
fn from(input: T) -> CommandHandlingError {
SystemError(input.into())
}
}
pub fn user_error<A>(msg: String) -> UResult<A> {
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! {
"" => ignore::VERB,
"help" => help::VERB,
"quit" => quit::VERB,
};
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"agree" => agree::VERB,
"connect" => login::VERB,
"less_explicit_mode" => less_explicit_mode::VERB,
"login" => login::VERB,
"register" => register::VERB,
};
static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
// Movement comments first:
"north" => movement::VERB,
"n" => movement::VERB,
"northeast" => movement::VERB,
"ne" => movement::VERB,
"east" => movement::VERB,
"e" => movement::VERB,
"southeast" => movement::VERB,
"se" => movement::VERB,
"south" => movement::VERB,
"s" => movement::VERB,
"southwest" => movement::VERB,
"sw" => movement::VERB,
"west" => movement::VERB,
"w" => movement::VERB,
"northwest" => movement::VERB,
"nw" => movement::VERB,
"up" => movement::VERB,
"down" => movement::VERB,
"in" => movement::VERB,
// Other commands (alphabetical except aliases grouped):
"allow" => allow::VERB,
"disallow" => allow::VERB,
"attack" => attack::VERB,
"butcher" => butcher::VERB,
"buy" => buy::VERB,
"c" => c::VERB,
"close" => close::VERB,
"corp" => corp::VERB,
"cut" => cut::VERB,
"delete" => delete::VERB,
"drink" => drink::VERB,
"drop" => drop::VERB,
"eat" => eat::VERB,
"fill" => fill::VERB,
"fire" => fire::VERB,
"follow" => follow::VERB,
"unfollow" => follow::VERB,
"gear" => gear::VERB,
"get" => get::VERB,
"hire" => hire::VERB,
"improv" => improvise::VERB,
"improvise" => improvise::VERB,
"improvize" => improvise::VERB,
"install" => install::VERB,
"inventory" => inventory::VERB,
"inv" => inventory::VERB,
"i" => inventory::VERB,
"kill" => attack::VERB,
"k" => attack::VERB,
"describe" => describe::VERB,
"l" => look::VERB,
"look" => look::VERB,
"read" => look::VERB,
"list" => list::VERB,
"load" => load::VERB,
"unload" => load::VERB,
"lm" => map::VERB,
"lmap" => map::VERB,
"gm" => map::VERB,
"gmap" => map::VERB,
"make" => make::VERB,
"open" => open::VERB,
"p" => page::VERB,
"page" => page::VERB,
"pg" => page::VERB,
"rep" => page::VERB,
"repl" => page::VERB,
"reply" => page::VERB,
"put" => put::VERB,
"recline" => recline::VERB,
"remove" => remove::VERB,
"rent" => rent::VERB,
"report" => report::VERB,
"\'" => say::VERB,
"say" => say::VERB,
"sc" => score::VERB,
"score" => score::VERB,
"sign" => sign::VERB,
"sit" => sit::VERB,
"stand" => stand::VERB,
"st" => status::VERB,
"stat" => status::VERB,
"status" => status::VERB,
"uninstall" => uninstall::VERB,
"use" => use_cmd::VERB,
"vacate" => vacate::VERB,
"-" => whisper::VERB,
"whisper" => whisper::VERB,
"tell" => whisper::VERB,
"wear" => wear::VERB,
"wield" => wield::VERB,
"who" => who::VERB,
"write" => write::VERB,
};
static STAFF_COMMANDS: UserVerbRegistry = phf_map! {
"staff_reset_spawns" => reset_spawns::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(user_dat) => {
if user_dat.terms.terms_complete {
result = result.or_else(|| REGISTERED_COMMANDS.get(cmd));
if user_dat.user_flags.contains(&UserFlag::Staff) {
result = result.or_else(|| STAFF_COMMANDS.get(cmd));
}
} else if cmd == "agree" {
result = Some(&agree::VERB);
}
}
}
result
}
#[cfg(not(test))]
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, 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.
warn!(
"Got command from session not in database: {}",
session.session
);
return Ok(());
}
Some(v) => v,
};
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 => {
trans
.queue_for_session(
session,
Some(ansi!(
"That's not a command I know. Try <bold>help<reset>\r\n"
)),
)
.await?;
trans.commit().await?;
}
Some(handler) => match handler.handle(&mut ctx, cmd, params).await {
Ok(()) => {
trans.commit().await?;
}
Err(UserError(err_msg)) => {
pool.queue_for_session(session, Some(&(err_msg + "\r\n")))
.await?;
}
Err(SystemError(e)) => Err(e)?,
},
}
pool.bump_session_time(&session).await?;
Ok(())
}
#[cfg(test)]
pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> {
unimplemented!();
}
pub fn is_likely_explicit(msg: &str) -> bool {
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
let markers = EXPLICIT_MARKER_WORDS.get_or_init(|| {
vec![
"fuck", "sex", "cock", "cunt", "dick", "pussy", "whore", "orgasm", "erection",
"nipple", "boob", "tit",
]
});
for word in markers {
if msg.contains(word) {
return true;
}
}
false
}
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
ctx.user_dat
.as_ref()
.ok_or_else(|| UserError("Not logged in".to_owned()))
}
pub fn get_user_or_fail_mut<'l>(ctx: &'l mut VerbContext) -> UResult<&'l mut User> {
ctx.user_dat
.as_mut()
.ok_or_else(|| UserError("Not logged in".to_owned()))
}
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
Ok(ctx
.trans
.find_item_by_type_code("player", &get_user_or_fail(ctx)?.username.to_lowercase())
.await?
.ok_or_else(|| {
UserError(
"Your character is gone, you'll need to re-register or ask an admin".to_owned(),
)
})?)
}
pub async fn search_item_for_user<'l>(
ctx: &'l VerbContext<'l>,
search: &'l ItemSearchParams<'l>,
) -> UResult<Arc<Item>> {
Ok(
match &ctx
.trans
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error(format!(
"Sorry, I couldn't find anything matching \"{}\".",
search.query
))?,
[match_it] => match_it.clone(),
[item1, ..] => item1.clone(),
},
)
}
pub async fn search_items_for_user<'l>(
ctx: &'l VerbContext<'l>,
search: &'l ItemSearchParams<'l>,
) -> UResult<Vec<Arc<Item>>> {
Ok(
match &ctx
.trans
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error(format!(
"Sorry, I couldn't find anything matching \"{}\".",
search.query
))?,
v => v.into_iter().map(|it| it.clone()).collect(),
},
)
}
#[cfg(test)]
mod test {
use crate::db::MockDBTrans;
#[test]
fn resolve_handler_finds_unregistered() {
use super::*;
let trans = MockDBTrans::new();
let sess: ListenerSession = Default::default();
let mut user_dat: Option<User> = None;
let mut session_dat: Session = Default::default();
let ctx = VerbContext {
session: &sess,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_dat,
};
assert_eq!(resolve_handler(&ctx, "less_explicit_mode").is_some(), true);
}
}