blastmud/blastmud_game/src/message_handler/user_commands/follow.rs

325 lines
12 KiB
Rust

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());
}
}