Support following other players.
This commit is contained in:
parent
261151881d
commit
4b524fda96
36
Cargo.lock
generated
36
Cargo.lock
generated
@ -76,6 +76,28 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e"
|
||||
dependencies = [
|
||||
"async-stream-impl",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream-impl"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.60"
|
||||
@ -170,6 +192,7 @@ dependencies = [
|
||||
"tokio-postgres",
|
||||
"tokio-serde",
|
||||
"tokio-stream",
|
||||
"tokio-test",
|
||||
"tokio-util",
|
||||
"uuid",
|
||||
"validator",
|
||||
@ -2045,6 +2068,19 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-test"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.17.2"
|
||||
|
@ -64,5 +64,5 @@ Create a user with a secret password, and username `blast`. Create a production
|
||||
|
||||
To get to the latest schema:
|
||||
* Run `psql -d template1 <schema/schema.sql` to create the temporary `blast_schemaonly` database.
|
||||
* Run `migra "postgres:///blast" "postgres:///blast_schemaonly" > /tmp/update.sql`
|
||||
* Run `migra "postgresql:///blast" "postgresql:///blast_schemaonly" > /tmp/update.sql`
|
||||
* Check `/tmp/update.sql` and if it looks good, apply it with `psql -U blast -d blast </tmp/update.sql`
|
||||
|
@ -41,3 +41,6 @@ humantime = "2.1.0"
|
||||
rust_decimal = "1.28.0"
|
||||
mockall = "0.11.3"
|
||||
mockall_double = "0.3.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4.2"
|
||||
|
@ -1569,6 +1569,34 @@ impl DBTrans {
|
||||
).await?.get(0))
|
||||
}
|
||||
|
||||
pub async fn count_followers_by_leader<'a>(self: &'a Self, leader: &str) -> DResult<i64> {
|
||||
Ok(self
|
||||
.pg_trans()?
|
||||
.query_one(
|
||||
"SELECT COUNT(*) FROM items WHERE details->'following'->>'follow_whom' = $1",
|
||||
&[&leader],
|
||||
)
|
||||
.await?
|
||||
.get(0))
|
||||
}
|
||||
|
||||
pub async fn find_followers_by_leader<'a>(
|
||||
self: &'a Self,
|
||||
leader: &str,
|
||||
) -> DResult<Vec<Arc<Item>>> {
|
||||
Ok(self
|
||||
.pg_trans()?
|
||||
.query(
|
||||
"SELECT details FROM items WHERE details->'following'->>'follow_whom' = $1",
|
||||
&[&leader],
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
||||
.map(Arc::new)
|
||||
.collect())
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -26,7 +26,7 @@ pub mod cut;
|
||||
pub mod delete;
|
||||
mod describe;
|
||||
pub mod drop;
|
||||
mod follow;
|
||||
pub mod follow;
|
||||
mod gear;
|
||||
pub mod get;
|
||||
mod help;
|
||||
|
@ -1,8 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::{
|
||||
get_player_item_or_fail, get_user_or_fail, look, rent::recursively_destroy_or_move_item,
|
||||
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
||||
follow::cancel_follow_by_leader, get_player_item_or_fail, get_user_or_fail, look,
|
||||
rent::recursively_destroy_or_move_item, user_error, UResult, UserError, UserVerb, UserVerbRef,
|
||||
VerbContext,
|
||||
};
|
||||
use crate::{
|
||||
models::task::{Task, TaskDetails, TaskMeta},
|
||||
@ -123,6 +124,7 @@ impl TaskHandler for DestroyUserHandler {
|
||||
None => return Ok(None),
|
||||
Some(p) => p,
|
||||
};
|
||||
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
|
||||
destroy_container(&ctx.trans, &player_item).await?;
|
||||
for dynzone in ctx.trans.find_dynzone_for_user(&username).await? {
|
||||
recursively_destroy_or_move_item(ctx, &dynzone).await?;
|
||||
|
@ -1,9 +1,164 @@
|
||||
use super::{
|
||||
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
|
||||
VerbContext,
|
||||
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 crate::db::ItemSearchParams;
|
||||
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(mut 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_session(&session_dat)
|
||||
)),
|
||||
)
|
||||
.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]
|
||||
@ -14,9 +169,9 @@ impl UserVerb for Verb {
|
||||
verb: &str,
|
||||
remaining: &str,
|
||||
) -> UResult<()> {
|
||||
let player_item = (*(get_player_item_or_fail(ctx).await?)).clone();
|
||||
let mut player_item = (*(get_player_item_or_fail(ctx).await?)).clone();
|
||||
if verb == "follow" {
|
||||
let follow_who = search_item_for_user(
|
||||
let follow_whom = search_item_for_user(
|
||||
ctx,
|
||||
&ItemSearchParams {
|
||||
include_loc_contents: true,
|
||||
@ -24,23 +179,146 @@ impl UserVerb for Verb {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if follow_who.item_type != "player" && follow_who.item_type != "npc" {
|
||||
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 == player_item.refstr() {
|
||||
if follow.follow_whom == follow_whom.refstr() {
|
||||
user_error(format!(
|
||||
"You're already following {}!",
|
||||
follow_who.pronouns.possessive
|
||||
follow_whom.pronouns.possessive
|
||||
))?;
|
||||
}
|
||||
// sess_dets.queue.filter()
|
||||
}
|
||||
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_session(&ctx.session_dat)
|
||||
)),
|
||||
)
|
||||
.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?;
|
||||
}
|
||||
user_error("Sorry, we're still building the follow command!".to_owned())?;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
use super::{
|
||||
follow::{
|
||||
propagate_move_to_followers, suspend_follow_for_independent_move,
|
||||
update_follow_for_failed_movement,
|
||||
},
|
||||
get_player_item_or_fail, look,
|
||||
open::{attempt_open_immediate, is_door_in_direction, DoorSituation},
|
||||
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
||||
@ -12,8 +16,7 @@ use crate::{
|
||||
item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType},
|
||||
},
|
||||
regular_tasks::queued_command::{
|
||||
queue_command_and_save, MovementSource, QueueCommand, QueueCommandHandler,
|
||||
QueuedCommandContext,
|
||||
queue_command, MovementSource, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||
},
|
||||
services::{
|
||||
check_consent,
|
||||
@ -33,6 +36,7 @@ use mockall_double::double;
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn announce_move(
|
||||
trans: &DBTrans,
|
||||
@ -298,11 +302,13 @@ pub async fn handle_fall(trans: &DBTrans, faller: &mut Item, fall_dist: u64) ->
|
||||
Ok(descriptor.to_owned())
|
||||
}
|
||||
|
||||
// Returns true if the move is either complete or still in progress.
|
||||
// Returns false if the move failed.
|
||||
async fn attempt_move_immediate(
|
||||
direction: &Direction,
|
||||
mut ctx: &mut QueuedCommandContext<'_>,
|
||||
source: &MovementSource,
|
||||
) -> UResult<()> {
|
||||
) -> UResult<bool> {
|
||||
let use_location = if ctx.item.death_data.is_some() {
|
||||
if ctx.item.item_type != "player" {
|
||||
user_error("Dead players don't move".to_owned())?;
|
||||
@ -332,9 +338,11 @@ async fn attempt_move_immediate(
|
||||
// Players take an extra step. So tell them to come back.
|
||||
ctx.item.queue.push_front(QueueCommand::Movement {
|
||||
direction: direction.clone(),
|
||||
source: source.clone(),
|
||||
source: MovementSource::Internal {
|
||||
event_id: source.event_id().clone(),
|
||||
},
|
||||
});
|
||||
return Ok(());
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -356,19 +364,21 @@ async fn attempt_move_immediate(
|
||||
let mut narrative = String::new();
|
||||
if skills <= -0.25 {
|
||||
// Crit fail - they have fallen.
|
||||
let (fall_dist, from_room, to_room) = if climb.height < 0 {
|
||||
let (fall_dist, from_room, to_room, got_there) = if climb.height < 0 {
|
||||
// At least they get to where they want to go!
|
||||
ctx.item.location = new_loc.clone();
|
||||
(
|
||||
climb.height.abs() as u64 - active_climb.height,
|
||||
new_loc.to_owned(),
|
||||
use_location.clone(),
|
||||
true,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
active_climb.height,
|
||||
use_location.clone(),
|
||||
new_loc.to_owned(),
|
||||
false,
|
||||
)
|
||||
};
|
||||
ctx.item.active_climb = None;
|
||||
@ -390,7 +400,7 @@ async fn attempt_move_immediate(
|
||||
broadcast_to_room(ctx.trans, &from_room, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||
broadcast_to_room(ctx.trans, &to_room, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||
ctx.item.queue.truncate(0);
|
||||
return Ok(());
|
||||
return Ok(got_there);
|
||||
} else if skills <= 0.0 {
|
||||
if climb.height >= 0 {
|
||||
narrative.push_str("You lose your grip and slide a metre back down");
|
||||
@ -454,9 +464,11 @@ async fn attempt_move_immediate(
|
||||
}
|
||||
ctx.item.queue.push_front(QueueCommand::Movement {
|
||||
direction: direction.clone(),
|
||||
source: source.clone(),
|
||||
source: MovementSource::Internal {
|
||||
event_id: source.event_id().clone(),
|
||||
},
|
||||
});
|
||||
return Ok(());
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -478,7 +490,9 @@ async fn attempt_move_immediate(
|
||||
|
||||
ctx.item.queue.push_front(QueueCommand::Movement {
|
||||
direction: direction.clone(),
|
||||
source: source.clone(),
|
||||
source: MovementSource::Internal {
|
||||
event_id: source.event_id().clone(),
|
||||
},
|
||||
});
|
||||
escape_check_only = true;
|
||||
}
|
||||
@ -567,13 +581,14 @@ async fn attempt_move_immediate(
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
return Ok(());
|
||||
ctx.item.queue.truncate(0);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if escape_check_only {
|
||||
return Ok(());
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if ctx.item.death_data.is_some() {
|
||||
@ -625,13 +640,28 @@ async fn attempt_move_immediate(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub struct QueueHandler;
|
||||
#[async_trait]
|
||||
impl QueueCommandHandler for QueueHandler {
|
||||
async fn start_command(&self, _ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
|
||||
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
|
||||
let (direction, source) = match ctx.command {
|
||||
QueueCommand::Movement { direction, source } => (direction, source),
|
||||
_ => user_error("Unexpected command".to_owned())?,
|
||||
};
|
||||
let use_location = if ctx.item.death_data.is_some() {
|
||||
if ctx.item.item_type != "player" {
|
||||
user_error("Dead players don't move".to_owned())?;
|
||||
}
|
||||
"room/repro_xv_respawn".to_owned()
|
||||
} else {
|
||||
ctx.item.location.clone()
|
||||
};
|
||||
// Solely to eliminate completely invalid moves before propagating.
|
||||
move_to_where(&use_location, direction, ctx).await?;
|
||||
propagate_move_to_followers(&ctx.trans, &mut ctx.item, &direction, &source).await?;
|
||||
Ok(time::Duration::from_secs(1))
|
||||
}
|
||||
|
||||
@ -641,7 +671,17 @@ impl QueueCommandHandler for QueueHandler {
|
||||
QueueCommand::Movement { direction, source } => (direction, source),
|
||||
_ => user_error("Unexpected command".to_owned())?,
|
||||
};
|
||||
attempt_move_immediate(direction, ctx, source).await?;
|
||||
match attempt_move_immediate(direction, ctx, source).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
update_follow_for_failed_movement(&ctx.trans, &mut ctx.item, source).await?;
|
||||
}
|
||||
Err(UserError(err)) => {
|
||||
update_follow_for_failed_movement(&ctx.trans, &mut ctx.item, source).await?;
|
||||
Err(UserError(err))?
|
||||
}
|
||||
Err(e) => Err(e)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -658,16 +698,21 @@ impl UserVerb for Verb {
|
||||
) -> UResult<()> {
|
||||
let dir = Direction::parse(&(verb.to_owned() + " " + remaining.trim()).trim())
|
||||
.ok_or_else(|| UserError("Unknown direction".to_owned()))?;
|
||||
let player_item = get_player_item_or_fail(ctx).await?;
|
||||
queue_command_and_save(
|
||||
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
|
||||
suspend_follow_for_independent_move(&mut player_item);
|
||||
queue_command(
|
||||
ctx,
|
||||
&player_item,
|
||||
&mut player_item,
|
||||
&QueueCommand::Movement {
|
||||
direction: dir.clone(),
|
||||
source: MovementSource::Command,
|
||||
source: MovementSource::Command {
|
||||
event_id: Uuid::new_v4(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
ctx.trans.save_item_model(&player_item).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
static VERB_INT: Verb = Verb;
|
||||
|
@ -20,11 +20,32 @@ use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::time;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)]
|
||||
pub enum MovementSource {
|
||||
Command,
|
||||
Follow,
|
||||
Command {
|
||||
event_id: Uuid,
|
||||
},
|
||||
Follow {
|
||||
origin_uuid: Uuid,
|
||||
chain: Vec<String>,
|
||||
},
|
||||
Internal {
|
||||
event_id: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
impl MovementSource {
|
||||
pub fn event_id(&self) -> &Uuid {
|
||||
match self {
|
||||
MovementSource::Command { ref event_id, .. } => event_id,
|
||||
MovementSource::Follow {
|
||||
ref origin_uuid, ..
|
||||
} => origin_uuid,
|
||||
MovementSource::Internal { ref event_id } => event_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
|
@ -1,7 +1,9 @@
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
message_handler::user_commands::{user_error, CommandHandlingError, UResult},
|
||||
message_handler::user_commands::{
|
||||
follow::cancel_follow_by_leader, user_error, CommandHandlingError, UResult,
|
||||
},
|
||||
models::{
|
||||
item::{DeathData, Item, LocationActionType, SkillType, Subattack},
|
||||
journal::JournalType,
|
||||
@ -445,6 +447,8 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
|
||||
.unwrap_or_else(|| vec![]),
|
||||
..Default::default()
|
||||
});
|
||||
whom.following = None;
|
||||
cancel_follow_by_leader(trans, &whom.refstr()).await?;
|
||||
let vic_is_npc = whom.item_type == "npc";
|
||||
if let Some(ac) = &whom.active_combat {
|
||||
let at_str = ac.attacking.clone();
|
||||
|
@ -27,6 +27,7 @@ use once_cell::sync::OnceCell;
|
||||
use rand::{prelude::*, thread_rng, Rng};
|
||||
use std::collections::BTreeMap;
|
||||
use std::time;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod melbs_citizen;
|
||||
mod melbs_dog;
|
||||
@ -406,7 +407,9 @@ impl TaskHandler for NPCWanderTaskHandler {
|
||||
&item,
|
||||
&QueueCommand::Movement {
|
||||
direction: dir.clone(),
|
||||
source: MovementSource::Command,
|
||||
source: MovementSource::Command {
|
||||
event_id: Uuid::new_v4(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
@ -65,7 +65,7 @@ pub fn room_list() -> Vec<Room> {
|
||||
code: "chonkers_climbing_top",
|
||||
name: "Top of the Climbing Wall",
|
||||
short: ansi!("<bgblack><white>CW<reset>"),
|
||||
description: ansi!("Congratulations, you made it to the top! It is quite snug up here in a little alove at the top of the wall, but the view from the top of the wall is amazing; you have a 180 degree view of buff men and women pumping iron and keeping themselves fit"),
|
||||
description: ansi!("Congratulations, you made it to the top! It is quite snug up here in a little alcove at the top of the wall, but the view from the top of the wall is amazing; you have a 180 degree view of buff men and women pumping iron and keeping themselves fit"),
|
||||
description_less_explicit: None,
|
||||
grid_coords: GridCoords { x: 0, y: -1, z: 1 },
|
||||
exits: vec!(
|
||||
|
19
docs/dbfixes.md
Normal file
19
docs/dbfixes.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Diagnostics to run on the database
|
||||
## Dead NPCs with no reclone task (permadead without intervention)
|
||||
|
||||
The following should be 0. I've seen it non-zero in test, possibly from old data though.
|
||||
|
||||
select count(*) from items i where details->>'item_type' = 'npc' and details->>'death_data' is not null and not exists (select 1 from tasks t where t.details->>'task_type' = 'RecloneNPC' and t.details->>'task_code' = i.details->>'item_code');
|
||||
|
||||
Fix with something like:
|
||||
|
||||
INSERT INTO tasks (details)
|
||||
SELECT JSONB_BUILD_OBJECT('is_static', false, 'task_type', 'RecloneNPC', 'task_code', details->>'item_code', 'task_details', JSONB_BUILD_OBJECT('npc_code', details->>'item_code'), 'next_scheduled', now(), 'consecutive_failure_count', 0) AS details FROM items i WHERE i.details->>'item_type' = 'npc' AND i.details->>'death_data' IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.details->>'task_type' = 'RecloneNPC' AND t.details->>'task_code' = i.details->>'item_code');
|
||||
|
||||
## Corpses with no rot handler
|
||||
|
||||
select count(*) from items i where details->>'item_type' = 'corpse' and not exists (select 1 from tasks t where t.details->>'task_type' = 'RotCorpse' and t.details->>'task_code' = i.details->>'item_code');
|
||||
|
||||
This is expected to be 0 - haven't seen any instances of it deviating, so any bugs seen involving corpses probably aren't due to stale data.
|
||||
|
@ -29,6 +29,7 @@ CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
|
||||
CREATE INDEX item_by_display ON items (lower(details->>'display'));
|
||||
CREATE INDEX item_by_owner ON items (lower(details->>'owner'));
|
||||
CREATE INDEX item_by_display_less_explicit ON items (lower(details->>'display_less_explicit'));
|
||||
CREATE INDEX item_by_following ON items ((details->'following'->>'follow_whom'));
|
||||
CREATE UNIQUE INDEX item_dynamic_entrance ON items (
|
||||
(details->'dynamic_entrance'->>'source_item'),
|
||||
(LOWER(details->'dynamic_entrance'->>'direction')));
|
||||
|
Loading…
Reference in New Issue
Block a user