From 3bd0412e4a53d1b640699ffc5eee978f94f8866e Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 25 Mar 2023 00:58:19 +1100 Subject: [PATCH] Allow people to join, leave, and fire from corps. --- blastmud_game/src/db.rs | 66 +++++++- .../src/message_handler/user_commands/corp.rs | 152 ++++++++++++++++-- .../src/message_handler/user_commands/drop.rs | 2 +- .../src/message_handler/user_commands/get.rs | 2 +- .../message_handler/user_commands/movement.rs | 2 +- .../src/message_handler/user_commands/say.rs | 2 +- .../message_handler/user_commands/use_cmd.rs | 2 +- .../message_handler/user_commands/wield.rs | 2 +- blastmud_game/src/models/corp.rs | 20 ++- blastmud_game/src/services.rs | 22 +-- blastmud_game/src/services/combat.rs | 2 +- blastmud_game/src/services/comms.rs | 25 +++ blastmud_game/src/services/effect.rs | 2 +- .../possession_type/corp_licence.rs | 2 +- 14 files changed, 258 insertions(+), 45 deletions(-) create mode 100644 blastmud_game/src/services/comms.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index f5ca9cd3..c109f407 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -18,7 +18,7 @@ use crate::models::{ }, task::{Task, TaskParse}, consent::{Consent, ConsentType}, - corp::{Corp, CorpId, CorpMembership}, + corp::{Corp, CorpId, CorpMembership, CorpCommType}, }; use tokio_postgres::types::ToSql; use std::collections::BTreeSet; @@ -886,6 +886,70 @@ impl DBTrans { .collect()) } + pub async fn broadcast_to_corp<'a>(self: &'a Self, + corp_id: &'a CorpId, + comm_type: &'a CorpCommType, + except_user: Option<&'a str>, + message: &'a str) -> DResult<()> { + let comm_type_s = serde_json::to_value(comm_type)?.as_str() + .ok_or("comm type doesn't serialise to JSON string")? + .to_owned(); + let mut params : Vec<&(dyn ToSql + Sync)> = + vec!(&message, &corp_id.0, &comm_type_s); + let mut query = "INSERT INTO sendqueue (session, listener, message) \ + SELECT s.session, s.listener, $1 FROM \ + sessions s \ + JOIN users u ON u.current_session = s.session \ + JOIN corp_membership m ON m.member_username = u.username \ + WHERE m.corp_id = $2 AND \ + (m.details->'comms_on') ? $3 AND \ + m.details->>'joined_at' IS NOT NULL".to_owned(); + match except_user.as_ref() { + None => {}, + Some(u) => { + query.push_str(" AND u.username <> $4"); + params.push(u); + } + } + self.pg_trans()? + .execute(&query, ¶ms).await?; + Ok(()) + } + + pub async fn list_corp_members<'a>(self: &'a Self, + corp_id: &'a CorpId) -> + DResult> { + Ok(self.pg_trans()? + .query("SELECT member_username, details \ + FROM corp_membership WHERE \ + corp_id = $1", &[&corp_id.0]).await? + .iter() + .filter_map(|i| Some( + (i.get("member_username"), + serde_json::from_value(i.get("details")).ok()?))) + .collect()) + } + + pub async fn delete_corp<'a>(self: &'a Self, + corp_id: &'a CorpId) -> DResult<()> { + self.pg_trans()? + .execute("DELETE FROM corps WHERE \ + corp_id = $1", &[&corp_id.0]).await?; + Ok(()) + } + + pub async fn delete_corp_membership<'a>( + self: &'a Self, + corp_id: &'a CorpId, + username: &'a str + ) -> DResult<()> { + self.pg_trans()? + .execute("DELETE FROM corp_membership WHERE \ + corp_id = $1 AND member_username = $2", + &[&corp_id.0, &username.to_lowercase()]).await?; + Ok(()) + } + pub async fn commit(mut self: Self) -> DResult<()> { let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); if let Some(trans) = trans_opt { diff --git a/blastmud_game/src/message_handler/user_commands/corp.rs b/blastmud_game/src/message_handler/user_commands/corp.rs index 8b14a789..93086985 100644 --- a/blastmud_game/src/message_handler/user_commands/corp.rs +++ b/blastmud_game/src/message_handler/user_commands/corp.rs @@ -10,8 +10,9 @@ use super::{ parsing::parse_command_name, }; use crate::{ - models::corp::{CorpMembership, CorpPermission}, + models::corp::{CorpMembership, CorpPermission, CorpCommType}, db::ItemSearchParams, + language::caps_first, }; use chrono::Utc; use async_trait::async_trait; @@ -36,7 +37,7 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> Some(c) => c }; if !check_corp_perm(&CorpPermission::Hire, &mem) || mem.joined_at.is_none() { - user_error("You don't have hire permissions for that corp".to_owned())?; + user_error("You don't have hiring permissions for that corp".to_owned())?; } let target_user = search_item_for_user(ctx, &ItemSearchParams { @@ -58,9 +59,15 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? { None => (), Some((_, _, CorpMembership { invited_at: Some(_), .. })) => - user_error("They've already been invited.".to_owned())?, + user_error( + format!("{}'s already been invited.", + &caps_first(&target_user.pronouns.subject) + ))?, Some((_, _, _)) => - user_error("They're already hired.".to_owned())?, + user_error( + format!("{}'s already hired.", + &caps_first(&target_user.pronouns.subject) + ))?, }; let new_mem = CorpMembership { @@ -81,18 +88,36 @@ async fn corp_invite(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> "" } ))).await?; - ctx.trans.queue_for_session( - &ctx.session, - Some(&format!( - "You offer to hire {} into {}.\n", - &target_user.display_for_sentence(!their_sess_dat.less_explicit_mode, 1, false), - &corp.name - ))).await?; + ctx.trans.broadcast_to_corp( + &corp_id, + &CorpCommType::Notice, None, + &format!("{} has invited {} to join {}!\n", + user.username, + &target_user.display_for_sentence(false, 1, false), + corp.name)).await?; Ok(()) } -async fn corp_join(_ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> { - user_error("Join not implemented yet".to_owned())? +async fn corp_join(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { + let user = get_user_or_fail(ctx)?; + let (corpid, corp, mut mem) = match ctx.trans.match_user_corp_by_name(remaining, &user.username).await? { + None => user_error("Corp not found".to_owned())?, + Some(v) => v + }; + if mem.joined_at.is_some() { + user_error("You have already joined that corp!".to_owned())?; + } + mem.joined_at = Some(Utc::now()); + mem.invited_at = None; + ctx.trans.upsert_corp_membership(&corpid, &user.username, &mem).await?; + + ctx.trans.broadcast_to_corp( + &corpid, + &CorpCommType::Notice, None, + &format!(ansi!("There is a loud cheer as {} accepts an offer to join {}!\n"), + &user.username, &corp.name)).await?; + + Ok(()) } async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> { @@ -124,6 +149,105 @@ async fn corp_list(ctx: &mut VerbContext<'_>, _remaining: &str) -> UResult<()> { Ok(()) } +async fn corp_leave(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { + let user = get_user_or_fail(ctx)?; + let (corpid, corp, mem) = match ctx.trans.match_user_corp_by_name(remaining, &user.username).await? { + None => user_error("Corp not found".to_owned())?, + Some(v) => v + }; + if mem.joined_at.is_some() { + ctx.trans.broadcast_to_corp( + &corpid, + &CorpCommType::Notice, None, + &format!(ansi!("There is a loud boo as {} resigns from {}!\n"), user.username, corp.name)).await?; + } else { + ctx.trans.queue_for_session(ctx.session, Some( + &format!("You decline your invitation to join {}\n", + corp.name))).await?; + } + + let mut delete_corp = false; + if mem.permissions.contains(&CorpPermission::Holder) { + let username_l = user.username.to_lowercase(); + let members = ctx.trans.list_corp_members(&corpid).await?; + if members.len() == 1 { + delete_corp = true; + } else if !members.iter().any( + |(name, mem)| *name != username_l && + mem.permissions.contains(&CorpPermission::Holder)) { + user_error("The last holder cannot resign from a non-empty \ + corp - fire everyone else first, or promote a \ + successor to holder".to_owned())?; + } + } + ctx.trans.delete_corp_membership(&corpid, &user.username).await?; + if delete_corp { + ctx.trans.delete_corp(&corpid).await?; + } + Ok(()) +} + +async fn corp_fire(ctx: &mut VerbContext<'_>, remaining: &str) -> UResult<()> { + let (target_raw, into_raw) = match remaining.rsplit_once(" from ") { + None => user_error( + ansi!("Usage: corp fire username from corpname").to_owned() + )?, + Some(c) => c + }; + let user = get_user_or_fail(ctx)?; + let player = get_player_item_or_fail(ctx).await?; + let (corp_id, corp, mem) = + match ctx.trans.match_user_corp_by_name(into_raw.trim(), &user.username).await? { + None => user_error("No such corp!".to_owned())?, + Some(c) => c + }; + if !check_corp_perm(&CorpPermission::Fire, &mem) || mem.joined_at.is_none() { + user_error("You don't have firing permissions for that corp".to_owned())?; + } + + let target_user = search_item_for_user(ctx, &ItemSearchParams { + include_all_players: true, + ..ItemSearchParams::base(&player, target_raw.trim()) + }).await?; + + if target_user.item_type != "player" { + user_error("Only players can be fired.".to_owned())?; + } + + match ctx.trans.match_user_corp_by_name(into_raw.trim(), &target_user.item_code).await? { + None => user_error(format!( + "{} isn't currently hired.", + &caps_first(&target_user.pronouns.subject) + + ))?, + Some((_, _, CorpMembership { permissions: their_perm, + joined_at: Some(their_join), ..})) => { + if their_perm.contains(&CorpPermission::Holder) { + if !mem.permissions.contains(&CorpPermission::Holder) { + user_error("I love the ambition, but only holders can fire holders!".to_owned())?; + } + if their_join < mem.joined_at.unwrap_or(Utc::now()) { + user_error("Whoah there young whippersnapper, holders can't fire more senior holders!".to_owned())?; + } + } + }, + _ => {} + } + + + ctx.trans.broadcast_to_corp( + &corp_id, + &CorpCommType::Notice, None, + &format!(ansi!("A nervous silence falls across \ + the workers as {} fires {} from {}.\n"), + user.username, + target_user.display_for_sentence(false, 1, false), + corp.name)).await?; + + ctx.trans.delete_corp_membership(&corp_id, &target_user.item_code).await?; + Ok(()) +} + pub struct Verb; #[async_trait] impl UserVerb for Verb { @@ -133,6 +257,8 @@ impl UserVerb for Verb { "hire" | "invite" => corp_invite(ctx, remaining).await?, "join" => corp_join(ctx, remaining).await?, "" | "list" => corp_list(ctx, remaining).await?, + "leave" | "resign" => corp_leave(ctx, remaining).await?, + "fire" | "dismiss" => corp_fire(ctx, remaining).await?, _ => user_error("Unknown command".to_owned())? } Ok(()) diff --git a/blastmud_game/src/message_handler/user_commands/drop.rs b/blastmud_game/src/message_handler/user_commands/drop.rs index dd0e5a91..634aacfc 100644 --- a/blastmud_game/src/message_handler/user_commands/drop.rs +++ b/blastmud_game/src/message_handler/user_commands/drop.rs @@ -21,7 +21,7 @@ use crate::{ TaskRunContext, }, services::{ - broadcast_to_room, + comms::broadcast_to_room, capacity::{ check_item_capacity, CapacityLevel, diff --git a/blastmud_game/src/message_handler/user_commands/get.rs b/blastmud_game/src/message_handler/user_commands/get.rs index d32eecaf..c6c56c32 100644 --- a/blastmud_game/src/message_handler/user_commands/get.rs +++ b/blastmud_game/src/message_handler/user_commands/get.rs @@ -17,7 +17,7 @@ use crate::{ queue_command }, services::{ - broadcast_to_room, + comms::broadcast_to_room, capacity::{ check_item_capacity, CapacityLevel, diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 345ba351..8e4af978 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -19,7 +19,7 @@ use crate::{ LocationActionType }, services::{ - broadcast_to_room, + comms::broadcast_to_room, skills::skill_check_and_grind, combat::stop_attacking_mut, combat::handle_resurrect, diff --git a/blastmud_game/src/message_handler/user_commands/say.rs b/blastmud_game/src/message_handler/user_commands/say.rs index 55f60e58..868e26f9 100644 --- a/blastmud_game/src/message_handler/user_commands/say.rs +++ b/blastmud_game/src/message_handler/user_commands/say.rs @@ -3,7 +3,7 @@ use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, get_player_item_or_fail, is_likely_explicit}; use crate::{ models::item::{Item, ItemFlag}, - services::broadcast_to_room, + services::comms::broadcast_to_room, }; use mockall_double::double; #[double] use crate::db::DBTrans; diff --git a/blastmud_game/src/message_handler/user_commands/use_cmd.rs b/blastmud_game/src/message_handler/user_commands/use_cmd.rs index ff636b6e..b4048a54 100644 --- a/blastmud_game/src/message_handler/user_commands/use_cmd.rs +++ b/blastmud_game/src/message_handler/user_commands/use_cmd.rs @@ -22,7 +22,7 @@ use crate::{ SkillType, }, services::{ - broadcast_to_room, + comms::broadcast_to_room, skills::skill_check_and_grind, effect::run_effects, check_consent, diff --git a/blastmud_game/src/message_handler/user_commands/wield.rs b/blastmud_game/src/message_handler/user_commands/wield.rs index 9c552694..85bfd7b1 100644 --- a/blastmud_game/src/message_handler/user_commands/wield.rs +++ b/blastmud_game/src/message_handler/user_commands/wield.rs @@ -20,7 +20,7 @@ use crate::{ SkillType, }, services::{ - broadcast_to_room, + comms::broadcast_to_room, skills::skill_check_and_grind, }, }; diff --git a/blastmud_game/src/models/corp.rs b/blastmud_game/src/models/corp.rs index f9a4150d..dba54d94 100644 --- a/blastmud_game/src/models/corp.rs +++ b/blastmud_game/src/models/corp.rs @@ -9,6 +9,15 @@ pub enum CorpPermission { ChangeJobTitle, } +#[derive(Serialize, Deserialize, PartialEq)] +pub enum CorpCommType { + Chat, + Notice, + Connect, + Reward, + Death, +} + pub struct CorpId(pub i64); #[derive(Serialize, Deserialize)] @@ -41,11 +50,12 @@ pub struct CorpMembership { pub allow_combat: bool, pub job_title: String, pub priority: i64, - pub chat_on: bool + pub comms_on: Vec, } impl Default for CorpMembership { fn default() -> CorpMembership { + use CorpCommType::*; CorpMembership { invited_at: None, joined_at: None, @@ -53,7 +63,13 @@ impl Default for CorpMembership { allow_combat: false, job_title: "Employee".to_owned(), priority: 100, - chat_on: true, + comms_on: vec!( + Chat, + Notice, + Connect, + Reward, + Death, + ), } } } diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index 0d048645..7ef1d606 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -7,30 +7,12 @@ use crate::{ use mockall_double::double; #[double] use crate::db::DBTrans; -pub mod skills; +pub mod comms; pub mod combat; +pub mod skills; pub mod capacity; pub mod effect; -pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>, - message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> { - for item in trans.find_items_by_location(location).await? { - if item.item_type != "player" || item.is_dead { - continue; - } - if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? { - if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) { - if let Some(msg) = message_nonexplicit { - trans.queue_for_session(&session, Some(msg)).await?; - } - return Ok(()); - } - trans.queue_for_session(&session, Some(message_explicit_ok)).await?; - } - } - 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() { diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 99b78b16..84e4efd8 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -1,6 +1,6 @@ use crate::{ services::{ - broadcast_to_room, + comms::broadcast_to_room, skills::skill_check_and_grind, skills::skill_check_only, }, diff --git a/blastmud_game/src/services/comms.rs b/blastmud_game/src/services/comms.rs new file mode 100644 index 00000000..cb020dfb --- /dev/null +++ b/blastmud_game/src/services/comms.rs @@ -0,0 +1,25 @@ +use crate::{ + DResult, + models::item::Item, +}; +use mockall_double::double; +#[double] use crate::db::DBTrans; + +pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>, + message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> { + for item in trans.find_items_by_location(location).await? { + if item.item_type != "player" || item.is_dead { + continue; + } + if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? { + if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) { + if let Some(msg) = message_nonexplicit { + trans.queue_for_session(&session, Some(msg)).await?; + } + return Ok(()); + } + trans.queue_for_session(&session, Some(message_explicit_ok)).await?; + } + } + Ok(()) +} diff --git a/blastmud_game/src/services/effect.rs b/blastmud_game/src/services/effect.rs index 09c33579..276b924a 100644 --- a/blastmud_game/src/services/effect.rs +++ b/blastmud_game/src/services/effect.rs @@ -18,7 +18,7 @@ use crate::{ } }; use super::{ - broadcast_to_room, + comms::broadcast_to_room, combat::change_health, }; use async_trait::async_trait; diff --git a/blastmud_game/src/static_content/possession_type/corp_licence.rs b/blastmud_game/src/static_content/possession_type/corp_licence.rs index 4c35f455..4b77b5b1 100644 --- a/blastmud_game/src/static_content/possession_type/corp_licence.rs +++ b/blastmud_game/src/static_content/possession_type/corp_licence.rs @@ -11,7 +11,7 @@ use crate::{ UResult, VerbContext, }, - services::broadcast_to_room, + services::comms::broadcast_to_room, }; use ansi::ansi; use async_trait::async_trait;