Various rtsim related stuff

This commit is contained in:
Isse 2023-06-03 22:14:18 +00:00
parent 272d57b4fa
commit a884e0e058
25 changed files with 824 additions and 292 deletions

View File

@ -87,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Underwater fall damage - Underwater fall damage
- The scale component now behaves properly - The scale component now behaves properly
- Multiple model support for dropped items (orichalcum armor) - Multiple model support for dropped items (orichalcum armor)
- Made rtsim monsters not go into too deep water, and certainly not outside the map.
- Fixed bug where npcs would be dismounted from vehicles if loaded/unloaded in a certain order.
## [0.14.0] - 2023-01-07 ## [0.14.0] - 2023-01-07

View File

@ -269,6 +269,10 @@ npc-speech-witness_death =
.a0 = No! .a0 = No!
.a1 = This is terrible! .a1 = This is terrible!
.a2 = Oh my goodness! .a2 = Oh my goodness!
npc-speech-welcome-aboard =
.a0 = Welcome aboard!
.a1 = Can I see your ticket... just kidding it's free!
.a2 = Have a nice ride!
npc-speech-dir_north = north npc-speech-dir_north = north
npc-speech-dir_north_east = north-east npc-speech-dir_north_east = north-east
npc-speech-dir_east = east npc-speech-dir_east = east

View File

@ -2233,7 +2233,7 @@ impl Client {
return Err(Error::Other("Failed to find entity from uid.".into())); return Err(Error::Other("Failed to find entity from uid.".into()));
} }
}, },
ServerGeneral::TimeOfDay(time_of_day, calendar, new_time) => { ServerGeneral::TimeOfDay(time_of_day, calendar, new_time, time_scale) => {
self.target_time_of_day = Some(time_of_day); self.target_time_of_day = Some(time_of_day);
*self.state.ecs_mut().write_resource() = calendar; *self.state.ecs_mut().write_resource() = calendar;
let mut time = self.state.ecs_mut().write_resource::<Time>(); let mut time = self.state.ecs_mut().write_resource::<Time>();
@ -2249,7 +2249,7 @@ impl Client {
} else { } else {
0.99 0.99
}; };
self.dt_adjustment = dt_adjustment; self.dt_adjustment = dt_adjustment * time_scale.0;
}, },
ServerGeneral::EntitySync(entity_sync_package) => { ServerGeneral::EntitySync(entity_sync_package) => {
self.state self.state

View File

@ -11,7 +11,7 @@ use common::{
lod, lod,
outcome::Outcome, outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook}, recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{Time, TimeOfDay}, resources::{Time, TimeOfDay, TimeScale},
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, terrain::{Block, TerrainChunk, TerrainChunkMeta, TerrainChunkSize},
trade::{PendingTrade, SitePrices, TradeId, TradeResult}, trade::{PendingTrade, SitePrices, TradeId, TradeResult},
@ -195,7 +195,7 @@ pub enum ServerGeneral {
ChatMsg(comp::ChatMsg), ChatMsg(comp::ChatMsg),
ChatMode(comp::ChatMode), ChatMode(comp::ChatMode),
SetPlayerEntity(Uid), SetPlayerEntity(Uid),
TimeOfDay(TimeOfDay, Calendar, Time), TimeOfDay(TimeOfDay, Calendar, Time, TimeScale),
EntitySync(sync::EntitySyncPackage), EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>, u64), CompSync(sync::CompSyncPackage<EcsCompPacket>, u64),
CreateEntity(sync::EntityPackage<EcsCompPacket>), CreateEntity(sync::EntityPackage<EcsCompPacket>),
@ -342,7 +342,7 @@ impl ServerMsg {
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_) | ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _, _) | ServerGeneral::TimeOfDay(_, _, _, _)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _) | ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_) | ServerGeneral::CreateEntity(_)

View File

