use super::{ get_player_item_or_fail, search_item_for_user, user_error, CommandHandlingError, DResult, UResult, UserError, UserVerb, UserVerbRef, VerbContext, }; #[double] use crate::db::DBTrans; use crate::{ db::ItemSearchParams, models::item::{FollowData, FollowState, Item}, regular_tasks::queued_command::{queue_command_for_npc, MovementSource, QueueCommand}, static_content::room::Direction, }; use async_trait::async_trait; use mockall_double::double; use std::mem; pub async fn propagate_move_to_followers( trans: &DBTrans, mover: &Item, direction: &Direction, source: &MovementSource, ) -> DResult<()> { let (event_id, chain) = match source { MovementSource::Command { event_id } => (event_id, vec![]), MovementSource::Follow { chain, origin_uuid } => (origin_uuid, chain.clone()), MovementSource::Internal { .. } => return Ok(()), // Not to be propagated. }; let mut new_chain = chain.clone(); new_chain.push(mover.refstr()); let new_movement = QueueCommand::Movement { direction: direction.clone(), source: MovementSource::Follow { origin_uuid: event_id.clone(), chain: new_chain, }, }; for follower in trans.find_followers_by_leader(&mover.refstr()).await? { let mut follower_mut = (*follower).clone(); if !chain.contains(&follower.refstr()) { if let Some(&FollowData { state: FollowState::IfSameRoom, ref follow_whom, }) = follower.following.as_ref() { if follower.location != mover.location { continue; } follower_mut.following = Some(FollowData { state: FollowState::Active, follow_whom: follow_whom.clone(), }); } // We use the NPC variant since adding [queued] will be confusing. match queue_command_for_npc(&trans, &mut follower_mut, &new_movement).await { Err(e) if e.to_string().starts_with("Can't queue more") => { suspend_follow_for_independent_move(&mut follower_mut); } Err(e) => return Err(e), _ => {} } trans.save_item_model(&follower_mut).await?; } } Ok(()) } pub async fn update_follow_for_failed_movement( trans: &DBTrans, player: &mut Item, source: &MovementSource, ) -> DResult<()> { // Failing is the same as an independent movement back. suspend_follow_for_independent_move(player); let event_id = source.event_id(); for follower in trans.find_followers_by_leader(&player.refstr()).await? { if let Some(QueueCommand::Movement { source: MovementSource::Follow { origin_uuid, chain }, .. }) = follower.queue.front().as_ref() { if origin_uuid == event_id && chain.last() == Some(&player.refstr()) { // The follower has already started moving (and their followers may indeed have // followed them in turn). So we treat the move the follower made // but the leader failed as an independent move. let mut follower_mut = (*follower).clone(); suspend_follow_for_independent_move(&mut follower_mut); trans.save_item_model(&follower_mut).await?; continue; } } // Otherwise, it's not too late, so just cancel it from the queue. let mut follower_mut = (*follower).clone(); follower_mut.queue.retain(|qit| match qit { QueueCommand::Movement { source: MovementSource::Follow { origin_uuid, chain }, .. } => !(origin_uuid == event_id && chain.last() == Some(&player.refstr())), _ => true, }); if follower_mut.queue != follower.queue { trans.save_item_model(&follower_mut).await?; } } Ok(()) } pub fn suspend_follow_for_independent_move(player: &mut Item) { if let Some(following) = player.following.as_mut() { following.state = FollowState::IfSameRoom; } } // Caller has to save follower. pub async fn stop_following(trans: &DBTrans, follower: &mut Item) -> UResult<()> { let old_following = mem::replace(&mut follower.following, None) .ok_or_else(|| UserError("You're not following anyone.".to_owned()))?; if let Some((follow_type, follow_code)) = old_following.follow_whom.split_once("/") { if let Some(old_item) = trans .find_item_by_type_code(&follow_type, &follow_code) .await? { if follower.item_type == "player" { if let Some((session, _session_dat)) = trans.find_session_for_player(&follower.item_code).await? { trans .queue_for_session( &session, Some(&format!( "You are no longer following {}.\n", old_item.display_for_sentence(1, false) )), ) .await?; } } } } Ok(()) } pub async fn cancel_follow_by_leader(trans: &DBTrans, leader: &str) -> DResult<()> { for follower in trans.find_followers_by_leader(leader).await? { if let Some(player_item) = trans .find_item_by_type_code(&follower.item_type, &follower.item_code) .await? { let mut player_item_mut = (*player_item).clone(); match stop_following(trans, &mut player_item_mut).await { Ok(_) => {} Err(CommandHandlingError::UserError(_)) => {} Err(CommandHandlingError::SystemError(e)) => Err(e)?, } trans.save_item_model(&player_item_mut).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 mut player_item = (*(get_player_item_or_fail(ctx).await?)).clone(); if verb == "follow" { let follow_whom = search_item_for_user( ctx, &ItemSearchParams { include_loc_contents: true, ..ItemSearchParams::base(&player_item, remaining.trim()) }, ) .await?; if follow_whom.item_type != "player" && follow_whom.item_type != "npc" { user_error("Only characters (player / NPC) can be followed.".to_owned())?; } if follow_whom.item_type == "player" && follow_whom.item_code == player_item.item_code { user_error("No point chasing your own tail!".to_owned())?; } if let Some(follow) = player_item.following.as_ref() { if follow.follow_whom == follow_whom.refstr() { user_error(format!( "You're already following {}!", follow_whom.pronouns.possessive ))?; } } if ctx .trans .count_followers_by_leader(&follow_whom.refstr()) .await? >= 20 { user_error(format!( "There's such a huge crowd following {}, there's not really room for more.", &follow_whom.pronouns.object ))?; } if player_item.active_climb.is_some() { user_error( "You can't focus on following someone while you are climbing!".to_owned(), )?; } player_item.queue.retain(|qit| match qit { QueueCommand::Movement { .. } => false, _ => true, }); if let Some(QueueCommand::Movement { direction, source }) = follow_whom.queue.front().as_ref() { let event_id = source.event_id(); // Safer to make a singleton chain, since follow is manual // intervention. Even if the current movement came from player, // still want to duplicate it, and don't want to duplicate // player in chain. let new_chain = vec![follow_whom.refstr()]; let new_movement = QueueCommand::Movement { direction: direction.clone(), source: MovementSource::Follow { origin_uuid: event_id.clone(), chain: new_chain, }, }; // We use the NPC variant since adding [queued] will be confusing. queue_command_for_npc(&ctx.trans, &mut player_item, &new_movement).await?; } player_item.following = Some(FollowData { follow_whom: follow_whom.refstr(), state: FollowState::Active, }); ctx.trans .queue_for_session( &ctx.session, Some(&format!( "You are now following {}.\n", &follow_whom.display_for_sentence(1, false) )), ) .await?; ctx.trans.save_item_model(&player_item).await?; } else { stop_following(&ctx.trans, &mut player_item).await?; ctx.trans.save_item_model(&player_item).await?; } Ok(()) } } static VERB_INT: Verb = Verb; pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; #[cfg(test)] mod test { use uuid::Uuid; use super::*; use crate::db::MockDBTrans; use std::sync::Arc; #[test] fn propagating_to_overflowing_queue_works() { let mut trans = MockDBTrans::default(); let leader: Item = Item { item_type: "test".to_owned(), item_code: "test1".to_owned(), ..Default::default() }; let source = MovementSource::Command { event_id: Uuid::parse_str("ed25224e-9925-4635-8e4f-e612376ef6e9").unwrap(), }; let follower: Item = Item { item_type: "test".to_owned(), item_code: "test2".to_owned(), queue: std::iter::repeat(QueueCommand::Movement { direction: Direction::NORTH, source: source.clone(), }) .take(20) .collect(), following: Some(FollowData { follow_whom: "test/test1".to_owned(), state: FollowState::Active, }), ..Default::default() }; trans .expect_find_followers_by_leader() .times(1) .withf(|m| m == "test/test1") .returning(move |_| Ok(vec![Arc::new(follower.clone())])); trans .expect_save_item_model() .times(1) .withf(|i| { &i.item_code == "test2" && i.queue.len() == 20 && i.following == Some(FollowData { follow_whom: "test/test1".to_owned(), state: FollowState::IfSameRoom, }) }) .returning(|_| Ok(())); assert!(tokio_test::block_on(propagate_move_to_followers( &trans, &leader, &Direction::NORTH, &source )) .is_ok()); } }