Enable PvP fighting.
This commit is contained in:
parent
73d92e3074
commit
b96fc2e772
@ -16,7 +16,8 @@ use crate::models::{
|
||||
Item,
|
||||
LocationActionType,
|
||||
},
|
||||
task::{Task, TaskParse}
|
||||
task::{Task, TaskParse},
|
||||
consent::{Consent, ConsentType},
|
||||
};
|
||||
use tokio_postgres::types::ToSql;
|
||||
use std::collections::BTreeSet;
|
||||
@ -730,6 +731,53 @@ impl DBTrans {
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn find_user_consent_by_parties_type(&self, consenting: &str, consented: &str,
|
||||
consent_type: &ConsentType) -> DResult<Option<Consent>> {
|
||||
match self.pg_trans()?.query_opt(
|
||||
"SELECT details FROM user_consent WHERE consenting_user = $1 AND \
|
||||
consented_user = $2 AND consent_type = $3",
|
||||
&[&consenting, &consented, &ConsentType::to_str(consent_type)]
|
||||
).await? {
|
||||
None => Ok(None),
|
||||
Some(row) => Ok(Some(serde_json::from_value(row.get(0))?))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_expired_user_consent(&self) -> DResult<()> {
|
||||
self.pg_trans()?.execute(
|
||||
"DELETE FROM user_consent WHERE details->>'expires' < $1",
|
||||
&[&Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Nanos, true)]
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_user_consent(&self,
|
||||
consenting: &str,
|
||||
consented: &str,
|
||||
consent_type: &ConsentType) -> DResult<()> {
|
||||
self.pg_trans()?.execute(
|
||||
"DELETE FROM user_consent WHERE consenting_user = $1 AND \
|
||||
consented_user = $2 AND consent_type = $3",
|
||||
&[&consenting, &consented, &ConsentType::to_str(consent_type)]
|
||||
).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upsert_user_consent(&self,
|
||||
consenting: &str,
|
||||
consented: &str,
|
||||
consent_type: &ConsentType,
|
||||
details: &Consent
|
||||
) -> DResult<()> {
|
||||
self.pg_trans()?
|
||||
.execute("INSERT INTO user_consent (consenting_user, consented_user, consent_type, details) VALUES ($1, $2, $3, $4) \
|
||||
ON CONFLICT (consenting_user, consented_user, consent_type) DO UPDATE SET \
|
||||
details = EXCLUDED.details",
|
||||
&[&consenting, &consented, &ConsentType::to_str(consent_type),
|
||||
&serde_json::to_value(details)?]).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn commit(mut self: Self) -> DResult<()> {
|
||||
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
|
||||
|
@ -12,6 +12,7 @@ use once_cell::sync::OnceCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod agree;
|
||||
mod allow;
|
||||
pub mod attack;
|
||||
mod buy;
|
||||
pub mod drop;
|
||||
@ -110,6 +111,9 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"down" => movement::VERB,
|
||||
|
||||
// Other commands (alphabetical except aliases grouped):
|
||||
"allow" => allow::VERB,
|
||||
"disallow" => allow::VERB,
|
||||
|
||||
"attack" => attack::VERB,
|
||||
"buy" => buy::VERB,
|
||||
"drop" => drop::VERB,
|
||||
|
711
blastmud_game/src/message_handler/user_commands/allow.rs
Normal file
711
blastmud_game/src/message_handler/user_commands/allow.rs
Normal file
@ -0,0 +1,711 @@
|
||||
use super::{
|
||||
VerbContext,
|
||||
UserVerb,
|
||||
UserVerbRef,
|
||||
UResult,
|
||||
user_error,
|
||||
get_player_item_or_fail,
|
||||
search_item_for_user,
|
||||
parsing
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use crate::{
|
||||
models::{
|
||||
consent::{Consent, ConsentType, ConsentStatus, FightConsent},
|
||||
item::Item
|
||||
},
|
||||
db::ItemSearchParams,
|
||||
static_content::room::room_map_by_code,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ConsentTarget<'t> {
|
||||
CorpTarget { from_corp: &'t str, to_corp: &'t str },
|
||||
UserTarget { to_user: &'t str }
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ConsentDetails<'t> {
|
||||
pub duration_minutes: Option<u64>,
|
||||
pub until_death: bool,
|
||||
pub allow_private: bool,
|
||||
pub only_in: Vec<&'t str>,
|
||||
pub allow_pick: bool,
|
||||
pub freely_revoke: bool,
|
||||
}
|
||||
|
||||
impl <'t>ConsentDetails<'t> {
|
||||
pub fn default_for(tp: &ConsentType) -> Self {
|
||||
Self {
|
||||
duration_minutes: None,
|
||||
until_death: false,
|
||||
allow_private: tp != &ConsentType::Fight,
|
||||
only_in: vec!(),
|
||||
allow_pick: false,
|
||||
freely_revoke: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_string(&self, consent_type: &ConsentType) -> String {
|
||||
let mut buf = String::new();
|
||||
match self.duration_minutes {
|
||||
None => {},
|
||||
Some(n) => buf.push_str(&format!("{} minutes ", n))
|
||||
}
|
||||
if self.until_death {
|
||||
buf.push_str("until death ");
|
||||
}
|
||||
if *consent_type == ConsentType::Fight {
|
||||
if self.allow_private {
|
||||
buf.push_str("allow private ");
|
||||
}
|
||||
if self.allow_pick {
|
||||
buf.push_str("allow pick ");
|
||||
}
|
||||
if self.freely_revoke {
|
||||
buf.push_str("allow revoke ");
|
||||
}
|
||||
} else {
|
||||
if !self.allow_private {
|
||||
buf.push_str("disallow private ");
|
||||
}
|
||||
}
|
||||
for loc in &self.only_in {
|
||||
buf.push_str(&format!("in {} ", loc));
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct AllowCommand<'t> {
|
||||
pub consent_type: ConsentType,
|
||||
pub consent_target: ConsentTarget<'t>,
|
||||
pub consent_details: ConsentDetails<'t>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConsentUpdateOutput {
|
||||
pub new_consent: Option<Consent>,
|
||||
pub first_party_message: Option<String>,
|
||||
pub counterparty_message: Option<String>,
|
||||
pub mirror_to_counterparty: bool
|
||||
}
|
||||
|
||||
fn compute_new_consent_state(
|
||||
player_display: &str,
|
||||
player_possessive: &str,
|
||||
counterplayer_display: &str,
|
||||
counterplayer_possessive: &str,
|
||||
selector: &str,
|
||||
consent_type: &ConsentType,
|
||||
new_consent_details: &ConsentDetails,
|
||||
my_current_consent: &Option<Consent>,
|
||||
their_current_consent: &Option<Consent>,
|
||||
is_allow: bool) -> ConsentUpdateOutput {
|
||||
if !is_allow {
|
||||
if consent_type != &ConsentType::Fight {
|
||||
if my_current_consent.is_none() {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: None,
|
||||
first_party_message: Some("There was no matching consent in force to disallow".to_owned()),
|
||||
counterparty_message: None,
|
||||
mirror_to_counterparty: false
|
||||
}
|
||||
} else {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: None,
|
||||
first_party_message:
|
||||
Some(format!("You no longer allow {} from {}",
|
||||
ConsentType::to_str(consent_type), counterplayer_display)),
|
||||
counterparty_message:
|
||||
Some(format!("{} no longer allows {} from you", player_display,
|
||||
ConsentType::to_str(consent_type))),
|
||||
mirror_to_counterparty: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if my_current_consent.as_ref().and_then(|c| c.fight_consent.as_ref())
|
||||
.map(|c| (c.status == ConsentStatus::PendingAdd)) == Some(true) {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: None,
|
||||
first_party_message:
|
||||
Some(format!("You are no longer offering to fight {}",
|
||||
counterplayer_display)),
|
||||
counterparty_message:
|
||||
Some(format!("{} no longer wants to fight you", player_display)),
|
||||
mirror_to_counterparty: false
|
||||
};
|
||||
}
|
||||
|
||||
if their_current_consent.as_ref().and_then(|c| c.fight_consent.as_ref())
|
||||
.map(|c| c.freely_revoke || (c.status == ConsentStatus::PendingDelete)) == Some(true) {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: None,
|
||||
first_party_message:
|
||||
Some(format!("You no longer allow {} from {}",
|
||||
ConsentType::to_str(consent_type), counterplayer_display)),
|
||||
counterparty_message:
|
||||
Some(format!("{} no longer allows {} from you", player_display,
|
||||
ConsentType::to_str(consent_type))),
|
||||
mirror_to_counterparty: true
|
||||
};
|
||||
}
|
||||
match my_current_consent.as_ref() {
|
||||
None => return ConsentUpdateOutput {
|
||||
new_consent: None,
|
||||
first_party_message: Some("There was no matching consent in force to disallow".to_owned()),
|
||||
counterparty_message: None,
|
||||
mirror_to_counterparty: false
|
||||
},
|
||||
Some(c) if c.fight_consent.as_ref().map(|fc| &fc.status) == Some(&ConsentStatus::PendingDelete) =>
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: Some((*c).clone()),
|
||||
first_party_message: Some(format!("You are already waiting on {} to agree to cancel consent to fight.",
|
||||
counterplayer_display
|
||||
)),
|
||||
counterparty_message: None,
|
||||
mirror_to_counterparty: false
|
||||
},
|
||||
Some(c) => return ConsentUpdateOutput {
|
||||
new_consent: Some(Consent {
|
||||
fight_consent: c.fight_consent.as_ref().map(|fc| FightConsent {
|
||||
status: ConsentStatus::PendingDelete, pending_change: None, ..*fc }),
|
||||
..((*c).clone()) }),
|
||||
first_party_message:
|
||||
Some(format!("Informing {} of your desire to withdraw consent to fight. The terms of your previous consent \
|
||||
were that it was not freely revokable, so the change will only take effect on {} \
|
||||
acceptance.",
|
||||
counterplayer_display, counterplayer_possessive)),
|
||||
counterparty_message:
|
||||
Some(format!("{} wants to withdraw {} consent to fight, but only can if you agree. To agree, type disallow \
|
||||
fight {}",
|
||||
player_display, player_possessive, selector)),
|
||||
mirror_to_counterparty: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let their_target_consent = their_current_consent.as_ref().and_then(
|
||||
|c| c.fight_consent.as_ref()
|
||||
.map(|fc| fc.pending_change.as_ref()
|
||||
.map(|c| (**c).clone()).unwrap_or(c.clone())));
|
||||
let mut new_consent = Consent::default();
|
||||
new_consent.only_in = new_consent_details.only_in.iter().map(|v| (*v).to_owned()).collect();
|
||||
let expires_minutes = if *consent_type != ConsentType::Fight {
|
||||
new_consent_details.duration_minutes
|
||||
} else {
|
||||
match new_consent_details.duration_minutes {
|
||||
None => Some(60 * 24 * 7),
|
||||
Some(n) => Some(n.min(60 * 24 * 7))
|
||||
}
|
||||
};
|
||||
new_consent.expires = expires_minutes
|
||||
.map(|n| chrono::Utc::now() +
|
||||
chrono::Duration::minutes(n.min(60 * 24 * 365 * 25) as i64));
|
||||
match their_target_consent.as_ref().and_then(|c| c.expires).and_then(
|
||||
|t_them| new_consent.expires.map(|t_me| (t_me, t_them))) {
|
||||
Some((t_me, t_them)) if (t_me - t_them).num_minutes().abs() < 5 => {
|
||||
new_consent.expires = Some(t_them);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
new_consent.allow_private = new_consent_details.allow_private;
|
||||
new_consent.until_death = new_consent_details.until_death;
|
||||
|
||||
new_consent.fight_consent = if *consent_type == ConsentType::Fight {
|
||||
Some(FightConsent {
|
||||
pending_change: None,
|
||||
allow_pick: new_consent_details.allow_pick,
|
||||
freely_revoke: new_consent_details.freely_revoke,
|
||||
..FightConsent::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if *consent_type == ConsentType::Fight {
|
||||
if Some(&new_consent) == their_target_consent.as_ref() {
|
||||
match new_consent.fight_consent.as_mut() {
|
||||
None => (),
|
||||
Some(mut m) => {
|
||||
m.pending_change = None;
|
||||
m.status = ConsentStatus::Active;
|
||||
}
|
||||
}
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: Some(new_consent),
|
||||
first_party_message: Some(format!("You can now fight {} - it's on!",
|
||||
counterplayer_display)),
|
||||
counterparty_message: Some(format!("{} accepted your offer to fight - it's on!",
|
||||
player_display)),
|
||||
mirror_to_counterparty: true
|
||||
};
|
||||
}
|
||||
match my_current_consent.as_ref() {
|
||||
None => {
|
||||
match new_consent.fight_consent.as_mut() {
|
||||
None => (),
|
||||
Some(mut m) => { m.status = ConsentStatus::PendingAdd; }
|
||||
}
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: Some(new_consent),
|
||||
first_party_message: Some(format!("{} has been asked for consent to fight.", counterplayer_display)),
|
||||
counterparty_message: Some(format!("{} wants to fight! To accept, type: allow fight {} {}",
|
||||
player_display, selector,
|
||||
&new_consent_details.as_string(consent_type))),
|
||||
mirror_to_counterparty: false
|
||||
};
|
||||
},
|
||||
Some(current) if current.fight_consent.as_ref().map(|fc| fc.status == ConsentStatus::PendingAdd) == Some(true) => {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: Some(new_consent),
|
||||
first_party_message: Some("Waiting for the other side to accept on the new terms".to_owned()),
|
||||
counterparty_message:
|
||||
Some(format!("{} changed {} proposed fight terms! To accept, type allow fight {} {}",
|
||||
player_display, player_possessive, selector, &new_consent_details.as_string(consent_type))),
|
||||
mirror_to_counterparty: false
|
||||
};
|
||||
},
|
||||
Some(current) => {
|
||||
return ConsentUpdateOutput {
|
||||
new_consent: Some(Consent {
|
||||
fight_consent: current.fight_consent.as_ref().map(|fc| FightConsent {
|
||||
pending_change: Some(Box::new(new_consent)),
|
||||
..(*fc).clone()
|
||||
}),
|
||||
..(*current).clone()
|
||||
}),
|
||||
first_party_message: Some("Waiting for the other side to accept the change of terms".to_owned()),
|
||||
counterparty_message:
|
||||
Some(format!("{} wants to amend the terms of the fight! To accept, type allow fight {} {}",
|
||||
player_display, selector, &new_consent_details.as_string(consent_type))),
|
||||
mirror_to_counterparty: false
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConsentUpdateOutput {
|
||||
new_consent: Some(new_consent),
|
||||
first_party_message: Some(format!("You now allow {} from {}", consent_type.to_str(), counterplayer_display)),
|
||||
counterparty_message: Some(format!("{} now allows {} from you", player_display, consent_type.to_str())),
|
||||
mirror_to_counterparty: false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_cancels_non_fight() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Sex,
|
||||
&ConsentDetails::default_for(&ConsentType::Sex),
|
||||
&Some(Consent::default()),
|
||||
&None,
|
||||
false
|
||||
);
|
||||
assert_eq!(result.new_consent, None);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_disallow_puts_into_pending_delete_fight() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
assert_eq!(result.new_consent.unwrap()
|
||||
.fight_consent.unwrap().status, ConsentStatus::PendingDelete);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_disallow_cancels_fight_if_freely_revoke() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
freely_revoke: true,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
freely_revoke: true,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert_eq!(result.new_consent, None);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
assert_eq!(result.mirror_to_counterparty, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_disallow_cancels_pending_delete_fight() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::PendingDelete,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert_eq!(result.new_consent, None);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
assert_eq!(result.mirror_to_counterparty, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_unilateral_double_disallow_doesnt_cancel() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::PendingDelete,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message, None);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_adds_nonfight_immediately() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Sex,
|
||||
&ConsentDetails::default_for(&ConsentType::Sex),
|
||||
&None,
|
||||
&None,
|
||||
true
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_creates_pending_add_for_fight() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&None,
|
||||
&None,
|
||||
true
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
assert_eq!(result.new_consent.unwrap().fight_consent.unwrap().status, ConsentStatus::PendingAdd);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_goes_active_if_pending_add_matches() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails::default_for(&ConsentType::Fight),
|
||||
&None,
|
||||
&Some(Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent::default()),
|
||||
..Consent::default()
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
assert_eq!(result.new_consent.unwrap().fight_consent.unwrap().status, ConsentStatus::Active);
|
||||
assert_eq!(result.mirror_to_counterparty, true);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_creates_pending_update_to_change_fight() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails {
|
||||
freely_revoke: true,
|
||||
..ConsentDetails::default_for(&ConsentType::Fight)
|
||||
},
|
||||
&Some(Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
let fc = result.new_consent.as_ref().unwrap().fight_consent.as_ref().unwrap();
|
||||
assert_eq!(fc.status, ConsentStatus::Active);
|
||||
assert_eq!(fc.freely_revoke, false);
|
||||
assert_eq!(fc.pending_change.is_some(), true);
|
||||
assert_eq!(fc.pending_change.as_ref().unwrap()
|
||||
.fight_consent.as_ref().unwrap().freely_revoke, true);
|
||||
assert_eq!(result.mirror_to_counterparty, false);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_new_consent_state_accepts_pending_update_to_change_fight_if_matches() {
|
||||
let result =
|
||||
compute_new_consent_state(
|
||||
"Foo", "his", "Bar", "her", "from bar",
|
||||
&ConsentType::Fight,
|
||||
&ConsentDetails {
|
||||
freely_revoke: true,
|
||||
..ConsentDetails::default_for(&ConsentType::Fight)
|
||||
},
|
||||
&Some(Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
&Some(Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent {
|
||||
status: ConsentStatus::Active,
|
||||
pending_change: Some(Box::new(
|
||||
Consent {
|
||||
expires: Some(chrono::Utc::now() + chrono::Duration::minutes(60 * 24 * 7)),
|
||||
fight_consent: Some(FightConsent {
|
||||
freely_revoke: true,
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}
|
||||
)),
|
||||
..FightConsent::default()
|
||||
}),
|
||||
..Consent::default()
|
||||
}),
|
||||
true
|
||||
);
|
||||
assert_eq!(result.new_consent.is_some(), true);
|
||||
let fc = result.new_consent.as_ref().unwrap().fight_consent.as_ref().unwrap();
|
||||
assert_eq!(fc.status, ConsentStatus::Active);
|
||||
assert_eq!(fc.freely_revoke, true);
|
||||
assert_eq!(fc.pending_change, None);
|
||||
assert_eq!(result.mirror_to_counterparty, true);
|
||||
assert_eq!(result.first_party_message.is_some(), true);
|
||||
assert_eq!(result.counterparty_message.is_some(), true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn handle_user_consent(ctx: &mut VerbContext<'_>, source_player: &Item,
|
||||
to_username: &str, is_allow: bool, cmd: &AllowCommand<'_>) -> UResult<()> {
|
||||
ctx.trans.delete_expired_user_consent().await?;
|
||||
let to_user_item = search_item_for_user(ctx, &ItemSearchParams {
|
||||
include_all_players: true,
|
||||
..ItemSearchParams::base(source_player, to_username)
|
||||
}).await?;
|
||||
|
||||
if source_player.item_code == to_user_item.item_code {
|
||||
user_error("You know that's you, right?".to_owned())?;
|
||||
}
|
||||
|
||||
let current_consent = ctx.trans.find_user_consent_by_parties_type(
|
||||
&source_player.item_code,
|
||||
&to_user_item.item_code,
|
||||
&cmd.consent_type
|
||||
).await?;
|
||||
let converse_consent = if cmd.consent_type == ConsentType::Fight {
|
||||
ctx.trans.find_user_consent_by_parties_type(
|
||||
&to_user_item.item_code,
|
||||
&source_player.item_code,
|
||||
&cmd.consent_type
|
||||
).await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let update = compute_new_consent_state(
|
||||
&source_player.display_for_sentence(false, 1, false),
|
||||
&source_player.pronouns.possessive,
|
||||
&to_user_item.display_for_sentence(false, 1, false),
|
||||
&to_user_item.pronouns.possessive,
|
||||
&("from ".to_owned() + &source_player.item_code),
|
||||
&cmd.consent_type, &cmd.consent_details,
|
||||
¤t_consent, &converse_consent, is_allow
|
||||
);
|
||||
|
||||
match update.new_consent.as_ref() {
|
||||
None => ctx.trans.delete_user_consent(
|
||||
&source_player.item_code,
|
||||
&to_user_item.item_code,
|
||||
&cmd.consent_type
|
||||
).await?,
|
||||
Some(consent) => ctx.trans.upsert_user_consent(
|
||||
&source_player.item_code,
|
||||
&to_user_item.item_code,
|
||||
&cmd.consent_type,
|
||||
consent
|
||||
).await?,
|
||||
}
|
||||
if update.mirror_to_counterparty {
|
||||
match update.new_consent.as_ref() {
|
||||
None => ctx.trans.delete_user_consent(
|
||||
&to_user_item.item_code,
|
||||
&source_player.item_code,
|
||||
&cmd.consent_type
|
||||
).await?,
|
||||
Some(consent) => ctx.trans.upsert_user_consent(
|
||||
&to_user_item.item_code,
|
||||
&source_player.item_code,
|
||||
&cmd.consent_type,
|
||||
consent
|
||||
).await?,
|
||||
}
|
||||
}
|
||||
|
||||
match update.first_party_message {
|
||||
None => {},
|
||||
Some(msg) => ctx.trans.queue_for_session(&ctx.session, Some(&(msg + "\n"))).await?
|
||||
}
|
||||
match update.counterparty_message {
|
||||
None => {},
|
||||
Some(msg) => {
|
||||
match ctx.trans.find_session_for_player(&to_user_item.item_code).await? {
|
||||
None => {},
|
||||
Some((session, _)) =>
|
||||
ctx.trans.queue_for_session(&session, Some(&(msg + "\n"))).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 is_allow = verb == "allow";
|
||||
|
||||
let remaining_trim = remaining.trim();
|
||||
if remaining_trim == "" {
|
||||
// TODO: List all allows
|
||||
} else {
|
||||
let mut cmd = match parsing::parse_allow(remaining, !ctx.session_dat.less_explicit_mode) {
|
||||
Err(msg) => user_error(msg)?,
|
||||
Ok(cmd) => cmd
|
||||
};
|
||||
for place in cmd.consent_details.only_in.iter_mut() {
|
||||
if place == &"here" {
|
||||
let loc_code = match player_item.location.split_once("/") {
|
||||
None => user_error("Can't use \"in here\" where you are now".to_owned())?,
|
||||
Some((loc_type, _)) if loc_type != "room" =>
|
||||
user_error("Can't use \"in here\" outside a public room".to_owned())?,
|
||||
Some((_, loc_code)) => loc_code
|
||||
};
|
||||
*place = loc_code;
|
||||
}
|
||||
if room_map_by_code().get(*place).is_none() {
|
||||
user_error(format!("Place {} not found", *place))?
|
||||
}
|
||||
}
|
||||
match cmd.consent_target {
|
||||
ConsentTarget::CorpTarget { .. } => user_error(
|
||||
"Corporate allow/disallow not implemented yet".to_owned())?,
|
||||
ConsentTarget::UserTarget { to_user } =>
|
||||
handle_user_consent(ctx, &player_item, to_user, is_allow, &cmd).await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
static VERB_INT: Verb = Verb;
|
||||
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -5,8 +5,12 @@ use ansi::ansi;
|
||||
use crate::{
|
||||
services::{
|
||||
combat::start_attack,
|
||||
check_consent,
|
||||
},
|
||||
models::{
|
||||
consent::ConsentType,
|
||||
item::ItemFlag,
|
||||
},
|
||||
|
||||
db::ItemSearchParams,
|
||||
};
|
||||
|
||||
@ -26,6 +30,18 @@ impl UserVerb for Verb {
|
||||
..ItemSearchParams::base(&player_item, remaining)
|
||||
}).await?;
|
||||
|
||||
let (loctype, loccode) = match player_item.location.split_once("/") {
|
||||
None => user_error("Your current location is invalid!".to_owned())?,
|
||||
Some(l) => l
|
||||
};
|
||||
let player_loc = match ctx.trans.find_item_by_type_code(loctype, loccode).await? {
|
||||
None => user_error("Your current location is invalid!".to_owned())?,
|
||||
Some(l) => l
|
||||
};
|
||||
if player_loc.flags.contains(&ItemFlag::NoSeeContents) {
|
||||
user_error("It is too foggy to even see who is here, let alone attack!".to_owned())?;
|
||||
}
|
||||
|
||||
match attack_whom.item_type.as_str() {
|
||||
"npc" => {}
|
||||
"player" => {},
|
||||
@ -36,9 +52,8 @@ impl UserVerb for Verb {
|
||||
user_error("That's you, silly!".to_string())?
|
||||
}
|
||||
|
||||
if attack_whom.is_challenge_attack_only {
|
||||
// Add challenge check here.
|
||||
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an accepted challenge is very much functional. [Try <bold>help challenge<reset>]").to_string())?
|
||||
if !check_consent(ctx.trans, "attack", &ConsentType::Fight, &player_item, &attack_whom).await? {
|
||||
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an consented is very much functional. [Try <bold>help allow<reset>]").to_string())?
|
||||
}
|
||||
|
||||
if attack_whom.is_dead {
|
||||
|
@ -1,12 +1,15 @@
|
||||
use nom::{
|
||||
bytes::complete::{take_till, take_till1, take_while},
|
||||
character::{complete::{space0, space1, alpha1, one_of, char, u8}},
|
||||
character::{complete::{space0, space1, alpha1, one_of, char, u8, u16}},
|
||||
combinator::{recognize, fail, eof},
|
||||
sequence::terminated,
|
||||
branch::alt,
|
||||
error::{context, VerboseError, VerboseErrorKind},
|
||||
IResult,
|
||||
};
|
||||
use super::allow::{AllowCommand, ConsentTarget, ConsentDetails};
|
||||
use ansi::{ansi, strip_special_characters};
|
||||
use crate::models::consent::ConsentType;
|
||||
|
||||
pub fn parse_command_name(input: &str) -> (&str, &str) {
|
||||
fn parse(input: &str) -> IResult<&str, &str> {
|
||||
@ -85,6 +88,157 @@ pub fn parse_on_or_default<'l>(input: &'l str, default_on: &'l str) -> (&'l str,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_duration_mins<'l>(input: &'l str) -> Result<(u64, &'l str), String> {
|
||||
let (input, number) = match u16::<&'l str, ()>(input) {
|
||||
Err(_) => Err("Invalid number - duration should start with a number, e.g. 5 minutes")?,
|
||||
Ok(n) => n
|
||||
};
|
||||
let (tok, input) = match input.trim_start().split_once(" ") {
|
||||
None => (input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
Ok((match tok.to_lowercase().as_str() {
|
||||
"min" | "mins" | "minute" | "minutes" => number as u64,
|
||||
"h" | "hr" | "hrs" | "hour" | "hours" => (number as u64) * 60,
|
||||
"d" | "day" | "days" => (number as u64) * 60 * 24,
|
||||
"w" | "wk" | "wks" | "week" | "weeks" => (number as u64) * 60 * 24 * 7,
|
||||
_ => Err("Duration number needs to be followed by a valid unit - minutes, hours, days or weeks")?
|
||||
}, input))
|
||||
}
|
||||
|
||||
pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand, String> {
|
||||
let usage: &'static str =
|
||||
ansi!("Usage: allow <lt>action> from <lt>user> <lt>options> | allow <lt>action> against <lt>corp> by <lt>corp> <lt>options>. Try <bold>help allow<reset> for more.");
|
||||
let (consent_type_s, input) = match input.trim_start().split_once(" ") {
|
||||
None => Err(usage),
|
||||
Some(v) => Ok(v)
|
||||
}?;
|
||||
let consent_type = match ConsentType::from_str(&consent_type_s.trim().to_lowercase()) {
|
||||
None => Err(
|
||||
if is_explicit { "Invalid consent type - options are fight, medicine, gifts, visit and sex" } else {
|
||||
"Invalid consent type - options are fight, medicine, gifts and visit"
|
||||
}),
|
||||
Some(ct) => Ok(ct)
|
||||
}?;
|
||||
|
||||
let (tok, mut input) = match input.trim_start().split_once(" ") {
|
||||
None => Err(usage),
|
||||
Some(v) => Ok(v)
|
||||
}?;
|
||||
let tok_trim = tok.trim_start().to_lowercase();
|
||||
let consent_target =
|
||||
if tok_trim == "against" {
|
||||
if consent_type != ConsentType::Fight {
|
||||
Err("corps can only currently consent to fight, no other actions")?
|
||||
} else {
|
||||
let (my_corp_raw, new_input) = match input.trim_start().split_once(" ") {
|
||||
None => Err(usage),
|
||||
Some(v) => Ok(v)
|
||||
}?;
|
||||
let my_corp = my_corp_raw.trim_start();
|
||||
let (tok, new_input) = match new_input.trim_start().split_once(" ") {
|
||||
None => Err(usage),
|
||||
Some(v) => Ok(v)
|
||||
}?;
|
||||
if tok.trim_start().to_lowercase() != "by" {
|
||||
Err(usage)?;
|
||||
}
|
||||
let (target_corp_raw, new_input) = match new_input.trim_start().split_once(" ") {
|
||||
None => (new_input.trim_start(), ""),
|
||||
Some(v) => v
|
||||
};
|
||||
input = new_input;
|
||||
ConsentTarget::CorpTarget { from_corp: my_corp, to_corp: target_corp_raw.trim_start() }
|
||||
}
|
||||
} else if tok_trim == "from" {
|
||||
let (target_user_raw, new_input) = match input.trim_start().split_once(" ") {
|
||||
None => (input.trim_start(), ""),
|
||||
Some(v) => v
|
||||
};
|
||||
input = new_input;
|
||||
ConsentTarget::UserTarget { to_user: target_user_raw.trim_start() }
|
||||
} else {
|
||||
Err(usage)?
|
||||
};
|
||||
|
||||
let mut consent_details = ConsentDetails::default_for(&consent_type);
|
||||
loop {
|
||||
input = input.trim_start();
|
||||
if input == "" {
|
||||
break;
|
||||
}
|
||||
let (tok, new_input) = match input.split_once(" ") {
|
||||
None => (input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
match tok.to_lowercase().as_str() {
|
||||
"for" => {
|
||||
let (minutes, new_input) = parse_duration_mins(new_input)?;
|
||||
input = new_input;
|
||||
consent_details.duration_minutes = Some(minutes);
|
||||
}
|
||||
"until" => {
|
||||
let (tok, new_input) = match new_input.split_once(" ") {
|
||||
None => (input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
if tok.trim_start().to_lowercase() != "death" {
|
||||
Err("Option until needs to be followed with death - until death")?
|
||||
}
|
||||
consent_details.until_death = true;
|
||||
input = new_input;
|
||||
}
|
||||
"allow" => {
|
||||
let (tok, new_input) = match new_input.split_once(" ") {
|
||||
None => (new_input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
match tok.trim_start().to_lowercase().as_str() {
|
||||
"private" => {
|
||||
consent_details.allow_private = true;
|
||||
},
|
||||
"pick" => {
|
||||
consent_details.allow_pick = true;
|
||||
},
|
||||
"revoke" => {
|
||||
consent_details.freely_revoke = true;
|
||||
},
|
||||
_ => Err("Option allow needs to be followed with private, pick or revoke - allow private | allow pick | allow revoke")?
|
||||
}
|
||||
input = new_input;
|
||||
}
|
||||
"disallow" => {
|
||||
let (tok, new_input) = match new_input.split_once(" ") {
|
||||
None => (new_input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
match tok.trim_start().to_lowercase().as_str() {
|
||||
"private" => {
|
||||
consent_details.allow_private = false;
|
||||
},
|
||||
"pick" => {
|
||||
consent_details.allow_pick = false;
|
||||
},
|
||||
_ => Err("Option disallow needs to be followed with private or pick - disallow private | disallow pick")?
|
||||
}
|
||||
input = new_input;
|
||||
}
|
||||
"in" => {
|
||||
let (tok, new_input) = match new_input.split_once(" ") {
|
||||
None => (new_input, ""),
|
||||
Some(v) => v
|
||||
};
|
||||
consent_details.only_in.push(tok);
|
||||
input = new_input;
|
||||
}
|
||||
_ => Err(format!("I don't understand the option \"{}\"", strip_special_characters(tok)))?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(AllowCommand { consent_type: consent_type, consent_target: consent_target, consent_details: consent_details })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -182,4 +336,42 @@ mod tests {
|
||||
fn parse_offset_supports_offset() {
|
||||
assert_eq!(parse_offset("2.hello world"), (Some(2), "hello world"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_consent_works_default_options_user() {
|
||||
assert_eq!(super::parse_allow("medicine From Athorina", false),
|
||||
Ok(AllowCommand {
|
||||
consent_type: ConsentType::Medicine,
|
||||
consent_target: ConsentTarget::UserTarget { to_user: "Athorina" },
|
||||
consent_details: ConsentDetails::default_for(&ConsentType::Medicine)
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_consent_works_default_options_corp() {
|
||||
assert_eq!(super::parse_allow("Fight Against megacorp By supercorp", false),
|
||||
Ok(AllowCommand {
|
||||
consent_type: ConsentType::Fight,
|
||||
consent_target: ConsentTarget::CorpTarget { from_corp: "megacorp", to_corp: "supercorp" },
|
||||
consent_details: ConsentDetails::default_for(&ConsentType::Fight)
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_consent_handles_options() {
|
||||
assert_eq!(super::parse_allow("fighT fRom athorina For 2 hOurs unTil deAth allOw priVate Disallow pIck alLow revoKe iN here in pit", false),
|
||||
Ok(AllowCommand {
|
||||
consent_type: ConsentType::Fight,
|
||||
consent_target: ConsentTarget::UserTarget { to_user: "athorina" },
|
||||
consent_details: ConsentDetails {
|
||||
duration_minutes: Some(120),
|
||||
until_death: true,
|
||||
allow_private: true,
|
||||
allow_pick: false,
|
||||
freely_revoke: true,
|
||||
only_in: vec!("here", "pit"),
|
||||
..ConsentDetails::default_for(&ConsentType::Fight)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,42 @@ use crate::models::{user::User, item::{Item, Pronouns}};
|
||||
use chrono::Utc;
|
||||
use ansi::ansi;
|
||||
use tokio::time;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn is_invalid_username(name: &str) -> bool {
|
||||
static INVALID_PREFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
|
||||
static INVALID_SUFFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
|
||||
static INVALID_WORDS: OnceCell<HashSet<&'static str>> = OnceCell::new();
|
||||
let invalid_prefixes = INVALID_PREFIXES.get_or_init(|| vec!(
|
||||
"admin", "god", "helper", "npc", "corpse", "dead"
|
||||
));
|
||||
let invalid_suffixes = INVALID_SUFFIXES.get_or_init(|| vec!(
|
||||
"bot"
|
||||
));
|
||||
let invalid_words = INVALID_WORDS.get_or_init(|| HashSet::from(
|
||||
["corp", "to", "from", "dog", "bot"]
|
||||
));
|
||||
if invalid_words.contains(name) {
|
||||
return true;
|
||||
}
|
||||
for pfx in invalid_prefixes.iter() {
|
||||
if name.starts_with(pfx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for sfx in invalid_suffixes.iter() {
|
||||
if name.ends_with(sfx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
pub struct Verb;
|
||||
#[async_trait]
|
||||
impl UserVerb for Verb {
|
||||
impl UserVerb for Verb {
|
||||
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> {
|
||||
let (username, password, email) = match parse_username(remaining) {
|
||||
Err(e) => user_error("Invalid username: ".to_owned() + e)?,
|
||||
@ -20,6 +52,10 @@ impl UserVerb for Verb {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if is_invalid_username(&username.to_lowercase()) {
|
||||
user_error("Sorry, that username isn't allowed. Try another".to_owned())?;
|
||||
}
|
||||
|
||||
if ctx.trans.find_by_username(username).await?.is_some() {
|
||||
user_error("Username already exists".to_owned())?;
|
||||
|
@ -94,13 +94,11 @@ impl QueueCommandHandler for QueueHandler {
|
||||
if item.location != format!("player/{}", player_item.item_code) {
|
||||
user_error("You try to wield it but realise you no longer have it".to_owned())?
|
||||
}
|
||||
let msg_exp = format!("{} wields {} {}\n",
|
||||
let msg_exp = format!("{} wields {}\n",
|
||||
&player_item.display_for_sentence(true, 1, true),
|
||||
&player_item.pronouns.possessive,
|
||||
&item.display_for_sentence(true, 1, false));
|
||||
let msg_nonexp = format!("{} wields {} {}\n",
|
||||
let msg_nonexp = format!("{} wields {}\n",
|
||||
&player_item.display_for_sentence(false, 1, true),
|
||||
&player_item.pronouns.possessive,
|
||||
&item.display_for_sentence(false, 1, false));
|
||||
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||
ctx.trans.set_exclusive_action_type_to(&item,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug)]
|
||||
pub enum ConsentType {
|
||||
Fight,
|
||||
Medicine,
|
||||
@ -10,27 +10,74 @@ pub enum ConsentType {
|
||||
Sex
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
impl ConsentType {
|
||||
pub fn from_str(inp: &str) -> Option<ConsentType> {
|
||||
use ConsentType::*;
|
||||
match inp {
|
||||
"fight" => Some(Fight),
|
||||
"medicine" => Some(Medicine),
|
||||
"gifts" => Some(Gifts),
|
||||
"visit" => Some(Visit),
|
||||
"sex" => Some(Sex),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
use ConsentType::*;
|
||||
match self {
|
||||
Fight => "fight",
|
||||
Medicine => "medicine",
|
||||
Gifts => "gifts",
|
||||
Visit => "visit",
|
||||
Sex => "sex",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
pub enum ConsentStatus {
|
||||
PendingAdd, // Added but awaiting other party to ratify by giving matching consent.
|
||||
Active, // Consent in force, no delete pending.
|
||||
PendingDelete, // Pending cancellation but other party has to also disallow to ratify.
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
pub struct FightConsent {
|
||||
status: ConsentStatus,
|
||||
pending_change: Option<Box<Consent>>,
|
||||
allow_pick: bool,
|
||||
freely_revoke: bool,
|
||||
pub status: ConsentStatus,
|
||||
pub pending_change: Option<Box<Consent>>,
|
||||
pub allow_pick: bool,
|
||||
pub freely_revoke: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Consent {
|
||||
consent_type: ConsentType,
|
||||
fight_consent: Option<FightConsent>,
|
||||
expires: Option<DateTime<Utc>>,
|
||||
only_in: Vec<String>,
|
||||
allow_private: bool,
|
||||
until_death: bool,
|
||||
impl Default for FightConsent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
status: ConsentStatus::PendingAdd,
|
||||
pending_change: None,
|
||||
allow_pick: false,
|
||||
freely_revoke: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
pub struct Consent {
|
||||
pub fight_consent: Option<FightConsent>,
|
||||
pub expires: Option<DateTime<Utc>>,
|
||||
pub only_in: Vec<String>,
|
||||
pub allow_private: bool,
|
||||
pub until_death: bool,
|
||||
}
|
||||
|
||||
impl Default for Consent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
fight_consent: None,
|
||||
expires: None,
|
||||
only_in: vec!(),
|
||||
allow_private: false,
|
||||
until_death: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -307,7 +307,6 @@ pub struct Item {
|
||||
pub presence_target: Option<String>, // e.g. what are they sitting on.
|
||||
pub is_static: bool,
|
||||
pub is_dead: bool,
|
||||
pub is_challenge_attack_only: bool,
|
||||
pub species: SpeciesType,
|
||||
pub health: u64,
|
||||
pub total_xp: u64,
|
||||
@ -385,7 +384,6 @@ impl Default for Item {
|
||||
presence_target: None,
|
||||
is_static: false,
|
||||
is_dead: false,
|
||||
is_challenge_attack_only: true,
|
||||
species: SpeciesType::Human,
|
||||
health: 24,
|
||||
total_xp: 0,
|
||||
|
@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
DResult,
|
||||
models::item::Item,
|
||||
models::consent::{Consent, ConsentType, ConsentStatus},
|
||||
static_content::npc::npc_by_code,
|
||||
};
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
@ -28,3 +30,57 @@ pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Optio
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
|
||||
if let Some((loctype, loccode)) = target.location.split_once("/") {
|
||||
if !consent.only_in.is_empty() {
|
||||
if loctype != "room" || !consent.only_in.iter().any(|v| v == loccode) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !consent.allow_private && loctype != "room" {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if !consent.only_in.is_empty() || !consent.allow_private {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(fight_consent) = consent.fight_consent.as_ref() {
|
||||
if fight_consent.status == ConsentStatus::PendingAdd {
|
||||
return false;
|
||||
}
|
||||
if !fight_consent.allow_pick && action == "pick" {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn check_consent(trans: &DBTrans, action: &str,
|
||||
consent_type: &ConsentType,
|
||||
by: &Item,
|
||||
target: &Item) -> DResult<bool> {
|
||||
// Consent is only a factor on actions by players towards other players or npcs.
|
||||
if by.item_type != "player" || (target.item_type != "player" && target.item_type != "npc") {
|
||||
return Ok(true);
|
||||
}
|
||||
if target.item_type == "npc" {
|
||||
return Ok(match npc_by_code().get(target.item_code.as_str()) {
|
||||
None => false,
|
||||
Some(npc) => npc.player_consents.contains(consent_type)
|
||||
});
|
||||
}
|
||||
|
||||
trans.delete_expired_user_consent().await?;
|
||||
if let Some(consent) = trans.find_user_consent_by_parties_type(
|
||||
&target.item_code, &by.item_code, consent_type).await? {
|
||||
if check_one_consent(&consent, action, &target) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ use super::{
|
||||
};
|
||||
use crate::models::{
|
||||
item::{Item, Pronouns, SkillType},
|
||||
task::{Task, TaskMeta, TaskRecurrence, TaskDetails}
|
||||
task::{Task, TaskMeta, TaskRecurrence, TaskDetails},
|
||||
consent::{ConsentType},
|
||||
};
|
||||
use crate::services::{
|
||||
combat::{
|
||||
@ -75,7 +76,6 @@ pub struct NPC {
|
||||
pub message_handler: Option<&'static (dyn NPCMessageHandler + Sync + Send)>,
|
||||
pub aliases: Vec<&'static str>,
|
||||
pub says: Vec<NPCSayInfo>,
|
||||
pub attackable: bool,
|
||||
pub aggression: u64,
|
||||
pub max_health: u64,
|
||||
pub intrinsic_weapon: Option<PossessionType>,
|
||||
@ -84,6 +84,7 @@ pub struct NPC {
|
||||
pub species: SpeciesType,
|
||||
pub wander_zones: Vec<&'static str>,
|
||||
pub kill_bonus: Option<KillBonus>,
|
||||
pub player_consents: Vec<ConsentType>,
|
||||
}
|
||||
|
||||
impl Default for NPC {
|
||||
@ -100,13 +101,13 @@ impl Default for NPC {
|
||||
total_xp: 1000,
|
||||
total_skills: SkillType::values().into_iter()
|
||||
.map(|sk| (sk.clone(), if &sk == &SkillType::Dodge { 8.0 } else { 10.0 })).collect(),
|
||||
attackable: false,
|
||||
aggression: 0,
|
||||
max_health: 24,
|
||||
intrinsic_weapon: None,
|
||||
species: SpeciesType::Human,
|
||||
wander_zones: vec!(),
|
||||
kill_bonus: None,
|
||||
player_consents: vec!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,7 +164,6 @@ pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
|
||||
location: c.spawn_location.to_owned(),
|
||||
is_static: true,
|
||||
pronouns: c.pronouns.clone(),
|
||||
is_challenge_attack_only: !c.attackable,
|
||||
total_xp: c.total_xp.clone(),
|
||||
total_skills: c.total_skills.clone(),
|
||||
species: c.species.clone(),
|
||||
|
@ -1,5 +1,8 @@
|
||||
use super::{NPC, NPCSayInfo, NPCSayType};
|
||||
use crate::models::item::Pronouns;
|
||||
use crate::models::{
|
||||
item::Pronouns,
|
||||
consent::ConsentType,
|
||||
};
|
||||
|
||||
pub fn npc_list() -> Vec<NPC> {
|
||||
use NPCSayType::FromFixedList;
|
||||
@ -29,6 +32,7 @@ pub fn npc_list() -> Vec<NPC> {
|
||||
message_handler: None,
|
||||
wander_zones: vec!("melbs"),
|
||||
says: vec!(melbs_citizen_stdsay.clone()),
|
||||
player_consents: vec!(ConsentType::Medicine, ConsentType::Sex),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use super::{NPC, KillBonus};
|
||||
use crate::models::item::Pronouns;
|
||||
use crate::models::{
|
||||
item::Pronouns,
|
||||
consent::ConsentType,
|
||||
};
|
||||
use crate::static_content::{
|
||||
possession_type::PossessionType,
|
||||
species::SpeciesType
|
||||
@ -11,7 +14,6 @@ macro_rules! dog {
|
||||
code: concat!("melbs_dog_", $code),
|
||||
name: concat!($adj, " dog"),
|
||||
pronouns: Pronouns { is_proper: false, ..Pronouns::default_inanimate() },
|
||||
attackable: true,
|
||||
aggression: 12,
|
||||
wander_zones: vec!("melbs"),
|
||||
description: "A malnourished looking dog. Its skeleton is visible through its thin and patchy fur. It smells terrible, and certainly doesn't look tame.",
|
||||
@ -23,6 +25,7 @@ macro_rules! dog {
|
||||
msg: "On your wristpad: Thank you for helping Melbs with animal control! Here's your fee.",
|
||||
payment: 100,
|
||||
}),
|
||||
player_consents: vec!(ConsentType::Fight),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
@ -69,15 +69,19 @@ CREATE TABLE corp_membership (
|
||||
CREATE TABLE user_consent (
|
||||
consenting_user TEXT NOT NULL REFERENCES users(username),
|
||||
consented_user TEXT NOT NULL REFERENCES users(username),
|
||||
consent_type TEXT NOT NULL,
|
||||
details JSONB NOT NULL,
|
||||
PRIMARY KEY (consenting_user, consented_user)
|
||||
PRIMARY KEY (consenting_user, consented_user, consent_type)
|
||||
);
|
||||
CREATE INDEX user_consent_by_consented ON user_consent (consented_user);
|
||||
CREATE INDEX user_consent_by_expires ON user_consent ((details->>'expires'));
|
||||
|
||||
CREATE TABLE corp_consent (
|
||||
consenting_corp BIGINT NOT NULL REFERENCES corps(corp_id),
|
||||
consented_corp BIGINT NOT NULL REFERENCES corps(corp_id),
|
||||
consent_type TEXT NOT NULL,
|
||||
details JSONB NOT NULL,
|
||||
PRIMARY KEY (consenting_corp, consented_corp)
|
||||
PRIMARY KEY (consenting_corp, consented_corp, consent_type)
|
||||
);
|
||||
CREATE INDEX corp_consent_by_consented ON corp_consent (consented_corp);
|
||||
CREATE INDEX corp_consent_by_expires ON corp_consent ((details->>'expires'));
|
||||
|
Loading…
Reference in New Issue
Block a user