@ -328,6 +328,7 @@ pub enum ServerChatCommand {
Sudo, Sudo,
Tell, Tell,
Time, Time,
TimeScale,
Tp, Tp,
Unban, Unban,
Version, Version,
@ -722,6 +723,11 @@ impl ServerChatCommand {
"Set the time of day", "Set the time of day",
Some(Admin), Some(Admin),
), ),
ServerChatCommand::TimeScale => cmd(
vec![Float("time scale", 1.0, Optional)],
"Set scaling of delta time",
Some(Admin),
),
ServerChatCommand::Tp => cmd( ServerChatCommand::Tp => cmd(
vec![ vec![
PlayerName(Optional), PlayerName(Optional),
@ -894,6 +900,7 @@ impl ServerChatCommand {
ServerChatCommand::Sudo => "sudo", ServerChatCommand::Sudo => "sudo",
ServerChatCommand::Tell => "tell", ServerChatCommand::Tell => "tell",
ServerChatCommand::Time => "time", ServerChatCommand::Time => "time",
ServerChatCommand::TimeScale => "time_scale",
ServerChatCommand::Tp => "tp", ServerChatCommand::Tp => "tp",
ServerChatCommand::RtsimTp => "rtsim_tp", ServerChatCommand::RtsimTp => "rtsim_tp",
ServerChatCommand::RtsimInfo => "rtsim_info", ServerChatCommand::RtsimInfo => "rtsim_info",

View File

@ -103,6 +103,32 @@ pub struct AllBodies<BodyMeta, SpeciesMeta> {
pub arthropod: BodyData<BodyMeta, arthropod::AllSpecies<SpeciesMeta>>, pub arthropod: BodyData<BodyMeta, arthropod::AllSpecies<SpeciesMeta>>,
} }
impl<BodyMeta, SpeciesMeta> AllBodies<BodyMeta, SpeciesMeta> {
/// Get species meta associated with the body.
///
/// Returns `None` if the body doesn't have any associated meta, i.e ships,
/// objects, itemdrops.
pub fn get_species_meta<'a>(&'a self, body: &Body) -> Option<&'a SpeciesMeta> {
Some(match body {
Body::Humanoid(b) => &self.humanoid.species[&b.species],
Body::QuadrupedSmall(b) => &self.quadruped_small.species[&b.species],
Body::QuadrupedMedium(b) => &self.quadruped_medium.species[&b.species],
Body::BirdMedium(b) => &self.bird_medium.species[&b.species],
Body::BirdLarge(b) => &self.bird_large.species[&b.species],
Body::FishMedium(b) => &self.fish_medium.species[&b.species],
Body::Dragon(b) => &self.dragon.species[&b.species],
Body::FishSmall(b) => &self.fish_small.species[&b.species],
Body::BipedLarge(b) => &self.biped_large.species[&b.species],
Body::BipedSmall(b) => &self.biped_small.species[&b.species],
Body::Golem(b) => &self.golem.species[&b.species],
Body::Theropod(b) => &self.theropod.species[&b.species],
Body::QuadrupedLow(b) => &self.quadruped_low.species[&b.species],
Body::Arthropod(b) => &self.arthropod.species[&b.species],
_ => return None,
})
}
}
/// Can only retrieve body metadata by direct index. /// Can only retrieve body metadata by direct index.
impl<BodyMeta, SpeciesMeta> core::ops::Index<NpcKind> for AllBodies<BodyMeta, SpeciesMeta> { impl<BodyMeta, SpeciesMeta> core::ops::Index<NpcKind> for AllBodies<BodyMeta, SpeciesMeta> {
type Output = BodyMeta; type Output = BodyMeta;

View File

@ -99,6 +99,7 @@ impl ModularBase {
hand_restriction.unwrap_or(Hands::One) hand_restriction.unwrap_or(Hands::One)
} }
#[inline(never)]
pub(super) fn kind( pub(super) fn kind(
&self, &self,
components: &[Item], components: &[Item],

View File

@ -173,18 +173,18 @@ impl Role for VolumeRider {
} }
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)] #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Volume { pub enum Volume<E> {
Terrain, Terrain,
Entity(Uid), Entity(E),
} }
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)] #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct VolumePos { pub struct VolumePos<E = Uid> {
pub kind: Volume, pub kind: Volume<E>,
pub pos: Vec3<i32>, pub pos: Vec3<i32>,
} }
impl VolumePos { impl<E> VolumePos<E> {
pub fn terrain(block_pos: Vec3<i32>) -> Self { pub fn terrain(block_pos: Vec3<i32>) -> Self {
Self { Self {
kind: Volume::Terrain, kind: Volume::Terrain,
@ -192,12 +192,22 @@ impl VolumePos {
} }
} }
pub fn entity(block_pos: Vec3<i32>, uid: Uid) -> Self { pub fn entity(block_pos: Vec3<i32>, uid: E) -> Self {
Self { Self {
kind: Volume::Entity(uid), kind: Volume::Entity(uid),
pos: block_pos, pos: block_pos,
} }
} }
pub fn try_map_entity<U>(self, f: impl FnOnce(E) -> Option<U>) -> Option<VolumePos<U>> {
Some(VolumePos {
pos: self.pos,
kind: match self.kind {
Volume::Terrain => Volume::Terrain,
Volume::Entity(e) => Volume::Entity(f(e)?),
},
})
}
} }
impl VolumePos { impl VolumePos {

View File

@ -13,6 +13,13 @@ pub struct TimeOfDay(pub f64);
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Time(pub f64); pub struct Time(pub f64);
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct TimeScale(pub f64);
impl Default for TimeScale {
fn default() -> Self { Self(1.0) }
}
/// A resource that stores the time since the previous tick. /// A resource that stores the time since the previous tick.
#[derive(Default)] #[derive(Default)]
pub struct DeltaTime(pub f32); pub struct DeltaTime(pub f32);

View File

@ -15,7 +15,7 @@ use common::{
region::RegionMap, region::RegionMap,
resources::{ resources::{
DeltaTime, EntitiesDiedLastTick, GameMode, PlayerEntity, PlayerPhysicsSettings, Time, DeltaTime, EntitiesDiedLastTick, GameMode, PlayerEntity, PlayerPhysicsSettings, Time,
TimeOfDay, TimeOfDay, TimeScale,
}, },
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
slowjob::SlowJobPool, slowjob::SlowJobPool,
@ -269,6 +269,7 @@ impl State {
ecs.insert(Calendar::default()); ecs.insert(Calendar::default());
ecs.insert(WeatherGrid::new(Vec2::zero())); ecs.insert(WeatherGrid::new(Vec2::zero()));
ecs.insert(Time(0.0)); ecs.insert(Time(0.0));
ecs.insert(TimeScale(1.0));
// Register unsynced resources used by the ECS. // Register unsynced resources used by the ECS.
ecs.insert(DeltaTime(0.0)); ecs.insert(DeltaTime(0.0));
@ -644,14 +645,16 @@ impl State {
} }
// Change the time accordingly. // Change the time accordingly.
let time_scale = self.ecs.read_resource::<TimeScale>().0;
self.ecs.write_resource::<TimeOfDay>().0 += self.ecs.write_resource::<TimeOfDay>().0 +=
dt.as_secs_f64() * server_constants.day_cycle_coefficient; dt.as_secs_f64() * server_constants.day_cycle_coefficient * time_scale;
self.ecs.write_resource::<Time>().0 += dt.as_secs_f64(); self.ecs.write_resource::<Time>().0 += dt.as_secs_f64() * time_scale;
// Update delta time. // Update delta time.
// Beyond a delta time of MAX_DELTA_TIME, start lagging to avoid skipping // Beyond a delta time of MAX_DELTA_TIME, start lagging to avoid skipping
// important physics events. // important physics events.
self.ecs.write_resource::<DeltaTime>().0 = dt.as_secs_f32().min(MAX_DELTA_TIME); self.ecs.write_resource::<DeltaTime>().0 =
(dt.as_secs_f32() * time_scale as f32).min(MAX_DELTA_TIME);
if update_terrain_and_regions { if update_terrain_and_regions {
self.update_region_map(); self.update_region_map();

View File

@ -1,3 +1,7 @@
pub mod predicate;
use predicate::Predicate;
use crate::{ use crate::{
data::{ data::{
npc::{Controller, Npc, NpcId}, npc::{Controller, Npc, NpcId},
@ -15,6 +19,39 @@ use rand_chacha::ChaChaRng;
use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow}; use std::{any::Any, collections::VecDeque, marker::PhantomData, ops::ControlFlow};
use world::{IndexRef, World}; use world::{IndexRef, World};
pub trait State: Clone + Send + Sync + 'static {}
impl<T: Clone + Send + Sync + 'static> State for T {}
#[derive(Clone, Copy)]
struct Resettable<T> {
original: T,
current: T,
}
impl<T: Clone> From<T> for Resettable<T> {
fn from(value: T) -> Self {
Self {
original: value.clone(),
current: value,
}
}
}
impl<T: Clone> Resettable<T> {
fn reset(&mut self) { self.current = self.original.clone(); }
}
impl<T> std::ops::Deref for Resettable<T> {
type Target = T;
fn deref(&self) -> &Self::Target { &self.current }
}
impl<T> std::ops::DerefMut for Resettable<T> {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.current }
}
/// The context provided to an [`Action`] while it is being performed. It should /// The context provided to an [`Action`] while it is being performed. It should
/// be possible to access any and all important information about the game world /// be possible to access any and all important information about the game world
/// through this struct. /// through this struct.
@ -33,6 +70,8 @@ pub struct NpcCtx<'a> {
pub sentiments: &'a mut Sentiments, pub sentiments: &'a mut Sentiments,
pub known_reports: &'a mut HashSet<ReportId>, pub known_reports: &'a mut HashSet<ReportId>,
/// The delta time since this npcs ai was last ran.
pub dt: f32,
pub rng: ChaChaRng, pub rng: ChaChaRng,
} }
@ -55,7 +94,7 @@ pub struct NpcCtx<'a> {
/// You should not need to implement this trait yourself when writing AI code. /// You should not need to implement this trait yourself when writing AI code.
/// If you find yourself wanting to implement it, please discuss with the core /// If you find yourself wanting to implement it, please discuss with the core
/// dev team first. /// dev team first.
pub trait Action<R = ()>: Any + Send + Sync { pub trait Action<S = (), R = ()>: Any + Send + Sync {
/// Returns `true` if the action should be considered the 'same' (i.e: /// Returns `true` if the action should be considered the 'same' (i.e:
/// achieving the same objective) as another. In general, the AI system /// achieving the same objective) as another. In general, the AI system
/// will try to avoid switching (and therefore restarting) tasks when the /// will try to avoid switching (and therefore restarting) tasks when the
@ -68,7 +107,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
Self: Sized; Self: Sized;
/// Like [`Action::is_same`], but allows for dynamic dispatch. /// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same_sized(&self, other: &dyn Action<R>) -> bool fn dyn_is_same_sized(&self, other: &dyn Action<S, R>) -> bool
where where
Self: Sized, Self: Sized,
{ {
@ -79,7 +118,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
} }
/// Like [`Action::is_same`], but allows for dynamic dispatch. /// Like [`Action::is_same`], but allows for dynamic dispatch.
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool; fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool;
/// Generate a backtrace for the action. The action should recursively push /// Generate a backtrace for the action. The action should recursively push
/// all of the tasks it is currently performing. /// all of the tasks it is currently performing.
@ -89,7 +128,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
fn reset(&mut self); fn reset(&mut self);
/// Perform the action for the current tick. /// Perform the action for the current tick.
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R>; fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R>;
/// Create an action that chains together two sub-actions, one after the /// Create an action that chains together two sub-actions, one after the
/// other. /// other.
@ -101,7 +140,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// goto(enemy_npc).then(attack(enemy_npc)) /// goto(enemy_npc).then(attack(enemy_npc))
/// ``` /// ```
#[must_use] #[must_use]
fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R> fn then<A1: Action<S, R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where where
Self: Sized, Self: Sized,
{ {
@ -122,7 +161,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// find_and_collect(ChunkResource::Flax).repeat() /// find_and_collect(ChunkResource::Flax).repeat()
/// ``` /// ```
#[must_use] #[must_use]
fn repeat<R1>(self) -> Repeat<Self, R1> fn repeat(self) -> Repeat<Self, R>
where where
Self: Sized, Self: Sized,
{ {
@ -138,11 +177,11 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0) /// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0)
/// ``` /// ```
#[must_use] #[must_use]
fn stop_if<F: FnMut(&mut NpcCtx) -> bool + Clone>(self, f: F) -> StopIf<Self, F> fn stop_if<P: Predicate + Clone>(self, p: P) -> StopIf<Self, P>
where where
Self: Sized, Self: Sized,
{ {
StopIf(self, f.clone(), f) StopIf(self, p.into())
} }
/// Pause an action to possibly perform another action. /// Pause an action to possibly perform another action.
@ -159,7 +198,11 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// }) /// })
/// ``` /// ```
#[must_use] #[must_use]
fn interrupt_with<A1: Action<R1>, R1, F: FnMut(&mut NpcCtx) -> Option<A1> + Clone>( fn interrupt_with<
A1: Action<S, R1>,
R1,
F: Fn(&mut NpcCtx, &mut S) -> Option<A1> + Send + Sync + 'static,
>(
self, self,
f: F, f: F,
) -> InterruptWith<Self, F, A1, R1> ) -> InterruptWith<Self, F, A1, R1>
@ -168,8 +211,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
{ {
InterruptWith { InterruptWith {
a0: self, a0: self,
f: f.clone(), f,
f2: f,
a1: None, a1: None,
phantom: PhantomData, phantom: PhantomData,
} }
@ -177,7 +219,7 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// Map the completion value of this action to something else. /// Map the completion value of this action to something else.
#[must_use] #[must_use]
fn map<F: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R> fn map<F: Fn(R, &mut S) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where where
Self: Sized, Self: Sized,
{ {
@ -209,13 +251,54 @@ pub trait Action<R = ()>: Any + Send + Sync {
/// } /// }
/// ``` /// ```
#[must_use] #[must_use]
fn boxed(self) -> Box<dyn Action<R>> fn boxed(self) -> Box<dyn Action<S, R>>
where where
Self: Sized, Self: Sized,
{ {
Box::new(self) Box::new(self)
} }
/// Set the state for child actions.
///
/// Note that state is reset when repeated.
///
/// # Example
///
/// ```ignore
/// just(|_, state: &mut i32| *state += 2)
/// // Outputs 5
/// .then(just(|_, state: &mut i32| println!("{state}")))
/// .with_state(3)
/// ```
#[must_use]
fn with_state<S0>(self, s: S) -> WithState<Self, S, S0>
where
Self: Sized,
S: Clone,
{
WithState(self, s.into(), PhantomData)
}
/// Map the current state for child actions, this map expects the return
/// value to have the same lifetime as the input state.
///
/// # Example
///
/// ```ignore
/// // Goes forward 5 steps
/// just(|_, state: &mut i32| go_forward(*state))
/// .map_state(|state: &mut (i32, i32)| &mut state.1)
/// .with_state((14, 5))
/// ```
#[must_use]
fn map_state<S0, F>(self, f: F) -> MapState<Self, F, S, S0>
where
F: Fn(&mut S0) -> &mut S,
Self: Sized,
{
MapState(self, f, PhantomData)
}
/// Add debugging information to the action that will be visible when using /// Add debugging information to the action that will be visible when using
/// the `/npc_info` command. /// the `/npc_info` command.
/// ///
@ -249,10 +332,10 @@ pub trait Action<R = ()>: Any + Send + Sync {
} }
} }
impl<R: 'static> Action<R> for Box<dyn Action<R>> { impl<S: State, R: 'static> Action<S, R> for Box<dyn Action<S, R>> {
fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) } fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool {
match (other as &dyn Any).downcast_ref::<Self>() { match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other), Some(other) => self.is_same(other),
None => false, None => false,
@ -263,10 +346,12 @@ impl<R: 'static> Action<R> for Box<dyn Action<R>> {
fn reset(&mut self) { (**self).reset(); } fn reset(&mut self) { (**self).reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { (**self).tick(ctx) } fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
(**self).tick(ctx, state)
}
} }
impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> { impl<S: State, R: 'static, A: Action<S, R>, B: Action<S, R>> Action<S, R> for Either<A, B> {
fn is_same(&self, other: &Self) -> bool { fn is_same(&self, other: &Self) -> bool {
match (self, other) { match (self, other) {
(Either::Left(x), Either::Left(y)) => x.is_same(y), (Either::Left(x), Either::Left(y)) => x.is_same(y),
@ -275,7 +360,7 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
} }
} }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
match self { match self {
@ -291,10 +376,10 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
} }
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
match self { match self {
Either::Left(x) => x.tick(ctx), Either::Left(x) => x.tick(ctx, state),
Either::Right(x) => x.tick(ctx), Either::Right(x) => x.tick(ctx, state),
} }
} }
} }
@ -305,13 +390,17 @@ impl<R: 'static, A: Action<R>, B: Action<R>> Action<R> for Either<A, B> {
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Now<F, A>(F, Option<A>); pub struct Now<F, A>(F, Option<A>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'static, A: Action<R>> impl<
Action<R> for Now<F, A> S: State,
R: Send + Sync + 'static,
F: Fn(&mut NpcCtx, &mut S) -> A + Send + Sync + 'static,
A: Action<S, R>,
> Action<S, R> for Now<F, A>
{ {
// TODO: This doesn't compare?! // TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 { if let Some(action) = &self.1 {
@ -321,12 +410,11 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'stati
} }
} }
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; } fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state? // TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
(self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx) (self.1.get_or_insert_with(|| (self.0)(ctx, state))).tick(ctx, state)
} }
} }
@ -342,9 +430,9 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'stati
/// // An action that makes an NPC immediately travel to its *current* home /// // An action that makes an NPC immediately travel to its *current* home
/// now(|ctx| goto(ctx.npc.home)) /// now(|ctx| goto(ctx.npc.home))
/// ``` /// ```
pub fn now<F, A>(f: F) -> Now<F, A> pub fn now<S, R, F, A: Action<S, R>>(f: F) -> Now<F, A>
where where
F: FnMut(&mut NpcCtx) -> A, F: Fn(&mut NpcCtx, &mut S) -> A + Send + Sync + 'static,
{ {
Now(f, None) Now(f, None)
} }
@ -356,15 +444,16 @@ where
pub struct Until<F, A, R>(F, Option<A>, PhantomData<R>); pub struct Until<F, A, R>(F, Option<A>, PhantomData<R>);
impl< impl<
S: State,
R: Send + Sync + 'static, R: Send + Sync + 'static,
F: FnMut(&mut NpcCtx) -> Option<A> + Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> Option<A> + Send + Sync + 'static,
A: Action<R>, A: Action<S, R>,
> Action<()> for Until<F, A, R> > Action<S, ()> for Until<F, A, R>
{ {
// TODO: This doesn't compare?! // TODO: This doesn't compare?!
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.1 { if let Some(action) = &self.1 {
@ -374,20 +463,18 @@ impl<
} }
} }
// TODO: Reset closure?
fn reset(&mut self) { self.1 = None; } fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state? fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<()> {
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> {
match &mut self.1 { match &mut self.1 {
Some(x) => match x.tick(ctx) { Some(x) => match x.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()), ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => { ControlFlow::Break(_) => {
self.1 = None; self.1 = None;
ControlFlow::Continue(()) ControlFlow::Continue(())
}, },
}, },
None => match (self.0)(ctx) { None => match (self.0)(ctx, state) {
Some(x) => { Some(x) => {
self.1 = Some(x); self.1 = Some(x);
ControlFlow::Continue(()) ControlFlow::Continue(())
@ -398,9 +485,9 @@ impl<
} }
} }
pub fn until<F, A, R>(f: F) -> Until<F, A, R> pub fn until<S, F, A: Action<S, R>, R>(f: F) -> Until<F, A, R>
where where
F: FnMut(&mut NpcCtx) -> Option<A>, F: Fn(&mut NpcCtx, &mut S) -> Option<A>,
{ {
Until(f, None, PhantomData) Until(f, None, PhantomData)
} }
@ -411,20 +498,20 @@ where
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Just<F, R = ()>(F, PhantomData<R>); pub struct Just<F, R = ()>(F, PhantomData<R>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static> Action<R> impl<S: State, R: Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> R + Send + Sync + 'static>
for Just<F, R> Action<S, R> for Just<F, R>
{ {
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {} fn backtrace(&self, _bt: &mut Vec<String>) {}
// TODO: Reset closure?
fn reset(&mut self) {} fn reset(&mut self) {}
// TODO: Reset closure state? fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { ControlFlow::Break((self.0)(ctx)) } ControlFlow::Break((self.0)(ctx, state))
}
} }
/// An action that executes some code just once when performed. /// An action that executes some code just once when performed.
@ -438,9 +525,9 @@ impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'stati
/// // Make the current NPC say 'Hello, world!' exactly once /// // Make the current NPC say 'Hello, world!' exactly once
/// just(|ctx| ctx.controller.say("Hello, world!")) /// just(|ctx| ctx.controller.say("Hello, world!"))
/// ``` /// ```
pub fn just<F, R: Send + Sync + 'static>(f: F) -> Just<F, R> pub fn just<S: State, F, R: Send + Sync + 'static>(f: F) -> Just<F, R>
where where
F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> R + Send + Sync + 'static,
{ {
Just(f, PhantomData) Just(f, PhantomData)
} }
@ -451,16 +538,18 @@ where
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Finish; pub struct Finish;
impl Action<()> for Finish { impl<S: State> Action<S, ()> for Finish {
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, _bt: &mut Vec<String>) {} fn backtrace(&self, _bt: &mut Vec<String>) {}
fn reset(&mut self) {} fn reset(&mut self) {}
fn tick(&mut self, _ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } fn tick(&mut self, _ctx: &mut NpcCtx, _state: &mut S) -> ControlFlow<()> {
ControlFlow::Break(())
}
} }
/// An action that immediately finishes without doing anything. /// An action that immediately finishes without doing anything.
@ -492,33 +581,33 @@ pub const URGENT: Priority = 0;
pub const IMPORTANT: Priority = 1; pub const IMPORTANT: Priority = 1;
pub const CASUAL: Priority = 2; pub const CASUAL: Priority = 2;
pub struct Node<R>(Box<dyn Action<R>>, Priority); pub struct Node<S, R>(Box<dyn Action<S, R>>, Priority);
/// Perform an action with [`URGENT`] priority (see [`choose`]). /// Perform an action with [`URGENT`] priority (see [`choose`]).
#[must_use] #[must_use]
pub fn urgent<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) } pub fn urgent<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), URGENT) }
/// Perform an action with [`IMPORTANT`] priority (see [`choose`]). /// Perform an action with [`IMPORTANT`] priority (see [`choose`]).
#[must_use] #[must_use]
pub fn important<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), IMPORTANT) } pub fn important<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), IMPORTANT) }
/// Perform an action with [`CASUAL`] priority (see [`choose`]). /// Perform an action with [`CASUAL`] priority (see [`choose`]).
#[must_use] #[must_use]
pub fn casual<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) } pub fn casual<S, A: Action<S, R>, R>(a: A) -> Node<S, R> { Node(Box::new(a), CASUAL) }
/// See [`choose`] and [`watch`]. /// See [`choose`] and [`watch`].
pub struct Tree<F, R> { pub struct Tree<S, F, R> {
next: F, next: F,
prev: Option<Node<R>>, prev: Option<Node<S, R>>,
interrupt: bool, interrupt: bool,
} }
impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Action<R> impl<S: State, F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static, R: 'static>
for Tree<F, R> Action<S, R> for Tree<S, F, R>
{ {
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(prev) = &self.prev { if let Some(prev) = &self.prev {
@ -530,9 +619,8 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
fn reset(&mut self) { self.prev = None; } fn reset(&mut self) { self.prev = None; }
// TODO: Reset `next` too? fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { let new = (self.next)(ctx, state);
let new = (self.next)(ctx);
let prev = match &mut self.prev { let prev = match &mut self.prev {
Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => { Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => {
@ -541,7 +629,7 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
_ => self.prev.insert(new), _ => self.prev.insert(new),
}; };
match prev.0.tick(ctx) { match prev.0.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()), ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(r) => { ControlFlow::Break(r) => {
self.prev = None; self.prev = None;
@ -575,9 +663,9 @@ impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Actio
/// }) /// })
/// ``` /// ```
#[must_use] #[must_use]
pub fn choose<R: 'static, F>(f: F) -> impl Action<R> pub fn choose<S: State, R: 'static, F>(f: F) -> impl Action<S, R>
where where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static,
{ {
Tree { Tree {
next: f, next: f,
@ -610,9 +698,9 @@ where
/// }) /// })
/// ``` /// ```
#[must_use] #[must_use]
pub fn watch<R: 'static, F>(f: F) -> impl Action<R> pub fn watch<S: State, R: 'static, F>(f: F) -> impl Action<S, R>
where where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, F: Fn(&mut NpcCtx, &mut S) -> Node<S, R> + Send + Sync + 'static,
{ {
Tree { Tree {
next: f, next: f,
@ -632,14 +720,19 @@ pub struct Then<A0, A1, R0> {
phantom: PhantomData<R0>, phantom: PhantomData<R0>,
} }
impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync + 'static> impl<
Action<R1> for Then<A0, A1, R0> S: State,
A0: Action<S, R0>,
A1: Action<S, R1>,
R0: Send + Sync + 'static,
R1: Send + Sync + 'static,
> Action<S, R1> for Then<A0, A1, R0>
{ {
fn is_same(&self, other: &Self) -> bool { fn is_same(&self, other: &Self) -> bool {
self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1) self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1)
} }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if self.a0_finished { if self.a0_finished {
@ -655,14 +748,14 @@ impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync
self.a1.reset(); self.a1.reset();
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R1> {
if !self.a0_finished { if !self.a0_finished {
match self.a0.tick(ctx) { match self.a0.tick(ctx, state) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()), ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a0_finished = true, ControlFlow::Break(_) => self.a0_finished = true,
} }
} }
self.a1.tick(ctx) self.a1.tick(ctx, state)
} }
} }
@ -673,22 +766,22 @@ impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync
pub struct InterruptWith<A0, F, A1, R1> { pub struct InterruptWith<A0, F, A1, R1> {
a0: A0, a0: A0,
f: F, f: F,
f2: F,
a1: Option<A1>, a1: Option<A1>,
phantom: PhantomData<R1>, phantom: PhantomData<R1>,
} }
impl< impl<
A0: Action<R0>, S: State,
A1: Action<R1>, A0: Action<S, R0>,
F: FnMut(&mut NpcCtx) -> Option<A1> + Clone + Send + Sync + 'static, A1: Action<S, R1>,
F: Fn(&mut NpcCtx, &mut S) -> Option<A1> + Send + Sync + 'static,
R0: Send + Sync + 'static, R0: Send + Sync + 'static,
R1: Send + Sync + 'static, R1: Send + Sync + 'static,
> Action<R0> for InterruptWith<A0, F, A1, R1> > Action<S, R0> for InterruptWith<A0, F, A1, R1>
{ {
fn is_same(&self, other: &Self) -> bool { self.a0.is_same(&other.a0) } fn is_same(&self, other: &Self) -> bool { self.a0.is_same(&other.a0) }
fn dyn_is_same(&self, other: &dyn Action<R0>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R0>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(a1) = &self.a1 { if let Some(a1) = &self.a1 {
@ -702,23 +795,22 @@ impl<
fn reset(&mut self) { fn reset(&mut self) {
self.a0.reset(); self.a0.reset();
self.f = self.f2.clone();
self.a1 = None; self.a1 = None;
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R0> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R0> {
if let Some(new_a1) = (self.f)(ctx) { if let Some(new_a1) = (self.f)(ctx, state) {
self.a1 = Some(new_a1); self.a1 = Some(new_a1);
} }
if let Some(a1) = &mut self.a1 { if let Some(a1) = &mut self.a1 {
match a1.tick(ctx) { match a1.tick(ctx, state) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()), ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a1 = None, ControlFlow::Break(_) => self.a1 = None,
} }
} }
self.a0.tick(ctx) self.a0.tick(ctx, state)
} }
} }
@ -728,17 +820,17 @@ impl<
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Repeat<A, R = ()>(A, PhantomData<R>); pub struct Repeat<A, R = ()>(A, PhantomData<R>);
impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> { impl<S: State, R: Send + Sync + 'static, A: Action<S, R>> Action<S, !> for Repeat<A, R> {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, !>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); } fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); } fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<!> {
match self.0.tick(ctx) { match self.0.tick(ctx, state) {
ControlFlow::Continue(()) => ControlFlow::Continue(()), ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => { ControlFlow::Break(_) => {
self.0.reset(); self.0.reset();
@ -752,17 +844,21 @@ impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
/// See [`seq`]. /// See [`seq`].
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Sequence<I, A, R = ()>(I, I, Option<A>, PhantomData<R>); pub struct Sequence<I, A, R = ()>(Resettable<I>, Option<A>, PhantomData<R>);
impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'static, A: Action<R>> impl<
Action<()> for Sequence<I, A, R> S: State,
R: Send + Sync + 'static,
I: Iterator<Item = A> + Clone + Send + Sync + 'static,
A: Action<S, R>,
> Action<S, ()> for Sequence<I, A, R>
{ {
fn is_same(&self, _other: &Self) -> bool { true } fn is_same(&self, _other: &Self) -> bool { true }
fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, ()>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
if let Some(action) = &self.2 { if let Some(action) = &self.1 {
action.backtrace(bt); action.backtrace(bt);
} else { } else {
bt.push("<thinking>".to_string()); bt.push("<thinking>".to_string());
@ -770,22 +866,22 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
} }
fn reset(&mut self) { fn reset(&mut self) {
self.0 = self.1.clone(); self.0.reset();
self.2 = None; self.1 = None;
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<()> {
let item = if let Some(prev) = &mut self.2 { let item = if let Some(prev) = &mut self.1 {
prev prev
} else { } else {
match self.0.next() { match self.0.next() {
Some(next) => self.2.insert(next), Some(next) => self.1.insert(next),
None => return ControlFlow::Break(()), None => return ControlFlow::Break(()),
} }
}; };
if let ControlFlow::Break(_) = item.tick(ctx) { if let ControlFlow::Break(_) = item.tick(ctx, state) {
self.2 = None; self.1 = None;
} }
ControlFlow::Continue(()) ControlFlow::Continue(())
@ -811,39 +907,41 @@ impl<R: Send + Sync + 'static, I: Iterator<Item = A> + Clone + Send + Sync + 'st
/// .map(|enemy| attack(enemy))) /// .map(|enemy| attack(enemy)))
/// ``` /// ```
#[must_use] #[must_use]
pub fn seq<I, A, R>(iter: I) -> Sequence<I, A, R> pub fn seq<S, I, A, R>(iter: I) -> Sequence<I, A, R>
where where
I: Iterator<Item = A> + Clone, I: Iterator<Item = A> + Clone,
A: Action<R>, A: Action<S, R>,
{ {
Sequence(iter.clone(), iter, None, PhantomData) Sequence(iter.into(), None, PhantomData)
} }
// StopIf // StopIf
/// See [`Action::stop_if`]. /// See [`Action::stop_if`].
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct StopIf<A, F>(A, F, F); pub struct StopIf<A, P>(A, Resettable<P>);
impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static, R> impl<S: State, A: Action<S, R>, P: Predicate + Clone + Send + Sync + 'static, R>
Action<Option<R>> for StopIf<A, F> Action<S, Option<R>> for StopIf<A, P>
{ {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, Option<R>>) -> bool {
self.dyn_is_same_sized(other)
}
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); } fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { fn reset(&mut self) {
self.0.reset(); self.0.reset();
self.1 = self.2.clone(); self.1.reset();
} }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<Option<R>> {
if (self.1)(ctx) { if self.1.should(ctx) {
ControlFlow::Break(None) ControlFlow::Break(None)
} else { } else {
self.0.tick(ctx).map_break(Some) self.0.tick(ctx, state).map_break(Some)
} }
} }
} }
@ -854,19 +952,24 @@ impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Map<A, F, R>(A, F, PhantomData<R>); pub struct Map<A, F, R>(A, F, PhantomData<R>);
impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1> impl<
Action<R1> for Map<A, F, R> S: State,
A: Action<S, R>,
F: Fn(R, &mut S) -> R1 + Send + Sync + 'static,
R: Send + Sync + 'static,
R1,
> Action<S, R1> for Map<A, F, R>
{ {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) } fn dyn_is_same(&self, other: &dyn Action<S, R1>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); } fn backtrace(&self, bt: &mut Vec<String>) { self.0.backtrace(bt); }
fn reset(&mut self) { self.0.reset(); } fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> { fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1) self.0.tick(ctx, state).map_break(|t| (self.1)(t, state))
} }
} }
@ -877,15 +980,16 @@ impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + '
pub struct Debug<A, F, T>(A, F, PhantomData<T>); pub struct Debug<A, F, T>(A, F, PhantomData<T>);
impl< impl<
A: Action<R>, S: 'static,
A: Action<S, R>,
F: Fn() -> T + Send + Sync + 'static, F: Fn() -> T + Send + Sync + 'static,
R: Send + Sync + 'static, R: Send + Sync + 'static,
T: Send + Sync + std::fmt::Display + 'static, T: Send + Sync + std::fmt::Display + 'static,
> Action<R> for Debug<A, F, T> > Action<S, R> for Debug<A, F, T>
{ {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } 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 dyn_is_same(&self, other: &dyn Action<S, R>) -> bool { self.dyn_is_same_sized(other) }
fn backtrace(&self, bt: &mut Vec<String>) { fn backtrace(&self, bt: &mut Vec<String>) {
bt.push((self.1)().to_string()); bt.push((self.1)().to_string());
@ -894,5 +998,56 @@ impl<
fn reset(&mut self) { self.0.reset(); } fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { self.0.tick(ctx) } fn tick(&mut self, ctx: &mut NpcCtx, state: &mut S) -> ControlFlow<R> {
self.0.tick(ctx, state)
}
}
#[derive(Copy, Clone)]
pub struct WithState<A, S, S0>(A, Resettable<S>, PhantomData<S0>);
impl<S0: State, S: State, R, A: Action<S, R>> Action<S0, R> for WithState<A, S, S0> {
fn is_same(&self, other: &Self) -> bool
where
Self: Sized,
{
self.0.is_same(&other.0)
}
fn dyn_is_same(&self, other: &dyn Action<S0, 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();
self.1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx, _state: &mut S0) -> ControlFlow<R> {
self.0.tick(ctx, &mut self.1.current)
}
}
#[derive(Copy, Clone)]
pub struct MapState<A, F, S, S0>(A, F, PhantomData<(S, S0)>);
impl<S0: State, S: State, R, A: Action<S, R>, F: Fn(&mut S0) -> &mut S + Send + Sync + 'static>
Action<S0, R> for MapState<A, F, S, S0>
{
fn is_same(&self, other: &Self) -> bool
where
Self: Sized,
{
self.0.is_same(&other.0)
}
fn dyn_is_same(&self, other: &dyn Action<S0, 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, state: &mut S0) -> ControlFlow<R> {
self.0.tick(ctx, (self.1)(state))
}
} }

93
rtsim/src/ai/predicate.rs Normal file
View File

@ -0,0 +1,93 @@
use rand::Rng;
use super::NpcCtx;
pub trait Predicate: Sized + Clone {
fn should(&mut self, ctx: &mut NpcCtx) -> bool;
fn chance(self, chance: f32) -> Chance<Self> {
Chance {
predicate: self,
chance,
}
}
/// Hint for when this will be true.
fn time_hint(&self) -> Option<f32> { None }
}
#[derive(Clone, Copy)]
pub struct Yes;
impl Predicate for Yes {
fn should(&mut self, _ctx: &mut NpcCtx) -> bool { true }
fn time_hint(&self) -> Option<f32> { Some(0.0) }
}
#[derive(Clone)]
pub struct EveryRange {
next: Option<f32>,
sample: std::ops::Range<f32>,
}
pub fn every_range(r: std::ops::Range<f32>) -> EveryRange {
EveryRange {
next: None,
sample: r,
}
}
impl Predicate for EveryRange {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
if let Some(ref mut next) = self.next {
*next -= ctx.dt;
if *next <= 0.0 {
*next += ctx.rng.gen_range(self.sample.clone());
true
} else {
false
}
} else {
self.next = Some(ctx.rng.gen_range(self.sample.clone()));
false
}
}
fn time_hint(&self) -> Option<f32> { self.next }
}
#[derive(Clone, Copy)]
pub struct Chance<P> {
predicate: P,
chance: f32,
}
impl<P: Predicate> Predicate for Chance<P> {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
self.predicate.should(ctx) && ctx.rng.gen_bool(self.chance as f64)
}
fn time_hint(&self) -> Option<f32> { self.predicate.time_hint() }
}
impl<F: Fn(&mut NpcCtx) -> bool + Clone> Predicate for F {
fn should(&mut self, ctx: &mut NpcCtx) -> bool { self(ctx) }
}
// Seconds
pub fn timeout(time: f64) -> Timeout { Timeout { seconds_left: time } }
#[derive(Clone, Copy)]
pub struct Timeout {
seconds_left: f64,
}
impl Predicate for Timeout {
fn should(&mut self, ctx: &mut NpcCtx) -> bool {
self.seconds_left -= ctx.dt as f64;
self.seconds_left <= 0.0
}
fn time_hint(&self) -> Option<f32> { Some(self.seconds_left.max(0.0) as f32) }
}

View File

@ -29,7 +29,7 @@ use std::{
/// Note that this number does *not* need incrementing on every change: most /// Note that this number does *not* need incrementing on every change: most
/// field removals/additions are fine. This number should only be incremented /// field removals/additions are fine. This number should only be incremented
/// when we wish to perform a *hard purge* of rtsim data. /// when we wish to perform a *hard purge* of rtsim data.
pub const CURRENT_VERSION: u32 = 2; pub const CURRENT_VERSION: u32 = 3;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Data { pub struct Data {

View File

@ -86,7 +86,7 @@ impl Controller {
} }
pub struct Brain { pub struct Brain {
pub action: Box<dyn Action<!>>, pub action: Box<dyn Action<(), !>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View File

@ -1,7 +1,8 @@
use crate::{RtState, Rule}; use crate::{RtState, Rule};
use common::{ use common::{
mounting::VolumePos,
resources::{Time, TimeOfDay}, resources::{Time, TimeOfDay},
rtsim::Actor, rtsim::{Actor, VehicleId},
}; };
use vek::*; use vek::*;
use world::{IndexRef, World}; use world::{IndexRef, World};
@ -36,3 +37,10 @@ pub struct OnDeath {
pub killer: Option<Actor>, pub killer: Option<Actor>,
} }
impl Event for OnDeath {} impl Event for OnDeath {}
#[derive(Clone)]
pub struct OnMountVolume {
pub actor: Actor,
pub pos: VolumePos<VehicleId>,
}
impl Event for OnMountVolume {}

View File

@ -7,7 +7,10 @@
trait_upcasting, trait_upcasting,
control_flow_enum, control_flow_enum,
let_chains, let_chains,
binary_heap_drain_sorted binary_heap_drain_sorted,
fn_traits,
unboxed_closures,
tuple_trait
)] )]
pub mod ai; pub mod ai;

View File

@ -1,7 +1,11 @@
use std::hash::BuildHasherDefault; use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{ use crate::{
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx}, ai::{
casual, choose, finish, important, just, now,
predicate::{every_range, timeout, Chance, EveryRange, Predicate},
seq, until, Action, NpcCtx, State,
},
data::{ data::{
npc::{Brain, PathData, SimulationMode}, npc::{Brain, PathData, SimulationMode},
ReportKind, Sentiment, Sites, ReportKind, Sentiment, Sites,
@ -52,6 +56,12 @@ const CARDINALS: &[Vec2<i32>] = &[
Vec2::new(0, -1), Vec2::new(0, -1),
]; ];
#[derive(Clone)]
struct DefaultState {
socialize_timer: EveryRange,
move_home_timer: Chance<EveryRange>,
}
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> { fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_()); let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default()); let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
@ -222,7 +232,17 @@ fn path_towns(
impl Rule for NpcAi { impl Rule for NpcAi {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|ctx| { // Keep track of the last `SIMULATED_TICK_SKIP` ticks, to know the deltatime
// since the last tick we ran the npc.
let mut last_ticks: VecDeque<_> = [1.0 / 30.0; SIMULATED_TICK_SKIP as usize]
.into_iter()
.collect();
rtstate.bind::<Self, OnTick>(move |ctx| {
last_ticks.push_front(ctx.event.dt);
if last_ticks.len() >= SIMULATED_TICK_SKIP as usize {
last_ticks.pop_back();
}
// Temporarily take the brains of NPCs out of their heads to appease the borrow // Temporarily take the brains of NPCs out of their heads to appease the borrow
// checker // checker
let mut npc_data = { let mut npc_data = {
@ -239,13 +259,20 @@ impl Rule for NpcAi {
let sentiments = std::mem::take(&mut npc.sentiments); let sentiments = std::mem::take(&mut npc.sentiments);
let known_reports = std::mem::take(&mut npc.known_reports); let known_reports = std::mem::take(&mut npc.known_reports);
let brain = npc.brain.take().unwrap_or_else(|| Brain { let brain = npc.brain.take().unwrap_or_else(|| Brain {
action: Box::new(think().repeat()), action: Box::new(think().repeat().with_state(DefaultState {
socialize_timer: every_range(15.0..30.0),
move_home_timer: every_range(400.0..2000.0).chance(0.5),
})),
}); });
(npc_id, controller, inbox, sentiments, known_reports, brain) (npc_id, controller, inbox, sentiments, known_reports, brain)
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}; };
// The sum of the last `SIMULATED_TICK_SKIP` tick deltatimes is the deltatime since
// simulated npcs ran this tick had their ai ran.
let simulated_dt = last_ticks.iter().sum::<f32>();
// Do a little thinking // Do a little thinking
{ {
let data = &*ctx.state.data(); let data = &*ctx.state.data();
@ -267,8 +294,13 @@ impl Rule for NpcAi {
inbox, inbox,
known_reports, known_reports,
sentiments, sentiments,
dt: if matches!(npc.mode, SimulationMode::Loaded) {
ctx.event.dt
} else {
simulated_dt
},
rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()), rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
}); }, &mut ());
}); });
} }
@ -287,20 +319,19 @@ impl Rule for NpcAi {
} }
} }
fn idle() -> impl Action { just(|ctx| ctx.controller.do_idle()).debug(|| "idle") } fn idle<S: State>() -> impl Action<S> { just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle") }
/// Try to walk toward a 3D position without caring for obstacles. /// Try to walk toward a 3D position without caring for obstacles.
fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action { fn goto<S: State>(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
const STEP_DIST: f32 = 24.0; const STEP_DIST: f32 = 24.0;
const WAYPOINT_DIST: f32 = 12.0; const WAYPOINT_DIST: f32 = 12.0;
let mut waypoint = None;
just(move |ctx| { just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
// If we're close to the next waypoint, complete it // If we're close to the next waypoint, complete it
if waypoint.map_or(false, |waypoint: Vec3<f32>| { if waypoint.map_or(false, |waypoint: Vec3<f32>| {
ctx.npc.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2) ctx.npc.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2)
}) { }) {
waypoint = None; *waypoint = None;
} }
// Get the next waypoint on the route toward the goal // Get the next waypoint on the route toward the goal
@ -309,40 +340,36 @@ fn goto(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action {
let len = rpos.magnitude(); let len = rpos.magnitude();
let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST); let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST);
wpos.with_z( wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()))
ctx.world
.sim()
.get_surface_alt_approx(wpos.xy().as_())
.unwrap_or(wpos.z),
)
}); });
ctx.controller.do_goto(*waypoint, speed_factor); ctx.controller.do_goto(*waypoint, speed_factor);
}) })
.repeat() .repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) .stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
})
.with_state(None)
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z)) .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {}) .map(|_, _| {})
} }
/// Try to walk fly a 3D position following the terrain altitude at an offset /// Try to walk fly a 3D position following the terrain altitude at an offset
/// without caring for obstacles. /// without caring for obstacles.
fn goto_flying( fn goto_flying<S: State>(
wpos: Vec3<f32>, wpos: Vec3<f32>,
speed_factor: f32, speed_factor: f32,
goal_dist: f32, goal_dist: f32,
step_dist: f32, step_dist: f32,
waypoint_dist: f32, waypoint_dist: f32,
height_offset: f32, height_offset: f32,
) -> impl Action { ) -> impl Action<S> {
let mut waypoint = None; just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
just(move |ctx| {
// If we're close to the next waypoint, complete it // If we're close to the next waypoint, complete it
if waypoint.map_or(false, |waypoint: Vec3<f32>| { if waypoint.map_or(false, |waypoint: Vec3<f32>| {
ctx.npc.wpos.distance_squared(waypoint) < waypoint_dist.powi(2) ctx.npc.wpos.distance_squared(waypoint) < waypoint_dist.powi(2)
}) { }) {
waypoint = None; *waypoint = None;
} }
// Get the next waypoint on the route toward the goal // Get the next waypoint on the route toward the goal
@ -351,43 +378,41 @@ fn goto_flying(
let len = rpos.magnitude(); let len = rpos.magnitude();
let wpos = ctx.npc.wpos + (rpos / len) * len.min(step_dist); let wpos = ctx.npc.wpos + (rpos / len) * len.min(step_dist);
wpos.with_z( wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()) + height_offset)
ctx.world
.sim()
.get_surface_alt_approx(wpos.xy().as_())
.map(|alt| alt + height_offset)
.unwrap_or(wpos.z),
)
}); });
ctx.controller.do_goto(*waypoint, speed_factor); ctx.controller.do_goto(*waypoint, speed_factor);
}) })
.repeat() .repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) .boxed()
.with_state(None)
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
})
.debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z)) .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
.map(|_| {}) .map(|_, _| {})
} }
/// Try to walk toward a 2D position on the terrain without caring for /// Try to walk toward a 2D position on the surface without caring for
/// obstacles. /// obstacles.
fn goto_2d(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action { fn goto_2d<S: State>(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
now(move |ctx| { now(move |ctx, _| {
let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); let wpos = wpos2d.with_z(ctx.world.sim().get_surface_alt_approx(wpos2d.as_()));
goto(wpos, speed_factor, goal_dist) goto(wpos, speed_factor, goal_dist)
}) })
} }
/// Try to fly toward a 2D position following the terrain altitude at an offset /// Try to fly toward a 2D position following the terrain altitude at an offset
/// without caring for obstacles. /// without caring for obstacles.
fn goto_2d_flying( fn goto_2d_flying<S: State>(
wpos2d: Vec2<f32>, wpos2d: Vec2<f32>,
speed_factor: f32, speed_factor: f32,
goal_dist: f32, goal_dist: f32,
step_dist: f32, step_dist: f32,
waypoint_dist: f32, waypoint_dist: f32,
height_offset: f32, height_offset: f32,
) -> impl Action { ) -> impl Action<S> {
now(move |ctx| { now(move |ctx, _| {
let wpos = wpos2d let wpos = wpos2d
.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0) + height_offset); .with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0) + height_offset);
goto_flying( goto_flying(
@ -401,11 +426,11 @@ fn goto_2d_flying(
}) })
} }
fn traverse_points<F>(mut next_point: F, speed_factor: f32) -> impl Action fn traverse_points<S: State, F>(next_point: F, speed_factor: f32) -> impl Action<S>
where where
F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Clone + Send + Sync + 'static,
{ {
until(move |ctx| { until(move |ctx, next_point: &mut F| {
let wpos = next_point(ctx)?; let wpos = next_point(ctx)?;
let wpos_site = |wpos: Vec2<f32>| { let wpos_site = |wpos: Vec2<f32>| {
@ -437,11 +462,12 @@ where
Some(Either::Right(goto_2d(wpos, speed_factor, 8.0))) Some(Either::Right(goto_2d(wpos, speed_factor, 8.0)))
} }
}) })
.with_state(next_point)
} }
/// Try to travel to a site. Where practical, paths will be taken. /// Try to travel to a site. Where practical, paths will be taken.
fn travel_to_point(wpos: Vec2<f32>, speed_factor: f32) -> impl Action { fn travel_to_point<S: State>(wpos: Vec2<f32>, speed_factor: f32) -> impl Action<S> {
now(move |ctx| { now(move |ctx, _| {
const WAYPOINT: f32 = 48.0; const WAYPOINT: f32 = 48.0;
let start = ctx.npc.wpos.xy(); let start = ctx.npc.wpos.xy();
let diff = wpos - start; let diff = wpos - start;
@ -453,8 +479,8 @@ fn travel_to_point(wpos: Vec2<f32>, speed_factor: f32) -> impl Action {
} }
/// Try to travel to a site. Where practical, paths will be taken. /// Try to travel to a site. Where practical, paths will be taken.
fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action { fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<S> {
now(move |ctx| { now(move |ctx, _| {
let sites = &ctx.state.data().sites; let sites = &ctx.state.data().sites;
let site_wpos = sites.get(tgt_site).map(|site| site.wpos.as_()); let site_wpos = sites.get(tgt_site).map(|site| site.wpos.as_());
@ -540,20 +566,14 @@ fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action {
finish().boxed() finish().boxed()
} }
// Stop the NPC early if we're near the site to prevent huddling around the centre // Stop the NPC early if we're near the site to prevent huddling around the centre
.stop_if(move |ctx| site_wpos.map_or(false, |site_wpos| ctx.npc.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2))) .stop_if(move |ctx: &mut NpcCtx| site_wpos.map_or(false, |site_wpos| ctx.npc.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2)))
}) })
.debug(move || format!("travel_to_site {:?}", tgt_site)) .debug(move || format!("travel_to_site {:?}", tgt_site))
.map(|_| ()) .map(|_, _| ())
} }
// Seconds fn talk_to<S: State>(tgt: Actor, _subject: Option<Subject>) -> impl Action<S> {
fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { now(move |ctx, _| {
let mut timeout = None;
move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time)
}
fn talk_to(tgt: Actor, _subject: Option<Subject>) -> impl Action {
now(move |ctx| {
if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) { if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) {
// Cut off the conversation sometimes to avoid infinite conversations (but only // Cut off the conversation sometimes to avoid infinite conversations (but only
// if the target is an NPC!) TODO: Don't special case this, have // if the target is an NPC!) TODO: Don't special case this, have
@ -598,23 +618,23 @@ fn talk_to(tgt: Actor, _subject: Option<Subject>) -> impl Action {
idle() idle()
.repeat() .repeat()
.stop_if(timeout(wait)) .stop_if(timeout(wait))
.then(just(move |ctx| ctx.controller.say(tgt, comment.clone()))) .then(just(move |ctx, _| ctx.controller.say(tgt, comment.clone())))
.r() .r()
} }
}) })
} }
fn socialize() -> impl Action { fn socialize() -> impl Action<EveryRange> {
now(|ctx| { now(move |ctx, socialize: &mut EveryRange| {
// Skip most socialising actions if we're not loaded // Skip most socialising actions if we're not loaded
if matches!(ctx.npc.mode, SimulationMode::Loaded) && ctx.rng.gen_bool(0.002) { if matches!(ctx.npc.mode, SimulationMode::Loaded) && socialize.should(ctx) {
// Sometimes dance // Sometimes dance
if ctx.rng.gen_bool(0.15) { if ctx.rng.gen_bool(0.15) {
return just(|ctx| ctx.controller.do_dance()) return just(|ctx, _| ctx.controller.do_dance())
.repeat() .repeat()
.stop_if(timeout(6.0)) .stop_if(timeout(6.0))
.debug(|| "dancing") .debug(|| "dancing")
.map(|_| ()) .map(|_, _| ())
.l() .l()
.l(); .l();
// Talk to nearby NPCs // Talk to nearby NPCs
@ -628,17 +648,16 @@ fn socialize() -> impl Action {
return talk_to(other, None) return talk_to(other, None)
// After talking, wait for a while // After talking, wait for a while
.then(idle().repeat().stop_if(timeout(4.0))) .then(idle().repeat().stop_if(timeout(4.0)))
.map(|_| ()) .map(|_, _| ())
.r().l(); .r().l();
} }
} }
idle().r() idle().r()
}) })
} }
fn adventure() -> impl Action { fn adventure() -> impl Action<DefaultState> {
choose(|ctx| { choose(|ctx, _| {
// Choose a random site that's fairly close by // Choose a random site that's fairly close by
if let Some(tgt_site) = ctx if let Some(tgt_site) = ctx
.state .state
@ -670,11 +689,11 @@ fn adventure() -> impl Action {
.map(|ws| ctx.index.sites.get(ws).name().to_string()) .map(|ws| ctx.index.sites.get(ws).name().to_string())
.unwrap_or_default(); .unwrap_or_default();
// Travel to the site // Travel to the site
important(just(move |ctx| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())]))) important(just(move |ctx, _| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
.then(travel_to_site(tgt_site, 0.6)) .then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes // Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
.map(|_| ()) .map(|_, _| ())
.boxed(), .boxed(),
) )
} else { } else {
@ -684,8 +703,8 @@ fn adventure() -> impl Action {
.debug(move || "adventure") .debug(move || "adventure")
} }
fn gather_ingredients() -> impl Action { fn gather_ingredients<S: State>() -> impl Action<S> {
just(|ctx| { just(|ctx, _| {
ctx.controller.do_gather( ctx.controller.do_gather(
&[ &[
ChunkResource::Fruit, ChunkResource::Fruit,
@ -697,8 +716,8 @@ fn gather_ingredients() -> impl Action {
.debug(|| "gather ingredients") .debug(|| "gather ingredients")
} }
fn hunt_animals() -> impl Action { fn hunt_animals<S: State>() -> impl Action<S> {
just(|ctx| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals") just(|ctx, _| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals")
} }
fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> { fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> {
@ -732,10 +751,10 @@ fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
}) })
} }
fn villager(visiting_site: SiteId) -> impl Action { fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
choose(move |ctx| { choose(move |ctx, state: &mut DefaultState| {
// Consider moving home if the home site gets too full // Consider moving home if the home site gets too full
if ctx.rng.gen_bool(0.0001) if state.move_home_timer.should(ctx)
&& let Some(home) = ctx.npc.home && let Some(home) = ctx.npc.home
&& Some(home) == ctx.npc.current_site && Some(home) == ctx.npc.current_site
&& let Some(home_pop_ratio) = ctx.state.data().sites.get(home) && let Some(home_pop_ratio) = ctx.state.data().sites.get(home)
@ -769,20 +788,20 @@ fn villager(visiting_site: SiteId) -> impl Action {
{ {
let site_name = ctx.state.data().sites[new_home].world_site let site_name = ctx.state.data().sites[new_home].world_site
.map(|ws| ctx.index.sites.get(ws).name().to_string()); .map(|ws| ctx.index.sites.get(ws).name().to_string());
return important(just(move |ctx| { return important(just(move |ctx, _| {
if let Some(site_name) = &site_name { if let Some(site_name) = &site_name {
ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())])) ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())]))
} }
}) })
.then(travel_to_site(new_home, 0.5)) .then(travel_to_site(new_home, 0.5))
.then(just(move |ctx| ctx.controller.set_new_home(new_home)))); .then(just(move |ctx, _| ctx.controller.set_new_home(new_home))));
} }
if DayPeriod::from(ctx.time_of_day.0).is_dark() if DayPeriod::from(ctx.time_of_day.0).is_dark()
&& !matches!(ctx.npc.profession(), Some(Profession::Guard)) && !matches!(ctx.npc.profession(), Some(Profession::Guard))
{ {
return important( return important(
now(move |ctx| { now(move |ctx, _| {
if let Some(house_wpos) = ctx if let Some(house_wpos) = ctx
.state .state
.data() .data()
@ -798,19 +817,19 @@ fn villager(visiting_site: SiteId) -> impl Action {
Some(site2.tile_center_wpos(house.root_tile()).as_()) Some(site2.tile_center_wpos(house.root_tile()).as_())
}) })
{ {
just(|ctx| { just(|ctx, _| {
ctx.controller ctx.controller
.say(None, Content::localized("npc-speech-night_time")) .say(None, Content::localized("npc-speech-night_time"))
}) })
.then(travel_to_point(house_wpos, 0.65)) .then(travel_to_point(house_wpos, 0.65))
.debug(|| "walk to house") .debug(|| "walk to house")
.then(socialize().repeat().debug(|| "wait in house")) .then(socialize().repeat().map_state(|state: &mut DefaultState| &mut state.socialize_timer).debug(|| "wait in house"))
.stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) .stop_if(|ctx: &mut NpcCtx| DayPeriod::from(ctx.time_of_day.0).is_light())
.then(just(|ctx| { .then(just(|ctx, _| {
ctx.controller ctx.controller
.say(None, Content::localized("npc-speech-day_time")) .say(None, Content::localized("npc-speech-day_time"))
})) }))
.map(|_| ()) .map(|_, _| ())
.boxed() .boxed()
} else { } else {
finish().boxed() finish().boxed()
@ -829,13 +848,13 @@ fn villager(visiting_site: SiteId) -> impl Action {
let wait_time = ctx.rng.gen_range(10.0..30.0); let wait_time = ctx.rng.gen_range(10.0..30.0);
gather_ingredients().repeat().stop_if(timeout(wait_time)) gather_ingredients().repeat().stop_if(timeout(wait_time))
}) })
.map(|_| ()), .map(|_, _| ()),
); );
} }
} else if matches!(ctx.npc.profession(), Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { } else if matches!(ctx.npc.profession(), Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return casual( return casual(
just(|ctx| { just(|ctx, _| {
ctx.controller ctx.controller
.say(None, Content::localized("npc-speech-start_hunting")) .say(None, Content::localized("npc-speech-start_hunting"))
}) })
@ -845,7 +864,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
let wait_time = ctx.rng.gen_range(30.0..60.0); let wait_time = ctx.rng.gen_range(30.0..60.0);
hunt_animals().repeat().stop_if(timeout(wait_time)) hunt_animals().repeat().stop_if(timeout(wait_time))
}) })
.map(|_| ()), .map(|_, _| ()),
); );
} }
} else if matches!(ctx.npc.profession(), Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) { } else if matches!(ctx.npc.profession(), Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) {
@ -853,9 +872,9 @@ fn villager(visiting_site: SiteId) -> impl Action {
return casual( return casual(
travel_to_point(plaza_wpos, 0.4) travel_to_point(plaza_wpos, 0.4)
.debug(|| "patrol") .debug(|| "patrol")
.interrupt_with(|ctx| { .interrupt_with(move |ctx, _| {
if ctx.rng.gen_bool(0.0003) { if ctx.rng.gen_bool(0.0003) {
Some(just(move |ctx| { Some(just(move |ctx, _| {
ctx.controller ctx.controller
.say(None, Content::localized("npc-speech-guard_thought")) .say(None, Content::localized("npc-speech-guard_thought"))
})) }))
@ -863,13 +882,13 @@ fn villager(visiting_site: SiteId) -> impl Action {
None None
} }
}) })
.map(|_| ()), .map(|_, _| ()),
); );
} }
} else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) } else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8)
{ {
return casual( return casual(
just(|ctx| { just(|ctx, _| {
// Try to direct our speech at nearby actors, if there are any // Try to direct our speech at nearby actors, if there are any
let (target, phrase) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx let (target, phrase) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx
.state .state
@ -890,12 +909,12 @@ fn villager(visiting_site: SiteId) -> impl Action {
.repeat() .repeat()
.stop_if(timeout(60.0)) .stop_if(timeout(60.0))
.debug(|| "sell wares") .debug(|| "sell wares")
.map(|_| ()), .map(|_, _| ()),
); );
} }
// If nothing else needs doing, walk between plazas and socialize // If nothing else needs doing, walk between plazas and socialize
casual(now(move |ctx| { casual(now(move |ctx, _| {
// Choose a plaza in the site we're visiting to walk to // Choose a plaza in the site we're visiting to walk to
if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) { if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
// Walk to the plaza... // Walk to the plaza...
@ -908,9 +927,10 @@ fn villager(visiting_site: SiteId) -> impl Action {
// ...then socialize for some time before moving on // ...then socialize for some time before moving on
.then(socialize() .then(socialize()
.repeat() .repeat()
.map_state(|state: &mut DefaultState| &mut state.socialize_timer)
.stop_if(timeout(ctx.rng.gen_range(30.0..90.0))) .stop_if(timeout(ctx.rng.gen_range(30.0..90.0)))
.debug(|| "wait at plaza")) .debug(|| "wait at plaza"))
.map(|_| ()) .map(|_, _| ())
})) }))
}) })
.debug(move || format!("villager at site {:?}", visiting_site)) .debug(move || format!("villager at site {:?}", visiting_site))
@ -937,9 +957,9 @@ fn follow(npc: NpcId, distance: f32) -> impl Action {
} }
*/ */
fn pilot(ship: common::comp::ship::Body) -> impl Action { fn pilot<S: State>(ship: common::comp::ship::Body) -> impl Action<S> {
// Travel between different towns in a straight line // Travel between different towns in a straight line
now(move |ctx| { now(move |ctx, _| {
let data = &*ctx.state.data(); let data = &*ctx.state.data();
let site = data let site = data
.sites .sites
@ -968,12 +988,12 @@ fn pilot(ship: common::comp::ship::Body) -> impl Action {
} }
}) })
.repeat() .repeat()
.map(|_| ()) .map(|_, _| ())
} }
fn captain() -> impl Action { fn captain<S: State>() -> impl Action<S> {
// For now just randomly travel the sea // For now just randomly travel the sea
now(|ctx| { now(|ctx, _| {
let chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos(); let chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos();
if let Some(chunk) = NEIGHBORS if let Some(chunk) = NEIGHBORS
.into_iter() .into_iter()
@ -999,10 +1019,10 @@ fn captain() -> impl Action {
} }
}) })
.repeat() .repeat()
.map(|_| ()) .map(|_, _| ())
} }
fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> { fn check_inbox<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
loop { loop {
match ctx.inbox.pop_front() { match ctx.inbox.pop_front() {
Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => { Some(NpcInput::Report(report_id)) if !ctx.known_reports.contains(&report_id) => {
@ -1047,8 +1067,10 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
}; };
ctx.known_reports.insert(report_id); ctx.known_reports.insert(report_id);
break Some( break Some(
just(move |ctx| ctx.controller.say(killer, Content::localized(phrase))) just(move |ctx, _| {
.l(), ctx.controller.say(killer, Content::localized(phrase))
})
.l(),
); );
}, },
Some(ReportKind::Death { .. }) => {}, // We don't care about death Some(ReportKind::Death { .. }) => {}, // We don't care about death
@ -1062,7 +1084,7 @@ fn check_inbox(ctx: &mut NpcCtx) -> Option<impl Action> {
} }
} }
fn check_for_enemies(ctx: &mut NpcCtx) -> Option<impl Action> { fn check_for_enemies<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S>> {
// TODO: Instead of checking all nearby actors every tick, it would be more // TODO: Instead of checking all nearby actors every tick, it would be more
// effective to have the actor grid generate a per-tick diff so that we only // effective to have the actor grid generate a per-tick diff so that we only
// need to check new actors in the local area. Be careful though: // need to check new actors in the local area. Be careful though:
@ -1074,17 +1096,17 @@ fn check_for_enemies(ctx: &mut NpcCtx) -> Option<impl Action> {
.npcs .npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos, 24.0) .nearby(Some(ctx.npc_id), ctx.npc.wpos, 24.0)
.find(|actor| ctx.sentiments.toward(*actor).is(Sentiment::ENEMY)) .find(|actor| ctx.sentiments.toward(*actor).is(Sentiment::ENEMY))
.map(|enemy| just(move |ctx| ctx.controller.attack(enemy))) .map(|enemy| just(move |ctx, _| ctx.controller.attack(enemy)))
} }
fn react_to_events(ctx: &mut NpcCtx) -> Option<impl Action> { fn react_to_events<S: State>(ctx: &mut NpcCtx, _: &mut S) -> Option<impl Action<S>> {
check_inbox(ctx) check_inbox::<S>(ctx)
.map(|action| action.boxed()) .map(|action| action.boxed())
.or_else(|| check_for_enemies(ctx).map(|action| action.boxed())) .or_else(|| check_for_enemies(ctx).map(|action| action.boxed()))
} }
fn humanoid() -> impl Action { fn humanoid() -> impl Action<DefaultState> {
choose(|ctx| { choose(|ctx, _| {
if let Some(riding) = &ctx.npc.riding { if let Some(riding) = &ctx.npc.riding {
if riding.steering { if riding.steering {
if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) { if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) {
@ -1100,7 +1122,9 @@ fn humanoid() -> impl Action {
casual(finish()) casual(finish())
} }
} else { } else {
important(socialize()) important(
socialize().map_state(|state: &mut DefaultState| &mut state.socialize_timer),
)
} }
} else { } else {
let action = if matches!( let action = if matches!(
@ -1119,8 +1143,8 @@ fn humanoid() -> impl Action {
}) })
} }
fn bird_large() -> impl Action { fn bird_large() -> impl Action<DefaultState> {
choose(|ctx| { choose(|ctx, _| {
let data = ctx.state.data(); let data = ctx.state.data();
if let Some(home) = ctx.npc.home { if let Some(home) = ctx.npc.home {
let is_home = ctx.npc.current_site.map_or(false, |site| home == site); let is_home = ctx.npc.current_site.map_or(false, |site| home == site);
@ -1161,26 +1185,58 @@ fn bird_large() -> impl Action {
}) })
} }
fn monster() -> impl Action { fn monster() -> impl Action<DefaultState> {
let mut bearing = Vec2::zero(); now(|ctx, bearing: &mut Vec2<f32>| {
now(move |ctx| { *bearing = bearing
bearing = bearing
.map(|e| e + ctx.rng.gen_range(-0.1..0.1)) .map(|e| e + ctx.rng.gen_range(-0.1..0.1))
.try_normalized() .try_normalized()
.unwrap_or_default(); .unwrap_or_default();
goto_2d(ctx.npc.wpos.xy() + bearing * 24.0, 0.7, 8.0) let bearing_dist = 24.0;
.debug(move || format!("Moving with a bearing of {:?}", bearing)) let mut pos = ctx.npc.wpos.xy() + *bearing * bearing_dist;
let is_deep_water = ctx
.world
.sim()
.get_interpolated(pos.as_(), |c| c.alt - c.water_alt)
.unwrap_or(f32::NEG_INFINITY)
< -10.0
&& ctx
.world
.sim()
.get(pos.as_().wpos_to_cpos())
.map_or(false, |c| c.river.is_ocean() || c.river.is_lake());
if !is_deep_water {
goto_2d(pos, 0.7, 8.0)
} else {
*bearing *= -1.0;
pos = ctx.npc.wpos.xy() + *bearing * 24.0;
goto_2d(pos, 0.7, 8.0)
}
// If we are too far away from our goal position we can stop since we aren't going to a specific place.
.stop_if(move |ctx: &mut NpcCtx| {
ctx.npc.wpos.xy().distance_squared(pos) > (bearing_dist + 5.0).powi(2)
})
.debug({
let bearing = *bearing;
move || format!("Moving with a bearing of {:?}", bearing)
})
}) })
.repeat() .repeat()
.map(|_| ()) .with_state(Vec2::<f32>::zero())
.map(|_, _| ())
} }
fn think() -> impl Action { fn think() -> impl Action<DefaultState> {
now(|ctx| match ctx.npc.body { now(|ctx, _| match ctx.npc.body {
common::comp::Body::Humanoid(_) => humanoid().l().l().l(), common::comp::Body::Humanoid(_) => humanoid().l().l().l(),
common::comp::Body::BirdLarge(_) => bird_large().r().l().l(), common::comp::Body::BirdLarge(_) => bird_large().r().l().l(),
_ => match &ctx.npc.role { _ => match &ctx.npc.role {
Role::Civilised(_) => socialize().l().r().l(), Role::Civilised(_) => socialize()
.map_state(|state: &mut DefaultState| &mut state.socialize_timer)
.l()
.r()
.l(),
Role::Monster => monster().r().r().l(), Role::Monster => monster().r().r().l(),
Role::Wild => idle().r(), Role::Wild => idle().r(),
}, },

View File

@ -1,26 +1,29 @@
use crate::{ use crate::{
data::{npc::SimulationMode, Npc}, data::{npc::SimulationMode, Npc},
event::{EventCtx, OnDeath, OnSetup, OnTick}, event::{EventCtx, OnDeath, OnMountVolume, OnSetup, OnTick},
RtState, Rule, RuleError, RtState, Rule, RuleError,
}; };
use common::{ use common::{
comp::{self, Body}, comp::{self, Body},
mounting::{Volume, VolumePos},
rtsim::{Actor, NpcAction, NpcActivity, Personality}, rtsim::{Actor, NpcAction, NpcActivity, Personality},
terrain::CoordinateConversions, terrain::{CoordinateConversions, TerrainChunkSize},
vol::RectVolSize,
}; };
use rand::prelude::*; use rand::prelude::*;
use rand_chacha::ChaChaRng; use rand_chacha::ChaChaRng;
use tracing::{error, warn}; use tracing::{error, warn};
use vek::Vec2; use vek::{Clamp, Vec2};
use world::site::SiteKind; use world::site::SiteKind;
pub struct SimulateNpcs; pub struct SimulateNpcs;
impl Rule for SimulateNpcs { impl Rule for SimulateNpcs {
fn start(rtstate: &mut RtState) -> Result<Self, RuleError> { fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
rtstate.bind::<Self, OnSetup>(on_setup); rtstate.bind(on_setup);
rtstate.bind::<Self, OnDeath>(on_death); rtstate.bind(on_death);
rtstate.bind::<Self, OnTick>(on_tick); rtstate.bind(on_tick);
rtstate.bind(on_mount_volume);
Ok(Self) Ok(Self)
} }
@ -43,6 +46,17 @@ fn on_setup(ctx: EventCtx<SimulateNpcs, OnSetup>) {
} }
} }
fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
let data = &mut *ctx.state.data_mut();
if let VolumePos { kind: Volume::Entity(vehicle), .. } = ctx.event.pos
&& let Some(vehicle) = data.npcs.vehicles.get(vehicle)
&& let Some(Actor::Npc(driver)) = vehicle.driver
&& let Some(driver) = data.npcs.get_mut(driver) {
driver.controller.actions.push(NpcAction::Say(Some(ctx.event.actor), comp::Content::localized("npc-speech-welcome-aboard")))
}
}
fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) { fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
let data = &mut *ctx.state.data_mut(); let data = &mut *ctx.state.data_mut();
@ -238,6 +252,20 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
}, },
None => {}, None => {},
} }
// Make sure NPCs remain on the surface and within the map
npc.wpos = npc
.wpos
.xy()
.clamped(
Vec2::zero(),
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
)
.with_z(
ctx.world
.sim()
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
+ npc.body.flying_height(),
);
} }
// Consume NPC actions // Consume NPC actions
@ -247,13 +275,6 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
NpcAction::Attack(_) => {}, // TODO: Implement simulated combat NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
} }
} }
// Make sure NPCs remain on the surface
npc.wpos.z = ctx
.world
.sim()
.get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32))
.unwrap_or(0.0)
+ npc.body.flying_height();
} }
// Move home if required // Move home if required
@ -271,4 +292,24 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
npc.home = Some(new_home); npc.home = Some(new_home);
} }
} }
for (_, vehicle) in data.npcs.vehicles.iter_mut() {
// Try to keep ships above ground and within the map
if matches!(vehicle.mode, SimulationMode::Simulated) {
vehicle.wpos = vehicle
.wpos
.xy()
.clamped(
Vec2::zero(),
(ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
)
.with_z(
vehicle.wpos.z.max(
ctx.world
.sim()
.get_surface_alt_approx(vehicle.wpos.xy().as_()),
),
);
}
}
} }

