Start user registration work

This commit is contained in:
Condorra 2022-12-26 01:30:59 +11:00
parent 218ca0b953
commit d4bc71e26f
15 changed files with 607 additions and 39 deletions

220
Cargo.lock generated
View File

@ -14,6 +14,15 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "ansi_macro"
version = "0.1.0"
@ -63,6 +72,18 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "bcrypt"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7e7c93a3fb23b2fdde989b2c9ec4dd153063ec81f408507f84c090cd91c6641"
dependencies = [
"base64 0.13.1",
"blowfish",
"getrandom",
"zeroize",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@ -76,7 +97,9 @@ dependencies = [
"ansi_macro",
"async-trait",
"base64 0.20.0",
"bcrypt",
"blastmud_interfaces",
"chrono",
"deadpool",
"deadpool-postgres",
"futures",
@ -134,6 +157,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]]
name = "buf_redux"
version = "0.8.4"
@ -174,6 +207,42 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
dependencies = [
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"serde",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "cipher"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "colored"
version = "2.0.0"
@ -185,6 +254,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cpufeatures"
version = "0.2.5"
@ -204,6 +279,50 @@ dependencies = [
"typenum",
]
[[package]]
name = "cxx"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
[[package]]
name = "cxxbridge-macro"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "deadpool"
version = "0.9.5"
@ -414,7 +533,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@ -549,6 +668,30 @@ dependencies = [
"want",
]
[[package]]
name = "iana-time-zone"
version = "0.1.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"winapi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
dependencies = [
"cxx",
"cxx-build",
]
[[package]]
name = "idna"
version = "0.3.0"
@ -569,6 +712,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "instant"
version = "0.1.12"
@ -605,6 +757,15 @@ version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]]
name = "link-cplusplus"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
dependencies = [
"cc",
]
[[package]]
name = "lock_api"
version = "0.4.9"
@ -678,7 +839,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
@ -1102,6 +1263,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scratch"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
[[package]]
name = "semver"
version = "1.0.14"
@ -1225,7 +1392,7 @@ dependencies = [
"atty",
"colored",
"log",
"time",
"time 0.3.17",
"windows-sys",
]
@ -1313,6 +1480,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.38"
@ -1333,6 +1509,17 @@ dependencies = [
"syn",
]
[[package]]
name = "time"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
dependencies = [
"libc",
"wasi 0.10.0+wasi-snapshot-preview1",
"winapi",
]
[[package]]
name = "time"
version = "0.3.17"
@ -1580,6 +1767,12 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.4"
@ -1666,6 +1859,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -1752,6 +1951,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -1814,3 +2022,9 @@ name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"

View File

@ -29,3 +29,5 @@ phf = { version = "0.11.1", features = ["macros"] }
async-trait = "0.1.60"
nom = "7.1.1"
ouroboros = "0.15.5"
chrono = { version = "0.4.23", features = ["serde"] }
bcrypt = "0.13.0"

View File

