Add gear command.
This commit is contained in:
parent
cf0d2f740b
commit
228c5fbb9b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
/target
|
||||
config
|
||||
docs/private
|
||||
*~
|
||||
|
173
ansi/src/lib.rs
173
ansi/src/lib.rs
@ -6,8 +6,10 @@ use std::rc::Rc;
|
||||
/// escape - so use this for untrusted input that you don't expect
|
||||
/// to contain ansi escapes at all.
|
||||
pub fn ignore_special_characters(input: &str) -> String {
|
||||
input.chars().filter(|c| *c == '\t' || *c == '\n' ||
|
||||
(*c >= ' ' && *c <= '~')).collect()
|
||||
input
|
||||
.chars()
|
||||
.filter(|c| *c == '\t' || *c == '\n' || (*c >= ' ' && *c <= '~'))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
@ -22,26 +24,35 @@ struct AnsiState {
|
||||
impl AnsiState {
|
||||
fn restore_ansi(self: &Self) -> String {
|
||||
let mut buf = String::new();
|
||||
if !(self.bold && self.underline && self.strike &&
|
||||
self.background != 0 && self.foreground != 0) {
|
||||
if !(self.bold
|
||||
&& self.underline
|
||||
&& self.strike
|
||||
&& self.background != 0
|
||||
&& self.foreground != 0)
|
||||
{
|
||||
buf.push_str(ansi!("<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(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
1
blastmud_game/rustfmt.toml
Normal file
1
blastmud_game/rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
edition = "2021"
|
@ -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"))?;
|
||||
|
@ -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,
|
||||
|
120
blastmud_game/src/message_handler/user_commands/gear.rs
Normal file
120
blastmud_game/src/message_handler/user_commands/gear.rs
Normal 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;
|
@ -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!(
|
||||
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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user