use super::{ VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, get_user_or_fail, search_item_for_user, parsing, corp::check_corp_perm, }; use async_trait::async_trait; use crate::{ models::{ consent::{Consent, ConsentType, ConsentStatus, FightConsent}, corp::{CorpPermission, CorpCommType}, item::Item }, db::ItemSearchParams, static_content::room::room_map_by_code, }; use ansi::ansi; use itertools::Itertools; #[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, 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, pub first_party_message: Option, pub counterparty_message: Option, 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, their_current_consent: &Option, 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, session_dat)) => if cmd.consent_type != ConsentType::Sex || !session_dat.less_explicit_mode { ctx.trans.queue_for_session(&session, Some(&(msg + "\n"))).await?; } } } } Ok(()) } async fn handle_corp_consent(ctx: &mut VerbContext<'_>, source_player: &Item, from_corp: &str, to_corp: &str, is_allow: bool, cmd: &AllowCommand<'_> ) -> UResult<()> { let user = get_user_or_fail(ctx)?; let (from_corp_id, from_corp, mem) = match ctx.trans.match_user_corp_by_name(from_corp, &user.username).await? { None => user_error("You don't seem to belong to a matching corp!".to_owned())?, Some(c) => c }; let (to_corp_id, to_corp) = match ctx.trans.find_corp_by_name(to_corp).await? { None => user_error("I didn't find the corp you want to fight against.".to_owned())?, Some(c) => c }; if !check_corp_perm(&CorpPermission::War, &mem) { user_error("You don't have permission to declare war on behalf of that corp.".to_owned())?; } if is_allow { let mut not_allowing_users = vec!(); for (name, mem) in ctx.trans.list_corp_members(&from_corp_id).await? { if name != source_player.item_code && !mem.allow_combat { not_allowing_users.push(name); } } if !not_allowing_users.is_empty() { user_error(format!(ansi!("War can only be declared when all corp mates allow combat for that corp. Ask them to run corp allow combat from {}, or have them fired! corp config {} allow combat required will stop anyone joining without it / turning it off. Corp mates with it off: {}"), &from_corp.name, &from_corp.name, ¬_allowing_users.iter().join(", ") ))?; } } ctx.trans.delete_expired_corp_consent().await?; let current_consent = ctx.trans.find_corp_consent_by_parties_type( &from_corp_id, &to_corp_id, &cmd.consent_type ).await?; let converse_consent = if cmd.consent_type == ConsentType::Fight { ctx.trans.find_corp_consent_by_parties_type( &to_corp_id, &from_corp_id, &cmd.consent_type ).await? } else { None }; let update = compute_new_consent_state( &from_corp.name, "their", &to_corp.name, "their", &("against ".to_owned() + &to_corp.name + " by " + &from_corp.name), &cmd.consent_type, &cmd.consent_details, ¤t_consent, &converse_consent, is_allow ); match update.new_consent.as_ref() { None => ctx.trans.delete_corp_consent( &from_corp_id, &to_corp_id, &cmd.consent_type ).await?, Some(consent) => ctx.trans.upsert_corp_consent( &from_corp_id, &to_corp_id, &cmd.consent_type, consent ).await?, } if update.mirror_to_counterparty { match update.new_consent.as_ref() { None => ctx.trans.delete_corp_consent( &to_corp_id, &from_corp_id, &cmd.consent_type ).await?, Some(consent) => ctx.trans.upsert_corp_consent( &to_corp_id, &from_corp_id, &cmd.consent_type, consent ).await?, } } match update.first_party_message { None => {}, Some(msg) => if update.counterparty_message.is_some() { let details_str = cmd.consent_details.as_string(&ConsentType::Fight); let details_str = if details_str != "" { format!(" ({})", &details_str.trim()) } else { "".to_owned() }; let action_str = if is_allow { "consented" } else { "withdrew consent" }; // If it goes to both corps, it is a diplomatic message so goes to whole source corp. ctx.trans.broadcast_to_corp( &from_corp_id, &CorpCommType::Consent, None, &format!(ansi!("{} {} to fight{} against {} on behalf \ of {}, with result: {}\n"), &source_player.display_for_sentence(false, 1, true), action_str, &details_str, &to_corp.name, &from_corp.name, &msg ) ).await?; } else { ctx.trans.queue_for_session(&ctx.session, Some(&(msg + "\n"))).await?; } } match update.counterparty_message { None => {}, Some(msg) => { ctx.trans.broadcast_to_corp( &to_corp_id, &CorpCommType::Consent, None, &format!(ansi!("Your wristpad buzzes with an corporate announcement to {}: {}\n"), &to_corp.name, &msg) ).await? } } Ok(()) } async fn handle_list_allows(ctx: &mut VerbContext<'_>, item: &Item) -> UResult<()> { let consents = ctx.trans.list_consents(&item.item_code).await?; let mut msg = String::new(); if consents.is_empty() { msg.push_str(ansi!("You have no consents. Type help allow to learn how to consent.")); } else { msg.push_str(ansi!("You are consenting to the following actions (type help allow to learn how to change):\n")); msg.push_str(&format!(ansi!("| {:20} | {:12} | {:40} |\n"), "User", "Consent type", "Details")); for (user, consent_type, details) in consents { msg.push_str(&format!("| {:20} | {:12} | {:40} |\n", user, consent_type.to_str(), details.to_str())); } } ctx.trans.queue_for_session(&ctx.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 == "" { handle_list_allows(ctx, &player_item).await?; } 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 { from_corp, to_corp } => handle_corp_consent(ctx, &player_item, from_corp, to_corp, is_allow, &cmd).await?, 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;