@ -9,6 +9,7 @@ use tokio_postgres::NoTls;
use crate::message_handler::ListenerSession;
use crate::DResult;
use crate::models::session::Session;
use crate::models::user::User;
use serde_json;
use futures::FutureExt;
@ -164,18 +165,34 @@ impl DBTrans {
Ok(())
}
pub async fn get_session_model(self: &Self, session: &ListenerSession) -> DResult<Option<Session>> {
pub async fn get_session_user_model(self: &Self, session: &ListenerSession) -> DResult<Option<(Session, Option<User>)>> {
match self.pg_trans()?
.query_opt("SELECT details FROM sessions WHERE session = $1", &[&session.session])
.query_opt("SELECT s.details AS sess_details, \
u.details AS user_details FROM sessions s \
LEFT JOIN users u ON u.current_session = s.session \
WHERE s.session = $1", &[&session.session])
.await? {
None => Ok(None),
Some(row) =>
Ok(Some(serde_json::from_value(
row.get("details")
)?))
Ok(Some(
(serde_json::from_value(
row.get("sess_details"))?,
match row.get::<&str, Option<serde_json::Value>>("user_details") {
None => None,
Some(v) => serde_json::from_value(v)?
})
))
}
}
pub async fn save_session_model(self: &Self, session: &ListenerSession, details: &Session)
-> DResult<()> {
self.pg_trans()?
.execute("UPDATE sessions SET details = $1 WHERE session = $2",
&[&serde_json::to_value(details)?, &session.session]).await?;
Ok(())
}
pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
for trans in trans_opt {

View File

@ -11,8 +11,11 @@ pub async fn handle(session: &ListenerSession, source: String, pool: &DBPool) ->
Welcome to <red>BlastMud<reset> - a text-based post-apocalyptic \
game <bold>restricted to adults (18+)<reset>\r\n\
Some commands to get you started:\r\n\
\t<bold>register <lt>username> <lt>password> <lt>email><reset> to register as a new user.\r\n\
\t<bold>register <lt>username> <lt>password> to register as a new user.\r\n\
\t<bold>connect <lt>username> <lt>password><reset> to log in as an existing user.\r\n\
\t<bold>help<reset> to learn more.\r\n"))).await?;
\t<bold>help<reset> to learn more.\r\n\
[Please note BlastMud is still under development. You are welcome to play as we \
develop it, but note it might still have bugs, unimplemented features, and \
unbalanced gameplay aspects].\r\n"))).await?;
Ok(())
}

View File

@ -4,17 +4,20 @@ use crate::db::{DBTrans, DBPool};
use ansi_macro::ansi;
use phf::phf_map;
use async_trait::async_trait;
use crate::models::session::Session;
use crate::models::{session::Session, user::User};
use log::warn;
mod parsing;
mod ignore;
mod help;
mod quit;
mod less_explicit_mode;
mod register;
pub struct VerbContext<'l> {
session: &'l ListenerSession,
session_dat: &'l mut Session,
user_dat: &'l mut Option<User>,
trans: &'l DBTrans
}
@ -26,10 +29,9 @@ use CommandHandlingError::*;
#[async_trait]
pub trait UserVerb {
async fn handle(self: &Self, ctx: &VerbContext, verb: &str, remaining: &str) -> UResult<()>;
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()>;
}
pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send);
pub type UResult<A> = Result<A, CommandHandlingError>;
impl From<Box<dyn std::error::Error + Send + Sync>> for CommandHandlingError {
@ -42,6 +44,9 @@ pub fn user_error<A>(msg: String) -> UResult<A> {
Err(UserError(msg))
}
/* Verb registries list types of commands available in different circumstances. */
pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send);
type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>;
static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
@ -50,10 +55,26 @@ static ALWAYS_AVAILABLE_COMMANDS: UserVerbRegistry = phf_map! {
"quit" => quit::VERB,
};
static UNREGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"less_explicit_mode" => less_explicit_mode::VERB,
"register" => register::VERB,
};
fn resolve_handler(ctx: &VerbContext, cmd: &str) -> Option<&'static UserVerbRef> {
let mut result = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
match ctx.user_dat {
None => { result = result.or_else(|| UNREGISTERED_COMMANDS.get(cmd)); }
Some(_) => {}
}
result
}
pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DResult<()> {
let (cmd, params) = parsing::parse_command_name(msg);
let trans = pool.start_transaction().await?;
let mut session_dat = match trans.get_session_model(session).await? {
let (mut session_dat, mut user_dat) = match trans.get_session_user_model(session).await? {
None => {
// If the session has been cleaned up from the database, there is
// nowhere to go from here, so just ignore it.
@ -62,8 +83,10 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
}
Some(v) => v
};
let handler_opt = ALWAYS_AVAILABLE_COMMANDS.get(cmd);
let ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat };
let mut ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat,
user_dat: &mut user_dat };
let handler_opt = resolve_handler(&ctx, cmd);
match handler_opt {
None => {
@ -74,7 +97,7 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
).await?;
}
Some(handler) => {
match handler.handle(&ctx, cmd, params).await {
match handler.handle(&mut ctx, cmd, params).await {
Ok(()) => {}
Err(UserError(err_msg)) => {
trans.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?;

View File

@ -28,7 +28,7 @@ static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let mut help = None;
if !ctx.session_dat.less_explicit_mode {
help = help.or_else(|| EXPLICIT_HELP_PAGES.get(remaining))

View File

@ -4,7 +4,7 @@ use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, _ctx: &VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
Ok(())
}
}

View File

@ -0,0 +1,16 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult
};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
(*ctx.session_dat).less_explicit_mode = true;
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,13 +1,17 @@
use nom::{
bytes::complete::{take_till1},
character::complete::space0,
bytes::complete::{take_till1, take_while},
character::{complete::{space0, space1, alpha1}},
combinator::{recognize, fail, eof},
sequence::terminated,
branch::alt,
error::{context, VerboseError, VerboseErrorKind},
IResult,
};
pub fn parse_command_name(input: &str) -> (&str, &str) {
fn parse(input: &str) -> IResult<&str, &str> {
let (input, _) = space0(input)?;
let (input, cmd) = take_till1(|c| c == ' ' || c == '\n')(input)?;
let (input, cmd) = take_till1(|c| c == ' ' || c == '\t')(input)?;
let (input, _) = space0(input)?;
Ok((input, cmd))
}
@ -17,3 +21,101 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
Ok((rest, command)) => (command, rest)
}
}
pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> {
const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _";
fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> {
let (input, l1) = context("Must start with a letter", alpha1)(input)?;
let (input, l2) = context(CATCHALL_ERROR,
take_while(|c: char| c.is_alphanumeric() || c == '_'))(input)?;
if l1.len() + l2.len() > 20 {
context("Limit of 20 characters", fail::<&str, &str, VerboseError<&str>>)(input)?;
}
Ok((input, ()))
}
match terminated(recognize(parse_valid), alt((space1, eof)))(input) {
Ok((input, username)) => Ok((username, input)),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) =>
Err(e.errors.into_iter().find_map(|k| match k.1 {
VerboseErrorKind::Context(s) => Some(s),
_ => None
}).unwrap_or(CATCHALL_ERROR)),
Err(_) => Err(CATCHALL_ERROR)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_parses_normal_command() {
assert_eq!(parse_command_name("help"),
("help", ""));
}
#[test]
fn it_parses_normal_command_with_arg() {
assert_eq!(parse_command_name("help \t testing stuff"),
("help", "testing stuff"));
}
#[test]
fn it_parses_commands_with_leading_whitespace() {
assert_eq!(parse_command_name(" \t \thelp \t testing stuff"),
("help", "testing stuff"));
}
#[test]
fn it_parses_empty_command_names() {
assert_eq!(parse_command_name(""),
("", ""));
assert_eq!(parse_command_name(" \t "),
("", ""));
}
#[test]
fn it_parses_usernames() {
assert_eq!(parse_username("Wizard123"), Ok(("Wizard123", "")));
}
#[test]
fn it_parses_usernames_with_further_args() {
assert_eq!(parse_username("Wizard_123 with cat"), Ok(("Wizard_123", "with cat")));
}
#[test]
fn it_parses_alpha_only_usernames() {
assert_eq!(parse_username("W"), Ok(("W", "")));
}
#[test]
fn it_fails_on_empty_usernames() {
assert_eq!(parse_username(""), Err("Must start with a letter"));
}
#[test]
fn it_fails_on_usernames_with_invalid_start() {
assert_eq!(parse_username("#hack"), Err("Must start with a letter"));
}
#[test]
fn it_fails_on_usernames_with_underscore_start() {
assert_eq!(parse_username("_hack"), Err("Must start with a letter"));
}
#[test]
fn it_fails_on_usernames_with_number_start() {
assert_eq!(parse_username("31337 #"), Err("Must start with a letter"));
}
#[test]
fn it_fails_on_usernames_with_bad_characters() {
assert_eq!(parse_username("Wizard!"), Err("Must only contain alphanumeric characters or _"));
}
#[test]
fn it_fails_on_long_usernames() {
assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters"));
}
}

View File

@ -7,7 +7,7 @@ use ansi_macro::ansi;
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> {
ctx.trans.queue_for_session(ctx.session,
Some(ansi!("<red>Bye!<reset>\r\n"))).await?;
ctx.trans.queue_for_session(ctx.session, None).await?;

View File

@ -0,0 +1,18 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
use async_trait::async_trait;
use super::{user_error, parsing::parse_username};
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
let (username, password) = match parse_username(remaining) {
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
Ok(r) => r
};
let _pwhash = bcrypt::hash(password, 10);
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1 +1,3 @@
pub mod session;
pub mod user;
pub mod item;

View File

@ -0,0 +1,56 @@
use serde::{Serialize, Deserialize};
use std::collections::BTreeMap;
use super::user::{SkillType, StatType};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum BuffCause {
WaitingTask { task_code: String, task_type: String },
ByItem { item_code: String, item_type: String }
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum BuffImpact {
ChangeStat { stat: StatType, magnitude: i16 },
ChangeSkill { stat: StatType, magnitude: i16 }
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Buff {
description: String,
cause: BuffCause,
impacts: Vec<BuffImpact>
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum Subattack {
Normal,
Powerattacking,
Feinting,
Grabbing,
Wrestling
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum LocationActionType {
Normal,
Sitting,
Reclining,
Worn, // Clothing etc...
Wielded,
Attacking(Subattack),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Item {
pub item_code: String,
pub item_type: String,
pub location: String, // Item reference as item_type/item_code.
pub action_type: LocationActionType,
pub presence_target: Option<String>, // e.g. what are they sitting on.
pub is_static: bool,
pub total_xp: u64,
pub total_stats: BTreeMap<StatType, u64>,
pub total_skills: BTreeMap<SkillType, u64>,
pub temporary_buffs: Vec<Buff>,
}

View File

@ -0,0 +1,119 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UserTermData {
pub accepted_terms: BTreeMap<String, DateTime<Utc>>,
pub last_presented_term: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UserExperienceData {
pub spent_xp: u64, // Since last chargen complete.
pub completed_journals: BTreeMap<String, DateTime<Utc>>,
pub xp_change_for_this_reroll: i64,
pub crafted_items: BTreeMap<String, u64>
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum SkillType {
Apraise,
Blades,
Bombs,
Chemistry,
Climb,
Clubs,
Craft,
Fish,
Fists,
Flails,
Fuck,
Hack,
Locksmith,
Medic,
Persuade,
Pilot,
Pistols,
Quickdraw,
Repair,
Ride,
Rifles,
Scavenge,
Science,
Sneak,
Spears,
Swim,
Teach,
Throw,
Track,
Wrestle,
Whips
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum StatType {
Brains,
Senses,
Brawn,
Reflexes,
Endurance,
Cool
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct User {
pub username: String,
pub password_hash: String, // bcrypted.
pub player_item_id: u64,
pub registered_at: Option<DateTime<Utc>>,
pub banned_until: Option<DateTime<Utc>>,
pub abandoned_at: Option<DateTime<Utc>>,
pub chargen_last_completed_at: Option<DateTime<Utc>>,
pub terms: UserTermData,
pub experience: UserExperienceData,
pub raw_skills: BTreeMap<SkillType, u16>,
pub raw_stats: BTreeMap<StatType, u16>,
// Reminder: Consider backwards compatibility when updating this. New fields should generally
// be an Option, or things will crash out for existing sessions.
}
impl Default for UserTermData {
fn default() -> Self {
UserTermData {
accepted_terms: BTreeMap::new(),
last_presented_term: None
}
}
}
impl Default for UserExperienceData {
fn default() -> Self {
UserExperienceData {
spent_xp: 0,
completed_journals: BTreeMap::new(),
xp_change_for_this_reroll: 0,
crafted_items: BTreeMap::new(),
}
}
}
impl Default for User {
fn default() -> Self {
User {
username: "unknown".to_owned(),
password_hash: "unknown".to_owned(),
player_item_id: 0,
registered_at: None,
banned_until: None,
abandoned_at: None,
chargen_last_completed_at: None,
terms: UserTermData::default(),
experience: UserExperienceData::default(),
raw_skills: BTreeMap::new(),
raw_stats: BTreeMap::new(),
}
}
}

View File

@ -18,15 +18,12 @@ CREATE TABLE sessions (
CREATE INDEX session_by_listener ON sessions(listener);
CREATE TABLE items (
item_id BIGINT NOT NULL PRIMARY KEY,
item_code TEXT NOT NULL,
item_type TEXT NOT NULL,
location BIGINT REFERENCES items(item_id),
details JSONB NOT NULL,
UNIQUE (item_code, item_type)
item_id BIGSERIAL NOT NULL PRIMARY KEY,
details JSONB NOT NULL
);
CREATE INDEX item_index ON items (item_code, item_type);
CREATE INDEX item_by_loc ON items (location);
CREATE UNIQUE INDEX item_index ON items ((details->>'item_code'), (details->>'item_type'));
CREATE INDEX item_by_loc ON items ((details->>'location'));
CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
CREATE TABLE users (
username TEXT NOT NULL PRIMARY KEY,
@ -35,6 +32,7 @@ CREATE TABLE users (
details JSONB NOT NULL
);
CREATE INDEX user_by_listener ON users(current_listener);
CREATE INDEX user_by_session ON users(current_session);
CREATE UNLOGGED TABLE sendqueue (
item BIGSERIAL NOT NULL PRIMARY KEY,
@ -44,11 +42,9 @@ CREATE UNLOGGED TABLE sendqueue (
);
CREATE TABLE tasks (
task_code TEXT NOT NULL,
task_type TEXT NOT NULL,
is_static BOOL NOT NULL,
next_scheduled TIMESTAMP WITH TIME ZONE NOT NULL,
details JSONB NOT NULL,
PRIMARY KEY (task_code, task_type)
task_id BIGSERIAL NOT NULL PRIMARY KEY,
details JSONB NOT NULL
);
CREATE INDEX task_by_next_scheduled ON tasks(next_scheduled);
CREATE UNIQUE INDEX tasks_by_code_type ON tasks((details->>'task_code'), (details->>'task_type'));
CREATE INDEX tasks_by_static ON tasks((cast(details->>'is_static' as boolean)));
CREATE INDEX tasks_by_scheduled ON tasks((details->>'next_scheduled'));