Add gear command.

This commit is contained in:
Condorra 2023-06-03 23:47:29 +10:00
parent cf0d2f740b
commit 228c5fbb9b
23 changed files with 984 additions and 584 deletions

1
.gitignore vendored
View File

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

View File

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

View File

@ -1,19 +1,19 @@
use proc_macro::TokenStream;
use syn::{parse_macro_input, Lit};
use quote::ToTokens;
use nom::{ use nom::{
branch::alt,
bytes::complete::{tag, take_till, take_till1},
combinator::eof, combinator::eof,
branch::alt, multi::fold_many0,
bytes::complete::{take_till, take_till1, tag},
sequence::{tuple, pair},
error::Error, error::Error,
Err, multi::fold_many0,
Parser sequence::{pair, tuple},
Err, Parser,
}; };
use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, Lit};
enum AnsiFrag<'l> { enum AnsiFrag<'l> {
Lit(&'l str), Lit(&'l str),
Special(&'l str) Special(&'l str),
} }
use AnsiFrag::Special; use AnsiFrag::Special;
@ -21,16 +21,19 @@ use AnsiFrag::Special;
pub fn ansi(input: TokenStream) -> TokenStream { pub fn ansi(input: TokenStream) -> TokenStream {
let raw = match parse_macro_input!(input as Lit) { let raw = match parse_macro_input!(input as Lit) {
Lit::Str(lit_str) => lit_str.value(), Lit::Str(lit_str) => lit_str.value(),
_ => panic!("Expected a string literal") _ => panic!("Expected a string literal"),
}; };
fn parser(i: &str) -> Result<String, Err<Error<&str>>> { fn parser(i: &str) -> Result<String, Err<Error<&str>>> {
pair(fold_many0( pair(
fold_many0(
alt(( alt((
take_till1(|c| c == '<').map(AnsiFrag::Lit), take_till1(|c| c == '<').map(AnsiFrag::Lit),
tuple((tag("<"), take_till(|c| c == '>'), tag(">"))).map(|t| AnsiFrag::Special(t.1)) tuple((tag("<"), take_till(|c| c == '>'), tag(">")))
.map(|t| AnsiFrag::Special(t.1)),
)), )),
|| "".to_owned(), || "".to_owned(),
|a, r| a + match r { |a, r| {
a + match r {
AnsiFrag::Lit(s) => &s, AnsiFrag::Lit(s) => &s,
Special(s) if s == "reset" => "\x1b[0m", Special(s) if s == "reset" => "\x1b[0m",
Special(s) if s == "bold" => "\x1b[1m", Special(s) if s == "bold" => "\x1b[1m",
@ -54,11 +57,17 @@ pub fn ansi(input: TokenStream) -> TokenStream {
Special(s) if s == "bgcyan" => "\x1b[46m", Special(s) if s == "bgcyan" => "\x1b[46m",
Special(s) if s == "bgwhite" => "\x1b[47m", Special(s) if s == "bgwhite" => "\x1b[47m",
Special(s) if s == "lt" => "<", Special(s) if s == "lt" => "<",
Special(r) => panic!("Unknown ansi type {}", r) Special(r) => panic!("Unknown ansi type {}", r),
} }
), eof)(i).map(|(_, (r, _))| r) },
),
eof,
)(i)
.map(|(_, (r, _))| r)
} }
TokenStream::from(parser(&raw) TokenStream::from(
.unwrap_or_else(|e| { panic!("Bad ansi literal: {}", e) }) parser(&raw)
.into_token_stream()) .unwrap_or_else(|e| panic!("Bad ansi literal: {}", e))
.into_token_stream(),
)
} }

View File

@ -2,8 +2,12 @@ use std::process::Command;
pub fn main() { pub fn main() {
let cmdout = Command::new("git") let cmdout = Command::new("git")
.arg("rev-parse").arg("HEAD") .arg("rev-parse")
.output().expect("git rev-parse HEAD failed"); .arg("HEAD")
println!("cargo:rustc-env=GIT_VERSION={}", .output()
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8")); .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

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

View File

@ -1,9 +1,9 @@
use std::fs;
use std::error::Error;
use serde::Deserialize;
use ring::signature;
use base64;
use crate::DResult; use crate::DResult;
use base64;
use ring::signature;
use serde::Deserialize;
use std::error::Error;
pub(crate) use std::fs;
#[derive(Deserialize)] #[derive(Deserialize)]
struct AV { struct AV {
@ -11,20 +11,20 @@ struct AV {
serial: u64, serial: u64,
cn: String, cn: String,
assertion: String, assertion: String,
sig: String sig: String,
} }
static KEY_BYTES: [u8; 65] = [ static KEY_BYTES: [u8; 65] = [
0x04, 0x4f, 0xa0, 0x8b, 0x32, 0xa7, 0x7f, 0xc1, 0x0a, 0xfc, 0x51, 0x95, 0x93, 0x57, 0x05, 0x04, 0x4f, 0xa0, 0x8b, 0x32, 0xa7, 0x7f, 0xc1, 0x0a, 0xfc, 0x51, 0x95, 0x93, 0x57, 0x05, 0xb3,
0xb3, 0x0f, 0xad, 0x16, 0x05, 0x3c, 0x7c, 0xfc, 0x02, 0xd2, 0x7a, 0x63, 0xff, 0xd3, 0x09, 0x0f, 0xad, 0x16, 0x05, 0x3c, 0x7c, 0xfc, 0x02, 0xd2, 0x7a, 0x63, 0xff, 0xd3, 0x09, 0xaa, 0x5b,
0xaa, 0x5b, 0x78, 0xfe, 0xa8, 0xc2, 0xc3, 0x02, 0xc2, 0xe6, 0xaf, 0x81, 0xc7, 0xa3, 0x03, 0x78, 0xfe, 0xa8, 0xc2, 0xc3, 0x02, 0xc2, 0xe6, 0xaf, 0x81, 0xc7, 0xa3, 0x03, 0xfa, 0x4d, 0xf1,
0xfa, 0x4d, 0xf1, 0xf9, 0xfc, 0x0a, 0x36, 0xef, 0x6b, 0x1e, 0x9d, 0xce, 0x6e, 0x60, 0xc6, 0xf9, 0xfc, 0x0a, 0x36, 0xef, 0x6b, 0x1e, 0x9d, 0xce, 0x6e, 0x60, 0xc6, 0xa8, 0xb3, 0x02, 0x35,
0xa8, 0xb3, 0x02, 0x35, 0x7e 0x7e,
]; ];
pub fn check() -> DResult<()> { pub fn check() -> DResult<()> {
let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?). 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>)?; .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." || 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" { av.assertion != "age>=18" {
Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?; Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?;

View File

@ -21,6 +21,7 @@ pub mod close;
pub mod corp; pub mod corp;
pub mod cut; pub mod cut;
pub mod drop; pub mod drop;
mod gear;
pub mod get; pub mod get;
mod describe; mod describe;
mod help; mod help;
@ -137,6 +138,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"corp" => corp::VERB, "corp" => corp::VERB,
"cut" => cut::VERB, "cut" => cut::VERB,
"drop" => drop::VERB, "drop" => drop::VERB,
"gear" => gear::VERB,
"get" => get::VERB, "get" => get::VERB,
"install" => install::VERB, "install" => install::VERB,
"inventory" => inventory::VERB, "inventory" => inventory::VERB,

View File

@ -0,0 +1,120 @@
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(if ctx.session_dat.less_explicit_mode {
item.display_less_explicit
.as_ref()
.map(|s| s.as_str())
.unwrap_or(&item.display)
} else {
&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,36 +1,35 @@
use serde::{Serialize, Deserialize};
use crate::{ use crate::{
models::item::{SkillType, Item, Pronouns},
models::consent::ConsentType,
message_handler::user_commands::{UResult, VerbContext}, message_handler::user_commands::{UResult, VerbContext},
static_content::{ models::consent::ConsentType,
room::Direction, models::item::{Item, Pronouns, SkillType},
species::BodyPart, static_content::{room::Direction, species::BodyPart},
},
}; };
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
use rand::seq::SliceRandom;
use async_trait::async_trait; use async_trait::async_trait;
use once_cell::sync::OnceCell;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
mod fangs;
mod whip;
mod blade; mod blade;
mod trauma_kit;
mod corp_licence; mod corp_licence;
mod lock; mod fangs;
mod meat;
pub mod head_armour; pub mod head_armour;
pub mod torso_armour; mod lock;
pub mod lower_armour; pub mod lower_armour;
mod meat;
pub mod torso_armour;
mod trauma_kit;
mod whip;
pub type AttackMessageChoice = Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>; pub type AttackMessageChoice =
pub type AttackMessageChoicePart = Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>; Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
pub type AttackMessageChoicePart =
Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
pub struct SkillScaling { pub struct SkillScaling {
pub skill: SkillType, pub skill: SkillType,
pub min_skill: f64, pub min_skill: f64,
pub mean_damage_per_point_over_min: f64 pub mean_damage_per_point_over_min: f64,
} }
#[allow(unused)] #[allow(unused)]
@ -40,7 +39,21 @@ pub enum DamageType {
Slash, Slash,
Pierce, Pierce,
Shock, Shock,
Bullet Bullet,
}
impl DamageType {
#[allow(unused)]
pub fn display(&self) -> &'static str {
use DamageType::*;
match self {
Beat => "beat",
Slash => "slash",
Pierce => "pierce",
Shock => "shock",
Bullet => "bullet",
}
}
} }
pub struct WeaponAttackData { pub struct WeaponAttackData {
@ -56,24 +69,27 @@ pub struct WeaponAttackData {
impl Default for WeaponAttackData { impl Default for WeaponAttackData {
fn default() -> Self { fn default() -> Self {
Self { Self {
start_messages: start_messages: vec![Box::new(|attacker, victim, exp| {
vec!(Box::new(|attacker, victim, exp| format!( format!(
"{} makes an attack on {}", "{} makes an attack on {}",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp,1, false)))), &victim.display_for_sentence(exp, 1, false)
success_messages: )
vec!(Box::new(|attacker, victim, part, exp| })],
format!("{}'s attack on {} hits {} {}", success_messages: vec![Box::new(|attacker, victim, part, exp| {
format!(
"{}'s attack on {} hits {} {}",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false), &victim.display_for_sentence(exp, 1, false),
&victim.pronouns.possessive, &victim.pronouns.possessive,
part.display(victim.sex.clone()) part.display(victim.sex.clone())
))), )
})],
mean_damage: 1.0, mean_damage: 1.0,
stdev_damage: 2.0, stdev_damage: 2.0,
base_damage_type: DamageType::Slash, base_damage_type: DamageType::Slash,
other_damage_types: vec!(), other_damage_types: vec![],
skill_scaling: vec!() skill_scaling: vec![],
} }
} }
} }
@ -114,10 +130,17 @@ impl Default for ChargeData {
pub enum UseEffect { pub enum UseEffect {
// messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message. // messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message.
BroadcastMessage { messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>}, BroadcastMessage {
messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>,
},
// skill_multiplier is always positive - sign flipped for crit fails. // skill_multiplier is always positive - sign flipped for crit fails.
ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64, ChangeTargetHealth {
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> }, delay_secs: u64,
base_effect: i64,
skill_multiplier: f64,
max_effect: i64,
message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send>,
},
} }
pub struct UseData { pub struct UseData {
@ -136,9 +159,9 @@ impl Default for UseData {
Self { Self {
uses_skill: SkillType::Medic, uses_skill: SkillType::Medic,
diff_level: 10.0, diff_level: 10.0,
crit_fail_effects: vec!(), crit_fail_effects: vec![],
fail_effects: vec!(), fail_effects: vec![],
success_effects: vec!(), success_effects: vec![],
errorf: Box::new(|_it, _target| None), errorf: Box::new(|_it, _target| None),
task_ref: "set me", task_ref: "set me",
needs_consent_check: None, needs_consent_check: None,
@ -161,7 +184,13 @@ pub struct WearData {
#[async_trait] #[async_trait]
pub trait WriteHandler { pub trait WriteHandler {
async fn write_cmd(&self, ctx: &mut VerbContext, player: &Item, on_what: &Item, write_what: &str) -> UResult<()>; async fn write_cmd(
&self,
ctx: &mut VerbContext,
player: &Item,
on_what: &Item,
write_what: &str,
) -> UResult<()>;
} }
#[async_trait] #[async_trait]
@ -171,8 +200,22 @@ pub trait ArglessHandler {
#[async_trait] #[async_trait]
pub trait InstallHandler { pub trait InstallHandler {
async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>; async fn install_cmd(
async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>; &self,
ctx: &mut VerbContext,
player: &Item,
what: &Item,
room: &Item,
direction: &Direction,
) -> UResult<()>;
async fn uninstall_cmd(
&self,
ctx: &mut VerbContext,
player: &Item,
what: &Item,
room: &Item,
direction: &Direction,
) -> UResult<()>;
} }
pub struct PossessionData { pub struct PossessionData {
@ -192,7 +235,7 @@ pub struct PossessionData {
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
pub can_butcher: bool, pub can_butcher: bool,
pub wear_data: Option<WearData> pub wear_data: Option<WearData>,
} }
impl Default for PossessionData { impl Default for PossessionData {
@ -203,7 +246,7 @@ impl Default for PossessionData {
display_less_explicit: None, display_less_explicit: None,
details: "A generic looking thing", details: "A generic looking thing",
details_less_explicit: None, details_less_explicit: None,
aliases: vec!(), aliases: vec![],
max_health: 10, max_health: 10,
weight: 100, weight: 100,
charge_data: None, charge_data: None,
@ -220,26 +263,27 @@ impl Default for PossessionData {
} }
impl WeaponAttackData { impl WeaponAttackData {
pub fn start_message( pub fn start_message(&self, attacker: &Item, victim: &Item, explicit_ok: bool) -> String {
&self,
attacker: &Item, victim: &Item, explicit_ok: bool) -> String {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
self.start_messages[..].choose(&mut rng).map( self.start_messages[..]
|f| f(attacker, victim, explicit_ok)).unwrap_or( .choose(&mut rng)
"No message defined yet".to_owned()) .map(|f| f(attacker, victim, explicit_ok))
.unwrap_or("No message defined yet".to_owned())
} }
pub fn success_message( pub fn success_message(
&self, attacker: &Item, victim: &Item, &self,
part: &BodyPart, explicit_ok: bool attacker: &Item,
victim: &Item,
part: &BodyPart,
explicit_ok: bool,
) -> String { ) -> String {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
self.success_messages[..].choose(&mut rng).map( self.success_messages[..]
|f| f(attacker, victim, part, explicit_ok)).unwrap_or( .choose(&mut rng)
"No message defined yet".to_owned()) .map(|f| f(attacker, victim, part, explicit_ok))
.unwrap_or("No message defined yet".to_owned())
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
@ -268,7 +312,6 @@ pub enum PossessionType {
Steak, Steak,
AnimalSkin, AnimalSkin,
SeveredHead, SeveredHead,
} }
impl Into<Item> for PossessionType { impl Into<Item> for PossessionType {
@ -281,15 +324,22 @@ impl Into<Item> for PossessionType {
display_less_explicit: possession_dat.display_less_explicit.map(|d| d.to_owned()), display_less_explicit: possession_dat.display_less_explicit.map(|d| d.to_owned()),
details: Some(possession_dat.details.to_owned()), details: Some(possession_dat.details.to_owned()),
details_less_explicit: possession_dat.details_less_explicit.map(|d| d.to_owned()), details_less_explicit: possession_dat.details_less_explicit.map(|d| d.to_owned()),
aliases: possession_dat.aliases.iter().map(|al| (*al).to_owned()).collect(), aliases: possession_dat
.aliases
.iter()
.map(|al| (*al).to_owned())
.collect(),
health: possession_dat.max_health, health: possession_dat.max_health,
weight: possession_dat.weight, weight: possession_dat.weight,
pronouns: Pronouns { pronouns: Pronouns {
is_proper: false, is_proper: false,
..Pronouns::default_inanimate() ..Pronouns::default_inanimate()
}, },
charges: possession_dat.charge_data.as_ref() charges: possession_dat
.map(|cd| cd.max_charges).unwrap_or(0), .charge_data
.as_ref()
.map(|cd| cd.max_charges)
.unwrap_or(0),
..Default::default() ..Default::default()
} }
} }
@ -297,53 +347,65 @@ impl Into<Item> for PossessionType {
pub fn fist() -> &'static WeaponData { pub fn fist() -> &'static WeaponData {
static FIST_WEAPON: OnceCell<WeaponData> = OnceCell::new(); static FIST_WEAPON: OnceCell<WeaponData> = OnceCell::new();
FIST_WEAPON.get_or_init(|| { FIST_WEAPON.get_or_init(|| WeaponData {
WeaponData {
uses_skill: SkillType::Fists, uses_skill: SkillType::Fists,
raw_min_to_learn: 0.0, raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0, raw_max_to_learn: 2.0,
normal_attack: WeaponAttackData { normal_attack: WeaponAttackData {
start_messages: vec!( start_messages: vec![Box::new(|attacker, victim, exp| {
Box::new(|attacker, victim, exp| format!(
format!("{} swings at {} with {} fists", "{} swings at {} with {} fists",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false), &victim.display_for_sentence(exp, 1, false),
&attacker.pronouns.possessive &attacker.pronouns.possessive
) )
) })],
), success_messages: vec![Box::new(|attacker, victim, part, exp| {
success_messages: vec!( format!(
Box::new(|attacker, victim, part, exp| "{}'s fists smash into {}'s {}",
format!("{}'s fists smash into {}'s {}",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false), &victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone()) &part.display(victim.sex.clone())
) )
) })],
),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}
}) })
} }
pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> { pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> {
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> = OnceCell::new(); static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> =
OnceCell::new();
use PossessionType::*; use PossessionType::*;
&POSSESSION_DATA.get_or_init(|| { &POSSESSION_DATA.get_or_init(|| {
vec!( vec![(Fangs, fangs::data())]
(Fangs, fangs::data()) .into_iter()
).into_iter()
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(corp_licence::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(
corp_licence::data()
.iter()
.map(|v| ((*v).0.clone(), &(*v).1)),
)
.chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(head_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(
.chain(torso_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) head_armour::data()
.chain(lower_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .iter()
.map(|v| ((*v).0.clone(), &(*v).1)),
)
.chain(
torso_armour::data()
.iter()
.map(|v| ((*v).0.clone(), &(*v).1)),
)
.chain(
lower_armour::data()
.iter()
.map(|v| ((*v).0.clone(), &(*v).1)),
)
.collect() .collect()
}) })
} }
@ -351,8 +413,15 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> { pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new(); static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
&RELEVANT.get_or_init(|| { &RELEVANT.get_or_init(|| {
possession_data().iter() possession_data()
.filter_map(|(pt, pd)| if pd.can_butcher { Some(pt.clone()) } else { None }) .iter()
.filter_map(|(pt, pd)| {
if pd.can_butcher {
Some(pt.clone())
} else {
None
}
})
.collect() .collect()
}) })
} }
@ -364,7 +433,12 @@ mod tests {
fn other_damage_types_add_to_less_than_one() { fn other_damage_types_add_to_less_than_one() {
for (_pt, pd) in possession_data().iter() { for (_pt, pd) in possession_data().iter() {
if let Some(weapon_data) = pd.weapon_data.as_ref() { if let Some(weapon_data) = pd.weapon_data.as_ref() {
let tot: f64 = weapon_data.normal_attack.other_damage_types.iter().map(|v|v.0).sum(); let tot: f64 = weapon_data
.normal_attack
.other_damage_types
.iter()
.map(|v| v.0)
.sum();
assert!(tot >= 0.0); assert!(tot >= 0.0);
assert!(tot < 1.0); assert!(tot < 1.0);
} }

View File

@ -1,10 +1,9 @@
use super::{PossessionData, WeaponData, WeaponAttackData, PossessionType}; use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::models::item::SkillType; use crate::models::item::SkillType;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(PossessionType::ButcherKnife, (PossessionType::ButcherKnife,
PossessionData { PossessionData {

View File

@ -1,16 +1,12 @@
use super::{PossessionData, PossessionType, WriteHandler, ArglessHandler, possession_data}; use super::{possession_data, ArglessHandler, PossessionData, PossessionType, WriteHandler};
use crate::{ use crate::{
models::{
item::{Item, ItemSpecialData},
corp::{Corp, CorpMembership, CorpPermission},
},
message_handler::user_commands::{ message_handler::user_commands::{
register::is_invalid_username, parsing::parse_username, register::is_invalid_username, user_error,
parsing::parse_username, CommandHandlingError::UserError, UResult, VerbContext,
user_error, },
UResult, models::{
CommandHandlingError::UserError, corp::{Corp, CorpMembership, CorpPermission},
VerbContext, item::{Item, ItemSpecialData},
}, },
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
}; };
@ -21,17 +17,23 @@ use once_cell::sync::OnceCell;
use super::PossessionType::*; use super::PossessionType::*;
pub struct CorpLicenceHandler { pub struct CorpLicenceHandler {}
}
#[async_trait] #[async_trait]
impl WriteHandler for CorpLicenceHandler { impl WriteHandler for CorpLicenceHandler {
async fn write_cmd(&self, ctx: &mut VerbContext, _player: &Item, on_what: &Item, write_what: &str) -> UResult<()> { async fn write_cmd(
&self,
ctx: &mut VerbContext,
_player: &Item,
on_what: &Item,
write_what: &str,
) -> UResult<()> {
let name = match parse_username(write_what) { let name = match parse_username(write_what) {
Err(e) => user_error("Invalid corp name: ".to_owned() + e)?, Err(e) => user_error("Invalid corp name: ".to_owned() + e)?,
Ok((_, rest)) if rest != "" => Ok((_, rest)) if rest != "" => {
user_error("No spaces allowed in corp names!".to_owned())?, user_error("No spaces allowed in corp names!".to_owned())?
Ok((name, _)) => name }
Ok((name, _)) => name,
}; };
if is_invalid_username(name) { if is_invalid_username(name) {
user_error("Sorry, that corp name isn't allowed. Try another".to_owned())?; user_error("Sorry, that corp name isn't allowed. Try another".to_owned())?;
@ -45,14 +47,22 @@ impl WriteHandler for CorpLicenceHandler {
let mut item_clone = on_what.clone(); let mut item_clone = on_what.clone();
item_clone.special_data = Some(ItemSpecialData::ItemWriting { item_clone.special_data = Some(ItemSpecialData::ItemWriting {
text: name.to_owned() text: name.to_owned(),
}); });
ctx.trans.save_item_model(&item_clone).await?; ctx.trans.save_item_model(&item_clone).await?;
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!( ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
"The pencil makes a scratching sound as you mark the paper with the attached \ "The pencil makes a scratching sound as you mark the paper with the attached \
pencil and write \"{}\" on it. [Hint: Try the <bold>use<reset> command to submit \ pencil and write \"{}\" on it. [Hint: Try the <bold>use<reset> command to submit \
your signed paperwork and register the corporation, or <bold>write<reset> again \ your signed paperwork and register the corporation, or <bold>write<reset> again \
to erase and change the name].\n"), name))).await?; to erase and change the name].\n"),
name
)),
)
.await?;
Ok(()) Ok(())
} }
@ -63,7 +73,7 @@ impl ArglessHandler for CorpLicenceHandler {
async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> { async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> {
let name = match what.special_data.as_ref() { let name = match what.special_data.as_ref() {
Some(ItemSpecialData::ItemWriting { text }) => text, Some(ItemSpecialData::ItemWriting { text }) => text,
_ => user_error("You have to write your corp's name on it first!".to_owned())? _ => user_error("You have to write your corp's name on it first!".to_owned())?,
}; };
if ctx.trans.find_by_username(&name).await?.is_some() { if ctx.trans.find_by_username(&name).await?.is_some() {
user_error("Corp name clashes with existing user name".to_owned())?; user_error("Corp name clashes with existing user name".to_owned())?;
@ -72,40 +82,60 @@ impl ArglessHandler for CorpLicenceHandler {
user_error("Corp name already taken!".to_owned())?; user_error("Corp name already taken!".to_owned())?;
} }
if ctx.trans.get_corp_memberships_for_user(&player.item_code).await?.len() >= 5 { if ctx
.trans
.get_corp_memberships_for_user(&player.item_code)
.await?
.len()
>= 5
{
user_error("You can't be in more than 5 corps".to_owned())?; user_error("You can't be in more than 5 corps".to_owned())?;
} }
broadcast_to_room(ctx.trans, &player.location, None, broadcast_to_room(
ctx.trans,
&player.location,
None,
&format!( &format!(
"{} signs a contract establishing {} as a corp\n", "{} signs a contract establishing {} as a corp\n",
&player.display_for_sentence(true, 1, true), &player.display_for_sentence(true, 1, true),
name name
), ),
Some( Some(&format!(
&format!("{} signs a contract establishing {} as a corp\n", "{} signs a contract establishing {} as a corp\n",
&player.display_for_sentence(false, 1, true), &player.display_for_sentence(false, 1, true),
name name
)),
) )
)).await?; .await?;
let corp_id = ctx.trans.create_corp(&Corp { let corp_id = ctx
.trans
.create_corp(&Corp {
name: name.to_owned(), name: name.to_owned(),
..Default::default() ..Default::default()
}).await?; })
ctx.trans.upsert_corp_membership( .await?;
&corp_id, &player.item_code, ctx.trans
.upsert_corp_membership(
&corp_id,
&player.item_code,
&CorpMembership { &CorpMembership {
joined_at: Some(Utc::now()), joined_at: Some(Utc::now()),
permissions: vec!(CorpPermission::Holder), permissions: vec![CorpPermission::Holder],
allow_combat: true, allow_combat: true,
job_title: "Founder".to_owned(), job_title: "Founder".to_owned(),
..Default::default() ..Default::default()
}).await?; },
)
.await?;
let mut what_mut = what.clone(); let mut what_mut = what.clone();
what_mut.possession_type = Some(CertificateOfIncorporation); what_mut.possession_type = Some(CertificateOfIncorporation);
let cp_data = possession_data().get(&CertificateOfIncorporation) let cp_data = possession_data()
.ok_or_else(|| UserError("Certificate of Incorporation no longer exists as an item".to_owned()))?; .get(&CertificateOfIncorporation)
.ok_or_else(|| {
UserError("Certificate of Incorporation no longer exists as an item".to_owned())
})?;
what_mut.display = cp_data.display.to_owned(); what_mut.display = cp_data.display.to_owned();
what_mut.details = Some(cp_data.details.to_owned()); what_mut.details = Some(cp_data.details.to_owned());
ctx.trans.save_item_model(&what_mut).await?; ctx.trans.save_item_model(&what_mut).await?;
@ -117,8 +147,7 @@ impl ArglessHandler for CorpLicenceHandler {
static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {}; static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {};
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(NewCorpLicence, (NewCorpLicence,
PossessionData { PossessionData {

View File

@ -1,40 +1,35 @@
use super::{PossessionData, WeaponData, WeaponAttackData}; use super::{PossessionData, WeaponAttackData, WeaponData};
use crate::models::item::SkillType; use crate::models::item::SkillType;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static PossessionData { pub fn data() -> &'static PossessionData {
static D: OnceCell<PossessionData> = OnceCell::new(); static D: OnceCell<PossessionData> = OnceCell::new();
D.get_or_init( D.get_or_init(|| PossessionData {
||
PossessionData {
weapon_data: Some(WeaponData { weapon_data: Some(WeaponData {
uses_skill: SkillType::Fists, uses_skill: SkillType::Fists,
raw_min_to_learn: 0.0, raw_min_to_learn: 0.0,
raw_max_to_learn: 2.0, raw_max_to_learn: 2.0,
normal_attack: WeaponAttackData { normal_attack: WeaponAttackData {
start_messages: vec!( start_messages: vec![Box::new(|attacker, victim, exp| {
Box::new(|attacker, victim, exp| format!(
format!("{} bares {} teeth and lunges at {}", "{} bares {} teeth and lunges at {}",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&attacker.pronouns.possessive, &attacker.pronouns.possessive,
&victim.display_for_sentence(exp, 1, false), &victim.display_for_sentence(exp, 1, false),
) )
) })],
), success_messages: vec![Box::new(|attacker, victim, part, exp| {
success_messages: vec!( format!(
Box::new(|attacker, victim, part, exp| "{}'s teeth connect and tear at the flesh of {}'s {}",
format!("{}'s teeth connect and tear at the flesh of {}'s {}",
&attacker.display_for_sentence(exp, 1, true), &attacker.display_for_sentence(exp, 1, true),
&victim.display_for_sentence(exp, 1, false), &victim.display_for_sentence(exp, 1, false),
&part.display(victim.sex.clone()) &part.display(victim.sex.clone())
) )
) })],
),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
} })
)
} }

View File

@ -1,10 +1,9 @@
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData}; use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
use crate::static_content::species::BodyPart; use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
( (
PossessionType::RustyMetalPot, PossessionType::RustyMetalPot,

View File

@ -1,13 +1,12 @@
use super::{PossessionData, ArglessHandler, PossessionType}; use super::{ArglessHandler, PossessionData, PossessionType};
use crate::{ use crate::{
message_handler::user_commands::{user_error, UResult, VerbContext},
models::item::{Item, LocationActionType}, models::item::{Item, LocationActionType},
message_handler::user_commands::{user_error, VerbContext, UResult}, services::{
static_content::{ capacity::{check_item_capacity, CapacityLevel},
possession_type::InstallHandler, comms::broadcast_to_room,
room::Direction,
}, },
services::{comms::broadcast_to_room, static_content::{possession_type::InstallHandler, room::Direction},
capacity::{check_item_capacity, CapacityLevel}}
}; };
use async_trait::async_trait; use async_trait::async_trait;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -32,23 +31,34 @@ static LOCK_WEIGHT: u64 = 500;
struct ScanLockInstall; struct ScanLockInstall;
#[async_trait] #[async_trait]
impl InstallHandler for ScanLockInstall { impl InstallHandler for ScanLockInstall {
async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, async fn install_cmd(
direction: &Direction) -> UResult<()> { &self,
ctx: &mut VerbContext,
player: &Item,
what: &Item,
room: &Item,
direction: &Direction,
) -> UResult<()> {
if what.action_type != LocationActionType::Normal { if what.action_type != LocationActionType::Normal {
user_error("That scanlock is already in use.".to_owned())?; user_error("That scanlock is already in use.".to_owned())?;
} }
if !ctx.trans.find_by_action_and_location( if !ctx
.trans
.find_by_action_and_location(
&room.refstr(), &room.refstr(),
&LocationActionType::InstalledOnDoorAsLock(direction.clone()) &LocationActionType::InstalledOnDoorAsLock(direction.clone()),
).await?.is_empty() { )
.await?
.is_empty()
{
user_error("There's already a lock on that door - uninstall it first.".to_owned())?; user_error("There's already a lock on that door - uninstall it first.".to_owned())?;
} }
match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? { match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => user_error(
user_error("That room has so much stuff, you can't install anything new." "That room has so much stuff, you can't install anything new.".to_owned(),
.to_owned())?, )?,
_ => {} _ => {}
}; };
let mut what_mut = (*what).clone(); let mut what_mut = (*what).clone();
@ -58,32 +68,45 @@ impl InstallHandler for ScanLockInstall {
ctx.trans.save_item_model(&what_mut).await?; ctx.trans.save_item_model(&what_mut).await?;
broadcast_to_room( broadcast_to_room(
&ctx.trans, &room.refstr(), None, &ctx.trans,
&format!("{} bangs the door to the {} as he installs {} on it.\n", &room.refstr(),
None,
&format!(
"{} bangs the door to the {} as he installs {} on it.\n",
&player.display_for_sentence(true, 1, true), &player.display_for_sentence(true, 1, true),
&direction.describe(), &direction.describe(),
&what.display_for_sentence(true, 1, false)), &what.display_for_sentence(true, 1, false)
Some( ),
&format!("{} bangs the door to the {} as he installs {} on it.\n", Some(&format!(
"{} bangs the door to the {} as he installs {} on it.\n",
&player.display_for_sentence(false, 1, true), &player.display_for_sentence(false, 1, true),
&direction.describe(), &direction.describe(),
&what.display_for_sentence(false, 1, false)), &what.display_for_sentence(false, 1, false)
)).await?; )),
)
.await?;
Ok(()) Ok(())
} }
async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, async fn uninstall_cmd(
direction: &Direction) -> UResult<()> { &self,
ctx: &mut VerbContext,
player: &Item,
what: &Item,
room: &Item,
direction: &Direction,
) -> UResult<()> {
if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) { if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) {
user_error("That scanlock is not installed as a lock.".to_owned())?; user_error("That scanlock is not installed as a lock.".to_owned())?;
} }
let mut what_mut = (*what).clone(); let mut what_mut = (*what).clone();
let extra_text = match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? { let extra_text =
match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => { CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
", dropping it on the floor since he can't hold it." ", dropping it on the floor since he can't hold it."
}, }
_ => { _ => {
what_mut.location = player.refstr(); what_mut.location = player.refstr();
"" ""
@ -94,29 +117,31 @@ impl InstallHandler for ScanLockInstall {
ctx.trans.save_item_model(&what_mut).await?; ctx.trans.save_item_model(&what_mut).await?;
broadcast_to_room( broadcast_to_room(
&ctx.trans, &room.refstr(), None, &ctx.trans,
&format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", &room.refstr(),
None,
&format!(
"{} bangs the door to the {} as he uninstalls {} from it{}.\n",
&player.display_for_sentence(true, 1, true), &player.display_for_sentence(true, 1, true),
&direction.describe(), &direction.describe(),
&what.display_for_sentence(true, 1, false), &what.display_for_sentence(true, 1, false),
extra_text extra_text
), ),
Some( Some(&format!(
&format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", "{} bangs the door to the {} as he uninstalls {} from it{}.\n",
&player.display_for_sentence(false, 1, true), &player.display_for_sentence(false, 1, true),
&direction.describe(), &direction.describe(),
&what.display_for_sentence(false, 1, false), &what.display_for_sentence(false, 1, false),
extra_text extra_text
), )),
)).await?; )
.await?;
Ok(()) Ok(())
} }
} }
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(PossessionType::Scanlock, (PossessionType::Scanlock,
PossessionData { PossessionData {

View File

@ -1,10 +1,9 @@
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData}; use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
use crate::static_content::species::BodyPart; use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
( (
PossessionType::LeatherPants, PossessionType::LeatherPants,

View File

@ -2,8 +2,7 @@ use super::{PossessionData, PossessionType};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(PossessionType::AnimalSkin, (PossessionType::AnimalSkin,
PossessionData { PossessionData {

View File

@ -1,10 +1,9 @@
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData}; use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
use crate::static_content::species::BodyPart; use crate::static_content::species::BodyPart;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
( (
PossessionType::LeatherJacket, PossessionType::LeatherJacket,

View File

@ -1,15 +1,11 @@
use super::{PossessionData, PossessionType, UseData, UseEffect, ChargeData}; use super::{ChargeData, PossessionData, PossessionType, UseData, UseEffect};
use crate::models::{ use crate::models::{consent::ConsentType, item::SkillType};
item::SkillType,
consent::ConsentType,
};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use super::PossessionType::*; use super::PossessionType::*;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(MediumTraumaKit, (MediumTraumaKit,
PossessionData { PossessionData {

View File

@ -1,10 +1,9 @@
use super::{PossessionData, PossessionType, WeaponData, WeaponAttackData}; use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::models::item::SkillType; use crate::models::item::SkillType;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
OnceCell::new();
&D.get_or_init(|| vec!( &D.get_or_init(|| vec!(
(PossessionType::AntennaWhip, (PossessionType::AntennaWhip,
PossessionData { PossessionData {

View File

@ -1,5 +1,5 @@
use uuid::Uuid;
use serde::*; use serde::*;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub enum MessageFromListener { pub enum MessageFromListener {
@ -7,7 +7,7 @@ pub enum MessageFromListener {
SessionConnected { session: Uuid, source: String }, SessionConnected { session: Uuid, source: String },
SessionDisconnected { session: Uuid }, SessionDisconnected { session: Uuid },
SessionSentLine { session: Uuid, msg: String }, SessionSentLine { session: Uuid, msg: String },
AcknowledgeMessage AcknowledgeMessage,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -15,5 +15,5 @@ pub enum MessageToListener {
GameserverVersion { version: String }, GameserverVersion { version: String },
DisconnectSession { session: Uuid }, DisconnectSession { session: Uuid },
SendToSession { session: Uuid, msg: String }, SendToSession { session: Uuid, msg: String },
AcknowledgeMessage AcknowledgeMessage,
} }

View File

@ -2,8 +2,12 @@ use std::process::Command;
pub fn main() { pub fn main() {
let cmdout = Command::new("git") let cmdout = Command::new("git")
.arg("rev-parse").arg("HEAD") .arg("rev-parse")
.output().expect("git rev-parse HEAD failed"); .arg("HEAD")
println!("cargo:rustc-env=GIT_VERSION={}", .output()
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8")); .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,31 +1,32 @@
use std::vec::Vec; use blastmud_interfaces::*;
use std::path::Path; use futures::prelude::*;
use log::{info, warn, LevelFilter};
use nix::{
sys::signal::{kill, Signal},
unistd::{getpid, Pid},
};
use serde::*;
use simple_logger::SimpleLogger;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::net::SocketAddr;
use std::fs; use std::fs;
use serde::*; use std::net::SocketAddr;
use tokio::task; use std::path::Path;
use tokio::time::{self, Duration}; use std::sync::Arc;
use tokio::net::{TcpStream, TcpListener, lookup_host}; use std::time::Instant;
use std::vec::Vec;
use tokio::io::{AsyncWriteExt, BufReader};
use tokio::net::{lookup_host, TcpListener, TcpStream};
use tokio::signal::unix::{signal, SignalKind}; use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::{mpsc, Mutex, RwLock}; use tokio::sync::{mpsc, Mutex, RwLock};
use tokio::io::{BufReader, AsyncWriteExt}; use tokio::task;
use log::{warn, info, LevelFilter}; use tokio::time::{self, Duration};
use simple_logger::SimpleLogger; use tokio_serde::formats::Cbor;
use std::sync::Arc; use tokio_stream::wrappers::ReceiverStream;
use blastmud_interfaces::*;
use tokio_util::codec; use tokio_util::codec;
use tokio_util::codec::length_delimited::LengthDelimitedCodec; use tokio_util::codec::length_delimited::LengthDelimitedCodec;
use tokio_serde::formats::Cbor;
use futures::prelude::*;
use uuid::Uuid; use uuid::Uuid;
use tokio_stream::wrappers::ReceiverStream; use warp::{self, filters::ws, Filter, Reply};
use warp::{
self, filters::ws, Filter, Reply
};
use std::time::Instant;
use nix::{sys::signal::{kill, Signal}, unistd::{Pid, getpid}};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
struct Config { struct Config {
@ -38,14 +39,14 @@ struct Config {
type DResult<A> = Result<A, Box<dyn Error + Send + Sync>>; type DResult<A> = Result<A, Box<dyn Error + Send + Sync>>;
fn read_latest_config() -> DResult<Config> { fn read_latest_config() -> DResult<Config> {
serde_yaml::from_str(&fs::read_to_string("listener.conf")?). serde_yaml::from_str(&fs::read_to_string("listener.conf")?)
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>) .map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum ServerTaskCommand { enum ServerTaskCommand {
SwitchTo { new_server: String }, SwitchTo { new_server: String },
Send { message: MessageFromListener } Send { message: MessageFromListener },
} }
fn run_server_task<FHandler, HandlerFut>( fn run_server_task<FHandler, HandlerFut>(
@ -54,32 +55,31 @@ fn run_server_task<FHandler, HandlerFut>(
mut receiver: ReceiverStream<ServerTaskCommand>, mut receiver: ReceiverStream<ServerTaskCommand>,
sender: mpsc::Sender<ServerTaskCommand>, sender: mpsc::Sender<ServerTaskCommand>,
server: String, server: String,
message_handler: FHandler message_handler: FHandler,
) ) where
where
FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static, FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static,
HandlerFut: Future<Output = ()> + Send + 'static HandlerFut: Future<Output = ()> + Send + 'static,
{ {
task::spawn(async move { task::spawn(async move {
let conn = loop { let conn = loop {
match TcpStream::connect(&server).await { match TcpStream::connect(&server).await {
Err(e) => warn!("Can't connect to {}: {}", server, e), Err(e) => warn!("Can't connect to {}: {}", server, e),
Ok(c) => break c Ok(c) => break c,
} }
time::sleep(Duration::from_secs(1)).await; time::sleep(Duration::from_secs(1)).await;
}; };
let mut conn_framed = tokio_serde::Framed::new( let mut conn_framed = tokio_serde::Framed::new(
codec::Framed::new(conn, LengthDelimitedCodec::new()), codec::Framed::new(conn, LengthDelimitedCodec::new()),
Cbor::<MessageToListener, MessageFromListener>::default() Cbor::<MessageToListener, MessageFromListener>::default(),
); );
let mut commands = stream::iter(vec!( let mut commands = stream::iter(vec![ServerTaskCommand::Send {
ServerTaskCommand::Send { message: MessageFromListener::ListenerPing { uuid: listener_id },
message: MessageFromListener::ListenerPing { uuid: listener_id } }])
}) .chain(stream::iter(
).chain( unfinished_business.map(|message| ServerTaskCommand::Send { message }),
stream::iter(unfinished_business.map(|message| ServerTaskCommand::Send { message })) ))
).chain(&mut receiver); .chain(&mut receiver);
'full_select: loop { 'full_select: loop {
tokio::select!( tokio::select!(
@ -216,24 +216,22 @@ where
} }
); );
} }
}); });
} }
enum SessionCommand { enum SessionCommand {
Disconnect, Disconnect,
SendString { message : String } SendString { message: String },
} }
struct SessionRecord { struct SessionRecord {
channel: mpsc::Sender<SessionCommand>, channel: mpsc::Sender<SessionCommand>,
disconnect_channel: mpsc::UnboundedSender<()> disconnect_channel: mpsc::UnboundedSender<()>,
} }
struct SessionIndexes { struct SessionIndexes {
by_uuid: BTreeMap<Uuid, SessionRecord>, by_uuid: BTreeMap<Uuid, SessionRecord>,
count_by_source: BTreeMap<String, u64> count_by_source: BTreeMap<String, u64>,
} }
type SessionMap = Arc<Mutex<SessionIndexes>>; type SessionMap = Arc<Mutex<SessionIndexes>>;
@ -244,19 +242,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
MessageToListener::GameserverVersion { version } => { MessageToListener::GameserverVersion { version } => {
let mut version_mut = version_data().write().await; let mut version_mut = version_data().write().await;
(*version_mut).gameserver_version = Some(version.clone()); (*version_mut).gameserver_version = Some(version.clone());
}, }
MessageToListener::DisconnectSession { session } => { MessageToListener::DisconnectSession { session } => {
match session_map.lock().await.by_uuid.get(&session) { match session_map.lock().await.by_uuid.get(&session) {
// Just silently ignore it if they are disconnected. // Just silently ignore it if they are disconnected.
None => {} None => {}
Some(SessionRecord { channel, disconnect_channel, .. }) => { Some(SessionRecord {
match channel.try_send(SessionCommand::Disconnect) { channel,
disconnect_channel,
..
}) => match channel.try_send(SessionCommand::Disconnect) {
Err(mpsc::error::TrySendError::Full(_)) => { Err(mpsc::error::TrySendError::Full(_)) => {
disconnect_channel.send(()).unwrap_or(()); disconnect_channel.send(()).unwrap_or(());
} }
_ => {} _ => {}
} },
}
} }
} }
MessageToListener::SendToSession { session, msg } => { MessageToListener::SendToSession { session, msg } => {
@ -264,7 +264,8 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
// Just silently ignore it if they are disconnected. // Just silently ignore it if they are disconnected.
None => {} None => {}
Some(SessionRecord { channel, .. }) => { Some(SessionRecord { channel, .. }) => {
channel.try_send(SessionCommand::SendString { message: msg }) channel
.try_send(SessionCommand::SendString { message: msg })
.unwrap_or(()); .unwrap_or(());
} }
} }
@ -272,14 +273,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
} }
} }
fn start_server_task(listener_id: Uuid, fn start_server_task(
listener_id: Uuid,
server: String, server: String,
session_map: SessionMap) -> mpsc::Sender<ServerTaskCommand> { session_map: SessionMap,
) -> mpsc::Sender<ServerTaskCommand> {
let (sender, receiver) = mpsc::channel(20); let (sender, receiver) = mpsc::channel(20);
let receiver_stream = ReceiverStream::new(receiver); let receiver_stream = ReceiverStream::new(receiver);
run_server_task(None, listener_id, receiver_stream, sender.clone(), server, run_server_task(
move |msg| handle_server_message(session_map.clone(), None,
msg) ); listener_id,
receiver_stream,
sender.clone(),
server,
move |msg| handle_server_message(session_map.clone(), msg),
);
sender sender
} }
@ -299,13 +307,13 @@ impl TokenBucket {
level: initial_level, level: initial_level,
last_topup: Instant::now(), last_topup: Instant::now(),
max_level, max_level,
alloc_per_ms alloc_per_ms,
} }
} }
pub fn update(self: &mut Self) { pub fn update(self: &mut Self) {
self.level = self.level = (self.level
(self.level + self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64)) + self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64))
.min(self.max_level); .min(self.max_level);
self.last_topup = Instant::now(); self.last_topup = Instant::now();
} }
@ -340,19 +348,18 @@ async fn handle_client_socket(
server: mpsc::Sender<ServerTaskCommand>, server: mpsc::Sender<ServerTaskCommand>,
active_sessions: SessionMap, active_sessions: SessionMap,
mut stream: TcpStream, mut stream: TcpStream,
addr: SocketAddr addr: SocketAddr,
) { ) {
let (rstream, mut wstream) = stream.split(); let (rstream, mut wstream) = stream.split();
let mut rbuf = codec::FramedRead::new( let mut rbuf = codec::FramedRead::new(
BufReader::new(rstream), BufReader::new(rstream),
codec::LinesCodec::new_with_max_length(512) codec::LinesCodec::new_with_max_length(512),
); );
let session = Uuid::new_v4(); let session = Uuid::new_v4();
let mut tok_bucket = let mut tok_bucket =
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS); TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
info!("Accepted session {} from {}", session, addr); info!("Accepted session {} from {}", session, addr);
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY); let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel(); let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
@ -360,8 +367,14 @@ async fn handle_client_socket(
let addr_str = addr.ip().to_string(); let addr_str = addr.ip().to_string();
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP { if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
drop(sess_idx_lock); drop(sess_idx_lock);
info!("Rejecting session {} because of too many concurrent connections", session); info!(
match wstream.write_all("Too many connections from same IP\r\n".as_bytes()).await { "Rejecting session {} because of too many concurrent connections",
session
);
match wstream
.write_all("Too many connections from same IP\r\n".as_bytes())
.await
{
Err(e) => { Err(e) => {
info!("Client connection {} got error {}", session, e); info!("Client connection {} got error {}", session, e);
} }
@ -369,17 +382,31 @@ async fn handle_client_socket(
} }
return; return;
} }
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|c| { *c += 1; }).or_insert(1); sess_idx_lock
.count_by_source
.entry(addr_str.clone())
.and_modify(|c| {
*c += 1;
})
.or_insert(1);
sess_idx_lock.by_uuid.insert( sess_idx_lock.by_uuid.insert(
session, SessionRecord { session,
SessionRecord {
channel: lsender.clone(), channel: lsender.clone(),
disconnect_channel: discon_sender.clone() disconnect_channel: discon_sender.clone(),
}); },
);
drop(sess_idx_lock); drop(sess_idx_lock);
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected { server
session, source: addr.to_string() .send(ServerTaskCommand::Send {
}}).await.unwrap(); message: MessageFromListener::SessionConnected {
session,
source: addr.to_string(),
},
})
.await
.unwrap();
'client_loop: loop { 'client_loop: loop {
tok_bucket.update(); tok_bucket.update();
@ -437,13 +464,21 @@ async fn handle_client_socket(
); );
} }
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected { server
session .send(ServerTaskCommand::Send {
}}).await.unwrap(); message: MessageFromListener::SessionDisconnected { session },
})
.await
.unwrap();
sess_idx_lock = active_sessions.lock().await; sess_idx_lock = active_sessions.lock().await;
sess_idx_lock.by_uuid.remove(&session); sess_idx_lock.by_uuid.remove(&session);
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|v| { *v -= 1; }); sess_idx_lock
.count_by_source
.entry(addr_str.clone())
.and_modify(|v| {
*v -= 1;
});
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 { if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
sess_idx_lock.count_by_source.remove(&addr_str); sess_idx_lock.count_by_source.remove(&addr_str);
} }
@ -453,9 +488,12 @@ fn start_pinger(listener: Uuid, server: mpsc::Sender<ServerTaskCommand>) {
task::spawn(async move { task::spawn(async move {
loop { loop {
time::sleep(Duration::from_secs(60)).await; time::sleep(Duration::from_secs(60)).await;
server.send(ServerTaskCommand::Send { server
message: MessageFromListener::ListenerPing { uuid: listener } .send(ServerTaskCommand::Send {
}).await.unwrap(); message: MessageFromListener::ListenerPing { uuid: listener },
})
.await
.unwrap();
} }
}); });
} }
@ -464,10 +502,13 @@ async fn handle_websocket(
mut ws: ws::WebSocket, mut ws: ws::WebSocket,
src: String, src: String,
active_sessions: SessionMap, active_sessions: SessionMap,
server: mpsc::Sender<ServerTaskCommand> server: mpsc::Sender<ServerTaskCommand>,
) { ) {
let session = Uuid::new_v4(); let session = Uuid::new_v4();
info!("Accepted websocket session {} with forwarded-for {}", session, src); info!(
"Accepted websocket session {} with forwarded-for {}",
session, src
);
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY); let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel(); let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
@ -476,8 +517,14 @@ async fn handle_websocket(
let addr_str: String = src.split(" ").last().unwrap_or("").to_string(); let addr_str: String = src.split(" ").last().unwrap_or("").to_string();
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP { if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
drop(sess_idx_lock); drop(sess_idx_lock);
info!("Rejecting session {} because of too many concurrent connections", session); info!(
match ws.send(ws::Message::text("Too many connections from same IP\r\n")).await { "Rejecting session {} because of too many concurrent connections",
session
);
match ws
.send(ws::Message::text("Too many connections from same IP\r\n"))
.await
{
Err(e) => { Err(e) => {
info!("Client connection {} got error {}", session, e); info!("Client connection {} got error {}", session, e);
} }
@ -485,17 +532,31 @@ async fn handle_websocket(
} }
return; return;
} }
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|c| { *c += 1; }).or_insert(1); sess_idx_lock
.count_by_source
.entry(addr_str.clone())
.and_modify(|c| {
*c += 1;
})
.or_insert(1);
sess_idx_lock.by_uuid.insert( sess_idx_lock.by_uuid.insert(
session, SessionRecord { session,
SessionRecord {
channel: lsender.clone(), channel: lsender.clone(),
disconnect_channel: discon_sender.clone() disconnect_channel: discon_sender.clone(),
}); },
);
drop(sess_idx_lock); drop(sess_idx_lock);
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected { server
session, source: src .send(ServerTaskCommand::Send {
}}).await.unwrap(); message: MessageFromListener::SessionConnected {
session,
source: src,
},
})
.await
.unwrap();
let mut tok_bucket = let mut tok_bucket =
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS); TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
@ -576,27 +637,35 @@ async fn handle_websocket(
); );
} }
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected { server
session .send(ServerTaskCommand::Send {
}}).await.unwrap(); message: MessageFromListener::SessionDisconnected { session },
})
.await
.unwrap();
sess_idx_lock = active_sessions.lock().await; sess_idx_lock = active_sessions.lock().await;
sess_idx_lock.by_uuid.remove(&session); sess_idx_lock.by_uuid.remove(&session);
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|v| { *v -= 1; }); sess_idx_lock
.count_by_source
.entry(addr_str.clone())
.and_modify(|v| {
*v -= 1;
});
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 { if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
sess_idx_lock.count_by_source.remove(&addr_str); sess_idx_lock.count_by_source.remove(&addr_str);
} }
} }
async fn upgrade_websocket(src: String, wsreq: ws::Ws, async fn upgrade_websocket(
src: String,
wsreq: ws::Ws,
active_sessions: SessionMap, active_sessions: SessionMap,
server_sender: mpsc::Sender<ServerTaskCommand>) -> server_sender: mpsc::Sender<ServerTaskCommand>,
Result<warp::reply::Response, warp::Rejection> { ) -> Result<warp::reply::Response, warp::Rejection> {
Ok( Ok(wsreq
wsreq.on_upgrade(|wss| handle_websocket( .on_upgrade(|wss| handle_websocket(wss, src, active_sessions, server_sender))
wss, src, active_sessions, .into_response())
server_sender)).into_response()
)
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -620,44 +689,52 @@ async fn respond_version() -> Result<warp::reply::Response, warp::Rejection> {
Ok(warp::reply::json(&*version_data().read().await).into_response()) Ok(warp::reply::json(&*version_data().read().await).into_response())
} }
async fn start_websocket(bind: String, active_sessions: SessionMap, server_sender: mpsc::Sender<ServerTaskCommand>) -> DResult<()> { async fn start_websocket(
let sockaddr = lookup_host(bind).await?.next().expect("Can't resolve websocket bind name"); bind: String,
let routes = active_sessions: SessionMap,
warp::get() server_sender: mpsc::Sender<ServerTaskCommand>,
) -> DResult<()> {
let sockaddr = lookup_host(bind)
.await?
.next()
.expect("Can't resolve websocket bind name");
let routes = warp::get()
.and(warp::path("wsgame")) .and(warp::path("wsgame"))
.and(warp::header("X-Forwarded-For")) .and(warp::header("X-Forwarded-For"))
.and(ws::ws()) .and(ws::ws())
.and_then(move |src, wsreq| upgrade_websocket(src, wsreq, active_sessions.clone(), server_sender.clone())) .and_then(move |src, wsreq| {
upgrade_websocket(src, wsreq, active_sessions.clone(), server_sender.clone())
})
.or(warp::get() .or(warp::get()
.and(warp::path("version")) .and(warp::path("version"))
.and_then(|| respond_version())); .and_then(|| respond_version()));
task::spawn( task::spawn(warp::serve(routes).run(sockaddr));
warp::serve(
routes
).run(sockaddr)
);
Ok(()) Ok(())
} }
pub fn replace_old_listener(pidfile: &str) -> DResult<()> { pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
match fs::read_to_string(pidfile) { match fs::read_to_string(pidfile) {
Err(e) => Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound { if e.kind() == std::io::ErrorKind::NotFound {
info!("pidfile not found, assuming not already running"); info!("pidfile not found, assuming not already running");
Ok(()) Ok(())
} else { } else {
info!("Error reading pidfile (other than NotFound): {}", e); info!("Error reading pidfile (other than NotFound): {}", e);
Err(Box::new(e) as Box::<dyn Error + Send + Sync>) Err(Box::new(e) as Box<dyn Error + Send + Sync>)
}
} }
Ok(f) => { Ok(f) => {
let pid: Pid = Pid::from_raw(f.parse().map_err(|e| Box::new(e) as Box::<dyn Error + Send + Sync>)?); let pid: Pid = Pid::from_raw(
f.parse()
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?,
);
if pid == getpid() { if pid == getpid() {
info!("Pid in pidfile is me - ignoring"); info!("Pid in pidfile is me - ignoring");
return Ok(()); return Ok(());
} }
match fs::read_to_string(format!("/proc/{}/cmdline", pid)) { match fs::read_to_string(format!("/proc/{}/cmdline", pid)) {
Ok(content) => Ok(content) => {
if content.contains("blastmud_listener") { if content.contains("blastmud_listener") {
info!("pid in pidfile references blastmud_listener; starting cutover"); info!("pid in pidfile references blastmud_listener; starting cutover");
kill(pid, Signal::SIGTERM) kill(pid, Signal::SIGTERM)
@ -666,6 +743,7 @@ pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
info!("Pid in pidfile is for process not including blastmud_listener - ignoring pidfile"); info!("Pid in pidfile is for process not including blastmud_listener - ignoring pidfile");
Ok(()) Ok(())
} }
}
Err(_) => { Err(_) => {
info!("Pid in pidfile is gone - ignoring pidfile"); info!("Pid in pidfile is gone - ignoring pidfile");
Ok(()) Ok(())
@ -675,25 +753,33 @@ pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
}?; }?;
info!("Writing new pidfile"); info!("Writing new pidfile");
fs::write(Path::new(pidfile), format!("{}", std::process::id())) fs::write(Path::new(pidfile), format!("{}", std::process::id()))
.map_err(|e| Box::new(e) as Box::<dyn Error + Send + Sync>) .map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> { async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); SimpleLogger::new()
.with_level(LevelFilter::Info)
.init()
.unwrap();
let listener_id = Uuid::new_v4(); let listener_id = Uuid::new_v4();
let mut config = read_latest_config()?; let mut config = read_latest_config()?;
let active_sessions: SessionMap = let active_sessions: SessionMap = Arc::new(Mutex::new(SessionIndexes {
Arc::new(Mutex::new(SessionIndexes { by_uuid: BTreeMap::new(), count_by_source: BTreeMap::new() })); by_uuid: BTreeMap::new(),
count_by_source: BTreeMap::new(),
}));
replace_old_listener(&config.pidfile)?; replace_old_listener(&config.pidfile)?;
let server_sender = start_server_task(listener_id, config.gameserver, active_sessions.clone()); let server_sender = start_server_task(listener_id, config.gameserver, active_sessions.clone());
start_pinger(listener_id, server_sender.clone()); start_pinger(listener_id, server_sender.clone());
// Note: for now, this cannot be reconfigured without a complete restart. // Note: for now, this cannot be reconfigured without a complete restart.
start_websocket(config.ws_listener, active_sessions.clone(), server_sender.clone()).await?; start_websocket(
config.ws_listener,
active_sessions.clone(),
server_sender.clone(),
)
.await?;
let mut sighups = signal(SignalKind::hangup())?; let mut sighups = signal(SignalKind::hangup())?;
@ -704,25 +790,30 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let active_sessions_for_listener = active_sessions.clone(); let active_sessions_for_listener = active_sessions.clone();
listen_handles.push(task::spawn(async move { listen_handles.push(task::spawn(async move {
match TcpListener::bind(&listener).await { match TcpListener::bind(&listener).await {
Err(e) => { warn!("Error listening to {}: {}", &listener, e); } Err(e) => {
Ok(listensock) => { warn!("Error listening to {}: {}", &listener, e);
loop { }
Ok(listensock) => loop {
match listensock.accept().await { match listensock.accept().await {
Err(e) => { warn!("Error accepting connection from {}: {}", Err(e) => {
&listener, e); } warn!("Error accepting connection from {}: {}", &listener, e);
}
Ok((stream, addr)) => { Ok((stream, addr)) => {
let server_sender_for_client = server_sender_for_listener.clone(); let server_sender_for_client = server_sender_for_listener.clone();
let active_sessions_for_client = active_sessions_for_listener.clone(); let active_sessions_for_client =
active_sessions_for_listener.clone();
task::spawn(async move { task::spawn(async move {
handle_client_socket(server_sender_for_client, handle_client_socket(
server_sender_for_client,
active_sessions_for_client, active_sessions_for_client,
stream, stream,
addr addr,
).await; )
}); } .await;
} });
} }
} }
},
} }
})); }));
} }
@ -735,7 +826,10 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// Note: It is deliberate behaviour to send this even if gameserver // Note: It is deliberate behaviour to send this even if gameserver
// hasn't changed - SIGHUP is to be used after a server hot cutover to tell // hasn't changed - SIGHUP is to be used after a server hot cutover to tell
// it to connect to the new server process even if on the same port. // it to connect to the new server process even if on the same port.
server_sender.send(ServerTaskCommand::SwitchTo { new_server: config.gameserver }) server_sender
.send(ServerTaskCommand::SwitchTo {
new_server: config.gameserver,
})
.await?; .await?;
for handle in &listen_handles { for handle in &listen_handles {
@ -744,7 +838,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test] #[test]

0
gear.rs Normal file
View File