Support following other players.

This commit is contained in:
Condorra 2023-06-30 23:46:38 +10:00
parent 261151881d
commit 4b524fda96
14 changed files with 479 additions and 39 deletions

36
Cargo.lock generated
View File

@ -76,6 +76,28 @@ dependencies = [
"syn", "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]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.60" version = "0.1.60"
@ -170,6 +192,7 @@ dependencies = [
"tokio-postgres", "tokio-postgres",
"tokio-serde", "tokio-serde",
"tokio-stream", "tokio-stream",
"tokio-test",
"tokio-util", "tokio-util",
"uuid", "uuid",
"validator", "validator",
@ -2045,6 +2068,19 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tokio-tungstenite" name = "tokio-tungstenite"
version = "0.17.2" version = "0.17.2"

View File

@ -64,5 +64,5 @@ Create a user with a secret password, and username `blast`. Create a production
To get to the latest schema: To get to the latest schema:
* Run `psql -d template1 <schema/schema.sql` to create the temporary `blast_schemaonly` database. * 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` * Check `/tmp/update.sql` and if it looks good, apply it with `psql -U blast -d blast </tmp/update.sql`

View File

@ -41,3 +41,6 @@ humantime = "2.1.0"
rust_decimal = "1.28.0" rust_decimal = "1.28.0"
mockall = "0.11.3" mockall = "0.11.3"
mockall_double = "0.3.0" mockall_double = "0.3.0"
[dev-dependencies]
tokio-test = "0.4.2"

View File

@ -1569,6 +1569,34 @@ impl DBTrans {
).await?.get(0)) ).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<()> { pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None)); let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));
if let Some(trans) = trans_opt { if let Some(trans) = trans_opt {

View File

@ -26,7 +26,7 @@ pub mod cut;
pub mod delete; pub mod delete;
mod describe; mod describe;
pub mod drop; pub mod drop;
mod follow; pub mod follow;
mod gear; mod gear;
pub mod get; pub mod get;
mod help; mod help;

View File

@ -1,8 +1,9 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use super::{ use super::{
get_player_item_or_fail, get_user_or_fail, look, rent::recursively_destroy_or_move_item, follow::cancel_follow_by_leader, get_player_item_or_fail, get_user_or_fail, look,
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext, rent::recursively_destroy_or_move_item, user_error, UResult, UserError, UserVerb, UserVerbRef,
VerbContext,
}; };
use crate::{ use crate::{
models::task::{Task, TaskDetails, TaskMeta}, models::task::{Task, TaskDetails, TaskMeta},
@ -123,6 +124,7 @@ impl TaskHandler for DestroyUserHandler {
None => return Ok(None), None => return Ok(None),
Some(p) => p, Some(p) => p,
}; };
cancel_follow_by_leader(&ctx.trans, &player_item.refstr()).await?;
destroy_container(&ctx.trans, &player_item).await?; destroy_container(&ctx.trans, &player_item).await?;
for dynzone in ctx.trans.find_dynzone_for_user(&username).await? { for dynzone in ctx.trans.find_dynzone_for_user(&username).await? {
recursively_destroy_or_move_item(ctx, &dynzone).await?; recursively_destroy_or_move_item(ctx, &dynzone).await?;

View File

@ -1,9 +1,164 @@
use super::{ use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef, get_player_item_or_fail, search_item_for_user, user_error, CommandHandlingError, DResult,
VerbContext, 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 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; pub struct Verb;
#[async_trait] #[async_trait]
@ -14,9 +169,9 @@ impl UserVerb for Verb {
verb: &str, verb: &str,
remaining: &str, remaining: &str,
) -> UResult<()> { ) -> 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" { if verb == "follow" {
let follow_who = search_item_for_user( let follow_whom = search_item_for_user(
ctx, ctx,
&ItemSearchParams { &ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
@ -24,23 +179,146 @@ impl UserVerb for Verb {
}, },
) )
.await?; .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())?; 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 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!( user_error(format!(
"You're already following {}!", "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 { } 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(()) Ok(())
} }
} }
static VERB_INT: Verb = Verb; static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; 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());
}
}

View File

@ -1,4 +1,8 @@
use super::{ use super::{
follow::{
propagate_move_to_followers, suspend_follow_for_independent_move,
update_follow_for_failed_movement,
},
get_player_item_or_fail, look, get_player_item_or_fail, look,
open::{attempt_open_immediate, is_door_in_direction, DoorSituation}, open::{attempt_open_immediate, is_door_in_direction, DoorSituation},
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
@ -12,8 +16,7 @@ use crate::{
item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType}, item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType},
}, },
regular_tasks::queued_command::{ regular_tasks::queued_command::{
queue_command_and_save, MovementSource, QueueCommand, QueueCommandHandler, queue_command, MovementSource, QueueCommand, QueueCommandHandler, QueuedCommandContext,
QueuedCommandContext,
}, },
services::{ services::{
check_consent, check_consent,
@ -33,6 +36,7 @@ use mockall_double::double;
use rand_distr::{Distribution, Normal}; use rand_distr::{Distribution, Normal};
use std::sync::Arc; use std::sync::Arc;
use std::time; use std::time;
use uuid::Uuid;
pub async fn announce_move( pub async fn announce_move(
trans: &DBTrans, trans: &DBTrans,
@ -298,11 +302,13 @@ pub async fn handle_fall(trans: &DBTrans, faller: &mut Item, fall_dist: u64) ->
Ok(descriptor.to_owned()) 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( async fn attempt_move_immediate(
direction: &Direction, direction: &Direction,
mut ctx: &mut QueuedCommandContext<'_>, mut ctx: &mut QueuedCommandContext<'_>,
source: &MovementSource, source: &MovementSource,
) -> UResult<()> { ) -> UResult<bool> {
let use_location = if ctx.item.death_data.is_some() { let use_location = if ctx.item.death_data.is_some() {
if ctx.item.item_type != "player" { if ctx.item.item_type != "player" {
user_error("Dead players don't move".to_owned())?; 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. // Players take an extra step. So tell them to come back.
ctx.item.queue.push_front(QueueCommand::Movement { ctx.item.queue.push_front(QueueCommand::Movement {
direction: direction.clone(), 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(); let mut narrative = String::new();
if skills <= -0.25 { if skills <= -0.25 {
// Crit fail - they have fallen. // 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! // At least they get to where they want to go!
ctx.item.location = new_loc.clone(); ctx.item.location = new_loc.clone();
( (
climb.height.abs() as u64 - active_climb.height, climb.height.abs() as u64 - active_climb.height,
new_loc.to_owned(), new_loc.to_owned(),
use_location.clone(), use_location.clone(),
true,
) )
} else { } else {
( (
active_climb.height, active_climb.height,
use_location.clone(), use_location.clone(),
new_loc.to_owned(), new_loc.to_owned(),
false,
) )
}; };
ctx.item.active_climb = None; 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, &from_room, None, &msg_exp, Some(&msg_nonexp)).await?;
broadcast_to_room(ctx.trans, &to_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); ctx.item.queue.truncate(0);
return Ok(()); return Ok(got_there);
} else if skills <= 0.0 { } else if skills <= 0.0 {
if climb.height >= 0 { if climb.height >= 0 {
narrative.push_str("You lose your grip and slide a metre back down"); 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 { ctx.item.queue.push_front(QueueCommand::Movement {
direction: direction.clone(), direction: direction.clone(),
source: source.clone(), source: MovementSource::Internal {
event_id: source.event_id().clone(),
},
}); });
return Ok(()); return Ok(true);
} }
} }
} else { } else {
@ -478,7 +490,9 @@ async fn attempt_move_immediate(
ctx.item.queue.push_front(QueueCommand::Movement { ctx.item.queue.push_front(QueueCommand::Movement {
direction: direction.clone(), direction: direction.clone(),
source: source.clone(), source: MovementSource::Internal {
event_id: source.event_id().clone(),
},
}); });
escape_check_only = true; escape_check_only = true;
} }
@ -567,13 +581,14 @@ async fn attempt_move_immediate(
) )
.await?; .await?;
} }
return Ok(()); ctx.item.queue.truncate(0);
return Ok(false);
} }
} }
} }
} }
if escape_check_only { if escape_check_only {
return Ok(()); return Ok(true);
} }
if ctx.item.death_data.is_some() { if ctx.item.death_data.is_some() {
@ -625,13 +640,28 @@ async fn attempt_move_immediate(
} }
} }
Ok(()) Ok(true)
} }
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { 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)) Ok(time::Duration::from_secs(1))
} }
@ -641,7 +671,17 @@ impl QueueCommandHandler for QueueHandler {
QueueCommand::Movement { direction, source } => (direction, source), QueueCommand::Movement { direction, source } => (direction, source),
_ => user_error("Unexpected command".to_owned())?, _ => 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(()) Ok(())
} }
} }
@ -658,16 +698,21 @@ impl UserVerb for Verb {
) -> UResult<()> { ) -> UResult<()> {
let dir = Direction::parse(&(verb.to_owned() + " " + remaining.trim()).trim()) let dir = Direction::parse(&(verb.to_owned() + " " + remaining.trim()).trim())
.ok_or_else(|| UserError("Unknown direction".to_owned()))?; .ok_or_else(|| UserError("Unknown direction".to_owned()))?;
let player_item = get_player_item_or_fail(ctx).await?; let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
queue_command_and_save( suspend_follow_for_independent_move(&mut player_item);
queue_command(
ctx, ctx,
&player_item, &mut player_item,
&QueueCommand::Movement { &QueueCommand::Movement {
direction: dir.clone(), 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; static VERB_INT: Verb = Verb;

View File

@ -20,11 +20,32 @@ use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
use std::time; use std::time;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)]
pub enum MovementSource { pub enum MovementSource {
Command, Command {
Follow, 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)] #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]

View File

@ -1,7 +1,9 @@
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{user_error, CommandHandlingError, UResult}, message_handler::user_commands::{
follow::cancel_follow_by_leader, user_error, CommandHandlingError, UResult,
},
models::{ models::{
item::{DeathData, Item, LocationActionType, SkillType, Subattack}, item::{DeathData, Item, LocationActionType, SkillType, Subattack},
journal::JournalType, journal::JournalType,
@ -445,6 +447,8 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
.unwrap_or_else(|| vec![]), .unwrap_or_else(|| vec![]),
..Default::default() ..Default::default()
}); });
whom.following = None;
cancel_follow_by_leader(trans, &whom.refstr()).await?;
let vic_is_npc = whom.item_type == "npc"; let vic_is_npc = whom.item_type == "npc";
if let Some(ac) = &whom.active_combat { if let Some(ac) = &whom.active_combat {
let at_str = ac.attacking.clone(); let at_str = ac.attacking.clone();

View File

@ -27,6 +27,7 @@ use once_cell::sync::OnceCell;
use rand::{prelude::*, thread_rng, Rng}; use rand::{prelude::*, thread_rng, Rng};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::time; use std::time;
use uuid::Uuid;
mod melbs_citizen; mod melbs_citizen;
mod melbs_dog; mod melbs_dog;
@ -406,7 +407,9 @@ impl TaskHandler for NPCWanderTaskHandler {
&item, &item,
&QueueCommand::Movement { &QueueCommand::Movement {
direction: dir.clone(), direction: dir.clone(),
source: MovementSource::Command, source: MovementSource::Command {
event_id: Uuid::new_v4(),
},
}, },
) )
.await?; .await?;

View File

@ -65,7 +65,7 @@ pub fn room_list() -> Vec<Room> {
code: "chonkers_climbing_top", code: "chonkers_climbing_top",
name: "Top of the Climbing Wall", name: "Top of the Climbing Wall",
short: ansi!("<bgblack><white>CW<reset>"), 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, description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: -1, z: 1 }, grid_coords: GridCoords { x: 0, y: -1, z: 1 },
exits: vec!( exits: vec!(

19
docs/dbfixes.md Normal file
View 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.

View File

@ -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_display ON items (lower(details->>'display'));
CREATE INDEX item_by_owner ON items (lower(details->>'owner')); 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_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 ( CREATE UNIQUE INDEX item_dynamic_entrance ON items (
(details->'dynamic_entrance'->>'source_item'), (details->'dynamic_entrance'->>'source_item'),
(LOWER(details->'dynamic_entrance'->>'direction'))); (LOWER(details->'dynamic_entrance'->>'direction')));