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
config
docs/private
*~

View File

@ -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!("<reset>"));
}
if self.bold { buf.push_str(ansi!("<bold>")); }
if self.underline { buf.push_str(ansi!("<under>")); }
if self.strike { buf.push_str(ansi!("<strike>")); }
if self.bold {
buf.push_str(ansi!("<bold>"));
}
if self.underline {
buf.push_str(ansi!("<under>"));
}
if self.strike {
buf.push_str(ansi!("<strike>"));
}
if self.background != 0 {
buf.push_str(&format!("\x1b[{}m", 39 + self.background)); }
buf.push_str(&format!("\x1b[{}m", 39 + self.background));
}
if self.foreground != 0 {
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground)); }
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground));
}
buf
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
struct AnsiEvent<'l> (
AnsiParseToken<'l>,
Rc<AnsiState>
);
struct AnsiEvent<'l>(AnsiParseToken<'l>, Rc<AnsiState>);
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum AnsiParseToken<'l> {
@ -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<AnsiEvent<'l>> {
@ -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<F>(input: &str, limit: F) -> String
where F: Fn(usize) -> usize {
where
F: Fn(usize) -> usize,
{
let mut it_main = AnsiIterator::new(input);
let mut start_word = true;
let mut row: usize = 0;
@ -338,9 +380,12 @@ pub fn word_wrap<F>(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<F>(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<F>(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<F>(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<F>(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!("<red>hello<green>world")),
"helloworld");
"helloworld"
);
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n");
"helloworld\n"
);
assert_eq!(
strip_special_characters("hello\r\x07world\n"),
"helloworld\n");
assert_eq!(
strip_special_characters("Test\x1b[5;5fing"),
"Test5fing");
"helloworld\n"
);
assert_eq!(strip_special_characters("Test\x1b[5;5fing"), "Test5fing");
}
#[test]
fn limit_special_characters_strips_some_things() {
assert_eq!(limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
ansi!("a<bgred><green>b<bggreen><red>c<reset>d"));
assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
"Test5fing");
assert_eq!(
limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
ansi!("a<bgred><green>b<bggreen><red>c<reset>d")
);
assert_eq!(limit_special_characters("Test\x1b[5;5fing"), "Test5fing");
}
#[test]
@ -537,5 +591,4 @@ mod test {
- -testing";
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::{
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<String, Err<Error<&str>>> {
pair(fold_many0(
alt((
take_till1(|c| c == '<').map(AnsiFrag::Lit),
tuple((tag("<"), take_till(|c| c == '>'), tag(">"))).map(|t| AnsiFrag::Special(t.1))
)),
|| "".to_owned(),
|a, r| a + match r {
AnsiFrag::Lit(s) => &s,
Special(s) if s == "reset" => "\x1b[0m",
Special(s) if s == "bold" => "\x1b[1m",
Special(s) if s == "under" => "\x1b[4m",
Special(s) if s == "strike" => "\x1b[9m",
Special(s) if s == "nounder" => "\x1b[24m",
Special(s) if s == "black" => "\x1b[30m",
Special(s) if s == "red" => "\x1b[31m",
Special(s) if s == "green" => "\x1b[32m",
Special(s) if s == "yellow" => "\x1b[33m",
Special(s) if s == "blue" => "\x1b[34m",
Special(s) if s == "magenta" => "\x1b[35m",
Special(s) if s == "cyan" => "\x1b[36m",
Special(s) if s == "white" => "\x1b[37m",
Special(s) if s == "bgblack" => "\x1b[40m",
Special(s) if s == "bgred" => "\x1b[41m",
Special(s) if s == "bggreen" => "\x1b[42m",
Special(s) if s == "bgyellow" => "\x1b[43m",
Special(s) if s == "bgblue" => "\x1b[44m",
Special(s) if s == "bgmagenta" => "\x1b[45m",
Special(s) if s == "bgcyan" => "\x1b[46m",
Special(s) if s == "bgwhite" => "\x1b[47m",
Special(s) if s == "lt" => "<",
Special(r) => panic!("Unknown ansi type {}", r)
}
), eof)(i).map(|(_, (r, _))| r)
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(),
)
}

View File

@ -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")
);
}

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 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<dyn Error + Send + Sync>)?;
let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?)
.map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)?;
if av.copyright != "This file is protected by copyright and may not be used or reproduced except as authorised by the copyright holder. All rights reserved." ||
av.assertion != "age>=18" {
Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?;

View File

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

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,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<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 type AttackMessageChoice =
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 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<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.
ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64,
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> },
ChangeTargetHealth {
delay_secs: u64,
base_effect: i64,
skill_multiplier: f64,
max_effect: i64,
message: Box<dyn Fn(&Item) -> (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<WeaponData>,
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<WearData>
pub wear_data: Option<WearData>,
}
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<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()),
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<Item> for PossessionType {
pub fn fist() -> &'static WeaponData {
static FIST_WEAPON: OnceCell<WeaponData> = 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<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::*;
&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<PossessionType, &'static Possessio
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
static RELEVANT: OnceCell<Vec<PossessionType>> = 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);
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::ButcherKnife,
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::{
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 <bold>use<reset> command to submit \
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(())
}
@ -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<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(NewCorpLicence,
PossessionData {

View File

@ -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<PossessionData> = 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()
})
}

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 once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(
PossessionType::RustyMetalPot,

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::Scanlock,
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 once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(
PossessionType::LeatherPants,

View File

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

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(MediumTraumaKit,
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 once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::AntennaWhip,
PossessionData {

View File

@ -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,
}

View File

@ -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")
);
}

View File

@ -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<A> = Result<A, Box<dyn Error + Send + Sync>>;
fn read_latest_config() -> DResult<Config> {
serde_yaml::from_str(&fs::read_to_string("listener.conf")?).
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
serde_yaml::from_str(&fs::read_to_string("listener.conf")?)
.map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
}
#[derive(Debug, Clone)]
enum ServerTaskCommand {
SwitchTo { new_server: String },
Send { message: MessageFromListener }
Send { message: MessageFromListener },
}
fn run_server_task<FHandler, HandlerFut>(
@ -54,33 +55,32 @@ fn run_server_task<FHandler, HandlerFut>(
mut receiver: ReceiverStream<ServerTaskCommand>,
sender: mpsc::Sender<ServerTaskCommand>,
server: String,
message_handler: FHandler
)
where
message_handler: FHandler,
) where
FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static,
HandlerFut: Future<Output = ()> + Send + 'static
HandlerFut: Future<Output = ()> + 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::<MessageToListener, MessageFromListener>::default()
Cbor::<MessageToListener, MessageFromListener>::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<SessionCommand>,
disconnect_channel: mpsc::UnboundedSender<()>
disconnect_channel: mpsc::UnboundedSender<()>,
}
struct SessionIndexes {
by_uuid: BTreeMap<Uuid, SessionRecord>,
count_by_source: BTreeMap<String, u64>
count_by_source: BTreeMap<String, u64>,
}
type SessionMap = Arc<Mutex<SessionIndexes>>;
@ -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<ServerTaskCommand> {
fn start_server_task(
listener_id: Uuid,
server: String,
session_map: SessionMap,
) -> mpsc::Sender<ServerTaskCommand> {
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<ServerTaskCommand>,
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<ServerTaskCommand>) {
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<ServerTaskCommand>
server: mpsc::Sender<ServerTaskCommand>,
) {
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<ServerTaskCommand>) ->
Result<warp::reply::Response, warp::Rejection> {
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<ServerTaskCommand>,
) -> Result<warp::reply::Response, warp::Rejection> {
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<warp::reply::Response, warp::Rejection> {
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<()> {
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<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::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::<dyn Error + Send + Sync>)
Err(Box::new(e) as Box<dyn Error + Send + Sync>)
}
}
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() {
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::<dyn Error + Send + Sync>)
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)
}
#[tokio::main]
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 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);
}
}

0
gear.rs Normal file
View File