forked from blasthavers/blastmud
325 lines
12 KiB
Rust
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());
|
|
}
|
|
}
|