Added npc_info, action backtraces

This commit is contained in:
Joshua Barretto 2023-01-05 15:09:32 +00:00
parent ac83cfc4a3
commit 2b3f0737d0
6 changed files with 230 additions and 36 deletions

View File

@ -311,6 +311,7 @@ pub enum ServerChatCommand {
Time,
Tp,
TpNpc,
NpcInfo,
Unban,
Version,
Waypoint,
@ -682,7 +683,12 @@ impl ServerChatCommand {
),
ServerChatCommand::TpNpc => cmd(
vec![Integer("npc index", 0, Required)],
"Teleport to a npc",
"Teleport to an rtsim npc",
Some(Moderator),
),
ServerChatCommand::NpcInfo => cmd(
vec![Integer("npc index", 0, Required)],
"Display information about an rtsim NPC",
Some(Moderator),
),
ServerChatCommand::Unban => cmd(
@ -808,6 +814,7 @@ impl ServerChatCommand {
ServerChatCommand::Time => "time",
ServerChatCommand::Tp => "tp",
ServerChatCommand::TpNpc => "tp_npc",
ServerChatCommand::NpcInfo => "npc_info",
ServerChatCommand::Unban => "unban",
ServerChatCommand::Version => "version",
ServerChatCommand::Waypoint => "waypoint",

View File

@ -99,7 +99,7 @@ pub enum ChunkResource {
Cotton,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Profession {
#[serde(rename = "0")]
Farmer,

View File

@ -67,6 +67,10 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool;
/// Generate a backtrace for the action. The action should recursively push
/// all of the tasks it is currently performing.
fn backtrace(&self, bt: &mut Vec<String>);
/// Reset the action to its initial state such that it can be repeated.
fn reset(&mut self);
@ -159,6 +163,21 @@ pub trait Action<R = ()>: Any + Send + Sync {
{
Box::new(self)
}
/// Add debugging information to the action that will be visible when using
/// the `/npc_info` command.
///
/// # Example
///
/// ```ignore
/// goto(npc.home).debug(|| "Going home")
/// ```
fn debug<F, T>(self, mk_info: F) -> Debug<Self, F, T>
where
Self: Sized,
{
Debug(self, mk_info, PhantomData)
}
}
impl<R: 'static> Action<R> for Box<dyn Action<R>> {
@ -171,6 +190,8 @@ impl<R: 'static> Action<R> for Box<dyn Action<R>> {
}
}
fn backtrace(&self, bt: &mut Vec<String>) { (**self).backtrace(bt) }
fn reset(&mut self) { (**self).reset(); }
// TODO: Reset closure state?
@ -191,6 +212,14 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'stati
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
@ -231,6 +260,8 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'stati
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {}
fn reset(&mut self) {}
// TODO: Reset closure state?
@ -266,6 +297,8 @@ impl Action<()> for Finish {
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {}
fn reset(&mut self) {}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) }
@ -324,6 +357,14 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(prev) = &self.prev {
prev.0.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
fn reset(&mut self) { self.prev = None; }
// TODO: Reset `next` too?
@ -435,6 +476,14 @@ impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if self.a0_finished {
self.a1.backtrace(bt);
} else {
self.a0.backtrace(bt);
}
}
fn reset(&mut self) {
self.a0.reset();
self.a0_finished = false;
@ -463,6 +512,8 @@ impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> {
@ -489,6 +540,14 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.2 {
action.backtrace(bt);
} else {
bt.push("<thinking>".to_string());
}
}
fn reset(&mut self) {
self.0 = self.1.clone();
self.2 = None;
@ -551,6 +610,8 @@ impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Act
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> {
@ -575,9 +636,38 @@ impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + '
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1)
}
}
// Debug
/// See [`Action::debug`].
#[derive(Copy, Clone)]
pub struct Debug<A, F, T>(A, F, PhantomData<T>);
impl<
A: Action<R>,
F: Fn() -> T + Send + Sync + 'static,
R: Send + Sync + 'static,
T: Send + Sync + std::fmt::Display + 'static,
> Action<R> for Debug<A, F, T>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) {
bt.push((self.1)().to_string());
self.0.backtrace(bt);
}
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { self.0.tick(ctx) }
}

