Add gear command.
This commit is contained in:
parent
cf0d2f740b
commit
228c5fbb9b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
config
|
config
|
||||||
docs/private
|
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
|
/// escape - so use this for untrusted input that you don't expect
|
||||||
/// to contain ansi escapes at all.
|
/// to contain ansi escapes at all.
|
||||||
pub fn ignore_special_characters(input: &str) -> String {
|
pub fn ignore_special_characters(input: &str) -> String {
|
||||||
input.chars().filter(|c| *c == '\t' || *c == '\n' ||
|
input
|
||||||
(*c >= ' ' && *c <= '~')).collect()
|
.chars()
|
||||||
|
.filter(|c| *c == '\t' || *c == '\n' || (*c >= ' ' && *c <= '~'))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
@ -22,26 +24,35 @@ struct AnsiState {
|
|||||||
impl AnsiState {
|
impl AnsiState {
|
||||||
fn restore_ansi(self: &Self) -> String {
|
fn restore_ansi(self: &Self) -> String {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
if !(self.bold && self.underline && self.strike &&
|
if !(self.bold
|
||||||
self.background != 0 && self.foreground != 0) {
|
&& self.underline
|
||||||
|
&& self.strike
|
||||||
|
&& self.background != 0
|
||||||
|
&& self.foreground != 0)
|
||||||
|
{
|
||||||
buf.push_str(ansi!("<reset>"));
|
buf.push_str(ansi!("<reset>"));
|
||||||
}
|
}
|
||||||
if self.bold { buf.push_str(ansi!("<bold>")); }
|
if self.bold {
|
||||||
if self.underline { buf.push_str(ansi!("<under>")); }
|
buf.push_str(ansi!("<bold>"));
|
||||||
if self.strike { buf.push_str(ansi!("<strike>")); }
|
}
|
||||||
|
if self.underline {
|
||||||
|
buf.push_str(ansi!("<under>"));
|
||||||
|
}
|
||||||
|
if self.strike {
|
||||||
|
buf.push_str(ansi!("<strike>"));
|
||||||
|
}
|
||||||
if self.background != 0 {
|
if self.background != 0 {
|
||||||
buf.push_str(&format!("\x1b[{}m", 39 + self.background)); }
|
buf.push_str(&format!("\x1b[{}m", 39 + self.background));
|
||||||
|
}
|
||||||
if self.foreground != 0 {
|
if self.foreground != 0 {
|
||||||
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground)); }
|
buf.push_str(&format!("\x1b[{}m", 29 + self.foreground));
|
||||||
|
}
|
||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
struct AnsiEvent<'l> (
|
struct AnsiEvent<'l>(AnsiParseToken<'l>, Rc<AnsiState>);
|
||||||
AnsiParseToken<'l>,
|
|
||||||
Rc<AnsiState>
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
enum AnsiParseToken<'l> {
|
enum AnsiParseToken<'l> {
|
||||||
@ -63,25 +74,25 @@ struct AnsiIterator<'l> {
|
|||||||
inject_spaces: u64,
|
inject_spaces: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl AnsiIterator<'_> {
|
impl AnsiIterator<'_> {
|
||||||
fn new<'l>(input: &'l str) -> AnsiIterator<'l> {
|
fn new<'l>(input: &'l str) -> AnsiIterator<'l> {
|
||||||
AnsiIterator { underlying: input.chars().enumerate(),
|
AnsiIterator {
|
||||||
|
underlying: input.chars().enumerate(),
|
||||||
input: input,
|
input: input,
|
||||||
state: Rc::new(AnsiState {
|
state: Rc::new(AnsiState {
|
||||||
background: 0,
|
background: 0,
|
||||||
foreground: 0,
|
foreground: 0,
|
||||||
bold: false,
|
bold: false,
|
||||||
underline: false,
|
underline: false,
|
||||||
strike: false
|
strike: false,
|
||||||
}),
|
}),
|
||||||
pending_col: false,
|
pending_col: false,
|
||||||
inject_spaces: 0
|
inject_spaces: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'l>Iterator for AnsiIterator<'l> {
|
impl<'l> Iterator for AnsiIterator<'l> {
|
||||||
type Item = AnsiEvent<'l>;
|
type Item = AnsiEvent<'l>;
|
||||||
|
|
||||||
fn next(self: &mut Self) -> Option<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 {
|
if self.inject_spaces > 0 {
|
||||||
self.pending_col = true;
|
self.pending_col = true;
|
||||||
self.inject_spaces -= 1;
|
self.inject_spaces -= 1;
|
||||||
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
|
return Some(AnsiEvent::<'l>(
|
||||||
|
AnsiParseToken::Character(' '),
|
||||||
|
self.state.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
while let Some((i0, c)) = self.underlying.next() {
|
while let Some((i0, c)) = self.underlying.next() {
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
@ -100,11 +114,17 @@ impl <'l>Iterator for AnsiIterator<'l> {
|
|||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
self.pending_col = true;
|
self.pending_col = true;
|
||||||
self.inject_spaces = 3;
|
self.inject_spaces = 3;
|
||||||
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(' '), self.state.clone()));
|
return Some(AnsiEvent::<'l>(
|
||||||
|
AnsiParseToken::Character(' '),
|
||||||
|
self.state.clone(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else if c >= ' ' && c <= '~' {
|
} else if c >= ' ' && c <= '~' {
|
||||||
self.pending_col = true;
|
self.pending_col = true;
|
||||||
return Some(AnsiEvent::<'l>(AnsiParseToken::Character(c), self.state.clone()));
|
return Some(AnsiEvent::<'l>(
|
||||||
|
AnsiParseToken::Character(c),
|
||||||
|
self.state.clone(),
|
||||||
|
));
|
||||||
} else if c == '\x1b' {
|
} else if c == '\x1b' {
|
||||||
if let Some((_, c2)) = self.underlying.next() {
|
if let Some((_, c2)) = self.underlying.next() {
|
||||||
if c2 != '[' {
|
if c2 != '[' {
|
||||||
@ -125,7 +145,9 @@ impl <'l>Iterator for AnsiIterator<'l> {
|
|||||||
cs_no *= 10;
|
cs_no *= 10;
|
||||||
cs_no += cs_no2;
|
cs_no += cs_no2;
|
||||||
imax = i3;
|
imax = i3;
|
||||||
} else { continue; }
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if cs2 != 'm' {
|
} else if cs2 != 'm' {
|
||||||
continue;
|
continue;
|
||||||
@ -141,30 +163,37 @@ impl <'l>Iterator for AnsiIterator<'l> {
|
|||||||
st.underline = false;
|
st.underline = false;
|
||||||
st.strike = false;
|
st.strike = false;
|
||||||
}
|
}
|
||||||
1 => { st.bold = true; }
|
1 => {
|
||||||
4 => { st.underline = true; }
|
st.bold = true;
|
||||||
9 => { st.strike = true; }
|
}
|
||||||
24 => { st.underline = false; }
|
4 => {
|
||||||
|
st.underline = true;
|
||||||
|
}
|
||||||
|
9 => {
|
||||||
|
st.strike = true;
|
||||||
|
}
|
||||||
|
24 => {
|
||||||
|
st.underline = false;
|
||||||
|
}
|
||||||
i if i >= 30 && i <= 37 => {
|
i if i >= 30 && i <= 37 => {
|
||||||
st.foreground = i as u64 - 29;
|
st.foreground = i as u64 - 29;
|
||||||
}
|
}
|
||||||
i if i >= 40 && i <= 47 => {
|
i if i >= 40 && i <= 47 => {
|
||||||
st.foreground = i as u64 - 39;
|
st.foreground = i as u64 - 39;
|
||||||
}
|
}
|
||||||
_ => continue
|
_ => continue,
|
||||||
}
|
}
|
||||||
drop(st);
|
drop(st);
|
||||||
return Some(AnsiEvent::<'l>(
|
return Some(AnsiEvent::<'l>(
|
||||||
AnsiParseToken::ControlSeq(
|
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
|
||||||
&self.input[i0..(imax + 1)]
|
self.state.clone(),
|
||||||
), self.state.clone()));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Strips out basic colours / character formatting codes cleanly. Tabs are
|
/// Strips out basic colours / character formatting codes cleanly. Tabs are
|
||||||
@ -193,7 +222,7 @@ pub fn limit_special_characters(input: &str) -> String {
|
|||||||
match e {
|
match e {
|
||||||
AnsiParseToken::Character(c) => buf.push(c),
|
AnsiParseToken::Character(c) => buf.push(c),
|
||||||
AnsiParseToken::Newline => buf.push('\n'),
|
AnsiParseToken::Newline => buf.push('\n'),
|
||||||
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
|
AnsiParseToken::ControlSeq(t) => buf.push_str(t),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buf
|
buf
|
||||||
@ -201,8 +230,13 @@ pub fn limit_special_characters(input: &str) -> String {
|
|||||||
|
|
||||||
/// Flows a second column around a first column, limiting the width of both
|
/// Flows a second column around a first column, limiting the width of both
|
||||||
/// columns as specified, and adding a gutter.
|
/// columns as specified, and adding a gutter.
|
||||||
pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
pub fn flow_around(
|
||||||
col2: &str, col2_width: usize) -> String {
|
col1: &str,
|
||||||
|
col1_width: usize,
|
||||||
|
gutter: &str,
|
||||||
|
col2: &str,
|
||||||
|
col2_width: usize,
|
||||||
|
) -> String {
|
||||||
let mut it1 = AnsiIterator::new(col1).peekable();
|
let mut it1 = AnsiIterator::new(col1).peekable();
|
||||||
let mut it2 = AnsiIterator::new(col2).peekable();
|
let mut it2 = AnsiIterator::new(col2).peekable();
|
||||||
|
|
||||||
@ -212,7 +246,7 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
'around_rows: loop {
|
'around_rows: loop {
|
||||||
match it1.peek() {
|
match it1.peek() {
|
||||||
None => break 'around_rows,
|
None => break 'around_rows,
|
||||||
Some(AnsiEvent(_, st)) => buf.push_str(&st.restore_ansi())
|
Some(AnsiEvent(_, st)) => buf.push_str(&st.restore_ansi()),
|
||||||
}
|
}
|
||||||
let mut fill_needed: usize = 0;
|
let mut fill_needed: usize = 0;
|
||||||
let mut skip_nl = true;
|
let mut skip_nl = true;
|
||||||
@ -244,7 +278,9 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
None => break,
|
None => break,
|
||||||
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
||||||
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
if fill_needed > 0 { buf.push_str(s); }
|
if fill_needed > 0 {
|
||||||
|
buf.push_str(s);
|
||||||
|
}
|
||||||
it1.next();
|
it1.next();
|
||||||
}
|
}
|
||||||
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
@ -254,7 +290,9 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _ in 0..fill_needed { buf.push(' '); }
|
for _ in 0..fill_needed {
|
||||||
|
buf.push(' ');
|
||||||
|
}
|
||||||
|
|
||||||
buf.push_str(gutter);
|
buf.push_str(gutter);
|
||||||
|
|
||||||
@ -285,7 +323,9 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
None => break,
|
None => break,
|
||||||
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
Some(AnsiEvent(AnsiParseToken::Character(_), _)) => break,
|
||||||
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(s), _)) => {
|
||||||
if fill_needed > 0 { buf.push_str(s); }
|
if fill_needed > 0 {
|
||||||
|
buf.push_str(s);
|
||||||
|
}
|
||||||
it2.next();
|
it2.next();
|
||||||
}
|
}
|
||||||
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
@ -303,7 +343,7 @@ pub fn flow_around(col1: &str, col1_width: usize, gutter: &str,
|
|||||||
match e {
|
match e {
|
||||||
AnsiParseToken::Character(c) => buf.push(c),
|
AnsiParseToken::Character(c) => buf.push(c),
|
||||||
AnsiParseToken::Newline => buf.push('\n'),
|
AnsiParseToken::Newline => buf.push('\n'),
|
||||||
AnsiParseToken::ControlSeq(t) => buf.push_str(t)
|
AnsiParseToken::ControlSeq(t) => buf.push_str(t),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -315,7 +355,9 @@ fn is_wrappable(c: char) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn word_wrap<F>(input: &str, limit: F) -> String
|
pub fn word_wrap<F>(input: &str, limit: F) -> String
|
||||||
where F: Fn(usize) -> usize {
|
where
|
||||||
|
F: Fn(usize) -> usize,
|
||||||
|
{
|
||||||
let mut it_main = AnsiIterator::new(input);
|
let mut it_main = AnsiIterator::new(input);
|
||||||
let mut start_word = true;
|
let mut start_word = true;
|
||||||
let mut row: usize = 0;
|
let mut row: usize = 0;
|
||||||
@ -338,9 +380,12 @@ pub fn word_wrap<F>(input: &str, limit: F) -> String
|
|||||||
let fits = 'check_fits: loop {
|
let fits = 'check_fits: loop {
|
||||||
match it_lookahead.next() {
|
match it_lookahead.next() {
|
||||||
None => break 'check_fits true,
|
None => break 'check_fits true,
|
||||||
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
|
break 'check_fits true
|
||||||
break 'check_fits is_wrappable(c),
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
|
||||||
|
break 'check_fits is_wrappable(c)
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -361,9 +406,13 @@ pub fn word_wrap<F>(input: &str, limit: F) -> String
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
assert!(col <= limit(row),
|
assert!(
|
||||||
|
col <= limit(row),
|
||||||
"col must be below limit, but found c={}, col={}, limit={}",
|
"col must be below limit, but found c={}, col={}, limit={}",
|
||||||
c, col, limit(row));
|
c,
|
||||||
|
col,
|
||||||
|
limit(row)
|
||||||
|
);
|
||||||
if !start_word {
|
if !start_word {
|
||||||
if col == limit(row) {
|
if col == limit(row) {
|
||||||
// We are about to hit the limit, and we need to decide
|
// We are about to hit the limit, and we need to decide
|
||||||
@ -372,9 +421,12 @@ pub fn word_wrap<F>(input: &str, limit: F) -> String
|
|||||||
let fits = 'check_fits: loop {
|
let fits = 'check_fits: loop {
|
||||||
match it_lookahead.next() {
|
match it_lookahead.next() {
|
||||||
None => break 'check_fits true,
|
None => break 'check_fits true,
|
||||||
Some(AnsiEvent(AnsiParseToken::Newline, _)) => break 'check_fits true,
|
Some(AnsiEvent(AnsiParseToken::Newline, _)) => {
|
||||||
Some(AnsiEvent(AnsiParseToken::Character(c), _)) =>
|
break 'check_fits true
|
||||||
break 'check_fits is_wrappable(c),
|
}
|
||||||
|
Some(AnsiEvent(AnsiParseToken::Character(c), _)) => {
|
||||||
|
break 'check_fits is_wrappable(c)
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -429,7 +481,7 @@ pub fn word_wrap<F>(input: &str, limit: F) -> String
|
|||||||
buf.push('\n');
|
buf.push('\n');
|
||||||
start_word = true;
|
start_word = true;
|
||||||
}
|
}
|
||||||
Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t)
|
Some(AnsiEvent(AnsiParseToken::ControlSeq(t), _)) => buf.push_str(t),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,24 +502,26 @@ mod test {
|
|||||||
assert_eq!(strip_special_characters("a\tb"), "a b");
|
assert_eq!(strip_special_characters("a\tb"), "a b");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
strip_special_characters(ansi!("<red>hello<green>world")),
|
strip_special_characters(ansi!("<red>hello<green>world")),
|
||||||
"helloworld");
|
"helloworld"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
strip_special_characters("hello\r\x07world\n"),
|
strip_special_characters("hello\r\x07world\n"),
|
||||||
"helloworld\n");
|
"helloworld\n"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
strip_special_characters("hello\r\x07world\n"),
|
strip_special_characters("hello\r\x07world\n"),
|
||||||
"helloworld\n");
|
"helloworld\n"
|
||||||
assert_eq!(
|
);
|
||||||
strip_special_characters("Test\x1b[5;5fing"),
|
assert_eq!(strip_special_characters("Test\x1b[5;5fing"), "Test5fing");
|
||||||
"Test5fing");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn limit_special_characters_strips_some_things() {
|
fn limit_special_characters_strips_some_things() {
|
||||||
assert_eq!(limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
|
assert_eq!(
|
||||||
ansi!("a<bgred><green>b<bggreen><red>c<reset>d"));
|
limit_special_characters(ansi!("a<bgred><green>b<bggreen><red>c<reset>d")),
|
||||||
assert_eq!(limit_special_characters("Test\x1b[5;5fing"),
|
ansi!("a<bgred><green>b<bggreen><red>c<reset>d")
|
||||||
"Test5fing");
|
);
|
||||||
|
assert_eq!(limit_special_characters("Test\x1b[5;5fing"), "Test5fing");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -537,5 +591,4 @@ mod test {
|
|||||||
- -testing";
|
- -testing";
|
||||||
assert_eq!(word_wrap(unwrapped, |_| 10), wrapped);
|
assert_eq!(word_wrap(unwrapped, |_| 10), wrapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
use proc_macro::TokenStream;
|
|
||||||
use syn::{parse_macro_input, Lit};
|
|
||||||
use quote::ToTokens;
|
|
||||||
use nom::{
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{tag, take_till, take_till1},
|
||||||
combinator::eof,
|
combinator::eof,
|
||||||
branch::alt, multi::fold_many0,
|
|
||||||
bytes::complete::{take_till, take_till1, tag},
|
|
||||||
sequence::{tuple, pair},
|
|
||||||
error::Error,
|
error::Error,
|
||||||
Err,
|
multi::fold_many0,
|
||||||
Parser
|
sequence::{pair, tuple},
|
||||||
|
Err, Parser,
|
||||||
};
|
};
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::ToTokens;
|
||||||
|
use syn::{parse_macro_input, Lit};
|
||||||
|
|
||||||
enum AnsiFrag<'l> {
|
enum AnsiFrag<'l> {
|
||||||
Lit(&'l str),
|
Lit(&'l str),
|
||||||
Special(&'l str)
|
Special(&'l str),
|
||||||
}
|
}
|
||||||
use AnsiFrag::Special;
|
use AnsiFrag::Special;
|
||||||
|
|
||||||
@ -21,16 +21,19 @@ use AnsiFrag::Special;
|
|||||||
pub fn ansi(input: TokenStream) -> TokenStream {
|
pub fn ansi(input: TokenStream) -> TokenStream {
|
||||||
let raw = match parse_macro_input!(input as Lit) {
|
let raw = match parse_macro_input!(input as Lit) {
|
||||||
Lit::Str(lit_str) => lit_str.value(),
|
Lit::Str(lit_str) => lit_str.value(),
|
||||||
_ => panic!("Expected a string literal")
|
_ => panic!("Expected a string literal"),
|
||||||
};
|
};
|
||||||
fn parser(i: &str) -> Result<String, Err<Error<&str>>> {
|
fn parser(i: &str) -> Result<String, Err<Error<&str>>> {
|
||||||
pair(fold_many0(
|
pair(
|
||||||
|
fold_many0(
|
||||||
alt((
|
alt((
|
||||||
take_till1(|c| c == '<').map(AnsiFrag::Lit),
|
take_till1(|c| c == '<').map(AnsiFrag::Lit),
|
||||||
tuple((tag("<"), take_till(|c| c == '>'), tag(">"))).map(|t| AnsiFrag::Special(t.1))
|
tuple((tag("<"), take_till(|c| c == '>'), tag(">")))
|
||||||
|
.map(|t| AnsiFrag::Special(t.1)),
|
||||||
)),
|
)),
|
||||||
|| "".to_owned(),
|
|| "".to_owned(),
|
||||||
|a, r| a + match r {
|
|a, r| {
|
||||||
|
a + match r {
|
||||||
AnsiFrag::Lit(s) => &s,
|
AnsiFrag::Lit(s) => &s,
|
||||||
Special(s) if s == "reset" => "\x1b[0m",
|
Special(s) if s == "reset" => "\x1b[0m",
|
||||||
Special(s) if s == "bold" => "\x1b[1m",
|
Special(s) if s == "bold" => "\x1b[1m",
|
||||||
@ -54,11 +57,17 @@ pub fn ansi(input: TokenStream) -> TokenStream {
|
|||||||
Special(s) if s == "bgcyan" => "\x1b[46m",
|
Special(s) if s == "bgcyan" => "\x1b[46m",
|
||||||
Special(s) if s == "bgwhite" => "\x1b[47m",
|
Special(s) if s == "bgwhite" => "\x1b[47m",
|
||||||
Special(s) if s == "lt" => "<",
|
Special(s) if s == "lt" => "<",
|
||||||
Special(r) => panic!("Unknown ansi type {}", r)
|
Special(r) => panic!("Unknown ansi type {}", r),
|
||||||
}
|
}
|
||||||
), eof)(i).map(|(_, (r, _))| r)
|
},
|
||||||
|
),
|
||||||
|
eof,
|
||||||
|
)(i)
|
||||||
|
.map(|(_, (r, _))| r)
|
||||||
}
|
}
|
||||||
TokenStream::from(parser(&raw)
|
TokenStream::from(
|
||||||
.unwrap_or_else(|e| { panic!("Bad ansi literal: {}", e) })
|
parser(&raw)
|
||||||
.into_token_stream())
|
.unwrap_or_else(|e| panic!("Bad ansi literal: {}", e))
|
||||||
|
.into_token_stream(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,12 @@ use std::process::Command;
|
|||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
let cmdout = Command::new("git")
|
let cmdout = Command::new("git")
|
||||||
.arg("rev-parse").arg("HEAD")
|
.arg("rev-parse")
|
||||||
.output().expect("git rev-parse HEAD failed");
|
.arg("HEAD")
|
||||||
println!("cargo:rustc-env=GIT_VERSION={}",
|
.output()
|
||||||
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8"));
|
.expect("git rev-parse HEAD failed");
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=GIT_VERSION={}",
|
||||||
|
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
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 crate::DResult;
|
||||||
|
use base64;
|
||||||
|
use ring::signature;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::error::Error;
|
||||||
|
pub(crate) use std::fs;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct AV {
|
struct AV {
|
||||||
@ -11,20 +11,20 @@ struct AV {
|
|||||||
serial: u64,
|
serial: u64,
|
||||||
cn: String,
|
cn: String,
|
||||||
assertion: String,
|
assertion: String,
|
||||||
sig: String
|
sig: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
static KEY_BYTES: [u8;65] = [
|
static KEY_BYTES: [u8; 65] = [
|
||||||
0x04, 0x4f, 0xa0, 0x8b, 0x32, 0xa7, 0x7f, 0xc1, 0x0a, 0xfc, 0x51, 0x95, 0x93, 0x57, 0x05,
|
0x04, 0x4f, 0xa0, 0x8b, 0x32, 0xa7, 0x7f, 0xc1, 0x0a, 0xfc, 0x51, 0x95, 0x93, 0x57, 0x05, 0xb3,
|
||||||
0xb3, 0x0f, 0xad, 0x16, 0x05, 0x3c, 0x7c, 0xfc, 0x02, 0xd2, 0x7a, 0x63, 0xff, 0xd3, 0x09,
|
0x0f, 0xad, 0x16, 0x05, 0x3c, 0x7c, 0xfc, 0x02, 0xd2, 0x7a, 0x63, 0xff, 0xd3, 0x09, 0xaa, 0x5b,
|
||||||
0xaa, 0x5b, 0x78, 0xfe, 0xa8, 0xc2, 0xc3, 0x02, 0xc2, 0xe6, 0xaf, 0x81, 0xc7, 0xa3, 0x03,
|
0x78, 0xfe, 0xa8, 0xc2, 0xc3, 0x02, 0xc2, 0xe6, 0xaf, 0x81, 0xc7, 0xa3, 0x03, 0xfa, 0x4d, 0xf1,
|
||||||
0xfa, 0x4d, 0xf1, 0xf9, 0xfc, 0x0a, 0x36, 0xef, 0x6b, 0x1e, 0x9d, 0xce, 0x6e, 0x60, 0xc6,
|
0xf9, 0xfc, 0x0a, 0x36, 0xef, 0x6b, 0x1e, 0x9d, 0xce, 0x6e, 0x60, 0xc6, 0xa8, 0xb3, 0x02, 0x35,
|
||||||
0xa8, 0xb3, 0x02, 0x35, 0x7e
|
0x7e,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn check() -> DResult<()> {
|
pub fn check() -> DResult<()> {
|
||||||
let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?).
|
let av: AV = serde_yaml::from_str(&fs::read_to_string("age-verification.yml")?)
|
||||||
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)?;
|
.map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)?;
|
||||||
if av.copyright != "This file is protected by copyright and may not be used or reproduced except as authorised by the copyright holder. All rights reserved." ||
|
if av.copyright != "This file is protected by copyright and may not be used or reproduced except as authorised by the copyright holder. All rights reserved." ||
|
||||||
av.assertion != "age>=18" {
|
av.assertion != "age>=18" {
|
||||||
Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?;
|
Err(Box::<dyn Error + Send + Sync>::from("Invalid age-verification.yml"))?;
|
||||||
|
@ -21,6 +21,7 @@ pub mod close;
|
|||||||
pub mod corp;
|
pub mod corp;
|
||||||
pub mod cut;
|
pub mod cut;
|
||||||
pub mod drop;
|
pub mod drop;
|
||||||
|
mod gear;
|
||||||
pub mod get;
|
pub mod get;
|
||||||
mod describe;
|
mod describe;
|
||||||
mod help;
|
mod help;
|
||||||
@ -137,6 +138,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
"corp" => corp::VERB,
|
"corp" => corp::VERB,
|
||||||
"cut" => cut::VERB,
|
"cut" => cut::VERB,
|
||||||
"drop" => drop::VERB,
|
"drop" => drop::VERB,
|
||||||
|
"gear" => gear::VERB,
|
||||||
"get" => get::VERB,
|
"get" => get::VERB,
|
||||||
"install" => install::VERB,
|
"install" => install::VERB,
|
||||||
"inventory" => inventory::VERB,
|
"inventory" => inventory::VERB,
|
||||||
|
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::{
|
use crate::{
|
||||||
models::item::{SkillType, Item, Pronouns},
|
|
||||||
models::consent::ConsentType,
|
|
||||||
message_handler::user_commands::{UResult, VerbContext},
|
message_handler::user_commands::{UResult, VerbContext},
|
||||||
static_content::{
|
models::consent::ConsentType,
|
||||||
room::Direction,
|
models::item::{Item, Pronouns, SkillType},
|
||||||
species::BodyPart,
|
static_content::{room::Direction, species::BodyPart},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
mod fangs;
|
|
||||||
mod whip;
|
|
||||||
mod blade;
|
mod blade;
|
||||||
mod trauma_kit;
|
|
||||||
mod corp_licence;
|
mod corp_licence;
|
||||||
mod lock;
|
mod fangs;
|
||||||
mod meat;
|
|
||||||
pub mod head_armour;
|
pub mod head_armour;
|
||||||
pub mod torso_armour;
|
mod lock;
|
||||||
pub mod lower_armour;
|
pub mod lower_armour;
|
||||||
|
mod meat;
|
||||||
|
pub mod torso_armour;
|
||||||
|
mod trauma_kit;
|
||||||
|
mod whip;
|
||||||
|
|
||||||
pub type AttackMessageChoice = Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
|
pub type AttackMessageChoice =
|
||||||
pub type AttackMessageChoicePart = Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
|
Vec<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
|
||||||
|
pub type AttackMessageChoicePart =
|
||||||
|
Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
|
||||||
|
|
||||||
pub struct SkillScaling {
|
pub struct SkillScaling {
|
||||||
pub skill: SkillType,
|
pub skill: SkillType,
|
||||||
pub min_skill: f64,
|
pub min_skill: f64,
|
||||||
pub mean_damage_per_point_over_min: f64
|
pub mean_damage_per_point_over_min: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
#[derive(Eq,Ord,Clone,PartialEq,PartialOrd)]
|
#[derive(Eq, Ord, Clone, PartialEq, PartialOrd)]
|
||||||
pub enum DamageType {
|
pub enum DamageType {
|
||||||
Beat,
|
Beat,
|
||||||
Slash,
|
Slash,
|
||||||
Pierce,
|
Pierce,
|
||||||
Shock,
|
Shock,
|
||||||
Bullet
|
Bullet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DamageType {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn display(&self) -> &'static str {
|
||||||
|
use DamageType::*;
|
||||||
|
match self {
|
||||||
|
Beat => "beat",
|
||||||
|
Slash => "slash",
|
||||||
|
Pierce => "pierce",
|
||||||
|
Shock => "shock",
|
||||||
|
Bullet => "bullet",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WeaponAttackData {
|
pub struct WeaponAttackData {
|
||||||
@ -56,24 +69,27 @@ pub struct WeaponAttackData {
|
|||||||
impl Default for WeaponAttackData {
|
impl Default for WeaponAttackData {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
start_messages:
|
start_messages: vec![Box::new(|attacker, victim, exp| {
|
||||||
vec!(Box::new(|attacker, victim, exp| format!(
|
format!(
|
||||||
"{} makes an attack on {}",
|
"{} makes an attack on {}",
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&victim.display_for_sentence(exp,1, false)))),
|
&victim.display_for_sentence(exp, 1, false)
|
||||||
success_messages:
|
)
|
||||||
vec!(Box::new(|attacker, victim, part, exp|
|
})],
|
||||||
format!("{}'s attack on {} hits {} {}",
|
success_messages: vec![Box::new(|attacker, victim, part, exp| {
|
||||||
|
format!(
|
||||||
|
"{}'s attack on {} hits {} {}",
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&victim.display_for_sentence(exp, 1, false),
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
&victim.pronouns.possessive,
|
&victim.pronouns.possessive,
|
||||||
part.display(victim.sex.clone())
|
part.display(victim.sex.clone())
|
||||||
))),
|
)
|
||||||
|
})],
|
||||||
mean_damage: 1.0,
|
mean_damage: 1.0,
|
||||||
stdev_damage: 2.0,
|
stdev_damage: 2.0,
|
||||||
base_damage_type: DamageType::Slash,
|
base_damage_type: DamageType::Slash,
|
||||||
other_damage_types: vec!(),
|
other_damage_types: vec![],
|
||||||
skill_scaling: vec!()
|
skill_scaling: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,10 +130,17 @@ impl Default for ChargeData {
|
|||||||
|
|
||||||
pub enum UseEffect {
|
pub enum UseEffect {
|
||||||
// messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message.
|
// messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message.
|
||||||
BroadcastMessage { messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>},
|
BroadcastMessage {
|
||||||
|
messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>,
|
||||||
|
},
|
||||||
// skill_multiplier is always positive - sign flipped for crit fails.
|
// skill_multiplier is always positive - sign flipped for crit fails.
|
||||||
ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64,
|
ChangeTargetHealth {
|
||||||
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> },
|
delay_secs: u64,
|
||||||
|
base_effect: i64,
|
||||||
|
skill_multiplier: f64,
|
||||||
|
max_effect: i64,
|
||||||
|
message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UseData {
|
pub struct UseData {
|
||||||
@ -136,9 +159,9 @@ impl Default for UseData {
|
|||||||
Self {
|
Self {
|
||||||
uses_skill: SkillType::Medic,
|
uses_skill: SkillType::Medic,
|
||||||
diff_level: 10.0,
|
diff_level: 10.0,
|
||||||
crit_fail_effects: vec!(),
|
crit_fail_effects: vec![],
|
||||||
fail_effects: vec!(),
|
fail_effects: vec![],
|
||||||
success_effects: vec!(),
|
success_effects: vec![],
|
||||||
errorf: Box::new(|_it, _target| None),
|
errorf: Box::new(|_it, _target| None),
|
||||||
task_ref: "set me",
|
task_ref: "set me",
|
||||||
needs_consent_check: None,
|
needs_consent_check: None,
|
||||||
@ -161,7 +184,13 @@ pub struct WearData {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait WriteHandler {
|
pub trait WriteHandler {
|
||||||
async fn write_cmd(&self, ctx: &mut VerbContext, player: &Item, on_what: &Item, write_what: &str) -> UResult<()>;
|
async fn write_cmd(
|
||||||
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
player: &Item,
|
||||||
|
on_what: &Item,
|
||||||
|
write_what: &str,
|
||||||
|
) -> UResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -171,8 +200,22 @@ pub trait ArglessHandler {
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait InstallHandler {
|
pub trait InstallHandler {
|
||||||
async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>;
|
async fn install_cmd(
|
||||||
async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>;
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
player: &Item,
|
||||||
|
what: &Item,
|
||||||
|
room: &Item,
|
||||||
|
direction: &Direction,
|
||||||
|
) -> UResult<()>;
|
||||||
|
async fn uninstall_cmd(
|
||||||
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
player: &Item,
|
||||||
|
what: &Item,
|
||||||
|
room: &Item,
|
||||||
|
direction: &Direction,
|
||||||
|
) -> UResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PossessionData {
|
pub struct PossessionData {
|
||||||
@ -192,7 +235,7 @@ pub struct PossessionData {
|
|||||||
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
|
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
|
||||||
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
|
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
|
||||||
pub can_butcher: bool,
|
pub can_butcher: bool,
|
||||||
pub wear_data: Option<WearData>
|
pub wear_data: Option<WearData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PossessionData {
|
impl Default for PossessionData {
|
||||||
@ -203,7 +246,7 @@ impl Default for PossessionData {
|
|||||||
display_less_explicit: None,
|
display_less_explicit: None,
|
||||||
details: "A generic looking thing",
|
details: "A generic looking thing",
|
||||||
details_less_explicit: None,
|
details_less_explicit: None,
|
||||||
aliases: vec!(),
|
aliases: vec![],
|
||||||
max_health: 10,
|
max_health: 10,
|
||||||
weight: 100,
|
weight: 100,
|
||||||
charge_data: None,
|
charge_data: None,
|
||||||
@ -220,26 +263,27 @@ impl Default for PossessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl WeaponAttackData {
|
impl WeaponAttackData {
|
||||||
pub fn start_message(
|
pub fn start_message(&self, attacker: &Item, victim: &Item, explicit_ok: bool) -> String {
|
||||||
&self,
|
|
||||||
attacker: &Item, victim: &Item, explicit_ok: bool) -> String {
|
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
self.start_messages[..].choose(&mut rng).map(
|
self.start_messages[..]
|
||||||
|f| f(attacker, victim, explicit_ok)).unwrap_or(
|
.choose(&mut rng)
|
||||||
"No message defined yet".to_owned())
|
.map(|f| f(attacker, victim, explicit_ok))
|
||||||
|
.unwrap_or("No message defined yet".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn success_message(
|
pub fn success_message(
|
||||||
&self, attacker: &Item, victim: &Item,
|
&self,
|
||||||
part: &BodyPart, explicit_ok: bool
|
attacker: &Item,
|
||||||
|
victim: &Item,
|
||||||
|
part: &BodyPart,
|
||||||
|
explicit_ok: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
self.success_messages[..].choose(&mut rng).map(
|
self.success_messages[..]
|
||||||
|f| f(attacker, victim, part, explicit_ok)).unwrap_or(
|
.choose(&mut rng)
|
||||||
"No message defined yet".to_owned())
|
.map(|f| f(attacker, victim, part, explicit_ok))
|
||||||
|
.unwrap_or("No message defined yet".to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -268,7 +312,6 @@ pub enum PossessionType {
|
|||||||
Steak,
|
Steak,
|
||||||
AnimalSkin,
|
AnimalSkin,
|
||||||
SeveredHead,
|
SeveredHead,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Item> for PossessionType {
|
impl Into<Item> for PossessionType {
|
||||||
@ -281,15 +324,22 @@ impl Into<Item> for PossessionType {
|
|||||||
display_less_explicit: possession_dat.display_less_explicit.map(|d| d.to_owned()),
|
display_less_explicit: possession_dat.display_less_explicit.map(|d| d.to_owned()),
|
||||||
details: Some(possession_dat.details.to_owned()),
|
details: Some(possession_dat.details.to_owned()),
|
||||||
details_less_explicit: possession_dat.details_less_explicit.map(|d| d.to_owned()),
|
details_less_explicit: possession_dat.details_less_explicit.map(|d| d.to_owned()),
|
||||||
aliases: possession_dat.aliases.iter().map(|al| (*al).to_owned()).collect(),
|
aliases: possession_dat
|
||||||
|
.aliases
|
||||||
|
.iter()
|
||||||
|
.map(|al| (*al).to_owned())
|
||||||
|
.collect(),
|
||||||
health: possession_dat.max_health,
|
health: possession_dat.max_health,
|
||||||
weight: possession_dat.weight,
|
weight: possession_dat.weight,
|
||||||
pronouns: Pronouns {
|
pronouns: Pronouns {
|
||||||
is_proper: false,
|
is_proper: false,
|
||||||
..Pronouns::default_inanimate()
|
..Pronouns::default_inanimate()
|
||||||
},
|
},
|
||||||
charges: possession_dat.charge_data.as_ref()
|
charges: possession_dat
|
||||||
.map(|cd| cd.max_charges).unwrap_or(0),
|
.charge_data
|
||||||
|
.as_ref()
|
||||||
|
.map(|cd| cd.max_charges)
|
||||||
|
.unwrap_or(0),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,53 +347,65 @@ impl Into<Item> for PossessionType {
|
|||||||
|
|
||||||
pub fn fist() -> &'static WeaponData {
|
pub fn fist() -> &'static WeaponData {
|
||||||
static FIST_WEAPON: OnceCell<WeaponData> = OnceCell::new();
|
static FIST_WEAPON: OnceCell<WeaponData> = OnceCell::new();
|
||||||
FIST_WEAPON.get_or_init(|| {
|
FIST_WEAPON.get_or_init(|| WeaponData {
|
||||||
WeaponData {
|
|
||||||
uses_skill: SkillType::Fists,
|
uses_skill: SkillType::Fists,
|
||||||
raw_min_to_learn: 0.0,
|
raw_min_to_learn: 0.0,
|
||||||
raw_max_to_learn: 2.0,
|
raw_max_to_learn: 2.0,
|
||||||
normal_attack: WeaponAttackData {
|
normal_attack: WeaponAttackData {
|
||||||
start_messages: vec!(
|
start_messages: vec![Box::new(|attacker, victim, exp| {
|
||||||
Box::new(|attacker, victim, exp|
|
format!(
|
||||||
format!("{} swings at {} with {} fists",
|
"{} swings at {} with {} fists",
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&victim.display_for_sentence(exp, 1, false),
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
&attacker.pronouns.possessive
|
&attacker.pronouns.possessive
|
||||||
)
|
)
|
||||||
)
|
})],
|
||||||
),
|
success_messages: vec![Box::new(|attacker, victim, part, exp| {
|
||||||
success_messages: vec!(
|
format!(
|
||||||
Box::new(|attacker, victim, part, exp|
|
"{}'s fists smash into {}'s {}",
|
||||||
format!("{}'s fists smash into {}'s {}",
|
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&victim.display_for_sentence(exp, 1, false),
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
&part.display(victim.sex.clone())
|
&part.display(victim.sex.clone())
|
||||||
)
|
)
|
||||||
)
|
})],
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> {
|
pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> {
|
||||||
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> = OnceCell::new();
|
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> =
|
||||||
|
OnceCell::new();
|
||||||
use PossessionType::*;
|
use PossessionType::*;
|
||||||
&POSSESSION_DATA.get_or_init(|| {
|
&POSSESSION_DATA.get_or_init(|| {
|
||||||
vec!(
|
vec![(Fangs, fangs::data())]
|
||||||
(Fangs, fangs::data())
|
.into_iter()
|
||||||
).into_iter()
|
|
||||||
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(corp_licence::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(
|
||||||
|
corp_licence::data()
|
||||||
|
.iter()
|
||||||
|
.map(|v| ((*v).0.clone(), &(*v).1)),
|
||||||
|
)
|
||||||
.chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(head_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(
|
||||||
.chain(torso_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
head_armour::data()
|
||||||
.chain(lower_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.iter()
|
||||||
|
.map(|v| ((*v).0.clone(), &(*v).1)),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
torso_armour::data()
|
||||||
|
.iter()
|
||||||
|
.map(|v| ((*v).0.clone(), &(*v).1)),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
lower_armour::data()
|
||||||
|
.iter()
|
||||||
|
.map(|v| ((*v).0.clone(), &(*v).1)),
|
||||||
|
)
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -351,8 +413,15 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
|
|||||||
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
||||||
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
|
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
|
||||||
&RELEVANT.get_or_init(|| {
|
&RELEVANT.get_or_init(|| {
|
||||||
possession_data().iter()
|
possession_data()
|
||||||
.filter_map(|(pt, pd)| if pd.can_butcher { Some(pt.clone()) } else { None })
|
.iter()
|
||||||
|
.filter_map(|(pt, pd)| {
|
||||||
|
if pd.can_butcher {
|
||||||
|
Some(pt.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -364,7 +433,12 @@ mod tests {
|
|||||||
fn other_damage_types_add_to_less_than_one() {
|
fn other_damage_types_add_to_less_than_one() {
|
||||||
for (_pt, pd) in possession_data().iter() {
|
for (_pt, pd) in possession_data().iter() {
|
||||||
if let Some(weapon_data) = pd.weapon_data.as_ref() {
|
if let Some(weapon_data) = pd.weapon_data.as_ref() {
|
||||||
let tot: f64 = weapon_data.normal_attack.other_damage_types.iter().map(|v|v.0).sum();
|
let tot: f64 = weapon_data
|
||||||
|
.normal_attack
|
||||||
|
.other_damage_types
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.0)
|
||||||
|
.sum();
|
||||||
assert!(tot >= 0.0);
|
assert!(tot >= 0.0);
|
||||||
assert!(tot < 1.0);
|
assert!(tot < 1.0);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use super::{PossessionData, WeaponData, WeaponAttackData, PossessionType};
|
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
|
||||||
use crate::models::item::SkillType;
|
use crate::models::item::SkillType;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(PossessionType::ButcherKnife,
|
(PossessionType::ButcherKnife,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
use super::{PossessionData, PossessionType, WriteHandler, ArglessHandler, possession_data};
|
use super::{possession_data, ArglessHandler, PossessionData, PossessionType, WriteHandler};
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
|
||||||
item::{Item, ItemSpecialData},
|
|
||||||
corp::{Corp, CorpMembership, CorpPermission},
|
|
||||||
},
|
|
||||||
message_handler::user_commands::{
|
message_handler::user_commands::{
|
||||||
register::is_invalid_username,
|
parsing::parse_username, register::is_invalid_username, user_error,
|
||||||
parsing::parse_username,
|
CommandHandlingError::UserError, UResult, VerbContext,
|
||||||
user_error,
|
},
|
||||||
UResult,
|
models::{
|
||||||
CommandHandlingError::UserError,
|
corp::{Corp, CorpMembership, CorpPermission},
|
||||||
VerbContext,
|
item::{Item, ItemSpecialData},
|
||||||
},
|
},
|
||||||
services::comms::broadcast_to_room,
|
services::comms::broadcast_to_room,
|
||||||
};
|
};
|
||||||
@ -21,17 +17,23 @@ use once_cell::sync::OnceCell;
|
|||||||
|
|
||||||
use super::PossessionType::*;
|
use super::PossessionType::*;
|
||||||
|
|
||||||
pub struct CorpLicenceHandler {
|
pub struct CorpLicenceHandler {}
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl WriteHandler for CorpLicenceHandler {
|
impl WriteHandler for CorpLicenceHandler {
|
||||||
async fn write_cmd(&self, ctx: &mut VerbContext, _player: &Item, on_what: &Item, write_what: &str) -> UResult<()> {
|
async fn write_cmd(
|
||||||
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
_player: &Item,
|
||||||
|
on_what: &Item,
|
||||||
|
write_what: &str,
|
||||||
|
) -> UResult<()> {
|
||||||
let name = match parse_username(write_what) {
|
let name = match parse_username(write_what) {
|
||||||
Err(e) => user_error("Invalid corp name: ".to_owned() + e)?,
|
Err(e) => user_error("Invalid corp name: ".to_owned() + e)?,
|
||||||
Ok((_, rest)) if rest != "" =>
|
Ok((_, rest)) if rest != "" => {
|
||||||
user_error("No spaces allowed in corp names!".to_owned())?,
|
user_error("No spaces allowed in corp names!".to_owned())?
|
||||||
Ok((name, _)) => name
|
}
|
||||||
|
Ok((name, _)) => name,
|
||||||
};
|
};
|
||||||
if is_invalid_username(name) {
|
if is_invalid_username(name) {
|
||||||
user_error("Sorry, that corp name isn't allowed. Try another".to_owned())?;
|
user_error("Sorry, that corp name isn't allowed. Try another".to_owned())?;
|
||||||
@ -45,14 +47,22 @@ impl WriteHandler for CorpLicenceHandler {
|
|||||||
|
|
||||||
let mut item_clone = on_what.clone();
|
let mut item_clone = on_what.clone();
|
||||||
item_clone.special_data = Some(ItemSpecialData::ItemWriting {
|
item_clone.special_data = Some(ItemSpecialData::ItemWriting {
|
||||||
text: name.to_owned()
|
text: name.to_owned(),
|
||||||
});
|
});
|
||||||
ctx.trans.save_item_model(&item_clone).await?;
|
ctx.trans.save_item_model(&item_clone).await?;
|
||||||
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!(
|
ctx.trans
|
||||||
|
.queue_for_session(
|
||||||
|
ctx.session,
|
||||||
|
Some(&format!(
|
||||||
|
ansi!(
|
||||||
"The pencil makes a scratching sound as you mark the paper with the attached \
|
"The pencil makes a scratching sound as you mark the paper with the attached \
|
||||||
pencil and write \"{}\" on it. [Hint: Try the <bold>use<reset> command to submit \
|
pencil and write \"{}\" on it. [Hint: Try the <bold>use<reset> command to submit \
|
||||||
your signed paperwork and register the corporation, or <bold>write<reset> again \
|
your signed paperwork and register the corporation, or <bold>write<reset> again \
|
||||||
to erase and change the name].\n"), name))).await?;
|
to erase and change the name].\n"),
|
||||||
|
name
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -63,7 +73,7 @@ impl ArglessHandler for CorpLicenceHandler {
|
|||||||
async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> {
|
async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> {
|
||||||
let name = match what.special_data.as_ref() {
|
let name = match what.special_data.as_ref() {
|
||||||
Some(ItemSpecialData::ItemWriting { text }) => text,
|
Some(ItemSpecialData::ItemWriting { text }) => text,
|
||||||
_ => user_error("You have to write your corp's name on it first!".to_owned())?
|
_ => user_error("You have to write your corp's name on it first!".to_owned())?,
|
||||||
};
|
};
|
||||||
if ctx.trans.find_by_username(&name).await?.is_some() {
|
if ctx.trans.find_by_username(&name).await?.is_some() {
|
||||||
user_error("Corp name clashes with existing user name".to_owned())?;
|
user_error("Corp name clashes with existing user name".to_owned())?;
|
||||||
@ -72,40 +82,60 @@ impl ArglessHandler for CorpLicenceHandler {
|
|||||||
user_error("Corp name already taken!".to_owned())?;
|
user_error("Corp name already taken!".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.trans.get_corp_memberships_for_user(&player.item_code).await?.len() >= 5 {
|
if ctx
|
||||||
|
.trans
|
||||||
|
.get_corp_memberships_for_user(&player.item_code)
|
||||||
|
.await?
|
||||||
|
.len()
|
||||||
|
>= 5
|
||||||
|
{
|
||||||
user_error("You can't be in more than 5 corps".to_owned())?;
|
user_error("You can't be in more than 5 corps".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast_to_room(ctx.trans, &player.location, None,
|
broadcast_to_room(
|
||||||
|
ctx.trans,
|
||||||
|
&player.location,
|
||||||
|
None,
|
||||||
&format!(
|
&format!(
|
||||||
"{} signs a contract establishing {} as a corp\n",
|
"{} signs a contract establishing {} as a corp\n",
|
||||||
&player.display_for_sentence(true, 1, true),
|
&player.display_for_sentence(true, 1, true),
|
||||||
name
|
name
|
||||||
),
|
),
|
||||||
Some(
|
Some(&format!(
|
||||||
&format!("{} signs a contract establishing {} as a corp\n",
|
"{} signs a contract establishing {} as a corp\n",
|
||||||
&player.display_for_sentence(false, 1, true),
|
&player.display_for_sentence(false, 1, true),
|
||||||
name
|
name
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
)).await?;
|
.await?;
|
||||||
let corp_id = ctx.trans.create_corp(&Corp {
|
let corp_id = ctx
|
||||||
|
.trans
|
||||||
|
.create_corp(&Corp {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}).await?;
|
})
|
||||||
ctx.trans.upsert_corp_membership(
|
.await?;
|
||||||
&corp_id, &player.item_code,
|
ctx.trans
|
||||||
|
.upsert_corp_membership(
|
||||||
|
&corp_id,
|
||||||
|
&player.item_code,
|
||||||
&CorpMembership {
|
&CorpMembership {
|
||||||
joined_at: Some(Utc::now()),
|
joined_at: Some(Utc::now()),
|
||||||
permissions: vec!(CorpPermission::Holder),
|
permissions: vec![CorpPermission::Holder],
|
||||||
allow_combat: true,
|
allow_combat: true,
|
||||||
job_title: "Founder".to_owned(),
|
job_title: "Founder".to_owned(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}).await?;
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut what_mut = what.clone();
|
let mut what_mut = what.clone();
|
||||||
what_mut.possession_type = Some(CertificateOfIncorporation);
|
what_mut.possession_type = Some(CertificateOfIncorporation);
|
||||||
let cp_data = possession_data().get(&CertificateOfIncorporation)
|
let cp_data = possession_data()
|
||||||
.ok_or_else(|| UserError("Certificate of Incorporation no longer exists as an item".to_owned()))?;
|
.get(&CertificateOfIncorporation)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError("Certificate of Incorporation no longer exists as an item".to_owned())
|
||||||
|
})?;
|
||||||
what_mut.display = cp_data.display.to_owned();
|
what_mut.display = cp_data.display.to_owned();
|
||||||
what_mut.details = Some(cp_data.details.to_owned());
|
what_mut.details = Some(cp_data.details.to_owned());
|
||||||
ctx.trans.save_item_model(&what_mut).await?;
|
ctx.trans.save_item_model(&what_mut).await?;
|
||||||
@ -117,8 +147,7 @@ impl ArglessHandler for CorpLicenceHandler {
|
|||||||
static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {};
|
static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {};
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(NewCorpLicence,
|
(NewCorpLicence,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,40 +1,35 @@
|
|||||||
use super::{PossessionData, WeaponData, WeaponAttackData};
|
use super::{PossessionData, WeaponAttackData, WeaponData};
|
||||||
use crate::models::item::SkillType;
|
use crate::models::item::SkillType;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static PossessionData {
|
pub fn data() -> &'static PossessionData {
|
||||||
static D: OnceCell<PossessionData> = OnceCell::new();
|
static D: OnceCell<PossessionData> = OnceCell::new();
|
||||||
D.get_or_init(
|
D.get_or_init(|| PossessionData {
|
||||||
||
|
|
||||||
PossessionData {
|
|
||||||
weapon_data: Some(WeaponData {
|
weapon_data: Some(WeaponData {
|
||||||
uses_skill: SkillType::Fists,
|
uses_skill: SkillType::Fists,
|
||||||
raw_min_to_learn: 0.0,
|
raw_min_to_learn: 0.0,
|
||||||
raw_max_to_learn: 2.0,
|
raw_max_to_learn: 2.0,
|
||||||
normal_attack: WeaponAttackData {
|
normal_attack: WeaponAttackData {
|
||||||
start_messages: vec!(
|
start_messages: vec![Box::new(|attacker, victim, exp| {
|
||||||
Box::new(|attacker, victim, exp|
|
format!(
|
||||||
format!("{} bares {} teeth and lunges at {}",
|
"{} bares {} teeth and lunges at {}",
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&attacker.pronouns.possessive,
|
&attacker.pronouns.possessive,
|
||||||
&victim.display_for_sentence(exp, 1, false),
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
)
|
)
|
||||||
)
|
})],
|
||||||
),
|
success_messages: vec![Box::new(|attacker, victim, part, exp| {
|
||||||
success_messages: vec!(
|
format!(
|
||||||
Box::new(|attacker, victim, part, exp|
|
"{}'s teeth connect and tear at the flesh of {}'s {}",
|
||||||
format!("{}'s teeth connect and tear at the flesh of {}'s {}",
|
|
||||||
&attacker.display_for_sentence(exp, 1, true),
|
&attacker.display_for_sentence(exp, 1, true),
|
||||||
&victim.display_for_sentence(exp, 1, false),
|
&victim.display_for_sentence(exp, 1, false),
|
||||||
&part.display(victim.sex.clone())
|
&part.display(victim.sex.clone())
|
||||||
)
|
)
|
||||||
)
|
})],
|
||||||
),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
|
use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
|
||||||
use crate::static_content::species::BodyPart;
|
use crate::static_content::species::BodyPart;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(
|
(
|
||||||
PossessionType::RustyMetalPot,
|
PossessionType::RustyMetalPot,
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
use super::{PossessionData, ArglessHandler, PossessionType};
|
use super::{ArglessHandler, PossessionData, PossessionType};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
message_handler::user_commands::{user_error, UResult, VerbContext},
|
||||||
models::item::{Item, LocationActionType},
|
models::item::{Item, LocationActionType},
|
||||||
message_handler::user_commands::{user_error, VerbContext, UResult},
|
services::{
|
||||||
static_content::{
|
capacity::{check_item_capacity, CapacityLevel},
|
||||||
possession_type::InstallHandler,
|
comms::broadcast_to_room,
|
||||||
room::Direction,
|
|
||||||
},
|
},
|
||||||
services::{comms::broadcast_to_room,
|
static_content::{possession_type::InstallHandler, room::Direction},
|
||||||
capacity::{check_item_capacity, CapacityLevel}}
|
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -32,23 +31,34 @@ static LOCK_WEIGHT: u64 = 500;
|
|||||||
struct ScanLockInstall;
|
struct ScanLockInstall;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl InstallHandler for ScanLockInstall {
|
impl InstallHandler for ScanLockInstall {
|
||||||
async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item,
|
async fn install_cmd(
|
||||||
direction: &Direction) -> UResult<()> {
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
player: &Item,
|
||||||
|
what: &Item,
|
||||||
|
room: &Item,
|
||||||
|
direction: &Direction,
|
||||||
|
) -> UResult<()> {
|
||||||
if what.action_type != LocationActionType::Normal {
|
if what.action_type != LocationActionType::Normal {
|
||||||
user_error("That scanlock is already in use.".to_owned())?;
|
user_error("That scanlock is already in use.".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.trans.find_by_action_and_location(
|
if !ctx
|
||||||
|
.trans
|
||||||
|
.find_by_action_and_location(
|
||||||
&room.refstr(),
|
&room.refstr(),
|
||||||
&LocationActionType::InstalledOnDoorAsLock(direction.clone())
|
&LocationActionType::InstalledOnDoorAsLock(direction.clone()),
|
||||||
).await?.is_empty() {
|
)
|
||||||
|
.await?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
user_error("There's already a lock on that door - uninstall it first.".to_owned())?;
|
user_error("There's already a lock on that door - uninstall it first.".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? {
|
match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? {
|
||||||
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit =>
|
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => user_error(
|
||||||
user_error("That room has so much stuff, you can't install anything new."
|
"That room has so much stuff, you can't install anything new.".to_owned(),
|
||||||
.to_owned())?,
|
)?,
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
let mut what_mut = (*what).clone();
|
let mut what_mut = (*what).clone();
|
||||||
@ -58,32 +68,45 @@ impl InstallHandler for ScanLockInstall {
|
|||||||
ctx.trans.save_item_model(&what_mut).await?;
|
ctx.trans.save_item_model(&what_mut).await?;
|
||||||
|
|
||||||
broadcast_to_room(
|
broadcast_to_room(
|
||||||
&ctx.trans, &room.refstr(), None,
|
&ctx.trans,
|
||||||
&format!("{} bangs the door to the {} as he installs {} on it.\n",
|
&room.refstr(),
|
||||||
|
None,
|
||||||
|
&format!(
|
||||||
|
"{} bangs the door to the {} as he installs {} on it.\n",
|
||||||
&player.display_for_sentence(true, 1, true),
|
&player.display_for_sentence(true, 1, true),
|
||||||
&direction.describe(),
|
&direction.describe(),
|
||||||
&what.display_for_sentence(true, 1, false)),
|
&what.display_for_sentence(true, 1, false)
|
||||||
Some(
|
),
|
||||||
&format!("{} bangs the door to the {} as he installs {} on it.\n",
|
Some(&format!(
|
||||||
|
"{} bangs the door to the {} as he installs {} on it.\n",
|
||||||
&player.display_for_sentence(false, 1, true),
|
&player.display_for_sentence(false, 1, true),
|
||||||
&direction.describe(),
|
&direction.describe(),
|
||||||
&what.display_for_sentence(false, 1, false)),
|
&what.display_for_sentence(false, 1, false)
|
||||||
)).await?;
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item,
|
async fn uninstall_cmd(
|
||||||
direction: &Direction) -> UResult<()> {
|
&self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
player: &Item,
|
||||||
|
what: &Item,
|
||||||
|
room: &Item,
|
||||||
|
direction: &Direction,
|
||||||
|
) -> UResult<()> {
|
||||||
if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) {
|
if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) {
|
||||||
user_error("That scanlock is not installed as a lock.".to_owned())?;
|
user_error("That scanlock is not installed as a lock.".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut what_mut = (*what).clone();
|
let mut what_mut = (*what).clone();
|
||||||
|
|
||||||
let extra_text = match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? {
|
let extra_text =
|
||||||
|
match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? {
|
||||||
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
|
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
|
||||||
", dropping it on the floor since he can't hold it."
|
", dropping it on the floor since he can't hold it."
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
what_mut.location = player.refstr();
|
what_mut.location = player.refstr();
|
||||||
""
|
""
|
||||||
@ -94,29 +117,31 @@ impl InstallHandler for ScanLockInstall {
|
|||||||
ctx.trans.save_item_model(&what_mut).await?;
|
ctx.trans.save_item_model(&what_mut).await?;
|
||||||
|
|
||||||
broadcast_to_room(
|
broadcast_to_room(
|
||||||
&ctx.trans, &room.refstr(), None,
|
&ctx.trans,
|
||||||
&format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n",
|
&room.refstr(),
|
||||||
|
None,
|
||||||
|
&format!(
|
||||||
|
"{} bangs the door to the {} as he uninstalls {} from it{}.\n",
|
||||||
&player.display_for_sentence(true, 1, true),
|
&player.display_for_sentence(true, 1, true),
|
||||||
&direction.describe(),
|
&direction.describe(),
|
||||||
&what.display_for_sentence(true, 1, false),
|
&what.display_for_sentence(true, 1, false),
|
||||||
extra_text
|
extra_text
|
||||||
),
|
),
|
||||||
Some(
|
Some(&format!(
|
||||||
&format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n",
|
"{} bangs the door to the {} as he uninstalls {} from it{}.\n",
|
||||||
&player.display_for_sentence(false, 1, true),
|
&player.display_for_sentence(false, 1, true),
|
||||||
&direction.describe(),
|
&direction.describe(),
|
||||||
&what.display_for_sentence(false, 1, false),
|
&what.display_for_sentence(false, 1, false),
|
||||||
extra_text
|
extra_text
|
||||||
),
|
)),
|
||||||
)).await?;
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(PossessionType::Scanlock,
|
(PossessionType::Scanlock,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
|
use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
|
||||||
use crate::static_content::species::BodyPart;
|
use crate::static_content::species::BodyPart;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(
|
(
|
||||||
PossessionType::LeatherPants,
|
PossessionType::LeatherPants,
|
||||||
|
@ -2,8 +2,7 @@ use super::{PossessionData, PossessionType};
|
|||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(PossessionType::AnimalSkin,
|
(PossessionType::AnimalSkin,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use super::{PossessionData, PossessionType, WearData, DamageType, SoakData};
|
use super::{DamageType, PossessionData, PossessionType, SoakData, WearData};
|
||||||
use crate::static_content::species::BodyPart;
|
use crate::static_content::species::BodyPart;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(
|
(
|
||||||
PossessionType::LeatherJacket,
|
PossessionType::LeatherJacket,
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
use super::{PossessionData, PossessionType, UseData, UseEffect, ChargeData};
|
use super::{ChargeData, PossessionData, PossessionType, UseData, UseEffect};
|
||||||
use crate::models::{
|
use crate::models::{consent::ConsentType, item::SkillType};
|
||||||
item::SkillType,
|
|
||||||
consent::ConsentType,
|
|
||||||
};
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
use super::PossessionType::*;
|
use super::PossessionType::*;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(MediumTraumaKit,
|
(MediumTraumaKit,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
use super::{PossessionData, PossessionType, WeaponData, WeaponAttackData};
|
use super::{PossessionData, PossessionType, WeaponAttackData, WeaponData};
|
||||||
use crate::models::item::SkillType;
|
use crate::models::item::SkillType;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
OnceCell::new();
|
|
||||||
&D.get_or_init(|| vec!(
|
&D.get_or_init(|| vec!(
|
||||||
(PossessionType::AntennaWhip,
|
(PossessionType::AntennaWhip,
|
||||||
PossessionData {
|
PossessionData {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use uuid::Uuid;
|
|
||||||
use serde::*;
|
use serde::*;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum MessageFromListener {
|
pub enum MessageFromListener {
|
||||||
@ -7,7 +7,7 @@ pub enum MessageFromListener {
|
|||||||
SessionConnected { session: Uuid, source: String },
|
SessionConnected { session: Uuid, source: String },
|
||||||
SessionDisconnected { session: Uuid },
|
SessionDisconnected { session: Uuid },
|
||||||
SessionSentLine { session: Uuid, msg: String },
|
SessionSentLine { session: Uuid, msg: String },
|
||||||
AcknowledgeMessage
|
AcknowledgeMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -15,5 +15,5 @@ pub enum MessageToListener {
|
|||||||
GameserverVersion { version: String },
|
GameserverVersion { version: String },
|
||||||
DisconnectSession { session: Uuid },
|
DisconnectSession { session: Uuid },
|
||||||
SendToSession { session: Uuid, msg: String },
|
SendToSession { session: Uuid, msg: String },
|
||||||
AcknowledgeMessage
|
AcknowledgeMessage,
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,12 @@ use std::process::Command;
|
|||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
let cmdout = Command::new("git")
|
let cmdout = Command::new("git")
|
||||||
.arg("rev-parse").arg("HEAD")
|
.arg("rev-parse")
|
||||||
.output().expect("git rev-parse HEAD failed");
|
.arg("HEAD")
|
||||||
println!("cargo:rustc-env=GIT_VERSION={}",
|
.output()
|
||||||
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8"));
|
.expect("git rev-parse HEAD failed");
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-env=GIT_VERSION={}",
|
||||||
|
String::from_utf8(cmdout.stdout).expect("git revision not UTF-8")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,32 @@
|
|||||||
use std::vec::Vec;
|
use blastmud_interfaces::*;
|
||||||
use std::path::Path;
|
use futures::prelude::*;
|
||||||
|
use log::{info, warn, LevelFilter};
|
||||||
|
use nix::{
|
||||||
|
sys::signal::{kill, Signal},
|
||||||
|
unistd::{getpid, Pid},
|
||||||
|
};
|
||||||
|
use serde::*;
|
||||||
|
use simple_logger::SimpleLogger;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use serde::*;
|
use std::net::SocketAddr;
|
||||||
use tokio::task;
|
use std::path::Path;
|
||||||
use tokio::time::{self, Duration};
|
use std::sync::Arc;
|
||||||
use tokio::net::{TcpStream, TcpListener, lookup_host};
|
use std::time::Instant;
|
||||||
|
use std::vec::Vec;
|
||||||
|
use tokio::io::{AsyncWriteExt, BufReader};
|
||||||
|
use tokio::net::{lookup_host, TcpListener, TcpStream};
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
use tokio::sync::{mpsc, Mutex, RwLock};
|
use tokio::sync::{mpsc, Mutex, RwLock};
|
||||||
use tokio::io::{BufReader, AsyncWriteExt};
|
use tokio::task;
|
||||||
use log::{warn, info, LevelFilter};
|
use tokio::time::{self, Duration};
|
||||||
use simple_logger::SimpleLogger;
|
use tokio_serde::formats::Cbor;
|
||||||
use std::sync::Arc;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use blastmud_interfaces::*;
|
|
||||||
use tokio_util::codec;
|
use tokio_util::codec;
|
||||||
use tokio_util::codec::length_delimited::LengthDelimitedCodec;
|
use tokio_util::codec::length_delimited::LengthDelimitedCodec;
|
||||||
use tokio_serde::formats::Cbor;
|
|
||||||
use futures::prelude::*;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use warp::{self, filters::ws, Filter, Reply};
|
||||||
use warp::{
|
|
||||||
self, filters::ws, Filter, Reply
|
|
||||||
};
|
|
||||||
use std::time::Instant;
|
|
||||||
use nix::{sys::signal::{kill, Signal}, unistd::{Pid, getpid}};
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
struct Config {
|
struct Config {
|
||||||
@ -38,14 +39,14 @@ struct Config {
|
|||||||
type DResult<A> = Result<A, Box<dyn Error + Send + Sync>>;
|
type DResult<A> = Result<A, Box<dyn Error + Send + Sync>>;
|
||||||
|
|
||||||
fn read_latest_config() -> DResult<Config> {
|
fn read_latest_config() -> DResult<Config> {
|
||||||
serde_yaml::from_str(&fs::read_to_string("listener.conf")?).
|
serde_yaml::from_str(&fs::read_to_string("listener.conf")?)
|
||||||
map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
|
.map_err(|error| Box::new(error) as Box<dyn Error + Send + Sync>)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum ServerTaskCommand {
|
enum ServerTaskCommand {
|
||||||
SwitchTo { new_server: String },
|
SwitchTo { new_server: String },
|
||||||
Send { message: MessageFromListener }
|
Send { message: MessageFromListener },
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_server_task<FHandler, HandlerFut>(
|
fn run_server_task<FHandler, HandlerFut>(
|
||||||
@ -54,32 +55,31 @@ fn run_server_task<FHandler, HandlerFut>(
|
|||||||
mut receiver: ReceiverStream<ServerTaskCommand>,
|
mut receiver: ReceiverStream<ServerTaskCommand>,
|
||||||
sender: mpsc::Sender<ServerTaskCommand>,
|
sender: mpsc::Sender<ServerTaskCommand>,
|
||||||
server: String,
|
server: String,
|
||||||
message_handler: FHandler
|
message_handler: FHandler,
|
||||||
)
|
) where
|
||||||
where
|
|
||||||
FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static,
|
FHandler: Fn(MessageToListener) -> HandlerFut + Send + 'static,
|
||||||
HandlerFut: Future<Output = ()> + Send + 'static
|
HandlerFut: Future<Output = ()> + Send + 'static,
|
||||||
{
|
{
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
let conn = loop {
|
let conn = loop {
|
||||||
match TcpStream::connect(&server).await {
|
match TcpStream::connect(&server).await {
|
||||||
Err(e) => warn!("Can't connect to {}: {}", server, e),
|
Err(e) => warn!("Can't connect to {}: {}", server, e),
|
||||||
Ok(c) => break c
|
Ok(c) => break c,
|
||||||
}
|
}
|
||||||
time::sleep(Duration::from_secs(1)).await;
|
time::sleep(Duration::from_secs(1)).await;
|
||||||
};
|
};
|
||||||
let mut conn_framed = tokio_serde::Framed::new(
|
let mut conn_framed = tokio_serde::Framed::new(
|
||||||
codec::Framed::new(conn, LengthDelimitedCodec::new()),
|
codec::Framed::new(conn, LengthDelimitedCodec::new()),
|
||||||
Cbor::<MessageToListener, MessageFromListener>::default()
|
Cbor::<MessageToListener, MessageFromListener>::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut commands = stream::iter(vec!(
|
let mut commands = stream::iter(vec![ServerTaskCommand::Send {
|
||||||
ServerTaskCommand::Send {
|
message: MessageFromListener::ListenerPing { uuid: listener_id },
|
||||||
message: MessageFromListener::ListenerPing { uuid: listener_id }
|
}])
|
||||||
})
|
.chain(stream::iter(
|
||||||
).chain(
|
unfinished_business.map(|message| ServerTaskCommand::Send { message }),
|
||||||
stream::iter(unfinished_business.map(|message| ServerTaskCommand::Send { message }))
|
))
|
||||||
).chain(&mut receiver);
|
.chain(&mut receiver);
|
||||||
|
|
||||||
'full_select: loop {
|
'full_select: loop {
|
||||||
tokio::select!(
|
tokio::select!(
|
||||||
@ -216,24 +216,22 @@ where
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SessionCommand {
|
enum SessionCommand {
|
||||||
Disconnect,
|
Disconnect,
|
||||||
SendString { message : String }
|
SendString { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SessionRecord {
|
struct SessionRecord {
|
||||||
channel: mpsc::Sender<SessionCommand>,
|
channel: mpsc::Sender<SessionCommand>,
|
||||||
disconnect_channel: mpsc::UnboundedSender<()>
|
disconnect_channel: mpsc::UnboundedSender<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SessionIndexes {
|
struct SessionIndexes {
|
||||||
by_uuid: BTreeMap<Uuid, SessionRecord>,
|
by_uuid: BTreeMap<Uuid, SessionRecord>,
|
||||||
count_by_source: BTreeMap<String, u64>
|
count_by_source: BTreeMap<String, u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type SessionMap = Arc<Mutex<SessionIndexes>>;
|
type SessionMap = Arc<Mutex<SessionIndexes>>;
|
||||||
@ -244,19 +242,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
|
|||||||
MessageToListener::GameserverVersion { version } => {
|
MessageToListener::GameserverVersion { version } => {
|
||||||
let mut version_mut = version_data().write().await;
|
let mut version_mut = version_data().write().await;
|
||||||
(*version_mut).gameserver_version = Some(version.clone());
|
(*version_mut).gameserver_version = Some(version.clone());
|
||||||
},
|
}
|
||||||
MessageToListener::DisconnectSession { session } => {
|
MessageToListener::DisconnectSession { session } => {
|
||||||
match session_map.lock().await.by_uuid.get(&session) {
|
match session_map.lock().await.by_uuid.get(&session) {
|
||||||
// Just silently ignore it if they are disconnected.
|
// Just silently ignore it if they are disconnected.
|
||||||
None => {}
|
None => {}
|
||||||
Some(SessionRecord { channel, disconnect_channel, .. }) => {
|
Some(SessionRecord {
|
||||||
match channel.try_send(SessionCommand::Disconnect) {
|
channel,
|
||||||
|
disconnect_channel,
|
||||||
|
..
|
||||||
|
}) => match channel.try_send(SessionCommand::Disconnect) {
|
||||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||||
disconnect_channel.send(()).unwrap_or(());
|
disconnect_channel.send(()).unwrap_or(());
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageToListener::SendToSession { session, msg } => {
|
MessageToListener::SendToSession { session, msg } => {
|
||||||
@ -264,7 +264,8 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
|
|||||||
// Just silently ignore it if they are disconnected.
|
// Just silently ignore it if they are disconnected.
|
||||||
None => {}
|
None => {}
|
||||||
Some(SessionRecord { channel, .. }) => {
|
Some(SessionRecord { channel, .. }) => {
|
||||||
channel.try_send(SessionCommand::SendString { message: msg })
|
channel
|
||||||
|
.try_send(SessionCommand::SendString { message: msg })
|
||||||
.unwrap_or(());
|
.unwrap_or(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -272,14 +273,21 @@ async fn handle_server_message(session_map: SessionMap, message: MessageToListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_server_task(listener_id: Uuid,
|
fn start_server_task(
|
||||||
|
listener_id: Uuid,
|
||||||
server: String,
|
server: String,
|
||||||
session_map: SessionMap) -> mpsc::Sender<ServerTaskCommand> {
|
session_map: SessionMap,
|
||||||
|
) -> mpsc::Sender<ServerTaskCommand> {
|
||||||
let (sender, receiver) = mpsc::channel(20);
|
let (sender, receiver) = mpsc::channel(20);
|
||||||
let receiver_stream = ReceiverStream::new(receiver);
|
let receiver_stream = ReceiverStream::new(receiver);
|
||||||
run_server_task(None, listener_id, receiver_stream, sender.clone(), server,
|
run_server_task(
|
||||||
move |msg| handle_server_message(session_map.clone(),
|
None,
|
||||||
msg) );
|
listener_id,
|
||||||
|
receiver_stream,
|
||||||
|
sender.clone(),
|
||||||
|
server,
|
||||||
|
move |msg| handle_server_message(session_map.clone(), msg),
|
||||||
|
);
|
||||||
sender
|
sender
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,13 +307,13 @@ impl TokenBucket {
|
|||||||
level: initial_level,
|
level: initial_level,
|
||||||
last_topup: Instant::now(),
|
last_topup: Instant::now(),
|
||||||
max_level,
|
max_level,
|
||||||
alloc_per_ms
|
alloc_per_ms,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(self: &mut Self) {
|
pub fn update(self: &mut Self) {
|
||||||
self.level =
|
self.level = (self.level
|
||||||
(self.level + self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64))
|
+ self.alloc_per_ms * (self.last_topup.elapsed().as_millis() as f64))
|
||||||
.min(self.max_level);
|
.min(self.max_level);
|
||||||
self.last_topup = Instant::now();
|
self.last_topup = Instant::now();
|
||||||
}
|
}
|
||||||
@ -340,19 +348,18 @@ async fn handle_client_socket(
|
|||||||
server: mpsc::Sender<ServerTaskCommand>,
|
server: mpsc::Sender<ServerTaskCommand>,
|
||||||
active_sessions: SessionMap,
|
active_sessions: SessionMap,
|
||||||
mut stream: TcpStream,
|
mut stream: TcpStream,
|
||||||
addr: SocketAddr
|
addr: SocketAddr,
|
||||||
) {
|
) {
|
||||||
let (rstream, mut wstream) = stream.split();
|
let (rstream, mut wstream) = stream.split();
|
||||||
let mut rbuf = codec::FramedRead::new(
|
let mut rbuf = codec::FramedRead::new(
|
||||||
BufReader::new(rstream),
|
BufReader::new(rstream),
|
||||||
codec::LinesCodec::new_with_max_length(512)
|
codec::LinesCodec::new_with_max_length(512),
|
||||||
);
|
);
|
||||||
let session = Uuid::new_v4();
|
let session = Uuid::new_v4();
|
||||||
let mut tok_bucket =
|
let mut tok_bucket =
|
||||||
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
|
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
|
||||||
info!("Accepted session {} from {}", session, addr);
|
info!("Accepted session {} from {}", session, addr);
|
||||||
|
|
||||||
|
|
||||||
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
|
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
|
||||||
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
|
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
@ -360,8 +367,14 @@ async fn handle_client_socket(
|
|||||||
let addr_str = addr.ip().to_string();
|
let addr_str = addr.ip().to_string();
|
||||||
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
|
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
|
||||||
drop(sess_idx_lock);
|
drop(sess_idx_lock);
|
||||||
info!("Rejecting session {} because of too many concurrent connections", session);
|
info!(
|
||||||
match wstream.write_all("Too many connections from same IP\r\n".as_bytes()).await {
|
"Rejecting session {} because of too many concurrent connections",
|
||||||
|
session
|
||||||
|
);
|
||||||
|
match wstream
|
||||||
|
.write_all("Too many connections from same IP\r\n".as_bytes())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Client connection {} got error {}", session, e);
|
info!("Client connection {} got error {}", session, e);
|
||||||
}
|
}
|
||||||
@ -369,17 +382,31 @@ async fn handle_client_socket(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|c| { *c += 1; }).or_insert(1);
|
sess_idx_lock
|
||||||
|
.count_by_source
|
||||||
|
.entry(addr_str.clone())
|
||||||
|
.and_modify(|c| {
|
||||||
|
*c += 1;
|
||||||
|
})
|
||||||
|
.or_insert(1);
|
||||||
sess_idx_lock.by_uuid.insert(
|
sess_idx_lock.by_uuid.insert(
|
||||||
session, SessionRecord {
|
session,
|
||||||
|
SessionRecord {
|
||||||
channel: lsender.clone(),
|
channel: lsender.clone(),
|
||||||
disconnect_channel: discon_sender.clone()
|
disconnect_channel: discon_sender.clone(),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
drop(sess_idx_lock);
|
drop(sess_idx_lock);
|
||||||
|
|
||||||
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected {
|
server
|
||||||
session, source: addr.to_string()
|
.send(ServerTaskCommand::Send {
|
||||||
}}).await.unwrap();
|
message: MessageFromListener::SessionConnected {
|
||||||
|
session,
|
||||||
|
source: addr.to_string(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
'client_loop: loop {
|
'client_loop: loop {
|
||||||
tok_bucket.update();
|
tok_bucket.update();
|
||||||
@ -437,13 +464,21 @@ async fn handle_client_socket(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected {
|
server
|
||||||
session
|
.send(ServerTaskCommand::Send {
|
||||||
}}).await.unwrap();
|
message: MessageFromListener::SessionDisconnected { session },
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
sess_idx_lock = active_sessions.lock().await;
|
sess_idx_lock = active_sessions.lock().await;
|
||||||
sess_idx_lock.by_uuid.remove(&session);
|
sess_idx_lock.by_uuid.remove(&session);
|
||||||
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|v| { *v -= 1; });
|
sess_idx_lock
|
||||||
|
.count_by_source
|
||||||
|
.entry(addr_str.clone())
|
||||||
|
.and_modify(|v| {
|
||||||
|
*v -= 1;
|
||||||
|
});
|
||||||
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
|
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
|
||||||
sess_idx_lock.count_by_source.remove(&addr_str);
|
sess_idx_lock.count_by_source.remove(&addr_str);
|
||||||
}
|
}
|
||||||
@ -453,9 +488,12 @@ fn start_pinger(listener: Uuid, server: mpsc::Sender<ServerTaskCommand>) {
|
|||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
time::sleep(Duration::from_secs(60)).await;
|
time::sleep(Duration::from_secs(60)).await;
|
||||||
server.send(ServerTaskCommand::Send {
|
server
|
||||||
message: MessageFromListener::ListenerPing { uuid: listener }
|
.send(ServerTaskCommand::Send {
|
||||||
}).await.unwrap();
|
message: MessageFromListener::ListenerPing { uuid: listener },
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -464,10 +502,13 @@ async fn handle_websocket(
|
|||||||
mut ws: ws::WebSocket,
|
mut ws: ws::WebSocket,
|
||||||
src: String,
|
src: String,
|
||||||
active_sessions: SessionMap,
|
active_sessions: SessionMap,
|
||||||
server: mpsc::Sender<ServerTaskCommand>
|
server: mpsc::Sender<ServerTaskCommand>,
|
||||||
) {
|
) {
|
||||||
let session = Uuid::new_v4();
|
let session = Uuid::new_v4();
|
||||||
info!("Accepted websocket session {} with forwarded-for {}", session, src);
|
info!(
|
||||||
|
"Accepted websocket session {} with forwarded-for {}",
|
||||||
|
session, src
|
||||||
|
);
|
||||||
|
|
||||||
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
|
let (lsender, mut lreceiver) = mpsc::channel(MAX_CAPACITY);
|
||||||
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
|
let (discon_sender, mut discon_receiver) = mpsc::unbounded_channel();
|
||||||
@ -476,8 +517,14 @@ async fn handle_websocket(
|
|||||||
let addr_str: String = src.split(" ").last().unwrap_or("").to_string();
|
let addr_str: String = src.split(" ").last().unwrap_or("").to_string();
|
||||||
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
|
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&0) >= MAX_CONNS_PER_IP {
|
||||||
drop(sess_idx_lock);
|
drop(sess_idx_lock);
|
||||||
info!("Rejecting session {} because of too many concurrent connections", session);
|
info!(
|
||||||
match ws.send(ws::Message::text("Too many connections from same IP\r\n")).await {
|
"Rejecting session {} because of too many concurrent connections",
|
||||||
|
session
|
||||||
|
);
|
||||||
|
match ws
|
||||||
|
.send(ws::Message::text("Too many connections from same IP\r\n"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("Client connection {} got error {}", session, e);
|
info!("Client connection {} got error {}", session, e);
|
||||||
}
|
}
|
||||||
@ -485,17 +532,31 @@ async fn handle_websocket(
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|c| { *c += 1; }).or_insert(1);
|
sess_idx_lock
|
||||||
|
.count_by_source
|
||||||
|
.entry(addr_str.clone())
|
||||||
|
.and_modify(|c| {
|
||||||
|
*c += 1;
|
||||||
|
})
|
||||||
|
.or_insert(1);
|
||||||
sess_idx_lock.by_uuid.insert(
|
sess_idx_lock.by_uuid.insert(
|
||||||
session, SessionRecord {
|
session,
|
||||||
|
SessionRecord {
|
||||||
channel: lsender.clone(),
|
channel: lsender.clone(),
|
||||||
disconnect_channel: discon_sender.clone()
|
disconnect_channel: discon_sender.clone(),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
drop(sess_idx_lock);
|
drop(sess_idx_lock);
|
||||||
|
|
||||||
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionConnected {
|
server
|
||||||
session, source: src
|
.send(ServerTaskCommand::Send {
|
||||||
}}).await.unwrap();
|
message: MessageFromListener::SessionConnected {
|
||||||
|
session,
|
||||||
|
source: src,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let mut tok_bucket =
|
let mut tok_bucket =
|
||||||
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
|
TokenBucket::new(CLIENT_INITIAL_TOKENS, CLIENT_MAX_LEVEL, CLIENT_ALLOC_PER_MS);
|
||||||
|
|
||||||
@ -576,27 +637,35 @@ async fn handle_websocket(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.send(ServerTaskCommand::Send { message: MessageFromListener::SessionDisconnected {
|
server
|
||||||
session
|
.send(ServerTaskCommand::Send {
|
||||||
}}).await.unwrap();
|
message: MessageFromListener::SessionDisconnected { session },
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
sess_idx_lock = active_sessions.lock().await;
|
sess_idx_lock = active_sessions.lock().await;
|
||||||
sess_idx_lock.by_uuid.remove(&session);
|
sess_idx_lock.by_uuid.remove(&session);
|
||||||
sess_idx_lock.count_by_source.entry(addr_str.clone()).and_modify(|v| { *v -= 1; });
|
sess_idx_lock
|
||||||
|
.count_by_source
|
||||||
|
.entry(addr_str.clone())
|
||||||
|
.and_modify(|v| {
|
||||||
|
*v -= 1;
|
||||||
|
});
|
||||||
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
|
if *sess_idx_lock.count_by_source.get(&addr_str).unwrap_or(&1) <= 0 {
|
||||||
sess_idx_lock.count_by_source.remove(&addr_str);
|
sess_idx_lock.count_by_source.remove(&addr_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn upgrade_websocket(src: String, wsreq: ws::Ws,
|
async fn upgrade_websocket(
|
||||||
|
src: String,
|
||||||
|
wsreq: ws::Ws,
|
||||||
active_sessions: SessionMap,
|
active_sessions: SessionMap,
|
||||||
server_sender: mpsc::Sender<ServerTaskCommand>) ->
|
server_sender: mpsc::Sender<ServerTaskCommand>,
|
||||||
Result<warp::reply::Response, warp::Rejection> {
|
) -> Result<warp::reply::Response, warp::Rejection> {
|
||||||
Ok(
|
Ok(wsreq
|
||||||
wsreq.on_upgrade(|wss| handle_websocket(
|
.on_upgrade(|wss| handle_websocket(wss, src, active_sessions, server_sender))
|
||||||
wss, src, active_sessions,
|
.into_response())
|
||||||
server_sender)).into_response()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -620,44 +689,52 @@ async fn respond_version() -> Result<warp::reply::Response, warp::Rejection> {
|
|||||||
Ok(warp::reply::json(&*version_data().read().await).into_response())
|
Ok(warp::reply::json(&*version_data().read().await).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_websocket(bind: String, active_sessions: SessionMap, server_sender: mpsc::Sender<ServerTaskCommand>) -> DResult<()> {
|
async fn start_websocket(
|
||||||
let sockaddr = lookup_host(bind).await?.next().expect("Can't resolve websocket bind name");
|
bind: String,
|
||||||
let routes =
|
active_sessions: SessionMap,
|
||||||
warp::get()
|
server_sender: mpsc::Sender<ServerTaskCommand>,
|
||||||
|
) -> DResult<()> {
|
||||||
|
let sockaddr = lookup_host(bind)
|
||||||
|
.await?
|
||||||
|
.next()
|
||||||
|
.expect("Can't resolve websocket bind name");
|
||||||
|
let routes = warp::get()
|
||||||
.and(warp::path("wsgame"))
|
.and(warp::path("wsgame"))
|
||||||
.and(warp::header("X-Forwarded-For"))
|
.and(warp::header("X-Forwarded-For"))
|
||||||
.and(ws::ws())
|
.and(ws::ws())
|
||||||
.and_then(move |src, wsreq| upgrade_websocket(src, wsreq, active_sessions.clone(), server_sender.clone()))
|
.and_then(move |src, wsreq| {
|
||||||
|
upgrade_websocket(src, wsreq, active_sessions.clone(), server_sender.clone())
|
||||||
|
})
|
||||||
.or(warp::get()
|
.or(warp::get()
|
||||||
.and(warp::path("version"))
|
.and(warp::path("version"))
|
||||||
.and_then(|| respond_version()));
|
.and_then(|| respond_version()));
|
||||||
|
|
||||||
task::spawn(
|
task::spawn(warp::serve(routes).run(sockaddr));
|
||||||
warp::serve(
|
|
||||||
routes
|
|
||||||
).run(sockaddr)
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
|
pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
|
||||||
match fs::read_to_string(pidfile) {
|
match fs::read_to_string(pidfile) {
|
||||||
Err(e) =>
|
Err(e) => {
|
||||||
if e.kind() == std::io::ErrorKind::NotFound {
|
if e.kind() == std::io::ErrorKind::NotFound {
|
||||||
info!("pidfile not found, assuming not already running");
|
info!("pidfile not found, assuming not already running");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
info!("Error reading pidfile (other than NotFound): {}", e);
|
info!("Error reading pidfile (other than NotFound): {}", e);
|
||||||
Err(Box::new(e) as Box::<dyn Error + Send + Sync>)
|
Err(Box::new(e) as Box<dyn Error + Send + Sync>)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(f) => {
|
Ok(f) => {
|
||||||
let pid: Pid = Pid::from_raw(f.parse().map_err(|e| Box::new(e) as Box::<dyn Error + Send + Sync>)?);
|
let pid: Pid = Pid::from_raw(
|
||||||
|
f.parse()
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?,
|
||||||
|
);
|
||||||
if pid == getpid() {
|
if pid == getpid() {
|
||||||
info!("Pid in pidfile is me - ignoring");
|
info!("Pid in pidfile is me - ignoring");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
match fs::read_to_string(format!("/proc/{}/cmdline", pid)) {
|
match fs::read_to_string(format!("/proc/{}/cmdline", pid)) {
|
||||||
Ok(content) =>
|
Ok(content) => {
|
||||||
if content.contains("blastmud_listener") {
|
if content.contains("blastmud_listener") {
|
||||||
info!("pid in pidfile references blastmud_listener; starting cutover");
|
info!("pid in pidfile references blastmud_listener; starting cutover");
|
||||||
kill(pid, Signal::SIGTERM)
|
kill(pid, Signal::SIGTERM)
|
||||||
@ -666,6 +743,7 @@ pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
|
|||||||
info!("Pid in pidfile is for process not including blastmud_listener - ignoring pidfile");
|
info!("Pid in pidfile is for process not including blastmud_listener - ignoring pidfile");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("Pid in pidfile is gone - ignoring pidfile");
|
info!("Pid in pidfile is gone - ignoring pidfile");
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -675,25 +753,33 @@ pub fn replace_old_listener(pidfile: &str) -> DResult<()> {
|
|||||||
}?;
|
}?;
|
||||||
info!("Writing new pidfile");
|
info!("Writing new pidfile");
|
||||||
fs::write(Path::new(pidfile), format!("{}", std::process::id()))
|
fs::write(Path::new(pidfile), format!("{}", std::process::id()))
|
||||||
.map_err(|e| Box::new(e) as Box::<dyn Error + Send + Sync>)
|
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
SimpleLogger::new().with_level(LevelFilter::Info).init().unwrap();
|
SimpleLogger::new()
|
||||||
|
.with_level(LevelFilter::Info)
|
||||||
|
.init()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let listener_id = Uuid::new_v4();
|
let listener_id = Uuid::new_v4();
|
||||||
let mut config = read_latest_config()?;
|
let mut config = read_latest_config()?;
|
||||||
let active_sessions: SessionMap =
|
let active_sessions: SessionMap = Arc::new(Mutex::new(SessionIndexes {
|
||||||
Arc::new(Mutex::new(SessionIndexes { by_uuid: BTreeMap::new(), count_by_source: BTreeMap::new() }));
|
by_uuid: BTreeMap::new(),
|
||||||
|
count_by_source: BTreeMap::new(),
|
||||||
|
}));
|
||||||
replace_old_listener(&config.pidfile)?;
|
replace_old_listener(&config.pidfile)?;
|
||||||
let server_sender = start_server_task(listener_id, config.gameserver, active_sessions.clone());
|
let server_sender = start_server_task(listener_id, config.gameserver, active_sessions.clone());
|
||||||
|
|
||||||
start_pinger(listener_id, server_sender.clone());
|
start_pinger(listener_id, server_sender.clone());
|
||||||
// Note: for now, this cannot be reconfigured without a complete restart.
|
// Note: for now, this cannot be reconfigured without a complete restart.
|
||||||
start_websocket(config.ws_listener, active_sessions.clone(), server_sender.clone()).await?;
|
start_websocket(
|
||||||
|
config.ws_listener,
|
||||||
|
active_sessions.clone(),
|
||||||
|
server_sender.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let mut sighups = signal(SignalKind::hangup())?;
|
let mut sighups = signal(SignalKind::hangup())?;
|
||||||
|
|
||||||
@ -704,25 +790,30 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
|||||||
let active_sessions_for_listener = active_sessions.clone();
|
let active_sessions_for_listener = active_sessions.clone();
|
||||||
listen_handles.push(task::spawn(async move {
|
listen_handles.push(task::spawn(async move {
|
||||||
match TcpListener::bind(&listener).await {
|
match TcpListener::bind(&listener).await {
|
||||||
Err(e) => { warn!("Error listening to {}: {}", &listener, e); }
|
Err(e) => {
|
||||||
Ok(listensock) => {
|
warn!("Error listening to {}: {}", &listener, e);
|
||||||
loop {
|
}
|
||||||
|
Ok(listensock) => loop {
|
||||||
match listensock.accept().await {
|
match listensock.accept().await {
|
||||||
Err(e) => { warn!("Error accepting connection from {}: {}",
|
Err(e) => {
|
||||||
&listener, e); }
|
warn!("Error accepting connection from {}: {}", &listener, e);
|
||||||
|
}
|
||||||
Ok((stream, addr)) => {
|
Ok((stream, addr)) => {
|
||||||
let server_sender_for_client = server_sender_for_listener.clone();
|
let server_sender_for_client = server_sender_for_listener.clone();
|
||||||
let active_sessions_for_client = active_sessions_for_listener.clone();
|
let active_sessions_for_client =
|
||||||
|
active_sessions_for_listener.clone();
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
handle_client_socket(server_sender_for_client,
|
handle_client_socket(
|
||||||
|
server_sender_for_client,
|
||||||
active_sessions_for_client,
|
active_sessions_for_client,
|
||||||
stream,
|
stream,
|
||||||
addr
|
addr,
|
||||||
).await;
|
)
|
||||||
}); }
|
.await;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -735,7 +826,10 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
|||||||
// Note: It is deliberate behaviour to send this even if gameserver
|
// Note: It is deliberate behaviour to send this even if gameserver
|
||||||
// hasn't changed - SIGHUP is to be used after a server hot cutover to tell
|
// hasn't changed - SIGHUP is to be used after a server hot cutover to tell
|
||||||
// it to connect to the new server process even if on the same port.
|
// it to connect to the new server process even if on the same port.
|
||||||
server_sender.send(ServerTaskCommand::SwitchTo { new_server: config.gameserver })
|
server_sender
|
||||||
|
.send(ServerTaskCommand::SwitchTo {
|
||||||
|
new_server: config.gameserver,
|
||||||
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for handle in &listen_handles {
|
for handle in &listen_handles {
|
||||||
@ -744,7 +838,6 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
|
Loading…
Reference in New Issue
Block a user