View File

@ -203,7 +203,7 @@ impl Client {
| ServerGeneral::ChatMsg(_) | ServerGeneral::ChatMsg(_)
| ServerGeneral::ChatMode(_) | ServerGeneral::ChatMode(_)
| ServerGeneral::SetPlayerEntity(_) | ServerGeneral::SetPlayerEntity(_)
| ServerGeneral::TimeOfDay(_, _, _) | ServerGeneral::TimeOfDay(_, _, _, _)
| ServerGeneral::EntitySync(_) | ServerGeneral::EntitySync(_)
| ServerGeneral::CompSync(_, _) | ServerGeneral::CompSync(_, _)
| ServerGeneral::CreateEntity(_) | ServerGeneral::CreateEntity(_)

View File

@ -44,7 +44,7 @@ use common::{
npc::{self, get_npc_name}, npc::{self, get_npc_name},
outcome::Outcome, outcome::Outcome,
parse_cmd_args, parse_cmd_args,
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay}, resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale},
rtsim::{Actor, Role}, rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
@ -186,6 +186,7 @@ fn do_command(
ServerChatCommand::Sudo => handle_sudo, ServerChatCommand::Sudo => handle_sudo,
ServerChatCommand::Tell => handle_tell, ServerChatCommand::Tell => handle_tell,
ServerChatCommand::Time => handle_time, ServerChatCommand::Time => handle_time,
ServerChatCommand::TimeScale => handle_time_scale,
ServerChatCommand::Tp => handle_tp, ServerChatCommand::Tp => handle_tp,
ServerChatCommand::RtsimTp => handle_rtsim_tp, ServerChatCommand::RtsimTp => handle_rtsim_tp,
ServerChatCommand::RtsimInfo => handle_rtsim_info, ServerChatCommand::RtsimInfo => handle_rtsim_info,
@ -1118,12 +1119,14 @@ fn handle_time(
let mut tod_lazymsg = None; let mut tod_lazymsg = None;
let clients = server.state.ecs().read_storage::<Client>(); let clients = server.state.ecs().read_storage::<Client>();
let calendar = server.state.ecs().read_resource::<Calendar>(); let calendar = server.state.ecs().read_resource::<Calendar>();
let time_scale = server.state.ecs().read_resource::<TimeScale>();
for client in (&clients).join() { for client in (&clients).join() {
let msg = tod_lazymsg.unwrap_or_else(|| { let msg = tod_lazymsg.unwrap_or_else(|| {
client.prepare(ServerGeneral::TimeOfDay( client.prepare(ServerGeneral::TimeOfDay(
TimeOfDay(new_time), TimeOfDay(new_time),
(*calendar).clone(), (*calendar).clone(),
*time, *time,
*time_scale,
)) ))
}); });
let _ = client.send_prepared(&msg); let _ = client.send_prepared(&msg);
@ -1144,6 +1147,64 @@ fn handle_time(
Ok(()) Ok(())
} }
fn handle_time_scale(
server: &mut Server,
client: EcsEntity,
_target: EcsEntity,
args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
let time_scale = server
.state
.ecs_mut()
.get_mut::<TimeScale>()
.expect("Expected time scale to be added.");
if args.is_empty() {
let time_scale = time_scale.0;
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandInfo,
format!("The current time scale is {time_scale}."),
),
);
} else if let Some(scale) = parse_cmd_args!(args, f64) {
time_scale.0 = scale.clamp(0.0001, 1000.0);
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, format!("Set time scale to {scale}.")),
);
// Update all clients with the new TimeOfDay (without this they would have to
// wait for the next 100th tick to receive the update).
let mut tod_lazymsg = None;
let clients = server.state.ecs().read_storage::<Client>();
let time = server.state.ecs().read_resource::<Time>();
let time_of_day = server.state.ecs().read_resource::<TimeOfDay>();
let calendar = server.state.ecs().read_resource::<Calendar>();
for client in (&clients).join() {
let msg = tod_lazymsg.unwrap_or_else(|| {
client.prepare(ServerGeneral::TimeOfDay(
*time_of_day,
(*calendar).clone(),
*time,
TimeScale(scale),
))
});
let _ = client.send_prepared(&msg);
tod_lazymsg = Some(msg);
}
} else {
server.notify_client(
client,
ServerGeneral::server_msg(
ChatType::CommandError,
"Wrong parameter, expected f32.".to_string(),
),
);
}
Ok(())
}
fn handle_health( fn handle_health(
server: &mut Server, server: &mut Server,
_client: EcsEntity, _client: EcsEntity,
@ -1302,6 +1363,7 @@ fn handle_rtsim_info(
let _ = writeln!(&mut info, "-- General Information --"); let _ = writeln!(&mut info, "-- General Information --");
let _ = writeln!(&mut info, "Seed: {}", npc.seed); let _ = writeln!(&mut info, "Seed: {}", npc.seed);
let _ = writeln!(&mut info, "Pos: {:?}", npc.wpos);
let _ = writeln!(&mut info, "Role: {:?}", npc.role); let _ = writeln!(&mut info, "Role: {:?}", npc.role);
let _ = writeln!(&mut info, "Home: {:?}", npc.home); let _ = writeln!(&mut info, "Home: {:?}", npc.home);
let _ = writeln!(&mut info, "Faction: {:?}", npc.faction); let _ = writeln!(&mut info, "Faction: {:?}", npc.faction);
@ -1309,6 +1371,11 @@ fn handle_rtsim_info(
let _ = writeln!(&mut info, "-- Status --"); let _ = writeln!(&mut info, "-- Status --");
let _ = writeln!(&mut info, "Current site: {:?}", npc.current_site); let _ = writeln!(&mut info, "Current site: {:?}", npc.current_site);
let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode); let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode);
let _ = writeln!(
&mut info,
"Riding: {:?}",
npc.riding.as_ref().map(|riding| riding.vehicle)
);
let _ = writeln!(&mut info, "-- Action State --"); let _ = writeln!(&mut info, "-- Action State --");
if let Some(brain) = &npc.brain { if let Some(brain) = &npc.brain {
let mut bt = Vec::new(); let mut bt = Vec::new();
@ -1345,7 +1412,7 @@ fn handle_rtsim_npc(
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|s| s.trim().to_lowercase()) .map(|s| s.trim().to_lowercase())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let npc_names = &*common::npc::NPC_NAMES.read();
let rtsim = server.state.ecs().read_resource::<RtSim>(); let rtsim = server.state.ecs().read_resource::<RtSim>();
let data = rtsim.state().data(); let data = rtsim.state().data();
let mut npcs = data let mut npcs = data
@ -1353,7 +1420,7 @@ fn handle_rtsim_npc(
.values() .values()
.enumerate() .enumerate()
.filter(|(idx, npc)| { .filter(|(idx, npc)| {
let tags = [ let mut tags = vec![
npc.profession() npc.profession()
.map(|p| format!("{:?}", p)) .map(|p| format!("{:?}", p))
.unwrap_or_default(), .unwrap_or_default(),
@ -1364,7 +1431,11 @@ fn handle_rtsim_npc(
}, },
format!("{:?}", npc.mode), format!("{:?}", npc.mode),
format!("{}", idx), format!("{}", idx),
npc_names[&npc.body].keyword.clone(),
]; ];
if let Some(species_meta) = npc_names.get_species_meta(&npc.body) {
tags.push(species_meta.keyword.clone());
}
terms terms
.iter() .iter()
.all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim()))) .all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim())))

View File

@ -1,4 +1,4 @@
use specs::{world::WorldExt, Entity as EcsEntity, Join}; use specs::{saveload::MarkerAllocator, world::WorldExt, Entity as EcsEntity, Join};
use vek::*; use vek::*;
use common::{ use common::{
@ -19,19 +19,20 @@ use common::{
link::Is, link::Is,
mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider}, mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
outcome::Outcome, outcome::Outcome,
rtsim::RtSimVehicle,
terrain::{Block, SpriteKind}, terrain::{Block, SpriteKind},
uid::Uid, uid::{Uid, UidAllocator},
vol::ReadVol, vol::ReadVol,
}; };
use common_net::sync::WorldSyncExt; use common_net::sync::WorldSyncExt;
use crate::{state_ext::StateExt, Server, Time}; use crate::{rtsim::RtSim, state_ext::StateExt, Server, Time};
use crate::pet::tame_pet; use crate::pet::tame_pet;
use hashbrown::{HashMap, HashSet}; use hashbrown::{HashMap, HashSet};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde::Deserialize; use serde::Deserialize;
use std::iter::FromIterator; use std::{iter::FromIterator, sync::Arc};
pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) { pub fn handle_lantern(server: &mut Server, entity: EcsEntity, enable: bool) {
let ecs = server.state_mut().ecs(); let ecs = server.state_mut().ecs();
@ -169,11 +170,28 @@ pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: Vo
let maybe_uid = state.ecs().read_storage::<Uid>().get(rider).copied(); let maybe_uid = state.ecs().read_storage::<Uid>().get(rider).copied();
if let Some(rider) = maybe_uid && within_range { if let Some(rider) = maybe_uid && within_range {
let _ = state.link(VolumeMounting { let _link_successful = state.link(VolumeMounting {
pos: volume_pos, pos: volume_pos,
block, block,
rider, rider,
}); }).is_ok();
#[cfg(feature = "worldgen")]
if _link_successful {
let uid_allocator = state.ecs().read_resource::<UidAllocator>();
if let Some(rider_entity) = uid_allocator.retrieve_entity_internal(rider.0)
&& let Some(rider_actor) = state.entity_as_actor(rider_entity)
&& let Some(volume_pos) = volume_pos.try_map_entity(|uid| {
let entity = uid_allocator.retrieve_entity_internal(uid.0)?;
state.read_storage::<RtSimVehicle>().get(entity).map(|v| v.0)
}) {
state.ecs().write_resource::<RtSim>().hook_character_mount_volume(
&state.ecs().read_resource::<Arc<world::World>>(),
state.ecs().read_resource::<world::IndexOwned>().as_index_ref(),
volume_pos,
rider_actor,
);
}
}
} }
} }
} }