View File

@ -22,7 +22,7 @@ use std::{
use vek::*;
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm};
#[derive(Copy, Clone, Default)]
#[derive(Copy, Clone, Debug, Default)]
pub enum NpcMode {
/// The NPC is unloaded and is being simulated via rtsim.
#[default]
@ -53,7 +53,7 @@ impl Controller {
}
pub struct Brain {
pub(crate) action: Box<dyn Action<!>>,
pub action: Box<dyn Action<!>>,
}
#[derive(Serialize, Deserialize)]

View File

@ -15,6 +15,7 @@ use common::{
rtsim::{Profession, SiteId},
store::Id,
terrain::TerrainChunkSize,
time::DayPeriod,
vol::RectVolSize,
};
use fxhash::FxHasher64;
@ -29,7 +30,7 @@ use vek::*;
use world::{
civ::{self, Track},
site::{Site as WorldSite, SiteKind},
site2::{self, TileKind},
site2::{self, PlotKind, TileKind},
IndexRef, World,
};
@ -313,7 +314,7 @@ impl Rule for NpcAi {
}
}
fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()) }
fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") }
/// Try to walk toward a 3D position without caring for obstacles.
fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action {
@ -342,6 +343,7 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action {
})
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2))
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {})
}
@ -366,12 +368,14 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
if let Some(current_site) = ctx.npc.current_site
&& let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world)
{
let track_count = tracks.path.len();
// For every track in the path we discovered between the sites...
seq(tracks
.path
.into_iter()
.enumerate()
// ...traverse the nodes of that path.
.map(|(track_id, reversed)| now(move |ctx| {
.map(move |(i, (track_id, reversed))| now(move |ctx| {
let track_len = ctx.world.civs().tracks.get(track_id).path().len();
// Tracks can be traversed backward (i.e: from end to beginning). Account for this.
seq(if reversed {
@ -379,7 +383,8 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
} else {
itertools::Either::Right(0..track_len)
}
.map(move |node_idx| now(move |ctx| {
.enumerate()
.map(move |(i, node_idx)| now(move |ctx| {
// Find the centre of the track node's chunk
let node_chunk_wpos = TerrainChunkSize::center_wpos(ctx.world
.civs()
@ -395,8 +400,10 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
// Walk toward the node
goto_2d(node_wpos.as_(), 1.0)
.debug(move || format!("traversing track node ({}/{})", i + 1, track_len))
})))
})))
})
.debug(move || format!("travel via track {:?} ({}/{})", track_id, i + 1, track_count))))
.boxed()
} else if let Some(site) = sites.get(tgt_site) {
// If all else fails, just walk toward the target site in a straight line
@ -406,12 +413,43 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action {
finish().boxed()
}
})
.debug(move || format!("travel_to_site {:?}", tgt_site))
}
// Seconds
fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
let end = ctx.time.0 + time;
move |ctx| ctx.time.0 > end
fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
let mut timeout = None;
move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time)
}
fn adventure() -> impl Action {
now(|ctx| {
// Choose a random site that's fairly close by
if let Some(tgt_site) = ctx
.state
.data()
.sites
.iter()
.filter(|(site_id, site)| {
// TODO: faction.is_some() is used as a proxy for whether the site likely has
// paths, don't do this
site.faction.is_some()
&& ctx.npc.current_site.map_or(true, |cs| *site_id != cs)
&& thread_rng().gen_bool(0.25)
})
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id)
{
// Travel to the site
travel_to_site(tgt_site)
// Stop for a few minutes
.then(villager().stop_if(timeout(60.0 * 3.0)))
.map(|_| ())
.boxed()
} else {
finish().boxed()
}
})
}
fn villager() -> impl Action {
@ -419,7 +457,34 @@ fn villager() -> impl Action {
if let Some(home) = ctx.npc.home {
if ctx.npc.current_site != Some(home) {
// Travel home if we're not there already
important(travel_to_site(home))
urgent(travel_to_site(home).debug(move || format!("travel home")))
} else if DayPeriod::from(ctx.time_of_day.0).is_dark() {
important(now(move |ctx| {
if let Some(house_wpos) = ctx
.state
.data()
.sites
.get(home)
.and_then(|home| ctx.index.sites.get(home.world_site?).site2())
.and_then(|site2| {
// Find a house
let house = site2
.plots()
.filter(|p| matches!(p.kind(), PlotKind::House(_)))
.choose(&mut thread_rng())?;
Some(site2.tile_center_wpos(house.root_tile()).as_())
})
{
goto_2d(house_wpos, 0.5)
.debug(|| "walk to house")
.then(idle().repeat().debug(|| "wait in house"))
.stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light())
.map(|_| ())
.boxed()
} else {
finish().boxed()
}
}))
} else if matches!(
ctx.npc.profession,
Some(Profession::Merchant | Profession::Blacksmith)
@ -440,11 +505,13 @@ fn villager() -> impl Action {
{
// Walk to the plaza...
goto_2d(plaza_wpos, 0.5)
.debug(|| "walk to plaza")
// ...then wait for some time before moving on
.then(now(|ctx| {
.then({
let wait_time = thread_rng().gen_range(10.0..30.0);
idle().repeat().stop_if(timeout(ctx, wait_time))
}))
idle().repeat().stop_if(timeout(wait_time))
.debug(|| "wait at plaza")
})
.map(|_| ())
.boxed()
} else {
@ -459,31 +526,13 @@ fn villager() -> impl Action {
casual(finish()) // Nothing to do if we're homeless!
}
})
.debug(move || format!("villager"))
}
fn think() -> impl Action {
choose(|ctx| {
if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) {
// Choose a random site that's fairly close by
if let Some(tgt_site) = ctx
.state
.data()
.sites
.iter()
.filter(|(site_id, site)| {
// TODO: faction.is_some() is used as a proxy for whether the site likely has
// paths, don't do this
site.faction.is_some()
&& ctx.npc.current_site.map_or(true, |cs| *site_id != cs)
&& thread_rng().gen_bool(0.25)
})
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id)
{
casual(travel_to_site(tgt_site))
} else {
casual(finish())
}
casual(adventure())
} else {
casual(villager())
}

View File

@ -185,6 +185,7 @@ fn do_command(
ServerChatCommand::Time => handle_time,
ServerChatCommand::Tp => handle_tp,
ServerChatCommand::TpNpc => handle_tp_npc,
ServerChatCommand::NpcInfo => handle_npc_info,
ServerChatCommand::Unban => handle_unban,
ServerChatCommand::Version => handle_version,
ServerChatCommand::Waypoint => handle_waypoint,
@ -1212,6 +1213,53 @@ fn handle_tp_npc(
})
}
fn handle_npc_info(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim2::RtSim;
if let Some(id) = parse_cmd_args!(args, u32) {
// TODO: Take some other identifier than an integer to this command.
let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data();
let npc = data
.npcs
.values()
.nth(id as usize)
.ok_or_else(|| format!("No NPC has index {}", id))?;
let mut info = String::new();
let _ = writeln!(&mut info, "-- General Information --");
let _ = writeln!(&mut info, "Seed: {}", npc.seed);
let _ = writeln!(&mut info, "Profession: {:?}", npc.profession);
let _ = writeln!(&mut info, "Home: {:?}", npc.home);
let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode);
let _ = writeln!(&mut info, "-- Action State --");
if let Some(brain) = &npc.brain {
let mut bt = Vec::new();
brain.action.backtrace(&mut bt);
for (i, action) in bt.into_iter().enumerate() {
let _ = writeln!(&mut info, "[{}] {}", i, action);
}
} else {
let _ = writeln!(&mut info, "<NPC has no brain>");
}
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, info),
);
Ok(())
} else {
Err(action.help_string())
}
}
fn handle_spawn(
server: &mut Server,
client: EcsEntity,