mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Added npc_info, action backtraces
This commit is contained in:
parent
ac83cfc4a3
commit
2b3f0737d0
@ -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",
|
||||
|
@ -99,7 +99,7 @@ pub enum ChunkResource {
|
||||
Cotton,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Profession {
|
||||
#[serde(rename = "0")]
|
||||
Farmer,
|
||||
|
@ -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) }
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user