blastmud/blastmud_game/src/message_handler/user_commands.rs
Condorra 19cef2d9c4 Allow selling in stores, with Josephine special behaviour
Also added a staff invincible mode to help clean out NPCs with wrong
inventory.
2024-02-26 22:35:55 +11:00

569 lines
14 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::ListenerSession;
#[cfg(not(test))]
use crate::db::is_concurrency_error;
#[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;
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;
mod feint;
pub mod fill;
mod fire;
pub mod follow;
mod gear;
pub mod get;
mod hack;
mod help;
pub mod hire;
mod ignore;
pub mod improvise;
mod install;
mod inventory;
mod invincible;
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 pay;
pub mod plug;
mod pow;
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 scan;
pub mod scavenge;
mod score;
mod sell;
mod share;
mod sign;
pub mod sit;
mod staff_show;
pub mod stand;
mod status;
mod stop;
mod turn;
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,
"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,
"hack" => hack::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,
"examine" => look::VERB,
"ex" => look::VERB,
"feint" => feint::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,
"pay" => pay::VERB,
"plug" => plug::VERB,
"pow" => pow::VERB,
"power" => pow::VERB,
"powerattack" => pow::VERB,
"put" => put::VERB,
"recline" => recline::VERB,
"remove" => remove::VERB,
"rent" => rent::VERB,
"report" => report::VERB,
"\'" => say::VERB,
"say" => say::VERB,
"scan" => scan::VERB,
"scavenge" => scavenge::VERB,
"search" => scavenge::VERB,
"sc" => score::VERB,
"score" => score::VERB,
"sell" => sell::VERB,
"share" => share::VERB,
"serious" => share::VERB,
"amicable" => share::VERB,
"joking" => share::VERB,
"parody" => share::VERB,
"play" => share::VERB,
"thoughts" => share::VERB,
"exploring" => share::VERB,
"roaming" => share::VERB,
"fishing" => share::VERB,
"good" => share::VERB,
"surviving" => share::VERB,
"slow" => share::VERB,
"normal" => share::VERB,
"intense" => share::VERB,
"sign" => sign::VERB,
"sit" => sit::VERB,
"stand" => stand::VERB,
"st" => status::VERB,
"stat" => status::VERB,
"stats" => status::VERB,
"status" => status::VERB,
"stop" => stop::VERB,
"turn" => turn::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_invincible" => invincible::VERB,
"staff_reset_spawns" => reset_spawns::VERB,
"staff_show" => staff_show::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_in_trans(
session: &ListenerSession,
msg: &str,
pool: &DBPool,
trans: DBTrans,
) -> DResult<()> {
let (cmd, params) = parsing::parse_command_name(msg);
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)?,
},
}
Ok(())
}
#[cfg(not(test))]
pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> {
loop {
let trans = pool.start_transaction().await?;
match handle_in_trans(session, msg, pool, trans).await {
Ok(_) => break,
Err(e) => {
if is_concurrency_error(e.as_ref()) {
continue;
} else {
return 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_illegal(msg: &str) -> bool {
static ILLEGAL_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
let illegal_markers = ILLEGAL_MARKER_WORDS.get_or_init(|| {
vec![
"lolita",
"jailbait",
"abu sayyaf",
"al-qaida",
"al-qaida",
"al-shabaab",
"boko haram",
"hamas",
"tahrir al-sham",
"hizballah",
"hezbollah",
"hurras al-din",
"islamic state",
"jaish-e-mohammad",
"jamaat mujahideen",
"jamaat mujahideen",
"jamaat nusrat",
"jamaat nusrat",
"jemaah islamiyah",
"kurdistan workers",
"lashkar-e-tayyiba",
"likud",
"national socialist order",
"palestinian islamic jihad",
"sonnenkrieg",
"race war",
// For now we'll block all URLs - we could allow by domain perhaps?
"http:",
"https:",
"ftp:",
]
});
static MINOR_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
let minor_markers =
MINOR_MARKER_WORDS.get_or_init(|| vec!["young", "underage", "child", "teen", "minor"]);
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
let explicit_markers = EXPLICIT_MARKER_WORDS.get_or_init(|| {
vec![
"fuck",
"sex",
"cock",
"cunt",
"dick",
"pussy",
"whore",
"orgasm",
"erection",
"nipple",
"boob",
"tit",
"xxx",
"nsfw",
"uncensored",
]
});
let msg_lower = msg.to_lowercase();
for word in illegal_markers {
if msg_lower.contains(word) {
return true;
}
}
let mut minor_word = false;
let mut explicit_word = false;
for word in minor_markers {
if msg_lower.contains(word) {
minor_word = true;
}
}
for word in explicit_markers {
if msg_lower.contains(word) {
explicit_word = true;
}
}
explicit_word && minor_word
}
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, "agree").is_some(), true);
}
}