View File

@ -5,7 +5,8 @@ pub mod tick;
use atomicwrites::{AtomicFile, OverwriteBehavior}; use atomicwrites::{AtomicFile, OverwriteBehavior};
use common::{ use common::{
grid::Grid, grid::Grid,
rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, mounting::VolumePos,
rtsim::{Actor, ChunkResource, RtSimEntity, RtSimVehicle, VehicleId, WorldSettings},
}; };
use common_ecs::dispatch; use common_ecs::dispatch;
use common_state::BlockDiff; use common_state::BlockDiff;
@ -13,7 +14,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
use enum_map::EnumMap; use enum_map::EnumMap;
use rtsim::{ use rtsim::{
data::{npc::SimulationMode, Data, ReadError}, data::{npc::SimulationMode, Data, ReadError},
event::{OnDeath, OnSetup}, event::{OnDeath, OnMountVolume, OnSetup},
RtState, RtState,
}; };
use specs::DispatcherBuilder; use specs::DispatcherBuilder;
@ -147,6 +148,16 @@ impl RtSim {
path path
} }
pub fn hook_character_mount_volume(
&mut self,
world: &World,
index: IndexRef,
pos: VolumePos<VehicleId>,
actor: Actor,
) {
self.state.emit(OnMountVolume { actor, pos }, world, index)
}
pub fn hook_load_chunk(&mut self, key: Vec2<i32>, max_res: EnumMap<ChunkResource, usize>) { pub fn hook_load_chunk(&mut self, key: Vec2<i32>, max_res: EnumMap<ChunkResource, usize>) {
if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) { if let Some(chunk_state) = self.state.get_resource_mut::<ChunkStates>().0.get_mut(key) {
*chunk_state = Some(LoadedChunkState { max_res }); *chunk_state = Some(LoadedChunkState { max_res });
@ -167,14 +178,26 @@ impl RtSim {
} }
pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) {
if let Some(npc) = self.state.get_data_mut().npcs.get_mut(entity.0) { if let Some(npc) = self
.state
.get_data_mut()
.npcs
.get_mut(entity.0)
.filter(|npc| npc.riding.is_none())
{
npc.mode = SimulationMode::Simulated; npc.mode = SimulationMode::Simulated;
} }
} }
pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) {
if let Some(vehicle) = self.state.get_data_mut().npcs.vehicles.get_mut(entity.0) { let data = self.state.get_data_mut();
if let Some(vehicle) = data.npcs.vehicles.get_mut(entity.0) {
vehicle.mode = SimulationMode::Simulated; vehicle.mode = SimulationMode::Simulated;
if let Some(Actor::Npc(npc)) = vehicle.driver {
if let Some(npc) = data.npcs.get_mut(npc) {
npc.mode = SimulationMode::Simulated;
}
}
} }
} }

