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,20 +74,20 @@ struct AnsiIterator<'l> {
inject_spaces: u64,
}
impl AnsiIterator<'_> {
fn new<'l>(input: &'l str) -> AnsiIterator<'l> {
AnsiIterator { underlying: input.chars().enumerate(),
AnsiIterator {
underlying: input.chars().enumerate(),
input: input,
state: Rc::new(AnsiState {
background: 0,
foreground: 0,
bold: false,
underline: false,
strike: false
strike: false,
}),
pending_col: false,
inject_spaces: 0
inject_spaces: 0,
}
}
}
@ -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,7 +343,7 @@ 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),
}
}
@ -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),
assert!(
col <= limit(row),
"col must be below limit, but found c={}, col={}, limit={}",
c, col, limit(row));
c,
col,
limit(row)
);
if !start_word {
if col == limit(row) {
// We are about to hit the limit, and we need to decide
@ -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),
}
}
@ -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,16 +21,19 @@ 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(
pair(
fold_many0(
alt((
take_till1(|c| c == '<').map(AnsiFrag::Lit),
tuple((tag("<"), take_till(|c| c == '>'), tag(">"))).map(|t| AnsiFrag::Special(t.1))
tuple((tag("<"), take_till(|c| c == '>'), tag(">")))
.map(|t| AnsiFrag::Special(t.1)),
)),
|| "".to_owned(),
|a, r| a + match r {
|a, r| {
a + match r {
AnsiFrag::Lit(s) => &s,
Special(s) if s == "reset" => "\x1b[0m",
Special(s) if s == "bold" => "\x1b[1m",
@ -54,11 +57,17 @@ pub fn ansi(input: TokenStream) -> TokenStream {
Special(s) if s == "bgcyan" => "\x1b[46m",
Special(s) if s == "bgwhite" => "\x1b[47m",
Special(s) if s == "lt" => "<",
Special(r) => panic!("Unknown ansi type {}", r)
Special(r) => panic!("Unknown ansi type {}", r),
}
), eof)(i).map(|(_, (r, _))| r)
},
),
eof,
)(i)
.map(|(_, (r, _))| r)
}
TokenStream::from(parser(&raw)
.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
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,36 +1,35 @@
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)]
@ -40,7 +39,21 @@ pub enum DamageType {
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!(
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 {} {}",
&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,8 +200,22 @@ 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 {
@ -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 {
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",
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 {}",
})],
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,40 +82,60 @@ 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,
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",
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 {
.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,
})
.await?;
ctx.trans
.upsert_corp_membership(
&corp_id,
&player.item_code,
&CorpMembership {
joined_at: Some(Utc::now()),
permissions: vec!(CorpPermission::Holder),
permissions: vec![CorpPermission::Holder],
allow_combat: true,
job_title: "Founder".to_owned(),
..Default::default()
}).await?;
},
)
.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?;
@ -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 {
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 {}",
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 {}",
})],
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(
if !ctx
.trans
.find_by_action_and_location(
&room.refstr(),
&LocationActionType::InstalledOnDoorAsLock(direction.clone())
).await?.is_empty() {
&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();
@ -58,32 +68,45 @@ impl InstallHandler for ScanLockInstall {
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",
&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",
&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?;
&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? {
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();
""
@ -94,29 +117,31 @@ impl InstallHandler for ScanLockInstall {
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",
&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",
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?;
)),
)
.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,5 +1,5 @@
use uuid::Uuid;
use serde::*;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum MessageFromListener {
@ -7,7 +7,7 @@ pub enum MessageFromListener {
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,32 +55,31 @@ 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!(
@ -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) {
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,
fn start_server_task(
listener_id: Uuid,
server: String,
session_map: SessionMap) -> mpsc::Sender<ServerTaskCommand> {
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,13 +307,13 @@ 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))
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();
}
@ -340,19 +348,18 @@ async fn handle_client_socket(
server: mpsc::Sender<ServerTaskCommand>,
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();
@ -360,8 +367,14 @@ async fn handle_client_socket(
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,17 +382,31 @@ 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();
@ -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,17 +532,31 @@ 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);
@ -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,
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()
)
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 {
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,25 +753,33 @@ 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())?;
@ -704,25 +790,30 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
let active_sessions_for_listener = active_sessions.clone();
listen_handles.push(task::spawn(async move {
match TcpListener::bind(&listener).await {
Err(e) => { warn!("Error listening to {}: {}", &listener, e); }
Ok(listensock) => {
loop {
Err(e) => {
warn!("Error listening to {}: {}", &listener, e);
}
Ok(listensock) => loop {
match listensock.accept().await {
Err(e) => { warn!("Error accepting connection from {}: {}",
&listener, e); }
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();
let active_sessions_for_client =
active_sessions_for_listener.clone();
task::spawn(async move {
handle_client_socket(server_sender_for_client,
handle_client_socket(
server_sender_for_client,
active_sessions_for_client,
stream,
addr
).await;
}); }
}
addr,
)
.await;
});
}
}
},
}
}));
}
@ -735,7 +826,10 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// Note: It is deliberate behaviour to send this even if gameserver
// 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 {
@ -744,7 +838,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
}
}
#[cfg(test)]
mod tests {
#[test]

0
gear.rs Normal file
View File