Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

211 changed files with 7445 additions and 45817 deletions

View File

@ -1,20 +0,0 @@
#!/bin/bash -e
set -Eeuo pipefail
export HOME=$(pwd)
export CARGO_HOME=$(pwd)/.cargo
cd blastmud-repo
BUILDSTAMP=$(date +%s)
echo Build timestamp for binary names: $BUILDSTAMP
echo Running tests
cargo test --target-dir ../target --profile release
echo Running main build
cargo build --target-dir ../target --profile release
echo Copying artifacts
echo $BUILDSTAMP >../binaries/buildstamp
cp ../target/release/blastmud_game ../binaries/
cp ../target/release/blastmud_listener ../binaries/

View File

@ -1,47 +0,0 @@
#!/bin/bash
set -Eeu
DEPLOY_HOST=172.19.11.5
mkdir -p ~/.ssh
cat >>~/.ssh/known_hosts <<END
172.19.11.5 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCm71djj4D0EX/rW9p0d4t/gIoUMK8kMJEs71KadFUu3Ij2BHqbS3U8G3sjm6qm7nU7c0QD1WNyzLbCp23qroobS2/TOjvewZMe5Gs4iGBmE5VLJHY/hD0AKHermcrv1z7KbLnLaCgqQdGdzqcPs07Z9BdbHBDHq57+VJsIQ9BCt67GevppPyV6sIQX9h6aoLo4179vwQ9tC7fPcp8GzO7PTcixMhWGF0A12YcMxA3OR5q3GoeQxGZ3wRXg7avNJhHuAI3tWIA5VUcn/DTuRC16ndSVuyxous/L+jxNuk7wDIDXRPuOin7edoJ7s4ZXxV7EWPKANZzzmStQzeLX7ew9K6uF1BNmzZArK03ts8H/h6Q/O8KB1/oCnAtpoCMHfDM2AiF9SOAa9S6yBmYFAGXgLV+BdnbZnIpOIE6zyPv3k/c9zzFW7ZoQ9V7VWomCX7FxL6uwoaBoVAeCSVkfrxFoXnzIyrcaGXBUlWftTKpB0jqCdJ/Eifquj9ImIE5CcNk=
172.19.11.5 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBC5PjzIMikn+AJWyvge7DqFsdMarG9CVDov9ITbDwNwucoeEUlNoA3hypyrBeatyRL3Y+jWtPV6uwzlRTKye/BY=
172.19.11.5 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEi7lEzVWCLed6VZgnIpfXF7+bugZ6uZwH2hOkIgtfAv
END
chmod 0600 ~/.ssh/known_hosts
echo "$DEPLOY_KEY" >deploy-key
chmod 0600 ./deploy-key
BUILDSTAMP=$(cat binaries/buildstamp)
tar -cvJf binary-archive-$BUILDSTAMP.tar.xz -C binaries blastmud_game blastmud_listener
scp -i deploy-key binary-archive-$BUILDSTAMP.tar.xz blast@$DEPLOY_HOST:/home/blast/archive/
ssh -i deploy-key blast@$DEPLOY_HOST "find /home/blast/archive -mtime +30 -delete && mkdir -p /home/blast/latest && tar -xvJf /home/blast/archive/binary-archive-$BUILDSTAMP.tar.xz -C /home/blast/latest"
(curl --max-time 10 https://ws.blastmud.org/version || true) >version
cd blastmud-repo
ROOT_COMMIT=$(git rev-list HEAD | tail -n 1)
cd ..
LISTENER_VERSION=$(jq -r .listener_version version || echo ROOT_COMMIT)
GAMESERVER_VERSION=$(jq -r .gameserver_version version || echo ROOT_COMMIT)
cd blastmud-repo
CURRENT_VERSION=$(git rev-parse HEAD)
LISTENER_CHANGES=$(git diff $LISTENER_VERSION $CURRENT_VERSION -- blastmud_listener blastmud_interfaces | wc -l)
GAMESERVER_CHANGES=$(git diff $GAMESERVER_VERSION $CURRENT_VERSION -- blastmud_game blastmud_interfaces | wc -l)
cd ..
echo Listener changes: $LISTENER_CHANGES Gameserver changes: $GAMESERVER_CHANGES
if [[ $GAMESERVER_CHANGES != "0" ]]; then
echo Deploying gameserver
ssh -i deploy-key blast@$DEPLOY_HOST "cp /home/blast/latest/blastmud_game /mnt/gameserver-app/tmp && mv /mnt/gameserver-app/tmp/blastmud_game /mnt/gameserver-app/exe"
else
echo No changes to gameserver, skipping.
fi
if [[ $LISTENER_CHANGES != "0" ]]; then
echo Deploying listener
ssh -i deploy-key blast@$DEPLOY_HOST "cp /home/blast/latest/blastmud_listener /mnt/listener-app/tmp && mv /mnt/listener-app/tmp/blastmud_listener /mnt/listener-app/exe"
else
echo No changes to listener, skipping.
fi

View File

@ -1,2 +0,0 @@
FROM debian:latest
RUN apt-get update && apt-get install -y jq ssh git curl ca-certificates xz-utils && rm -rf /var/lib/apt/lists/*

View File

@ -1 +0,0 @@
A base image for running our deploys. It is used by CI, but isn't auto-deployed by it.

View File

@ -1,2 +0,0 @@
docker build -t blasthavers/deploy-base:latest .
docker push blasthavers/deploy-base:latest

View File

@ -1,2 +0,0 @@
#!/bin/sh
exec fly -t bm set-pipeline -c .concourse.yml -p blastmud-build

View File

@ -1,47 +0,0 @@
jobs:
- name: build-blastmud-image
public: true
build_log_retention:
days: 365
builds: 50
plan:
- get: blastmud-repo
trigger: true
- task: build
config:
platform: linux
image_resource:
type: registry-image
source:
repository: rust
inputs:
- name: blastmud-repo
caches:
- path: target
- path: .cargo
outputs:
- name: binaries
run:
path: blastmud-repo/.ci/build
- task: deploy
config:
platform: linux
image_resource:
type: registry-image
source:
repository: blasthavers/deploy-base
inputs:
- name: binaries
- name: blastmud-repo
params:
DEPLOY_KEY: ((deploy_key))
run:
path: blastmud-repo/.ci/deploy
resources:
- name: blastmud-repo
type: git
check_every: never
webhook_token: ((webhook_token))
source:
uri: https://git.blastmud.org/blasthavers/blastmud.git
branch: main

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
/target
config
docs/private
*~

1605
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,16 +4,6 @@ members = [
"blastmud_listener",
"blastmud_interfaces",
"blastmud_game",
"ansi_markup",
"ansi_macro",
"ansi",
]
resolver = "2"
[profile.release-with-debug]
inherits = "release"
debug = true
[profile.dev]
lto = false

View File

@ -6,6 +6,40 @@ core written in Rust rather than in any form of softcode. Only user data forms p
Even the map is programmed in a normal text editor, and can be
tested locally before being deployed to the game.
# Age verification file
The Blastmud game is for adults only (18 years of age or older). In order to make a complete game, three
components are required:
* This game server codebase - which is publicly available and shareable under a permissive (3-clause BSD-style license). It isn't playable as a game by itself.
* A client. Openly available software such as telnet, tintin++, or mudlet can be used as this component.
* A closed-source age verification file, to be placed as `age-verification.yml` alongside the `gameserver.conf` file. This file is copyrighted with all rights reserved (except for the use by the person it is intended for to run the game) and cannot legally be given to anyone else. The initial author of Blastmud intends to provide an `age-verification.yml` file to anyone I am satisfied is not a minor.
## Why does a Free/Open Source project deliberately include a requirement for a non-Open Source file?
In the jurisdiction where the initial author is based, it is illegal to distribute unclassified or R18+ classified games (defined as playable software / data / some combination of it) to people under 18. Restricting access to all components of the game would be an impediment for easy collaboration on the game.
So a decision was made to only distribute a non-playable Free / Open Source component without restrictions (and to ensure this non-playable component doesn't, by itself, meet the definition of either a computer game or a submittable publication).
## I obtained an `age-verification.yml` from the initial author - can I share it / publish it?
No, this file is licensed solely to you and it is a breach of copyright law to publish it without consent from the initial author. A takedown request for the material might be sent, the shared `age-verification.yml` might be revoked in future versions of the server codebase, and you could even be sued for copyright infringement.
Depending on your jurisdiction, publishing a complete game (including `age-verification.yml`) to people who are under 18 could also be a crime.
If you attempt to use the official Blastmud GitHub project (or any other resources) to share `age-verification.yml` (e.g. through issues or pull requests), the material will be deleted and you will be blocked from further interaction with the project (unless we are satisfied it was accidental).
You are allowed to put it on a computer system / server where it is only accessible to a limited number of people known to you, as long as you have verified all those people are 18 or over, and know not to further distribute the file.
## Can I change / remove the code so it doesn't need `age-verification.yml`?
The license for Blastmud allows you to change the code and redistribute your changes. If you are forking Blastmud to create your own game engine, you could change
the age verification keypair or entirely remove the code. You may not call such a modified game Blastmud. Please be aware that if
you modify the code to create a complete computer game, in some jurisdictions you might have to get your fork classified, and might
have legal obligations not to distribute it to anyone under a certain age.
Regarding the use of official Blastmud resources such as our GitHub project and game server instance: to ensure minors are protected, you must not post versions of Blastmud that disable the checking of `age-verification.yml` (or post any other complete unclassified game or game that is unsuitable for minors of any age), nor post patches, pull requests, or instructions for doing the same. You may be blocked from further interaction with the project if you do this (unless we are satisfied it was accidental).
# Architecture
Blastmud consists of the following main components:
@ -16,7 +50,7 @@ Blastmud consists of the following main components:
# Status
Blastmud is under active development, but is currently playable.
Blastmud is not yet playable, it is under development.
# Schema management
We only keep the latest version in version control, and use migra (pip3 install migra) to identify changes between
@ -27,6 +61,6 @@ The latest schema is under `schema`.
Create a user with a secret password, and username `blast`. Create a production database called `blast`.
To get to the latest schema:
* Run `psql -d template1 <schema/schema.sql` to create the temporary `blast_schemaonly` database.
* Run `migra "postgresql:///blast" "postgresql:///blast_schemaonly" > /tmp/update.sql`
* Check `/tmp/update.sql` and if it looks good, apply it with `psql -U blast -d blast </tmp/update.sql`
* Run `psql <schema/schema.sql` to create the temporary `blast_schemaonly` database.
* Run `migra "postgres:///blast" "postgres:///blast_schemaonly" > /tmp/update.sql`
* Check `/tmp/update.sql` and if it looks good, apply it with `psql -u blast -d blast </tmp/update.sql`

View File

@ -5,4 +5,3 @@ edition = "2021"
[dependencies]
ansi_macro = { path = "../ansi_macro" }
nom = "7.1.3"

View File

@ -6,10 +6,8 @@ use std::rc::Rc;
/// escape - so use this for untrusted input that you don't expect
/// to contain ansi escapes at all.
pub fn ignore_special_characters(input: &str) -> String {
input
.chars()
.filter(|c| *c == '\t' || *c == '\n' || (*c >= ' ' && *c <= '~'))
.collect()
input.chars().filter(|c| *c == '\t' || *c == '\n' ||
(*c >= ' ' && *c <= '~')).collect()
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -24,35 +22,26 @@ struct AnsiState {
impl AnsiState {
fn restore_ansi(self: &Self) -> String {
let mut buf = String::new();
if !(self.bold
&& self.underline
&& self.strike
&& self.background != 0
&& self.foreground != 0)
{
if !(self.bold && self.underline && self.strike &&
self.background != 0 && self.foreground != 0) {
buf.push_str(ansi!("<reset>"));
}
if self.bold {
buf.push_str(ansi!("<bold>"));
}
if self.underline {
buf.push_str(ansi!("<under>"));
}
if self.strike {
buf.push_str(ansi!("<strike>"));
}
if self.bold { buf.push_str(ansi!("<bold>")); }
if self.underline { buf.push_str(ansi!("<under>")); }
if self.strike { buf.push_str(ansi!("<strike>")); }
if self.background != 0 {
buf.push_str(&format!("\x1b[{}m", 39 + self.background));
}
buf.push_str(&format!("\x1b[{}m", 39 + self.background)); }
if self.foreground != 0 {
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground));
}
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground)); }
buf
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct AnsiEvent<'l>(AnsiParseToken<'l>, Rc<AnsiState>);
struct AnsiEvent<'l> (
AnsiParseToken<'l>,
Rc<AnsiState>
);
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum AnsiParseToken<'l> {
@ -74,20 +63,20 @@ struct AnsiIterator<'l> {
inject_spaces: u64,
}
impl AnsiIterator<'_> {
fn new<'l>(input: &'l str) -> AnsiIterator<'l> {
AnsiIterator {
underlying: input.chars().enumerate(),
AnsiIterator { underlying: input.chars().enumerate(),
input: input,
state: Rc::new(AnsiState {
background: 0,
foreground: 0,
bold: false,
underline: false,
strike: false,
strike: false
}),
pending_col: false,
inject_spaces: 0,
inject_spaces: 0
}
}
}
@ -102,10 +91,7 @@ impl<'l> Iterator for AnsiIterator<'l> {
if self.inject_spaces > 0 {
self.pending_col = true;
self.inject_spaces -= 1;
return Some(AnsiEvent::<'l>(
AnsiParseToken::Character(' '),
self.state.clone(),
));
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
}
while let Some((i0, c)) = self.underlying.next() {
if c == '\n' {
@ -114,17 +100,11 @@ impl<'l> Iterator for AnsiIterator<'l> {
for _ in 0..4 {
self.pending_col = true;
self.inject_spaces = 3;
return Some(AnsiEvent::<'l>(
AnsiParseToken::Character(' '),
self.state.clone(),
));
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
}
} else if c >= ' ' && c <= '~' {
self.pending_col = true;
return Some(AnsiEvent::<'l>(
AnsiParseToken::Character(c),
self.state.clone(),
));
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(c), self.state.clone()));
} else if c == '\x1b' {
if let Some((_, c2)) = self.underlying.next() {
if c2 != '[' {
@ -145,9 +125,7 @@ impl<'l> Iterator for AnsiIterator<'l> {
cs_no *= 10;
cs_no += cs_no2;
imax = i3;
} else {
continue;
}
} else { continue; }
}
} else if cs2 != 'm' {
continue;
@ -163,36 +141,30 @@ impl<'l> Iterator for AnsiIterator<'l> {
st.underline = false;
st.strike = false;
}
1 => {
st.bold = true;
}
4 => {
st.underline = true;
}
9 => {
st.strike = true;
}
24 => {
st.underline = false;
}
1 => { st.bold = true; }
4 => { st.underline = true; }
9 => { st.strike = true; }
24 => { st.underline = false; }
i if i >= 30 && i <= 37 => {
st.foreground = i as u64 - 29;
}
i if i >= 40 && i <= 47 => {
st.foreground = i as u64 - 39;
}
_ => continue,
_ => continue
}
drop(st);
return Some(AnsiEvent::<'l>(
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
self.state.clone(),
));
AnsiParseToken::ControlSeq(
&self.input[i0..(imax + 1)]
), self.state.clone()));
}
}
}
}
None
}
}
/// Strips out basic colours / character formatting codes cleanly. Tabs are
@ -221,7 +193,7 @@ pub fn limit_special_characters(input: &str) -> String {
match e {
AnsiParseToken::Character(c) => buf.push(c),
AnsiParseToken::Newline => buf.push('\n'),
AnsiParseToken::ControlSeq(t) => buf.push_str(t),
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
}
}
buf
@ -229,13 +201,8 @@ pub fn limit_special_characters(input: &str) -> String {
/// Flows a second column around a first column, limiting the width of both
/// columns as specified, and adding a gutter.
pub fn flow_around(
col1: &str,
col1_width: usize,
gutter: &str,
col2: &str,
col2_width: usize,
) -> String {
pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
col2: &str, col2_width: usize) -> String {
let mut it1 = AnsiIterator::new(col1).peekable();
let mut it2 = AnsiIterator::new(col2).peekable();
@ -245,7 +212,7 @@ pub fn flow_around(
'around_rows: loop {
match it1.peek() {
None => break 'around_rows,
Some(AnsiEvent(_, st)) => buf.push_str(&st.restore_ansi()),
Some(AnsiEvent(_, st)) => buf.push_str(&st.restore_ansi())
}
let mut fill_needed: usize = 0;
let mut skip_nl = true;
@ -277,9 +244,7 @@ pub fn flow_around(
None => break,
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
if fill_needed > 0 {
buf.push_str(s);
}
if fill_needed > 0 { buf.push_str(s); }
it1.next();
}
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
@ -289,9 +254,7 @@ pub fn flow_around(
}
}
}
for _ in 0..fill_needed {
buf.push(' ');
}
for _ in 0..fill_needed { buf.push(' '); }
buf.push_str(gutter);
@ -322,9 +285,7 @@ pub fn flow_around(
None => break,
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
if fill_needed > 0 {
buf.push_str(s);
}
if fill_needed > 0 { buf.push_str(s); }
it2.next();
}
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
@ -342,7 +303,7 @@ pub fn flow_around(
match e {
AnsiParseToken::Character(c) => buf.push(c),
AnsiParseToken::Newline => buf.push('\n'),
AnsiParseToken::ControlSeq(t) => buf.push_str(t),
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
}
}
@ -354,9 +315,7 @@ fn is_wrappable(c: char) -> bool {
}
pub fn word_wrap<F>(input: &str, limit: F) -> String
where
F: Fn(usize) -> usize,
{
where F: Fn(usize) -> usize {
let mut it_main = AnsiIterator::new(input);
let mut start_word = true;
let mut row: usize = 0;
@ -379,12 +338,9 @@ where
let fits = 'check_fits: loop {
match it_lookahead.next() {
None => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
break 'check_fits true
}
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
break 'check_fits is_wrappable(c)
}
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
break 'check_fits is_wrappable(c),
_ => {}
}
};
@ -405,13 +361,9 @@ where
}
continue;
}
assert!(
col <= limit(row),
assert!(col <= limit(row),
"col must be below limit, but found c={}, col={}, limit={}",
c,
col,
limit(row)
);
c, col, limit(row));
if !start_word {
if col == limit(row) {
// We are about to hit the limit, and we need to decide
@ -420,12 +372,9 @@ where
let fits = 'check_fits: loop {
match it_lookahead.next() {
None => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
break 'check_fits true
}
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
break 'check_fits is_wrappable(c)
}
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
break 'check_fits is_wrappable(c),
_ => {}
}
};
@ -480,7 +429,7 @@ where
buf.push('\n');
start_word = true;
}
Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t),
Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t)
}
}
@ -501,26 +450,24 @@ mod test {
assert_eq!(strip_special_characters("a\tb"), "a b");
assert_eq!(
strip_special_characters(ansi!("<red>hello<green>world")),
"helloworld"
);
"helloworld");
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n"
);
"helloworld\n");
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n"
);
assert_eq!(strip_special_characters("Test\x1b[5;5fing"), "Test5fing");
"helloworld\n");
assert_eq!(
strip_special_characters("Test\x1b[5;5fing"),
"Test5fing");
}
#[test]
fn limit_special_characters_strips_some_things() {
assert_eq!(
limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
ansi!("a<bgred><green>b<bggreen><red>c<reset>d")
);
assert_eq!(limit_special_characters("Test\x1b[5;5fing"), "Test5fing");
assert_eq!(limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
ansi!("a<bgred><green>b<bggreen><red>c<reset>d"));
assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
"Test5fing");
}
#[test]
@ -590,4 +537,5 @@ mod test {
- -testing";
assert_eq!(word_wrap(unwrapped, |_| 10), wrapped);
}
}

View File

@ -7,7 +7,6 @@ edition = "2021"
proc-macro = true
[dependencies]
nom = "7.1.3"
quote = "1.0.35"
syn = "2.0.52"
ansi_markup = { path = "../ansi_markup" }
nom = "7.1.1"
quote = "1.0.23"
syn = "1.0.107"

View File

@ -1,17 +1,64 @@
use ansi_markup::parse_ansi_markup;
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, Lit};
use quote::ToTokens;
use nom::{
combinator::eof,
branch::alt, multi::fold_many0,
bytes::complete::{take_till, take_till1, tag},
sequence::{tuple, pair},
error::Error,
Err,
Parser
};
enum AnsiFrag<'l> {
Lit(&'l str),
Special(&'l str)
}
use AnsiFrag::Special;
#[proc_macro]
pub fn ansi(input: TokenStream) -> TokenStream {
let raw = match parse_macro_input!(input as Lit) {
Lit::Str(lit_str) => lit_str.value(),
_ => panic!("Expected a string literal"),
_ => panic!("Expected a string literal")
};
TokenStream::from(
parse_ansi_markup(&raw)
.unwrap_or_else(|e| panic!("Bad ansi literal: {}", e))
.into_token_stream(),
)
fn parser(i: &str) -> Result<String, Err<Error<&str>>> {
pair(fold_many0(
alt((
take_till1(|c| c == '<').map(AnsiFrag::Lit),
tuple((tag("<"), take_till(|c| c == '>'), tag(">"))).map(|t| AnsiFrag::Special(t.1))
)),
|| "".to_owned(),
|a, r| a + match r {
AnsiFrag::Lit(s) => &s,
Special(s) if s == "reset" => "\x1b[0m",
Special(s) if s == "bold" => "\x1b[1m",
Special(s) if s == "under" => "\x1b[4m",
Special(s) if s == "strike" => "\x1b[9m",
Special(s) if s == "nounder" => "\x1b[24m",
Special(s) if s == "black" => "\x1b[30m",
Special(s) if s == "red" => "\x1b[31m",
Special(s) if s == "green" => "\x1b[32m",
Special(s) if s == "yellow" => "\x1b[33m",
Special(s) if s == "blue" => "\x1b[34m",
Special(s) if s == "magenta" => "\x1b[35m",
Special(s) if s == "cyan" => "\x1b[36m",
Special(s) if s == "white" => "\x1b[37m",
Special(s) if s == "bgblack" => "\x1b[40m",
Special(s) if s == "bgred" => "\x1b[41m",
Special(s) if s == "bggreen" => "\x1b[42m",
Special(s) if s == "bgyellow" => "\x1b[43m",
Special(s) if s == "bgblue" => "\x1b[44m",
Special(s) if s == "bgmagenta" => "\x1b[45m",
Special(s) if s == "bgcyan" => "\x1b[46m",
Special(s) if s == "bgwhite" => "\x1b[47m",
Special(s) if s == "lt" => "<",
Special(r) => panic!("Unknown ansi type {}", r)
}
), eof)(i).map(|(_, (r, _))| r)
}
TokenStream::from(parser(&raw)
.unwrap_or_else(|e| { panic!("Bad ansi literal: {}", e) })
.into_token_stream())
}

View File

@ -1,7 +0,0 @@
[package]
name = "ansi_markup"
version = "0.1.0"
edition = "2021"
[dependencies]
nom = "7.1.3"

View File

@ -1,62 +0,0 @@
use nom::{
branch::alt,
bytes::complete::{tag, take_till, take_till1},
combinator::eof,
error::Error,
multi::fold_many0,
sequence::{pair, tuple},
Err, Parser,
};
enum AnsiFrag<'l> {
Lit(&'l str),
Special(&'l str),
}
use AnsiFrag::Special;
pub fn parse_ansi_markup(i: &str) -> Result<String, Err<Error<&str>>> {
pair(
fold_many0(
alt((
take_till1(|c| c == '<').map(AnsiFrag::Lit),
tuple((tag("<"), take_till(|c| c == '>'), tag(">")))
.map(|t| AnsiFrag::Special(t.1)),
)),
|| "".to_owned(),
|a, r| {
a + match r {
AnsiFrag::Lit(s) => &s,
Special(s) if s == "reset" => "\x1b[0m",
Special(s) if s == "bold" => "\x1b[1m",
Special(s) if s == "under" => "\x1b[4m",
Special(s) if s == "strike" => "\x1b[9m",
Special(s) if s == "nounder" => "\x1b[24m",
Special(s) if s == "black" => "\x1b[30m",
Special(s) if s == "red" => "\x1b[31m",
Special(s) if s == "green" => "\x1b[32m",
Special(s) if s == "yellow" => "\x1b[33m",
Special(s) if s == "blue" => "\x1b[34m",
Special(s) if s == "magenta" => "\x1b[35m",
Special(s) if s == "cyan" => "\x1b[36m",
Special(s) if s == "white" => "\x1b[37m",
Special(s) if s == "bgblack" => "\x1b[40m",
Special(s) if s == "bgred" => "\x1b[41m",
Special(s) if s == "bggreen" => "\x1b[42m",
Special(s) if s == "bgyellow" => "\x1b[43m",
Special(s) if s == "bgblue" => "\x1b[44m",
Special(s) if s == "bgmagenta" => "\x1b[45m",
Special(s) if s == "bgcyan" => "\x1b[46m",
Special(s) if s == "bgwhite" => "\x1b[47m",
Special(s) if s == "lt" => "<",
Special(r) => panic!("Unknown ansi type {}", r),
}
},
),
eof,
)(i)
.map(|(_, (r, _))| r)
}
pub fn parse_ansi_markup_escape(i: &str) -> Result<String, Err<Error<&str>>> {
parse_ansi_markup(i).map(|o| o.replace('\x1b', "\\e"))
}

View File

@ -6,46 +6,33 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.21.7"
base64 = "0.20.0"
blastmud_interfaces = { path = "../blastmud_interfaces" }
ansi = { path = "../ansi" }
ansi_markup = { path = "../ansi_markup" }
deadpool = "0.10.0"
deadpool-postgres = { version = "0.12.1", features = ["serde"] }
futures = "0.3.30"
log = "0.4.21"
nix = { version = "0.27.1", features = ["process", "signal"] }
ring = "0.17.8"
serde = { version = "1.0.197", features = ["derive", "serde_derive"] }
serde_yaml = "0.9.32"
simple_logger = "4.3.3"
tokio = { version = "1.36.0", features = ["signal", "net", "macros", "rt-multi-thread", "rt", "tokio-macros", "time", "sync", "io-util"] }
tokio-postgres = { version = "0.7.10", features = ["with-uuid-1", "with-serde_json-1"] }
deadpool = "0.9.5"
deadpool-postgres = { version = "0.10.3", features = ["serde"] }
futures = "0.3.25"
log = "0.4.17"
nix = "0.26.1"
ring = "0.16.20"
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", "with-serde_json-1"] }
tokio-serde = { version = "0.8.0", features = ["serde", "serde_cbor", "cbor"] }
tokio-stream = "0.1.14"
tokio-util = { version = "0.7.10", features = ["codec"] }
uuid = { version = "1.7.0", features = ["v4", "serde", "rng"] }
serde_json = "1.0.114"
async-trait = "0.1.77"
nom = "7.1.3"
ouroboros = "0.18.3"
chrono = { version = "0.4.35", features = ["serde"] }
bcrypt = "0.15.0"
validator = "0.16.1"
itertools = "0.12.1"
once_cell = "1.19.0"
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"
ouroboros = "0.15.5"
chrono = { version = "0.4.23", features = ["serde"] }
bcrypt = "0.13.0"
validator = "0.16.0"
itertools = "0.10.5"
once_cell = "1.16.0"
rand = "0.8.5"
async-recursion = "1.0.5"
rand_distr = "0.4.3"
humantime = "2.1.0"
rust_decimal = "1.34.3"
mockall_double = "0.3.1"
[dev-dependencies]
tokio-test = "0.4.3"
mockall = "0.12.1"
[features]
default = []
# Export data to YAML files in /tmp on startup
yamldump = []
async-recursion = "1.0.0"

View File

@ -1,13 +0,0 @@
use std::process::Command;
pub fn main() {
let cmdout = Command::new("git")
.arg("rev-parse")
.arg("HEAD")
.output()
.expect("git rev-parse HEAD failed");
println!(
"cargo:rustc-env=GIT_VERSION={}",
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8")
);
}

View File

@ -1 +0,0 @@
edition = "2021"

38
blastmud_game/src/av.rs Normal file
View File

@ -0,0 +1,38 @@
use std::fs;
use std::error::Error;
use serde::Deserialize;
use ring::signature;
use base64;
use crate::DResult;
#[derive(Deserialize)]
struct AV {
copyright: String,
serial: u64,
cn: String,
assertion: String,
sig: String
}
static KEY_BYTES: [u8;65] = [
0x04, 0x4f, 0xa0, 0x8b, 0x32, 0xa7, 0x7f, 0xc1, 0x0a, 0xfc, 0x51, 0x95, 0x93, 0x57, 0x05,
0xb3, 0x0f, 0xad, 0x16, 0x05, 0x3c, 0x7c, 0xfc, 0x02, 0xd2, 0x7a, 0x63, 0xff, 0xd3, 0x09,
0xaa, 0x5b, 0x78, 0xfe, 0xa8, 0xc2, 0xc3, 0x02, 0xc2, 0xe6, 0xaf, 0x81, 0xc7, 0xa3, 0x03,
0xfa, 0x4d, 0xf1, 0xf9, 0xfc, 0x0a, 0x36, 0xef, 0x6b, 0x1e, 0x9d, 0xce, 0x6e, 0x60, 0xc6,
0xa8, 0xb3, 0x02, 0x35, 0x7e
];
pub fn check() -> DResult<()> {
let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?).
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)?;
if av.copyright != "This file is protected by copyright and may not be used or reproduced except as authorised by the copyright holder. All rights reserved." ||
av.assertion != "age>=18" {
Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?;
}
let sign_text = format!("cn={};{};serial={}", av.cn, av.assertion, av.serial);
let key: signature::UnparsedPublicKey<&[u8]> =
signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, &KEY_BYTES);
key.verify(&sign_text.as_bytes(), &base64::decode(av.sig)?)
.map_err(|_| Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml signature"))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
use once_cell::sync::OnceCell;
use rust_decimal::Decimal;
struct PluralRule<'l> {
match_suffix: &'l str,
@ -7,262 +6,51 @@ struct PluralRule<'l> {
append_suffix: &'l str,
}
pub fn pluralise(orig_input: &str) -> String {
let mut extra_suffix: &str = "";
let mut input: &str = orig_input;
'wordsplit: for split_word in vec!["pair", "box", "jar", "tube"] {
for (idx, _) in input.match_indices(split_word) {
let end_idx = idx + split_word.len();
if end_idx == input.len() {
continue;
}
if (idx == 0 || &input[idx - 1..idx] == " ") && &input[end_idx..end_idx + 1] == " " {
extra_suffix = &input[end_idx..];
input = &input[0..end_idx];
break 'wordsplit;
}
}
}
pub fn pluralise(input: &str) -> String {
static PLURAL_RULES: OnceCell<Vec<PluralRule>> = OnceCell::new();
let plural_rules = PLURAL_RULES.get_or_init(|| {
vec![
PluralRule {
match_suffix: "foot",
drop: 3,
append_suffix: "eet",
},
PluralRule {
match_suffix: "tooth",
drop: 4,
append_suffix: "eeth",
},
PluralRule {
match_suffix: "man",
drop: 2,
append_suffix: "en",
},
PluralRule {
match_suffix: "mouse",
drop: 4,
append_suffix: "ice",
},
PluralRule {
match_suffix: "louse",
drop: 4,
append_suffix: "ice",
},
PluralRule {
match_suffix: "fish",
drop: 0,
append_suffix: "",
},
PluralRule {
match_suffix: "sheep",
drop: 0,
append_suffix: "",
},
PluralRule {
match_suffix: "deer",
drop: 0,
append_suffix: "",
},
PluralRule {
match_suffix: "pox",
drop: 0,
append_suffix: "",
},
PluralRule {
match_suffix: "cis",
drop: 2,
append_suffix: "es",
},
PluralRule {
match_suffix: "sis",
drop: 2,
append_suffix: "es",
},
PluralRule {
match_suffix: "xis",
drop: 2,
append_suffix: "es",
},
PluralRule {
match_suffix: "ss",
drop: 0,
append_suffix: "es",
},
PluralRule {
match_suffix: "ch",
drop: 0,
append_suffix: "es",
},
PluralRule {
match_suffix: "sh",
drop: 0,
append_suffix: "es",
},
PluralRule {
match_suffix: "ife",
drop: 2,
append_suffix: "ves",
},
PluralRule {
match_suffix: "lf",
drop: 1,
append_suffix: "ves",
},
PluralRule {
match_suffix: "arf",
drop: 1,
append_suffix: "ves",
},
PluralRule {
match_suffix: "ay",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "ey",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "iy",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "oy",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "uy",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "y",
drop: 1,
append_suffix: "ies",
},
PluralRule {
match_suffix: "ao",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "eo",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "io",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "oo",
drop: 0,
append_suffix: "s",
},
PluralRule {
match_suffix: "uo",
drop: 0,
append_suffix: "s",
},
let plural_rules = PLURAL_RULES.get_or_init(|| vec!(
PluralRule { match_suffix: "foot", drop: 3, append_suffix: "eet" },
PluralRule { match_suffix: "tooth", drop: 4, append_suffix: "eeth" },
PluralRule { match_suffix: "man", drop: 2, append_suffix: "en" },
PluralRule { match_suffix: "mouse", drop: 4, append_suffix: "ice" },
PluralRule { match_suffix: "louse", drop: 4, append_suffix: "ice" },
PluralRule { match_suffix: "fish", drop: 0, append_suffix: "" },
PluralRule { match_suffix: "sheep", drop: 0, append_suffix: "" },
PluralRule { match_suffix: "deer", drop: 0, append_suffix: "" },
PluralRule { match_suffix: "pox", drop: 0, append_suffix: "" },
PluralRule { match_suffix: "cis", drop: 2, append_suffix: "es" },
PluralRule { match_suffix: "sis", drop: 2, append_suffix: "es" },
PluralRule { match_suffix: "xis", drop: 2, append_suffix: "es" },
PluralRule { match_suffix: "ss", drop: 0, append_suffix: "es" },
PluralRule { match_suffix: "ch", drop: 0, append_suffix: "es" },
PluralRule { match_suffix: "sh", drop: 0, append_suffix: "es" },
PluralRule { match_suffix: "ife", drop: 2, append_suffix: "ves" },
PluralRule { match_suffix: "lf", drop: 1, append_suffix: "ves" },
PluralRule { match_suffix: "arf", drop: 1, append_suffix: "ves" },
PluralRule { match_suffix: "ay", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "ey", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "iy", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "oy", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "uy", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "y", drop: 1, append_suffix: "ies" },
PluralRule { match_suffix: "ao", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "eo", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "io", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "oo", drop: 0, append_suffix: "s" },
PluralRule { match_suffix: "uo", drop: 0, append_suffix: "s" },
// The o rule could be much larger... we'll add specific exceptions as
// the come up.
PluralRule {
match_suffix: "o",
drop: 0,
append_suffix: "es",
},
PluralRule { match_suffix: "o", drop: 0, append_suffix: "es" },
// Lots of possible exceptions here.
PluralRule {
match_suffix: "ex",
drop: 0,
append_suffix: "es",
},
PluralRule {
match_suffix: "ox",
drop: 0,
append_suffix: "es",
},
]
});
PluralRule { match_suffix: "ex", drop: 0, append_suffix: "es" },
));
for rule in plural_rules {
if input.ends_with(rule.match_suffix) {
return input[0..(input.len() - rule.drop)].to_owned()
+ rule.append_suffix
+ extra_suffix;
return input[0..(input.len() - rule.drop)].to_owned() + rule.append_suffix;
}
}
input.to_owned() + "s" + extra_suffix
}
pub fn indefinite_article(countable_word: &str) -> &'static str {
if countable_word.is_empty() {
return "";
}
let vowels = ["a", "e", "i", "o", "u"];
if !vowels.contains(&&countable_word[0..1]) {
if countable_word.starts_with("honor")
|| countable_word.starts_with("honour")
|| countable_word.starts_with("honest")
|| countable_word.starts_with("hour")
|| countable_word.starts_with("heir")
{
return "an";
}
return "a";
}
if countable_word.starts_with("eu")
|| countable_word.starts_with("one")
|| countable_word.starts_with("once")
{
return "a";
}
if countable_word.starts_with("e") {
if countable_word.starts_with("ewe") {
return "a";
}
return "an";
}
if countable_word.starts_with("u") {
if countable_word.len() < 3 {
return "an";
}
if countable_word.starts_with("uni") {
if countable_word.starts_with("unid")
|| countable_word.starts_with("unim")
|| countable_word.starts_with("unin")
{
// unidentified, unimaginable, uninhabited etc...
return "an";
}
// Words like unilateral
return "a";
}
if countable_word.starts_with("unani") || countable_word.starts_with("ubiq") {
return "a";
}
if ["r", "s", "t"].contains(&&countable_word[1..2]) {
if vowels.contains(&&countable_word[2..3]) {
// All u[rst][aeiou] words, e.g. usury, need "a"
return "a";
}
return "an";
}
if countable_word.starts_with("ubiq")
|| countable_word.starts_with("uku")
|| countable_word.starts_with("ukr")
{
return "a";
}
}
return "an";
input.to_owned() + "s"
}
pub fn caps_first(inp: &str) -> String {
@ -277,26 +65,7 @@ pub fn join_words(words: &[&str]) -> String {
match words.split_last() {
None => "".to_string(),
Some((last, [])) => last.to_string(),
Some((last, rest)) => rest.join(", ") + " and " + last,
}
}
pub fn join_words_or(words: &[&str]) -> String {
match words.split_last() {
None => "".to_string(),
Some((last, [])) => last.to_string(),
Some((last, rest)) => rest.join(", ") + " or " + last,
}
}
pub fn weight(grams: u64) -> String {
if grams > 999 {
format!(
"{} kg",
Decimal::from_i128_with_scale(grams as i128, 3).normalize()
)
} else {
format!("{} g", grams)
Some((last, rest)) => rest.join(", ") + " and " + last
}
}
@ -304,7 +73,7 @@ pub fn weight(grams: u64) -> String {
mod test {
#[test]
fn pluralise_should_follow_english_rules() {
for (word, plural) in vec![
for (word, plural) in vec!(
("cat", "cats"),
("wolf", "wolves"),
("scarf", "scarves"),
@ -319,94 +88,33 @@ mod test {
("killer blowfly", "killer blowflies"),
("house mouse", "house mice"),
("zombie sheep", "zombie sheep"),
("brown pair of pants", "brown pairs of pants"),
("good pair", "good pairs"),
("repair kit", "repair kits"),
("box of wolves", "boxes of wolves"),
("jar of acid", "jars of acid"),
] {
) {
assert_eq!(super::pluralise(word), plural);
}
}
#[test]
fn indefinite_article_should_follow_english_rules() {
for (article, word) in vec![
("a", "cat"),
("a", "human"),
("an", "apple"),
("an", "easter egg"),
("an", "indigo orb"),
("an", "orange"),
("an", "urchin"),
("an", "hour"),
("a", "once-in-a-lifetime opportunity"),
("a", "uranium-covered field"),
("a", "usurper to the throne"),
("a", "Ukrainian hero"),
("a", "universal truth"),
("an", "uninvited guest"),
("a", "unanimous decision"),
("a", "European getaway"),
("an", "utter disaster"),
("a", "uterus"),
("a", "user"),
("a", "ubiquitous hazard"),
("a", "unitary plan"),
] {
let result = super::indefinite_article(&word.to_lowercase());
assert_eq!(
format!("{} {}", result, word),
format!("{} {}", article, word)
);
}
}
#[test]
fn caps_first_works() {
for (inp, outp) in vec![
for (inp, outp) in vec!(
("", ""),
("cat", "Cat"),
("Cat", "Cat"),
("hello world", "Hello world"),
] {
) {
assert_eq!(super::caps_first(inp), outp);
}
}
#[test]
fn join_words_works() {
for (inp, outp) in vec![
(vec![], ""),
(vec!["cat"], "cat"),
(vec!["cat", "dog"], "cat and dog"),
(vec!["cat", "dog", "fish"], "cat, dog and fish"),
(
vec!["wolf", "cat", "dog", "fish"],
"wolf, cat, dog and fish",
),
] {
for (inp, outp) in vec!(
(vec!(), ""),
(vec!("cat"), "cat"),
(vec!("cat", "dog"), "cat and dog"),
(vec!("cat", "dog", "fish"), "cat, dog and fish"),
(vec!("wolf", "cat", "dog", "fish"), "wolf, cat, dog and fish"),
) {
assert_eq!(super::join_words(&inp[..]), outp);
}
}
#[test]
fn join_words_or_works() {
for (inp, outp) in vec![
(vec![], ""),
(vec!["cat"], "cat"),
(vec!["cat", "dog"], "cat or dog"),
(vec!["cat", "dog", "fish"], "cat, dog or fish"),
(vec!["wolf", "cat", "dog", "fish"], "wolf, cat, dog or fish"),
] {
assert_eq!(super::join_words_or(&inp[..]), outp);
}
}
#[test]
fn weight_works() {
assert_eq!(super::weight(100), "100 g");
assert_eq!(super::weight(1000), "1 kg");
assert_eq!(super::weight(1100), "1.1 kg");
}
}

View File

@ -1,37 +1,36 @@
use crate::DResult;
use blastmud_interfaces::*;
use futures::prelude::*;
use log::{info, warn};
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Instant;
use tokio::net::{lookup_host, TcpSocket, TcpStream};
use tokio::sync::{mpsc, oneshot, Mutex};
use tokio::{task, time};
use tokio_serde::formats::Cbor;
use tokio::net::{TcpSocket, TcpStream, lookup_host};
use log::{info, warn};
use tokio_util::codec;
use tokio_util::codec::length_delimited::LengthDelimitedCodec;
use tokio_serde::formats::Cbor;
use blastmud_interfaces::*;
use futures::prelude::*;
use tokio::sync::{Mutex, mpsc, oneshot};
use std::net::SocketAddr;
use std::sync::Arc;
use uuid::Uuid;
use std::collections::BTreeMap;
use crate::DResult;
use std::time::Instant;
#[derive(Debug)]
pub struct ListenerSend {
pub message: MessageToListener,
pub ack_notify: oneshot::Sender<()>,
pub ack_notify: oneshot::Sender<()>
}
pub type ListenerMap = Arc<Mutex<BTreeMap<Uuid, mpsc::Sender<ListenerSend>>>>;
async fn handle_from_listener<FHandler, HandlerFut>(
conn: TcpStream,
message_handler: FHandler,
listener_map: ListenerMap,
) where
listener_map: ListenerMap)
where
FHandler: Fn(Uuid, MessageFromListener) -> HandlerFut + Send + 'static,
HandlerFut: Future<Output = DResult<()>> + Send + 'static,
{
HandlerFut: Future<Output = DResult<()>> + Send + 'static {
let mut conn_framed = tokio_serde::Framed::new(
codec::Framed::new(conn, LengthDelimitedCodec::new()),
Cbor::<MessageFromListener, MessageToListener>::default(),
Cbor::<MessageFromListener, MessageToListener>::default()
);
let listener_id = match conn_framed.try_next().await {
@ -44,7 +43,7 @@ async fn handle_from_listener<FHandler, HandlerFut>(
}
};
uuid
}
},
Ok(Some(msg)) => {
warn!("Got non-ping first message from listener: {:?}", msg);
return;
@ -54,24 +53,15 @@ async fn handle_from_listener<FHandler, HandlerFut>(
return;
}
Err(e) => {
warn!(
"Lost listener connection to error {} before first message",
e
);
warn!("Lost listener connection to error {} before first message", e);
return;
}
};
match conn_framed
.send(MessageToListener::AcknowledgeMessage)
.await
{
match conn_framed.send(MessageToListener::AcknowledgeMessage).await {
Ok(_) => {}
Err(e) => {
warn!(
"Got error sending listener acknowledge for initial ping: {}",
e
);
warn!("Got error sending listener acknowledge for initial ping: {}", e);
return;
}
}
@ -199,20 +189,17 @@ pub fn make_listener_map() -> ListenerMap {
pub async fn start_listener<FHandler, HandlerFut>(
bind_to: String,
listener_map: ListenerMap,
handle_message: FHandler,
handle_message: FHandler
) -> DResult<()>
where
FHandler: Fn(Uuid, MessageFromListener) -> HandlerFut + Send + Clone + 'static,
HandlerFut: Future<Output = DResult<()>> + Send + 'static,
HandlerFut: Future<Output = DResult<()>> + Send + 'static
{
info!("Starting listener on {}", bind_to);
let addr = lookup_host(bind_to)
.await?
.next()
.expect("listener address didn't resolve");
let addr = lookup_host(bind_to).await?.next().expect("listener address didn't resolve");
let socket = match addr {
SocketAddr::V4 {..} => TcpSocket::new_v4()?,
SocketAddr::V6 { .. } => TcpSocket::new_v6()?,
SocketAddr::V6 {..} => TcpSocket::new_v6()?
};
socket.set_reuseaddr(true)?;
socket.set_reuseport(true)?;
@ -228,11 +215,7 @@ where
}
Ok((socket, _)) => {
info!("Accepted new inbound connection from listener");
task::spawn(handle_from_listener(
socket,
handle_message.clone(),
listener_map_for_task.clone(),
));
task::spawn(handle_from_listener(socket, handle_message.clone(), listener_map_for_task.clone()));
}
}
}

View File

@ -1,50 +1,44 @@
#![cfg_attr(test, allow(unused))]
use db::DBPool;
use log::{info, LevelFilter};
use serde::Deserialize;
use simple_logger::SimpleLogger;
use std::error::Error;
use std::fs;
use std::error::Error;
use log::{info, error, LevelFilter};
use simple_logger::SimpleLogger;
use tokio::signal::unix::{signal, SignalKind};
#[cfg(feature = "yamldump")]
use static_content::dumper::dump_static_content;
use db::DBPool;
mod db;
mod language;
mod listener;
mod message_handler;
mod models;
mod regular_tasks;
mod services;
mod static_content;
mod version_cutover;
mod av;
mod regular_tasks;
mod models;
mod static_content;
mod language;
mod services;
pub type DResult<T> = Result<T, Box<dyn Error + Send + Sync + 'static>>;
pub type DResult<T> = Result<T, Box<dyn Error + Send + Sync>>;
#[derive(Deserialize, Debug)]
struct Config {
listener: String,
pidfile: String,
database_conn_string: String,
database_conn_string: String
}
fn read_latest_config() -> DResult<Config> {
serde_yaml::from_str(&fs::read_to_string("gameserver.conf")?)
.map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
serde_yaml::from_str(&fs::read_to_string("gameserver.conf")?).
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
}
#[tokio::main(worker_threads = 2)]
#[cfg(not(test))]
#[tokio::main]
async fn main() -> DResult<()> {
SimpleLogger::new()
.with_level(LevelFilter::Info)
.init()
.unwrap();
#[cfg(feature = "yamldump")]
dump_static_content()?;
SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap();
av::check().or_else(|e| -> Result<(), Box<dyn Error + Send + Sync>> {
error!("Couldn't verify age-verification.yml - this is not a complete game. Check README.md: {}", e);
Err(e)
})?;
let config = read_latest_config()?;
let pool = DBPool::start(&config.database_conn_string)?;
@ -56,12 +50,11 @@ async fn main() -> DResult<()> {
let listener_map = listener::make_listener_map();
let mh_pool = pool.clone();
listener::start_listener(
config.listener,
listener_map.clone(),
move |listener_id, msg| message_handler::handle(listener_id, msg, mh_pool.clone()),
)
.await?;
listener::start_listener(config.listener, listener_map.clone(),
move |listener_id, msg| {
message_handler::handle(listener_id, msg, mh_pool.clone())
}
).await?;
static_content::refresh_static_content(&pool).await?;

View File

@ -1,8 +1,8 @@
use crate::db;
use crate::DResult;
use blastmud_interfaces::*;
use uuid::Uuid;
use crate::db;
use MessageFromListener::*;
use uuid::Uuid;
use crate::DResult;
mod new_session;
pub mod user_commands;
@ -10,31 +10,19 @@ pub mod user_commands;
#[derive(Clone,Debug)]
pub struct ListenerSession {
pub listener: Uuid,
pub session: Uuid,
pub session: Uuid
}
#[cfg(test)]
impl Default for ListenerSession {
fn default() -> ListenerSession {
use uuid::uuid;
ListenerSession {
listener: uuid!("6f9c9b61-9228-4427-abd7-c4aef127a862"),
session: uuid!("668efb68-79d3-4004-9d6a-1e5757792e1a"),
}
}
}
pub async fn handle(listener: Uuid, msg: MessageFromListener, pool: db::DBPool) -> DResult<()> {
pub async fn handle(listener: Uuid, msg: MessageFromListener, pool: db::DBPool)
-> DResult<()> {
match msg {
ListenerPing { .. } => {
pool.record_listener_ping(listener).await?;
}
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?;
pool.end_session(ListenerSession { listener, session }).await?;
}
SessionSentLine { session, msg } => {
user_commands::handle(&ListenerSession { listener, session }, &msg, &pool).await?;

View File

@ -1,61 +1,21 @@
use crate::db::DBPool;
use crate::message_handler::ListenerSession;
use crate::models::session::Session;
use crate::DResult;
use crate::db::DBPool;
use ansi::ansi;
use std::default::Default;
// ANSI art version of the symbol we are legally required to display per:
// https://www.legislation.gov.au/Details/F2017C00102
// https://dom111.github.io/image-to-ansi/ can help convert it.
const AUS_RATING_SYMBOL: &'static str = "\x1b[38;5;235;48;5;234m▄\x1b[38;5;234;48;5;234m▄\x1b[38;5;235;48;5;235m▄\x1b[38;5;235;48;5;234m▄\x1b[38;5;234;48;5;234m▄▄▄\x1b[38;5;234;48;5;235m▄▄\x1b[38;5;234;48;5;234m▄\x1b[38;5;234;48;5;235m▄\x1b[38;5;234;48;5;234m▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\x1b[38;5;234;48;5;235m▄\x1b[38;5;234;48;5;234m▄▄\x1b[38;5;234;48;5;235m▄\x1b[38;5;234;48;5;234m▄▄\x1b[38;5;235;48;5;234m▄\x1b[38;5;234;48;5;234m▄\x1b[38;5;234;48;5;235m▄\x1b[38;5;234;48;5;234m▄\x1b[38;5;235;48;5;235m▄\x1b[m
\x1b[38;5;233;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;233;48;5;235m\x1b[38;5;235;48;5;234m\x1b[38;5;236;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;236;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;233m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;236;48;5;233m\x1b[38;5;145;48;5;236m\x1b[38;5;230;48;5;245m\x1b[38;5;187;48;5;250m\x1b[38;5;186;48;5;188m\x1b[38;5;186;48;5;253m\x1b[38;5;186;48;5;254m\x1b[38;5;186;48;5;253m\x1b[38;5;187;48;5;251m\x1b[38;5;230;48;5;247m\x1b[38;5;188;48;5;239m\x1b[38;5;240;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;235;48;5;233m\x1b[38;5;250;48;5;245m\x1b[38;5;187;48;5;230m\x1b[38;5;185;48;5;143m\x1b[38;5;220;48;5;185m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;221m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;184m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;221m\x1b[38;5;11;48;5;185m\x1b[38;5;184;48;5;143m\x1b[38;5;185;48;5;229m\x1b[38;5;224;48;5;252m\x1b[38;5;240;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;235;48;5;234m\x1b[m
\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;254;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;221;48;5;185m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;185;48;5;179m\x1b[38;5;255;48;5;230m\x1b[38;5;243;48;5;243m\x1b[38;5;233;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[m
\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;254m\x1b[38;5;186;48;5;186m\x1b[38;5;184;48;5;185m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;243;48;5;243m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;11;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;184;48;5;11m\x1b[38;5;184;48;5;220m\x1b[38;5;184;48;5;11m\x1b[38;5;178;48;5;11m\x1b[38;5;184;48;5;11m\x1b[38;5;184;48;5;220m\x1b[38;5;184;48;5;11m\x1b[38;5;185;48;5;11m\x1b[38;5;184;48;5;227m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;184;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;3;48;5;58m\x1b[38;5;142;48;5;58m\x1b[38;5;185;48;5;94m\x1b[38;5;184;48;5;58m\x1b[38;5;184;48;5;3m\x1b[38;5;184;48;5;94m\x1b[38;5;185;48;5;58m\x1b[38;5;184;48;5;58m\x1b[38;5;184;48;5;94m\x1b[38;5;184;48;5;58m\x1b[38;5;184;48;5;94m\x1b[38;5;185;48;5;94m\x1b[38;5;184;48;5;94m\x1b[38;5;184;48;5;58m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[48;5;230m \x1b[48;5;242m \x1b[48;5;233m \x1b[48;5;234m \x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;221;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;227;48;5;227m\x1b[38;5;94;48;5;58m\x1b[38;5;178;48;5;142m\x1b[38;5;227;48;5;220m\x1b[38;5;178;48;5;11m\x1b[38;5;100;48;5;11m\x1b[38;5;3;48;5;11m\x1b[38;5;3;48;5;227m\x1b[38;5;3;48;5;221m\x1b[38;5;3;48;5;11m\x1b[38;5;142;48;5;11m\x1b[38;5;185;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;11m\x1b[38;5;142;48;5;11m\x1b[38;5;3;48;5;11m\x1b[38;5;3;48;5;227m\x1b[38;5;142;48;5;227m\x1b[38;5;184;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;220m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;178m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[48;5;230m \x1b[48;5;242m \x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;227m\x1b[38;5;94;48;5;58m\x1b[38;5;142;48;5;142m\x1b[38;5;227;48;5;227m\x1b[38;5;142;48;5;142m\x1b[38;5;234;48;5;235m\x1b[38;5;233;48;5;235m\x1b[38;5;235;48;5;234m\x1b[38;5;237;48;5;233m\x1b[38;5;58;48;5;233m\x1b[38;5;58;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;233;48;5;58m\x1b[38;5;236;48;5;143m\x1b[38;5;185;48;5;11m\x1b[38;5;58;48;5;184m\x1b[38;5;234;48;5;58m\x1b[38;5;233;48;5;236m\x1b[38;5;236;48;5;234m\x1b[38;5;58;48;5;233m\x1b[38;5;237;48;5;233m\x1b[38;5;234;48;5;236m\x1b[38;5;234;48;5;58m\x1b[38;5;58;48;5;184m\x1b[38;5;185;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;184;48;5;184m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[48;5;11m \x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[48;5;230m \x1b[48;5;242m \x1b[48;5;233m \x1b[48;5;234m \x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;220m\x1b[48;5;11m \x1b[38;5;227;48;5;227m\x1b[48;5;3m \x1b[38;5;142;48;5;142m\x1b[38;5;11;48;5;227m\x1b[38;5;142;48;5;142m\x1b[38;5;235;48;5;235m\x1b[38;5;233;48;5;233m\x1b[38;5;142;48;5;101m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;184m\x1b[38;5;143;48;5;58m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;3;48;5;142m\x1b[38;5;100;48;5;143m\x1b[38;5;234;48;5;234m\x1b[38;5;137;48;5;58m\x1b[38;5;227;48;5;185m\x1b[38;5;11;48;5;191m\x1b[38;5;11;48;5;185m\x1b[38;5;184;48;5;58m\x1b[38;5;58;48;5;234m\x1b[38;5;58;48;5;235m\x1b[38;5;136;48;5;143m\x1b[38;5;11;48;5;11m\x1b[38;5;184;48;5;184m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;227m\x1b[38;5;94;48;5;3m\x1b[38;5;142;48;5;142m\x1b[38;5;11;48;5;227m\x1b[38;5;142;48;5;142m\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;233m\x1b[38;5;59;48;5;136m\x1b[38;5;137;48;5;227m\x1b[38;5;137;48;5;11m\x1b[38;5;3;48;5;227m\x1b[38;5;237;48;5;143m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;0m\x1b[38;5;143;48;5;136m\x1b[38;5;3;48;5;101m\x1b[38;5;234;48;5;232m\x1b[38;5;235;48;5;233m\x1b[38;5;142;48;5;143m\x1b[38;5;11;48;5;220m\x1b[38;5;184;48;5;11m\x1b[38;5;100;48;5;11m\x1b[38;5;3;48;5;227m\x1b[38;5;101;48;5;221m\x1b[38;5;100;48;5;228m\x1b[38;5;142;48;5;227m\x1b[38;5;11;48;5;11m\x1b[38;5;184;48;5;184m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[48;5;11m \x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[48;5;233m \x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;221m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;227m\x1b[48;5;94m \x1b[38;5;143;48;5;142m\x1b[38;5;191;48;5;227m\x1b[38;5;143;48;5;142m\x1b[38;5;236;48;5;235m\x1b[38;5;234;48;5;233m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;58;48;5;233m\x1b[38;5;142;48;5;235m\x1b[38;5;11;48;5;3m\x1b[38;5;11;48;5;185m\x1b[38;5;3;48;5;64m\x1b[38;5;233;48;5;233m\x1b[38;5;235;48;5;235m\x1b[38;5;142;48;5;142m\x1b[38;5;11;48;5;11m\x1b[38;5;143;48;5;100m\x1b[38;5;58;48;5;234m\x1b[38;5;233;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;58;48;5;3m\x1b[38;5;11;48;5;11m\x1b[38;5;184;48;5;184m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[48;5;233m \x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[48;5;235m \x1b[48;5;253m \x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;3;48;5;3m\x1b[38;5;142;48;5;142m\x1b[38;5;11;48;5;227m\x1b[38;5;142;48;5;142m\x1b[38;5;234;48;5;234m\x1b[38;5;233;48;5;233m\x1b[38;5;137;48;5;100m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;184m\x1b[38;5;11;48;5;221m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;136;48;5;100m\x1b[38;5;234;48;5;234m\x1b[38;5;233;48;5;234m\x1b[38;5;3;48;5;142m\x1b[38;5;227;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;227m\x1b[38;5;142;48;5;221m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;233m\x1b[38;5;3;48;5;94m\x1b[38;5;11;48;5;220m\x1b[38;5;184;48;5;220m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[48;5;235m \x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;3;48;5;3m\x1b[38;5;142;48;5;142m\x1b[38;5;11;48;5;11m\x1b[38;5;142;48;5;142m\x1b[38;5;235;48;5;234m\x1b[38;5;233;48;5;232m\x1b[38;5;137;48;5;137m\x1b[38;5;227;48;5;227m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;185m\x1b[38;5;100;48;5;235m\x1b[38;5;236;48;5;233m\x1b[38;5;233;48;5;235m\x1b[38;5;234;48;5;100m\x1b[38;5;235;48;5;143m\x1b[38;5;233;48;5;106m\x1b[38;5;234;48;5;236m\x1b[38;5;235;48;5;234m\x1b[38;5;3;48;5;237m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;184;48;5;184m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[48;5;185m \x1b[38;5;230;48;5;230m\x1b[48;5;242m \x1b[48;5;233m \x1b[48;5;234m \x1b[38;5;235;48;5;235m\x1b[m
\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[48;5;253m \x1b[38;5;186;48;5;186m\x1b[38;5;221;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;221;48;5;227m\x1b[38;5;3;48;5;94m\x1b[38;5;185;48;5;142m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;142m\x1b[38;5;178;48;5;58m\x1b[38;5;185;48;5;236m\x1b[38;5;221;48;5;142m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;142m\x1b[38;5;185;48;5;58m\x1b[38;5;179;48;5;236m\x1b[38;5;179;48;5;235m\x1b[38;5;185;48;5;236m\x1b[38;5;184;48;5;58m\x1b[38;5;227;48;5;100m\x1b[38;5;11;48;5;185m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;221;48;5;185m\x1b[38;5;58;48;5;58m\x1b[38;5;184;48;5;184m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[48;5;233m \x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[m
\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;234m\x1b[48;5;235m \x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;184;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;221m\x1b[38;5;58;48;5;58m\x1b[38;5;3;48;5;142m\x1b[38;5;100;48;5;227m\x1b[38;5;100;48;5;11m\x1b[38;5;136;48;5;11m\x1b[38;5;136;48;5;220m\x1b[38;5;100;48;5;11m\x1b[38;5;136;48;5;11m\x1b[38;5;100;48;5;11m\x1b[38;5;100;48;5;227m\x1b[38;5;100;48;5;11m\x1b[38;5;136;48;5;11m\x1b[38;5;100;48;5;11m\x1b[38;5;100;48;5;185m\x1b[38;5;58;48;5;58m\x1b[38;5;178;48;5;178m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;179;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[48;5;235m \x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;184;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;142m\x1b[38;5;11;48;5;100m\x1b[38;5;11;48;5;136m\x1b[38;5;11;48;5;100m\x1b[38;5;227;48;5;100m\x1b[38;5;11;48;5;100m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[48;5;230m \x1b[38;5;242;48;5;242m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[48;5;234m \x1b[m
\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;253;48;5;253m\x1b[38;5;186;48;5;186m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;220;48;5;11m\x1b[38;5;11;48;5;11m\x1b[48;5;11m \x1b[38;5;11;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;230;48;5;230m\x1b[38;5;242;48;5;242m\x1b[38;5;234;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;254;48;5;254m\x1b[38;5;186;48;5;186m\x1b[38;5;221;48;5;185m\x1b[38;5;11;48;5;11m\x1b[38;5;11;48;5;220m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;179;48;5;185m\x1b[38;5;255;48;5;229m\x1b[38;5;242;48;5;243m\x1b[38;5;233;48;5;234m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;247;48;5;252m\x1b[38;5;230;48;5;187m\x1b[38;5;143;48;5;185m\x1b[38;5;185;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;11;48;5;11m\x1b[38;5;227;48;5;11m\x1b[38;5;185;48;5;11m\x1b[38;5;185;48;5;185m\x1b[38;5;186;48;5;143m\x1b[38;5;252;48;5;230m\x1b[38;5;237;48;5;241m\x1b[38;5;234;48;5;233m\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;233m\x1b[38;5;234;48;5;239m\x1b[38;5;59;48;5;188m\x1b[38;5;251;48;5;229m\x1b[38;5;254;48;5;185m\x1b[38;5;230;48;5;179m\x1b[38;5;224;48;5;185m\x1b[38;5;230;48;5;185m\x1b[38;5;255;48;5;185m\x1b[38;5;230;48;5;185m\x1b[38;5;229;48;5;185m\x1b[38;5;255;48;5;179m\x1b[38;5;230;48;5;143m\x1b[38;5;252;48;5;186m\x1b[38;5;8;48;5;230m\x1b[38;5;235;48;5;8m\x1b[38;5;233;48;5;233m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;233m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;240m\x1b[38;5;234;48;5;243m\x1b[38;5;234;48;5;242m\x1b[38;5;233;48;5;242m\x1b[38;5;234;48;5;242m\x1b[38;5;234;48;5;241m\x1b[38;5;234;48;5;242m\x1b[38;5;233;48;5;241m\x1b[38;5;234;48;5;237m\x1b[38;5;235;48;5;233m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[m
\x1b[38;5;235;48;5;235m\x1b[38;5;235;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;234;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;233m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;235m\x1b[38;5;234;48;5;234m\x1b[38;5;235;48;5;234m\x1b[38;5;235;48;5;235m\x1b[m
";
use crate::models::session::Session;
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, Some(&(ansi!("\
pool.start_session(session, &Session { source, ..Default::default() }).await?;
pool.queue_for_session(&session, Some(&ansi!("\
Welcome to <red>BlastMud<reset> - a text-based post-apocalyptic \
game\r\n").to_owned() + AUS_RATING_SYMBOL + ansi!("\r\n\
Parental guidance recommended. Violence. Online interactivity.\r\n\
\r\n\
game <bold>restricted to adults (18+)<reset>\r\n\
Some commands to get you started:\r\n\
\t<bold>register <lt>username> <lt>password> <lt>email><reset> to register as a new user.\r\n\
\t<bold>login <lt>username> <lt>password><reset> to log in as an existing user.\r\n\
\t<bold>help<reset> to learn more.\r\n\
[Please contact staff@blastmud.org with any feedback or suggestions on how to \r\n\
improve Blastmud, to report any inappropriate user generated content or behaviour, or if you \r\n\
need any other help from the game's operators; use <bold>report abuse<reset> immediately after \r\n\
receiving any inappropriate message to store evidence].\r\n\
Blastmud's privacy policy: https://blastmud.org/privacy/\r\n")))).await?;
[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(())
}

View File

@ -1,118 +1,52 @@
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 crate::db::{DBTrans, DBPool, ItemSearchParams};
use ansi::ansi;
use phf::phf_map;
use async_trait::async_trait;
#[cfg(not(test))]
use crate::models::{session::Session, user::User, item::Item};
use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use std::sync::Arc;
mod agree;
mod allow;
mod attack;
mod butcher;
pub mod buy;
mod c;
pub mod close;
pub mod corp;
pub mod cut;
pub mod delete;
pub mod attack;
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 less_explicit_mode;
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;
mod register;
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 trans: &'l DBTrans
}
pub enum CommandHandlingError {
UserError(String),
SystemError(Box<dyn std::error::Error + Send + Sync>),
SystemError(Box<dyn std::error::Error + Send + Sync>)
}
pub use CommandHandlingError::*;
use CommandHandlingError::*;
#[async_trait]
pub trait UserVerb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str)
-> UResult<()>;
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>>,
{
impl<T> From<T> for CommandHandlingError where T: Into<Box<dyn std::error::Error + Send + Sync>> {
fn from(input: T) -> CommandHandlingError {
SystemError(input.into())
}
@ -122,198 +56,76 @@ 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>;
pub fn always_available_commands() -> &'static BTreeMap<&'static str, UserVerbRef> {
static V: OnceCell<BTreeMap<&'static str, UserVerbRef>> = OnceCell::new();
V.get_or_init(|| {
vec![
("", ignore::VERB),
("help", help::VERB),
("quit", quit::VERB),
]
.into_iter()
.collect()
})
}
static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
"" => ignore::VERB,
"help" => help::VERB,
"quit" => quit::VERB,
};
pub fn unregistered_commands() -> &'static BTreeMap<&'static str, UserVerbRef> {
static V: OnceCell<BTreeMap<&'static str, UserVerbRef>> = OnceCell::new();
V.get_or_init(|| {
vec![
("agree", agree::VERB),
("connect", login::VERB),
("login", login::VERB),
("register", register::VERB),
]
.into_iter()
.collect()
})
}
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,
};
pub fn registered_commands() -> &'static BTreeMap<&'static str, UserVerbRef> {
static V: OnceCell<BTreeMap<&'static str, UserVerbRef>> = OnceCell::new();
V.get_or_init(|| {
vec![
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),
]
.into_iter()
.collect()
})
}
"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,
pub fn staff_commands() -> &'static BTreeMap<&'static str, UserVerbRef> {
static V: OnceCell<BTreeMap<&'static str, UserVerbRef>> = OnceCell::new();
V.get_or_init(|| {
vec![
("staff_invincible", invincible::VERB),
("staff_reset_spawns", reset_spawns::VERB),
("staff_show", staff_show::VERB),
]
.into_iter()
.collect()
})
}
// Other commands (alphabetical except aliases grouped):
"attack" => attack::VERB,
"kill" => attack::VERB,
"k" => attack::VERB,
"describe" => describe::VERB,
"l" => look::VERB,
"look" => look::VERB,
"read" => look::VERB,
"lmap" => map::VERB,
"\'" => say::VERB,
"say" => say::VERB,
"-" => whisper::VERB,
"whisper" => whisper::VERB,
"tell" => whisper::VERB,
};
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
let mut result = always_available_commands().get(cmd);
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
match &ctx.user_dat {
None => {
result = result.or_else(|| unregistered_commands().get(cmd));
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));
}
result = result.or_else(|| REGISTERED_COMMANDS.get(cmd));
} else if cmd == "agree" {
result = Some(&agree::VERB);
}
@ -323,247 +135,83 @@ fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef>
result
}
#[cfg(not(test))]
pub async fn handle_in_trans(
session: &ListenerSession,
msg: &str,
pool: &DBPool,
trans: DBTrans,
) -> DResult<()> {
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
);
warn!("Got command from session not in database: {}", session.session);
return Ok(());
}
Some(v) => v,
Some(v) => v
};
let mut ctx = VerbContext {
session,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_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 => {
trans
.queue_for_session(
session,
trans.queue_for_session(session,
Some(ansi!(
"That's not a command I know. Try <bold>help<reset>\r\n"
)),
)
.await?;
))
).await?;
trans.commit().await?;
}
Some(handler) => match handler.handle(&mut ctx, cmd, params).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?;
pool.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?;
}
Err(SystemError(e)) => Err(e)?
}
}
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);
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
}
}
}
}
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
false
}
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
ctx.user_dat
.as_ref()
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()
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(),
)
})?)
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
))?,
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("Sorry, I couldn't find anything matching.".to_owned())?,
[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);
}
[item1, ..] =>
item1.clone(),
})
}

View File

@ -1,15 +1,14 @@
use super::{user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use crate::models::user::{User, UserTermData};
use ansi::ansi;
use async_trait::async_trait;
use base64::Engine;
use ansi::ansi;
use chrono::Utc;
pub struct Verb;
static REQUIRED_AGREEMENTS: [&str;4] = [
"I acknowledge that I am over 12 years of age, and that if I am under 16 years of age, I have permission \
from a parent or legal guardian to play the game.",
"I acknowledge that BlastMud is for adults only, and certify that I am over 18 years of age \
(or any higher relevant age of majority in my country) and want to view this content.",
"THIS GAME IS PROVIDED BY THE CREATORS, STAFF, VOLUNTEERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR \
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND \
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CREATORS, STAFF, VOLUNTEERS OR \
@ -25,11 +24,7 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
is personally identifying information, or is objectionable or abhorrent (including, without \
limitation, any content related to sexual violence, real or fictional children under 18, bestiality, \
the promotion or glorification of proscribed drug use, or fetishes that involve degrading or \
inflicting pain on someone for the enjoyment of others), or which promotes terrorism. \
I acknowledge that the game and services provided in connection with it are open to players under \
18 years of age, and agree that I will not send any sexual or age-restricted content to any player \
I have not verified is over 18, nor attempt to groom any such player for sexual activity at a later time. \
I agree to defend, indemnify, and hold \
inflicting pain on someone for the enjoyment of others). I agree to defend, indemnify, and hold \
harmless the creators, staff, volunteers and contributors in any matter relating to content sent \
(or re-sent) by me, in any matter arising from the game sending content to me, and in any matter \
consequential to sharing my password, using an insecure password, or otherwise allowing or taking \
@ -45,38 +40,33 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
fn user_mut<'a>(ctx: &'a mut VerbContext) -> UResult<&'a mut User> {
match ctx.user_dat.as_mut() {
None => Err("Checked agreements before user logged in, which is a logic error")?,
Some(user_dat) => Ok(user_dat),
Some(user_dat) => Ok(user_dat)
}
}
fn terms<'a>(ctx: &'a VerbContext<'a>) -> UResult<&'a UserTermData> {
match ctx.user_dat.as_ref() {
None => Err("Checked agreements before user logged in, which is a logic error")?,
Some(user_dat) => Ok(&user_dat.terms),
Some(user_dat) => Ok(&user_dat.terms)
}
}
fn first_outstanding_agreement(ctx: &VerbContext) -> UResult<Option<(String, String)>> {
let existing_terms = &terms(ctx)?.accepted_terms;
for agreement in REQUIRED_AGREEMENTS {
let shortcode = base64::engine::general_purpose::STANDARD_NO_PAD.encode(
ring::digest::digest(&ring::digest::SHA256, agreement.as_bytes()),
)[0..20]
.to_owned();
let shortcode =
base64::encode(ring::digest::digest(&ring::digest::SHA256,
agreement.as_bytes()))[0..20].to_owned();
match existing_terms.get(&shortcode) {
None => {
return Ok(Some((agreement.to_owned(), shortcode)));
}
None => { return Ok(Some((agreement.to_owned(), shortcode))); }
Some(_) => {}
}
}
Ok(None)
}
pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool>
where
'b: 'a,
{
pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool> where 'b: 'a {
match first_outstanding_agreement(ctx)? {
None => {
let user = user_mut(ctx)?;
@ -88,20 +78,12 @@ where
let user = user_mut(ctx)?;
user.terms.terms_complete = false;
user.terms.last_presented_term = Some(hash);
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
"Please review the following:\r\n\
\t{}\r\n\
Type <green><bold>agree<reset> to accept. If you can't or don't agree, you \
unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"
),
text
)),
)
.await?;
unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"),
text))).await?;
Ok(false)
}
}
@ -109,37 +91,24 @@ where
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
let user = user_mut(ctx)?;
match user.terms.last_presented_term.as_ref() {
None => {
drop(user);
user_error("There was nothing pending your agreement.".to_owned())?;
}
Some(last_term) => {
user.terms
.accepted_terms
.insert(last_term.to_owned(), Utc::now());
user.terms.accepted_terms.insert(last_term.to_owned(), Utc::now());
drop(user);
if check_and_notify_accepts(ctx).await? {
ctx.trans
.queue_for_session(
ctx.session,
Some(ansi!(
"That was the last of the terms to agree to - welcome onboard!\r\n\
Hint: Try <bold>l<reset> to look around.\r\n"
)),
)
.await?;
ctx.trans.queue_for_session(ctx.session, Some(
ansi!("That was the last of the terms to agree to - welcome onboard!\r\n\
Hint: Try <bold>l<reset> to look around.\r\n"))).await?;
}
}
}
ctx.trans
.save_user_model(ctx.user_dat.as_ref().unwrap())
.await?;
ctx.trans.save_user_model(ctx.user_dat.as_ref().unwrap()).await?;
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,89 +1,113 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
db::ItemSearchParams,
models::{consent::ConsentType, effect::EffectType, item::ItemFlag},
services::{check_consent, combat::start_attack},
};
use ansi::ansi;
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error,
get_player_item_or_fail, search_item_for_user};
use async_trait::async_trait;
use ansi::ansi;
use crate::services::broadcast_to_room;
use crate::db::{DBTrans, ItemSearchParams};
use crate::models::{item::{Item, LocationActionType, Subattack}};
use async_recursion::async_recursion;
pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
let mut new_to_whom = (*to_whom).clone();
if let Some(ac) = new_to_whom.active_combat.as_mut() {
let old_attacker = format!("{}/{}", by_whom.item_type, by_whom.item_code);
ac.attacked_by.retain(|v| v != &old_attacker);
trans.save_item_model(&new_to_whom).await?;
}
let mut new_by_whom = (*by_whom).clone();
if let Some(ac) = new_by_whom.active_combat.as_mut() {
ac.attacking = None;
}
new_by_whom.action_type = LocationActionType::Normal;
trans.save_item_model(&new_by_whom).await?;
Ok(())
}
#[async_recursion]
pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UResult<()> {
let mut msg_exp = String::new();
let mut msg_nonexp = String::new();
let mut verb: String = "attacks".to_string();
match by_whom.action_type {
LocationActionType::Sitting | LocationActionType::Reclining => {
msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display));
msg_nonexp.push_str(&format!(ansi!("{} stands up.\n"),
by_whom.display_less_explicit.as_ref().unwrap_or(&by_whom.display)));
},
LocationActionType::Attacking(_) => {
match by_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref().and_then(|s| s.split_once("/"))) {
Some((cur_type, cur_code)) if cur_type == to_whom.item_type && cur_code == to_whom.item_code =>
user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?,
Some((cur_type, cur_code)) => {
if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? {
stop_attacking(trans, by_whom, &cur_item_arc).await?;
}
}
_ => {}
}
verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on";
},
_ => {}
}
msg_exp.push_str(&format!(
ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(true, 1, true),
verb,
&to_whom.display_for_sentence(true, 1, false))
);
msg_nonexp.push_str(&format!(
ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(false, 1, true),
verb,
&to_whom.display_for_sentence(false, 1, false))
);
broadcast_to_room(trans, &by_whom.location, None, &msg_exp, Some(msg_nonexp.as_str())).await?;
let mut by_whom_for_update = by_whom.clone();
by_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacking =
Some(format!("{}/{}",
&to_whom.item_type, &to_whom.item_code));
by_whom_for_update.action_type = LocationActionType::Attacking(Subattack::Normal);
let mut to_whom_for_update = to_whom.clone();
to_whom_for_update.active_combat.get_or_insert_with(|| Default::default()).attacked_by.push(
format!("{}/{}",
&by_whom.item_type, &by_whom.item_code)
);
trans.save_item_model(&by_whom_for_update).await?;
trans.save_item_model(&to_whom_for_update).await?;
// Auto-counterattack if victim isn't busy.
if to_whom_for_update.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()) == None {
start_attack(trans, &to_whom_for_update, &by_whom_for_update).await?;
}
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("It doesn't really seem fair, but you realise you won't be able to attack anyone while you're dead!".to_string())?;
}
if player_item
.active_effects
.iter()
.any(|v| v.0 == EffectType::Stunned)
{
user_error("You're too stunned to attack.".to_owned())?;
}
let attack_whom = search_item_for_user(
ctx,
&ItemSearchParams {
let attack_whom = search_item_for_user(ctx, &ItemSearchParams {
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, remaining)
},
)
.await?;
let (loctype, loccode) = match player_item.location.split_once("/") {
None => user_error("Your current location is invalid!".to_owned())?,
Some(l) => l,
};
let player_loc = match ctx.trans.find_item_by_type_code(loctype, loccode).await? {
None => user_error("Your current location is invalid!".to_owned())?,
Some(l) => l,
};
if player_loc.flags.contains(&ItemFlag::NoSeeContents) {
user_error("It is too foggy to even see who is here, let alone attack!".to_owned())?;
}
}).await?;
match attack_whom.item_type.as_str() {
"npc" => {}
"player" => {}
_ => user_error("Only characters (players / NPCs) can be attacked".to_string())?,
"player" => {},
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
}
if attack_whom.item_code == player_item.item_code
&& attack_whom.item_type == player_item.item_type
{
if attack_whom.item_code == player_item.item_code && attack_whom.item_type == player_item.item_type {
user_error("That's you, silly!".to_string())?
}
if !check_consent(
ctx.trans,
"attack",
&ConsentType::Fight,
&player_item,
&attack_whom,
)
.await?
{
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an consented is very much functional. [Try <bold>help allow<reset>]").to_string())?
}
if attack_whom.death_data.is_some() {
user_error("There's no point attacking the dead!".to_string())?
}
if attack_whom.active_climb.is_some() {
user_error("They are already climbing away!".to_string())?
if attack_whom.is_challenge_attack_only {
// Add challenge check here.
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an accepted challenge very much functional. [Try <bold>help challenge<reset>]").to_string())?
}
start_attack(&ctx.trans, &player_item, &attack_whom).await

View File

@ -1,91 +0,0 @@
use super::{
cut::ensure_has_butcher_tool, get_player_item_or_fail, search_item_for_user, user_error,
UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::item::DeathData,
regular_tasks::queued_command::{queue_command, QueueCommand},
services::combat::corpsify_item,
static_content::possession_type::possession_data,
};
use async_trait::async_trait;
use std::sync::Arc;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?
}
let possible_corpse = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
dead_first: true,
..ItemSearchParams::base(&player_item, remaining.trim())
},
)
.await?;
let possession_types = match possible_corpse.death_data.as_ref() {
None => user_error(format!(
"You can't do that while {} is still alive!",
possible_corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining,
}
.clone();
let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse
} else if possible_corpse.item_type == "npc" || possible_corpse.item_type == "player" {
let mut possible_corpse_mut = (*possible_corpse).clone();
possible_corpse_mut.location = if possible_corpse.item_type == "npc" {
"room/valhalla"
} else {
"room/repro_xv_respawn"
}
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else {
user_error("You can't butcher that!".to_owned())?
};
ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
let mut player_item_mut = (*player_item).clone();
for possession_type in possession_types {
let possession_data = possession_data()
.get(&possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,191 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_offset, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::item::Item,
services::{
capacity::{check_item_capacity, check_item_ref_capacity, CapacityLevel},
urges::change_stress_considering_cool,
},
static_content::possession_type::possession_data,
static_content::room,
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use mockall_double::double;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"Nobody seems to listen when you try to buy... possibly because you're dead."
.to_owned(),
)?
}
let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
user_error("Can't buy anything because you're not in a shop.".to_owned())?;
}
let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r,
};
if room.stock_list.is_empty() {
user_error("Can't buy anything because you're not in a shop.".to_owned())?
}
let (offset_m, remaining) = parse_offset(remaining);
let mut offset_remaining = offset_m.unwrap_or(1);
let match_item = remaining.trim().to_lowercase();
if match_item == "" {
user_error("You need to specify what to buy.".to_owned())?
}
for stock in &room.stock_list {
if !stock.can_buy {
continue;
}
if let Some(possession_type) = possession_data().get(&stock.possession_type) {
if possession_type
.display
.to_lowercase()
.starts_with(&match_item)
|| possession_type
.aliases
.iter()
.any(|al| al.starts_with(&match_item))
{
if offset_remaining <= 1 {
if let Some(user) = ctx.user_dat.as_mut() {
let mut paid_price = stock.list_price;
if user.credits < stock.list_price {
if stock.poverty_discount {
if let Some(urges) = player_item.urges.as_ref() {
if urges.stress.value > 500 {
user_error(ansi!("You don't have the money to pay full price, \
and are too tired to try begging for a \
discount. [Hint: Try to <bold>recline<reset> \
for a bit and try again]").to_owned())?;
}
let mut player_item_mut = (*player_item).clone();
change_stress_considering_cool(
&ctx.trans,
&mut player_item_mut,
2000,
)
.await?;
ctx.trans.save_item_model(&player_item_mut).await?;
}
paid_price = user.credits;
user.credits = 0;
} else {
user_error(
"You don't have enough credits to buy that!".to_owned(),
)?;
}
} else {
user.credits -= stock.list_price;
}
let player_item_str = player_item.refstr();
let item_code = ctx.trans.alloc_item_code().await?;
let loc = match check_item_capacity(
ctx.trans,
&player_item,
possession_type.weight,
)
.await?
{
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
match check_item_ref_capacity(
ctx.trans,
&player_item.location,
possession_type.weight,
)
.await?
{
CapacityLevel::AboveItemLimit => user_error(
"You can't carry it, and there is too much stuff \
here already"
.to_owned(),
)?,
_ => {
ctx.trans.queue_for_session(
&ctx.session,
Some(
"It's too much for you to carry so you leave it on the ground.\n")
).await?;
&player_item.location
}
}
}
_ => &player_item_str,
};
let new_item = Item {
item_code: format!("{}", item_code),
location: loc.to_owned(),
..stock.possession_type.clone().into()
};
ctx.trans.create_item(&new_item).await?;
create_item_default_contents(&ctx.trans, &new_item).await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"Your wristpad beeps for a deduction of {} credits.\n",
paid_price
)),
)
.await?;
}
return Ok(());
} else {
offset_remaining -= 1;
}
}
}
}
user_error(ansi!("That doesn't seem to be for sale. Try <bold>list<reset>").to_owned())
}
}
pub async fn create_item_default_contents(trans: &DBTrans, item: &Item) -> DResult<()> {
if let Some(container_data) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pt| pt.container_data.as_ref())
{
for sub_possession_type in &container_data.default_contents {
let sub_item_code = trans.alloc_item_code().await?;
let new_sub_item = Item {
item_code: format!("{}", sub_item_code),
location: item.refstr(),
..sub_possession_type.clone().into()
};
trans.create_item(&new_sub_item).await?;
}
}
Ok(())
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,72 +0,0 @@
use super::{
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::models::corp::CorpCommType;
use ansi::{ansi, ignore_special_characters};
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<()> {
let user = get_user_or_fail(ctx)?;
let (corp_id, corp, msg) = if remaining.starts_with("@") {
match remaining[1..].split_once(" ") {
None => user_error(
"Usage: c message (lowest ordered corp) or c @corpname message".to_owned(),
)?,
Some((corpname, msg)) => {
let (corp_id, corp, _) = match ctx
.trans
.match_user_corp_by_name(corpname.trim(), &user.username)
.await?
{
None => {
user_error("You don't seem to belong to a matching corp!".to_owned())?
}
Some(c) => c,
};
(corp_id, corp, msg.trim())
}
}
} else {
let (corp_id, corp) = match ctx.trans.get_default_corp_for_user(&user.username).await? {
None => user_error("You're not a member of any corps.".to_owned())?,
Some(v) => v,
};
(corp_id, corp, remaining.trim())
};
let msg = ignore_special_characters(msg);
if msg.trim() == "" {
user_error("Message required.".to_owned())?;
}
let player = get_player_item_or_fail(ctx).await?;
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Chat,
Some(&user.username),
&format!(
ansi!("<yellow>{} (to {}): <reset><bold>\"{}\"<reset>\n"),
&player.display_for_sentence(1, true),
&corp.name,
&msg
),
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,160 +0,0 @@
use super::{
get_player_item_or_fail,
open::{is_door_in_direction, DoorSituation},
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::DoorState,
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::comms::broadcast_to_room,
static_content::room::Direction,
};
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
let direction = match ctx.command {
QueueCommand::CloseDoor { direction } => direction,
_ => user_error("Unexpected queued command".to_owned())?,
};
let use_location = if ctx.item.death_data.is_some() {
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
} else {
&ctx.item.location
};
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
DoorSituation::DoorIntoRoom {
state: DoorState { open: false, .. },
..
}
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: false, .. },
..
} => user_error("The door is already closed.".to_owned())?,
_ => {}
}
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
let direction = match ctx.command {
QueueCommand::CloseDoor { direction } => direction,
_ => user_error("Unexpected queued command".to_owned())?,
};
let use_location = if ctx.item.death_data.is_some() {
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
} else {
&ctx.item.location
};
let (room_1, dir_in_room, room_2) =
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
DoorSituation::DoorIntoRoom {
state: DoorState { open: false, .. },
..
}
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: false, .. },
..
} => user_error("The door is already closed.".to_owned())?,
DoorSituation::DoorIntoRoom {
room_with_door,
current_room,
..
} => {
if let Some(revdir) = direction.reverse() {
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&revdir) {
(*door).open = false;
}
};
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, revdir, current_room)
} else {
user_error("There's no door possible there.".to_owned())?
}
}
DoorSituation::DoorOutOfRoom {
room_with_door,
new_room,
..
} => {
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) {
(*door).open = false;
}
}
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, direction.clone(), new_room)
}
};
for (loc, dir) in [
(&room_1.refstr(), &dir_in_room.describe()),
(
&room_2.refstr(),
&dir_in_room
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned()),
),
] {
broadcast_to_room(
&ctx.trans,
loc,
None,
&format!(
"{} closes the door to the {}.\n",
&ctx.item.display_for_sentence(1, true),
dir
),
)
.await?;
}
ctx.trans
.delete_task(
"SwingShut",
&format!("{}/{}", &room_1.refstr(), &dir_in_room.describe()),
)
.await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let dir =
Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?;
let player_item = get_player_item_or_fail(ctx).await?;
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::CloseDoor {
direction: dir.clone(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,995 +0,0 @@
use super::{
get_player_item_or_fail, get_user_or_fail, parsing::parse_command_name,
rent::recursively_destroy_or_move_item, search_item_for_user, user_error, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
language::caps_first,
models::corp::{CorpCommType, CorpMembership, CorpPermission},
};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use chrono::Utc;
use humantime;
use itertools::Itertools;
use std::collections::BTreeSet;
pub fn check_corp_perm(perm: &CorpPermission, mem: &CorpMembership) -> bool {
mem.permissions
.iter()
.any(|p| *p == CorpPermission::Holder || *p == *perm)
}
async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (target_raw, into_raw) = match remaining.rsplit_once(" into ") {
None => user_error(
ansi!("Usage: <bold>corp hire<reset> username <bold>into<reset> corpname").to_owned(),
)?,
Some(c) => c,
};
let user = get_user_or_fail(ctx)?;
let player = get_player_item_or_fail(ctx).await?;
let (corp_id, corp, mem) = match ctx
.trans
.match_user_corp_by_name(into_raw.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if !check_corp_perm(&CorpPermission::Hire, &mem) || mem.joined_at.is_none() {
user_error("You don't have hiring permissions for that corp".to_owned())?;
}
let target_user = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player, target_raw.trim())
},
)
.await?;
if target_user.item_type != "player" {
user_error("Only players can be hired.".to_owned())?;
}
let (their_sess, _their_sess_dat) = match ctx
.trans
.find_session_for_player(&target_user.item_code)
.await?
{
None => user_error("The user needs to be logged in while you hire them.".to_owned())?,
Some(c) => c,
};
if ctx.trans.list_corp_members(&corp_id).await?.len() > 40 {
user_error(
"Your corp seems a bit too big to manage already \
- fire someone first!"
.to_owned(),
)?;
}
ctx.trans.expire_old_invites().await?;
match ctx
.trans
.match_user_corp_by_name(into_raw.trim(), &target_user.item_code)
.await?
{
None => (),
Some((
_,
_,
CorpMembership {
invited_at: Some(_),
..
},
)) => user_error(format!(
"{}'s already been invited.",
&caps_first(&target_user.pronouns.subject)
))?,
Some((_, _, _)) => user_error(format!(
"{}'s already hired.",
&caps_first(&target_user.pronouns.subject)
))?,
};
let new_mem = CorpMembership {
invited_at: Some(Utc::now()),
allow_combat: corp.allow_combat_required,
permissions: corp.member_permissions.clone(),
..Default::default()
};
ctx.trans
.upsert_corp_membership(&corp_id, &target_user.item_code, &new_mem)
.await?;
ctx.trans.queue_for_session(&their_sess, Some(&format!(
ansi!("{} wants to hire you into {}! Type <bold>corp join {}<reset> to accept.{}\n"),
&player.display_for_sentence(1, true),
&corp.name,
&corp.name,
if new_mem.allow_combat {
" This corp is configured to allow the leadership to declare war on other corps; if you join, you may be attacked by members of other corps, even without your personal consent."
} else {
""
}
))).await?;
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
"{} has invited {} to join {}!\n",
user.username,
&target_user.display_for_sentence(1, false),
corp.name
),
)
.await?;
Ok(())
}
async fn corp_join(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let user = get_user_or_fail(ctx)?;
let (corpid, corp, mut mem) = match ctx
.trans
.match_user_corp_by_name(remaining, &user.username)
.await?
{
None => user_error("Corp not found".to_owned())?,
Some(v) => v,
};
if mem.joined_at.is_some() {
user_error("You have already joined that corp!".to_owned())?;
}
mem.joined_at = Some(Utc::now());
mem.invited_at = None;
ctx.trans
.upsert_corp_membership(&corpid, &user.username, &mem)
.await?;
ctx.trans
.broadcast_to_corp(
&corpid,
&CorpCommType::Notice,
None,
&format!(
ansi!("There is a loud <red>cheer<reset> as {} accepts an offer to join {}!\n"),
&user.username, &corp.name
),
)
.await?;
Ok(())
}
async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> {
let mut msg = String::new();
let user = get_user_or_fail(ctx)?;
let corps = ctx
.trans
.get_corp_memberships_for_user(&user.username)
.await?;
if corps.is_empty() {
msg.push_str(ansi!(
"You don't yet belong to any corps - try <bold>who<reset> to see the \
corps of people online, and message someone in a corp to see if they \
will hire you! Or buy a new corp licence at the Kings Office in Melbs.\n"
));
} else {
msg.push_str("You belong to the following corps:\n");
msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:20} | {:7} | {:5} |<reset>\n"),
"Name", "Combat?", "Order"
));
for corp in &corps {
match corp {
(
_,
name,
CorpMembership {
allow_combat: combat,
priority: p,
..
},
) => {
msg.push_str(&format!(
"| {:20} | {:7} | {:5} |\n",
name,
if *combat { "Y" } else { "N" },
p
));
}
}
}
}
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let user = get_user_or_fail(ctx)?;
let (corpid, corp, mem) = match ctx
.trans
.match_user_corp_by_name(remaining, &user.username)
.await?
{
None => user_error("Corp not found".to_owned())?,
Some(v) => v,
};
if mem.joined_at.is_some() {
ctx.trans
.broadcast_to_corp(
&corpid,
&CorpCommType::Notice,
None,
&format!(
ansi!("There is a loud <red>boo<reset> as {} resigns from {}!\n"),
user.username, corp.name
),
)
.await?;
} else {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
"You decline your invitation to join {}\n",
corp.name
)),
)
.await?;
}
let mut delete_corp = false;
if mem.permissions.contains(&CorpPermission::Holder) {
let username_l = user.username.to_lowercase();
let members = ctx.trans.list_corp_members(&corpid).await?;
if members.len() == 1 {
delete_corp = true;
} else if !members.iter().any(|(name, mem)| {
*name != username_l
&& mem.joined_at.is_some()
&& mem.permissions.contains(&CorpPermission::Holder)
}) {
user_error(
"The last holder cannot resign from a non-empty \
corp - fire everyone else first, or promote a \
successor to holder"
.to_owned(),
)?;
}
}
ctx.trans
.delete_corp_membership(&corpid, &user.username)
.await?;
if delete_corp {
for dynzone in ctx
.trans
.find_dynzone_for_owner(&format!("corp/{}", &corp.name))
.await?
{
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_corp(&corpid).await?;
}
Ok(())
}
async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (target_raw, into_raw) = match remaining.rsplit_once(" from ") {
None => user_error(
ansi!("Usage: <bold>corp fire<reset> username <bold>from<reset> corpname").to_owned(),
)?,
Some(c) => c,
};
let user = get_user_or_fail(ctx)?;
let player = get_player_item_or_fail(ctx).await?;
let (corp_id, corp, mem) = match ctx
.trans
.match_user_corp_by_name(into_raw.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if !check_corp_perm(&CorpPermission::Fire, &mem) || mem.joined_at.is_none() {
user_error("You don't have firing permissions for that corp".to_owned())?;
}
let target_user = search_item_for_user(
ctx,
&ItemSearchParams {
include_all_players: true,
..ItemSearchParams::base(&player, target_raw.trim())
},
)
.await?;
if target_user.item_type != "player" {
user_error("Only players can be fired.".to_owned())?;
}
if target_user.item_code == player.item_code {
user_error("Fire yourself? You know you can just resign right?".to_owned())?;
}
match ctx
.trans
.match_user_corp_by_name(into_raw.trim(), &target_user.item_code)
.await?
{
None => user_error(format!(
"{} isn't currently hired.",
&caps_first(&target_user.pronouns.subject)
))?,
Some((
_,
_,
CorpMembership {
permissions: their_perm,
joined_at: Some(their_join),
..
},
)) => {
if their_perm.contains(&CorpPermission::Holder) {
if !mem.permissions.contains(&CorpPermission::Holder) {
user_error(
"I love the ambition, but only holders can fire holders!".to_owned(),
)?;
}
if their_join < mem.joined_at.unwrap_or(Utc::now()) {
user_error(
"Whoah there young whippersnapper, holders can't fire more senior holders!"
.to_owned(),
)?;
}
}
}
_ => {}
}
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
ansi!(
"A <blue>nervous silence<reset> falls across \
the workers as {} fires {} from {}.\n"
),
user.username,
target_user.display_for_sentence(1, false),
corp.name
),
)
.await?;
ctx.trans
.delete_corp_membership(&corp_id, &target_user.item_code)
.await?;
Ok(())
}
async fn corp_promote(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let usage_error = || {
user_error(
ansi!("Usage: <bold>corp promote<reset> username <bold>in<reset> corpname <bold>to<reset> title <bold>privileges<reset> privileges\n\
Title can be any plain text up to 20 characters long.\n\
Permissions start with + or - (to add or take away) followed immediately by a permission name (e.g. holder, hire, fire, war)").to_owned()
)
};
let (remaining, permissions_raw) = match remaining.rsplit_once(" privileges ") {
None => usage_error()?,
Some(c) => c,
};
let (target_raw, remaining) = match remaining.split_once(" in ") {
None => usage_error()?,
Some(c) => c,
};
let (corpname_raw, title_raw) = match remaining.split_once(" to ") {
None => usage_error()?,
Some(c) => c,
};
let title = ignore_special_characters(title_raw.trim());
if title.len() > 20 {
user_error("New title must be 20 characters or less".to_owned())?;
}
let psplit = permissions_raw.split(" ");
let mut perm_add: BTreeSet<CorpPermission> = BTreeSet::new();
let mut perm_rem: BTreeSet<CorpPermission> = BTreeSet::new();
for perm in psplit {
let add = if perm.starts_with("+") {
true
} else if perm.starts_with("-") {
false
} else {
user_error(format!(
"Expected {} to start with + or -",
ignore_special_characters(perm)
))?
};
let perm = ignore_special_characters(&perm[1..]).to_lowercase();
let perm = match CorpPermission::parse(&perm) {
None => user_error(format!("Unknown permission {}", perm))?,
Some(v) => v,
};
(if add { &mut perm_add } else { &mut perm_rem }).insert(perm);
}
match perm_add.intersection(&perm_rem).next() {
Some(perm) => user_error(format!(
"You tried to both add and remove privilege {} - make up your mind!",
perm.display()
))?,
None => {}
}
let user = get_user_or_fail(ctx)?;
let player = get_player_item_or_fail(ctx).await?;
let (corp_id, corp, mem) = match ctx
.trans
.match_user_corp_by_name(corpname_raw.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if !check_corp_perm(&CorpPermission::Promote, &mem) || mem.joined_at.is_none() {
user_error("You don't have promote permissions for that corp".to_owned())?;
}
let target_user = search_item_for_user(
ctx,
&ItemSearchParams {
include_all_players: true,
..ItemSearchParams::base(&player, target_raw.trim())
},
)
.await?;
if target_user.item_type != "player" {
user_error("Only players can be promoted.".to_owned())?;
}
let mut their_mem = match ctx
.trans
.match_user_corp_by_name(corpname_raw.trim(), &target_user.item_code)
.await?
{
None => user_error(format!(
"{} isn't currently hired.",
&caps_first(&target_user.pronouns.subject)
))?,
Some((_, _, v)) => v,
};
match their_mem {
CorpMembership {
permissions: ref their_perm,
joined_at: Some(their_join),
..
} => {
if their_perm.contains(&CorpPermission::Holder) {
if !mem.permissions.contains(&CorpPermission::Holder) {
user_error(
"I love the ambition, but only holders can promote/demote holders!"
.to_owned(),
)?;
}
if their_join < mem.joined_at.unwrap_or(Utc::now()) {
user_error("Whoah there young whippersnapper, holders can't promote/demote more senior holders!".to_owned())?;
}
}
}
_ => {}
}
if target_user.item_code == player.item_code {
if !mem.permissions.contains(&CorpPermission::Holder) {
user_error("Only holders can promote / demote themselves".to_owned())?
} else if perm_rem.contains(&CorpPermission::Holder) {
let members = ctx.trans.list_corp_members(&corp_id).await?;
if !members.iter().any(|(name, mem)| {
*name != player.item_code
&& mem.joined_at.is_some()
&& mem.permissions.contains(&CorpPermission::Holder)
}) {
user_error(
"The last holder cannot demote themselves - \
promote a successor to holder first"
.to_owned(),
)?
}
}
}
if !mem.permissions.contains(&CorpPermission::Holder)
&& !(&perm_add | &perm_rem).is_subset(&mem.permissions.clone().into_iter().collect())
{
user_error("You can only change permissions you have yourself.".to_owned())?
}
let perm_str_raw = perm_add
.iter()
.map(|v| "+".to_owned() + v.display())
.join(" ")
+ " "
+ &perm_rem
.iter()
.map(|v| "-".to_owned() + v.display())
.join(" ");
ctx.trans.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice, None,
&format!("Everyone looks up from their desk as {} changes {}'s job title in {} to {} ({}).\n",
user.username,
target_user.display_for_sentence(1, false),
corp.name, &title, &perm_str_raw.trim())).await?;
their_mem.job_title = title;
their_mem.permissions = (&(&their_mem
.permissions
.clone()
.into_iter()
.collect::<BTreeSet<CorpPermission>>()
| &perm_add)
- &perm_rem)
.into_iter()
.collect();
ctx.trans
.upsert_corp_membership(&corp_id, &target_user.item_code, &their_mem)
.await?;
Ok(())
}
async fn corp_info(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let user = get_user_or_fail(ctx)?;
let (corp_id, corp, _) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
let mut msg = String::new();
let founded_ago = humantime::format_duration(std::time::Duration::from_secs(
(Utc::now() - corp.founded).num_seconds() as u64,
));
msg.push_str(&format!(
ansi!("<bold>{}'s essential information<reset>\nFounded: {} ago\nCredits: ${}\nTax rate: {}%\n"),
&corp.name, &founded_ago, corp.credits, corp.tax as f64 * 1E-2
));
let members = ctx.trans.list_corp_members(&corp_id).await?;
if corp.allow_combat_required {
msg.push_str(
"Members ARE required to allow the corp to consent to fighting on their behalf",
);
let tot_mem = members.len();
let mem_allow_combat = members.iter().filter(|(_, m)| m.allow_combat).count();
if tot_mem == mem_allow_combat {
msg.push_str(", and all members have done so.\n");
} else {
msg.push_str("- waiting on the following members to do so: ");
for (u, _) in members.iter().filter(|(_, m)| !m.allow_combat) {
msg.push_str(&caps_first(&u));
msg.push(' ');
}
msg.push('\n');
}
} else {
msg.push_str(
"Members are NOT required to allow the corp to consent to fighting on their behalf.\n",
);
}
msg.push_str(&format!(
"Base member privileges: {}\n",
&corp
.member_permissions
.iter()
.map(|p| p.display())
.join(" ")
));
msg.push_str("Members:\n");
msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:20} | {:20} | {:20} |<reset>\n"),
"Name", "Title", "Privileges"
));
for (user, mem) in members {
msg.push_str(&format!(
ansi!("| {:20} | {:20} | {:20} |\n"),
caps_first(&user),
mem.job_title,
mem.permissions.iter().map(|p| p.display()).join(" ")
));
}
ctx.trans
.queue_for_session(&ctx.session, Some(&msg))
.await?;
Ok(())
}
async fn corp_allow_combat(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (command, remaining) = parse_command_name(remaining);
if command != "from" {
user_error("Usage: corp allow combat from corpname".to_owned())?;
}
let user = get_user_or_fail(ctx)?;
let (corp_id, corp, mut mem) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if mem.allow_combat {
user_error(format!("You already allow combat for {}.", &corp.name))?;
}
mem.allow_combat = true;
ctx.trans
.upsert_corp_membership(&corp_id, &user.username.to_lowercase(), &mem)
.await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some("You now allow the corp to consent to fighting on your behalf.\n"),
)
.await?;
Ok(())
}
async fn corp_disallow_combat(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (command, remaining) = parse_command_name(remaining);
if command != "from" {
user_error("Usage: corp disallow combat from corpname".to_owned())?
}
let user = get_user_or_fail(ctx)?;
let (corp_id, corp, mut mem) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if !mem.allow_combat {
user_error("You already disallow combat for that corp.".to_owned())?;
}
if corp.allow_combat_required {
user_error(format!(
ansi!(
"That corp requires everyone to allow combat as a condition \
of employment, so you can't do that. You could do \
<bold>corp resign {}<reset> instead."
),
&corp.name
))?;
}
mem.allow_combat = false;
ctx.trans
.upsert_corp_membership(&corp_id, &user.username.to_lowercase(), &mem)
.await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some("You no longer allow the corp to consent to fighting on your behalf.\n"),
)
.await?;
Ok(())
}
async fn corp_config(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (corp_name_raw, remaining) = match remaining.split_once(" ") {
None => user_error("Usage: corp config corpname ...".to_owned())?,
Some(v) => v,
};
let remaining = remaining.trim().to_lowercase();
let user = get_user_or_fail(ctx)?;
let (corp_id, mut corp, mem) = match ctx
.trans
.match_user_corp_by_name(corp_name_raw.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
if !check_corp_perm(&CorpPermission::Configure, &mem) {
user_error("You don't have permission to change settings for that corp.".to_owned())?;
}
if remaining == "allow combat required" {
corp.allow_combat_required = true;
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
"{} announces a policy decision for {}: all new members must allow the \
corp to consent to fights on their behalf.\n",
&caps_first(&user.username),
&corp.name
),
)
.await?;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
} else if remaining == "allow combat not required" {
corp.allow_combat_required = false;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
ctx.trans.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice, None,
&format!("{} announces a policy decision for {}: new members no longer need to allow the \
corp to consent to fights on their behalf. Any member can now use \
\"corp disallow combat from {}\" if they want to.\n",
&caps_first(&user.username),
&corp.name, &corp.name)).await?;
} else if remaining.starts_with("base privileges ") {
let remaining = remaining[("base privileges ".len())..].trim();
let psplit = remaining.split(" ");
let mut perms: BTreeSet<CorpPermission> = BTreeSet::new();
for perm in psplit {
let perm = ignore_special_characters(&perm.trim()).to_lowercase();
if perm == "none" {
continue;
}
let perm = match CorpPermission::parse(&perm) {
None => user_error(format!("Unknown permission {}", perm))?,
Some(p) if p == CorpPermission::Holder => {
user_error("You can't set holder as a base privilege.".to_owned())?
}
Some(p) if !check_corp_perm(&p, &mem) => {
user_error("You can't set base privilege you don't have yourself.".to_owned())?
}
Some(p) => p,
};
perms.insert(perm);
}
corp.member_permissions = perms.clone().into_iter().collect();
let perm_str = &corp
.member_permissions
.iter()
.map(|p| p.display())
.join(" ");
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
"{} announces a policy decision for {}: new members get \
the following privileges: {}.\n",
&caps_first(&user.username),
&corp.name,
if perm_str == "" { "none" } else { &perm_str }
),
)
.await?;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
} else if remaining.starts_with("tax rate ") {
let remaining = remaining[("tax rate ".len())..].trim();
let remaining = match remaining.split_once("%") {
None => remaining,
Some((r, e)) => {
if e != "" {
user_error("Tax rate must be a number".to_owned())?
} else {
r.trim()
}
}
};
let rate: f32 = match remaining.parse::<f32>() {
Err(_) => user_error("Tax rate must be a number".to_owned())?,
Ok(r) => r,
};
if rate < 0.0 || rate >= 80.0 {
user_error("Tax rate must be 0-80%".to_owned())?;
}
corp.tax = (rate * 100.0) as u16;
ctx.trans.update_corp_details(&corp_id, &corp).await?;
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
"{} has changed the tax rate for {} to {}%\n",
user.username,
corp.name,
(corp.tax as f64) / 100.0
),
)
.await?;
} else {
user_error("Configurations you can set:\n\tallow combat required\n\tallow combat not required\nbase permissions permission_name permission_name\ntax rate 12.34%".to_owned())?;
}
Ok(())
}
async fn corp_subscribe(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let user = get_user_or_fail(ctx)?;
let (subs, remaining) = match remaining.split_once(" from ") {
None => {
let (_, corp, mem) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"Subscriptions for {}: {}\n",
&corp.name,
&mem.comms_on.iter().map(|m| m.display()).join(" ")
)),
)
.await?;
return Ok(());
}
Some(v) => v,
};
let mut subs_add = BTreeSet::<CorpCommType>::new();
for sub in subs.trim().split(" ") {
let sub = ignore_special_characters(&sub.trim());
match CorpCommType::parse(&sub) {
None => user_error(format!("Invalid commtype: {}", sub))?,
Some(t) => subs_add.insert(t),
};
}
let (corp_id, _, mut mem) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
mem.comms_on = (&mem.comms_on.into_iter().collect::<BTreeSet<CorpCommType>>() | &subs_add)
.into_iter()
.collect();
ctx.trans
.upsert_corp_membership(&corp_id, &user.username, &mem)
.await?;
ctx.trans
.queue_for_session(&ctx.session, Some("Subscriptions updated.\n"))
.await?;
Ok(())
}
async fn corp_unsubscribe(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (subs, remaining) = match remaining.split_once(" from ") {
None => user_error(
"Usage: corp unsubscribe commtype commtype ... from corpname\n\
commtypes: chat notice connect reward death consent"
.to_owned(),
)?,
Some(v) => v,
};
let mut subs_del = BTreeSet::<CorpCommType>::new();
for sub in subs.trim().split(" ") {
let sub = ignore_special_characters(&sub.trim());
match CorpCommType::parse(&sub) {
None => user_error(format!("Invalid commtype: {}", sub))?,
Some(t) => subs_del.insert(t),
};
}
let user = get_user_or_fail(ctx)?;
let (corp_id, _, mut mem) = match ctx
.trans
.match_user_corp_by_name(remaining.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
mem.comms_on = (&mem.comms_on.into_iter().collect::<BTreeSet<CorpCommType>>() - &subs_del)
.into_iter()
.collect();
ctx.trans
.upsert_corp_membership(&corp_id, &user.username, &mem)
.await?;
ctx.trans
.queue_for_session(&ctx.session, Some("Subscriptions updated.\n"))
.await?;
Ok(())
}
async fn corp_order(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> {
let (corpname, number_s) = match remaining.split_once(" as ") {
None => user_error("Usage: corp order corpname as number".to_owned())?,
Some(v) => v,
};
let number = match nom::character::complete::i64::<&str, ()>(number_s) {
Ok((rest, num)) if rest.trim() == "" => num,
_ => user_error("Usage: corp order corpname as number".to_owned())?,
};
let user = get_user_or_fail(ctx)?;
let (corp_id, _, mut mem) = match ctx
.trans
.match_user_corp_by_name(corpname.trim(), &user.username)
.await?
{
None => user_error("You don't seem to belong to a matching corp!".to_owned())?,
Some(c) => c,
};
mem.priority = number;
ctx.trans
.upsert_corp_membership(&corp_id, &user.username, &mem)
.await?;
ctx.trans
.queue_for_session(&ctx.session, Some("Updated!\n"))
.await?;
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (command, remaining) = parse_command_name(remaining);
match command.to_lowercase().as_str() {
"hire" | "invite" => corp_invite(ctx, remaining).await?,
"join" => corp_join(ctx, remaining).await?,
"" | "list" => corp_list(ctx, remaining).await?,
"leave" | "resign" => corp_leave(ctx, remaining).await?,
"fire" | "dismiss" => corp_fire(ctx, remaining).await?,
"promote" | "demote" => corp_promote(ctx, remaining).await?,
"info" => corp_info(ctx, remaining).await?,
"allow" => {
let (command, remaining) = parse_command_name(remaining);
if command.to_lowercase() != "combat" {
user_error("Allow must be followed with combat".to_owned())?
} else {
corp_allow_combat(ctx, remaining).await?
}
}
"disallow" => {
let (command, remaining) = parse_command_name(remaining);
if command.to_lowercase() != "combat" {
user_error("Disallow must be followed with combat".to_owned())?
} else {
corp_disallow_combat(ctx, remaining).await?
}
}
"configure" | "config" => {
corp_config(ctx, remaining).await?;
}
"sub" | "subscribe" => {
corp_subscribe(ctx, remaining).await?;
}
"unsub" | "unsubscribe" => {
corp_unsubscribe(ctx, remaining).await?;
}
"order" => {
corp_order(ctx, remaining).await?;
}
_ => user_error("Unknown command".to_owned())?,
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,358 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
db::ItemSearchParams,
language::join_words,
models::item::{DeathData, Item, SkillType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, CapacityLevel},
combat::corpsify_item,
comms::broadcast_to_room,
destroy_container,
skills::skill_check_and_grind,
urges::change_stress_considering_cool,
},
static_content::possession_type::{can_butcher_possessions, possession_data},
};
use ansi::ansi;
use async_trait::async_trait;
use mockall_double::double;
use std::{sync::Arc, time};
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?;
}
if ctx.item.urges.as_ref().map(|u| u.stress.value).unwrap_or(0) > 7000 {
user_error(
ansi!(
"You are too tired and stressed to consider crafts. Maybe try to \
<bold>sit<reset> or <bold>recline<reset> for a bit!"
)
.to_owned(),
)?;
}
let (from_corpse_id, what_part) = match ctx.command {
QueueCommand::Cut {
from_corpse,
what_part,
} => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?,
};
let corpse = match ctx
.trans
.find_item_by_type_code("corpse", &from_corpse_id)
.await?
{
None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it,
};
if corpse.location != ctx.item.location {
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(1, false)
))?
}
ensure_has_butcher_tool(&ctx.trans, &ctx.item).await?;
match corpse.death_data.as_ref() {
None => user_error(format!(
"You can't do that while {} is still alive!",
corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => {
if !parts_remaining.iter().any(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part)
== Some(true)
}) {
user_error(format!(
"That part ({}) is now gone. Parts you can cut: {}",
&what_part,
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))?;
}
}
};
let msg = format!(
"{} prepares to cut {} from {}\n",
&ctx.item.display_for_sentence(1, true),
&what_part,
&corpse.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?;
}
let (from_corpse_id, what_part) = match ctx.command {
QueueCommand::Cut {
from_corpse,
what_part,
} => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?,
};
ensure_has_butcher_tool(&ctx.trans, &ctx.item).await?;
let corpse = match ctx
.trans
.find_item_by_type_code("corpse", &from_corpse_id)
.await?
{
None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it,
};
if corpse.location != ctx.item.location {
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(1, false)
))?
}
let possession_type = match corpse.death_data.as_ref() {
None => user_error(format!(
"You can't do that while {} is still alive!",
corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining
.iter()
.find(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part)
== Some(true)
})
.ok_or_else(|| {
UserError(format!(
"Parts you can cut: {}",
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))
})?,
};
let possession_data = possession_data()
.get(possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
let mut corpse_mut = (*corpse).clone();
match corpse_mut.death_data.as_mut() {
None => {}
Some(dd) => {
dd.parts_remaining = dd
.parts_remaining
.iter()
.take_while(|pt| pt != &possession_type)
.chain(
dd.parts_remaining
.iter()
.skip_while(|pt| pt != &possession_type)
.skip(1),
)
.map(|pt| (*pt).clone())
.collect()
}
}
match check_item_capacity(&ctx.trans, &ctx.item, possession_data.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("You have too much stuff to take that on!".to_owned())?
}
_ => {}
}
if corpse_mut
.death_data
.as_ref()
.map(|dd| dd.parts_remaining.is_empty())
== Some(true)
{
destroy_container(&ctx.trans, &corpse_mut).await?;
} else {
ctx.trans.save_item_model(&corpse_mut).await?;
}
if skill_check_and_grind(&ctx.trans, ctx.item, &SkillType::Craft, 10.0).await? < 0.0 {
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 500).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} tries to cut the {} from {}, but only leaves a mutilated mess.\n",
&ctx.item.display_for_sentence(1, true),
possession_data.display,
corpse.display_for_sentence(1, false)
),
)
.await?;
} else {
let mut new_item: Item = (*possession_type).clone().into();
new_item.item_code = format!("{}", ctx.trans.alloc_item_code().await?);
new_item.location = ctx.item.refstr();
ctx.trans.save_item_model(&new_item).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} expertly cuts the {} from {}.\n",
&ctx.item.display_for_sentence(1, true),
possession_data.display,
corpse.display_for_sentence(1, false)
),
)
.await?;
}
Ok(())
}
}
pub async fn ensure_has_butcher_tool(trans: &DBTrans, player_item: &Item) -> UResult<()> {
if trans
.count_matching_possessions(&player_item.refstr(), &can_butcher_possessions())
.await?
< 1
{
user_error("You have nothing sharp on you suitable for butchery!".to_owned())?;
}
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (what_raw, corpse_raw) = match remaining.split_once(" from ") {
None => user_error(
ansi!("Usage: <bold>cut<reset> thing <bold>from<reset> corpse").to_owned(),
)?,
Some(v) => v,
};
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?
}
let possible_corpse = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
dead_first: true,
..ItemSearchParams::base(&player_item, corpse_raw.trim())
},
)
.await?;
let what_norm = what_raw.trim().to_lowercase();
let possession_type = match possible_corpse.death_data.as_ref() {
None => user_error(format!(
"You can't do that while {} is still alive!",
possible_corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining
.iter()
.find(|pt| {
possession_data().get(pt).map(|pd| {
pd.display.to_lowercase() == what_norm
|| pd.aliases.iter().any(|a| a.to_lowercase() == what_norm)
}) == Some(true)
})
.ok_or_else(|| {
UserError(format!(
"Parts you can cut: {}",
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))
})?,
}
.clone();
let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse
} else if possible_corpse.item_type == "npc" || possible_corpse.item_type == "player" {
let mut possible_corpse_mut = (*possible_corpse).clone();
possible_corpse_mut.location = if possible_corpse.item_type == "npc" {
"room/valhalla"
} else {
"room/repro_xv_respawn"
}
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else {
user_error("You can't butcher that!".to_owned())?
};
let possession_data = possession_data()
.get(&possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,212 +0,0 @@
use std::collections::BTreeMap;
use super::{
follow::cancel_follow_by_leader, get_player_item_or_fail, get_user_or_fail, look,
rent::recursively_destroy_or_move_item, user_error, UResult, UserError, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
models::task::{Task, TaskDetails, TaskMeta},
regular_tasks::{TaskHandler, TaskRunContext},
services::{
combat::{corpsify_item, handle_death},
destroy_container,
skills::calculate_total_stats_skills_for_user,
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use rand::{distributions::Alphanumeric, Rng};
use std::time;
async fn verify_code(
ctx: &mut VerbContext<'_>,
input: &str,
base_command: &str,
action_text: &str,
) -> UResult<bool> {
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
let code = match &user_dat.danger_code {
None => {
let new_code = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>();
user_dat.danger_code = Some(new_code.clone());
new_code
}
Some(code) => code.clone(),
};
let input_tr = input.trim();
if input_tr == "" || !input_tr.starts_with("code ") {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("To verify you want to {}, type <bold>delete {} code {}<reset>\n"),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
if input_tr["code ".len()..].trim() != code {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
"Your confirmation code didn't match! \
To verify you want to {}, type <bold>delete {} code {}<reset>\n"
),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
Ok(true)
}
async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> {
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
handle_death(&ctx.trans, &mut player_item).await?;
corpsify_item(&ctx.trans, &player_item).await?;
player_item.death_data = None;
player_item.location = "room/repro_xv_chargen".to_owned();
player_item.total_xp = ((player_item.total_xp as i64)
- user_dat.experience.xp_change_for_this_reroll)
.max(0) as u64;
player_item.urges = Some(Default::default());
user_dat.experience.xp_change_for_this_reroll = 0;
user_dat.raw_stats = BTreeMap::new();
user_dat.raw_skills = BTreeMap::new();
user_dat.wristpad_hacks = vec![];
user_dat.scan_codes = vec![];
user_dat.quest_progress = None;
calculate_total_stats_skills_for_user(&mut player_item, &user_dat);
ctx.trans.save_user_model(&user_dat).await?;
ctx.trans.save_item_model(&player_item).await?;
look::VERB.handle(ctx, "look", "").await?;
Ok(())
}
#[derive(Clone)]
pub struct DestroyUserHandler;
pub static DESTROY_USER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DestroyUserHandler;
#[async_trait]
impl TaskHandler for DestroyUserHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let username = match &ctx.task.details {
TaskDetails::DestroyUser { username } => username.clone(),
_ => {
return Ok(None);
}
};
let _user = match ctx.trans.find_by_username(&username).await? {
None => return Ok(None),
Some(u) => u,
};
let player_item = match ctx
.trans
.find_item_by_type_code("player", &username.to_lowercase())
.await?
{
None => return Ok(None),
Some(p) => p,
};
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
destroy_container(&ctx.trans, &player_item).await?;
for dynzone in ctx
.trans
.find_dynzone_for_owner(&format!("player/{}", &username.to_lowercase()))
.await?
{
recursively_destroy_or_move_item(&ctx.trans, &dynzone).await?;
}
ctx.trans.delete_user(&username).await?;
Ok(None)
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let username = get_user_or_fail(ctx)?.username.clone();
if rtrim.starts_with("character forever") {
if !verify_code(ctx, &rtrim["character forever".len()..], "character forever",
"permanently destroy your character (after a one week reflection period), making the name available for other players").await? {
return Ok(());
}
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: username.clone(),
next_scheduled: Utc::now() + chrono::TimeDelta::try_days(7).unwrap(),
..Default::default()
},
details: TaskDetails::DestroyUser { username },
})
.await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(
"Puny human, your permanent destruction has been scheduled \
for one week from now! If you change your mind, just log \
in again before the week is up. After one week, your username \
will be available for others to take, and you will need to start \
again with a new character. This character will count towards the \
character limit for the week, but will not once it is deleted. \
Goodbye forever!\n",
),
)
.await?;
ctx.trans.queue_for_session(ctx.session, None).await?;
} else if rtrim.starts_with("stats") {
if !verify_code(ctx, &rtrim["stats".len()..], "stats",
"kill your character, reset your stats and non-journal XP, and pick new stats to reclone with").await? {
return Ok(());
}
reset_stats(ctx).await?;
} else {
user_error(
ansi!("Try <bold>delete character forever<reset> or <bold>delete stats<reset>")
.to_owned(),
)?
}
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
user_dat.danger_code = None;
ctx.trans.save_user_model(&user_dat).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,19 +1,19 @@
use super::{
get_player_item_or_fail, parsing::parse_to_space, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
UserVerb,
UserVerbRef,
UResult,
parsing::parse_to_space,
user_error,
get_player_item_or_fail
};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use ansi::{ansi, ignore_special_characters};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (me, remaining) = parse_to_space(remaining);
let (as_word, remaining) = parse_to_space(remaining);
let remaining = ignore_special_characters(remaining.trim());
@ -22,28 +22,17 @@ impl UserVerb for Verb {
}
if remaining.len() < 40 {
user_error(format!(
"That's too short by {} characters.",
40 - remaining.len()
))?;
user_error(format!("That's too short by {} characters.", 40 - remaining.len()))?;
}
if remaining.len() > 255 {
user_error(format!(
"That's too short by {} characters.",
remaining.len() - 255
))?;
user_error(format!("That's too short by {} characters.", remaining.len() - 255))?;
}
let mut item = (*get_player_item_or_fail(ctx).await?).clone();
item.details = Some(remaining);
ctx.trans.save_item_model(&item).await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(ansi!("<green>Character description updated.<reset>\n")),
)
.await?;
ctx.trans.queue_for_session(ctx.session, Some(ansi!("<green>Character description updated.<reset>\n"))).await?;
Ok(())
}

View File

@ -1,236 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::recalculate_container_weight_mut,
comms::broadcast_to_room,
urges::{hunger_changed, thirst_changed},
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::{collections::BTreeMap, time};
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item_type, item_code) = match ctx.command {
QueueCommand::Drink {
item_type,
item_code,
} => (item_type, item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location && item.location != ctx.item.refstr() {
user_error(format!(
"You try to drink {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
let msg = format!(
"{} prepares to drink from {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item_type, item_code) = match ctx.command {
QueueCommand::Drink {
item_type,
item_code,
} => (item_type, item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location && item.location != ctx.item.refstr() {
user_error(format!(
"You try to drink {} but realise you no longer have it!",
&item.display_for_sentence(1, false)
))?
}
let liquid_details = item
.liquid_details
.as_ref()
.ok_or_else(|| UserError("You try to drink, but it's empty!".to_owned()))?;
let mut it = liquid_details.contents.iter();
let contents = it
.next()
.ok_or_else(|| UserError("You try to drink, but it's empty!".to_owned()))?;
let contents_2 = it.next();
if !contents_2.is_none() {
user_error("It seems to be a weird mixture of different fluids... you are not sure you should drink it!".to_owned())?;
}
let drink_data = match contents.0.drink_data() {
None => user_error(format!(
"It smells like {}... you are not sure you should drink it!",
contents.0.display()
))?,
Some(v) => v,
};
let urges = ctx
.item
.urges
.as_ref()
.ok_or_else(|| UserError("You don't seem to have the thirst.".to_owned()))?;
if (urges.thirst.value as i16) < -drink_data.thirst_impact {
user_error("You don't seem to have the thirst.".to_owned())?;
}
let how_many_left = (if contents.1 <= &1 {
1
} else {
contents.1.clone()
}) as u64;
let how_many_to_fill = if drink_data.thirst_impact >= 0 {
1
} else {
urges.thirst.value / ((-drink_data.thirst_impact) as u16)
};
let how_many_drunk = how_many_to_fill.min(how_many_left.min(10000) as u16).max(1);
let msg = format!(
"{} drinks from {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
if let Some(urges) = ctx.item.urges.as_mut() {
urges.hunger.last_value = urges.hunger.value;
urges.hunger.value = (urges.hunger.value as i64
+ (how_many_drunk as i64) * (drink_data.hunger_impact as i64))
.clamp(0, 10000) as u16;
urges.thirst.last_value = urges.thirst.value;
urges.thirst.value = (urges.thirst.value as i64
+ (how_many_drunk as i64) * (drink_data.thirst_impact as i64))
.clamp(0, 10000) as u16;
}
hunger_changed(&ctx.trans, &ctx.item).await?;
thirst_changed(&ctx.trans, &ctx.item).await?;
let mut item_mut = (*item).clone();
if let Some(ld) = item_mut.liquid_details.as_mut() {
if (*contents.1) <= how_many_drunk as u64 {
ld.contents = BTreeMap::new();
} else {
ld.contents
.entry(contents.0.clone())
.and_modify(|v| *v -= how_many_drunk as u64);
}
}
match item_mut.liquid_details.as_mut() {
None => {}
Some(ld) => {
ld.contents = ld
.contents
.clone()
.into_iter()
.filter(|c| c.1 != 0)
.collect()
}
}
recalculate_container_weight_mut(&ctx.trans, &mut item_mut).await?;
ctx.trans.save_item_model(&item_mut).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if !remaining.starts_with("from ") {
user_error(ansi!("Try <bold>drink from<reset> container.").to_owned())?;
}
remaining = remaining[5..].trim();
let mut drink_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
drink_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
drink_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: drink_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to drink it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
for target in targets {
if target.item_type != "possession" && target.item_type != "fixed_item" {
user_error("You can't drink that!".to_owned())?;
}
if target.liquid_details.is_none() {
user_error("There's nothing to drink!".to_owned())?;
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Drink {
item_type: target.item_type.clone(),
item_code: target.item_code.clone(),
},
)
.await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,270 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
item::{Item, ItemFlag, LocationActionType},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{
queued_command::{queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext},
TaskHandler, TaskRunContext,
},
services::{
capacity::{check_item_ref_capacity, CapacityLevel},
comms::broadcast_to_room,
},
static_content::possession_type::possession_data,
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use mockall_double::double;
use std::time;
pub struct ExpireItemTaskHandler;
#[async_trait]
impl TaskHandler for ExpireItemTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let item_code = match &mut ctx.task.details {
TaskDetails::ExpireItem { item_code } => item_code,
_ => Err("Expected ExpireItem type")?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", item_code)
.await?
{
None => {
return Ok(None);
}
Some(it) => it,
};
let (loc_type, loc_code) = match item.location.split_once("/") {
None => return Ok(None),
Some(p) => p,
};
if loc_type != "room" {
return Ok(None);
}
let loc_item = match ctx.trans.find_item_by_type_code(loc_type, loc_code).await? {
None => return Ok(None),
Some(i) => i,
};
if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) {
return Ok(None);
}
ctx.trans.delete_item("possession", item_code).await?;
Ok(None)
}
}
pub static EXPIRE_ITEM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ExpireItemTaskHandler;
pub async fn consider_expire_job_for_item(trans: &DBTrans, item: &Item) -> DResult<()> {
let (loc_type, loc_code) = match item.location.split_once("/") {
None => return Ok(()),
Some(p) => p,
};
if loc_type != "room" {
return Ok(());
}
let loc_item = match trans.find_item_by_type_code(loc_type, loc_code).await? {
None => return Ok(()),
Some(i) => i,
};
if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) {
return Ok(());
}
trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("{}/{}", item.item_type, item.item_code),
next_scheduled: Utc::now() + chrono::TimeDelta::try_hours(1).unwrap(),
..Default::default()
},
details: TaskDetails::ExpireItem {
item_code: item.item_code.clone(),
},
})
.await?;
Ok(())
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to drop it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Drop { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to drop {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
}
let msg = format!(
"{} prepares to drop {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Drop { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to drop {} but realise you no longer have it!",
&item.display_for_sentence(1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
}
let possession_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
match check_item_ref_capacity(ctx.trans, &ctx.item.location, possession_data.weight).await?
{
CapacityLevel::AboveItemLimit => user_error(format!(
"You can't drop {}, because it is so cluttered here there is no where to put it!",
&item.display_for_sentence(1, false)
))?,
_ => (),
}
let msg = format!(
"{} drops {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
let mut item_mut = (*item).clone();
item_mut.location = ctx.item.location.clone();
consider_expire_job_for_item(ctx.trans, &item_mut).await?;
item_mut.action_type = LocationActionType::Normal;
ctx.trans.save_item_model(&item_mut).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
get_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to drop it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let mut player_item_mut = (*player_item).clone();
for target in targets {
if target.item_type != "possession" {
user_error("You can't drop that!".to_owned())?;
}
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Drop {
possession_id: target.item_code.clone(),
},
)
.await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,221 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::LocationActionType,
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
comms::broadcast_to_room,
urges::{hunger_changed, thirst_changed},
},
static_content::possession_type::possession_data,
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Eat { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to eat {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
let msg = format!(
"{} prepares to eat {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Eat { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) {
user_error(format!(
"You try to eat {} but realise you no longer have it!",
&item.display_for_sentence(1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
}
let possession_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
let eat_data = possession_data
.eat_data
.as_ref()
.ok_or_else(|| UserError("You can't eat that!".to_owned()))?;
let urges = ctx
.item
.urges
.as_ref()
.ok_or_else(|| UserError("You don't seem to have the appetite.".to_owned()))?;
if (urges.hunger.value as i16) < -eat_data.hunger_impact {
user_error("You don't seem to have the appetite.".to_owned())?;
}
let how_many_left = (if item.charges <= 1 { 1 } else { item.charges }) as u16;
let how_many_to_fill = if eat_data.hunger_impact >= 0 {
1
} else {
urges.hunger.value / ((-eat_data.hunger_impact) as u16)
};
let how_many_eaten = how_many_to_fill.min(how_many_left).max(1);
let msg = format!(
"{} {} {}\n",
&ctx.item.display_for_sentence(1, true),
&(if how_many_eaten == how_many_left {
"polishes off".to_owned()
} else {
format!(
"eats {} bite{} from",
how_many_eaten,
if how_many_eaten == 1 { "" } else { "s" }
)
}),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
if let Some(urges) = ctx.item.urges.as_mut() {
urges.hunger.last_value = urges.hunger.value;
urges.hunger.value = (urges.hunger.value as i16
+ (how_many_eaten as i16) * eat_data.hunger_impact)
.clamp(0, 10000) as u16;
urges.thirst.last_value = urges.thirst.value;
urges.thirst.value = (urges.thirst.value as i16
+ (how_many_eaten as i16) * eat_data.thirst_impact)
.clamp(0, 10000) as u16;
}
hunger_changed(&ctx.trans, &ctx.item).await?;
thirst_changed(&ctx.trans, &ctx.item).await?;
if item.charges <= (how_many_eaten as u8) {
ctx.trans.delete_item("possession", &item_id).await?;
} else {
let mut item_mut = (*item).clone();
item_mut.charges -= how_many_eaten as u8;
ctx.trans.save_item_model(&item_mut).await?;
}
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut eat_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
eat_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
eat_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: eat_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to eat it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let mut player_item_mut = (*player_item).clone();
for target in targets {
if target.item_type != "possession" {
user_error("You can't eat that!".to_owned())?;
}
target
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.eat_data.as_ref())
.ok_or_else(|| UserError("You can't eat that!".to_owned()))?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Eat {
possession_id: target.item_code.clone(),
},
)
.await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,37 +0,0 @@
use crate::{models::effect::EffectType, services::combat::switch_to_feint};
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Feint while dead? You can't even do a regular attack.".to_owned())?;
}
if player_item
.active_effects
.iter()
.any(|v| v.0 == EffectType::Stunned)
{
user_error(
"You stay still like a stunned mullet, unable to gain the composure to feint."
.to_owned(),
)?;
}
switch_to_feint(ctx, &player_item).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,315 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, LiquidDetails, LiquidType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{capacity::recalculate_container_weight_mut, comms::broadcast_to_room},
};
use ansi::ansi;
use async_trait::async_trait;
use std::collections::{btree_map::Entry, BTreeMap};
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to fill it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (from_item_type, from_item_code, to_item_type, to_item_code) = match ctx.command {
QueueCommand::Fill {
from_item_type,
from_item_code,
to_item_type,
to_item_code,
} => (from_item_type, from_item_code, to_item_type, to_item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let from_item = match ctx
.trans
.find_item_by_type_code(&from_item_type, &from_item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
let to_item = match ctx
.trans
.find_item_by_type_code(&to_item_type, &to_item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if to_item.location != ctx.item.location && to_item.location != ctx.item.refstr() {
user_error(format!(
"You try to fill {} but realise you no longer have it",
to_item.display_for_sentence(1, false)
))?
}
if from_item.location != ctx.item.location && from_item.location != ctx.item.refstr() {
user_error(format!(
"You try to fill from {} but realise you no longer have it",
to_item.display_for_sentence(1, false)
))?
}
let msg = format!(
"{} prepares to fill {} from {}\n",
&ctx.item.display_for_sentence(1, true),
&to_item.display_for_sentence(1, false),
&from_item.display_for_sentence(1, false),
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to fill it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (from_item_type, from_item_code, to_item_type, to_item_code) = match ctx.command {
QueueCommand::Fill {
from_item_type,
from_item_code,
to_item_type,
to_item_code,
} => (from_item_type, from_item_code, to_item_type, to_item_code),
_ => user_error("Unexpected command".to_owned())?,
};
let from_item = match ctx
.trans
.find_item_by_type_code(&from_item_type, &from_item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
let to_item = match ctx
.trans
.find_item_by_type_code(&to_item_type, &to_item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if to_item.location != ctx.item.location && to_item.location != ctx.item.refstr() {
user_error(format!(
"You try to fill {} but realise you no longer have it",
to_item.display_for_sentence(1, false)
))?
}
if from_item.location != ctx.item.location && from_item.location != ctx.item.refstr() {
user_error(format!(
"You try to fill from {} but realise you no longer have it",
to_item.display_for_sentence(1, false)
))?
}
let from_liquid_details = match from_item.liquid_details.as_ref() {
None => user_error(format!(
"{} appears to be empty.",
from_item.display_for_sentence(1, true)
))?,
Some(v) => v,
};
let available_vol = from_liquid_details
.contents
.iter()
.map(|r| r.1.clone())
.sum::<u64>();
if available_vol == 0 {
user_error(format!(
"{} appears to be empty.",
from_item.display_for_sentence(1, true)
))?
}
let into_liqdata = match to_item
.static_data()
.and_then(|pd| pd.liquid_container_data.as_ref())
{
None => user_error(format!(
"You can't find a way to fill {}.",
to_item.display_for_sentence(1, false)
))?,
Some(v) => v,
};
if let Some(allowed) = &into_liqdata.allowed_contents {
for (liq_type, _) in &from_liquid_details.contents {
if !allowed.contains(&liq_type) {
user_error(format!(
"You don't think putting {} into {} is a good idea.",
liq_type.display(),
to_item.display_for_sentence(1, false)
))?;
}
}
}
let capacity_remaining = into_liqdata.capacity
- to_item
.liquid_details
.as_ref()
.map(|ld| ld.contents.iter().map(|c| c.1.clone()).sum::<u64>())
.unwrap_or(0);
let actually_transferred = available_vol.min(capacity_remaining);
if actually_transferred == 0 {
user_error(format!(
"You don't think you can get any more into {}.",
to_item.display_for_sentence(1, false)
))?;
}
let transfer_frac = (actually_transferred as f64) / (available_vol as f64);
let mut remaining_total = actually_transferred;
let transfer_volumes: BTreeMap<LiquidType, u64> = from_liquid_details
.contents
.iter()
.flat_map(|(liqtype, vol)| {
let move_vol = (((*vol as f64) * transfer_frac).ceil() as u64).min(remaining_total);
remaining_total -= move_vol;
if move_vol > 0 {
Some((liqtype.clone(), move_vol))
} else {
None
}
})
.collect();
let mut to_item_mut: Item = (*to_item).clone();
match to_item_mut.liquid_details.as_mut() {
None => {
to_item_mut.liquid_details = Some(LiquidDetails {
contents: transfer_volumes.clone(),
})
}
Some(ld) => {
for (liq, vol) in &transfer_volumes {
ld.contents
.entry(liq.clone())
.and_modify(|v| {
*v += *vol;
})
.or_insert(vol.clone());
}
}
}
let mut from_item_mut: Item = (*from_item).clone();
if let Some(ld) = from_item_mut.liquid_details.as_mut() {
for (liq, vol) in &transfer_volumes {
match ld.contents.entry(liq.clone()) {
Entry::Vacant(_) => {}
Entry::Occupied(mut ent) => {
if ent.get() <= vol {
ent.remove();
} else {
*(ent.get_mut()) -= *vol;
}
}
}
}
}
recalculate_container_weight_mut(&ctx.trans, &mut from_item_mut).await?;
recalculate_container_weight_mut(&ctx.trans, &mut to_item_mut).await?;
ctx.trans.save_item_model(&from_item_mut).await?;
ctx.trans.save_item_model(&to_item_mut).await?;
let msg = format!(
"{} fills {} from {}\n",
&ctx.item.display_for_sentence(1, true),
&to_item.display_for_sentence(1, false),
&from_item.display_for_sentence(1, false),
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let (to_str, from_str) = match remaining.split_once(" from ") {
None => user_error(
ansi!("Try <bold>fill<reset> container <bold>from<reset> container.").to_owned(),
)?,
Some((to_str, from_str)) => (to_str.trim(), from_str.trim()),
};
let to_target = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
..ItemSearchParams::base(&player_item, to_str)
},
)
.await?;
let from_target = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
..ItemSearchParams::base(&player_item, from_str)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to fill it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
if from_target.item_type != "possession" && from_target.item_type != "fixed_item" {
user_error("You can't fill from that!".to_owned())?;
}
if to_target.item_type != "possession" && to_target.item_type != "fixed_item" {
user_error("You can't fill that!".to_owned())?;
}
if to_target.item_type == from_target.item_type
&& to_target.item_code == from_target.item_code
{
user_error(
"You can't figure out how to fill something from itself - a shame!".to_owned(),
)?;
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Fill {
from_item_type: from_target.item_type.clone(),
from_item_code: from_target.item_code.clone(),
to_item_type: to_target.item_type.clone(),
to_item_code: to_target.item_code.clone(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,70 +0,0 @@
use super::{get_player_item_or_fail, UResult, UserError, UserVerb, UserVerbRef, VerbContext};
use crate::{
models::item::{Item, ItemSpecialData},
static_content::npc::npc_by_code,
};
use ansi::ansi;
use async_trait::async_trait;
use std::sync::Arc;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let npc_name = remaining.trim().to_lowercase();
let player_item = get_player_item_or_fail(ctx).await?;
let mut match_staff: Vec<Arc<Item>> = ctx
.trans
.find_staff_by_hirer(&player_item.item_code)
.await?
.into_iter()
.filter(|it| {
it.display.to_lowercase().starts_with(&npc_name)
|| it
.aliases
.iter()
.any(|al| al.to_lowercase().starts_with(&npc_name))
})
.collect();
match_staff.sort_by_key(|it| (it.display.len() as i64 - npc_name.len() as i64).abs());
let npc: Arc<Item> = match_staff.first().ok_or_else(|| UserError(ansi!("You don't have any matching employees. Try <bold>hire<reset> to list your staff.").to_owned()))?.clone();
let hire_dat = npc_by_code()
.get(npc.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
.ok_or_else(|| {
UserError(
"Sorry, I've forgotten how to fire them! Ask the game staff for help."
.to_owned(),
)
})?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"A hushed silence falls over the room as you fire {}.\n",
npc.display_for_sentence(1, false),
)),
)
.await?;
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.fire_handler(&ctx.trans, &player_item, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
ctx.trans.save_item_model(&npc_mut).await?;
ctx.trans.delete_task("ChargeWages", &npc.item_code).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,324 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, CommandHandlingError, DResult,
UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
db::ItemSearchParams,
models::item::{FollowData, FollowState, Item},
regular_tasks::queued_command::{queue_command_for_npc, MovementSource, QueueCommand},
static_content::room::Direction,
};
use async_trait::async_trait;
use mockall_double::double;
use std::mem;
pub async fn propagate_move_to_followers(
trans: &DBTrans,
mover: &Item,
direction: &Direction,
source: &MovementSource,
) -> DResult<()> {
let (event_id, chain) = match source {
MovementSource::Command { event_id } => (event_id, vec![]),
MovementSource::Follow { chain, origin_uuid } => (origin_uuid, chain.clone()),
MovementSource::Internal { .. } => return Ok(()), // Not to be propagated.
};
let mut new_chain = chain.clone();
new_chain.push(mover.refstr());
let new_movement = QueueCommand::Movement {
direction: direction.clone(),
source: MovementSource::Follow {
origin_uuid: event_id.clone(),
chain: new_chain,
},
};
for follower in trans.find_followers_by_leader(&mover.refstr()).await? {
let mut follower_mut = (*follower).clone();
if !chain.contains(&follower.refstr()) {
if let Some(&FollowData {
state: FollowState::IfSameRoom,
ref follow_whom,
}) = follower.following.as_ref()
{
if follower.location != mover.location {
continue;
}
follower_mut.following = Some(FollowData {
state: FollowState::Active,
follow_whom: follow_whom.clone(),
});
}
// We use the NPC variant since adding [queued] will be confusing.
match queue_command_for_npc(&trans, &mut follower_mut, &new_movement).await {
Err(e) if e.to_string().starts_with("Can't queue more") => {
suspend_follow_for_independent_move(&mut follower_mut);
}
Err(e) => return Err(e),
_ => {}
}
trans.save_item_model(&follower_mut).await?;
}
}
Ok(())
}
pub async fn update_follow_for_failed_movement(
trans: &DBTrans,
player: &mut Item,
source: &MovementSource,
) -> DResult<()> {
// Failing is the same as an independent movement back.
suspend_follow_for_independent_move(player);
let event_id = source.event_id();
for follower in trans.find_followers_by_leader(&player.refstr()).await? {
if let Some(QueueCommand::Movement {
source: MovementSource::Follow { origin_uuid, chain },
..
}) = follower.queue.front().as_ref()
{
if origin_uuid == event_id && chain.last() == Some(&player.refstr()) {
// The follower has already started moving (and their followers may indeed have
// followed them in turn). So we treat the move the follower made
// but the leader failed as an independent move.
let mut follower_mut = (*follower).clone();
suspend_follow_for_independent_move(&mut follower_mut);
trans.save_item_model(&follower_mut).await?;
continue;
}
}
// Otherwise, it's not too late, so just cancel it from the queue.
let mut follower_mut = (*follower).clone();
follower_mut.queue.retain(|qit| match qit {
QueueCommand::Movement {
source: MovementSource::Follow { origin_uuid, chain },
..
} => !(origin_uuid == event_id && chain.last() == Some(&player.refstr())),
_ => true,
});
if follower_mut.queue != follower.queue {
trans.save_item_model(&follower_mut).await?;
}
}
Ok(())
}
pub fn suspend_follow_for_independent_move(player: &mut Item) {
if let Some(following) = player.following.as_mut() {
following.state = FollowState::IfSameRoom;
}
}
// Caller has to save follower.
pub async fn stop_following(trans: &DBTrans, follower: &mut Item) -> UResult<()> {
let old_following = mem::replace(&mut follower.following, None)
.ok_or_else(|| UserError("You're not following anyone.".to_owned()))?;
if let Some((follow_type, follow_code)) = old_following.follow_whom.split_once("/") {
if let Some(old_item) = trans
.find_item_by_type_code(&follow_type, &follow_code)
.await?
{
if follower.item_type == "player" {
if let Some((session, _session_dat)) =
trans.find_session_for_player(&follower.item_code).await?
{
trans
.queue_for_session(
&session,
Some(&format!(
"You are no longer following {}.\n",
old_item.display_for_sentence(1, false)
)),
)
.await?;
}
}
}
}
Ok(())
}
pub async fn cancel_follow_by_leader(trans: &DBTrans, leader: &str) -> DResult<()> {
for follower in trans.find_followers_by_leader(leader).await? {
if let Some(player_item) = trans
.find_item_by_type_code(&follower.item_type, &follower.item_code)
.await?
{
let mut player_item_mut = (*player_item).clone();
match stop_following(trans, &mut player_item_mut).await {
Ok(_) => {}
Err(CommandHandlingError::UserError(_)) => {}
Err(CommandHandlingError::SystemError(e)) => Err(e)?,
}
trans.save_item_model(&player_item_mut).await?;
}
}
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
verb: &str,
remaining: &str,
) -> UResult<()> {
let mut player_item = (*(get_player_item_or_fail(ctx).await?)).clone();
if verb == "follow" {
let follow_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, remaining.trim())
},
)
.await?;
if follow_whom.item_type != "player" && follow_whom.item_type != "npc" {
user_error("Only characters (player / NPC) can be followed.".to_owned())?;
}
if follow_whom.item_type == "player" && follow_whom.item_code == player_item.item_code {
user_error("No point chasing your own tail!".to_owned())?;
}
if let Some(follow) = player_item.following.as_ref() {
if follow.follow_whom == follow_whom.refstr() {
user_error(format!(
"You're already following {}!",
follow_whom.pronouns.possessive
))?;
}
}
if ctx
.trans
.count_followers_by_leader(&follow_whom.refstr())
.await?
>= 20
{
user_error(format!(
"There's such a huge crowd following {}, there's not really room for more.",
&follow_whom.pronouns.object
))?;
}
if player_item.active_climb.is_some() {
user_error(
"You can't focus on following someone while you are climbing!".to_owned(),
)?;
}
player_item.queue.retain(|qit| match qit {
QueueCommand::Movement { .. } => false,
_ => true,
});
if let Some(QueueCommand::Movement { direction, source }) =
follow_whom.queue.front().as_ref()
{
let event_id = source.event_id();
// Safer to make a singleton chain, since follow is manual
// intervention. Even if the current movement came from player,
// still want to duplicate it, and don't want to duplicate
// player in chain.
let new_chain = vec![follow_whom.refstr()];
let new_movement = QueueCommand::Movement {
direction: direction.clone(),
source: MovementSource::Follow {
origin_uuid: event_id.clone(),
chain: new_chain,
},
};
// We use the NPC variant since adding [queued] will be confusing.
queue_command_for_npc(&ctx.trans, &mut player_item, &new_movement).await?;
}
player_item.following = Some(FollowData {
follow_whom: follow_whom.refstr(),
state: FollowState::Active,
});
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You are now following {}.\n",
&follow_whom.display_for_sentence(1, false)
)),
)
.await?;
ctx.trans.save_item_model(&player_item).await?;
} else {
stop_following(&ctx.trans, &mut player_item).await?;
ctx.trans.save_item_model(&player_item).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
#[cfg(test)]
mod test {
use uuid::Uuid;
use super::*;
use crate::db::MockDBTrans;
use std::sync::Arc;
#[test]
fn propagating_to_overflowing_queue_works() {
let mut trans = MockDBTrans::default();
let leader: Item = Item {
item_type: "test".to_owned(),
item_code: "test1".to_owned(),
..Default::default()
};
let source = MovementSource::Command {
event_id: Uuid::parse_str("ed25224e-9925-4635-8e4f-e612376ef6e9").unwrap(),
};
let follower: Item = Item {
item_type: "test".to_owned(),
item_code: "test2".to_owned(),
queue: std::iter::repeat(QueueCommand::Movement {
direction: Direction::NORTH,
source: source.clone(),
})
.take(20)
.collect(),
following: Some(FollowData {
follow_whom: "test/test1".to_owned(),
state: FollowState::Active,
}),
..Default::default()
};
trans
.expect_find_followers_by_leader()
.times(1)
.withf(|m| m == "test/test1")
.returning(move |_| Ok(vec![Arc::new(follower.clone())]));
trans
.expect_save_item_model()
.times(1)
.withf(|i| {
&i.item_code == "test2"
&& i.queue.len() == 20
&& i.following
== Some(FollowData {
follow_whom: "test/test1".to_owned(),
state: FollowState::IfSameRoom,
})
})
.returning(|_| Ok(()));
assert!(tokio_test::block_on(propagate_move_to_followers(
&trans,
&leader,
&Direction::NORTH,
&source
))
.is_ok());
}
}

View File

@ -1,113 +0,0 @@
use super::{get_player_item_or_fail, UResult, UserError, UserVerb, UserVerbRef, VerbContext};
use crate::{
models::item::LocationActionType,
static_content::{
possession_type::{possession_data, DamageType},
species::species_info_map,
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::collections::BTreeMap;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let all_gear = ctx
.trans
.find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn)
.await?;
let mut msg = String::new();
msg.push_str(&format!(
ansi!(
"<bgblue><white><bold>| {:6} | {:25} | {:5} | {:5} | {:5} | {:5} | {:5} |<reset>\n"
),
"Part", "Clothing", "Beat", "Slash", "Prce", "Shock", "Bullt"
));
for body_part in &species_info_map()
.get(&player_item.species)
.ok_or_else(|| UserError("Species not found".to_owned()))?
.body_parts
{
let mut damage_ranges: BTreeMap<DamageType, (f64, f64)> = BTreeMap::new();
for damtyp in [
DamageType::Beat,
DamageType::Slash,
DamageType::Pierce,
DamageType::Shock,
DamageType::Bullet,
] {
damage_ranges.insert(damtyp, (0.0, 0.0));
}
let mut worn: String = String::new();
for item in &all_gear {
if let Some(wear_data) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.wear_data.as_ref())
{
if wear_data.covers_parts.contains(&body_part) {
if !worn.is_empty() {
worn.push_str(", ");
}
worn.push_str(&item.display);
for entry in damage_ranges.iter_mut() {
if let Some(soak_data) = wear_data.soaks.get(entry.0) {
let (old_min, old_max) = entry.1;
*entry.1 =
(*old_min + soak_data.min_soak, *old_max + soak_data.max_soak);
}
}
}
}
}
worn.truncate(25);
msg.push_str(&format!(
ansi!("| <bold>{:6}<reset> | {:25} | {:2}-{:2} | {:2}-{:2} | {:2}-{:2} | {:2}-{:2} | {:2}-{:2} |\n"),
body_part.display(player_item.sex.clone()),
&worn,
damage_ranges.get(&DamageType::Beat).map(|dt| dt.0).unwrap_or(0.0),
damage_ranges.get(&DamageType::Beat).map(|dt| dt.1).unwrap_or(0.0),
damage_ranges.get(&DamageType::Slash).map(|dt| dt.0).unwrap_or(0.0),
damage_ranges.get(&DamageType::Slash).map(|dt| dt.1).unwrap_or(0.0),
damage_ranges.get(&DamageType::Pierce).map(|dt| dt.0).unwrap_or(0.0),
damage_ranges.get(&DamageType::Pierce).map(|dt| dt.1).unwrap_or(0.0),
damage_ranges.get(&DamageType::Shock).map(|dt| dt.0).unwrap_or(0.0),
damage_ranges.get(&DamageType::Shock).map(|dt| dt.1).unwrap_or(0.0),
damage_ranges.get(&DamageType::Bullet).map(|dt| dt.0).unwrap_or(0.0),
damage_ranges.get(&DamageType::Bullet).map(|dt| dt.1).unwrap_or(0.0),
));
}
let mut total_dodge: f64 = 0.0;
for item in &all_gear {
if let Some(wear_data) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.wear_data.as_ref())
{
total_dodge += wear_data.dodge_penalty;
}
}
msg.push_str(&format!(
"Total dodge penalty from armour: {}\n",
total_dodge
));
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,343 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, LocationActionType},
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel},
comms::broadcast_to_room,
},
static_content::possession_type::{possession_data, ContainerFlag},
};
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
match ctx.command {
QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
item.display_for_sentence(1, false)
))?
}
let msg = format!(
"{} fumbles around trying to pick up {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
}
QueueCommand::GetFromContainer {
from_item_id,
get_possession_id,
} => {
let (from_item_type, from_item_code) = from_item_id
.split_once("/")
.ok_or_else(|| UserError("Bad from_item_id for get".to_owned()))?;
let container = ctx
.trans
.find_item_by_type_code(&from_item_type, &from_item_code)
.await?
.ok_or_else(|| UserError("Item to get from not found".to_owned()))?;
if container.location != ctx.item.location
&& container.location != ctx.item.refstr()
{
user_error(format!(
"You try to get something from {} but realise {} is no longer there",
container.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &get_possession_id)
.await?
.ok_or_else(|| UserError("Item to get not found".to_owned()))?;
if item.location != container.refstr() {
user_error(format!(
"You try to get {} but realise it is no longer in {}",
item.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let msg = format!(
"{} fumbles around trying to get {} from {}.\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false),
&container.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
}
_ => user_error("Unexpected command".to_owned())?,
};
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item, container_opt) = match ctx.command {
QueueCommand::Get { possession_id } => {
let item = match ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.location {
user_error(format!(
"You try to get {} but realise it is no longer there",
&item.display_for_sentence(1, false)
))?
}
ctx.trans.delete_task("ChargeItem", &item.refstr()).await?;
let msg = format!(
"{} picks up {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
(item, None)
}
QueueCommand::GetFromContainer {
from_item_id,
get_possession_id,
} => {
let (from_item_type, from_item_code) = from_item_id
.split_once("/")
.ok_or_else(|| UserError("Bad from_item_id for get".to_owned()))?;
let container = ctx
.trans
.find_item_by_type_code(from_item_type, from_item_code)
.await?
.ok_or_else(|| UserError("Item to get from not found".to_owned()))?;
if container.location != ctx.item.location
&& container.location != ctx.item.refstr()
{
user_error(format!(
"You try to get something from {} but realise {} is no longer there",
container.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &get_possession_id)
.await?
.ok_or_else(|| UserError("Item to get not found".to_owned()))?;
if item.location != container.refstr() {
user_error(format!(
"You try to get {} but realise it is no longer in {}",
item.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let msg = format!(
"{} gets {} from {}.\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false),
&container.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
(item, Some(container))
}
_ => user_error("Unexpected command".to_owned())?,
};
let possession_dat = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
match check_item_capacity(ctx.trans, &ctx.item, possession_dat.weight).await? {
CapacityLevel::AboveItemLimit => {
user_error("You just can't hold that many things!".to_owned())?
}
CapacityLevel::OverBurdened => user_error(format!(
"Fuck! You can't get {} because it is too heavy!",
&item.display_for_sentence(1, false)
))?,
_ => (),
}
let mut item_mut = (*item).clone();
item_mut.location = ctx.item.refstr();
item_mut.action_type = LocationActionType::Normal;
ctx.trans.save_item_model(&item_mut).await?;
if let Some(container) = container_opt {
if container
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.container_data.as_ref())
.map_or(false, |cd| {
cd.container_flags.contains(&ContainerFlag::DestroyOnEmpty)
})
&& ctx
.trans
.get_location_stats(&container.refstr())
.await?
.total_count
== 0
{
ctx.trans
.delete_item(&container.item_type, &container.item_code)
.await?;
} else {
recalculate_container_weight(&ctx.trans, &container).await?;
}
}
Ok(())
}
}
fn check_get_from_allowed(from_what: &Item) -> UResult<()> {
if from_what.item_type == "player" || from_what.item_type == "npc" {
if from_what.death_data.is_none() {
user_error(format!(
"You don't think {} would let you just take it like that!",
&from_what.pronouns.subject
))?;
}
return Ok(());
}
if !vec!["possession", "static_item"].contains(&from_what.item_type.as_str()) {
user_error("You can't get from that!".to_owned())?;
}
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
get_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
get_limit = Some(n);
remaining = remaining2;
}
let (search_what, for_what, include_contents, include_loc_contents) =
match remaining.split_once(" from ") {
None => (player_item.clone(), remaining, false, true),
Some((item_str_raw, container_str_raw)) => {
let container = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
include_contents: true,
..ItemSearchParams::base(&player_item, container_str_raw.trim())
},
)
.await?;
(container, item_str_raw.trim(), true, false)
}
};
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents,
include_contents,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&search_what, for_what)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
if include_contents {
check_get_from_allowed(&search_what)?;
}
let mut did_anything: bool = false;
let mut player_item_mut = (*player_item).clone();
for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" {
user_error("You can't get that!".to_owned())?;
}
did_anything = true;
if include_loc_contents {
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Get {
possession_id: target.item_code.clone(),
},
)
.await?;
} else {
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::GetFromContainer {
from_item_id: search_what.refstr(),
get_possession_id: target.item_code.clone(),
},
)
.await?;
}
}
if !did_anything {
user_error("I didn't find anything matching.".to_owned())?;
} else {
ctx.trans.save_item_model(&player_item_mut).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,96 +0,0 @@
use crate::{
models::user::{wristpad_hack_data, xp_to_hack_slots},
services::skills::calculate_total_stats_skills_for_user,
static_content::room::room_map_by_code,
};
use super::{
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use ansi::ansi;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Your location is invalid".to_owned()))?;
if loc_type != "room" {
user_error("You can't find a hacking unit here.".to_owned())?;
}
let room = room_map_by_code()
.get(&loc_code)
.ok_or_else(|| UserError("Your location no longer exists!".to_owned()))?;
let allowed_hack = room
.wristpad_hack_allowed
.as_ref()
.ok_or_else(|| UserError("You can't find a hacking unit here.".to_owned()))?;
let hack_data = wristpad_hack_data()
.get(allowed_hack)
.ok_or_else(|| UserError("The hacking unit is currently broken.".to_owned()))?;
if hack_data.name.to_lowercase() != remaining.trim() {
user_error(format!(
ansi!("The equipment here only allows you to <bold>hack {}<reset>"),
&hack_data.name
))?;
}
let user = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
let slots_available = xp_to_hack_slots(player_item.total_xp) as usize;
let slots_used = user.wristpad_hacks.len();
if slots_used >= slots_available {
user_error(format!(
"Your wristpad crashes and reboots, flashing up an error that \
there was no space to install the hack. [You only have {} slots \
total on your wristpad to install hacks - try getting some \
more experience to earn more]",
slots_available
))?;
}
if user.wristpad_hacks.contains(&allowed_hack) {
user_error(
"Your wristpad crashes and reboots, flashing up an error that \
the same hack was already found on the device."
.to_owned(),
)?;
}
user.wristpad_hacks.push(allowed_hack.clone());
ctx.trans.save_user_model(&user).await?;
let mut player_mut = (*player_item).clone();
calculate_total_stats_skills_for_user(&mut player_mut, user);
ctx.trans.save_item_model(&player_mut).await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"Your wristpad beeps and reboots. You notice new icon on \
it indicating the {} hack has been applied succesfully!\n",
hack_data.name
)),
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,102 +1,77 @@
use std::collections::BTreeMap;
use super::{CommandHandlingError::UserError, UResult, UserVerb, UserVerbRef, VerbContext};
use ansi_markup::parse_ansi_markup;
use super::{
VerbContext, UserVerb, UserVerbRef, UResult,
CommandHandlingError::UserError
};
use async_trait::async_trait;
use once_cell::sync::OnceCell;
use serde_yaml::from_str as from_yaml_str;
use ansi::ansi;
use phf::phf_map;
fn load_help_yaml(input: &str) -> BTreeMap<String, String> {
let mut map: BTreeMap<String, String> = from_yaml_str(input).unwrap();
for val in map.values_mut() {
*val = parse_ansi_markup(val).unwrap();
}
map
}
static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
"<topicname>" =>
ansi!("You are supposed to replace <lt>topicname> with the topic you want \
to learn about. Example:\r\n\
\t<bold>help register<reset> will tell you about the register command.")
};
fn always_help_pages() -> &'static BTreeMap<String, String> {
static CELL: OnceCell<BTreeMap<String, String>> = OnceCell::new();
static UNREGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
"" =>
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
commands can be used as a topicname.\r\n\
Topics of interest to unregistered users:\r\n\
\t<bold>register<reset>\tLearn about the <bold>register<reset> command.\r\n\
\t<bold>login<reset>\tLearn how to log in as an existing user.\r\n"),
"register" =>
ansi!("Registers a new user. You are allowed at most 5 at once.\r\n\
\t<bold>register <lt>username> <lt>password> <lt>email><reset>\r\n\
Email will be used to check you don't have too many accounts and \
in case you need to reset your password."),
"login" =>
ansi!("Logs in as an existing user.\r\n\
\t<bold>login <lt>username> <lt>password<reset>")
};
CELL.get_or_init(|| load_help_yaml(include_str!("help/always.yaml")))
}
static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
"" =>
ansi!("Type <bold>help <lt>topicname><reset> to learn about a topic. Most \
commands can be used as a topicname.\r\n\
Topics of interest:\r\n\
\t<bold>newbie<reset>\tLearn the absolute basics."),
"newbie" =>
ansi!("So you've just landed in BlastMud, and want to know how to get started?\r\n\
As we develop the game, this will eventually have some useful information for you!"),
};
fn registered_help_pages() -> &'static BTreeMap<String, String> {
static CELL: OnceCell<BTreeMap<String, String>> = OnceCell::new();
CELL.get_or_init(|| load_help_yaml(include_str!("help/registered.yaml")))
}
fn unregistered_help_pages() -> &'static BTreeMap<String, String> {
static CELL: OnceCell<BTreeMap<String, String>> = OnceCell::new();
CELL.get_or_init(|| load_help_yaml(include_str!("help/unregistered.yaml")))
}
static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
"fuck" =>
ansi!("Type <bold>fuck <lt>name><reset> to fuck someone. It only works if \
they have consented.")
};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let mut help = None;
let is_unregistered = match ctx.user_dat {
None => true,
Some(user_dat) => !user_dat.terms.terms_complete,
Some(user_dat) => !user_dat.terms.terms_complete
};
let remaining = remaining.trim();
if is_unregistered {
help = help.or_else(|| unregistered_help_pages().get(remaining));
help = help.or_else(|| UNREGISTERED_HELP_PAGES.get(remaining));
} else {
help = help.or_else(|| registered_help_pages().get(remaining));
help = help.or_else(|| REGISTERED_HELP_PAGES.get(remaining));
if !ctx.session_dat.less_explicit_mode {
help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining))
}
help = help.or_else(|| always_help_pages().get(remaining).map(|v| v));
let help_final = help.ok_or(UserError("No help available on that".to_string()))?;
ctx.trans
.queue_for_session(ctx.session, Some(&(help_final.clone() + "\n")))
.await?;
}
help = help.or_else(|| ALWAYS_HELP_PAGES.get(remaining));
let help_final = help.ok_or(
UserError("No help available on that".to_string()))?;
ctx.trans.queue_for_session(ctx.session,
Some(&(help_final.to_string() + "\r\n"))
).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use crate::message_handler::user_commands::registered_commands;
use super::*;
#[test]
fn registered_help_ok() {
registered_help_pages();
}
#[test]
fn unregistered_help_ok() {
unregistered_help_pages();
}
#[test]
fn always_help_ok() {
always_help_pages();
}
#[test]
fn all_registered_user_commands_documented() {
let help_keys: BTreeSet<String> =
registered_help_pages().keys().map(|k| k.clone()).collect();
let cmds: BTreeSet<String> = registered_commands()
.keys()
.map(|k| k.to_string())
.collect();
assert_eq!(
cmds.difference(&help_keys).collect::<BTreeSet<&String>>(),
BTreeSet::<&String>::new()
);
}
}

View File

@ -1,3 +0,0 @@
"<topicname>": |-
You are supposed to replace <lt>topicname> with the topic you want to learn about. Example:
<bold>help register<reset> will tell you about the register command.

View File

@ -1,405 +0,0 @@
"": |-
Type <bold>help <lt>topicname><reset> to learn about a topic. Most commands can be used as a topicname.
Topics of interest:
<bold>newbie<reset> Learn the absolute basics.
<bold>movement<reset> Commands for moving around.
<bold>possessions<reset> Commands for dealing with possessions / weapons.
<bold>talk<reset> Find out how to talk in the game.
<bold>combat<reset> Learn how to fight.
<bold>information<reset> Learn how to find out about the world and your character.
newbie: |-
So you've just landed in Blastmud, and want to know how to get started?
You control your character, and can tell your character to move around the
world, and see things through their eyes.
The world (yes, even outside!) is divided up into rooms, and each room has
exits that you are allowed to take, normally called north, south, east, west,
northeast, northwest, southeast, southwest, up and down (sometimes you can also go in).
Try <bold>look<reset> (or <bold>l<reset>) to look at the current room. It will
also show you all the exits you can take, and a little ASCII-art map showing
the local layout. <bold>lmap<reset> shows you a more detailed map showing the
directions you can move in. You can also look at objects or players with
<bold>l<reset> followed by the name of the object (shortening is okay).
Once you know what direction to move, you can type the direction, using either
the full name of the direction (e.g. <bold>northeast<reset> or <bold>south<reset>,
or a short form (<bold>n<reset>, <bold>e<reset>, <bold>s<reset>, <bold>w<reset>, <bold>ne<reset>, <bold>nw<reset>, <bold>se<reset>, <bold>sw<reset>, <bold>up<reset>, <bold>down<reset>).
Also try the map commands - <bold>lmap<reset> for a local map including exits, and <bold>gmap<reset> for a giant
map to let you see the broader context.
movement: &movement |-
Once you know what direction to move, you can type the direction, using either
the full name of the direction (e.g. <bold>northeast<reset> or <bold>south<reset>,
or a short form (<bold>n<reset>, <bold>e<reset>, <bold>s<reset>, <bold>w<reset>, <bold>ne<reset>, <bold>nw<reset>, <bold>se<reset>, <bold>sw<reset>, <bold>up<reset>, <bold>down<reset>).
n: *movement
north: *movement
ne: *movement
northeast: *movement
e: *movement
east: *movement
se: *movement
southeast: *movement
s: *movement
south: *movement
sw: *movement
southwest: *movement
w: *movement
west: *movement
nw: *movement
northwest: *movement
up: *movement
down: *movement
in: *movement
look: &look >-
Try <bold>look<reset>, <bold>l<reset>, <bold>examine<reset> or <bold>ex<reset> to look at the current room.
Follow it with the name of an exit to look at an adjacent room.
Or name an object or player in the room to look at that.
You can also use <bold>look at<reset> thing <bold>for sale<reset> while in a shop to inspect the wares before
you buy.
l: *look
ex: *look
read: *look
examine: *look
describe: Use <bold>describe me as<reset> description to set how you appear when people look at you.
who: Use <bold>who<reset> to see what players are online.
combat: &combat >-
Type <bold>kill<reset> character or <bold>k<reset> character to attack a character.
If their health points hits zero, they die. If your health points hit zero, you die.
Note that characters can reclone so death isn't fully permanent (see <bold>help death<reset>).
Combat depends on your ability to dodge the enemy's attacks (based on your dodge skill),
your armour's ability to soak their attacks, and the skill in the weapon you are wielding
- and of course the same factors for your opponents.
You can only attack one character at a time, but more than one can gang up on you!
When you fight, you might notice you improve at your skills (see <bold>help skills<reset>),
and also gain more experience. You gain more skills using a weapon matched to your current
experience level, and more experience fighting harder opponents - but if you take on someone
too tough, you might die and lose experience recloning!
Remember, you can always (at least while you are not dead) try to run away from combat by just
by using movement commands (see <bold>help movement<reset> - but if your dodge skill isn't high
enough, it might fail.
k: *combat
kill: *combat
attack: *combat
pow: &pow >-
Type <bold>powerattack<reset> or <bold>pow<reset> while you are in combat to make your
next move a powerattack. For weapons where it is possible, a powerattack takes longer
to do, but hits harder, and has a higher chance of inflicting critical damage on the
enemy.
power: *pow
powerattack: *pow
feint: >-
Use <bold>feint<reset> while you are in combat to make your next move a feint. This
will pit your intelligence against your opponent, and confuse them if you win. But
beware, if they win, you might end up confused!
death: >-
In Blastmud, you can die if your health points reach zero. While you are dead, you can't
move or talk! Unless someone quickly comes and uses a defibrilator on you, your current
body is gone. Luckily, thanks to residual Gazos-Murlison technology that survived the
fall of the empire, you can still re-clone into a new body. However, this has some
downsides: you lose a bit of your experience in the process, and anything you had on your
person stays with the corpse of your old body! So it is certainly worth trying not to die!
skills: >-
Blastmud has lots of different skills that you can learn. There are two different
measures of a skill. Total skill is how good you are in absolute terms - you get some skill from your stats,
and some from your raws. Raw skill is how much you have improved by learning the skill as you
play the game. Your raw skill caps out at 15 per skill, and adds on to the contribution from
your stats, and any temporary buffs or debuffs caused by your character's state.
information: &info |-
Commands to examine the world:
<bold>look<reset> (or <bold>l<reset>) to look around - follow it with an exit or
a character / item name for detail on that.
<bold>lmap<reset> - get a map showing exits and places.
<bold>gmap<reset> - see a larger (giant) map.
lmap: *info
lm: *info
gmap: *info
gm: *info
talk: &talk |-
Use:
<bold>'<reset>message to send message to the room.
<bold>-<reset>user message to whisper to someone.
<bold>p<reset> user message to page someone on your wristpad.
<bold>reply<reset> message (or <bold>rep<reset> message) to page back the last person to page you.
say: *talk
whisper: *talk
page: *talk
reply: *talk
"'": *talk
"-": *talk
rep: *talk
repl: *talk
tell: *talk
p: *talk
pg: *talk
possessions: &possessions |-
Use:
<bold>get<reset> item to pick something up from the ground.
<bold>drop<reset> item to drop something from your inventory.
<bold>inv<reset> to see what you are holding.
<bold>buy<reset> item to buy something from the shop you are in.
<bold>sell<reset> item to sell something to the shop you are in.
<bold>list<reset> to see the price list for the stop.
<bold>wield<reset> to hold a weapon in your inventory for use when you attack.
<bold>gear<reset> to see what you are wearing, and how much protection it offers.
Hint: get and drop support an item name, but you can also prefix it with a number - e.g. <bold>get 2 whip<reset>. Instead of a number, you can use <bold>all<reset>. You can also omit the item name to match any possession, e.g. <bold>drop all<reset> will drop everything you have.
get: *possessions
drop: *possessions
i: *possessions
inv: *possessions
inventory: *possessions
buy: *possessions
sell: *possessions
list: *possessions
wield: *possessions
gear: *possessions
allow: &consent |-
<bold>allow<reset> is the corner-stone of Blastmud's consent system. Consents in Blastmud let you choose how you want to play with other players (it only affects players, not NPCs). There are 5 types of consent: <bold>fight<reset> (for hostile actions like attack or pick), <bold>medicine<reset> (for medical actions, including those that might crit fail and do harm), <bold>gifts<reset> (lets them give you things), <bold>visit<reset> (lets them on to a tile owned by you legally), and <bold>share<reset> (lets them local share knowledge with you, making both parties stronger).
To allow, as an individual, use the syntax <bold>allow <reset>type <bold>from <reset>player options
As a corp, use the syntax <bold>allow <reset>type <bold>against <reset>corpname <bold>by<reset> corpname options
Options can be blank to use defaults, or can be one or more of the following, separated by spaces:
<bold>for<reset> n <bold>minutes<reset> - replace n with a number. You can use hours, days, or weeks instead of minutes. This makes the consent expire. Fight expires after a week if you don't give a shorter period, and all other consent types have no expiry unless you specify one.
<bold>until death<reset> - makes the consent valid only until you next die.
<bold>allow private<reset> - makes the consent valid even in privately owned places. This is the default for anything except fight.
<bold>disallow private<reset> - the opposite of allow private.
<bold>in<reset> place - limits where fighting can happen to selected public places. You can use
<bold>here<reset>, or if you know the code, a place name. You can use this option more than once to allow any place, and if you don't use the option, it means anywhere (subject to allow private).
<bold>allow pick<reset> - fight only - include picking in your consent.
<bold>allow revoke<reset> - fight only - allows the player to revoke any time with disallow.
Consents for anything except fight take effect immediately to let the other player do the action.
Consents for fight take effect when the other player executes a reciprocal allow command.
Consents for anything except than fight can be revoked instantly with:
<bold>disallow<reset> action <bold>from<reset> player
Consent for fight can be revoked similarly if the consent used the <bold>allow revoke<reset> option.
Otherwise, attempting to revoke informs the other player, and it is revoked when they also issue a disallow command.
consent: *consent
disallow: *consent
corp: |-
<bold>corp<reset> is the entry point to commands related to corps. It supports the following subcommands:
<bold>corp hire<reset> username <bold>into<reset> corpname
Hires a player into your corp.
<bold>corp join<reset> corpname
Accepts an offer to hire you.
<bold>corp list<reset>
List the corps you belong to.
<bold>corp leave<reset> corpname
Resign from a corp.
<bold>corp fire<reset> username <bold>from<reset> corpname
Fires a player from your corp.
<bold>corp promote<reset> username <bold>in<reset> corpname <bold>to<reset> title <bold>privileges<reset> privileges
Promotes/demotes a player within your corp.
<bold>corp info<reset> corpname
Get information about a corp.
<bold>corp allow combat<reset>
Allow the corp to consent to combat on your behalf.
<bold>corp disallow combat<reset>
Don't allow the corp to consent to combat on your behalf.
<bold>corp config<reset> corpname setting
Configure corp settings.
<bold>corp sub<reset> type <bold>from<reset> corpname
Subscribe to corp broadcasts.
<bold>corp unsub<reset> type <bold>from<reset> corpname
Unsubscribe from corp broadcasts.
<bold>corp order<reset> corpname <bold>as<reset> number
Set the position of the corp in your list.
Help is available for many individual corp commands, e.g. try <bold>help corp hire<reset>
"corp hire": "<bold>corp hire<reset> username <bold>into<reset> corpname Hires a player into your corp."
"corp join": "<bold>corp join<reset> corpname Accepts an offer to hire you."
"corp list": "<bold>corp list<reset> List the corps you belong to."
"corp leave": &corp-leave "<bold>corp leave<reset> corpname Resign from a corp."
"corp resign": *corp-leave
"corp fire": "<bold>corp fire<reset> username <bold>from<reset> corpname Fires a player from your corp."
"corp promote": |-
<bold>corp promote<reset> username <bold>in<reset> corpname <bold>to<reset> title <bold>privileges<reset> privileges
Promotes or demotes a player to a specific title within a corp, and modifies their privileges.
<bold>username<reset> - the player you want to promote.
<bold>corpname<reset> - the name of the corporation you want to promote the player within.
<bold>title<reset> - the title you want to assign to the player.
<bold>privileges<reset> - the privileges you want to grant or remove from the player, prefixed with + or - respectively, separated by spaces.
The following privileges exist:
<bold>holder<reset> - The owner of the corp (implies all other privileges).
<bold>hire<reset> - Can hire new people into the corp.
<bold>fire<reset> - Can fire people from the corp.
<bold>promote<reset> - Can promote/demote people.
<bold>war<reset> - Can declare war against other corps on behalf of the corp.
<bold>configure<reset> - Can change the settings of the corp.
<bold>finance<reset> - Can access (or embezzle!) the corps funds.
"corp info": "<bold>corp info<reset> corpname Get information about a corp."
"corp allow combat": "<bold>corp allow combat<reset> Allow the corp to consent to combat on your behalf."
"corp disallow combat": "<bold>corp disallow combat<reset> Don't allow the corp to consent to combat on your behalf."
"corp config": |-
<bold>corp config<reset> corpname setting
Configure various settings for your corp, such as member privileges and combat consent requirements.
<bold>corpname<reset> - the name of your corp.
<bold>setting<reset> - one of the following options:
<bold>allow combat required<reset> - Requires that all new members have the allow combat setting.
<bold>allow combat not required<reset> - Stops requiring that new members have allow combat on.
<bold>base privileges privilege_name privilege_name<reset> - Sets the privileges that new members receive when they join the corporation. See <bold>help corp promote<reset> for privileges.
"corp sub": |-
<bold>corp sub<reset> type <bold>from<reset> corpname
Subscribes to corp broadcasts. Type is one of:
chat - Messages from users.
notice - Important corp notices.
connect - Connecting players.
reward - Rewards paid to the corp.
death - Deaths of players in the corp.
consent - Changes to corp consents (war declarations).
"corp unsub": |-
<bold>corp unsub<reset> type <bold>from<reset> corpname
Unsubscribe from corp broadcasts. Type is one of:
chat - Messages from users.
notice - Important corp notices.
connect - Connecting players.
reward - Rewards paid to the corp.
death - Deaths of players in the corp.
consent - Changes to corp consents (war declarations).
"corp order": "<bold>corp order<reset> corpname <bold>as<reset> number Set the position of the corp in your list."
c: >-
<bold>c<reset> sends a message to one of your corporations. Use it as <bold>c<reset> message to send message to your
first corp (use <bold>corp order<reset> to choose which corp is first). Or explicitly name a corp like this:
<bold>corp @<reset>corpname message
"install": "<bold>install<reset> item <bold>on door to<reset> direction Installs hardware such as a lock on a door."
"uninstall": "<bold>uninstall<reset> item <bold>from door to<reset> direction Removes installed hardware such as a lock on a door."
"gear": "<bold>gear<reset> See equipment you have on, and what protection it offers you."
"delete": |-
Delete is the most destructive command in the game - used to reset your character, or even give it up forever. All commands below echo out a command including a single-use command you can use to confirm the command - they don't take effect without the code. This prevents accidentally pasting in dangerous commands.
<bold>delete character forever<reset> Destroy your character permanently. The command starts a one week countdown at the end of which your account and character (and anything in their possession) are permanently destroyed (as in we irreversibly wipe your data from the server), and any places you are renting get evicted. The command disconnects you immediately. If you reconnect and log in again within the week, the countdown is stopped and the deletion aborted. After the one week, anyone can register again with the same username. At the end of the week, when your character is destroyed, it no longer counts towards the limit of 5 usernames per person.
<bold>delete stats<reset> Kills your character instantly (leaving anything you are carrying on your corpse) and sends you back to the choice room to pick new stats. Any XP gained, apart from XP from journals, is reset back to 0.
eat: |-
If you don't eat, you will get hungry, and eventually be so starving you can barely move. Try <bold>eat<reset> something, in relation to some food in your inventory. If you don't have food, try to find a restaurant, or cut a steak with a butcher's knife from a dead body.
drink: |-
If you don't drink, you will get so thirsty you can barely move. Try <bold>drink from<reset> something, using either a bottle in your inventory, or a fountain or similar outside. See also <bold>help fill<reset> to learn how to fill containers.
fill: |-
You can fill a container (like a bottle) using a command like <bold>fill<reset> container <bold>from<reset> source, where source is where you want to get the liquid from (e.g. a fountain), and container is what you want to put the liquid in. If you don't have a container, you might be able to buy one at a shop, or craft one.
put: |-
You can put something in a container using a command like <bold>put<reset> thing <bold>in<reset> container. See also <bold>help get<reset> to learn how to get them back out.
get: |-
You can get something out of a container using a command like <bold>get<reset> thing <bold>from<reset> container.
sharing: &sharing |-
Sharing lets you share knowledge with another player. Because knowledge is power, sharing will increase the stats of both players for a little while (10 minutes); the better a job you do at sharing, the greater the impact on your stats (up to a maximum of two points of increase to every stat). If you share twice, you will forget what you from the first sharing, and the more recent one will affect your stats. Sharing is therefore a great way to collaborate with another player to temporarily become more capable. However, sharing effectively takes great skill [both player skill, and share skill in the game].
Type <bold>share knowledge with<reset> player to start a sharing session. You will immediately begin sharing, and if you type <bold>share status<reset>, you'll start to see growth in certain interest types. Sharing stops when the pair sharing get to 100% on any interest level and get bored - so you'll want to change topics to avoid that. To get the best score, try to get all interest levels balanced to just below the maximum before you go over on any. However, there are a couple of catches: firstly, changing anything about the conversation is going to be awkward (might fail) if you try too soon after the last change - the higher your Share skill, the faster you can change with a high chance of success. Secondly, to discuss some topics, you need to switch to an appropriate conversation style (amicable, playful, or serious) for the topic first - which could take even longer if your skill level is low. It is a lot easier if yyou work with your conversational partner - they might be able to change earlier.
Throughout the conversation, the game will tell you what styles you can change to, and what topics you can change to under the current style. Just type the name of the topic or style to switch. Each topic has a different rate at which it increases or decreases different interest types.
If you find the pace of the conversation too fast or slow, you can also change the pace with <bold>share slowly<reset>, <bold>share normally<reset> or <bold>share quickly<reset>. Keep in mind changing the pace counts as a change, like changing topics - but once you have set the pace, it scales how fast conversational interest levels grow.
amicable: *sharing
exploring: *sharing
fishing: *sharing
good: *sharing
joking: *sharing
parody: *sharing
play: *sharing
serious: *sharing
share: *sharing
slow: *sharing
normal: *sharing
intense: *sharing
surviving: *sharing
thoughts: *sharing
roaming: *sharing
butcher: &butcher >-
Use the <bold>butcher<reset> command to cut everything possible out of a corpse.
Alternatively, you can cut one part from a corpse by doing something like:
<bold>cut<reset> steak <bold>from<reset> dead dog
This typically requires craft skill.
cut: *butcher
hire: &hiring >-
Use <bold>hire<reset> npc to hire an NPC (e.g. a Roboporter) into your personal employment.
Use <bold>fire<reset> npc to fire them and terminate their employment.
Please note that these commands hire them personally. See also <bold>corp hire<reset>
and <bold>corp fire<reset> to learn about hiring into corps.
fire: *hiring
follow: &follow >-
Use <bold>follow<reset> player to start following a player around.
Use <bold>unfollow<reset> to stop following.
unfollow: *follow
open: &doors >-
Use <bold>open<reset> direction to open a door (and attempting to unlock).
Doors will auto-open just by moving through them, but opening lets you look first,
or let someone else in.
Use <bold>close<reset> direction to close the door in that direction (and lock if applicable).
doors: *doors
close: *doors
hack: >-
If you've found a room where you can install a wristpad hack, use
<bold>hack<reset> hackname to install that hack on your wristpad.
The room description will generally tell you what hackname to use.
improvise: &improvise >-
If you don't have a crafting bench or instructions, you might still be able to make something.
Try <bold>improv with<reset> item to explore what you can make with item.
Once you know what to make, try <bold>improv<reset> output <bold>from<reset> item1, item2, ...
This will let you use as many items as you need.
improv: *improvise
improvize: *improvise
make: &craft >-
To craft, you generally need some form of instructions, a craft bench of some form (which might be a stove, specialised workbench, etc...), and some ingredients.
To start crafting, firstly <bold>put<reset> the instructions (or the entire book) into the craft bench. See <bold>help put<reset> for help on putting. Likewise put the ingredients in the bench
Then use <bold>make<reset> thing <bold>on<reset> bench, where thing is the thing the instructions describe how to
make. The thing will end up in the craft bench. Pay attention to make sure you get it out, and not the recipe.
craft: *craft
crafting: *craft
load: &loading >-
To load an item onto an NPC such as a roboporter (so it will carry your stuff around for you), try:
<bold>load<reset> item <bold>onto<reset> npc
You can unload with <bold>unload<reset> item <bold>from<reset> npc
unload: *loading
pay: >-
Use <bold>pay<reset> $amount <bold> [<bold>from<reset> corp] to recipient
Leave off the <bold>from<reset> corp part if it is from you.
Recipient can be a corp or a user.
You must have finance privileges in the corp you are paying from.
plug: >-
Use <bold>plug in<reset> equipment to plug something electrical in to charge.
It needs to be on the ground (not in inventory), and you need to do it where there is power.
recline: &posture >-
Use <bold>sit<reset> to sit down.
Use <bold>recline<reset> to recline (lie down).
Use <bold>stand<reset> to stand up.
sit: *posture
stand: *posture
stand up: *posture
rent: &rent >-
Renting property gives you somewhere to stay, or gives your corp an HQ.
To rent, go to the place where property is on offer (usually the lobby), and use
<bold>rent<reset> roomtype
To rent for a corp, use:
<bold>rent<reset> roomtype <bold>for<reset> corpname
To stop renting, use:
<bold>vacate<reset> roomtype [<bold>for<reset> corpname]
vacate: *rent
renting: *rent
property: *rent
report abuse: &report
If someone breaks the rules (for example, by posting content that isn't allowed, or by harassing another user),
immediately type <bold>report abuse<reset> to record the last 80 things the MUD sent to you, and give you a report
ID. Once you have a report ID, send it to staff@blastmud.org with a description of the problem. We'll be able to
look up the report ID to see what you are referring to.
report: *report
abuse: *report
harassment: *report
scan: >-
Use the <bold>scan<reset> command to scan a QR code found in the game with your wristpad.
score: &score >-
Use the <bold>score<reset> command to see your stats, skills, experience, and hacks.
sc: *score
scavenge: &scavenge >-
Use the <bold>scavenge<reset> or <bold>search<reset> command to look for hidden treasure (or junk) at the current location.
search: *scavenge
sign: Use <bold>sign<reset> thing to sign a contract.
status: &stats >-
Use <bold>stats<reset> to see your character's basic stats and credits.
st: *stats
stat: *stats
stats: *stats
stop: Use <bold>stop<reset> to stop any queued actions, and reverse the current one if possible.
turn: &turn >-
Use <bold>turn on<reset> thing to turn something on.
Use <bold>turn off<reset> thing to turn something off.
use: Use <bold>use<reset> item [<bold>on<reset> character] to use something on someone (optional).
wear: &clothes >-
Use <bold>wear<reset> clothing to wear something.
Use <bold>remove<reset> clothing to remove some clothing.
Clothing must be removed in the opposite order it is layered on.
remove: *clothes
write: Use <bold>write<reset> message <bold>on<reset> item to write on something.

View File

@ -1,12 +0,0 @@
"": |-
Type <bold>help <lt>topicname><reset> to learn about a topic. Most commands can be used as a topicname.
Topics of interest to unregistered users:
<bold>register<reset>\tLearn about the <bold>register<reset> command.
<bold>login<reset>\tLearn how to log in as an existing user.
register: |-
Registers a new user. You are allowed at most 5 at once.
<bold>register <lt>username> <lt>password> <lt>email><reset>
Email will be used to check you don't have too many accounts and in case you need to reset your password.
login: |-
Logs in as an existing user.
<bold>login <lt>username> <lt>password<reset>

View File

@ -1,246 +0,0 @@
use super::{
get_player_item_or_fail, get_user_or_fail, get_user_or_fail_mut, search_items_for_user,
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::{
item::{Item, ItemFlag, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::npc::npc_by_code,
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use std::sync::Arc;
use std::time;
static RESIGNATION_NOTICE: &'static str = ansi!("You're a <red>terrible employer<reset>, you <bold>HAVEN'T PAID MY WAGES!<reset> Do you even have the credits? I don't work for free, so you can forget it! I quit, effective immediately!");
pub struct ChargeWagesTaskHandler;
#[async_trait]
impl TaskHandler for ChargeWagesTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let npc_code = match &mut ctx.task.details {
TaskDetails::ChargeWages { npc } => npc,
_ => Err("Expected ChargeWages type")?,
};
let hire_dat = match npc_by_code()
.get(npc_code.as_str())
.and_then(|npci| npci.hire_data.as_ref())
{
None => return Ok(None), // I guess it is free until fixed in the db?
Some(d) => d,
};
let npc: Arc<Item> = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? {
None => return Ok(None),
Some(d) => d,
};
let hired_by = match npc.special_data.as_ref() {
Some(ItemSpecialData::HireData {
hired_by: Some(hired_by),
}) => hired_by,
_ => return Ok(None),
};
let mut bill_user = match ctx.trans.find_by_username(&hired_by).await? {
None => {
// Happens if the user is deleted while hiring an NPC.
let mut npc_mut: Item = (*npc).clone();
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
// No player, so the NPC fires itself.
hire_dat
.handler
.fire_handler(&ctx.trans, &npc, &mut npc_mut)
.await?;
ctx.trans.save_item_model(&npc_mut).await?;
return Ok(None);
}
Some(user) => user,
};
let bill_player = ctx
.trans
.find_item_by_type_code("player", &hired_by)
.await?
.ok_or_else(|| "Player hiring NPC missing but user still there.")?;
let sess_and_dat = ctx.trans.find_session_for_player(&hired_by).await?;
if hire_dat.price > bill_user.credits {
if let Some((sess, _sess_dat)) = sess_and_dat {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"<yellow>{} says: <reset><bold>\"{}\"<reset>\n",
&npc.display_for_sentence(1, false),
RESIGNATION_NOTICE
)),
)
.await?;
}
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.fire_handler(&ctx.trans, &bill_player, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData { hired_by: None });
ctx.trans.save_item_model(&npc_mut).await?;
return Ok(None);
}
bill_user.credits -= hire_dat.price;
ctx.trans.save_user_model(&bill_user).await?;
match sess_and_dat.as_ref() {
None => {},
Some((sess, _sess_dat)) => ctx.trans.queue_for_session(
sess, Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${} for wages for {}.<reset>\n"),
&hire_dat.price, &npc.display_for_sentence(1, false)
))
).await?
}
Ok(Some(time::Duration::from_secs(hire_dat.frequency_secs)))
}
}
pub static CHARGE_WAGES_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeWagesTaskHandler;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let npc_name = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
if npc_name == "" {
let staff = ctx
.trans
.find_staff_by_hirer(&player_item.item_code)
.await?;
let mut msg: String = String::new();
if staff.is_empty() {
msg.push_str("You aren't currently employing anyone.\n");
} else {
msg.push_str("You are currently employing:\n");
for emp in staff {
if let Some(hire_dat) = npc_by_code()
.get(emp.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
{
msg.push_str(&format!(
"* {} @ ${} / {}\n",
emp.display_for_sentence(1, false),
hire_dat.price,
humantime::format_duration(std::time::Duration::from_secs(
hire_dat.frequency_secs,
)),
));
}
}
}
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
return Ok(());
}
let to_hire_if_free = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
item_type_only: Some("npc"),
..ItemSearchParams::base(&player_item, npc_name)
},
)
.await?;
let npc = to_hire_if_free
.into_iter()
.find(|it| {
it.flags.contains(&ItemFlag::Hireable)
&& match it.special_data {
Some(ItemSpecialData::HireData { hired_by: Some(_) }) => false,
_ => true,
}
})
.ok_or_else(|| UserError("Nothing here is available for hire right now.".to_owned()))?;
let hire_dat = npc_by_code()
.get(npc.item_code.as_str())
.as_ref()
.and_then(|npci| npci.hire_data.as_ref())
.ok_or_else(|| UserError("Sorry, I've forgotten how to hire that out!".to_owned()))?;
let user = get_user_or_fail(ctx)?;
if user.credits < hire_dat.price {
user_error(format!(
ansi!(
"<yellow>{} says: <reset><bold>\"You wouldn't be able to afford me.\"<reset>"
),
npc.display_for_sentence(1, false)
))?;
}
if ctx
.trans
.count_staff_by_hirer(&player_item.item_code)
.await?
>= 5
{
user_error(
"You don't think you could supervise that many employees at once!".to_owned(),
)?;
}
let user_mut = get_user_or_fail_mut(ctx)?;
user_mut.credits -= hire_dat.price;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"You hire {}, and your wristpad beeps for a deduction of ${}.\n",
npc.display_for_sentence(1, false),
hire_dat.price
)),
)
.await?;
let mut npc_mut = (*npc).clone();
hire_dat
.handler
.hire_handler(&ctx.trans, &ctx.session, &player_item, &mut npc_mut)
.await?;
npc_mut.special_data = Some(ItemSpecialData::HireData {
hired_by: Some(player_item.item_code.clone()),
});
ctx.trans.save_item_model(&npc_mut).await?;
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: npc.item_code.clone(),
is_static: false,
recurrence: None, // Managed by the handler.
next_scheduled: Utc::now()
+ Duration::try_seconds(hire_dat.frequency_secs as i64).unwrap(),
..Default::default()
},
details: TaskDetails::ChargeWages {
npc: npc.item_code.clone(),
},
})
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,15 +1,10 @@
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
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<()> {
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
Ok(())
}
}

View File

@ -1,458 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
language::{self, indefinite_article},
models::item::Item,
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
comms::broadcast_to_room,
destroy_container,
skills::{crit_fail_penalty_for_skill, skill_check_and_grind},
urges::change_stress_considering_cool,
},
static_content::possession_type::{
improv_by_ingredient, improv_by_output, possession_data, possession_type_names,
PossessionData, PossessionType,
},
};
use ansi::ansi;
use async_trait::async_trait;
use rand::seq::IteratorRandom;
use rand::seq::SliceRandom;
use std::collections::BTreeSet;
use std::sync::Arc;
use std::time;
pub struct WithQueueHandler;
#[async_trait]
impl QueueCommandHandler for WithQueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let item_id = match ctx.command {
QueueCommand::ImprovWith { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
if ctx.item.urges.as_ref().map(|u| u.stress.value).unwrap_or(0) > 7000 {
user_error(
ansi!(
"You are too tired and stressed to consider crafts. Maybe try to \
<bold>sit<reset> or <bold>recline<reset> for a bit!"
)
.to_owned(),
)?;
}
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.refstr() {
user_error("You try improvising but realise you no longer have it.".to_owned())?;
}
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} tries to work out what {} can make from {}.\n",
&ctx.item.display_for_sentence(1, true),
&ctx.item.pronouns.subject,
&item.display_for_sentence(1, false),
),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let item_id = match ctx.command {
QueueCommand::ImprovWith { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.refstr() {
user_error("You try improvising but realise you no longer have it.".to_owned())?;
}
let session = if ctx.item.item_type == "player" {
ctx.trans
.find_session_for_player(&ctx.item.item_code)
.await?
} else {
None
};
let opts: Vec<&'static PossessionData> = improv_by_ingredient()
.get(
item.possession_type
.as_ref()
.ok_or_else(|| UserError("You can't improvise with that!".to_owned()))?,
)
.ok_or_else(|| {
UserError(format!(
"You can't think of anything you could make with {}",
item.display_for_sentence(1, false)
))
})?
.iter()
.filter_map(|it| possession_data().get(&it.output).map(|v| *v))
.collect();
let result_data = opts
.as_slice()
.choose(&mut rand::thread_rng())
.ok_or_else(|| {
UserError(format!(
"You can't think of anything you could make with {}",
item.display_for_sentence(1, false)
))
})?;
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You think you could make {} {} from {}\n",
indefinite_article(result_data.display),
result_data.display,
item.display_for_sentence(1, false)
)),
)
.await?;
}
Ok(())
}
}
pub struct FromQueueHandler;
#[async_trait]
impl QueueCommandHandler for FromQueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let (already_used, item_ids) = match ctx.command {
QueueCommand::ImprovFrom {
possession_ids,
already_used,
..
} => (already_used, possession_ids),
_ => user_error("Unexpected command".to_owned())?,
};
if !already_used.is_empty() {
return Ok(time::Duration::from_secs(1));
}
for item_id in item_ids {
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.refstr() {
user_error("You try improvising but realise you no longer have the things you'd planned to use."
.to_owned())?;
}
}
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
let (output, possession_ids, already_used) = match ctx.command {
QueueCommand::ImprovFrom {
output,
possession_ids,
already_used,
} => (output, possession_ids, already_used),
_ => user_error("Unexpected command".to_owned())?,
};
let craft_data = improv_by_output().get(&output).ok_or_else(|| {
UserError("You don't think it is possible to improvise that.".to_owned())
})?;
let mut ingredients_left: Vec<PossessionType> = craft_data.inputs.clone();
let mut to_destroy_if_success: Vec<Arc<Item>> = Vec::new();
for item_id in already_used {
let item = ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
.ok_or_else(|| UserError("Item used in crafting not found.".to_owned()))?;
to_destroy_if_success.push(item.clone());
let possession_type = item
.possession_type
.as_ref()
.ok_or_else(|| UserError("Item used in crafting not a possession.".to_owned()))?;
if let Some(match_pos) = ingredients_left.iter().position(|pt| pt == possession_type) {
ingredients_left.remove(match_pos);
}
}
let mut possession_id_iter = possession_ids.iter();
let session = if ctx.item.item_type == "player" {
ctx.trans
.find_session_for_player(&ctx.item.item_code)
.await?
} else {
None
};
match possession_id_iter.next() {
None => {
let choice = ingredients_left
.iter()
.choose(&mut rand::thread_rng())
.clone();
match choice {
// Nothing left to add, and nothing needed - success!
None => {
for item in to_destroy_if_success {
destroy_container(&ctx.trans, &item).await?;
}
let mut new_item: Item = craft_data.output.clone().into();
new_item.item_code = ctx.trans.alloc_item_code().await?.to_string();
new_item.location = ctx.item.refstr();
ctx.trans.create_item(&new_item).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} proudly holds up the {} {} just made.\n",
&ctx.item.display_for_sentence(1, true),
&new_item.display_for_sentence(1, false),
&ctx.item.pronouns.subject
),
)
.await?;
}
// Nothing left to add, but recipe incomplete.
Some(missing_type) => {
let possession_data =
possession_data().get(missing_type).ok_or_else(|| {
UserError(
"It looks like it's no longer possible to improvise that."
.to_owned(),
)
})?;
user_error(format!(
"You realise you'll also need {} {} to craft that.",
language::indefinite_article(possession_data.display),
possession_data.display
))?;
}
}
}
Some(possession_id) => {
let item = ctx
.trans
.find_item_by_type_code("possession", &possession_id)
.await?
.ok_or_else(|| {
UserError(
"An item you planned to use for crafting seems to be gone.".to_owned(),
)
})?;
if !ingredients_left.contains(
item.possession_type
.as_ref()
.ok_or_else(|| UserError("Uncraftable item used.".to_owned()))?,
) {
user_error(format!(
"You try adding {}, but it doesn't really seem to fit right.",
&item.display_for_sentence(1, false)
))?;
}
let skill_result = skill_check_and_grind(
&ctx.trans,
ctx.item,
&craft_data.skill,
craft_data.difficulty,
)
.await?;
if skill_result <= -0.5 {
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 1000).await?;
crit_fail_penalty_for_skill(&ctx.trans, ctx.item, &craft_data.skill).await?;
ctx.trans
.delete_item(&item.item_type, &item.item_code)
.await?;
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try adding {}, but it goes badly and you waste it.\n",
&item.display_for_sentence(1, false)
)),
)
.await?;
}
} else if skill_result <= 0.0 {
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 500).await?;
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try and fail at adding {}.\n",
&item.display_for_sentence(1, false)
)),
)
.await?;
}
} else {
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try adding {}.\n",
&item.display_for_sentence(1, false),
)),
)
.await?;
}
let mut new_possession_ids = possession_ids.clone();
new_possession_ids.remove(possession_id);
let mut new_already_used = already_used.clone();
new_already_used.insert(possession_id.clone());
ctx.item.queue.push_front(QueueCommand::ImprovFrom {
output: output.clone(),
possession_ids: new_possession_ids,
already_used: new_already_used,
});
}
}
}
Ok(())
}
}
async fn improv_query(
ctx: &mut VerbContext<'_>,
player_item: &Item,
with_what: &str,
) -> UResult<()> {
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(player_item, with_what)
},
)
.await?;
if item.item_type != "possession" {
user_error("You can't improvise with that!".to_owned())?
}
queue_command_and_save(
ctx,
player_item,
&QueueCommand::ImprovWith {
possession_id: item.item_code.clone(),
},
)
.await
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at improvisation.".to_owned())?;
}
if rtrim.starts_with("with ") {
return improv_query(ctx, &player_item, rtrim["with ".len()..].trim_start()).await;
}
let (output, inputs_str) = rtrim.split_once(" from ").ok_or_else(
|| UserError(ansi!("Try <bold>improvise with <reset>item or <bold>improvise<reset> item <bold>from<reset> item, item, ...<reset>").to_owned()))?;
let output_type: PossessionType = match possession_type_names()
.get(&output.trim().to_lowercase())
.map(|x| x.as_slice())
.unwrap_or_else(|| &[])
{
[] => user_error("I don't recognise the thing you want to make.".to_owned())?,
[t] => t.clone(),
_ => user_error(
"You'll have to be more specific about what you want to make.".to_owned(),
)?,
};
let inputs = inputs_str.split(",").map(|v| v.trim());
let mut input_ids: BTreeSet<String> = BTreeSet::new();
for mut input in inputs {
let mut use_limit = Some(1);
if input == "all" || input.starts_with("all ") {
input = input[3..].trim();
use_limit = None;
} else if let (Some(n), remaining2) = parse_count(input) {
use_limit = Some(n);
input = remaining2;
}
let items = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
limit: use_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, input)
},
)
.await?;
for item in items {
if item.item_type != "possession" {
user_error(format!(
"You can't improvise with {}!",
&item.display_for_sentence(1, false)
))?
}
input_ids.insert(item.item_code.to_owned());
}
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::ImprovFrom {
output: output_type,
possession_ids: input_ids,
already_used: BTreeSet::new(),
},
)
.await
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,97 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::item::ItemFlag,
static_content::{possession_type::possession_data, room::Direction},
};
use ansi::ansi;
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<()> {
let (install_what_raw, what_dir_raw) = match remaining.rsplit_once(" on door to ") {
None => user_error(ansi!("Install where? Try <bold>install<reset> <lt>lock> <bold>on door to<reset> <lt>direction>").to_owned())?,
Some(v) => v
};
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"Apparently, you have to be alive to work as an installer.\
So discriminatory!"
.to_owned(),
)?;
}
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(&player_item, install_what_raw.trim())
},
)
.await?;
if item.item_type != "possession" {
user_error("You can't install that!".to_owned())?;
}
let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.install_handler)
{
None => user_error("You can't install that!".to_owned())?,
Some(h) => h,
};
let (loc_t, loc_c) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location".to_owned()))?;
let loc_item = ctx
.trans
.find_item_by_type_code(loc_t, loc_c)
.await?
.ok_or_else(|| UserError("Can't find your location".to_owned()))?;
if loc_item.owner.as_ref() != Some(&player_item.refstr())
|| !loc_item.flags.contains(&ItemFlag::PrivatePlace)
{
user_error(
"You can only install things while standing in a private room you own. \
If you are outside, try installing from the inside."
.to_owned(),
)?;
}
let dir = Direction::parse(what_dir_raw.trim())
.ok_or_else(|| UserError("Invalid direction.".to_owned()))?;
loc_item
.door_states
.as_ref()
.and_then(|ds| ds.get(&dir))
.ok_or_else(|| {
UserError(
"No door to that direction in this room - are you on the wrong side?"
.to_owned(),
)
})?;
handler
.install_cmd(ctx, &player_item, &item, &loc_item, &dir)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,76 +0,0 @@
use super::{get_player_item_or_fail, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::{
language::weight,
models::item::{Item, LocationActionType},
};
use async_trait::async_trait;
use itertools::Itertools;
use std::sync::Arc;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
ctx.trans
.queue_for_session(
ctx.session,
Some("The dead don't really have an inventory.\n"),
)
.await?;
}
let inv = ctx
.trans
.find_items_by_location(&format!(
"{}/{}",
&player_item.item_type, &player_item.item_code
))
.await?;
let all_groups: Vec<Vec<&Arc<Item>>> = inv
.iter()
.group_by(|i| (i.display_for_sentence(1, false), &i.action_type))
.into_iter()
.map(|(_, g)| g.collect::<Vec<&Arc<Item>>>())
.collect::<Vec<Vec<&Arc<Item>>>>();
let mut response = String::new();
let mut total: u64 = 0;
for items in all_groups {
let item = items[0];
if item.item_type != "possession" {
continue;
}
let it_total = items.iter().map(|it| it.weight).sum();
total += it_total;
response.push_str(&format!(
"{} [{}]{}\n",
item.display_for_sentence(items.len(), true),
weight(it_total),
match item.action_type {
LocationActionType::Worn => " (worn)",
LocationActionType::Wielded => " (wielded)",
_ => "",
}
));
}
response.push_str(&format!(
"Total weight: {} ({} max)\n",
weight(total),
weight(player_item.max_carry())
));
if response == "" {
response.push_str("You aren't carrying anything.\n");
}
ctx.trans
.queue_for_session(ctx.session, Some(&response))
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,43 +0,0 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::models::item::ItemFlag;
use ansi::ansi;
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<()> {
let requester = get_player_item_or_fail(ctx).await?;
let remaining = remaining.trim();
let state = if remaining == "on" {
true
} else if remaining == "off" {
false
} else {
return user_error(
ansi!("use <bold>staff_invincible on<reset> or <bold>staff_invincible off<reset>")
.to_owned(),
);
};
let mut requester = (*requester).clone();
requester.flags = requester
.flags
.into_iter()
.filter(|f| f != &ItemFlag::Invincible)
.collect();
if state {
requester.flags.push(ItemFlag::Invincible);
}
ctx.trans.save_item_model(&requester).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -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;

View File

@ -1,77 +0,0 @@
use super::{
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{language, static_content::possession_type::possession_data, static_content::room};
use ansi::ansi;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let user = get_user_or_fail(ctx)?;
if player_item.death_data.is_some() {
user_error(
"Nobody seems to offer you any prices... possibly because you're dead.".to_owned(),
)?
}
let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
user_error("Can't list stock because you're not in a shop.".to_owned())?;
}
let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r,
};
if room.stock_list.is_empty() {
user_error("Can't list stock because you're not in a shop.".to_owned())?
}
let mut msg = String::new();
msg.push_str(&format!(
ansi!("<bold><bgblue><white>| {:40} | {:15} |<reset>\n"),
ansi!("Item"),
ansi!("Price")
));
for stock in &room.stock_list {
if !stock.can_buy {
continue;
}
if let Some(possession_type) = possession_data().get(&stock.possession_type) {
let display = &possession_type.display;
let mut price = stock.list_price;
if stock.poverty_discount && price > user.credits {
price = user.credits;
}
msg.push_str(&format!(
"| {:40} | {:15.2} |\n",
&language::caps_first(&display),
&price
))
}
}
msg.push_str(ansi!(
"\nUse <bold>buy<reset> item to purchase something.\n"
));
ctx.trans
.queue_for_session(&ctx.session, Some(&msg))
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,124 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, UResult, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::item::{ItemFlag, ItemSpecialData},
services::{
capacity::{check_item_capacity, check_item_ref_capacity, CapacityLevel},
comms::broadcast_to_room,
},
};
use ansi::ansi;
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<()> {
let reverse: bool = verb == "unload";
let sep = if reverse { "from" } else { "onto" };
let (mut item_name, npc_name) = match remaining.split_once(sep) {
None => user_error(format!(
ansi!("I couldn't understand that. Try <bold>{}<reset> item {} npc"),
verb, sep
))?,
Some(v) => v,
};
let player_item = get_player_item_or_fail(ctx).await?;
let npc = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, npc_name.trim())
},
)
.await?;
if !npc.flags.contains(&ItemFlag::CanLoad) {
user_error(format!("You can't {} things {} that!", verb, sep))?
}
match npc.special_data.as_ref() {
Some(ItemSpecialData::HireData {
hired_by: Some(hired_by),
}) if hired_by == &player_item.item_code => {}
_ => user_error(format!(
"{} doesn't seem to be letting you do that. Try hiring {} first!",
npc.display_for_sentence(1, false),
npc.pronouns.object
))?,
}
let mut item_limit = Some(1);
if item_name == "all" || item_name.starts_with("all ") {
item_name = item_name[3..].trim();
item_limit = None;
} else if let (Some(n), remaining2) = parse_count(item_name) {
item_limit = Some(n);
item_name = remaining2;
}
let items = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: reverse,
include_loc_contents: !reverse,
item_type_only: Some("possession"),
limit: item_limit.unwrap_or(100),
..ItemSearchParams::base(&npc, item_name.trim())
},
)
.await?;
for item in items {
if reverse {
match check_item_ref_capacity(&ctx.trans, &player_item.location, item.weight)
.await?
{
CapacityLevel::AboveItemLimit => user_error(
"There is not enough space here to unload another item.".to_owned(),
)?,
_ => {}
}
} else {
match check_item_capacity(&ctx.trans, &npc, item.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("There is not enough capacity to load that item.".to_owned())?
}
_ => {}
}
}
let mut item_mut = (*item).clone();
item_mut.location = if reverse {
npc.location.clone()
} else {
npc.refstr()
};
ctx.trans.save_item_model(&item_mut).await?;
broadcast_to_room(
&ctx.trans,
&npc.location,
None,
&format!(
"{} {}s {} {} {}\n",
&npc.display_for_sentence(1, true),
verb,
&item.display_for_sentence(1, false),
sep,
&npc.pronouns.intensive
),
)
.await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,68 +1,35 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use super::look;
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::urges::set_has_urges_if_needed;
use async_trait::async_trait;
use tokio::time;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (username, password) = match remaining.split_whitespace().collect::<Vec<&str>>()[..] {
[] | [_] => user_error("Too few options to login".to_owned())?,
[username, password] => (username, password),
_ => user_error("Too many options to login".to_owned())?,
};
let username_exact = match ctx.trans.find_by_username(username).await? {
match ctx.trans.find_by_username(username).await? {
None => user_error("No such user.".to_owned())?,
Some(user) => {
time::sleep(time::Duration::from_secs(5)).await;
if !bcrypt::verify(password, &user.password_hash)? {
user_error("Invalid password.".to_owned())?
}
let username_exact = user.username.clone();
*ctx.user_dat = Some(user);
username_exact
}
};
ctx.trans
.attach_user_to_session(username, ctx.session)
.await?;
if ctx
.trans
.check_task_by_type_code("DestroyUser", &username_exact)
.await?
{
ctx.trans
.queue_for_session(
ctx.session,
Some(
"Your username was scheduled to self-destruct - that has now been \
cancelled because you logged in.\n",
),
)
.await?;
ctx.trans
.delete_task("DestroyUser", &username_exact)
.await?;
}
ctx.trans.attach_user_to_session(username, ctx.session).await?;
super::agree::check_and_notify_accepts(ctx).await?;
if let Some(user) = ctx.user_dat {
ctx.trans.save_user_model(user).await?;
look::VERB.handle(ctx, "look", "").await?;
}
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
set_has_urges_if_needed(&ctx.trans, &mut player_item).await?;
ctx.trans.save_item_model(&player_item).await?;
Ok(())
}

View File

@ -1,530 +1,90 @@
use super::{
get_player_item_or_fail,
map::{render_map, render_map_dyn},
open::{is_door_in_direction, DoorSituation},
search_item_for_user, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
db::{ItemSearchParams, ObjectRef},
language,
models::{
effect::EffectType,
item::{
DoorState, Item, ItemFlag, ItemSpecialData, LiquidDetails, LiquidType,
LocationActionType, Subattack,
},
},
services::{combat::max_health, skills::calc_level_gap},
static_content::{
dynzone,
possession_type::{possession_data, recipe_craft_by_recipe},
room::{self, room_map_by_code, Direction},
species::species_info_map,
},
};
use ansi::{ansi, flow_around, word_wrap};
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail, search_item_for_user};
use async_trait::async_trait;
use ansi::{ansi, flow_around, word_wrap};
use crate::db::ItemSearchParams;
use crate::models::{item::{Item, LocationActionType, Subattack, ItemFlag}};
use crate::static_content::room::{self, Direction};
use itertools::Itertools;
use mockall_double::double;
use std::collections::BTreeSet;
use std::sync::Arc;
pub async fn describe_normal_item(
player_item: &Item,
ctx: &VerbContext<'_>,
item: &Item,
) -> UResult<()> {
let mut contents_desc = String::new();
let mut items = ctx
.trans
.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.await?;
items.sort_unstable_by(|it1, it2| {
(&it1.action_type)
.cmp(&it2.action_type)
.then((&it1.display).cmp(&it2.display))
});
let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter()
.filter(|it| it.action_type != LocationActionType::Worn)
.group_by(|i| (i.display_for_sentence(1, false), &i.action_type))
.into_iter()
.map(|(_, g)| g.collect::<Vec<&Arc<Item>>>())
.collect::<Vec<Vec<&Arc<Item>>>>();
if all_groups.len() > 0 {
contents_desc.push_str(&(language::caps_first(&item.pronouns.subject)));
if item.item_type == "player" || item.item_type == "npc" {
contents_desc.push_str("'s carrying ");
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
let mut buf = String::new();
let my_loc = &room.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
let max_x = min_x + (width as i64);
let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64);
for y in min_y..max_y {
for x in min_x..max_x {
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else {
contents_desc.push_str(" contains ");
}
let mut phrases = Vec::<String>::new();
for group_items in all_groups {
let head = &group_items[0];
let mut details = head.display_for_sentence(group_items.len(), false);
match head.action_type {
LocationActionType::Wielded => details.push_str(" (wielded)"),
LocationActionType::Worn => continue,
_ => {}
}
phrases.push(details);
}
let phrases_str: Vec<&str> = phrases.iter().map(|p| p.as_str()).collect();
contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n"));
}
if let Some(liq_data) = item
.static_data()
.and_then(|sd| sd.liquid_container_data.as_ref())
{
match item.liquid_details.as_ref() {
Some(LiquidDetails { contents, .. }) if !contents.is_empty() => {
let total_volume: u64 = contents.iter().map(|c| c.1.clone()).sum();
let vol_frac = (total_volume as f64) / (liq_data.capacity as f64);
if vol_frac >= 0.99 {
contents_desc.push_str("It's full to the top with");
} else if vol_frac >= 0.75 {
contents_desc.push_str("It's nearly completely full of");
} else if vol_frac > 0.6 {
contents_desc.push_str("It's more than half full of");
} else if vol_frac > 0.4 {
contents_desc.push_str("It's about half full of");
} else if vol_frac > 0.29 {
contents_desc.push_str("It's about a third full of");
} else if vol_frac > 0.22 {
contents_desc.push_str("It's about a quarter full of");
buf.push_str(room::room_map_by_zloc()
.get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z }))
.map(|r| if room.zone == r.zone {
r.short
} else {
contents_desc.push_str("It contains a tiny bit of");
}
contents_desc.push_str(" ");
let mut it = contents.iter();
let f1_opt = it.next();
let f2_opt = it.next();
match (f1_opt, f2_opt) {
(Some((&LiquidType::Water, _)), None) => contents_desc.push_str("water"),
_ => contents_desc.push_str("mixed fluids"),
}
contents_desc.push_str(".\n");
}
_ => contents_desc.push_str("It's completely dry.\n"),
}
}
let anything_worn = items
.iter()
.any(|it| it.action_type == LocationActionType::Worn);
if anything_worn {
let mut any_part_text = false;
let mut seen_clothes: BTreeSet<String> = BTreeSet::new();
for part in species_info_map()
.get(&item.species)
.map(|s| s.body_parts.clone())
.unwrap_or_else(|| vec![])
{
if let Some((top_item, covering_parts)) = items
.iter()
.filter_map(|it| {
if it.action_type != LocationActionType::Worn {
None
} else {
it.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref())
.and_then(|wd| {
if wd.covers_parts.contains(&part) {
Some((it, wd.covers_parts.clone()))
} else {
None
}
r.secondary_zones.iter()
.find(|sz| sz.zone == room.zone)
.map(|sz| sz.short)
.expect("Secondary zone missing")
})
}
})
.filter_map(|(it, parts)| it.action_type_started.map(|st| ((it, parts), st)))
.max_by_key(|(_it, st)| st.clone())
.map(|(it, _)| it)
{
any_part_text = true;
let display = top_item.display_for_sentence(1, false);
if !seen_clothes.contains(&display) {
seen_clothes.insert(display.clone());
contents_desc.push_str(&format!(
"On {} {}, you see {}. ",
&item.pronouns.possessive,
&language::join_words(
&covering_parts
.iter()
.map(|p| p.display(None))
.collect::<Vec<&'static str>>()
),
&display
));
}
} else {
any_part_text = true;
contents_desc.push_str(&format!(
"{} {} {} completely bare. ",
&language::caps_first(&item.pronouns.possessive),
part.display(item.sex.clone()),
part.copula(item.sex.clone())
));
.unwrap_or(" "));
}
}
if any_part_text {
contents_desc.push_str("\n");
buf.push('\n');
}
buf
}
let health_max = max_health(&item);
if health_max > 0 {
let health_ratio = (item.health as f64) / (health_max as f64);
if item.item_type == "player" || item.item_type == "npc" {
if health_ratio == 1.0 {
contents_desc.push_str(&format!(
"{} is in perfect health.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.75 {
contents_desc.push_str(&format!(
"{} has some minor cuts and bruises.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.5 {
contents_desc.push_str(&format!(
"{} has deep wounds all over {} body.\n",
&language::caps_first(&item.pronouns.subject),
&item.pronouns.possessive
));
} else if health_ratio >= 0.25 {
contents_desc.push_str(&format!(
"{} looks seriously injured.\n",
&language::caps_first(&item.pronouns.subject)
));
} else {
contents_desc.push_str(&format!(
"{} looks like {}'s on death's door.\n",
&language::caps_first(&item.pronouns.subject),
&item.pronouns.possessive
));
}
if item
.active_effects
.iter()
.any(|e| e.0 == EffectType::Bandages)
{
contents_desc.push_str(&format!(
"{} is wrapped up in bandages.\n",
&language::caps_first(&item.pronouns.subject)
));
}
} else if item.item_type == "possession" {
if health_ratio == 1.0 {
contents_desc.push_str(&format!(
"{}'s in perfect condition.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.75 {
contents_desc.push_str(&format!(
"{}'s slightly beaten up.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.5 {
contents_desc.push_str(&format!(
"{}'s pretty beaten up.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.25 {
contents_desc.push_str(&format!(
"{}'s seriously damaged.\n",
&language::caps_first(&item.pronouns.subject)
));
} else {
contents_desc.push_str(&format!(
"{}'s nearly completely destroyed.\n",
&language::caps_first(&item.pronouns.subject)
));
}
}
}
if item.item_type == "possession" {
if let Some(poss_data) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
if let Some(describer) = poss_data.computed_extra_details {
contents_desc.push_str(
&describer
.describe_for(&ctx.trans, item, player_item)
.await?,
);
}
if let Some(charge_data) = poss_data.charge_data.as_ref() {
let unit = if item.charges == 1 {
charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix
} else {
language::pluralise(charge_data.charge_name_prefix)
+ " "
+ charge_data.charge_name_suffix
};
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
}
}
if let Some(recipe_craft_data) = item
.possession_type
.as_ref()
.and_then(|pt| recipe_craft_by_recipe().get(pt))
{
contents_desc.push_str("You will need:\n");
for (input_pt, count) in &recipe_craft_data.craft_data.inputs.iter().counts() {
if let Some(pd) = possession_data().get(&input_pt) {
let thing = pd.display;
contents_desc.push_str(&format!(
" {} {}\n",
count,
&(if count != &1 {
language::pluralise(thing)
} else {
thing.to_owned()
})
));
}
}
match recipe_craft_data.bench.as_ref() {
None => contents_desc.push_str("You can make this without any special bench.\n"),
Some(bench) => {
if let Some(pd) = possession_data().get(bench) {
contents_desc
.push_str(&format!("You'll need to make this on a {}.\n", pd.display))
}
}
}
let diff = calc_level_gap(
&player_item,
&recipe_craft_data.craft_data.skill,
recipe_craft_data.craft_data.difficulty,
);
let challenge_level = if diff > 5.0 {
"You are rather unlikely to succeed in making this."
} else if diff >= 4.0 {
"You're not that likely to succeed in making this, and you're likely to be too confused to learn anything making it."
} else if diff >= 3.0 {
"You've got about a 1/4 chance to succeed at making this, and you might learn something making it."
} else if diff >= 2.0 {
"You've got about a 1/3 chance to succeed at making this, and you might learn something making it."
} else if diff >= 0.0 {
"You've got a less than 50/50 chance to succeed at making this, and you'll probably learn a lot."
} else if diff >= -2.0 {
"You've got a better than 50/50 chance to succeed at making this, and you'll probably learn a lot."
} else if diff >= -3.0 {
"Three out of four times, you'll succeed at making this, and you might still learn something."
} else if diff >= -4.0 {
"Most of the time, you'll succeed at making this, but you'll only rarely learn something new."
} else {
"You're highly likely to succeed at making this, but unlikely to learn anything new."
};
contents_desc.push_str(&format!("{}\n", challenge_level));
}
}
ctx.trans
.queue_for_session(
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> {
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(
"{}\n{}\n{}",
&item.display_for_sentence(1, false),
&item.details.as_ref().map(|d| d.as_str()).unwrap_or(""),
contents_desc,
)),
)
.await?;
Some(&format!("{}\n{}\n",
&item.display_for_session(&ctx.session_dat),
item.details_for_session(&ctx.session_dat).unwrap_or("")
))
).await?;
Ok(())
}
fn exits_for(room: &room::Room) -> String {
let exit_text: Vec<String> = room
.exits
.iter()
.map(|ex| {
format!(
"{}{}",
if ex.exit_climb.is_some() {
ansi!("<red>^")
} else {
ansi!("<yellow>")
},
ex.direction.describe()
)
})
.collect();
format!(
ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"),
exit_text.join(" ")
)
let exit_text: Vec<String> =
room.exits.iter().map(|ex| format!(ansi!("<yellow>{}"),
ex.direction.describe())).collect();
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
}
fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String {
let exit_text: Vec<String> = dynroom
.exits
.iter()
.map(|ex| format!(ansi!("<yellow>{}"), ex.direction.describe()))
.collect();
format!(
ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"),
exit_text.join(" ")
)
}
pub async fn is_room_illuminated(
ctx: &VerbContext<'_>,
player_item: &Item,
location: &Item,
room: &room::Room,
) -> UResult<bool> {
let consider_exits: Vec<ObjectRef> = room
.exits
.iter()
.filter_map(|ex| room::resolve_exit(room, ex))
// Do we want to consider skipping exits behind closed doors?
.map(|r| ObjectRef {
item_type: "room".to_owned(),
item_code: r.code.to_owned(),
})
.collect();
Ok(ctx
.trans
.is_illuminated(player_item, location, &consider_exits)
.await?)
}
pub async fn describe_room(
ctx: &VerbContext<'_>,
player_item: &Item,
item: &Item,
room: &room::Room,
contents: &str,
) -> UResult<()> {
let zone = room::zone_details()
.get(room.zone.as_str())
.map(|z| z.display)
.unwrap_or("Outside of time");
let illum = is_room_illuminated(ctx, player_item, item, room).await?;
let desc = flow_around(
&render_map(room, 5, 5),
10,
ansi!("<reset> "),
&word_wrap(
&format!(
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}{}.{}\n{}\n"),
item.display_for_sentence(1, false),
pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item,
room: &room::Room, contents: &str) -> UResult<()> {
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time");
ctx.trans.queue_for_session(
ctx.session,
Some(&flow_around(&render_map(room, 5, 5), 10, ansi!("<reset> "),
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat),
zone,
&item.details.as_ref().map(|d| d.as_str()).unwrap_or(""),
item.details_dyn_suffix
.as_ref()
.map(|d| d.as_str())
.unwrap_or(""),
contents,
exits_for(room)
),
|row| if row >= 5 { 80 } else { 68 },
),
68,
);
ctx.trans
.queue_for_session(
ctx.session,
Some(if illum {
&desc
} else {
"It's too dark to see much.\n"
}),
)
.await?;
item.details_for_session(
&ctx.session_dat).unwrap_or(""),
contents, exits_for(room)),
|row| if row >= 5 { 80 } else { 68 }), 68))
).await?;
Ok(())
}
pub async fn describe_dynroom(
ctx: &VerbContext<'_>,
item: &Item,
dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom,
contents: &str,
) -> UResult<()> {
ctx.trans
.queue_for_session(
ctx.session,
Some(&flow_around(
&render_map_dyn(dynzone, dynroom, 5, 5),
10,
ansi!("<reset> "),
&word_wrap(
&format!(
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_sentence(1, false),
dynzone.zonename,
&item.details.as_ref().map(|d| d.as_str()).unwrap_or(""),
contents,
exits_for_dyn(dynroom)
),
|row| if row >= 5 { 80 } else { 68 },
),
68,
)),
)
.await?;
Ok(())
}
async fn describe_door(
ctx: &VerbContext<'_>,
room_item: &Item,
state: &DoorState,
direction: &Direction,
) -> UResult<()> {
let mut msg = format!("That exit is blocked by {}.", &state.description);
if let Some(lock) = ctx
.trans
.find_by_action_and_location(
&room_item.refstr(),
&LocationActionType::InstalledOnDoorAsLock((*direction).clone()),
)
.await?
.first()
{
let lock_desc = lock.display_for_sentence(1, false);
msg.push_str(&format!(" The door is locked with {}", &lock_desc));
}
msg.push('\n');
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> {
async fn list_item_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> {
if item.flags.contains(&ItemFlag::NoSeeContents) {
return Ok(" It is too foggy to see who or what else is here.".to_owned());
}
let mut buf = String::new();
let mut items = ctx
.trans
.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.await?;
let mut items = ctx.trans.find_items_by_location(&format!("{}/{}",
item.item_type, item.item_code)).await?;
items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display));
let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter()
.filter(|i| {
i.action_type.is_visible_in_look() && !i.flags.contains(&ItemFlag::DontListInLook)
})
.group_by(|i| i.display_for_sentence(1, false))
.group_by(|i| i.display_for_sentence(true, 1, false))
.into_iter()
.map(|(_, g)|g.collect::<Vec<&Arc<Item>>>())
.collect::<Vec<Vec<&Arc<Item>>>>();
@ -533,44 +93,14 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
let head = &group_items[0];
let is_creature = head.item_type == "player" || head.item_type.starts_with("npc");
buf.push(' ');
buf.push_str(&head.display_for_sentence(group_items.len(), true));
buf.push_str(if group_items.len() > 1 {
" are "
} else {
" is "
});
buf.push_str(&head.display_for_sentence(!ctx.session_dat.less_explicit_mode,
group_items.len(), true));
buf.push_str(if group_items.len() > 1 { " are " } else { " is "});
match head.action_type {
LocationActionType::Sitting(ref on) => {
buf.push_str("sitting ");
if let Some((on_type, on_code)) =
on.as_ref().and_then(|on_ref| on_ref.split_once("/"))
{
if let Some(sit_on) = ctx.trans.find_item_by_type_code(on_type, on_code).await?
{
buf.push_str("on ");
buf.push_str(&sit_on.display_for_sentence(1, false));
}
}
}
LocationActionType::Reclining(ref on) => {
buf.push_str("reclining ");
if let Some((on_type, on_code)) =
on.as_ref().and_then(|on_ref| on_ref.split_once("/"))
{
if let Some(sit_on) = ctx.trans.find_item_by_type_code(on_type, on_code).await?
{
buf.push_str("on ");
buf.push_str(&sit_on.display_for_sentence(1, false));
}
}
}
LocationActionType::Normal | LocationActionType::Attacking(_) if is_creature => {
if head.death_data.is_some() {
buf.push_str("lying ");
} else {
buf.push_str("standing ");
}
}
LocationActionType::Sitting => buf.push_str("sitting "),
LocationActionType::Reclining => buf.push_str("reclining "),
LocationActionType::Normal | LocationActionType::Attacking(_) if is_creature =>
buf.push_str("standing "),
_ => {}
}
buf.push_str("here");
@ -580,24 +110,20 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
Subattack::Feinting => buf.push_str(", feinting "),
Subattack::Grabbing => buf.push_str(", grabbing "),
Subattack::Wrestling => buf.push_str(", wrestling "),
_ => buf.push_str(", attacking "),
_ => buf.push_str(", attacking ")
}
match &head
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.clone())
.or_else(|| head.presence_target.clone())
{
match &head.presence_target {
None => buf.push_str("someone"),
Some(who) => match who.split_once("/") {
None => buf.push_str("someone"),
Some((ttype, tcode)) => {
Some((ttype, tcode)) =>
match ctx.trans.find_item_by_type_code(ttype, tcode).await? {
None => buf.push_str("someone"),
Some(it) => buf.push_str(&it.display_for_sentence(1, false)),
Some(it) => buf.push_str(
&it.display_for_session(&ctx.session_dat)
)
}
}
},
}
}
buf.push('.');
@ -605,234 +131,38 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
Ok(buf)
}
pub async fn direction_to_item(
trans: &DBTrans,
use_location: &str,
direction: &Direction,
) -> UResult<Option<Arc<Item>>> {
// Firstly check dynamic exits, since they apply to rooms and dynrooms...
if let Some(dynroom_result) = trans.find_exact_dyn_exit(use_location, direction).await? {
return Ok(Some(Arc::new(dynroom_result)));
}
let (heretype, herecode) = use_location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype == "dynroom" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v,
};
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?,
};
let dynzone = dynzone::dynzone_by_type()
.get(
&dynzone::DynzoneType::from_str(dynzone_code).ok_or_else(|| {
UserError("The type of your current zone no longer exists".to_owned())
})?,
)
.ok_or_else(|| {
UserError("The type of your current zone no longer exists".to_owned())
})?;
let dynroom = dynzone
.dyn_rooms
.get(dynroom_code.as_str())
.ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target {
dynzone::ExitTarget::ExitZone => {
let (zonetype, zonecode) = old_dynroom_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
let zoneitem = trans
.find_item_by_type_code(zonetype, zonecode)
.await?
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData {
zone_exit: None, ..
}) => user_error("That exit doesn't seem to go anywhere".to_owned())?,
Some(ItemSpecialData::DynzoneData {
zone_exit: Some(zone_exit),
..
}) => zone_exit,
_ => user_error(
"The zone you are in has invalid data associated with it".to_owned(),
)?,
};
let (zone_exit_type, zone_exit_code) =
zone_exit.split_once("/").ok_or_else(|| {
UserError("Oops, that way out seems to be broken.".to_owned())
})?;
Ok(trans
.find_item_by_type_code(zone_exit_type, zone_exit_code)
.await?)
}
dynzone::ExitTarget::Intrazone { subcode } => {
let to_item = trans
.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode)
.await?
.ok_or_else(|| {
UserError("Can't find the room in that direction.".to_owned())
})?;
Ok(Some(Arc::new(to_item)))
}
};
}
if heretype != "room" {
user_error("Navigating outside rooms not yet supported.".to_owned())?
}
let room = room::room_map_by_code()
.get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
let new_room = room::resolve_exit(room, exit)
.ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok(trans.find_item_by_type_code("room", &new_room.code).await?)
}
async fn describe_store_item(
ctx: &VerbContext<'_>,
player_item: &Item,
location_type: &str,
location_code: &str,
item: &str,
) -> UResult<()> {
if location_type != "room" {
user_error("Nothing is for sale here.".to_owned())?;
}
let room = room_map_by_code()
.get(location_code)
.ok_or_else(|| UserError("Couldn't find your room to look.".to_owned()))?;
if room.stock_list.is_empty() {
user_error("Nothing is for sale here.".to_owned())?
}
let (poss_type, _poss_data) = room
.stock_list
.iter()
.filter(|rs| rs.can_buy)
.filter_map(|rs| match possession_data().get(&rs.possession_type) {
Some(pd) => Some((rs.possession_type.clone(), pd)),
None => None,
})
.find(|(_, pd)| {
pd.display.starts_with(item) || pd.aliases.iter().any(|a| a.starts_with(item))
})
.ok_or_else(|| UserError("Couldn't find anything like that for sale.".to_owned()))?;
let tmp_item: Item = poss_type.clone().into();
describe_normal_item(&player_item, ctx, &tmp_item).await
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut rem_trim = remaining.trim().to_lowercase();
let rem_orig = rem_trim.clone();
if rem_trim.starts_with("in ") {
rem_trim = rem_trim[3..].trim_start().to_owned();
}
if rem_trim.starts_with("at ") {
rem_trim = rem_trim[3..].trim_start().to_owned();
}
let use_location = if player_item.death_data.is_some() {
"room/repro_xv_respawn"
} else {
&player_item.location
};
let (heretype, herecode) = use_location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if rem_trim.ends_with(" for sale") {
return Ok(describe_store_item(
ctx,
&player_item,
heretype,
herecode,
rem_trim[0..(rem_trim.len() - 9)].trim_end(),
)
.await?);
}
let rem_trim = remaining.trim().to_lowercase();
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
let item: Arc<Item> = if rem_trim == "" {
ctx.trans
.find_item_by_type_code(heretype, herecode)
.await?
ctx.trans.find_item_by_type_code(heretype, herecode).await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
} else if let Some(dir) =
Direction::parse(&rem_trim).or_else(|| Direction::parse(&rem_orig))
{
// This is complex because "in" is overloaded, and if this fails, we want
// to also consider if they are looking in a container.
match is_door_in_direction(&ctx.trans, &dir, use_location).await {
Ok(DoorSituation::NoDoor)
| Ok(DoorSituation::DoorOutOfRoom {
state: DoorState { open: true, .. },
..
})
| Ok(DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
..
})
| Err(UserError(_)) => {}
Ok(DoorSituation::DoorIntoRoom {
state,
room_with_door,
..
}) => {
if let Some(rev_dir) = dir.reverse() {
return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
} else if let Some(dir) = Direction::parse(&rem_trim) {
if heretype != "room" {
// Fix this when we have planes / boats / roomkits.
user_error("Navigating outside rooms not yet supported.".to_owned())?
} else {
if let Some(room) = room::room_map_by_code().get(herecode) {
match room.exits.iter().find(|ex| ex.direction == *dir) {
None => user_error("There is nothing in that direction".to_owned())?,
Some(exit) => {
match room::resolve_exit(room, exit) {
None => user_error("There is nothing in that direction".to_owned())?,
Some(room2) =>
ctx.trans.find_item_by_type_code("room", room2.code).await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
}
}
Ok(DoorSituation::DoorOutOfRoom {
state,
room_with_door,
..
}) => {
return describe_door(ctx, &room_with_door, &state, &dir).await;
}
Err(e) => Err(e)?,
} else {
user_error("Can't find your current location".to_owned())?
}
match direction_to_item(&ctx.trans, use_location, &dir).await {
Ok(Some(item)) => item,
Ok(None) | Err(UserError(_)) => search_item_for_user(
&ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &rem_trim)
},
)
.await
.map_err(|e| match e {
UserError(_) => UserError("There's nothing in that direction".to_owned()),
e => e,
})?,
Err(e) => Err(e)?,
}
} else if rem_trim == "me" || rem_trim == "self" {
player_item.clone()
@ -842,49 +172,20 @@ impl UserVerb for Verb {
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &rem_trim)
},
)
.await?
}
).await?
};
if item.item_type == "room" {
let room = room::room_map_by_code()
.get(item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
describe_room(
ctx,
&player_item,
&item,
&room,
&list_room_contents(ctx, &item).await?,
)
.await?;
} else if item.item_type == "dynroom" {
let (dynzone, dynroom) = match &item.special_data {
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t| dynzone::dynzone_by_type().get(&dz_t))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?,
_ => user_error("Expected dynroom to have DynroomData".to_owned())?,
};
describe_dynroom(
ctx,
&item,
&dynzone,
&dynroom,
&list_room_contents(ctx, &item).await?,
)
.await?;
if item.item_type != "room" {
describe_normal_item(ctx, &item).await?;
} else {
describe_normal_item(&player_item, ctx, &item).await?;
let room =
room::room_map_by_code().get(item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
describe_room(ctx, &item, &room, &list_item_contents(ctx, &item).await?).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,417 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult,
UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, ItemFlag},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
comms::broadcast_to_room,
destroy_container,
skills::{crit_fail_penalty_for_skill, skill_check_and_grind},
urges::change_stress_considering_cool,
},
static_content::possession_type::{
possession_data, recipe_craft_by_recipe, CraftData, PossessionType,
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
use std::{collections::BTreeSet, sync::Arc};
// This is written this way for future expansion to dynamic recipes.
async fn get_craft_data_for_instructions<'l>(instructions: &'l Item) -> UResult<Option<CraftData>> {
// For now, only static recipes, so we just fetch them...
Ok(instructions
.possession_type
.as_ref()
.and_then(|pt| recipe_craft_by_recipe().get(pt))
.map(|rcd| rcd.craft_data.clone()))
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error("The dead aren't very good at making stuff.".to_owned())?;
}
if ctx.item.urges.as_ref().map(|u| u.stress.value).unwrap_or(0) > 7000 {
user_error(
ansi!(
"You are too tired and stressed to consider crafts. Maybe try to \
<bold>sit<reset> or <bold>recline<reset> for a bit!"
)
.to_owned(),
)?;
}
let (bench_id_opt, instructions_id) = match ctx.command {
QueueCommand::Make {
ref bench_possession_id,
ref instructions_possession_id,
..
} => (
bench_possession_id.as_ref().map(|s| s.as_str()),
instructions_possession_id,
),
_ => user_error("Unexpected command".to_owned())?,
};
let (expected_location, bench_opt) = match bench_id_opt {
None => (ctx.item.location.clone(), None),
Some(bench_id) => {
let bench = ctx
.trans
.find_item_by_type_code("possession", bench_id)
.await?
.ok_or_else(|| {
UserError(
"Hmm, you can't find the equipment you were planning to use!"
.to_owned(),
)
})?;
if bench.location != ctx.item.location {
user_error(
"Hmm, you can't find the equipment you were planning to use!".to_owned(),
)?;
}
(bench.refstr(), Some(bench))
}
};
let instructions = ctx
.trans
.find_item_by_type_code("possession", instructions_id)
.await?
.ok_or_else(|| {
UserError(
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
)
})?;
if instructions.location != expected_location {
user_error(
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
)?;
}
let mut msg = format!(
"{} starts fiddling around trying to make something",
&ctx.item.display_for_sentence(1, true)
);
match bench_opt {
None => {}
Some(bench) => {
msg.push_str(&format!(" on {}", bench.display_for_sentence(1, false)));
}
}
msg.push_str(".\n");
broadcast_to_room(&ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
let (bench_id_opt, instructions_id, already_used) = match ctx.command {
QueueCommand::Make {
ref bench_possession_id,
ref instructions_possession_id,
ref already_used,
} => (
bench_possession_id.as_ref().map(|s| s.as_str()),
instructions_possession_id,
already_used,
),
_ => user_error("Unexpected command".to_owned())?,
};
let (expected_location, bench_opt) = match bench_id_opt {
None => (ctx.item.location.clone(), None),
Some(bench_id) => {
let bench = ctx
.trans
.find_item_by_type_code("possession", bench_id)
.await?
.ok_or_else(|| {
UserError(
"Hmm, you can't find the equipment you were planning to use!"
.to_owned(),
)
})?;
if bench.location != ctx.item.location {
user_error(
"Hmm, you can't find the equipment you were planning to use!".to_owned(),
)?;
}
(bench.refstr(), Some(bench))
}
};
let instructions = ctx
.trans
.find_item_by_type_code("possession", instructions_id)
.await?
.ok_or_else(|| {
UserError(
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
)
})?;
if instructions.location != expected_location {
user_error(
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
)?;
}
if let Some(bench) = bench_opt.as_ref() {
if let Some(bench_data) = bench
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.bench_data)
{
bench_data
.check_make(&ctx.trans, bench, &instructions)
.await?;
}
}
let on_what = match bench_opt {
None => "".to_owned(),
Some(bench) => format!(" on {}", bench.display_for_sentence(1, false)),
};
let craft_data = get_craft_data_for_instructions(&instructions)
.await?
.ok_or_else(|| UserError("Looks like you can't make that anymore.".to_owned()))?;
let mut ingredients_left: Vec<PossessionType> = craft_data.inputs.clone();
let mut to_destroy_if_success: Vec<Arc<Item>> = Vec::new();
for item_id in already_used.iter() {
let item = ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
.ok_or_else(|| UserError("Item used in crafting not found.".to_owned()))?;
to_destroy_if_success.push(item.clone());
let possession_type = item
.possession_type
.as_ref()
.ok_or_else(|| UserError("Item used in crafting not a possession.".to_owned()))?;
if let Some(match_pos) = ingredients_left.iter().position(|pt| pt == possession_type) {
ingredients_left.remove(match_pos);
}
}
let session = if ctx.item.item_type == "player" {
ctx.trans
.find_session_for_player(&ctx.item.item_code)
.await?
} else {
None
};
match ingredients_left.iter().next() {
None => {
for item in to_destroy_if_success {
destroy_container(&ctx.trans, &item).await?;
}
let mut new_item: Item = craft_data.output.clone().into();
new_item.item_code = ctx.trans.alloc_item_code().await?.to_string();
new_item.location = expected_location.clone();
ctx.trans.create_item(&new_item).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} makes a {}{}.\n",
&ctx.item.display_for_sentence(1, true),
&new_item.display_for_sentence(1, false),
&on_what
),
)
.await?;
}
Some(possession_type) => {
let addable = ctx
.trans
.find_items_by_location_possession_type_excluding(
expected_location.as_str(),
possession_type,
&already_used.iter().map(|v| v.as_str()).collect(),
)
.await?;
let pd = possession_data().get(&possession_type).ok_or_else(|| {
UserError(
"Looks like something needed to make that is something I know nothing about!".to_owned(),
)
})?;
match addable.iter().next() {
None => user_error(format!("You realise you'd need {}.", pd.display))?,
Some(item) => {
let skill_result = skill_check_and_grind(
&ctx.trans,
ctx.item,
&craft_data.skill,
craft_data.difficulty,
)
.await?;
if skill_result <= -0.5 {
crit_fail_penalty_for_skill(&ctx.trans, ctx.item, &craft_data.skill)
.await?;
ctx.trans
.delete_item(&item.item_type, &item.item_code)
.await?;
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 1000).await?;
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try adding {}, but it goes badly and you waste it.\n",
&item.display_for_sentence(1, false)
)),
)
.await?;
}
} else if skill_result <= 0.0 {
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 500).await?;
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try and fail at adding {}.\n",
&item.display_for_sentence(1, false)
)),
)
.await?;
}
} else {
if let Some((sess, _)) = session {
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
"You try adding {}.\n",
&item.display_for_sentence(1, false),
)),
)
.await?;
}
let mut new_already_used = (*already_used).clone();
new_already_used.insert(item.item_code.clone());
ctx.item.queue.push_front(QueueCommand::Make {
bench_possession_id: bench_id_opt.map(|id| id.to_owned()),
instructions_possession_id: instructions_id.to_string(),
already_used: new_already_used,
});
}
}
}
}
}
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("The dead aren't very good at making stuff.".to_owned())?;
}
let (bench, output) = match rtrim.split_once(" on ") {
None => (None, rtrim),
Some((output_str, bench_str)) => {
let bench = search_item_for_user(
ctx,
&ItemSearchParams {
item_type_only: Some("possession"),
include_loc_contents: true,
..ItemSearchParams::base(&player_item, bench_str.trim())
},
)
.await?;
(Some(bench), output_str.trim())
}
};
let instructions = search_item_for_user(
ctx,
&ItemSearchParams {
item_type_only: Some("possession"),
include_contents: true,
flagged_only: Some(ItemFlag::Instructions),
..ItemSearchParams::base(bench.as_ref().unwrap_or(&player_item), output.trim())
},
)
.await?;
let recipe_craft = instructions
.possession_type
.as_ref()
.and_then(|pt| recipe_craft_by_recipe().get(&pt))
.ok_or_else(|| {
UserError(
"Sorry, those instructions no longer seem to form part of the game!".to_owned(),
)
})?;
match (recipe_craft.bench.as_ref(), bench.as_ref()) {
(Some(bench_type), None) => user_error(format!(
"The {} can only be made on the {}.",
&instructions.display_for_sentence(1, false),
possession_data()
.get(bench_type)
.map(|pd| pd.display)
.unwrap_or("bench")
))?,
(Some(bench_type), Some(bench))
if bench.possession_type.as_ref() != Some(bench_type) =>
{
user_error(format!(
"The {} can only be made on the {}.",
&instructions.display_for_sentence(1, false),
possession_data()
.get(bench_type)
.map(|pd| pd.display)
.unwrap_or("bench")
))?
}
_ => {}
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Make {
bench_possession_id: bench.as_ref().map(|b| b.item_code.clone()),
instructions_possession_id: instructions.item_code.clone(),
already_used: BTreeSet::<String>::new(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,133 +1,15 @@
use super::{
get_player_item_or_fail, look::is_room_illuminated, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, ItemSpecialData},
static_content::{
dynzone,
room::{self, Direction, GridCoords},
},
};
use ansi::{ansi, flow_around};
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail};
use async_trait::async_trait;
use ansi::{ansi, flow_around};
use crate::{
models::item::Item,
static_content::room::{self, Direction}
};
use std::sync::Arc;
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
let mut buf = String::new();
let my_loc = &room.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
let max_x = min_x + (width as i64);
let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64);
for y in min_y..max_y {
for x in min_x..max_x {
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else {
buf.push_str(
&room::room_map_by_zloc()
.get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z }))
.map(|r| {
if room.zone == r.zone {
r.short.as_str()
} else {
r.secondary_zones
.iter()
.find(|sz| sz.zone == room.zone)
.map(|sz| sz.short.as_str())
.expect("Secondary zone missing")
}
})
.unwrap_or(" "),
);
}
}
buf.push('\n');
}
buf
}
pub fn render_map_dyn(
dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom,
width: usize,
height: usize,
) -> String {
let mut buf = String::new();
let my_loc = &dynroom.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
let max_x = min_x + (width as i64);
let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64);
let main_exit: Option<GridCoords> = dynzone
.dyn_rooms
.iter()
.flat_map(|(_, dr)| {
dr.exits
.iter()
.filter(|ex| match ex.target {
dynzone::ExitTarget::ExitZone => true,
_ => false,
})
.map(|ex| dr.grid_coords.apply(&ex.direction))
})
.next();
for y in min_y..max_y {
for x in min_x..max_x {
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else {
buf.push_str(
dynzone
.dyn_rooms
.iter()
.find(|(_, dr)| {
dr.grid_coords.x == x
&& dr.grid_coords.y == y
&& dr.grid_coords.z == my_loc.z
})
.map(|(_, r)| r.short)
.or_else(|| {
main_exit.as_ref().and_then(|ex_pos| {
if ex_pos.x == x && ex_pos.y == y && ex_pos.z == my_loc.z {
Some("<<")
} else {
None
}
})
})
.unwrap_or(" "),
);
}
}
buf.push('\n');
}
buf
}
fn has_exit_to_dir_in_zone(room: &Option<&&room::Room>, zone: &str, direction: &Direction) -> bool {
match room.as_ref().and_then(|r| {
r.exits
.iter()
.find(|ex| ex.direction == *direction)
.and_then(|ex| room::resolve_exit(r, ex))
}) {
None => false,
Some(other_room) => {
other_room.zone == zone || other_room.secondary_zones.iter().any(|z| z.zone == zone)
}
}
}
pub fn render_lmap(
room: &room::Room,
width: usize,
height: usize,
captions_needed: &mut Vec<(usize, &'static str, &'static str)>,
) -> String {
pub fn render_lmap(room: &room::Room, width: usize, height: usize,
captions_needed: &mut Vec<(usize, &'static str, &'static str)>) -> String {
let mut buf = String::new();
let my_loc = &room.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
@ -137,237 +19,67 @@ pub fn render_lmap(
for y in min_y..max_y {
for x in min_x..max_x {
let coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room = room::room_map_by_zloc().get(&(&room.zone, &coord));
let coord_room = room::room_map_by_zloc()
.get(&(&room.zone, &coord));
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red> () <reset>"))
} else {
let code_capt_opt = coord_room.map(|r| {
if room.zone == r.zone {
(
r.short.as_str(),
if r.should_caption {
Some((
r.name.as_str(),
((my_loc.x as i64 - r.grid_coords.x).abs()
+ (my_loc.y as i64 - r.grid_coords.y).abs())
as usize,
))
let code_capt_opt = coord_room.map(
|r| if room.zone == r.zone {
(r.short, if r.should_caption {
Some((r.name, ((my_loc.x as i64 - r.grid_coords.x).abs() +
(my_loc.y as i64 - r.grid_coords.y).abs()
) as usize)) } else { None })
} else {
None
},
)
} else {
r.secondary_zones
.iter()
r.secondary_zones.iter()
.find(|sz| sz.zone == room.zone)
.map(|sz| {
(
sz.short.as_str(),
sz.caption.as_ref().map(|c| {
(
c.as_str(),
((my_loc.x as i64 - r.grid_coords.x).abs()
+ (my_loc.y as i64 - r.grid_coords.y).abs())
as usize,
)
}),
)
})
.map(|sz| (sz.short, sz.caption.map(
|c| (c, ((my_loc.x as i64 - r.grid_coords.x).abs() +
(my_loc.y as i64 - r.grid_coords.y).abs())
as usize))))
.expect("Secondary zone missing")
}
});
match code_capt_opt {
None => buf.push_str(" "),
Some((code, capt_opt)) => {
if let Some((capt, closeness)) = capt_opt {
captions_needed.push((closeness, &code, &capt));
captions_needed.push((closeness, code, capt));
}
buf.push('[');
buf.push_str(&code);
buf.push_str(code);
buf.push(']');
}
}
}
if has_exit_to_dir_in_zone(&coord_room, &room.zone, &Direction::EAST) {
buf.push('-')
} else {
buf.push(' ')
match coord_room.and_then(
|r| r.exits.iter().find(|ex| ex.direction == Direction::EAST)) {
None => buf.push(' '),
Some(_) => buf.push('-')
}
}
buf.push('\n');
for x in min_x..max_x {
let mut coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room = room::room_map_by_zloc().get(&(&room.zone, &coord));
if has_exit_to_dir_in_zone(&coord_room, &room.zone, &Direction::SOUTH) {
buf.push_str(" | ");
} else {
buf.push_str(" ");
}
let has_se = has_exit_to_dir_in_zone(&coord_room, &room.zone, &Direction::SOUTHEAST);
coord.x += 1;
let coord_room_s = room::room_map_by_zloc().get(&(&room.zone, &coord));
let has_sw = has_exit_to_dir_in_zone(&coord_room_s, &room.zone, &Direction::SOUTHWEST);
if has_se && has_sw {
buf.push('X');
} else if has_se {
buf.push('\\');
} else if has_sw {
buf.push('/');
} else {
buf.push(' ');
}
}
buf.push('\n');
}
captions_needed.sort_unstable_by(|a, b| a.0.cmp(&b.0));
buf
}
pub fn render_lmap_dynroom<'l, 'm>(
zone: &'l dynzone::Dynzone,
room: &'l dynzone::Dynroom,
width: usize,
height: usize,
captions_needed: &'m mut Vec<(usize, &'l str, &'l str)>,
connectwhere: Option<&'l str>,
) -> String {
let mut buf = String::new();
let my_loc = &room.grid_coords;
let min_x = my_loc.x - (width as i64) / 2;
let max_x = min_x + (width as i64);
let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64);
let main_exit_dat: Option<(GridCoords, Direction)> = zone
.dyn_rooms
.iter()
.flat_map(|(_, dr)| {
dr.exits
.iter()
.filter(|ex| match ex.target {
dynzone::ExitTarget::ExitZone => true,
_ => false,
})
.map(|ex| (dr.grid_coords.apply(&ex.direction), ex.direction.clone()))
})
.next();
let main_exit = main_exit_dat.as_ref();
for y in min_y..max_y {
for x in min_x..max_x {
let coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room: Option<&dynzone::Dynroom> = zone
.dyn_rooms
.iter()
.find(|(_, dr)| {
dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z
})
.map(|(_, r)| r);
if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red> () <reset>"));
if let Some(room) = coord_room {
if room.should_caption {
captions_needed.push((
(((my_loc.x as i64 - room.grid_coords.x).abs()
+ (my_loc.y as i64 - room.grid_coords.y).abs())
as usize),
room.short,
room.name,
));
}
match room.exits.iter().find(|ex| ex.direction == Direction::EAST) {
None => buf.push(' '),
Some(_) => buf.push('-'),
}
}
} else if let Some(room) = coord_room {
if room.should_caption {
captions_needed.push((
(((my_loc.x as i64 - room.grid_coords.x).abs()
+ (my_loc.y as i64 - room.grid_coords.y).abs())
as usize),
room.short,
room.name,
));
}
buf.push('[');
buf.push_str(room.short);
buf.push(']');
match room.exits.iter().find(|ex| ex.direction == Direction::EAST) {
None => buf.push(' '),
Some(_) => buf.push('-'),
}
} else if main_exit.map(|ex| &ex.0) == Some(&coord) {
buf.push_str("[<<]");
match main_exit {
Some((ex_coord, Direction::WEST)) => {
buf.push('-');
if let Some(connect) = connectwhere {
captions_needed.push((
((my_loc.x as i64 - ex_coord.x).abs()
+ (my_loc.y as i64 - ex_coord.y).abs())
as usize,
"<<",
connect,
))
}
}
_ => buf.push(' '),
}
} else {
buf.push_str(" ");
}
}
buf.push('\n');
for x in min_x..max_x {
let mut coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room: Option<&'l dynzone::Dynroom> = zone
.dyn_rooms
.iter()
.find(|(_, dr)| {
dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z
})
.map(|(_, r)| r);
match coord_room
.and_then(|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH))
{
Some(_) => buf.push_str(" | "),
None if main_exit == Some(&(coord.clone(), Direction::NORTH)) => {
buf.push_str(" | ")
}
let coord_room = room::room_map_by_zloc()
.get(&(&room.zone, &coord));
match coord_room.and_then(
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH)) {
None => buf.push_str(" "),
Some(_) => buf.push_str(" | ")
}
let has_se = coord_room
.and_then(|r| {
r.exits
.iter()
.find(|ex| ex.direction == Direction::SOUTHEAST)
})
.is_some()
|| (main_exit == Some(&(coord.clone(), Direction::NORTHWEST)));
coord.x += 1;
let coord_room_s = zone
.dyn_rooms
.iter()
.find(|(_, dr)| {
dr.grid_coords.x == coord.x
&& dr.grid_coords.y == coord.y
&& dr.grid_coords.z == my_loc.z
})
.map(|(_, r)| r);
let has_sw = coord_room_s
.and_then(|r| {
r.exits
.iter()
.find(|ex| ex.direction == Direction::SOUTHWEST)
})
.is_some()
|| (main_exit == Some(&(coord, Direction::NORTHEAST)));
if has_se && has_sw {
let has_se = coord_room.and_then(
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTHEAST))
.is_some();
coord.y += 1;
let coord_room_s = room::room_map_by_zloc()
.get(&(&room.zone, &coord));
let has_ne = coord_room_s.and_then(
|r| r.exits.iter().find(|ex| ex.direction == Direction::NORTHEAST))
.is_some();
if has_se && has_ne {
buf.push('X');
} else if has_se {
buf.push('\\');
} else if has_sw {
} else if has_ne {
buf.push('/');
} else {
buf.push(' ');
@ -379,190 +91,44 @@ pub fn render_lmap_dynroom<'l, 'm>(
buf
}
pub fn caption_lmap<'l>(
captions: &Vec<(usize, &'l str, &'l str)>,
width: usize,
height: usize,
) -> String {
pub fn caption_lmap(captions: &Vec<(usize, &'static str, &'static str)>, width: usize, height: usize) -> String {
let mut buf = String::new();
for room in captions.iter().take(height) {
buf.push_str(&format!(
ansi!("{}<bold>: {:.*}<reset>\n"),
room.1, width, room.2
));
buf.push_str(&format!(ansi!("{}<bold>: {:.*}<reset>\n"), room.1, width, room.2));
}
buf
}
#[async_trait]
trait MapType {
async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()>;
async fn map_room_dyn<'a>(
&self,
ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom,
zoneref: &str,
) -> UResult<()>;
}
pub struct LmapType;
#[async_trait]
impl MapType for LmapType {
async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()> {
pub async fn lmap_room(ctx: &VerbContext<'_>,
room: &room::Room) -> UResult<()> {
let mut captions: Vec<(usize, &'static str, &'static str)> = Vec::new();
ctx.trans
.queue_for_session(
ctx.trans.queue_for_session(
ctx.session,
Some(&flow_around(
&render_lmap(room, 9, 7, &mut captions),
45,
ansi!("<reset> "),
&caption_lmap(&captions, 14, 27),
31,
)),
)
.await?;
Some(&flow_around(&render_lmap(room, 9, 7, &mut captions), 45, ansi!("<reset> "),
&caption_lmap(&captions, 14, 27), 31
))
).await?;
Ok(())
}
async fn map_room_dyn<'a>(
&self,
ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom,
zoneref: &str,
) -> UResult<()> {
let mut captions: Vec<(usize, &str, &str)> = Vec::new();
let connectwhere_name_opt: Option<String> = match zoneref.split_once("/") {
None => None,
Some((zone_t, zone_c)) => {
let zone_item: Option<Arc<Item>> =
ctx.trans.find_item_by_type_code(zone_t, zone_c).await?;
match zone_item.as_ref().map(|v| v.as_ref()) {
Some(Item {
special_data:
Some(ItemSpecialData::DynzoneData {
zone_exit: Some(zone_exit),
..
}),
..
}) => match zone_exit.split_once("/") {
None => None,
Some((ex_t, ex_c)) => {
match ctx.trans.find_item_by_type_code(ex_t, ex_c).await?.as_ref() {
Some(dest_item) => Some(dest_item.display_for_sentence(1, true)),
None => None,
}
}
},
_ => None,
}
}
};
let lmap_str = render_lmap_dynroom(
zone,
room,
9,
7,
&mut captions,
connectwhere_name_opt.as_ref().map(|v| v.as_str()),
);
ctx.trans
.queue_for_session(
ctx.session,
Some(&flow_around(
&lmap_str,
45,
ansi!("<reset> "),
&caption_lmap(&captions, 14, 27),
31,
)),
)
.await?;
Ok(())
}
}
pub struct GmapType;
#[async_trait]
impl MapType for GmapType {
async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()> {
ctx.trans
.queue_for_session(ctx.session, Some(&render_map(room, 32, 18)))
.await?;
Ok(())
}
async fn map_room_dyn<'a>(
&self,
ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom,
_zoneref: &str,
) -> UResult<()> {
ctx.trans
.queue_for_session(ctx.session, Some(&render_map_dyn(zone, room, 16, 9)))
.await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
if remaining.trim() != "" {
user_error("map commands don't take anything after them".to_owned())?;
}
let map_type: Box<dyn MapType + Sync + Send> = match verb {
"lmap" | "lm" => Box::new(LmapType),
"gmap" | "gm" => Box::new(GmapType),
_ => user_error("I don't know how to show that map type.".to_owned())?,
};
let player_item = get_player_item_or_fail(ctx).await?;
let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
let room_item: Arc<Item> = ctx
.trans
.find_item_by_type_code(heretype, herecode)
.await?
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
let room_item: Arc<Item> = ctx.trans.find_item_by_type_code(heretype, herecode).await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?;
if room_item.item_type == "room" {
let room = room::room_map_by_code()
.get(room_item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
if !is_room_illuminated(ctx, &player_item, &room_item, room).await? {
user_error("It's too dark here to make out the map.".to_owned())?;
}
map_type.map_room(ctx, &room).await?;
} else if room_item.item_type == "dynroom" {
let (dynzone, dynroom) = match &room_item.special_data {
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t| dynzone::dynzone_by_type().get(&dz_t))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?,
_ => user_error("Expected dynroom to have DynroomData".to_owned())?,
};
map_type
.map_room_dyn(ctx, &dynzone, &dynroom, &room_item.location)
.await?;
} else {
if room_item.item_type != "room" {
user_error("Can't map here".to_owned())?;
} else {
let room =
room::room_map_by_code().get(room_item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
lmap_room(ctx, &room).await?;
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,348 +0,0 @@
use super::{
get_player_item_or_fail, look::direction_to_item, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
item::{DoorState, Item, LocationActionType},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{
queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
TaskHandler, TaskRunContext,
},
services::comms::broadcast_to_room,
static_content::room::Direction,
DResult,
};
use async_trait::async_trait;
use chrono::{self, Utc};
use mockall_double::double;
use std::sync::Arc;
use std::time;
#[derive(Clone)]
pub struct SwingShutHandler;
pub static SWING_SHUT_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &SwingShutHandler;
#[async_trait]
impl TaskHandler for SwingShutHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (room_str, direction) = match &ctx.task.details {
TaskDetails::SwingShut {
room_item,
direction,
} => (room_item, direction),
_ => {
return Ok(None);
}
};
let (room_item_type, room_item_code) = match room_str.split_once("/") {
None => {
return Ok(None);
}
Some(v) => v,
};
let room_item = match ctx
.trans
.find_item_by_type_code(room_item_type, room_item_code)
.await?
{
None => {
return Ok(None);
}
Some(v) => v,
};
let mut room_item_mut = (*room_item).clone();
let door_state = match room_item_mut
.door_states
.as_mut()
.and_then(|ds| ds.get_mut(&direction))
{
Some(v) if v.open => v,
_ => return Ok(None),
};
(*door_state).open = false;
ctx.trans.save_item_model(&room_item_mut).await?;
let msg = format!(
"The door to the {} swings shut with a click.\n",
&direction.describe()
);
broadcast_to_room(&ctx.trans, &room_str, None, &msg).await?;
if let Ok(Some(other_room)) = direction_to_item(&ctx.trans, &room_str, &direction).await {
let msg = format!(
"The door to the {} swings shut with a click.\n",
&direction
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned())
);
broadcast_to_room(&ctx.trans, &other_room.refstr(), None, &msg).await?;
}
Ok(None)
}
}
pub async fn attempt_open_immediate(
ctx: &mut QueuedCommandContext<'_>,
direction: &Direction,
) -> UResult<()> {
let use_location = if ctx.item.death_data.is_some() {
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
} else {
&ctx.item.location
};
let (room_1, dir_in_room, room_2) =
match is_door_in_direction(ctx.trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
..
}
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: true, .. },
..
} => user_error("The door is already open.".to_owned())?,
DoorSituation::DoorIntoRoom {
room_with_door,
current_room,
..
} => {
let entering_room_loc = room_with_door.refstr();
if let Some(revdir) = direction.reverse() {
if let Some(lock) = ctx
.trans
.find_by_action_and_location(
&entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir.clone()),
)
.await?
.first()
{
if let Some(lockcheck) =
lock.static_data().and_then(|pd| pd.lockcheck_handler)
{
lockcheck.cmd(ctx, &lock).await?
}
}
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&revdir) {
(*door).open = true;
}
}
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, revdir, current_room)
} else {
user_error("There's no door possible there.".to_owned())?
}
}
DoorSituation::DoorOutOfRoom {
room_with_door,
new_room,
..
} => {
let mut entering_room_mut = (*room_with_door).clone();
if let Some(door_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) {
(*door).open = true;
}
}
ctx.trans.save_item_model(&entering_room_mut).await?;
(room_with_door, direction.clone(), new_room)
}
};
for (loc, dir) in [
(&room_1.refstr(), &dir_in_room.describe()),
(
&room_2.refstr(),
&dir_in_room
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned()),
),
] {
broadcast_to_room(
&ctx.trans,
loc,
None,
&format!(
"{} opens the door to the {}.\n",
&ctx.item.display_for_sentence(1, true),
dir
),
)
.await?;
}
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()),
next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(120).unwrap(),
..Default::default()
},
details: TaskDetails::SwingShut {
room_item: room_1.refstr(),
direction: dir_in_room.clone(),
},
})
.await?;
Ok(())
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
let direction = match ctx.command {
QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())?,
};
let use_location = if ctx.item.death_data.is_some() {
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
} else {
&ctx.item.location
};
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
..
}
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: true, .. },
..
} => user_error("The door is already open.".to_owned())?,
DoorSituation::DoorIntoRoom {
room_with_door: entering_room,
..
} => {
let entering_room_loc = entering_room.refstr();
if let Some(revdir) = direction.reverse() {
if let Some(lock) = ctx
.trans
.find_by_action_and_location(
&entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir),
)
.await?
.first()
{
if let Some(lockcheck) =
lock.static_data().and_then(|pd| pd.lockcheck_handler)
{
lockcheck.cmd(ctx, &lock).await?
}
}
}
}
_ => {}
}
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
let direction = match ctx.command {
QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())?,
};
attempt_open_immediate(ctx, &direction).await?;
Ok(())
}
}
pub enum DoorSituation {
NoDoor,
DoorIntoRoom {
state: DoorState,
room_with_door: Arc<Item>,
current_room: Arc<Item>,
}, // Can be locked etc...
DoorOutOfRoom {
state: DoorState,
room_with_door: Arc<Item>,
new_room: Arc<Item>,
}, // No lockable.
}
pub async fn is_door_in_direction(
trans: &DBTrans,
direction: &Direction,
use_location: &str,
) -> UResult<DoorSituation> {
let (loc_type_t, loc_type_c) = use_location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
let cur_loc_item = trans
.find_item_by_type_code(loc_type_t, loc_type_c)
.await?
.ok_or_else(|| UserError("Can't find your current location anymore.".to_owned()))?;
let new_loc_item = direction_to_item(trans, use_location, direction)
.await?
.ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?;
if let Some(door_state) = cur_loc_item
.door_states
.as_ref()
.and_then(|v| v.get(direction))
{
return Ok(DoorSituation::DoorOutOfRoom {
state: door_state.clone(),
room_with_door: cur_loc_item,
new_room: new_loc_item,
});
}
if let Some(door_state) = new_loc_item.door_states.as_ref().and_then(|v| {
direction
.reverse()
.as_ref()
.and_then(|rev| v.get(rev).map(|door| door.clone()))
}) {
return Ok(DoorSituation::DoorIntoRoom {
state: door_state.clone(),
room_with_door: new_loc_item,
current_room: cur_loc_item,
});
}
Ok(DoorSituation::NoDoor)
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let dir =
Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?;
let player_item = get_player_item_or_fail(ctx).await?;
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::OpenDoor {
direction: dir.clone(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,108 +0,0 @@
use super::{
get_player_item_or_fail, is_likely_illegal, parsing::parse_to_space, search_item_for_user,
user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
use ansi::{ansi, ignore_special_characters};
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<()> {
let (to_whom_name, say_what_raw) = if verb.starts_with("r") {
let last_page_from = match ctx
.user_dat
.as_ref()
.and_then(|u| u.last_page_from.as_ref())
{
None => user_error("No one has paged you, so you can't reply.".to_owned())?,
Some(m) => (*m).clone(),
};
(last_page_from, remaining)
} else {
let (to_whom, say_what) = parse_to_space(remaining);
(to_whom.to_owned(), say_what)
};
let say_what = ignore_special_characters(&say_what_raw);
if say_what == "" {
user_error("You need to provide a message to send.".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?;
}
let to_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_active_players: true,
limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name)
},
)
.await?;
match to_whom.item_type.as_str() {
"player" => {}
_ => user_error("Only players accept pages".to_string())?,
}
if is_likely_illegal(&say_what) {
user_error("Your message was rejected by the content filter".to_string())?;
}
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<blue>You page {} on your wristpad: \"{}\"<reset>\n"),
to_whom.display_for_sentence(1, false),
say_what
)),
)
.await?;
if player_item == to_whom {
return Ok(());
}
match to_whom.item_type.as_str() {
"player" => {
match ctx
.trans
.find_session_for_player(&to_whom.item_code)
.await?
{
None => user_error("That character is asleep.".to_string())?,
Some((other_session, _other_session_dets)) => {
if let Some(mut user) =
ctx.trans.find_by_username(&to_whom.item_code).await?
{
user.last_page_from = Some(player_item.item_code.clone());
ctx.trans.save_user_model(&user).await?;
}
ctx.trans
.queue_for_session(
&other_session,
Some(&format!(
ansi!("<blue>Your wristpad beeps with page from {}: \"{}\"<reset>\n"),
player_item.display_for_sentence(1, false),
say_what
)),
)
.await?;
}
}
}
_ => {}
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,17 +1,10 @@
use super::{
allow::{AllowCommand, ConsentDetails, ConsentTarget},
pay::{FinancialAccount, PaymentRequest},
};
use crate::models::consent::ConsentType;
use ansi::{ansi, strip_special_characters};
use nom::{
bytes::complete::{take_till, take_till1, take_while},
character::{complete::{space0, space1, alpha1, one_of, char, u8}},
combinator::{recognize, fail, eof},
sequence::terminated,
branch::alt,
bytes::complete::tag,
bytes::complete::{take_till, take_till1, take_while, take_while1},
character::complete::{alpha1, char, one_of, space0, space1, u16, u64, u8},
combinator::{cut, eof, fail, map, opt, peek, recognize},
error::{context, VerboseError, VerboseErrorKind},
sequence::{pair, preceded, terminated},
IResult,
};
@ -20,7 +13,7 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
let (input, _) = space0(input)?;
let (input, cmd) = alt((
recognize(one_of("-\"':.")),
take_till1(|c| c == ' ' || c == '\t'),
take_till1(|c| c == ' ' || c == '\t')
))(input)?;
let (input, _) = space0(input)?;
Ok((input, cmd))
@ -28,7 +21,7 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
match parse(input) {
/* This parser only fails on empty / whitespace only strings. */
Err(_) => ("", ""),
Ok((rest, command)) => (command, rest),
Ok((rest, command)) => (command, rest)
}
}
@ -38,7 +31,7 @@ pub fn parse_to_space(input: &str) -> (&str, &str) {
}
match parser(input) {
Err(_) => ("", ""), /* Impossible? */
Ok((rest, token)) => (token, rest),
Ok((rest, token)) => (token, rest)
}
}
@ -48,17 +41,7 @@ pub fn parse_offset(input: &str) -> (Option<u8>, &str) {
}
match parser(input) {
Err(_) => (None, input),
Ok((rest, result)) => (Some(result), rest),
}
}
pub fn parse_count(input: &str) -> (Option<u8>, &str) {
fn parser(input: &str) -> IResult<&str, u8> {
terminated(u8, char(' '))(input)
}
match parser(input) {
Err(_) => (None, input),
Ok((rest, result)) => (Some(result), rest),
Ok((rest, result)) => (Some(result), rest)
}
}
@ -66,256 +49,21 @@ 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)?;
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)?;
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 {
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),
}
}
pub fn parse_on_or_default<'l>(input: &'l str, default_on: &'l str) -> (&'l str, &'l str) {
if let Some((a, b)) = input.split_once(" on ") {
(a, b)
} else {
(input, default_on)
}
}
pub fn parse_duration_mins<'l>(input: &'l str) -> Result<(u64, &'l str), String> {
let (input, number) = match u16::<&'l str, ()>(input) {
Err(_) => Err("Invalid number - duration should start with a number, e.g. 5 minutes")?,
Ok(n) => n,
};
let (tok, input) = match input.trim_start().split_once(" ") {
None => (input, ""),
Some(v) => v,
};
Ok((match tok.to_lowercase().as_str() {
"min" | "mins" | "minute" | "minutes" => number as u64,
"h" | "hr" | "hrs" | "hour" | "hours" => (number as u64) * 60,
"d" | "day" | "days" => (number as u64) * 60 * 24,
"w" | "wk" | "wks" | "week" | "weeks" => (number as u64) * 60 * 24 * 7,
_ => Err("Duration number needs to be followed by a valid unit - minutes, hours, days or weeks")?
}, input))
}
pub fn parse_allow<'l>(input: &'l str) -> Result<AllowCommand, String> {
let usage: &'static str =
ansi!("Usage: allow <lt>action> from <lt>user> <lt>options> | allow <lt>action> against <lt>corp> by <lt>corp> <lt>options>. Try <bold>help allow<reset> for more.");
let (consent_type_s, input) = match input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v),
}?;
let consent_type = match ConsentType::from_str(&consent_type_s.trim().to_lowercase()) {
None => Err("Invalid consent type - options are fight, medicine, gifts, visit and share"),
Some(ct) => Ok(ct),
}?;
let (tok, mut input) = match input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v),
}?;
let tok_trim = tok.trim_start().to_lowercase();
let consent_target = if tok_trim == "against" {
if consent_type != ConsentType::Fight {
Err("corps can only currently consent to fight, no other actions")?
} else {
let (my_corp_raw, new_input) = match input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v),
}?;
let my_corp = my_corp_raw.trim_start();
let (tok, new_input) = match new_input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v),
}?;
if tok.trim_start().to_lowercase() != "by" {
Err(usage)?;
}
let (target_corp_raw, new_input) = match new_input.trim_start().split_once(" ") {
None => (new_input.trim_start(), ""),
Some(v) => v,
};
input = new_input;
ConsentTarget::CorpTarget {
from_corp: my_corp,
to_corp: target_corp_raw.trim_start(),
}
}
} else if tok_trim == "from" {
let (target_user_raw, new_input) = match input.trim_start().split_once(" ") {
None => (input.trim_start(), ""),
Some(v) => v,
};
input = new_input;
ConsentTarget::UserTarget {
to_user: target_user_raw.trim_start(),
}
} else {
Err(usage)?
};
let mut consent_details = ConsentDetails::default_for(&consent_type);
loop {
input = input.trim_start();
if input == "" {
break;
}
let (tok, new_input) = match input.split_once(" ") {
None => (input, ""),
Some(v) => v,
};
match tok.to_lowercase().as_str() {
"for" => {
let (minutes, new_input) = parse_duration_mins(new_input)?;
input = new_input;
consent_details.duration_minutes = Some(minutes);
}
"until" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v,
};
if tok.trim_start().to_lowercase() != "death" {
Err("Option until needs to be followed with death - until death")?
}
consent_details.until_death = true;
input = new_input;
}
"allow" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v,
};
match tok.trim_start().to_lowercase().as_str() {
"private" => {
consent_details.allow_private = true;
},
"pick" => {
consent_details.allow_pick = true;
},
"revoke" => {
consent_details.freely_revoke = true;
},
_ => Err("Option allow needs to be followed with private, pick or revoke - allow private | allow pick | allow revoke")?
}
input = new_input;
}
"disallow" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v,
};
match tok.trim_start().to_lowercase().as_str() {
"private" => {
consent_details.allow_private = false;
},
"pick" => {
consent_details.allow_pick = false;
},
_ => Err("Option disallow needs to be followed with private or pick - disallow private | disallow pick")?
}
input = new_input;
}
"in" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v,
};
consent_details.only_in.push(tok);
input = new_input;
}
_ => Err(format!(
"I don't understand the option \"{}\"",
strip_special_characters(tok)
))?,
}
}
Ok(AllowCommand {
consent_type: consent_type,
consent_target: consent_target,
consent_details: consent_details,
})
}
pub fn parse_payment_request(
input: &str,
my_account: &FinancialAccount,
) -> Result<PaymentRequest<FinancialAccount>, &'static str> {
let input = input.trim_start();
let account_parser = || {
context(
"Expected account to pay to/from to be username, corpname, or me",
cut(terminated(
alt((
map(tag::<&str, &str, VerboseError<&str>>("treasury"), |_| {
FinancialAccount::Treasury
}),
map(tag("me"), |_| my_account.clone()),
map(
take_while1(|c: char| c.is_alphanumeric() || c == '_'),
|n: &str| FinancialAccount::NamedAccount(n.to_owned()),
),
)),
peek(alt((eof, space1))),
)),
)
};
let res = pair(
context(
"No amount to pay found",
preceded(opt(char('$')), terminated(u64, space1)),
),
pair(
opt(preceded(
tag("from"),
preceded(space1, terminated(account_parser(), space1)),
)),
terminated(
context(
"You need to specify who to pay with \"to\"",
preceded(preceded(tag("to"), space1), account_parser()),
),
eof,
),
),
)(input);
const CATCHALL_ERROR: &'static str = "Usage: \"pay $123 from account to account\". Leave off from to default to from you. Account can be \"me\" or a username or corpname";
match res {
Ok((_, (amount, (from, to)))) => Ok(PaymentRequest {
amount,
from: from.unwrap_or_else(|| my_account.clone()),
to,
}),
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),
_ => None
}).unwrap_or(CATCHALL_ERROR)),
Err(_) => Err(CATCHALL_ERROR)
}
}
@ -325,29 +73,28 @@ mod tests {
#[test]
fn it_parses_normal_command() {
assert_eq!(parse_command_name("help"), ("help", ""));
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")
);
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")
);
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 "), ("", ""));
assert_eq!(parse_command_name(""),
("", ""));
assert_eq!(parse_command_name(" \t "),
("", ""));
}
#[test]
@ -357,10 +104,7 @@ mod tests {
#[test]
fn it_parses_usernames_with_further_args() {
assert_eq!(
parse_username("Wizard_123 with cat"),
Ok(("Wizard_123", "with cat"))
);
assert_eq!(parse_username("Wizard_123 with cat"), Ok(("Wizard_123", "with cat")));
}
#[test]
@ -390,18 +134,12 @@ mod tests {
#[test]
fn it_fails_on_usernames_with_bad_characters() {
assert_eq!(
parse_username("Wizard!"),
Err("Must only contain alphanumeric characters or _")
);
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")
);
assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters"));
}
#[test]
@ -426,125 +164,4 @@ mod tests {
fn parse_offset_supports_offset() {
assert_eq!(parse_offset("2.hello world"), (Some(2), "hello world"))
}
#[test]
fn parse_consent_works_default_options_user() {
assert_eq!(
super::parse_allow("medicine From Athorina"),
Ok(AllowCommand {
consent_type: ConsentType::Medicine,
consent_target: ConsentTarget::UserTarget {
to_user: "Athorina"
},
consent_details: ConsentDetails::default_for(&ConsentType::Medicine)
})
)
}
#[test]
fn parse_consent_works_default_options_corp() {
assert_eq!(
super::parse_allow("Fight Against megacorp By supercorp"),
Ok(AllowCommand {
consent_type: ConsentType::Fight,
consent_target: ConsentTarget::CorpTarget {
from_corp: "megacorp",
to_corp: "supercorp"
},
consent_details: ConsentDetails::default_for(&ConsentType::Fight)
})
)
}
#[test]
fn parse_consent_handles_options() {
assert_eq!(super::parse_allow("fighT fRom athorina For 2 hOurs unTil deAth allOw priVate Disallow pIck alLow revoKe iN here in pit"),
Ok(AllowCommand {
consent_type: ConsentType::Fight,
consent_target: ConsentTarget::UserTarget { to_user: "athorina" },
consent_details: ConsentDetails {
duration_minutes: Some(120),
until_death: true,
allow_private: true,
allow_pick: false,
freely_revoke: true,
only_in: vec!("here", "pit"),
..ConsentDetails::default_for(&ConsentType::Fight)
}
}))
}
#[test]
fn parse_payment_request_happy_path_works() {
assert_eq!(
super::parse_payment_request(
"$10 to mafia",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Ok(PaymentRequest {
amount: 10,
from: FinancialAccount::NamedAccount("namey".to_owned()),
to: FinancialAccount::NamedAccount("mafia".to_owned())
})
)
}
#[test]
fn parse_payment_request_explicit_from_works() {
assert_eq!(
super::parse_payment_request(
"10 from mafia to treasury",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Ok(PaymentRequest {
amount: 10,
from: FinancialAccount::NamedAccount("mafia".to_owned()),
to: FinancialAccount::Treasury
})
)
}
#[test]
fn parse_payment_wrong_start_err() {
assert_eq!(
super::parse_payment_request(
"nonsense",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("No amount to pay found")
)
}
#[test]
fn parse_payment_wrong_from_err() {
assert_eq!(
super::parse_payment_request(
"10 from ! to me",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("Expected account to pay to/from to be username, corpname, or me")
)
}
#[test]
fn parse_payment_wrong_to_err() {
assert_eq!(
super::parse_payment_request(
"10 from me for fun",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("You need to specify who to pay with \"to\"")
)
}
#[test]
fn parse_payment_trailing_junk_err() {
assert_eq!(
super::parse_payment_request(
"10 from me to me for lolz",
&FinancialAccount::NamedAccount("namey".to_owned())
),
Err("Usage: \"pay $123 from account to account\". Leave off from to default to from you. Account can be \"me\" or a username or corpname")
)
}
}

View File

@ -1,267 +0,0 @@
use super::{
corp::check_corp_perm, get_user_or_fail, parsing::parse_payment_request, user_error, UResult,
UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{Corp, CorpCommType, CorpId, CorpPermission},
user::{User, UserFlag},
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use log::info;
use mockall_double::double;
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum FinancialAccount {
NamedAccount(String),
Treasury, // Staff only source of unlimited funds
}
#[derive(Clone, PartialEq, Debug)]
pub enum ResolvedFinancialAccount {
CorpAccount((CorpId, Corp)),
UserAccount(User),
Treasury,
}
#[derive(Clone, PartialEq, Debug)]
pub struct PaymentRequest<AccDetails> {
pub from: AccDetails,
pub to: AccDetails,
pub amount: u64,
}
async fn resolve_account(
trans: &DBTrans,
inp: &FinancialAccount,
) -> UResult<ResolvedFinancialAccount> {
match inp {
FinancialAccount::Treasury => Ok(ResolvedFinancialAccount::Treasury),
FinancialAccount::NamedAccount(ref name) => {
match trans.find_corp_by_name(name.as_str()).await? {
Some(corp) => Ok(ResolvedFinancialAccount::CorpAccount(corp)),
None => match trans.find_by_username(name.as_str()).await? {
Some(user) => Ok(ResolvedFinancialAccount::UserAccount(user)),
None => user_error(format!(
"I couldn't find a user or corp matching \"{}\"",
name.as_str()
)),
},
}
}
}
}
async fn resolve_payment_request(
trans: &DBTrans,
inp: &PaymentRequest<FinancialAccount>,
) -> UResult<PaymentRequest<ResolvedFinancialAccount>> {
Ok(PaymentRequest {
from: resolve_account(trans, &inp.from).await?,
to: resolve_account(trans, &inp.to).await?,
amount: inp.amount.clone(),
})
}
async fn verify_access(
trans: &DBTrans,
user: &User,
inp: &PaymentRequest<ResolvedFinancialAccount>,
) -> UResult<()> {
if inp.from == inp.to {
user_error("That seems a bit like a money-go-round!".to_owned())?;
}
if inp.from == ResolvedFinancialAccount::Treasury
|| inp.to == ResolvedFinancialAccount::Treasury
{
if !user.user_flags.contains(&UserFlag::Staff) {
user_error("I'm sorry Dave, I can't let you do that!".to_owned())?;
}
// Treasury deliberately bypasses all other access checks, but
// even staff have to follow the rules for payments not involving
// treasury.
} else {
match inp.from {
ResolvedFinancialAccount::UserAccount(ref usr) => {
if usr.username != user.username {
user_error("Sadly, it appears the wristpad system won't let you access other people's funds.".to_owned())?;
}
}
ResolvedFinancialAccount::CorpAccount(ref corp) => {
match trans
.match_user_corp_by_name(&corp.1.name, &user.username)
.await?
{
None => user_error(format!("You're not even a member of {}!", &corp.1.name))?,
Some((_, _, mem)) => {
if !check_corp_perm(&CorpPermission::Finance, &mem) {
user_error(format!(
"You lack finance permissions in {}",
&corp.1.name
))?;
}
}
}
}
_ => user_error("Oops, can't verify you have access to pay".to_owned())?,
}
}
Ok(())
}
fn get_name(acc: &ResolvedFinancialAccount) -> String {
match acc {
ResolvedFinancialAccount::Treasury => "Treasury".to_owned(),
ResolvedFinancialAccount::UserAccount(ref usr) => usr.username.clone(),
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.name.clone(),
}
}
fn get_balance(acc: &ResolvedFinancialAccount) -> u64 {
match acc {
ResolvedFinancialAccount::Treasury => 10000000000,
ResolvedFinancialAccount::UserAccount(ref usr) => usr.credits,
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.credits,
}
}
async fn modify_balance<F>(trans: &DBTrans, acc: &mut ResolvedFinancialAccount, f: F) -> DResult<()>
where
F: Fn(u64) -> u64,
{
match acc {
ResolvedFinancialAccount::Treasury => {}
ResolvedFinancialAccount::UserAccount(ref mut usr) => {
usr.credits = f(usr.credits);
trans.save_user_model(&usr).await?;
}
ResolvedFinancialAccount::CorpAccount(ref mut corp) => {
corp.1.credits = f(corp.1.credits);
trans.update_corp_details(&corp.0, &corp.1).await?;
}
}
Ok(())
}
fn involves_treasury(req: &PaymentRequest<ResolvedFinancialAccount>) -> bool {
req.from == ResolvedFinancialAccount::Treasury || req.to == ResolvedFinancialAccount::Treasury
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let user = get_user_or_fail(ctx)?;
let req = match parse_payment_request(
remaining,
&FinancialAccount::NamedAccount(user.username.clone()),
) {
Err(msg) => user_error(msg.to_owned())?,
Ok(r) => r,
};
if req.amount == 0 || req.amount >= 10000000000000 {
user_error(
"The wristpad seems to require a number between $1 and $10 trillion.".to_owned(),
)?;
}
let mut req = resolve_payment_request(&ctx.trans, &req).await?;
verify_access(&ctx.trans, &user, &req).await?;
if get_balance(&req.from) < req.amount {
user_error(ansi!("The wristpad plays a sad sounding beep, and a red cross comes up, with the text: <red>DECLINED Insufficient Funds<reset>. [Hint: Try having rich parents, or not spending all your credits on smashed avo, and you might be able to afford to make your payments]").to_owned())?;
}
modify_balance(&ctx.trans, &mut req.to, |a| a + req.amount).await?;
modify_balance(&ctx.trans, &mut req.from, |a| a - req.amount).await?;
let mut notify_corps: Vec<(CorpId, String)> = vec![];
match req.from {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
match req.to {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
for (corp_id, corp_name) in notify_corps {
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
ansi!("<green>[{}] {} has transferred ${} from {} to {}.<reset>\n"),
&corp_name,
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
&get_name(&req.to),
),
)
.await?;
}
match req.to {
ResolvedFinancialAccount::UserAccount(ref other_usr)
if other_usr.username != user.username =>
{
if let Some((sess, _)) = ctx
.trans
.find_session_for_player(&other_usr.username.to_lowercase())
.await?
{
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
ansi!("<green>{} has transferred ${} from {} to you.<reset>\n"),
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
)),
)
.await?;
}
}
_ => {}
}
if involves_treasury(&req) {
info!(
"{} has transferred ${} from {} to {}.",
&user.username,
req.amount,
&get_name(&req.from),
&get_name(&req.to),
);
}
ctx.trans.queue_for_session(&ctx.session,
Some(ansi!("Your wristpad displays a <green>green<reset> tick, indicating a successful transaction.\n"))).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,143 +0,0 @@
use crate::{
db::ItemSearchParams,
models::{
item::ItemSpecialData,
task::{Task, TaskDetails, TaskMeta},
},
services::comms::broadcast_to_room,
static_content::{
dynzone::{dynzone_by_type, DynzoneType},
possession_type::possession_data,
room::room_map_by_code,
},
DResult,
};
use super::{
get_player_item_or_fail, search_item_for_user, user_error, CommandHandlingError, UResult,
UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use ansi::ansi;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use mockall_double::double;
async fn place_has_power(trans: &DBTrans, place_type: &str, place_code: &str) -> DResult<bool> {
match place_type {
"room" => room_map_by_code()
.get(place_code)
.map(|r| Ok(r.has_power))
.unwrap_or(Ok(false)),
"dynroom" => trans
.find_item_by_type_code(place_type, place_code)
.await?
.and_then(|place| match place.special_data.as_ref() {
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => DynzoneType::from_str(&dynzone_code)
.and_then(|dzt| dynzone_by_type().get(&dzt))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()))
.map(|dr| Ok(dr.has_power)),
_ => None,
})
.unwrap_or(Ok(false)),
_ => Ok(false),
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (word, remaining) = match remaining.split_once(" ") {
None => user_error(ansi!("Try <bold>plug in<reset> something").to_owned())?,
Some(v) => v,
};
if word.trim() != "in" {
user_error(ansi!("Try <bold>plug in<reset> something").to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Plugging metal things in can make you dead, but being dead doesn't let you plug things in.".to_owned())?;
}
let remaining = remaining.trim();
let item = search_item_for_user(
ctx,
&ItemSearchParams {
limit: 1,
include_loc_contents: true,
item_type_only: Some("possession"),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await
.map_err(|e| match e {
CommandHandlingError::UserError(m) => CommandHandlingError::UserError(
m + " I only looked in the room - try dropping an item first.",
),
e => e,
})?;
let charge_data = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.charge_data.as_ref())
{
Some(v) if v.electric_recharge => v,
_ => user_error("You can't recharge that!".to_owned())?,
};
let (place_type, place_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Bad location".to_owned()))?;
if !place_has_power(&ctx.trans, place_type, place_code).await? {
user_error("You can't find any power sockets to plug in to.".to_owned())?;
}
if item.charges >= charge_data.max_charges {
user_error("You realise it's already fully charged.".to_owned())?;
}
let refstr = item.refstr();
if ctx
.trans
.check_task_by_type_code("ChargeItem", &refstr)
.await?
{
user_error("It's already charging!".to_owned())?;
}
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: refstr.clone(),
next_scheduled: Utc::now() + Duration::try_minutes(1).unwrap(),
..Default::default()
},
details: TaskDetails::ChargeItem { item: refstr },
})
.await?;
broadcast_to_room(
&ctx.trans,
&player_item.location,
None,
&format!(
"{} plugs in {}.\n",
&player_item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
),
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,33 +0,0 @@
use crate::{models::effect::EffectType, services::combat::switch_to_power_attack};
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Power attack while dead? You can't even do a regular attack.".to_owned())?;
}
if player_item
.active_effects
.iter()
.any(|v| v.0 == EffectType::Stunned)
{
user_error("You stay still like a stunned mullet, unable to gain the composure to powerattack.".to_owned())?;
}
switch_to_power_attack(ctx, &player_item).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,383 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_item_for_user, search_items_for_user,
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::{Item, ItemFlag, LocationActionType},
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel},
comms::broadcast_to_room,
},
static_content::possession_type::{possession_data, ContainerFlag},
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to put it in, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (container_code, item_code) = match ctx.command {
QueueCommand::Put {
container_possession_id,
target_possession_id,
} => (container_possession_id, target_possession_id),
_ => user_error("Expected Put command".to_owned())?,
};
let container = ctx
.trans
.find_item_by_type_code("possession", &container_code)
.await?
.ok_or_else(|| UserError("Item to put in not found".to_owned()))?;
if container.location != ctx.item.location && container.location != ctx.item.refstr() {
user_error(format!(
"You try to put something in {} but realise {} is no longer there",
container.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &item_code)
.await?
.ok_or_else(|| UserError("Item to place not found".to_owned()))?;
if item.location != ctx.item.refstr() {
user_error(format!(
"You try to put {} in {}, but realise you no longer have it",
item.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let msg = format!(
"{} fumbles around trying to put {} in {}.\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false),
&container.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to put it in, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (container_code, item_code) = match ctx.command {
QueueCommand::Put {
container_possession_id,
target_possession_id,
} => (container_possession_id, target_possession_id),
_ => user_error("Expected Put command".to_owned())?,
};
let container = ctx
.trans
.find_item_by_type_code("possession", &container_code)
.await?
.ok_or_else(|| UserError("Item to put in not found".to_owned()))?;
if container.location != ctx.item.location && container.location != ctx.item.refstr() {
user_error(format!(
"You try to put something in {} but realise {} is no longer there",
container.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let item = ctx
.trans
.find_item_by_type_code("possession", &item_code)
.await?
.ok_or_else(|| UserError("Item to place not found".to_owned()))?;
if item.location != ctx.item.refstr() {
user_error(format!(
"You try to put {} in {}, but realise you no longer have it",
item.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))?
}
let container_data = container
.possession_type
.as_ref()
.and_then(|pt| {
possession_data()
.get(pt)
.and_then(|pd| pd.container_data.as_ref())
})
.ok_or_else(|| {
UserError(format!(
"You try to put {} in {}, but can't find out a way to get anything in it",
item.display_for_sentence(1, false),
container.display_for_sentence(1, false),
))
})?;
container_data.checker.check_place(&container, &item)?;
let msg = format!(
"{} puts {} in {}.\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false),
&container.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
let possession_dat = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
};
let is_loadable = container
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.map_or(false, |pd| pd.weapon_data.is_some());
match check_item_capacity(ctx.trans, &container, possession_dat.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened if is_loadable => {
user_error(format!(
"{} is already fully loaded",
container.display_for_sentence(1, true)
))?
}
CapacityLevel::AboveItemLimit => user_error(format!(
"{} just can't hold that many things!",
container.display_for_sentence(1, true),
))?,
CapacityLevel::OverBurdened => user_error(format!(
"{} You can't place {} because it is too heavy!",
if true { "Fuck!" } else { "Rats!" },
&ctx.item.display_for_sentence(1, false)
))?,
_ => (),
}
let mut item_mut = (*item).clone();
item_mut.location = container.refstr();
item_mut.action_type = LocationActionType::Normal;
ctx.trans.save_item_model(&item_mut).await?;
recalculate_container_weight(&ctx.trans, &container).await?;
Ok(())
}
}
pub enum PutCasesThatSkipContainer {
BookInWorkstation,
AmmoInWeapon,
}
impl PutCasesThatSkipContainer {
async fn announce(
&self,
ctx: &mut VerbContext<'_>,
into_what: &Item,
target: &Item,
) -> UResult<()> {
match self {
Self::BookInWorkstation => {
ctx.trans
.queue_for_session(&ctx.session,
Some(
&format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n",
&target.display_for_sentence(1, false),
&into_what.display_for_sentence(1, false)),
)
).await?;
}
_ => {}
}
Ok(())
}
}
fn check_for_special_put_case(
into_what: &Item,
target: &Item,
) -> Option<PutCasesThatSkipContainer> {
if into_what.flags.contains(&ItemFlag::Bench) && target.flags.contains(&ItemFlag::Book) {
return Some(PutCasesThatSkipContainer::BookInWorkstation);
}
let into_pd = match into_what
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
{
None => return None,
Some(v) => v,
};
if into_pd.container_data.is_none() {
return None;
}
let target_pd = match target
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
{
None => return None,
Some(v) => v,
};
let target_cd = match target_pd.container_data.as_ref() {
None => return None,
Some(v) => v,
};
if into_pd.weapon_data.is_some() && target_cd.container_flags.contains(&ContainerFlag::AmmoClip)
{
return Some(PutCasesThatSkipContainer::AmmoInWeapon);
}
None
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
get_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
get_limit = Some(n);
remaining = remaining2;
}
let (into_what, for_what) = match remaining
.split_once(" in ")
.or_else(|| remaining.split_once(" into "))
{
None => {
user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())?
}
Some((item_str_raw, container_str_raw)) => {
let container = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
include_contents: true,
item_type_only: Some("possession"),
..ItemSearchParams::base(&player_item, container_str_raw.trim())
},
)
.await?;
(container, item_str_raw.trim())
}
};
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, for_what)
},
)
.await?;
if player_item.death_data.is_some() {
user_error(
"You try to put it in, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let mut did_anything: bool = false;
let mut player_item_mut = (*player_item).clone();
for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type == into_what.item_type && target.item_code == into_what.item_code {
user_error(
"You briefly ponder whether something can contain itself, but it blows your mind and you give up.".to_owned()
)?;
}
if target.item_type != "possession" {
user_error("You can't put that in something!".to_owned())?;
}
did_anything = true;
match check_for_special_put_case(&into_what, &target) {
None => {}
Some(special_put) => {
let subitems = ctx.trans.find_items_by_location(&target.refstr()).await?;
if !subitems.is_empty() {
ctx.trans
.queue_for_session(&ctx.session,
Some(
&format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n",
&target.display_for_sentence(1, false),
&into_what.display_for_sentence(1, false)),
)
).await?;
for subitem in subitems.iter().take(10) {
special_put.announce(ctx, &into_what, &target).await?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::GetFromContainer {
from_item_id: target.refstr(),
get_possession_id: subitem.item_code.clone(),
},
)
.await?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Put {
container_possession_id: into_what.item_code.clone(),
target_possession_id: subitem.item_code.clone(),
},
)
.await?;
}
continue;
}
}
}
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Put {
container_possession_id: into_what.item_code.clone(),
target_possession_id: target.item_code.clone(),
},
)
.await?;
}
if !did_anything {
user_error("I didn't find anything matching.".to_owned())?;
} else {
ctx.trans.save_item_model(&player_item_mut).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,19 +1,15 @@
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use ansi::ansi;
use super::{
VerbContext, UserVerb, UserVerbRef, UResult
};
use async_trait::async_trait;
use ansi::ansi;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
ctx.trans
.queue_for_session(ctx.session, Some(ansi!("<red>Bye!<reset>\r\n")))
.await?;
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
ctx.trans.queue_for_session(ctx.session,
Some(ansi!("<red>Bye!<reset>\r\n"))).await?;
ctx.trans.queue_for_session(ctx.session, None).await?;
Ok(())
}

View File

@ -1,237 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::LocationActionType,
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{comms::broadcast_to_room, urges::recalculate_urge_growth},
static_content::room::{room_map_by_code, MaterialType},
};
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to recline, but it turns out the dead can't even do that!".to_owned(),
)?;
}
let item_ref = match ctx.command {
QueueCommand::Recline { item } => item,
_ => user_error("Unexpected command".to_owned())?,
};
if ctx
.item
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.is_some()
{
user_error(
"Recline... while fighting? You can't figure out how to make it work".to_owned(),
)?
}
if ctx.item.active_climb.is_some() {
user_error(
"Recline... while climbing? You can't figure out how to make it work".to_owned(),
)?
}
match ctx.item.action_type {
LocationActionType::Reclining { .. } => {
user_error("You're already reclining.".to_owned())?
}
_ => {}
}
let (loc_type, loc_code) = match ctx.item.location.split_once("/") {
None => user_error("You've ended up in a bad place.".to_owned())?,
Some(v) => v,
};
if loc_type == "room" {
if let Some(room) = room_map_by_code().get(loc_code) {
match room.material_type {
MaterialType::Underwater | MaterialType::WaterSurface => {
user_error("You're already floating around".to_owned())?
}
_ => {}
}
}
}
match item_ref {
None => {}
Some(item_ref) => {
let (item_type, item_code) = match item_ref.split_once("/") {
None => user_error("Invalid item ref in Reclining command".to_owned())?,
Some(v) => v,
};
match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(item) => {
if item.location != ctx.item.location {
user_error(format!(
"You try to reclining on {} but realise it's no longer here",
item.display_for_sentence(1, false)
))?
}
}
}
}
};
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to recline, but it turns out the dead can't even do that!".to_owned(),
)?;
}
let item_ref = match ctx.command {
QueueCommand::Recline { item } => item,
_ => user_error("Unexpected command".to_owned())?,
};
if ctx
.item
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.is_some()
{
user_error(
"Recline... while fighting? You can't figure out how to make it work".to_owned(),
)?
}
if ctx.item.active_climb.is_some() {
user_error(
"Recline... while climbing? You can't figure out how to make it work".to_owned(),
)?
}
match ctx.item.action_type {
LocationActionType::Reclining { .. } => {
user_error("You're already reclining.".to_owned())?
}
_ => {}
}
let (loc_type, loc_code) = match ctx.item.location.split_once("/") {
None => user_error("You've ended up in a bad place.".to_owned())?,
Some(v) => v,
};
if loc_type == "room" {
if let Some(room) = room_map_by_code().get(loc_code) {
match room.material_type {
MaterialType::Underwater | MaterialType::WaterSurface => {
user_error("You're already floating around".to_owned())?
}
_ => {}
}
}
}
let (item, desc) = match item_ref {
None => (None, "the floor".to_owned()),
Some(item_ref) => {
let (item_type, item_code) = match item_ref.split_once("/") {
None => user_error("Invalid item ref in Sit command".to_owned())?,
Some(v) => v,
};
match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(item) => {
if item.location != ctx.item.location {
user_error(format!(
"You try to recline on {} but realise it's no longer here",
item.display_for_sentence(1, false)
))?
}
(Some(item.clone()), item.display_for_sentence(1, false))
}
}
}
};
let msg = format!(
"{} reclines on {}.\n",
&ctx.item.display_for_sentence(1, true),
&desc
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
ctx.item.action_type = LocationActionType::Reclining(item.map(|it| it.refstr()));
recalculate_urge_growth(&ctx.trans, &mut ctx.item).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if remaining.starts_with("on ") {
remaining = remaining[3..].trim();
}
if remaining == "" {
remaining = "floor";
}
let target = if remaining == "here" || remaining == "ground" || remaining == "floor" {
None
} else {
Some(
search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?,
)
};
if player_item.death_data.is_some() {
user_error(
"You try to recline, but it turns out the dead can't even do that!".to_owned(),
)?;
}
if let Some(target) = target.as_ref() {
if target
.static_data()
.and_then(|pd| pd.sit_data.as_ref())
.is_none()
{
user_error("You can't recline on that!".to_owned())?;
}
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Recline {
item: target.map(|t| t.refstr()),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,98 +1,36 @@
use super::{parsing::parse_username, user_error};
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use crate::models::{
item::{Item, Pronouns},
user::User,
};
use ansi::ansi;
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
use async_trait::async_trait;
use super::{user_error, parsing::parse_username};
use crate::models::{user::User, item::{Item, Pronouns}};
use chrono::Utc;
use once_cell::sync::OnceCell;
use std::collections::HashSet;
use ansi::ansi;
use tokio::time;
pub fn is_invalid_username(name: &str) -> bool {
static INVALID_PREFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
static INVALID_SUFFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
static INVALID_WORDS: OnceCell<HashSet<&'static str>> = OnceCell::new();
let invalid_prefixes =
INVALID_PREFIXES.get_or_init(|| vec!["admin", "god", "helper", "npc", "corpse", "dead"]);
let invalid_suffixes = INVALID_SUFFIXES.get_or_init(|| vec!["bot"]);
let invalid_words = INVALID_WORDS.get_or_init(|| {
HashSet::from([
"corp",
"to",
"from",
"dog",
"bot",
"for",
"against",
"on",
"privileges",
"as",
"treasury",
])
});
if invalid_words.contains(name) {
return true;
}
for pfx in invalid_prefixes.iter() {
if name.starts_with(pfx) {
return true;
}
}
for sfx in invalid_suffixes.iter() {
if name.ends_with(sfx) {
return true;
}
}
false
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (username, password, email) = match parse_username(remaining) {
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
Ok((username, rest)) => match rest.split_whitespace().collect::<Vec<&str>>()[..] {
Ok((username, rest)) => {
match rest.split_whitespace().collect::<Vec<&str>>()[..] {
[password, email] => (username, password, email),
[] | [_] => user_error(
"Too few options to register - supply username, password, and email".to_owned(),
)?,
_ => user_error(
"Too many options to register - supply username, password, and email"
.to_owned(),
)?,
},
};
if is_invalid_username(&username.to_lowercase()) {
user_error("Sorry, that username isn't allowed. Try another".to_owned())?;
[] | [_] => user_error("Too few options to register - supply username, password, and email".to_owned())?,
_ => user_error("Too many options to register - supply username, password, and email".to_owned())?,
}
}
};
if ctx.trans.find_by_username(username).await?.is_some() {
user_error("Username already exists".to_owned())?;
}
if ctx.trans.find_corp_by_name(username).await?.is_some() {
user_error("Username clashes with existing corp name".to_owned())?;
}
if password.len() < 6 {
user_error("Password must be 6 characters long or longer".to_owned())?;
} else if !validator::validate_email(email) {
user_error(
"Please supply a valid email in case you need to reset your password.".to_owned(),
)?;
user_error("Please supply a valid email in case you need to reset your password.".to_owned())?;
}
let player_item_id = ctx
.trans
.create_item(&Item {
let player_item_id = ctx.trans.create_item(&Item {
item_type: "player".to_owned(),
item_code: username.to_lowercase(),
display: username.to_owned(),
@ -100,8 +38,7 @@ impl UserVerb for Verb {
location: "room/repro_xv_chargen".to_owned(),
pronouns: Pronouns::default_animate(),
..Item::default()
})
.await?;
}).await?;
// Force a wait to protect against abuse.
time::sleep(time::Duration::from_secs(5)).await;
@ -116,19 +53,13 @@ impl UserVerb for Verb {
};
*ctx.user_dat = Some(user_dat);
ctx.trans
.queue_for_session(
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(
ansi!("Welcome <bold>{}<reset>, you are now officially registered.\r\n"),
&username
)),
)
.await?;
Some(&format!(ansi!("Welcome <bold>{}<reset>, you are now officially registered.\r\n"),
&username))
).await?;
super::agree::check_and_notify_accepts(ctx).await?;
ctx.trans
.create_user(ctx.session, ctx.user_dat.as_ref().unwrap())
.await?;
ctx.trans.create_user(ctx.session, ctx.user_dat.as_ref().unwrap()).await?;
Ok(())
}

View File

@ -1,242 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::{BuffCause, Item, LocationActionType},
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user},
static_content::possession_type::{possession_data, WearData},
};
use async_trait::async_trait;
use std::time;
async fn check_removeable(ctx: &mut QueuedCommandContext<'_>, item: &Item) -> UResult<()> {
if item.location != ctx.item.refstr() {
user_error(format!(
"You try to remove {} but realise you no longer have it.",
&item.display_for_sentence(1, false)
))?
}
if item.action_type != LocationActionType::Worn {
user_error("You realise you're not wearing it!".to_owned())?;
}
let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| {
UserError(
"That item no longer exists in the game so can't be handled. Ask staff for help."
.to_owned(),
)
})?;
let wear_data = poss_data.wear_data.as_ref().ok_or_else(|| {
UserError(
"You seem to be wearing something that isn't clothes! Ask staff for help.".to_owned(),
)
})?;
let other_clothes = ctx
.trans
.find_by_action_and_location(&ctx.item.refstr(), &LocationActionType::Worn)
.await?;
if let Some(my_worn_since) = item.action_type_started {
for part in &wear_data.covers_parts {
if let Some(other_item) = other_clothes.iter().find(|other_item| {
match other_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref())
{
None => false,
Some(WearData { covers_parts, .. }) => {
covers_parts.contains(&part)
&& other_item
.action_type_started
.map(|other_worn_since| other_worn_since > my_worn_since)
.unwrap_or(false)
}
}
}) {
user_error(format!(
"You can't do that without first removing your {} from your {}.",
&other_item.display_for_sentence(1, false),
part.display(ctx.item.sex.clone())
))?;
}
}
}
Ok(())
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to remove it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Remove { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
check_removeable(ctx, &item).await?;
let msg = format!(
"{} fumbles around trying to take off {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to remove it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Remove { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
check_removeable(ctx, &item).await?;
let msg = format!(
"{} removes {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
let mut item_mut = (*item).clone();
item_mut.action_type = LocationActionType::Normal;
item_mut.action_type_started = None;
let poss_data = item.possession_type.as_ref()
.and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| UserError(
"That item no longer exists in the game so can't be handled. Ask staff for help.".to_owned()))?;
let wear_data = poss_data.wear_data.as_ref().ok_or_else(|| {
UserError(
"You seem to be wearing something that isn't clothes! Ask staff for help."
.to_owned(),
)
})?;
ctx.trans.save_item_model(&item_mut).await?;
if wear_data.dodge_penalty != 0.0 {
ctx.item.temporary_buffs = ctx
.item
.temporary_buffs
.clone()
.into_iter()
.filter(|buf| {
buf.cause
!= BuffCause::ByItem {
item_code: item_mut.item_code.clone(),
item_type: item_mut.item_type.clone(),
}
})
.collect();
if ctx.item.item_type == "player" {
if let Some(usr) = ctx.trans.find_by_username(&ctx.item.item_code).await? {
calculate_total_stats_skills_for_user(ctx.item, &usr);
}
}
}
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
get_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
item_action_type_only: Some(&LocationActionType::Worn),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error("The dead don't undress themselves".to_owned())?;
}
let mut did_anything: bool = false;
let mut player_item_mut = (*player_item).clone();
for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" {
user_error("You can't remove that!".to_owned())?;
}
did_anything = true;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Remove {
possession_id: target.item_code.clone(),
},
)
.await?;
}
if !did_anything {
user_error("I didn't find anything matching.".to_owned())?;
} else {
ctx.trans.save_item_model(&player_item_mut).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,544 +0,0 @@
use super::{
corp::check_corp_perm, get_player_item_or_fail, get_user_or_fail, user_error, UResult,
UserError, UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::{
dynzone::dynzone_by_type,
npc::npc_by_code,
room::{room_map_by_code, Direction, RentSuiteType},
},
DResult,
};
use ansi::ansi;
use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::{DateTime, TimeDelta, Utc};
use itertools::Itertools;
use log::info;
use mockall_double::double;
use std::time;
#[async_recursion]
pub async fn recursively_destroy_or_move_item(trans: &DBTrans, item: &Item) -> DResult<()> {
let mut item_mut = item.clone();
match item.item_type.as_str() {
"npc" => {
let npc = match npc_by_code().get(item.item_code.as_str()) {
None => return Ok(()),
Some(r) => r,
};
item_mut.location = npc.spawn_location.to_owned();
trans.save_item_model(&item_mut).await?;
return Ok(());
}
"player" => {
let session = trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() {
Some((listener_sess, _)) => {
trans.queue_for_session(
&listener_sess,
Some(ansi!("<red>The landlord barges in with a bunch of very big hired goons, who stuff you in a sack, while the landlord mumbles something about vacant possession.<reset> After what seems like an eternity being jostled along while stuffed in the sack, they dump you out into a room that seems to be some kind of homeless shelter, and beat a hasty retreat.\n"))
).await?;
}
None => {}
}
item_mut.location = "room/melbs_homelessshelter".to_owned();
trans.save_item_model(&item_mut).await?;
return Ok(());
}
_ => {}
}
trans.delete_item(&item.item_type, &item.item_code).await?;
let loc = format!("{}/{}", &item.item_type, &item.item_code);
// It's paginated so we loop to get everything...
loop {
let result = trans.find_items_by_location(&loc).await?;
if result.is_empty() {
return Ok(());
}
for sub_item in result {
recursively_destroy_or_move_item(trans, &sub_item).await?;
}
}
}
static EVICTION_NOTICE: &'static str = ansi!(". Nailed to the door is a notice: <red>Listen here you lazy bum - you didn't pay your rent on time, and so unless you come down in the next 24 hours and re-rent the place (and pay the setup fee again for wasting my time), I'm having you, and any other deadbeats who might be in there with you, evicted, and I'm just gonna sell anything I find in there<reset>");
async fn bill_residential_room(
ctx: &mut TaskRunContext<'_>,
bill_player_code: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
let mut bill_user = match ctx.trans.find_by_username(bill_player_code).await? {
None => return Ok(None),
Some(user) => user,
};
let session = ctx.trans.find_session_for_player(bill_player_code).await?;
// Check if they have enough money.
if bill_user.credits < daily_price {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
let mut zone_item_mut = (*zone_item).clone();
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + TimeDelta::try_days(1).unwrap()),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
_ => None,
},
});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx
.trans
.find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/doorstep"))
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details =
Some(doorstep_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<red>Your wristpad beeps a sad sounding tone as your landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money fast \
and hurry to reception (in the next 24 hours) and sign a new rental \
agreement for the premises, or all your stuff will be gone forever!\n"),
&zone_item.display
)),
).await?
}
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_user.credits -= daily_price;
ctx.trans.save_user_model(&bill_user).await?;
match session.as_ref() {
None => {},
Some((listener, _)) => ctx.trans.queue_for_session(
listener, Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${} for rent for {}.<reset>\n"),
daily_price, &zone_item.display
))
).await?
}
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
async fn bill_commercial_room(
ctx: &mut TaskRunContext<'_>,
bill_corp: &str,
daily_price: u64,
zone_item: &Item,
vacate_after: Option<DateTime<Utc>>,
) -> DResult<Option<time::Duration>> {
let mut bill_corp = match ctx.trans.find_corp_by_name(bill_corp).await? {
None => return Ok(None),
Some(c) => c,
};
// Check if they have enough money.
if bill_corp.1.credits < daily_price {
if vacate_after.is_some() {
// If they are already on their way out, just ignore it - but we do need
// to keep the task in case they change their mind.
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
let mut zone_item_mut = (*zone_item).clone();
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + TimeDelta::try_days(1).unwrap()),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
_ => None,
},
});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx
.trans
.find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/reception"))
.await?
{
None => {}
Some(reception_room) => {
let mut reception_mut = (*reception_room).clone();
reception_mut.details =
Some(reception_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE);
ctx.trans.save_item_model(&reception_mut).await?;
}
}
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<red>All wristpads of members of {} beep a sad sounding tone as the corp's landlord at {} \
tries and fails to take rent.<reset> You'd better earn some more money for the corp \
fast and have your holder hurry to reception (in the next 24 hours) and sign a new \
lease agreement for the premises, or all your corp's stuff will be gone forever!\n"),
&bill_corp.1.name,
&zone_item.display
),
).await?;
return Ok(Some(time::Duration::from_secs(3600 * 24)));
}
bill_corp.1.credits -= daily_price;
ctx.trans
.update_corp_details(&bill_corp.0, &bill_corp.1)
.await?;
ctx.trans.broadcast_to_corp(
&bill_corp.0,
&CorpCommType::Notice,
None,
&format!(
ansi!("<yellow>Your wristpad beeps as a deduction is made from {}'s account of ${} for rent for {}.<reset>\n"),
&bill_corp.1.name,
daily_price, &zone_item.display
)
).await?;
Ok(Some(time::Duration::from_secs(3600 * 24)))
}
pub struct ChargeRoomTaskHandler;
#[async_trait]
impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (zone_item_ref, daily_price) = match ctx.task.details {
TaskDetails::ChargeRoom {
ref zone_item,
ref daily_price,
} => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?,
};
let zone_item_code = match zone_item_ref.split_once("/") {
Some(("dynzone", c)) => c,
_ => Err("Invalid zone item ref when charging room")?,
};
let zone_item = match ctx
.trans
.find_item_by_type_code("dynzone", zone_item_code)
.await?
{
None => {
info!(
"Can't charge rent for dynzone {}, it's gone",
zone_item_code
);
return Ok(None);
}
Some(it) => it,
};
let vacate_after = match zone_item.special_data {
Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after,
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")?,
};
match vacate_after {
Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(&ctx.trans, &zone_item).await?;
return Ok(None);
}
_ => (),
}
match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) {
Some((player_item_type, player_item_code)) if player_item_type == "player" => {
bill_residential_room(
ctx,
player_item_code,
daily_price.clone(),
&zone_item,
vacate_after,
)
.await
}
Some((item_type, corpname)) if item_type == "corp" => {
bill_commercial_room(ctx, corpname, daily_price.clone(), &zone_item, vacate_after)
.await
}
_ => {
info!(
"Can't charge rent for dynzone {}, owner {:?} isn't chargeable",
zone_item_code, &zone_item.owner
);
return Ok(None);
}
}
}
}
pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &ChargeRoomTaskHandler;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let (item_name, corp_name) = match remaining.split_once(" for ") {
None => (remaining, None),
Some((i, c)) => (i.trim(), Some(c.trim())),
};
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" {
user_error("You can't rent anything from here.".to_owned())?;
}
let room = room_map_by_code()
.get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() {
user_error("You can't rent anything from here.".to_owned())?;
}
let rentinfo = match room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
{
None => user_error(format!(
"Rent must be followed by the specific thing you want to rent: {}",
room.rentable_dynzone
.iter()
.map(|ri| ri.rent_what.as_str())
.join(", ")
))?,
Some(v) => v,
};
let user = get_user_or_fail(ctx)?;
let mut corp = match (&rentinfo.suite_type, corp_name) {
(RentSuiteType::Commercial, None) =>
user_error(format!(
ansi!("This is a commercial suite, you need to rent it in the name of a corp. Try <bold>rent {} for corpname<reset>"),
item_name
))?,
(RentSuiteType::Residential, Some(_)) =>
user_error("This is a residential suite, you can't rent it for a corp. Try finding a commercial suite for your corp, or rent it personally.".to_owned())?,
(RentSuiteType::Residential, None) => None,
(RentSuiteType::Commercial, Some(n)) => match ctx.trans.match_user_corp_by_name(&n, &user.username).await? {
None => user_error("I can't find that corp in your list of corps!".to_owned())?,
Some((_, _, mem)) if !check_corp_perm(&CorpPermission::Holder, &mem) => user_error("You don't have holder permissions in that corp.".to_owned())?,
Some((corp_id, corp, _)) => Some((corp_id, corp))
},
};
match corp.as_ref() {
None if user.credits < rentinfo.setup_fee =>
user_error("The robot rolls its eyes at you derisively. \"I don't think so - you couldn't even afford the setup fee!\"".to_owned())?,
Some((_, c)) if c.credits < rentinfo.setup_fee =>
user_error("The robot rolls its eyes at you derisively. \"I don't think so - your corp couldn't even afford the setup fee!\"".to_owned())?,
_ => {}
}
let zone = dynzone_by_type().get(&rentinfo.dynzone).ok_or_else(|| {
UserError("That seems to no longer exist, so you can't rent it.".to_owned())
})?;
let exit = Direction::IN {
item: corp
.as_ref()
.map(|c| c.1.name.clone())
.unwrap_or_else(|| player_item.display.clone()),
};
match ctx
.trans
.find_exact_dyn_exit(&player_item.location, &exit)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => {}
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) = ctx
.trans
.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: None, ..
}) => user_error(
"You can only rent one apartment here, and you already have one!"
.to_owned(),
)?,
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_),
zone_exit: ref ex,
}) => {
match corp.as_mut() {
None => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee
)),
)
.await?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just cancelled plans to vacate a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData {
vacate_after: None,
zone_exit: ex.clone(),
}),
..(*ex_zone).clone()
})
.await?;
match ctx
.trans
.find_item_by_type_code(
"dynroom",
&(ex_zone.item_code.clone() + "/" + zone.entrypoint_subcode),
)
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some(
doorstep_mut
.details
.clone()
.unwrap_or("".to_owned())
.replace(EVICTION_NOTICE, ""),
);
ctx.trans.save_item_model(&doorstep_mut).await?;
}
}
ctx.trans.queue_for_session(
ctx.session,
Some(&format!(ansi!(
"\"Okay - let's forget this ever happened - and apart from me having a few extra credits for my trouble, your lease will continue as before!\"\n")
))).await?;
return Ok(());
}
_ => {}
}
}
}
}
let owner = corp
.as_ref()
.map(|c| format!("corp/{}", c.1.name))
.unwrap_or_else(|| player_item.refstr());
let zonecode = zone
.create_instance(
ctx.trans,
&player_item.location,
"You can only rent one apartment here, and you already have one!",
&owner,
&exit,
)
.await?;
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("charge_rent/{}", &zonecode),
is_static: false,
recurrence: None, // Managed by the handler.
next_scheduled: Utc::now() + TimeDelta::try_days(1).unwrap(),
..Default::default()
},
details: TaskDetails::ChargeRoom {
zone_item: zonecode.clone(),
daily_price: rentinfo.daily_price,
},
})
.await?;
match corp.as_mut() {
None => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee
)),
)
.await?;
}
Some(corptup) => {
corptup.1.credits -= rentinfo.setup_fee;
ctx.trans
.update_corp_details(&corptup.0, &corptup.1)
.await?;
ctx.trans
.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
None,
&format!(
"[{}] {} just rented a {} for the corp for a setup fee of ${}.\n",
&corptup.1.name, &user.username, item_name, rentinfo.setup_fee
),
)
.await?;
}
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,41 +0,0 @@
use super::{get_user_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use ansi::ansi;
use async_trait::async_trait;
use uuid::Uuid;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
if remaining != "abuse" {
user_error(ansi!("Try <bold>report abuse<reset>.").to_owned())?;
}
let user = get_user_or_fail(ctx)?;
let username = user.username.to_lowercase();
if ctx.trans.clean_and_count_abusereports(&username).await? > 10 {
user_error(
ansi!(
"You have too many recent abuse reports logged to record any more. \
Contact staff@blastmud.org for help."
)
.to_owned(),
)?;
}
let uuid = Uuid::new_v4();
ctx.trans
.sendqueue_to_abusereport(&uuid, &username, &ctx.session)
.await?;
ctx.trans.queue_for_session(ctx.session, Some(&format!("Up to the last 80 things we sent you since you logged in will now be retained for at least 60 days under reference {}. Send an email to staff@blastmud.org explaining exactly what happened, and include this reference. We'll be able to look it up and investigate.\n", &uuid))).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,27 +0,0 @@
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::spawn::refresh_all_spawn_points;
use async_trait::async_trait;
use log::info;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
info!(
"Staff-triggered spawn reset by {}",
ctx.user_dat
.as_ref()
.map(|u| u.username.clone())
.unwrap_or_else(|| "???".to_owned())
);
refresh_all_spawn_points(&ctx.trans).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,70 +1,61 @@
use super::{
get_player_item_or_fail, is_likely_illegal, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
user_error,
get_player_item_or_fail, is_likely_explicit};
use crate::{
models::item::{Item, ItemFlag},
services::comms::broadcast_to_room,
services::broadcast_to_room,
db::DBTrans
};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use mockall_double::double;
use ansi::{ignore_special_characters, ansi};
pub async fn say_to_room<'l>(
trans: &DBTrans,
from_item: &Item,
location: &str,
say_what: &str,
is_explicit: bool
) -> UResult<()> {
let (loc_type, loc_code) = location
.split_once("/")
let (loc_type, loc_code) = location.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
let room_item = trans
.find_item_by_type_code(loc_type, loc_code)
.await?
let room_item = trans.find_item_by_type_code(loc_type, loc_code).await?
.ok_or_else(|| UserError("Room missing".to_owned()))?;
if room_item.flags.contains(&ItemFlag::NoSay) {
user_error(
"Your wristpad vibrates and flashes up an error - apparently it has \
been programmed to block your voice from working here."
.to_owned(),
)?
user_error("Your wristpad vibrates and flashes up an error - apparently it has \
been programmed to block your voice from working here.".to_owned())?
}
if is_likely_illegal(&say_what) {
user_error("Your message was rejected by the content filter".to_string())?;
}
let msg = format!(
let msg_exp = format!(
ansi!("<yellow>{} says: <reset><bold>\"{}\"<reset>\n"),
from_item.display_for_sentence(1, true),
from_item.display_for_sentence(true, 1, true),
say_what
);
let msg_lessexp = format!(
ansi!("<yellow>{} says: <reset><bold>\"{}\"<reset>\n"),
from_item.display_for_sentence(false, 1, true),
say_what
);
broadcast_to_room(trans, location, Some(from_item), &msg).await?;
broadcast_to_room(
trans,
location,
Some(from_item),
&msg_exp,
if is_explicit { None } else { Some(&msg_lessexp) }
).await?;
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let say_what = ignore_special_characters(remaining);
if say_what == "" {
user_error("You need to provide a message to send.".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?;
}
say_to_room(ctx.trans, &player_item, &player_item.location, &say_what).await
say_to_room(ctx.trans, &player_item, &player_item.location,
&say_what, is_likely_explicit(&say_what)).await
}
}
static VERB_INT: Verb = Verb;

View File

@ -1,56 +0,0 @@
use crate::static_content::room::room_map_by_code;
use super::{
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Your location is invalid".to_owned()))?;
if loc_type != "room" {
user_error("You can't find anything to scan here.".to_owned())?;
}
let room = room_map_by_code()
.get(&loc_code)
.ok_or_else(|| UserError("Your location no longer exists!".to_owned()))?;
let allowed_scan = room
.scan_code
.as_ref()
.ok_or_else(|| UserError("You can't find anything to scan here.".to_owned()))?;
let user = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
if !user.scan_codes.contains(&allowed_scan) {
user.scan_codes.push(allowed_scan.clone());
ctx.trans.save_user_model(&user).await?;
}
ctx.trans
.queue_for_session(
&ctx.session,
Some("Your wristpad beeps indicating a successful scan.\n"),
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,134 +0,0 @@
use super::{
drop::consider_expire_job_for_item, get_player_item_or_fail, user_error, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::{LocationActionType, Scavtype, SkillType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, CapacityLevel},
comms::broadcast_to_room,
skills::skill_check_and_grind,
},
};
use ansi::ansi;
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to search the area, but you realise your ghost hands aren't good for searching".to_owned(),
)?;
}
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
ansi!("<blue>{} starts methodically searching the area.<reset>\n"),
ctx.item.display_for_sentence(1, true),
),
)
.await?;
Ok(time::Duration::from_secs(3))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to search the area, but you realise your ghost hands aren't good for searching".to_owned(),
)?;
}
let scav = ctx
.item
.total_skills
.get(&SkillType::Scavenge)
.unwrap_or(&0.0)
.clone();
let found_opt = ctx
.trans
.find_scavhidden_item_by_location(
&ctx.item.location,
&Scavtype::Scavenge,
(scav * 100.0).max(0.0) as i64,
)
.await?;
let mut found = match found_opt {
None => user_error("You have a look, and you're pretty sure there's nothing to find here. [Try searching elsewhere, or come back later]".to_owned())?,
Some(v) => v,
};
let diff: f64 = (match found.action_type {
LocationActionType::Scavhidden { difficulty, .. } => difficulty,
_ => 800,
}) as f64
/ 100.0;
if skill_check_and_grind(&ctx.trans, &mut ctx.item, &SkillType::Scavenge, diff).await? < 0.0
{
// No crit fail for now, it is either yes or no.
match ctx.get_session().await? {
None => {},
Some((sess, _)) =>
ctx.trans.queue_for_session(&sess, Some("You give up searching, but you still have a feeling there is something here.\n")).await?
}
return Ok(());
}
found.action_type = LocationActionType::Normal;
match check_item_capacity(&ctx.trans, &ctx.item, found.weight).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
consider_expire_job_for_item(&ctx.trans, &found).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!("{} seems to have found {} - but can't carry it so drops it on the ground.\n",
ctx.item.display_for_sentence(1, true),
found.display_for_sentence(1, false),
),
).await?;
}
_ => {
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} seems to have found {}.\n",
ctx.item.display_for_sentence(1, true),
found.display_for_sentence(1, false),
),
)
.await?;
found.location = ctx.item.refstr();
}
}
ctx.trans.save_item_model(&found).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
queue_command_and_save(ctx, &player_item, &QueueCommand::Scavenge).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,102 +0,0 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::{
language,
models::{
item::{SkillType, StatType},
user::{wristpad_hack_data, xp_to_hack_slots},
},
};
use ansi::ansi;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?,
Some(user) => user,
};
let mut msg = String::new();
msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"),
"Stat", "Raw", "Total"
));
for st in StatType::values().iter() {
msg.push_str(&format!(
ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"),
st.display(),
user.raw_stats.get(st).unwrap_or(&0.0),
player_item.total_stats.get(st).unwrap_or(&0.0)
));
}
msg.push_str("\n");
msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"),
"Skill", "Raw", "Total"
));
for st in SkillType::values().iter() {
msg.push_str(&format!(
ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"),
st.display(),
user.raw_skills.get(st).unwrap_or(&0.0),
player_item.total_skills.get(st).unwrap_or(&0.0)
));
}
msg.push_str("\n");
msg.push_str(&format!(
ansi!(
"Experience: <bold>{}<reset> total, \
change of {} this re-roll, \
{} spent since re-roll.\n"
),
player_item.total_xp,
user.experience.xp_change_for_this_reroll,
user.experience.spent_xp
));
if !user.wristpad_hacks.is_empty() {
let hack_names = user
.wristpad_hacks
.iter()
.map(|h| {
wristpad_hack_data()
.get(h)
.map(|hd| hd.name)
.unwrap_or("UNKNOWN")
})
.collect::<Vec<&'static str>>();
msg.push_str(&format!(
"You have hacks installed on your wristpad: {}\n",
&language::join_words(&hack_names)
));
}
let hack_slots = xp_to_hack_slots(player_item.total_xp) as usize;
if hack_slots > user.wristpad_hacks.len() {
let free_slots = hack_slots - user.wristpad_hacks.len();
msg.push_str(&format!(
"You have {} free hack slot{} on your wristpad.\n",
free_slots,
if free_slots == 1 { "" } else { "s" }
));
} else {
msg.push_str("You have no free hack slots on your wristpad.\n");
};
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,135 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
db::ItemSearchParams,
language::pluralise,
models::item::Item,
services::combat::max_health,
static_content::{
possession_type::possession_data,
room::{self, Room},
},
};
use async_trait::async_trait;
async fn check_sell_trigger(
ctx: &mut VerbContext<'_>,
player_item: &Item,
room: &Room,
sell_item: &Item,
) -> UResult<()> {
let trigger = match room.sell_trigger.as_ref() {
None => return Ok(()),
Some(tr) => tr,
};
trigger.handle_sell(ctx, room, player_item, sell_item).await
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"Nobody seems to listen when you try to sell... possibly because you're dead."
.to_owned(),
)?
}
let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
user_error("Can't sell anything because you're not in a shop.".to_owned())?;
}
let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r,
};
if room.stock_list.is_empty() {
user_error("Can't sell anything because you're not in a shop.".to_owned())?
}
let sell_item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
..ItemSearchParams::base(&player_item, remaining)
},
)
.await?;
if let Some(charge_data) = sell_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.charge_data.as_ref())
{
if charge_data.max_charges > sell_item.charges {
user_error(format!(
"No one will want to buy a used {}!",
&sell_item.display
))?
}
}
if sell_item.health < max_health(&sell_item) {
user_error(format!(
"No one will want to buy a damaged {}!",
&sell_item.display
))?
}
for stock in &room.stock_list {
if Some(stock.possession_type.clone()) != sell_item.possession_type {
continue;
}
let stats = ctx.trans.get_location_stats(&sell_item.refstr()).await?;
if stats.total_count > 0 {
user_error("Shouldn't you empty it first?".to_owned())?;
}
let sell_discount = match stock.can_sell {
None => continue,
Some(d) => d,
};
if let Some(user) = ctx.user_dat.as_mut() {
let sell_price =
((stock.list_price as f64) * (sell_discount as f64 / 10000.0)) as u64;
user.credits += stock.list_price;
ctx.trans
.delete_item(&sell_item.item_type, &sell_item.item_code)
.await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"Your wristpad beeps for a credit of {} credits.\n",
sell_price
)),
)
.await?;
check_sell_trigger(ctx, &player_item, &room, &sell_item).await?;
return Ok(());
}
}
user_error(format!(
"Sorry, this store doesn't buy {}!",
pluralise(&sell_item.display)
))?
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,112 +0,0 @@
use crate::{
db::ItemSearchParams,
models::item::{ConversationIntensity, ConversationalStyle},
services::sharing::{
change_conversation_intensity, change_conversation_topic, change_conversational_style,
display_conversation_status, parse_conversation_topic, start_conversation,
},
};
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use ansi::ansi;
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<()> {
let mut player_item = (*(get_player_item_or_fail(ctx).await?)).clone();
if player_item.death_data.is_some() {
user_error("You can't do that, you're dead!".to_string())?;
}
let remaining = remaining.trim();
if let Some(ac) = player_item.active_conversation.as_ref() {
match ConversationalStyle::from_name(verb) {
None => {}
Some(style) => {
change_conversational_style(ctx, &mut player_item, style).await?;
ctx.trans.save_item_model(&player_item).await?;
return Ok(());
}
}
match parse_conversation_topic(&format!("{} {}", verb, remaining))
.map_err(|e| UserError(e.to_owned()))?
{
None => {}
Some(topic) => {
change_conversation_topic(ctx, &mut player_item, topic).await?;
ctx.trans.save_item_model(&player_item).await?;
return Ok(());
}
}
if verb == "share" {
match ConversationIntensity::from_adverb(remaining) {
None => {}
Some(intensity) => {
change_conversation_intensity(ctx, &mut player_item, intensity).await?;
ctx.trans.save_item_model(&player_item).await?;
return Ok(());
}
}
if remaining == "status" {
let (partner_type, partner_code) = ac
.partner_ref
.split_once("/")
.ok_or_else(|| UserError("Bad share partner".to_owned()))?;
if let Some(partner) = ctx
.trans
.find_item_by_type_code(partner_type, partner_code)
.await?
{
display_conversation_status(&ctx.trans, &player_item, &partner).await?;
return Ok(());
}
}
}
user_error("You're already sharing knowledge!".to_owned())?;
}
let (word2, remaining) = remaining.split_once(" ").ok_or_else(|| {
UserError(
ansi!("Start your encounter with the <bold>share knowledge with<reset> command first.")
.to_owned(),
)
})?;
let (word3, remaining) = remaining.trim().split_once(" ").ok_or_else(|| {
UserError(ansi!("<bold>share knowledge with<reset> command first.").to_owned())
})?;
if verb != "share" || word2.trim() != "knowledge" || word3.trim() != "with" {
user_error(
ansi!("Start your encounter with the <bold>share knowledge with<reset> command first.")
.to_owned(),
)?;
}
let with_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, remaining)
},
)
.await?;
start_conversation(&ctx.trans, &player_item, &with_whom).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,49 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
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<()> {
let item_name = remaining.trim();
if item_name == "" {
user_error("Sign what? Try: sign something".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(&player_item, item_name)
},
)
.await?;
if item.item_type != "possession" {
user_error("You can't sign that!".to_owned())?;
}
let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.sign_handler)
{
None => user_error("You can't sign that!".to_owned())?,
Some(h) => h,
};
handler.cmd(ctx, &player_item, &item).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,198 +0,0 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::item::LocationActionType,
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{comms::broadcast_to_room, urges::recalculate_urge_growth},
};
use async_trait::async_trait;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error("You try to sit, but it turns out the dead can't even do that!".to_owned())?;
}
let item_ref = match ctx.command {
QueueCommand::Sit { item } => item,
_ => user_error("Unexpected command".to_owned())?,
};
if ctx
.item
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.is_some()
{
user_error(
"Sit... while fighting? You can't figure out how to make it work".to_owned(),
)?
}
if ctx.item.active_climb.is_some() {
user_error(
"Sit... while climbing? You can't figure out how to make it work".to_owned(),
)?
}
match ctx.item.action_type {
LocationActionType::Sitting { .. } => user_error("You're already sitting.".to_owned())?,
_ => {}
}
match item_ref {
None => {}
Some(item_ref) => {
let (item_type, item_code) = match item_ref.split_once("/") {
None => user_error("Invalid item ref in Sit command".to_owned())?,
Some(v) => v,
};
match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(item) => {
if item.location != ctx.item.location {
user_error(format!(
"You try to sit on {} but realise it's no longer here",
item.display_for_sentence(1, false)
))?
}
}
}
}
};
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error("You try to sit, but it turns out the dead can't even do that!".to_owned())?;
}
let item_ref = match ctx.command {
QueueCommand::Sit { item } => item,
_ => user_error("Unexpected command".to_owned())?,
};
if ctx
.item
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.is_some()
{
user_error(
"Sit... while fighting? You can't figure out how to make it work".to_owned(),
)?
}
if ctx.item.active_climb.is_some() {
user_error(
"Sit... while climbing? You can't figure out how to make it work".to_owned(),
)?
}
match ctx.item.action_type {
LocationActionType::Sitting { .. } => user_error("You're already sitting.".to_owned())?,
_ => {}
}
let (item, desc) = match item_ref {
None => (None, "the floor".to_owned()),
Some(item_ref) => {
let (item_type, item_code) = match item_ref.split_once("/") {
None => user_error("Invalid item ref in Sit command".to_owned())?,
Some(v) => v,
};
match ctx
.trans
.find_item_by_type_code(&item_type, &item_code)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(item) => {
if item.location != ctx.item.location {
user_error(format!(
"You try to sit on {} but realise it's no longer here",
item.display_for_sentence(1, false)
))?
}
(Some(item.clone()), item.display_for_sentence(1, false))
}
}
}
};
let msg = format!(
"{} sits on {}.\n",
&ctx.item.display_for_sentence(1, true),
&desc
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
ctx.item.action_type = LocationActionType::Sitting(item.map(|it| it.refstr()));
recalculate_urge_growth(&ctx.trans, &mut ctx.item).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if remaining.starts_with("on ") {
remaining = remaining[3..].trim();
}
if remaining == "" {
remaining = "floor";
}
let target = if remaining == "here" || remaining == "ground" || remaining == "floor" {
None
} else {
Some(
search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?,
)
};
if player_item.death_data.is_some() {
user_error("You try to sit, but it turns out the dead can't even do that!".to_owned())?;
}
if let Some(target) = target.as_ref() {
if target
.static_data()
.and_then(|pd| pd.sit_data.as_ref())
.is_none()
{
user_error("You can't sit on that!".to_owned())?;
}
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Sit {
item: target.map(|t| t.refstr()),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,52 +0,0 @@
use super::{
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::static_content::room;
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<()> {
let requester = get_player_item_or_fail(ctx).await?;
let mut resp: String = String::new();
if remaining == "loc" {
let (loc_type, loc_code) = requester
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location string".to_owned()))?;
resp.push_str(&format!("Location code: {}\n", loc_code));
if loc_type == "room" {
resp.push_str("You're in an ordinary room.\n");
let room = room::room_map_by_code().get(loc_code).ok_or_else(|| {
UserError("Can't find your current room in static data".to_owned())
})?;
resp.push_str(&format!(
"Grid coords: x={}, y={}, z={}. Coordinates get lower as you go \
north, west, down.\n",
room.grid_coords.x, room.grid_coords.y, room.grid_coords.z
));
} else if loc_type == "dynroom" {
resp.push_str("You're in a dynamic room.\n");
} else {
resp.push_str(&format!(
"Weird - you're in a location of type {}.\n",
loc_type
));
}
} else {
user_error("Unknown subcommand. Try loc.".to_owned())?;
}
ctx.trans
.queue_for_session(&ctx.session, Some(&resp))
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,90 +0,0 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
#[double]
use crate::db::DBTrans;
use crate::{
models::item::{Item, LocationActionType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{comms::broadcast_to_room, urges::recalculate_urge_growth},
};
use async_trait::async_trait;
use mockall_double::double;
use std::time;
pub async fn stand_if_needed(trans: &DBTrans, who: &mut Item) -> UResult<()> {
match who.action_type {
LocationActionType::Sitting { .. } | LocationActionType::Reclining { .. } => {}
_ => return Ok(()),
}
let msg = format!("{} stands up.\n", &who.display_for_sentence(1, true));
broadcast_to_room(trans, &who.location, None, &msg).await?;
who.action_type = LocationActionType::Normal;
recalculate_urge_growth(trans, who).await?;
Ok(())
}
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to stand, but it turns out the dead can't even do that!".to_owned(),
)?;
}
match ctx.command {
QueueCommand::Stand => {}
_ => user_error("Unexpected command".to_owned())?,
};
match ctx.item.action_type {
LocationActionType::Sitting { .. } | LocationActionType::Reclining { .. } => {}
_ => user_error("You're already standing.".to_owned())?,
}
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to stand, but it turns out the dead can't even do that!".to_owned(),
)?;
}
match ctx.command {
QueueCommand::Stand => {}
_ => user_error("Unexpected command".to_owned())?,
};
match ctx.item.action_type {
LocationActionType::Sitting { .. } | LocationActionType::Reclining { .. } => {}
_ => user_error("You're already standing.".to_owned())?,
}
stand_if_needed(&ctx.trans, &mut ctx.item).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"You try to stand, but it turns out the dead can't even do that!".to_owned(),
)?;
}
queue_command_and_save(ctx, &player_item, &QueueCommand::Stand).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,81 +0,0 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::{
models::item::Urges,
services::{combat::max_health, display::bar_n_of_m},
};
use ansi::ansi;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?,
Some(user) => user,
};
let mut msg = String::new();
let maxh = max_health(&player_item);
msg.push_str(&format!(
ansi!("<bold>Health [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m(player_item.health, maxh),
player_item.health,
maxh
));
let (hunger, thirst, stress) = match player_item.urges.as_ref() {
None => (0, 0, 0),
Some(Urges {
hunger,
thirst,
stress,
}) => (hunger.value, thirst.value, stress.value),
};
msg.push_str(&format!(
ansi!("<bold>Hunger [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((hunger / 200) as u64, 50),
hunger / 100,
100
));
msg.push_str(&format!(
ansi!("<bold>Thirst [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((thirst / 200) as u64, 50),
thirst / 100,
100
));
msg.push_str(&format!(
ansi!("<bold>Stress [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m((stress / 200) as u64, 50),
stress / 100,
100
));
msg.push_str(&format!(
ansi!("<bold>Credits <green>${}<reset>\n"),
user.credits
));
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
#[cfg(test)]
mod test {
#[test]
fn bar_n_of_m_works() {
assert_eq!(super::bar_n_of_m(4, 7), "|||| ");
assert_eq!(super::bar_n_of_m(8, 7), "|||||||");
assert_eq!(super::bar_n_of_m(8, 8), "||||||||");
assert_eq!(super::bar_n_of_m(0, 5), " ");
}
}

View File

@ -1,75 +0,0 @@
use super::{
get_player_item_or_fail, movement::reverse_climb, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::services::sharing::stop_conversation_mut;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("You're dead. If you wanted to just stop being dead, I'm afraid it doesn't work like that (although you can try going up to resurrect into a new body). If you wanted to stop doing whatever you were doing when you died, then the good news is that your death already put a stop to it.".to_owned())?;
}
let mut player_item_mut = (*player_item).clone();
let mut queue_head = player_item_mut.queue.pop_front();
if player_item_mut.active_conversation.is_some() {
stop_conversation_mut(
&ctx.trans,
&mut player_item_mut,
&format!(
"holds up {} hand to stop the conversation with",
&player_item.pronouns.possessive
),
)
.await?;
ctx.trans.save_item_model(&player_item_mut).await?;
return Ok(());
}
if player_item.active_combat.is_some() {
// Otherwise, we assume they wanted to stop escaping etc...
if queue_head.is_none() {
user_error("You can't just stop fighting - either fight to the death or try moving to another room.".to_owned())?;
}
}
let mut msg = String::new();
if let Some(_active_climb) = player_item_mut.active_climb.as_mut() {
if let Some(ref mut queue_head_v) = queue_head {
msg.push_str(&reverse_climb(&mut player_item_mut, &ctx.trans, queue_head_v).await?);
}
}
let qlen = player_item_mut.queue.len();
if qlen > 0 {
msg.push_str(&format!(
"You cancel your plans to take {} action{}.\n",
qlen,
if qlen > 1 { "s" } else { "" },
));
} else if msg == "" {
user_error("You weren't doing anything that could be stopped.".to_owned())?;
}
player_item_mut.queue = queue_head.clone().into_iter().collect();
ctx.trans.save_item_model(&player_item_mut).await?;
ctx.trans
.queue_for_session(&ctx.session, Some(&msg))
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,73 +0,0 @@
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use ansi::ansi;
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<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("You can't figure out to turn things on/off whilst dead.".to_owned())?;
}
let (state, item_name) = remaining.split_once(" ").ok_or_else(|| {
UserError(
ansi!("Try <bold>turn on something<reset> or <bold>turn off something<reset>.")
.to_owned(),
)
})?;
let state = state.trim().to_lowercase();
let to_state = if state == "on" {
true
} else if state == "off" {
false
} else {
user_error(
ansi!("Try <bold>turn on something<reset> or <bold>turn off something<reset>.")
.to_owned(),
)?
};
let item_name = item_name.trim();
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, item_name)
},
)
.await?;
let handler = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.turn_toggle_handler)
.ok_or_else(|| {
UserError(format!(
"You can't turn that {}!",
if to_state { "on" } else { "off" }
))
})?;
handler.turn_cmd(ctx, &player_item, &item, to_state).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,95 +0,0 @@
use super::{
get_player_item_or_fail, search_items_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
models::item::ItemFlag,
static_content::{possession_type::possession_data, room::Direction},
};
use ansi::ansi;
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<()> {
let (uninstall_what_raw, what_dir_raw) = match remaining.rsplit_once(" from door to ") {
None => user_error(ansi!("Uninstall from where? Try <bold>uninstall<reset> <lt>lock> <bold>from door to<reset> <lt>direction>").to_owned())?,
Some(v) => v
};
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"Apparently, you have to be alive to work as an uninstaller. \
So discriminatory!"
.to_owned(),
)?;
}
let (loc_t, loc_c) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location".to_owned()))?;
let loc_item = ctx
.trans
.find_item_by_type_code(loc_t, loc_c)
.await?
.ok_or_else(|| UserError("Can't find your location".to_owned()))?;
if loc_item.owner.as_ref() != Some(&player_item.refstr())
|| !loc_item.flags.contains(&ItemFlag::PrivatePlace)
{
user_error(
"You can only uninstall things while standing in a private room you own. \
If you are outside, try uninstalling from the inside."
.to_owned(),
)?;
}
let dir = Direction::parse(what_dir_raw.trim())
.ok_or_else(|| UserError("Invalid direction.".to_owned()))?;
let cand_items = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, uninstall_what_raw.trim())
},
)
.await?;
let item = cand_items
.iter()
.find(|it| it.action_type.is_in_direction(&dir))
.ok_or_else(|| {
UserError(
"Sorry, I couldn't find anything matching installed on that door.".to_owned(),
)
})?;
if item.item_type != "possession" {
user_error("You can't uninstall that!".to_owned())?;
}
let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.install_handler)
{
None => user_error("You can't uninstall that!".to_owned())?,
Some(h) => h,
};
handler
.uninstall_cmd(ctx, &player_item, &item, &loc_item, &dir)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,357 +0,0 @@
use super::{
get_player_item_or_fail, parsing, search_item_for_user, user_error, ItemSearchParams, UResult,
UserVerb, UserVerbRef, VerbContext,
};
use crate::{
language,
models::item::SkillType,
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
check_consent, comms::broadcast_to_room, effect::run_effects, skills::skill_check_and_grind,
},
static_content::possession_type::possession_data,
};
use async_trait::async_trait;
use std::sync::Arc;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to use it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (item_id, target_type_code) = match ctx.command {
QueueCommand::Use {
possession_id,
target_id,
} => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("player/{}", ctx.item.item_code) {
user_error(format!(
"You try to use {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
let (target_type, target_code) = match target_type_code.split_once("/") {
None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
Some(spl) => spl,
};
let is_self_use = target_type == "player" && target_code == ctx.item.item_code;
let target = if is_self_use {
Arc::new(ctx.item.clone())
} else {
match ctx
.trans
.find_item_by_type_code(&target_type, &target_code)
.await?
{
None => user_error(format!(
"Couldn't handle use command (target {} missing)",
target_type_code
))?,
Some(it) => it,
}
};
if !is_self_use
&& target.location != ctx.item.location
&& target.location != format!("player/{}", ctx.item.item_code)
{
let target_name = target.display_for_sentence(1, false);
user_error(format!(
"You try to use {} on {}, but realise {} is no longer here",
item.display_for_sentence(1, false),
target_name,
target_name
))?
}
let msg = format!(
"{} prepares to use {} {} on {}\n",
&ctx.item.display_for_sentence(1, true),
&ctx.item.pronouns.possessive,
&item.display_for_sentence(0, false),
&if is_self_use {
ctx.item.pronouns.intensive.clone()
} else {
target.display_for_sentence(1, false)
}
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
let mut draw_level: f64 = *ctx
.item
.total_skills
.get(&SkillType::Quickdraw)
.to_owned()
.unwrap_or(&8.0);
let skill_result =
skill_check_and_grind(ctx.trans, ctx.item, &SkillType::Quickdraw, draw_level).await?;
if skill_result < -0.5 {
draw_level -= 2.0;
} else if skill_result < -0.25 {
draw_level -= 1.0;
} else if skill_result > 0.5 {
draw_level += 2.0;
} else if skill_result > 0.25 {
draw_level += 1.0;
}
let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0);
Ok(time::Duration::from_millis(
(wait_ticks * 500.0).round() as u64
))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to use it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (ref item_id, ref target_type_code) = match ctx.command {
QueueCommand::Use {
possession_id,
target_id,
} => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != format!("player/{}", ctx.item.item_code) {
user_error(format!(
"You try to use {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
let (ref target_type, ref target_code) = match target_type_code.split_once("/") {
None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
Some(ref sp) => sp.clone(),
};
let target = match ctx
.trans
.find_item_by_type_code(&target_type, &target_code)
.await?
{
None => user_error("Couldn't handle use command (target missing)".to_owned())?,
Some(it) => it,
};
if target.location != ctx.item.location
&& target.location != format!("player/{}", ctx.item.item_code)
{
let target_name = target.display_for_sentence(1, false);
user_error(format!(
"You try to use {} on {}, but realise {} is no longer here",
item.display_for_sentence(1, false),
target_name,
target_name
))?
}
let use_data = match item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.use_data.as_ref())
{
None => user_error("You can't use that!".to_owned())?,
Some(d) => d,
};
if let Some(consent_type) = use_data.needs_consent_check.as_ref() {
if !check_consent(ctx.trans, "use", consent_type, &ctx.item, &target).await? {
user_error(format!(
"{} doesn't allow {} from you",
&target.display_for_sentence(1, true),
consent_type.to_str()
))?
}
}
if let Some(charge_data) = item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.charge_data.as_ref())
{
if item.charges < 1 {
user_error(format!(
"{} has no {} {} left",
item.display_for_sentence(1, true),
&language::pluralise(charge_data.charge_name_prefix),
charge_data.charge_name_suffix
))?;
}
}
if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?;
}
let is_self_use = target_type == &"player" && target_code == &ctx.item.item_code;
let skillcheck = skill_check_and_grind(
&ctx.trans,
ctx.item,
&use_data.uses_skill,
use_data.diff_level,
)
.await?;
let (effects, skilllvl) = if skillcheck <= -0.5 {
// 0-1 how bad was the crit fail?
(&use_data.crit_fail_effects, (-0.5 - skillcheck) * 2.0)
} else if skillcheck < 0.0 {
(&use_data.fail_effects, -skillcheck * 2.0)
} else {
(&use_data.success_effects, skillcheck)
};
let mut target_mut = if is_self_use {
None
} else {
Some((*target).clone())
};
if let Some(actual_effects) = effects {
run_effects(
ctx.trans,
&actual_effects,
ctx.item,
&item,
target_mut.as_mut(),
skilllvl,
)
.await?;
}
if let Some(target_mut_save) = target_mut {
ctx.trans.save_item_model(&target_mut_save).await?;
}
let mut item_mut = (*item).clone();
let mut save_item = false;
if item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.charge_data.as_ref())
.is_some()
{
item_mut.charges -= 1;
save_item = true;
}
if item_mut.charges == 0 {
if let Some((new_poss, new_poss_dat)) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|poss_data| poss_data.becomes_on_spent.as_ref())
.and_then(|poss_type| {
possession_data()
.get(&poss_type)
.map(|poss_dat| (poss_type, poss_dat))
})
{
item_mut.possession_type = Some(new_poss.clone());
item_mut.display = new_poss_dat.display.to_owned();
item_mut.details = Some(new_poss_dat.details.to_owned());
item_mut.aliases = new_poss_dat
.aliases
.iter()
.map(|al| (*al).to_owned())
.collect();
item_mut.health = new_poss_dat.max_health;
item_mut.weight = new_poss_dat.weight;
save_item = true;
}
}
if save_item {
ctx.trans.save_item_model(&item_mut).await?;
}
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error(
"You try to use it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let (what_name, whom_name) = parsing::parse_on_or_default(remaining, "me");
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: 1,
..ItemSearchParams::base(&player_item, &what_name)
},
)
.await?;
let target = if whom_name == "me" || whom_name == "self" {
player_item.clone()
} else {
search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &whom_name)
},
)
.await?
};
let use_data = match item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.use_data.as_ref())
{
None => user_error("You can't use that!".to_owned())?,
Some(d) => d,
};
if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?;
}
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Use {
possession_id: item.item_code.clone(),
target_id: format!("{}/{}", target.item_type, target.item_code),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,145 +0,0 @@
use super::{
corp::check_corp_perm, get_player_item_or_fail, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
models::{
corp::{CorpCommType, CorpPermission},
item::{Item, ItemSpecialData},
},
static_content::room::{room_map_by_code, Direction, RentSuiteType},
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::{TimeDelta, Utc};
use itertools::Itertools;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let (item_name, corp_name) = match remaining.split_once(" for ") {
None => (remaining, None),
Some((i, c)) => (i.trim(), Some(c.trim())),
};
let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" {
user_error(
"You must go to where you rented the place (e.g. reception) to vacate.".to_owned(),
)?;
}
let room = room_map_by_code()
.get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() {
user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?;
}
let rentinfo = match room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
{
None => user_error(format!(
"Vacate must be followed by the specific thing you want to vacate: {}",
room.rentable_dynzone
.iter()
.map(|ri| ri.rent_what.as_str())
.join(", ")
))?,
Some(v) => v,
};
let corp = match (&rentinfo.suite_type, corp_name) {
(RentSuiteType::Commercial, None) =>
user_error(format!(
ansi!("This is a commercial suite, you need to vacate it using the name of the corp. Try <bold>vacate {} for corpname<reset>"),
item_name
))?,
(RentSuiteType::Residential, Some(_)) =>
user_error("This is a residential suite, you can't vacate it for a corp. Try <bold>vacate {}<reset>".to_owned())?,
(RentSuiteType::Residential, None) => None,
(RentSuiteType::Commercial, Some(n)) => match ctx.trans.match_user_corp_by_name(&n, &player_item.item_code).await? {
None => user_error("I can't find that corp in your list of corps!".to_owned())?,
Some((_, _, mem)) if !check_corp_perm(&CorpPermission::Holder, &mem) => user_error("You don't have holder permissions in that corp.".to_owned())?,
Some((corp_id, corp, _)) => Some((corp_id, corp))
},
};
let exit = Direction::IN {
item: corp
.as_ref()
.map(|c| c.1.name.clone())
.unwrap_or_else(|| player_item.display.clone()),
};
match ctx
.trans
.find_exact_dyn_exit(&player_item.location, &exit)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => user_error("You aren't renting anything from here!".to_owned())?,
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) = ctx
.trans
.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_),
..
}) => user_error("Your lease is already up for termination.".to_owned())?,
Some(ItemSpecialData::DynzoneData {
vacate_after: None,
zone_exit: ref ex,
}) => {
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData {
zone_exit: ex.clone(),
vacate_after: Some(
Utc::now() + TimeDelta::try_days(1).unwrap(),
),
}),
..(*ex_zone).clone()
})
.await?;
ctx.trans.queue_for_session(ctx.session, Some("The robot files away your notice of intention to vacate. \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"\n")).await?;
match corp {
None => {},
Some(corptup) =>
ctx.trans.broadcast_to_corp(
&corptup.0,
&CorpCommType::Notice,
Some(&player_item.item_code),
&format!(
ansi!("<cyan>[{}] {} just gave notice to vacate {}! The landlord replied with: \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"<reset>\n"),
&corptup.1.name,
&player_item.display_for_sentence(1, false),
&ex_zone.display
)
).await?
}
}
_ => user_error("The premises seem to be broken anyway".to_owned())?,
}
}
}
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,237 +0,0 @@
use super::{
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::item::{Buff, BuffCause, BuffImpact, LocationActionType, SkillType},
regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user},
static_content::possession_type::possession_data,
};
use async_trait::async_trait;
use chrono::Utc;
use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You try to wear it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Wear { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.refstr() {
user_error(format!(
"You try to wear {} but realise you no longer have it",
item.display_for_sentence(1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error("You realise you're already wearing it!".to_owned())?;
}
let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| {
UserError("That item no longer exists in the game so can't be handled".to_owned())
})?;
poss_data
.wear_data
.as_ref()
.ok_or_else(|| UserError("You can't wear that!".to_owned()))?;
let msg = format!(
"{} fumbles around trying to put on {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You try to wear it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
}
let item_id = match ctx.command {
QueueCommand::Wear { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())?,
};
let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?,
Some(it) => it,
};
if item.location != ctx.item.refstr() {
user_error(format!(
"You try to wear {} but realise it is no longer there.",
&item.display_for_sentence(1, false)
))?
}
if item.action_type == LocationActionType::Worn {
user_error("You realise you're already wearing it!".to_owned())?;
}
let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| {
UserError("That item no longer exists in the game so can't be handled".to_owned())
})?;
let wear_data = poss_data
.wear_data
.as_ref()
.ok_or_else(|| UserError("You can't wear that!".to_owned()))?;
let other_clothes = ctx
.trans
.find_by_action_and_location(&ctx.item.refstr(), &LocationActionType::Worn)
.await?;
for part in &wear_data.covers_parts {
let thickness: f64 =
other_clothes
.iter()
.fold(wear_data.thickness, |tot, other_item| {
match other_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref())
{
Some(wd) if wd.covers_parts.contains(&part) => tot + wd.thickness,
_ => tot,
}
});
if thickness > 12.0 {
user_error(format!(
"You're wearing too much on your {} already.",
part.display(ctx.item.sex.clone())
))?;
}
}
let msg = format!(
"{} wears {}\n",
&ctx.item.display_for_sentence(1, true),
&item.display_for_sentence(1, false)
);
broadcast_to_room(ctx.trans, &ctx.item.location, None, &msg).await?;
let mut item_mut = (*item).clone();
item_mut.action_type = LocationActionType::Worn;
item_mut.action_type_started = Some(Utc::now());
if wear_data.dodge_penalty != 0.0 {
ctx.item.temporary_buffs.push(Buff {
description: "Dodge penalty".to_owned(),
code: "dodge".to_owned(),
cause: BuffCause::ByItem {
item_type: item_mut.item_type.clone(),
item_code: item_mut.item_code.clone(),
},
impacts: vec![BuffImpact::ChangeSkill {
skill: SkillType::Dodge,
magnitude: -wear_data.dodge_penalty,
}],
});
if ctx.item.item_type == "player" {
if let Some(usr) = ctx.trans.find_by_username(&ctx.item.item_code).await? {
calculate_total_stats_skills_for_user(ctx.item, &usr);
}
}
}
ctx.trans.save_item_model(&item_mut).await?;
Ok(())
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
remaining = remaining[3..].trim();
get_limit = None;
} else if let (Some(n), remaining2) = parse_count(remaining) {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
item_action_type_only: Some(&LocationActionType::Normal),
..ItemSearchParams::base(&player_item, &remaining)
},
)
.await?;
if player_item.death_data.is_some() {
user_error("The dead don't dress themselves".to_owned())?;
}
let mut did_anything: bool = false;
let mut player_item_mut = (*player_item).clone();
for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" {
user_error("You can't wear that!".to_owned())?;
}
did_anything = true;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Wear {
possession_id: target.item_code.clone(),
},
)
.await?;
}
if !did_anything {
user_error("I didn't find anything matching.".to_owned())?;
} else {
ctx.trans.save_item_model(&player_item_mut).await?;
}
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,60 +1,39 @@
use super::{
get_player_item_or_fail, is_likely_illegal, parsing::parse_to_space, search_item_for_user,
user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
ItemSearchParams, user_error,
get_player_item_or_fail, is_likely_explicit,
search_item_for_user,
parsing::parse_to_space};
use crate::static_content::npc::npc_by_code;
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (to_whom_name, say_what_raw) = parse_to_space(remaining);
let say_what = ignore_special_characters(say_what_raw);
if say_what == "" {
user_error("You need to provide a message to send.".to_owned())?;
}
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?;
}
let to_whom = search_item_for_user(
ctx,
&ItemSearchParams {
let to_whom = search_item_for_user(ctx, &ItemSearchParams {
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name)
},
)
.await?;
}).await?;
match to_whom.item_type.as_str() {
"npc" => {}
"player" => {}
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?,
"player" => {},
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
}
if is_likely_illegal(&say_what) {
user_error("Your message was rejected by the content filter".to_string())?;
}
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ctx.trans.queue_for_session(ctx.session, Some(&format!(
ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"),
player_item.display_for_sentence(1, false),
to_whom.display_for_sentence(1, false),
player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat),
say_what
)),
)
.await?;
))).await?;
if player_item == to_whom {
return Ok(());
@ -62,38 +41,32 @@ impl UserVerb for Verb {
match to_whom.item_type.as_str() {
"npc" => {
let npc = npc_by_code()
.get(to_whom.item_code.as_str())
let npc = npc_by_code().get(to_whom.item_code.as_str())
.map(Ok)
.unwrap_or_else(|| user_error("That NPC is no longer available".to_owned()))?;
if let Some(handler) = npc.message_handler {
handler
.handle(ctx, &player_item, &to_whom, &say_what)
.await?;
handler.handle(ctx, &player_item, &to_whom, &say_what).await?;
}
}
"player" => {
match ctx
.trans
.find_session_for_player(&to_whom.item_code)
.await?
{
match ctx.trans.find_session_for_player(&to_whom.item_code).await? {
None => user_error("That character is asleep.".to_string())?,
Some((other_session, _other_session_dets)) => {
ctx.trans
.queue_for_session(
&other_session,
Some(&format!(
Some((other_session, other_session_dets)) => {
if other_session_dets.less_explicit_mode && is_likely_explicit(&say_what) {
user_error("That player is on a client that doesn't allow explicit \
content, and your message looked explicit, so it wasn't sent."
.to_owned())?
} else {
ctx.trans.queue_for_session(&other_session, Some(&format!(
ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"),
player_item.display_for_sentence(1, false),
to_whom.display_for_sentence(1, false),
player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat),
say_what
)),
)
.await?;
))).await?;
}
}
}
},
_ => {}
}

View File

@ -1,41 +0,0 @@
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use chrono::Utc;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let mut msg = String::new();
msg.push_str(&format!(
ansi!("<bold><bgblue><white>| {:20} | {:20} | {:15} |<reset>\n"),
ansi!("Username"),
ansi!("Corp"),
ansi!("Idle")
));
for online in ctx.trans.get_online_info().await? {
if let Some(online_time) = online.time {
let diff = humantime::format_duration(std::time::Duration::from_secs(
(Utc::now() - online_time).num_seconds() as u64,
));
msg.push_str(&format!(
"| {:20} | {:20} | {:15} |\n",
&ignore_special_characters(&online.username),
&ignore_special_characters(&online.corp.unwrap_or("".to_string())),
&format!("{}", &diff)
));
}
}
msg.push_str("\n");
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

Some files were not shown because too many files have changed in this diff Show More