From 228c5fbb9b01510bab78822e61831f2b71652234 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 3 Jun 2023 23:47:29 +1000 Subject: [PATCH] Add gear command. --- .gitignore | 1 + ansi/src/lib.rs | 195 +++++--- ansi_macro/src/lib.rs | 101 ++-- blastmud_game/build.rs | 12 +- blastmud_game/rustfmt.toml | 1 + blastmud_game/src/av.rs | 28 +- .../src/message_handler/user_commands.rs | 2 + .../src/message_handler/user_commands/gear.rs | 120 +++++ .../src/static_content/possession_type.rs | 280 ++++++----- .../static_content/possession_type/blade.rs | 5 +- .../possession_type/corp_licence.rs | 133 +++--- .../static_content/possession_type/fangs.rs | 63 ++- .../possession_type/head_armour.rs | 5 +- .../static_content/possession_type/lock.rs | 141 +++--- .../possession_type/lower_armour.rs | 5 +- .../static_content/possession_type/meat.rs | 3 +- .../possession_type/torso_armour.rs | 5 +- .../possession_type/trauma_kit.rs | 10 +- .../static_content/possession_type/whip.rs | 5 +- blastmud_interfaces/src/lib.rs | 8 +- blastmud_listener/build.rs | 12 +- blastmud_listener/src/main.rs | 433 +++++++++++------- gear.rs | 0 23 files changed, 984 insertions(+), 584 deletions(-) create mode 100644 blastmud_game/rustfmt.toml create mode 100644 blastmud_game/src/message_handler/user_commands/gear.rs create mode 100644 gear.rs diff --git a/.gitignore b/.gitignore index c5a9bb0..28c4138 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target config docs/private +*~ diff --git a/ansi/src/lib.rs b/ansi/src/lib.rs index 34fa0b3..106579b 100644 --- a/ansi/src/lib.rs +++ b/ansi/src/lib.rs @@ -6,8 +6,10 @@ 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)] @@ -22,26 +24,35 @@ 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!("")); } - if self.bold { buf.push_str(ansi!("")); } - if self.underline { buf.push_str(ansi!("")); } - if self.strike { buf.push_str(ansi!("")); } + if self.bold { + buf.push_str(ansi!("")); + } + if self.underline { + buf.push_str(ansi!("")); + } + if self.strike { + buf.push_str(ansi!("")); + } 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 -); +struct AnsiEvent<'l>(AnsiParseToken<'l>, Rc); #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] enum AnsiParseToken<'l> { @@ -63,25 +74,25 @@ struct AnsiIterator<'l> { inject_spaces: u64, } - impl AnsiIterator<'_> { fn new<'l>(input: &'l str) -> AnsiIterator<'l> { - AnsiIterator { underlying: input.chars().enumerate(), - input: input, - state: Rc::new(AnsiState { - background: 0, - foreground: 0, - bold: false, - underline: false, - strike: false - }), - pending_col: false, - inject_spaces: 0 + AnsiIterator { + underlying: input.chars().enumerate(), + input: input, + state: Rc::new(AnsiState { + background: 0, + foreground: 0, + bold: false, + underline: false, + strike: false, + }), + pending_col: false, + inject_spaces: 0, } } } -impl <'l>Iterator for AnsiIterator<'l> { +impl<'l> Iterator for AnsiIterator<'l> { type Item = AnsiEvent<'l>; fn next(self: &mut Self) -> Option> { @@ -91,7 +102,10 @@ 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' { @@ -100,11 +114,17 @@ 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 != '[' { @@ -125,7 +145,9 @@ impl <'l>Iterator for AnsiIterator<'l> { cs_no *= 10; cs_no += cs_no2; imax = i3; - } else { continue; } + } else { + continue; + } } } else if cs2 != 'm' { continue; @@ -141,30 +163,37 @@ 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 @@ -193,7 +222,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 @@ -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 /// 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(); @@ -212,7 +246,7 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str, '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; @@ -244,7 +278,9 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str, 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, _)) => { @@ -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); @@ -285,7 +323,9 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str, 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, _)) => { @@ -303,10 +343,10 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str, 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 } @@ -315,7 +355,9 @@ fn is_wrappable(c: char) -> bool { } pub fn word_wrap(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; @@ -338,9 +380,12 @@ pub fn word_wrap(input: &str, limit: F) -> String 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) + } _ => {} } }; @@ -361,9 +406,13 @@ pub fn word_wrap(input: &str, limit: F) -> String } continue; } - assert!(col <= limit(row), - "col must be below limit, but found c={}, col={}, limit={}", - c, col, limit(row)); + assert!( + col <= limit(row), + "col must be below limit, but found c={}, col={}, limit={}", + c, + col, + limit(row) + ); if !start_word { if col == limit(row) { // We are about to hit the limit, and we need to decide @@ -372,9 +421,12 @@ pub fn word_wrap(input: &str, limit: F) -> String 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) + } _ => {} } }; @@ -429,7 +481,7 @@ pub fn word_wrap(input: &str, limit: F) -> String buf.push('\n'); start_word = true; } - Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t) + Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t), } } @@ -439,7 +491,7 @@ pub fn word_wrap(input: &str, limit: F) -> String #[cfg(test)] mod test { use super::*; - + #[test] fn ignore_special_characters_removes_esc() { assert_eq!(ignore_special_characters("hello\x1b[world"), "hello[world"); @@ -450,24 +502,26 @@ mod test { assert_eq!(strip_special_characters("a\tb"), "a b"); assert_eq!( strip_special_characters(ansi!("helloworld")), - "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!("abcd")), - ansi!("abcd")); - assert_eq!(limit_special_characters("Test\x1b[5;5fing"), - "Test5fing"); + assert_eq!( + limit_special_characters(ansi!("abcd")), + ansi!("abcd") + ); + assert_eq!(limit_special_characters("Test\x1b[5;5fing"), "Test5fing"); } #[test] @@ -537,5 +591,4 @@ mod test { - -testing"; assert_eq!(word_wrap(unwrapped, |_| 10), wrapped); } - } diff --git a/ansi_macro/src/lib.rs b/ansi_macro/src/lib.rs index f08ccd4..4b51420 100644 --- a/ansi_macro/src/lib.rs +++ b/ansi_macro/src/lib.rs @@ -1,19 +1,19 @@ -use proc_macro::TokenStream; -use syn::{parse_macro_input, Lit}; -use quote::ToTokens; use nom::{ + branch::alt, + bytes::complete::{tag, take_till, take_till1}, combinator::eof, - branch::alt, multi::fold_many0, - bytes::complete::{take_till, take_till1, tag}, - sequence::{tuple, pair}, error::Error, - Err, - Parser + multi::fold_many0, + sequence::{pair, tuple}, + Err, Parser, }; +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::{parse_macro_input, Lit}; enum AnsiFrag<'l> { Lit(&'l str), - Special(&'l str) + Special(&'l str), } use AnsiFrag::Special; @@ -21,44 +21,53 @@ use AnsiFrag::Special; 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"), }; fn parser(i: &str) -> Result>> { - 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) + 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()) + TokenStream::from( + parser(&raw) + .unwrap_or_else(|e| panic!("Bad ansi literal: {}", e)) + .into_token_stream(), + ) } diff --git a/blastmud_game/build.rs b/blastmud_game/build.rs index 7691571..f910036 100644 --- a/blastmud_game/build.rs +++ b/blastmud_game/build.rs @@ -2,8 +2,12 @@ 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")); + .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") + ); } diff --git a/blastmud_game/rustfmt.toml b/blastmud_game/rustfmt.toml new file mode 100644 index 0000000..3a26366 --- /dev/null +++ b/blastmud_game/rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" diff --git a/blastmud_game/src/av.rs b/blastmud_game/src/av.rs index 241e474..9962846 100644 --- a/blastmud_game/src/av.rs +++ b/blastmud_game/src/av.rs @@ -1,9 +1,9 @@ -use std::fs; -use std::error::Error; -use serde::Deserialize; -use ring::signature; -use base64; use crate::DResult; +use base64; +use ring::signature; +use serde::Deserialize; +use std::error::Error; +pub(crate) use std::fs; #[derive(Deserialize)] struct AV { @@ -11,20 +11,20 @@ struct AV { serial: u64, cn: String, assertion: String, - sig: 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 +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)?; + let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?) + .map_err(|error| Box::new(error) as Box)?; 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::::from("Invalid age-verification.yml"))?; diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 25ed0fe..96e0e71 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -21,6 +21,7 @@ pub mod close; pub mod corp; pub mod cut; pub mod drop; +mod gear; pub mod get; mod describe; mod help; @@ -137,6 +138,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "corp" => corp::VERB, "cut" => cut::VERB, "drop" => drop::VERB, + "gear" => gear::VERB, "get" => get::VERB, "install" => install::VERB, "inventory" => inventory::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/gear.rs b/blastmud_game/src/message_handler/user_commands/gear.rs new file mode 100644 index 0000000..8ba7686 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/gear.rs @@ -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!( + "| {:6} | {:25} | {:5} | {:5} | {:5} | {:5} | {:5} |\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 = 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!("| {:6} | {: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; diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index ce7943e..8db5362 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -1,46 +1,59 @@ -use serde::{Serialize, Deserialize}; use crate::{ - models::item::{SkillType, Item, Pronouns}, - models::consent::ConsentType, message_handler::user_commands::{UResult, VerbContext}, - static_content::{ - room::Direction, - species::BodyPart, - }, + models::consent::ConsentType, + models::item::{Item, Pronouns, SkillType}, + 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 once_cell::sync::OnceCell; +use rand::seq::SliceRandom; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -mod fangs; -mod whip; mod blade; -mod trauma_kit; mod corp_licence; -mod lock; -mod meat; +mod fangs; pub mod head_armour; -pub mod torso_armour; +mod lock; pub mod lower_armour; +mod meat; +pub mod torso_armour; +mod trauma_kit; +mod whip; -pub type AttackMessageChoice = Vec String + 'static + Sync + Send>>; -pub type AttackMessageChoicePart = Vec String + 'static + Sync + Send>>; +pub type AttackMessageChoice = + Vec String + 'static + Sync + Send>>; +pub type AttackMessageChoicePart = + Vec String + 'static + Sync + Send>>; pub struct SkillScaling { pub skill: SkillType, pub min_skill: f64, - pub mean_damage_per_point_over_min: f64 + pub mean_damage_per_point_over_min: f64, } #[allow(unused)] -#[derive(Eq,Ord,Clone,PartialEq,PartialOrd)] +#[derive(Eq, Ord, Clone, PartialEq, PartialOrd)] pub enum DamageType { Beat, Slash, Pierce, 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 { @@ -56,24 +69,27 @@ pub struct WeaponAttackData { impl Default for WeaponAttackData { fn default() -> Self { Self { - start_messages: - vec!(Box::new(|attacker, victim, exp| format!( - "{} makes an attack on {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp,1, false)))), - success_messages: - vec!(Box::new(|attacker, victim, part, exp| - format!("{}'s attack on {} hits {} {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &victim.pronouns.possessive, - part.display(victim.sex.clone()) - ))), + start_messages: vec![Box::new(|attacker, victim, exp| { + format!( + "{} makes an attack on {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false) + ) + })], + success_messages: vec![Box::new(|attacker, victim, part, exp| { + format!( + "{}'s attack on {} hits {} {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &victim.pronouns.possessive, + part.display(victim.sex.clone()) + ) + })], mean_damage: 1.0, stdev_damage: 2.0, base_damage_type: DamageType::Slash, - other_damage_types: vec!(), - skill_scaling: vec!() + other_damage_types: vec![], + skill_scaling: vec![], } } } @@ -114,10 +130,17 @@ impl Default for ChargeData { pub enum UseEffect { // messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message. - BroadcastMessage { messagef: Box (String, String) + Sync + Send>}, + BroadcastMessage { + messagef: Box (String, String) + Sync + Send>, + }, // skill_multiplier is always positive - sign flipped for crit fails. - ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64, - max_effect: i64, message: Box (String, String) + Sync + Send> }, + ChangeTargetHealth { + delay_secs: u64, + base_effect: i64, + skill_multiplier: f64, + max_effect: i64, + message: Box (String, String) + Sync + Send>, + }, } pub struct UseData { @@ -136,9 +159,9 @@ impl Default for UseData { Self { uses_skill: SkillType::Medic, diff_level: 10.0, - crit_fail_effects: vec!(), - fail_effects: vec!(), - success_effects: vec!(), + crit_fail_effects: vec![], + fail_effects: vec![], + success_effects: vec![], errorf: Box::new(|_it, _target| None), task_ref: "set me", needs_consent_check: None, @@ -161,7 +184,13 @@ pub struct WearData { #[async_trait] 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] @@ -171,15 +200,29 @@ pub trait ArglessHandler { #[async_trait] pub trait InstallHandler { - async fn install_cmd(&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<()>; + async fn install_cmd( + &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 weapon_data: Option, pub display: &'static str, pub display_less_explicit: Option<&'static str>, - pub details: &'static str, + pub details: &'static str, pub details_less_explicit: Option<&'static str>, pub aliases: Vec<&'static str>, pub max_health: u64, @@ -192,7 +235,7 @@ pub struct PossessionData { pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub can_butcher: bool, - pub wear_data: Option + pub wear_data: Option, } impl Default for PossessionData { @@ -203,7 +246,7 @@ impl Default for PossessionData { display_less_explicit: None, details: "A generic looking thing", details_less_explicit: None, - aliases: vec!(), + aliases: vec![], max_health: 10, weight: 100, charge_data: None, @@ -220,26 +263,27 @@ impl Default for PossessionData { } impl WeaponAttackData { - pub fn start_message( - &self, - attacker: &Item, victim: &Item, explicit_ok: bool) -> String { + pub fn start_message(&self, attacker: &Item, victim: &Item, explicit_ok: bool) -> String { let mut rng = rand::thread_rng(); - self.start_messages[..].choose(&mut rng).map( - |f| f(attacker, victim, explicit_ok)).unwrap_or( - "No message defined yet".to_owned()) + self.start_messages[..] + .choose(&mut rng) + .map(|f| f(attacker, victim, explicit_ok)) + .unwrap_or("No message defined yet".to_owned()) } pub fn success_message( - &self, attacker: &Item, victim: &Item, - part: &BodyPart, explicit_ok: bool + &self, + attacker: &Item, + victim: &Item, + part: &BodyPart, + explicit_ok: bool, ) -> String { let mut rng = rand::thread_rng(); - self.success_messages[..].choose(&mut rng).map( - |f| f(attacker, victim, part, explicit_ok)).unwrap_or( - "No message defined yet".to_owned()) + self.success_messages[..] + .choose(&mut rng) + .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)] @@ -268,7 +312,6 @@ pub enum PossessionType { Steak, AnimalSkin, SeveredHead, - } impl Into for PossessionType { @@ -281,15 +324,22 @@ impl Into for PossessionType { display_less_explicit: possession_dat.display_less_explicit.map(|d| d.to_owned()), details: Some(possession_dat.details.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, weight: possession_dat.weight, pronouns: Pronouns { is_proper: false, ..Pronouns::default_inanimate() }, - charges: possession_dat.charge_data.as_ref() - .map(|cd| cd.max_charges).unwrap_or(0), + charges: possession_dat + .charge_data + .as_ref() + .map(|cd| cd.max_charges) + .unwrap_or(0), ..Default::default() } } @@ -297,53 +347,65 @@ impl Into for PossessionType { pub fn fist() -> &'static WeaponData { static FIST_WEAPON: OnceCell = OnceCell::new(); - FIST_WEAPON.get_or_init(|| { - WeaponData { - uses_skill: SkillType::Fists, - raw_min_to_learn: 0.0, - raw_max_to_learn: 2.0, - normal_attack: WeaponAttackData { - start_messages: vec!( - Box::new(|attacker, victim, exp| - format!("{} swings at {} with {} fists", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &attacker.pronouns.possessive - ) - ) - ), - success_messages: vec!( - Box::new(|attacker, victim, part, exp| - format!("{}'s fists smash into {}'s {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &part.display(victim.sex.clone()) - ) - ) - ), - ..Default::default() - }, + FIST_WEAPON.get_or_init(|| WeaponData { + uses_skill: SkillType::Fists, + raw_min_to_learn: 0.0, + raw_max_to_learn: 2.0, + normal_attack: WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim, exp| { + format!( + "{} swings at {} with {} fists", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &attacker.pronouns.possessive + ) + })], + success_messages: vec![Box::new(|attacker, victim, part, exp| { + format!( + "{}'s fists smash into {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + })], ..Default::default() - } + }, + ..Default::default() }) } pub fn possession_data() -> &'static BTreeMap { - static POSSESSION_DATA: OnceCell> = OnceCell::new(); + static POSSESSION_DATA: OnceCell> = + OnceCell::new(); use PossessionType::*; &POSSESSION_DATA.get_or_init(|| { - vec!( - (Fangs, fangs::data()) - ).into_iter() + vec![(Fangs, fangs::data())] + .into_iter() .chain(whip::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(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(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) - .chain(head_armour::data().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))) + .chain( + head_armour::data() + .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() }) } @@ -351,8 +413,15 @@ pub fn possession_data() -> &'static BTreeMap &'static Vec { static RELEVANT: OnceCell> = OnceCell::new(); &RELEVANT.get_or_init(|| { - possession_data().iter() - .filter_map(|(pt, pd)| if pd.can_butcher { Some(pt.clone()) } else { None }) + possession_data() + .iter() + .filter_map(|(pt, pd)| { + if pd.can_butcher { + Some(pt.clone()) + } else { + None + } + }) .collect() }) } @@ -364,7 +433,12 @@ mod tests { fn other_damage_types_add_to_less_than_one() { for (_pt, pd) in possession_data().iter() { 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 < 1.0); } diff --git a/blastmud_game/src/static_content/possession_type/blade.rs b/blastmud_game/src/static_content/possession_type/blade.rs index 71ecd0e..96dc5f1 100644 --- a/blastmud_game/src/static_content/possession_type/blade.rs +++ b/blastmud_game/src/static_content/possession_type/blade.rs @@ -1,10 +1,9 @@ -use super::{PossessionData, WeaponData, WeaponAttackData, PossessionType}; +use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData}; use crate::models::item::SkillType; use once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (PossessionType::ButcherKnife, PossessionData { diff --git a/blastmud_game/src/static_content/possession_type/corp_licence.rs b/blastmud_game/src/static_content/possession_type/corp_licence.rs index ea9a6bf..ba1dec3 100644 --- a/blastmud_game/src/static_content/possession_type/corp_licence.rs +++ b/blastmud_game/src/static_content/possession_type/corp_licence.rs @@ -1,16 +1,12 @@ -use super::{PossessionData, PossessionType, WriteHandler, ArglessHandler, possession_data}; +use super::{possession_data, ArglessHandler, PossessionData, PossessionType, WriteHandler}; use crate::{ - models::{ - item::{Item, ItemSpecialData}, - corp::{Corp, CorpMembership, CorpPermission}, - }, message_handler::user_commands::{ - register::is_invalid_username, - parsing::parse_username, - user_error, - UResult, - CommandHandlingError::UserError, - VerbContext, + parsing::parse_username, register::is_invalid_username, user_error, + CommandHandlingError::UserError, UResult, VerbContext, + }, + models::{ + corp::{Corp, CorpMembership, CorpPermission}, + item::{Item, ItemSpecialData}, }, services::comms::broadcast_to_room, }; @@ -21,17 +17,23 @@ use once_cell::sync::OnceCell; use super::PossessionType::*; -pub struct CorpLicenceHandler { -} +pub struct CorpLicenceHandler {} #[async_trait] 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) { Err(e) => user_error("Invalid corp name: ".to_owned() + e)?, - Ok((_, rest)) if rest != "" => - user_error("No spaces allowed in corp names!".to_owned())?, - Ok((name, _)) => name + Ok((_, rest)) if rest != "" => { + user_error("No spaces allowed in corp names!".to_owned())? + } + Ok((name, _)) => name, }; if is_invalid_username(name) { 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(); 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.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 \ pencil and write \"{}\" on it. [Hint: Try the use command to submit \ your signed paperwork and register the corporation, or write again \ - to erase and change the name].\n"), name))).await?; + to erase and change the name].\n"), + name + )), + ) + .await?; Ok(()) } @@ -63,7 +73,7 @@ impl ArglessHandler for CorpLicenceHandler { async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> { let name = match what.special_data.as_ref() { 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() { user_error("Corp name clashes with existing user name".to_owned())?; @@ -72,44 +82,64 @@ impl ArglessHandler for CorpLicenceHandler { 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())?; } - - broadcast_to_room(ctx.trans, &player.location, None, - &format!( - "{} signs a contract establishing {} as a corp\n", - &player.display_for_sentence(true, 1, true), - name - ), - Some( - &format!("{} signs a contract establishing {} as a corp\n", - &player.display_for_sentence(false, 1, true), - name - ) - )).await?; - let corp_id = ctx.trans.create_corp(&Corp { - name: name.to_owned(), - ..Default::default() - }).await?; - ctx.trans.upsert_corp_membership( - &corp_id, &player.item_code, - &CorpMembership { - joined_at: Some(Utc::now()), - permissions: vec!(CorpPermission::Holder), - allow_combat: true, - job_title: "Founder".to_owned(), + + broadcast_to_room( + ctx.trans, + &player.location, + None, + &format!( + "{} signs a contract establishing {} as a corp\n", + &player.display_for_sentence(true, 1, true), + name + ), + Some(&format!( + "{} signs a contract establishing {} as a corp\n", + &player.display_for_sentence(false, 1, true), + name + )), + ) + .await?; + let corp_id = ctx + .trans + .create_corp(&Corp { + name: name.to_owned(), ..Default::default() - }).await?; + }) + .await?; + ctx.trans + .upsert_corp_membership( + &corp_id, + &player.item_code, + &CorpMembership { + joined_at: Some(Utc::now()), + permissions: vec![CorpPermission::Holder], + allow_combat: true, + job_title: "Founder".to_owned(), + ..Default::default() + }, + ) + .await?; let mut what_mut = what.clone(); what_mut.possession_type = Some(CertificateOfIncorporation); - let cp_data = possession_data().get(&CertificateOfIncorporation) - .ok_or_else(|| UserError("Certificate of Incorporation no longer exists as an item".to_owned()))?; + let cp_data = possession_data() + .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.details = Some(cp_data.details.to_owned()); ctx.trans.save_item_model(&what_mut).await?; - + Ok(()) } } @@ -117,8 +147,7 @@ impl ArglessHandler for CorpLicenceHandler { static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {}; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (NewCorpLicence, PossessionData { diff --git a/blastmud_game/src/static_content/possession_type/fangs.rs b/blastmud_game/src/static_content/possession_type/fangs.rs index 10a373c..d3a82be 100644 --- a/blastmud_game/src/static_content/possession_type/fangs.rs +++ b/blastmud_game/src/static_content/possession_type/fangs.rs @@ -1,40 +1,35 @@ -use super::{PossessionData, WeaponData, WeaponAttackData}; +use super::{PossessionData, WeaponAttackData, WeaponData}; use crate::models::item::SkillType; use once_cell::sync::OnceCell; pub fn data() -> &'static PossessionData { static D: OnceCell = OnceCell::new(); - D.get_or_init( - || - PossessionData { - weapon_data: Some(WeaponData { - uses_skill: SkillType::Fists, - raw_min_to_learn: 0.0, - raw_max_to_learn: 2.0, - normal_attack: WeaponAttackData { - start_messages: vec!( - Box::new(|attacker, victim, exp| - format!("{} bares {} teeth and lunges at {}", - &attacker.display_for_sentence(exp, 1, true), - &attacker.pronouns.possessive, - &victim.display_for_sentence(exp, 1, false), - ) - ) - ), - success_messages: vec!( - Box::new(|attacker, victim, part, exp| - format!("{}'s teeth connect and tear at the flesh of {}'s {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &part.display(victim.sex.clone()) - ) - ) - ), - ..Default::default() - }, - ..Default::default() - }), - ..Default::default() - } - ) + D.get_or_init(|| PossessionData { + weapon_data: Some(WeaponData { + uses_skill: SkillType::Fists, + raw_min_to_learn: 0.0, + raw_max_to_learn: 2.0, + normal_attack: WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim, exp| { + format!( + "{} bares {} teeth and lunges at {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part, exp| { + format!( + "{}'s teeth connect and tear at the flesh of {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + })], + ..Default::default() + }, + ..Default::default() + }), + ..Default::default() + }) } diff --git a/blastmud_game/src/static_content/possession_type/head_armour.rs b/blastmud_game/src/static_content/possession_type/head_armour.rs index 20c1f1a..23c9db7 100644 --- a/blastmud_game/src/static_content/possession_type/head_armour.rs +++ b/blastmud_game/src/static_content/possession_type/head_armour.rs @@ -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 once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( ( PossessionType::RustyMetalPot, diff --git a/blastmud_game/src/static_content/possession_type/lock.rs b/blastmud_game/src/static_content/possession_type/lock.rs index 63650bb..6b22ceb 100644 --- a/blastmud_game/src/static_content/possession_type/lock.rs +++ b/blastmud_game/src/static_content/possession_type/lock.rs @@ -1,13 +1,12 @@ -use super::{PossessionData, ArglessHandler, PossessionType}; +use super::{ArglessHandler, PossessionData, PossessionType}; use crate::{ + message_handler::user_commands::{user_error, UResult, VerbContext}, models::item::{Item, LocationActionType}, - message_handler::user_commands::{user_error, VerbContext, UResult}, - static_content::{ - possession_type::InstallHandler, - room::Direction, + services::{ + capacity::{check_item_capacity, CapacityLevel}, + comms::broadcast_to_room, }, - services::{comms::broadcast_to_room, - capacity::{check_item_capacity, CapacityLevel}} + static_content::{possession_type::InstallHandler, room::Direction}, }; use async_trait::async_trait; use once_cell::sync::OnceCell; @@ -32,23 +31,34 @@ static LOCK_WEIGHT: u64 = 500; struct ScanLockInstall; #[async_trait] impl InstallHandler for ScanLockInstall { - async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, - direction: &Direction) -> UResult<()> { + async fn install_cmd( + &self, + ctx: &mut VerbContext, + player: &Item, + what: &Item, + room: &Item, + direction: &Direction, + ) -> UResult<()> { if what.action_type != LocationActionType::Normal { user_error("That scanlock is already in use.".to_owned())?; } - if !ctx.trans.find_by_action_and_location( - &room.refstr(), - &LocationActionType::InstalledOnDoorAsLock(direction.clone()) - ).await?.is_empty() { + if !ctx + .trans + .find_by_action_and_location( + &room.refstr(), + &LocationActionType::InstalledOnDoorAsLock(direction.clone()), + ) + .await? + .is_empty() + { 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? { - CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => - user_error("That room has so much stuff, you can't install anything new." - .to_owned())?, + CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => user_error( + "That room has so much stuff, you can't install anything new.".to_owned(), + )?, _ => {} }; let mut what_mut = (*what).clone(); @@ -56,67 +66,82 @@ impl InstallHandler for ScanLockInstall { what_mut.action_type = LocationActionType::InstalledOnDoorAsLock(direction.clone()); what_mut.owner = room.owner.clone(); ctx.trans.save_item_model(&what_mut).await?; - + broadcast_to_room( - &ctx.trans, &room.refstr(), None, - &format!("{} bangs the door to the {} as he installs {} on it.\n", - &player.display_for_sentence(true, 1, true), - &direction.describe(), - &what.display_for_sentence(true, 1, false)), - Some( - &format!("{} bangs the door to the {} as he installs {} on it.\n", - &player.display_for_sentence(false, 1, true), - &direction.describe(), - &what.display_for_sentence(false, 1, false)), - )).await?; + &ctx.trans, + &room.refstr(), + None, + &format!( + "{} bangs the door to the {} as he installs {} on it.\n", + &player.display_for_sentence(true, 1, true), + &direction.describe(), + &what.display_for_sentence(true, 1, false) + ), + Some(&format!( + "{} bangs the door to the {} as he installs {} on it.\n", + &player.display_for_sentence(false, 1, true), + &direction.describe(), + &what.display_for_sentence(false, 1, false) + )), + ) + .await?; Ok(()) } - async fn uninstall_cmd(&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<()> { if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) { user_error("That scanlock is not installed as a lock.".to_owned())?; } - + let mut what_mut = (*what).clone(); - let extra_text = match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? { - CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => { - ", dropping it on the floor since he can't hold it." - }, - _ => { - what_mut.location = player.refstr(); - "" - } - }; + let extra_text = + match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? { + CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => { + ", dropping it on the floor since he can't hold it." + } + _ => { + what_mut.location = player.refstr(); + "" + } + }; what_mut.action_type = LocationActionType::Normal; what_mut.owner = None; ctx.trans.save_item_model(&what_mut).await?; - + broadcast_to_room( - &ctx.trans, &room.refstr(), None, - &format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", - &player.display_for_sentence(true, 1, true), - &direction.describe(), - &what.display_for_sentence(true, 1, false), - extra_text + &ctx.trans, + &room.refstr(), + None, + &format!( + "{} bangs the door to the {} as he uninstalls {} from it{}.\n", + &player.display_for_sentence(true, 1, true), + &direction.describe(), + &what.display_for_sentence(true, 1, false), + extra_text ), - Some( - &format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", - &player.display_for_sentence(false, 1, true), - &direction.describe(), - &what.display_for_sentence(false, 1, false), - extra_text - ), - )).await?; + Some(&format!( + "{} bangs the door to the {} as he uninstalls {} from it{}.\n", + &player.display_for_sentence(false, 1, true), + &direction.describe(), + &what.display_for_sentence(false, 1, false), + extra_text + )), + ) + .await?; Ok(()) } } - pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (PossessionType::Scanlock, PossessionData { diff --git a/blastmud_game/src/static_content/possession_type/lower_armour.rs b/blastmud_game/src/static_content/possession_type/lower_armour.rs index 72f6b73..ed23ef6 100644 --- a/blastmud_game/src/static_content/possession_type/lower_armour.rs +++ b/blastmud_game/src/static_content/possession_type/lower_armour.rs @@ -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 once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( ( PossessionType::LeatherPants, diff --git a/blastmud_game/src/static_content/possession_type/meat.rs b/blastmud_game/src/static_content/possession_type/meat.rs index 9167f19..30df882 100644 --- a/blastmud_game/src/static_content/possession_type/meat.rs +++ b/blastmud_game/src/static_content/possession_type/meat.rs @@ -2,8 +2,7 @@ use super::{PossessionData, PossessionType}; use once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (PossessionType::AnimalSkin, PossessionData { diff --git a/blastmud_game/src/static_content/possession_type/torso_armour.rs b/blastmud_game/src/static_content/possession_type/torso_armour.rs index e88e6b8..c92c4ef 100644 --- a/blastmud_game/src/static_content/possession_type/torso_armour.rs +++ b/blastmud_game/src/static_content/possession_type/torso_armour.rs @@ -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 once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( ( PossessionType::LeatherJacket, diff --git a/blastmud_game/src/static_content/possession_type/trauma_kit.rs b/blastmud_game/src/static_content/possession_type/trauma_kit.rs index f4a276c..aaeac13 100644 --- a/blastmud_game/src/static_content/possession_type/trauma_kit.rs +++ b/blastmud_game/src/static_content/possession_type/trauma_kit.rs @@ -1,15 +1,11 @@ -use super::{PossessionData, PossessionType, UseData, UseEffect, ChargeData}; -use crate::models::{ - item::SkillType, - consent::ConsentType, -}; +use super::{ChargeData, PossessionData, PossessionType, UseData, UseEffect}; +use crate::models::{consent::ConsentType, item::SkillType}; use once_cell::sync::OnceCell; use super::PossessionType::*; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (MediumTraumaKit, PossessionData { diff --git a/blastmud_game/src/static_content/possession_type/whip.rs b/blastmud_game/src/static_content/possession_type/whip.rs index 97b5603..13d6280 100644 --- a/blastmud_game/src/static_content/possession_type/whip.rs +++ b/blastmud_game/src/static_content/possession_type/whip.rs @@ -1,10 +1,9 @@ -use super::{PossessionData, PossessionType, WeaponData, WeaponAttackData}; +use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData}; use crate::models::item::SkillType; use once_cell::sync::OnceCell; pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { - static D: OnceCell> = - OnceCell::new(); + static D: OnceCell> = OnceCell::new(); &D.get_or_init(|| vec!( (PossessionType::AntennaWhip, PossessionData { diff --git a/blastmud_interfaces/src/lib.rs b/blastmud_interfaces/src/lib.rs index 393c4cb..d3d98cf 100644 --- a/blastmud_interfaces/src/lib.rs +++ b/blastmud_interfaces/src/lib.rs @@ -1,13 +1,13 @@ -use uuid::Uuid; use serde::*; - +use uuid::Uuid; + #[derive(Serialize, Deserialize, Clone, Debug)] pub enum MessageFromListener { ListenerPing { uuid: Uuid }, SessionConnected { session: Uuid, source: String }, SessionDisconnected { session: Uuid }, SessionSentLine { session: Uuid, msg: String }, - AcknowledgeMessage + AcknowledgeMessage, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -15,5 +15,5 @@ pub enum MessageToListener { GameserverVersion { version: String }, DisconnectSession { session: Uuid }, SendToSession { session: Uuid, msg: String }, - AcknowledgeMessage + AcknowledgeMessage, } diff --git a/blastmud_listener/build.rs b/blastmud_listener/build.rs index 7691571..f910036 100644 --- a/blastmud_listener/build.rs +++ b/blastmud_listener/build.rs @@ -2,8 +2,12 @@ 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")); + .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") + ); } diff --git a/blastmud_listener/src/main.rs b/blastmud_listener/src/main.rs index 0f577be..23f2c9a 100644 --- a/blastmud_listener/src/main.rs +++ b/blastmud_listener/src/main.rs @@ -1,31 +1,32 @@ -use std::vec::Vec; -use std::path::Path; +use blastmud_interfaces::*; +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::error::Error; -use std::net::SocketAddr; use std::fs; -use serde::*; -use tokio::task; -use tokio::time::{self, Duration}; -use tokio::net::{TcpStream, TcpListener, lookup_host}; +use std::net::SocketAddr; +use std::path::Path; +use std::sync::Arc; +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::sync::{mpsc, Mutex, RwLock}; -use tokio::io::{BufReader, AsyncWriteExt}; -use log::{warn, info, LevelFilter}; -use simple_logger::SimpleLogger; -use std::sync::Arc; -use blastmud_interfaces::*; +use tokio::task; +use tokio::time::{self, Duration}; +use tokio_serde::formats::Cbor; +use tokio_stream::wrappers::ReceiverStream; use tokio_util::codec; use tokio_util::codec::length_delimited::LengthDelimitedCodec; -use tokio_serde::formats::Cbor; -use futures::prelude::*; use uuid::Uuid; -use tokio_stream::wrappers::ReceiverStream; -use warp::{ - self, filters::ws, Filter, Reply -}; -use std::time::Instant; -use nix::{sys::signal::{kill, Signal}, unistd::{Pid, getpid}}; +use warp::{self, filters::ws, Filter, Reply}; #[derive(Deserialize, Debug)] struct Config { @@ -38,14 +39,14 @@ struct Config { type DResult = Result>; fn read_latest_config() -> DResult { - serde_yaml::from_str(&fs::read_to_string("listener.conf")?). - map_err(|error| Box::new(error) as Box) + serde_yaml::from_str(&fs::read_to_string("listener.conf")?) + .map_err(|error| Box::new(error) as Box) } #[derive(Debug, Clone)] enum ServerTaskCommand { SwitchTo { new_server: String }, - Send { message: MessageFromListener } + Send { message: MessageFromListener }, } fn run_server_task( @@ -54,33 +55,32 @@ fn run_server_task( mut receiver: ReceiverStream, sender: mpsc::Sender, server: String, - message_handler: FHandler -) -where + message_handler: FHandler, +) where FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static, - HandlerFut: Future + Send + 'static + HandlerFut: Future + Send + 'static, { task::spawn(async move { let conn = loop { match TcpStream::connect(&server).await { Err(e) => warn!("Can't connect to {}: {}", server, e), - Ok(c) => break c + Ok(c) => break c, } time::sleep(Duration::from_secs(1)).await; }; let mut conn_framed = tokio_serde::Framed::new( codec::Framed::new(conn, LengthDelimitedCodec::new()), - Cbor::::default() + Cbor::::default(), ); - let mut commands = stream::iter(vec!( - ServerTaskCommand::Send { - message: MessageFromListener::ListenerPing { uuid: listener_id } - }) - ).chain( - stream::iter(unfinished_business.map(|message| ServerTaskCommand::Send { message })) - ).chain(&mut receiver); - + let mut commands = stream::iter(vec![ServerTaskCommand::Send { + message: MessageFromListener::ListenerPing { uuid: listener_id }, + }]) + .chain(stream::iter( + unfinished_business.map(|message| ServerTaskCommand::Send { message }), + )) + .chain(&mut receiver); + 'full_select: loop { tokio::select!( req = conn_framed.try_next() => { @@ -116,7 +116,7 @@ where Ok(Some(msg)) => { let mhfut = message_handler(msg); mhfut.await; - + match conn_framed.send(MessageFromListener::AcknowledgeMessage).await { Ok(_) => {} Err(e) => { @@ -151,7 +151,7 @@ where ); break 'full_select; } - Ok(None) => { + Ok(None) => { warn!("Got connection closed from {}", server); run_server_task( Some(message), @@ -169,7 +169,7 @@ where Ok(Some(msg)) => { let mhfut = message_handler(msg); mhfut.await; - + match conn_framed.send(MessageFromListener::AcknowledgeMessage).await { Ok(_) => {} Err(e) => { @@ -180,7 +180,7 @@ where } } } - + } } } @@ -216,24 +216,22 @@ where } ); } - }); - } enum SessionCommand { Disconnect, - SendString { message : String } + SendString { message: String }, } struct SessionRecord { channel: mpsc::Sender, - disconnect_channel: mpsc::UnboundedSender<()> + disconnect_channel: mpsc::UnboundedSender<()>, } struct SessionIndexes { by_uuid: BTreeMap, - count_by_source: BTreeMap + count_by_source: BTreeMap, } type SessionMap = Arc>; @@ -244,19 +242,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen MessageToListener::GameserverVersion { version } => { let mut version_mut = version_data().write().await; (*version_mut).gameserver_version = Some(version.clone()); - }, + } MessageToListener::DisconnectSession { session } => { match session_map.lock().await.by_uuid.get(&session) { // Just silently ignore it if they are disconnected. None => {} - Some(SessionRecord { channel, disconnect_channel, .. }) => { - match channel.try_send(SessionCommand::Disconnect) { - Err(mpsc::error::TrySendError::Full(_)) => { - disconnect_channel.send(()).unwrap_or(()); - } - _ => {} + Some(SessionRecord { + channel, + disconnect_channel, + .. + }) => match channel.try_send(SessionCommand::Disconnect) { + Err(mpsc::error::TrySendError::Full(_)) => { + disconnect_channel.send(()).unwrap_or(()); } - } + _ => {} + }, } } 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. None => {} Some(SessionRecord { channel, .. }) => { - channel.try_send(SessionCommand::SendString { message: msg }) + channel + .try_send(SessionCommand::SendString { message: msg }) .unwrap_or(()); } } @@ -272,14 +273,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen } } -fn start_server_task(listener_id: Uuid, - server: String, - session_map: SessionMap) -> mpsc::Sender { +fn start_server_task( + listener_id: Uuid, + server: String, + session_map: SessionMap, +) -> mpsc::Sender { let (sender, receiver) = mpsc::channel(20); let receiver_stream = ReceiverStream::new(receiver); - run_server_task(None, listener_id, receiver_stream, sender.clone(), server, - move |msg| handle_server_message(session_map.clone(), - msg) ); + run_server_task( + None, + listener_id, + receiver_stream, + sender.clone(), + server, + move |msg| handle_server_message(session_map.clone(), msg), + ); sender } @@ -299,14 +307,14 @@ impl TokenBucket { level: initial_level, last_topup: Instant::now(), max_level, - alloc_per_ms + alloc_per_ms, } } pub fn update(self: &mut Self) { - self.level = - (self.level + self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64)) - .min(self.max_level); + self.level = (self.level + + self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64)) + .min(self.max_level); self.last_topup = Instant::now(); } @@ -317,11 +325,11 @@ impl TokenBucket { pub fn consume_minor(self: &mut Self) { self.level = self.level - 0.1; } - + pub fn nearly_empty(self: &Self) -> bool { self.level < 1.0 } - + pub fn has_capacity(self: &Self) -> bool { self.level > 0.0 } @@ -338,30 +346,35 @@ const MAX_CONNS_PER_IP: u64 = 5; async fn handle_client_socket( server: mpsc::Sender, - active_sessions: SessionMap, + active_sessions: SessionMap, mut stream: TcpStream, - addr: SocketAddr + addr: SocketAddr, ) { let (rstream, mut wstream) = stream.split(); let mut rbuf = codec::FramedRead::new( BufReader::new(rstream), - codec::LinesCodec::new_with_max_length(512) + codec::LinesCodec::new_with_max_length(512), ); let session = Uuid::new_v4(); let mut tok_bucket = TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS); info!("Accepted session {} from {}", session, addr); - let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY); let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel(); - + let mut sess_idx_lock = active_sessions.lock().await; let addr_str = addr.ip().to_string(); if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP { drop(sess_idx_lock); - info!("Rejecting session {} because of too many concurrent connections", session); - match wstream.write_all("Too many connections from same IP\r\n".as_bytes()).await { + info!( + "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) => { info!("Client connection {} got error {}", session, e); } @@ -369,18 +382,32 @@ async fn handle_client_socket( } 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( - session, SessionRecord { + session, + SessionRecord { channel: lsender.clone(), - disconnect_channel: discon_sender.clone() - }); + disconnect_channel: discon_sender.clone(), + }, + ); drop(sess_idx_lock); - - server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected { - session, source: addr.to_string() - }}).await.unwrap(); - + + server + .send(ServerTaskCommand::Send { + message: MessageFromListener::SessionConnected { + session, + source: addr.to_string(), + }, + }) + .await + .unwrap(); + 'client_loop: loop { tok_bucket.update(); tokio::select!( @@ -437,13 +464,21 @@ async fn handle_client_socket( ); } - server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected { - session - }}).await.unwrap(); - + server + .send(ServerTaskCommand::Send { + message: MessageFromListener::SessionDisconnected { session }, + }) + .await + .unwrap(); + sess_idx_lock = active_sessions.lock().await; 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 { sess_idx_lock.count_by_source.remove(&addr_str); } @@ -453,9 +488,12 @@ fn start_pinger(listener: Uuid, server: mpsc::Sender) { task::spawn(async move { loop { time::sleep(Duration::from_secs(60)).await; - server.send(ServerTaskCommand::Send { - message: MessageFromListener::ListenerPing { uuid: listener } - }).await.unwrap(); + server + .send(ServerTaskCommand::Send { + message: MessageFromListener::ListenerPing { uuid: listener }, + }) + .await + .unwrap(); } }); } @@ -464,10 +502,13 @@ async fn handle_websocket( mut ws: ws::WebSocket, src: String, active_sessions: SessionMap, - server: mpsc::Sender + server: mpsc::Sender, ) { 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 (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(); if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP { drop(sess_idx_lock); - info!("Rejecting session {} because of too many concurrent connections", session); - match ws.send(ws::Message::text("Too many connections from same IP\r\n")).await { + info!( + "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) => { info!("Client connection {} got error {}", session, e); } @@ -485,20 +532,34 @@ async fn handle_websocket( } 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( - session, SessionRecord { + session, + SessionRecord { channel: lsender.clone(), - disconnect_channel: discon_sender.clone() - }); + disconnect_channel: discon_sender.clone(), + }, + ); drop(sess_idx_lock); - - server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected { - session, source: src - }}).await.unwrap(); + + server + .send(ServerTaskCommand::Send { + message: MessageFromListener::SessionConnected { + session, + source: src, + }, + }) + .await + .unwrap(); let mut tok_bucket = TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS); - + 'client_loop: loop { tok_bucket.update(); tokio::select!( @@ -550,8 +611,8 @@ async fn handle_websocket( } tok_bucket.consume_minor(); } else { - tok_bucket.consume(); - + tok_bucket.consume(); + server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionSentLine { session, @@ -576,27 +637,35 @@ async fn handle_websocket( ); } - server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected { - session - }}).await.unwrap(); - + server + .send(ServerTaskCommand::Send { + message: MessageFromListener::SessionDisconnected { session }, + }) + .await + .unwrap(); + sess_idx_lock = active_sessions.lock().await; 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 { sess_idx_lock.count_by_source.remove(&addr_str); } } -async fn upgrade_websocket(src: String, wsreq: ws::Ws, - active_sessions: SessionMap, - server_sender: mpsc::Sender) -> - Result { - Ok( - wsreq.on_upgrade(|wss| handle_websocket( - wss, src, active_sessions, - server_sender)).into_response() - ) +async fn upgrade_websocket( + src: String, + wsreq: ws::Ws, + active_sessions: SessionMap, + server_sender: mpsc::Sender, +) -> Result { + Ok(wsreq + .on_upgrade(|wss| handle_websocket(wss, src, active_sessions, server_sender)) + .into_response()) } #[derive(Serialize)] @@ -620,44 +689,52 @@ async fn respond_version() -> Result { Ok(warp::reply::json(&*version_data().read().await).into_response()) } -async fn start_websocket(bind: String, active_sessions: SessionMap, server_sender: mpsc::Sender) -> DResult<()> { - let sockaddr = lookup_host(bind).await?.next().expect("Can't resolve websocket bind name"); - let routes = - warp::get() +async fn start_websocket( + bind: String, + active_sessions: SessionMap, + server_sender: mpsc::Sender, +) -> 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::header("X-Forwarded-For")) .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() .and(warp::path("version")) .and_then(|| respond_version())); - task::spawn( - warp::serve( - routes - ).run(sockaddr) - ); + task::spawn(warp::serve(routes).run(sockaddr)); Ok(()) } pub fn replace_old_listener(pidfile: &str) -> DResult<()> { match fs::read_to_string(pidfile) { - Err(e) => + Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { info!("pidfile not found, assuming not already running"); Ok(()) - } else { + } else { info!("Error reading pidfile (other than NotFound): {}", e); - Err(Box::new(e) as Box::) + Err(Box::new(e) as Box) } + } Ok(f) => { - let pid: Pid = Pid::from_raw(f.parse().map_err(|e| Box::new(e) as Box::)?); + let pid: Pid = Pid::from_raw( + f.parse() + .map_err(|e| Box::new(e) as Box)?, + ); if pid == getpid() { info!("Pid in pidfile is me - ignoring"); return Ok(()); - } + } match fs::read_to_string(format!("/proc/{}/cmdline", pid)) { - Ok(content) => + Ok(content) => { if content.contains("blastmud_listener") { info!("pid in pidfile references blastmud_listener; starting cutover"); 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"); Ok(()) } + } Err(_) => { info!("Pid in pidfile is gone - ignoring pidfile"); Ok(()) @@ -675,81 +753,96 @@ pub fn replace_old_listener(pidfile: &str) -> DResult<()> { }?; info!("Writing new pidfile"); fs::write(Path::new(pidfile), format!("{}", std::process::id())) - .map_err(|e| Box::new(e) as Box::) + .map_err(|e| Box::new(e) as Box) } - #[tokio::main] async fn main() -> Result<(), Box> { - SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap(); - + SimpleLogger::new() + .with_level(LevelFilter::Info) + .init() + .unwrap(); let listener_id = Uuid::new_v4(); let mut config = read_latest_config()?; - let active_sessions: SessionMap = - Arc::new(Mutex::new(SessionIndexes { by_uuid: BTreeMap::new(), count_by_source: BTreeMap::new() })); + let active_sessions: SessionMap = Arc::new(Mutex::new(SessionIndexes { + by_uuid: BTreeMap::new(), + count_by_source: BTreeMap::new(), + })); replace_old_listener(&config.pidfile)?; let server_sender = start_server_task(listener_id, config.gameserver, active_sessions.clone()); start_pinger(listener_id, server_sender.clone()); // 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())?; - + loop { let mut listen_handles = Vec::new(); for listener in config.listeners.clone() { let server_sender_for_listener = server_sender.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 { - Err(e) => { warn!("Error listening to {}: {}", &listener, e); } - Ok(listensock) => { - loop { - match listensock.accept().await { - Err(e) => { warn!("Error accepting connection from {}: {}", - &listener, e); } - Ok((stream, addr)) => { - let server_sender_for_client = server_sender_for_listener.clone(); - let active_sessions_for_client = active_sessions_for_listener.clone(); - task::spawn(async move { - handle_client_socket(server_sender_for_client, - active_sessions_for_client, - stream, - addr - ).await; - }); } + Err(e) => { + warn!("Error listening to {}: {}", &listener, e); + } + Ok(listensock) => loop { + match listensock.accept().await { + Err(e) => { + warn!("Error accepting connection from {}: {}", &listener, e); + } + Ok((stream, addr)) => { + let server_sender_for_client = server_sender_for_listener.clone(); + let active_sessions_for_client = + active_sessions_for_listener.clone(); + task::spawn(async move { + handle_client_socket( + server_sender_for_client, + active_sessions_for_client, + stream, + addr, + ) + .await; + }); } } - } + }, } })); } - + sighups.recv().await; - + info!("Reloading configurations"); config = read_latest_config()?; - + // 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 // 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?; - + for handle in &listen_handles { handle.abort(); } } } - #[cfg(test)] mod tests { #[test] fn doesnt_stop_reading_at_max_capacity() { use crate::*; - assert!(MAX_CAPACITY > STOP_READING_CAPACITY); + assert!(MAX_CAPACITY > STOP_READING_CAPACITY); } } diff --git a/gear.rs b/gear.rs new file mode 100644 index 0000000..e69de29