View File

@ -6,7 +6,7 @@ use common::{
event::EventBus, event::EventBus,
outcome::Outcome, outcome::Outcome,
region::{Event as RegionEvent, RegionMap}, region::{Event as RegionEvent, RegionMap},
resources::{PlayerPhysicsSettings, Time, TimeOfDay}, resources::{PlayerPhysicsSettings, Time, TimeOfDay, TimeScale},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
uid::Uid, uid::Uid,
vol::RectVolSize, vol::RectVolSize,
@ -31,6 +31,7 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, TimeOfDay>, ReadExpect<'a, TimeOfDay>,
ReadExpect<'a, Time>, ReadExpect<'a, Time>,
ReadExpect<'a, Calendar>, ReadExpect<'a, Calendar>,
ReadExpect<'a, TimeScale>,
ReadExpect<'a, RegionMap>, ReadExpect<'a, RegionMap>,
ReadExpect<'a, UpdateTrackers>, ReadExpect<'a, UpdateTrackers>,
ReadStorage<'a, Pos>, ReadStorage<'a, Pos>,
@ -63,6 +64,7 @@ impl<'a> System<'a> for Sys {
time_of_day, time_of_day,
time, time,
calendar, calendar,
time_scale,
region_map, region_map,
trackers, trackers,
positions, positions,
@ -418,6 +420,7 @@ impl<'a> System<'a> for Sys {
*time_of_day, *time_of_day,
(*calendar).clone(), (*calendar).clone(),
*time, *time,
*time_scale,
)) ))
}); });
// We don't care much about stream errors here since they could just represent // We don't care much about stream errors here since they could just represent

View File

@ -1931,10 +1931,11 @@ impl WorldSim {
} }
/// Get the altitude of the surface, could be water or ground. /// Get the altitude of the surface, could be water or ground.
pub fn get_surface_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> { pub fn get_surface_alt_approx(&self, wpos: Vec2<i32>) -> f32 {
self.get_interpolated(wpos, |chunk| chunk.alt) self.get_interpolated(wpos, |chunk| chunk.alt)
.zip(self.get_interpolated(wpos, |chunk| chunk.water_alt)) .zip(self.get_interpolated(wpos, |chunk| chunk.water_alt))
.map(|(alt, water_alt)| alt.max(water_alt)) .map(|(alt, water_alt)| alt.max(water_alt))
.unwrap_or(CONFIG.sea_level)
} }
pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> { pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> Option<f32> {

View File

@ -258,7 +258,7 @@ fn render_flat(bridge: &Bridge, painter: &Painter) {
let len = bridge.dir.select(aabr.size()); let len = bridge.dir.select(aabr.size());
let true_offset = vault_width + vault_offset; let true_offset = vault_width + vault_offset;
let n = len / true_offset; let n = (len / true_offset).max(1);
let p = len / n; let p = len / n;
let holes = painter let holes = painter