From ca80d831cee72ac500f5e1f8eefcd2401f9bc104 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 15 May 2022 23:29:21 +0100 Subject: [PATCH 001/139] Added rtsim crate, added initial persistence model --- Cargo.lock | 9 +++++ Cargo.toml | 1 + rtsim/Cargo.toml | 9 +++++ rtsim/src/data/helper.rs | 54 +++++++++++++++++++++++++ rtsim/src/data/mod.rs | 10 +++++ rtsim/src/data/version/mod.rs | 72 +++++++++++++++++++++++++++++++++ rtsim/src/data/version/world.rs | 17 ++++++++ rtsim/src/data/world.rs | 1 + rtsim/src/lib.rs | 7 ++++ 9 files changed, 180 insertions(+) create mode 100644 rtsim/Cargo.toml create mode 100644 rtsim/src/data/helper.rs create mode 100644 rtsim/src/data/mod.rs create mode 100644 rtsim/src/data/version/mod.rs create mode 100644 rtsim/src/data/version/world.rs create mode 100644 rtsim/src/data/world.rs create mode 100644 rtsim/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ae40df69e3..a9bc358e08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6884,6 +6884,15 @@ dependencies = [ "veloren-plugin-derive", ] +[[package]] +name = "veloren-rtsim" +version = "0.10.0" +dependencies = [ + "ron 0.7.0", + "serde", + "veloren-common", +] + [[package]] name = "veloren-server" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 5b61b4528a..e428f0d722 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "plugin/api", "plugin/derive", "plugin/rt", + "rtsim", "server", "server/agent", "server-cli", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml new file mode 100644 index 0000000000..370da5a0d1 --- /dev/null +++ b/rtsim/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "veloren-rtsim" +version = "0.10.0" +edition = "2021" + +[dependencies] +common = { package = "veloren-common", path = "../common" } +ron = "0.7" +serde = { version = "1.0.110", features = ["derive"] } diff --git a/rtsim/src/data/helper.rs b/rtsim/src/data/helper.rs new file mode 100644 index 0000000000..703d666d3f --- /dev/null +++ b/rtsim/src/data/helper.rs @@ -0,0 +1,54 @@ +use serde::{ + de::{DeserializeOwned, Error}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +pub struct V(pub T); + +impl Serialize for V { + fn serialize(&self, serializer: S) -> Result { + self.0.serialize(serializer) + } +} + +impl<'de, T: Version> Deserialize<'de> for V { + fn deserialize>(deserializer: D) -> Result { + T::try_from_value_compat(ron::Value::deserialize(deserializer)?) + .map(Self) + .map_err(|e| D::Error::custom(e)) + } +} + +impl> Latest for V { + fn to_unversioned(self) -> U { self.0.to_unversioned() } + + fn from_unversioned(x: U) -> Self { Self(T::from_unversioned(x)) } +} + +pub trait Latest { + fn to_unversioned(self) -> T; + fn from_unversioned(x: T) -> Self; +} + +pub trait Version: Sized + DeserializeOwned { + type Prev: Version; + + fn migrate(prev: Self::Prev) -> Self; + + fn try_from_value_compat(value: ron::Value) -> Result { + value.clone().into_rust().or_else(|e| { + Ok(Self::migrate( + ::Prev::try_from_value_compat(value).map_err(|_| e)?, + )) + }) + } +} + +#[derive(Deserialize)] +pub enum Bottom {} + +impl Version for Bottom { + type Prev = Self; + + fn migrate(prev: Self::Prev) -> Self { prev } +} diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs new file mode 100644 index 0000000000..140a0eb0f0 --- /dev/null +++ b/rtsim/src/data/mod.rs @@ -0,0 +1,10 @@ +pub mod helper; +pub mod version; + +pub mod world; + +pub use self::world::World; + +pub struct Data { + world: World, +} diff --git a/rtsim/src/data/version/mod.rs b/rtsim/src/data/version/mod.rs new file mode 100644 index 0000000000..af563e3403 --- /dev/null +++ b/rtsim/src/data/version/mod.rs @@ -0,0 +1,72 @@ +// # Hey, you! Yes, you! +// +// Don't touch anything in this module, or any sub-modules. No, really. Bad +// stuff will happen. +// +// You're only an exception to this rule if you fulfil the following criteria: +// +// - You *really* understand exactly how the versioning system in `helper.rs` +// works, what assumptions it makes, and how all of this can go badly wrong. +// +// - You are creating a new version of a data structure, and *not* modifying an +// existing one. +// +// - You've thought really carefully about things and you've come to the +// conclusion that there's just no way to add the feature you want to add +// without creating a new version of the data structure in question. +// +// That said, here's how to make a change to one of the structures in this +// module, or submodules. +// +// 1) Duplicate the latest version of the data structure and the `Version` impl +// for it (later versions should be kept at the top of each file). +// +// 2) Rename the duplicated version, incrementing the version number (i.e: V0 +// becomes V1). +// +// 3) Change the `type Prev =` associated type in the new `Version` impl to the +// previous versions' type. You will need to write an implementation of +// `migrate` that migrates from the old version to the new version. +// +// 4) *Change* the existing `Latest` impl so that it uses the new version you +// have created. +// +// 5) If your data structure is contained within another data structure, you +// will need to similarly update the parent data structure too, also +// following these instructions. +// +// The *golden rule* is that, once merged to master, an old version's type must +// not be changed! + +pub mod world; + +use super::{ + helper::{Bottom, Latest, Version, V}, + Data, +}; +use serde::{Deserialize, Serialize}; + +impl Latest for DataV0 { + fn to_unversioned(self) -> Data { + Data { + world: self.world.to_unversioned(), + } + } + + fn from_unversioned(data: Data) -> Self { + Self { + world: Latest::from_unversioned(data.world), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct DataV0 { + world: V, +} + +impl Version for DataV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} diff --git a/rtsim/src/data/version/world.rs b/rtsim/src/data/version/world.rs new file mode 100644 index 0000000000..3c15eeb31e --- /dev/null +++ b/rtsim/src/data/version/world.rs @@ -0,0 +1,17 @@ +use super::*; +use crate::data::World; + +impl Latest for WorldV0 { + fn to_unversioned(self) -> World { World {} } + + fn from_unversioned(world: World) -> Self { Self {} } +} + +#[derive(Serialize, Deserialize)] +pub struct WorldV0 {} + +impl Version for WorldV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} diff --git a/rtsim/src/data/world.rs b/rtsim/src/data/world.rs new file mode 100644 index 0000000000..36b2386888 --- /dev/null +++ b/rtsim/src/data/world.rs @@ -0,0 +1 @@ +pub struct World {} diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs new file mode 100644 index 0000000000..dd3133d228 --- /dev/null +++ b/rtsim/src/lib.rs @@ -0,0 +1,7 @@ +pub mod data; + +/* +pub struct RtState<'a> { + +} +*/ From 0cafafdaa7d3d34c3f13471016b1e0f0604e7420 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 16 May 2022 20:11:49 +0100 Subject: [PATCH 002/139] Began integrating rtsim2 into server --- Cargo.lock | 5 +- rtsim/Cargo.toml | 4 +- rtsim/src/data/actor.rs | 13 + rtsim/src/data/helper.rs | 5 +- rtsim/src/data/mod.rs | 26 +- rtsim/src/data/nature.rs | 1 + rtsim/src/data/version/actor.rs | 85 +++++++ rtsim/src/data/version/mod.rs | 30 ++- rtsim/src/data/version/nature.rs | 17 ++ rtsim/src/data/version/world.rs | 17 -- rtsim/src/data/world.rs | 1 - rtsim/src/gen/mod.rs | 14 ++ rtsim/src/lib.rs | 9 +- server/Cargo.toml | 1 + server/agent/src/data.rs | 2 +- server/src/error.rs | 2 + server/src/events/entity_manipulation.rs | 2 + server/src/lib.rs | 25 ++ server/src/rtsim2/mod.rs | 70 ++++++ server/src/rtsim2/tick.rs | 179 ++++++++++++++ server/src/sys/agent.rs | 12 +- server/src/sys/agent/behavior_tree.rs | 18 +- .../sys/agent/behavior_tree/interaction.rs | 231 +++++++++--------- server/src/sys/terrain.rs | 19 +- 24 files changed, 625 insertions(+), 163 deletions(-) create mode 100644 rtsim/src/data/actor.rs create mode 100644 rtsim/src/data/nature.rs create mode 100644 rtsim/src/data/version/actor.rs create mode 100644 rtsim/src/data/version/nature.rs delete mode 100644 rtsim/src/data/version/world.rs delete mode 100644 rtsim/src/data/world.rs create mode 100644 rtsim/src/gen/mod.rs create mode 100644 server/src/rtsim2/mod.rs create mode 100644 server/src/rtsim2/tick.rs diff --git a/Cargo.lock b/Cargo.lock index a9bc358e08..3a1c24ede9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6888,9 +6888,11 @@ dependencies = [ name = "veloren-rtsim" version = "0.10.0" dependencies = [ - "ron 0.7.0", + "hashbrown 0.12.3", + "ron 0.8.0", "serde", "veloren-common", + "veloren-world", ] [[package]] @@ -6942,6 +6944,7 @@ dependencies = [ "veloren-common-systems", "veloren-network", "veloren-plugin-api", + "veloren-rtsim", "veloren-server-agent", "veloren-world", ] diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 370da5a0d1..662da3835c 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -5,5 +5,7 @@ edition = "2021" [dependencies] common = { package = "veloren-common", path = "../common" } -ron = "0.7" +world = { package = "veloren-world", path = "../world" } +ron = "0.8" serde = { version = "1.0.110", features = ["derive"] } +hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } diff --git a/rtsim/src/data/actor.rs b/rtsim/src/data/actor.rs new file mode 100644 index 0000000000..81893f29f5 --- /dev/null +++ b/rtsim/src/data/actor.rs @@ -0,0 +1,13 @@ +use hashbrown::HashMap; + +#[derive(Copy, Clone, Hash, PartialEq, Eq)] +pub struct ActorId { + pub idx: u32, + pub gen: u32, +} + +pub struct Actor {} + +pub struct Actors { + pub actors: HashMap, +} diff --git a/rtsim/src/data/helper.rs b/rtsim/src/data/helper.rs index 703d666d3f..6ea6b221d1 100644 --- a/rtsim/src/data/helper.rs +++ b/rtsim/src/data/helper.rs @@ -3,6 +3,7 @@ use serde::{ Deserialize, Deserializer, Serialize, Serializer, }; +#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)] pub struct V(pub T); impl Serialize for V { @@ -22,12 +23,12 @@ impl<'de, T: Version> Deserialize<'de> for V { impl> Latest for V { fn to_unversioned(self) -> U { self.0.to_unversioned() } - fn from_unversioned(x: U) -> Self { Self(T::from_unversioned(x)) } + fn from_unversioned(x: &U) -> Self { Self(T::from_unversioned(x)) } } pub trait Latest { fn to_unversioned(self) -> T; - fn from_unversioned(x: T) -> Self; + fn from_unversioned(x: &T) -> Self; } pub trait Version: Sized + DeserializeOwned { diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 140a0eb0f0..fd56da5a5e 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,10 +1,30 @@ pub mod helper; pub mod version; -pub mod world; +pub mod actor; +pub mod nature; -pub use self::world::World; +pub use self::{ + actor::{Actor, ActorId, Actors}, + nature::Nature, +}; + +use self::helper::Latest; +use ron::error::SpannedResult; +use serde::Deserialize; +use std::io::{Read, Write}; pub struct Data { - world: World, + pub nature: Nature, + pub actors: Actors, +} + +impl Data { + pub fn from_reader(reader: R) -> SpannedResult { + ron::de::from_reader(reader).map(version::LatestData::to_unversioned) + } + + pub fn write_to(&self, writer: W) -> Result<(), ron::Error> { + ron::ser::to_writer(writer, &version::LatestData::from_unversioned(self)) + } } diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs new file mode 100644 index 0000000000..dbc53fab83 --- /dev/null +++ b/rtsim/src/data/nature.rs @@ -0,0 +1 @@ +pub struct Nature {} diff --git a/rtsim/src/data/version/actor.rs b/rtsim/src/data/version/actor.rs new file mode 100644 index 0000000000..490e0a14d2 --- /dev/null +++ b/rtsim/src/data/version/actor.rs @@ -0,0 +1,85 @@ +use super::*; +use crate::data::{Actor, ActorId, Actors}; +use hashbrown::HashMap; + +// ActorId + +impl Latest for ActorIdV0 { + fn to_unversioned(self) -> ActorId { + ActorId { + idx: self.idx, + gen: self.gen, + } + } + + fn from_unversioned(id: &ActorId) -> Self { + Self { + idx: id.idx, + gen: id.gen, + } + } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] +pub struct ActorIdV0 { + pub idx: u32, + pub gen: u32, +} + +impl Version for ActorIdV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} + +// Actor + +impl Latest for ActorV0 { + fn to_unversioned(self) -> Actor { Actor {} } + + fn from_unversioned(actor: &Actor) -> Self { Self {} } +} + +#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] +pub struct ActorV0 {} + +impl Version for ActorV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} + +// Actors + +impl Latest for ActorsV0 { + fn to_unversioned(self) -> Actors { + Actors { + actors: self + .actors + .into_iter() + .map(|(k, v)| (k.to_unversioned(), v.to_unversioned())) + .collect(), + } + } + + fn from_unversioned(actors: &Actors) -> Self { + Self { + actors: actors + .actors + .iter() + .map(|(k, v)| (Latest::from_unversioned(k), Latest::from_unversioned(v))) + .collect(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct ActorsV0 { + actors: HashMap, V>, +} + +impl Version for ActorsV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} diff --git a/rtsim/src/data/version/mod.rs b/rtsim/src/data/version/mod.rs index af563e3403..cc86307cee 100644 --- a/rtsim/src/data/version/mod.rs +++ b/rtsim/src/data/version/mod.rs @@ -15,11 +15,17 @@ // conclusion that there's just no way to add the feature you want to add // without creating a new version of the data structure in question. // -// That said, here's how to make a change to one of the structures in this -// module, or submodules. +// Please note that in *very specific* cases, it is possible to make a change to +// an existing data structure that is backward-compatible. For example, adding a +// new variant to an enum or a new field to a struct (where said field is +// annotated with `#[serde(default)]`) is generally considered to be a +// backward-compatible change. +// +// That said, here's how to make a breaking change to one of the structures in +// this module, or submodules. // // 1) Duplicate the latest version of the data structure and the `Version` impl -// for it (later versions should be kept at the top of each file). +// for it (later versions should be kept at the top of each file). // // 2) Rename the duplicated version, incrementing the version number (i.e: V0 // becomes V1). @@ -38,7 +44,8 @@ // The *golden rule* is that, once merged to master, an old version's type must // not be changed! -pub mod world; +pub mod actor; +pub mod nature; use super::{ helper::{Bottom, Latest, Version, V}, @@ -46,23 +53,28 @@ use super::{ }; use serde::{Deserialize, Serialize}; -impl Latest for DataV0 { +pub type LatestData = DataV0; + +impl Latest for LatestData { fn to_unversioned(self) -> Data { Data { - world: self.world.to_unversioned(), + nature: self.nature.to_unversioned(), + actors: self.actors.to_unversioned(), } } - fn from_unversioned(data: Data) -> Self { + fn from_unversioned(data: &Data) -> Self { Self { - world: Latest::from_unversioned(data.world), + nature: Latest::from_unversioned(&data.nature), + actors: Latest::from_unversioned(&data.actors), } } } #[derive(Serialize, Deserialize)] pub struct DataV0 { - world: V, + nature: V, + actors: V, } impl Version for DataV0 { diff --git a/rtsim/src/data/version/nature.rs b/rtsim/src/data/version/nature.rs new file mode 100644 index 0000000000..1a10f0b929 --- /dev/null +++ b/rtsim/src/data/version/nature.rs @@ -0,0 +1,17 @@ +use super::*; +use crate::data::Nature; + +impl Latest for NatureV0 { + fn to_unversioned(self) -> Nature { Nature {} } + + fn from_unversioned(nature: &Nature) -> Self { Self {} } +} + +#[derive(Serialize, Deserialize)] +pub struct NatureV0 {} + +impl Version for NatureV0 { + type Prev = Bottom; + + fn migrate(x: Self::Prev) -> Self { match x {} } +} diff --git a/rtsim/src/data/version/world.rs b/rtsim/src/data/version/world.rs deleted file mode 100644 index 3c15eeb31e..0000000000 --- a/rtsim/src/data/version/world.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; -use crate::data::World; - -impl Latest for WorldV0 { - fn to_unversioned(self) -> World { World {} } - - fn from_unversioned(world: World) -> Self { Self {} } -} - -#[derive(Serialize, Deserialize)] -pub struct WorldV0 {} - -impl Version for WorldV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/data/world.rs b/rtsim/src/data/world.rs deleted file mode 100644 index 36b2386888..0000000000 --- a/rtsim/src/data/world.rs +++ /dev/null @@ -1 +0,0 @@ -pub struct World {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs new file mode 100644 index 0000000000..d1f9032a42 --- /dev/null +++ b/rtsim/src/gen/mod.rs @@ -0,0 +1,14 @@ +use crate::data::{Actors, Data, Nature}; +use hashbrown::HashMap; +use world::World; + +impl Data { + pub fn generate(world: &World) -> Self { + Self { + nature: Nature {}, + actors: Actors { + actors: HashMap::default(), + }, + } + } +} diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index dd3133d228..975ea54412 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,7 +1,10 @@ pub mod data; +pub mod gen; -/* -pub struct RtState<'a> { +use self::data::Data; +use std::sync::Arc; +use world::World; +pub struct RtState { + pub data: Data, } -*/ diff --git a/server/Cargo.toml b/server/Cargo.toml index 0cc75a54e1..6d3659fe7c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -23,6 +23,7 @@ common-state = { package = "veloren-common-state", path = "../common/state" } common-systems = { package = "veloren-common-systems", path = "../common/systems" } common-net = { package = "veloren-common-net", path = "../common/net" } world = { package = "veloren-world", path = "../world" } +rtsim2 = { package = "veloren-rtsim", path = "../rtsim" } network = { package = "veloren-network", path = "../network", features = ["metrics", "compression", "quic"], default-features = false } server-agent = {package = "veloren-server-agent", path = "agent"} diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 0443413009..c6a1f18eec 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -236,7 +236,7 @@ pub struct ReadData<'a> { pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, - pub rtsim_entities: ReadStorage<'a, RtSimEntity>, + // pub rtsim_entities: ReadStorage<'a, RtSimEntity>, pub buffs: ReadStorage<'a, Buffs>, pub combos: ReadStorage<'a, Combo>, pub active_abilities: ReadStorage<'a, ActiveAbilities>, diff --git a/server/src/error.rs b/server/src/error.rs index ac666301b3..e4a5c974cb 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -9,6 +9,7 @@ pub enum Error { StreamErr(StreamError), DatabaseErr(rusqlite::Error), PersistenceErr(PersistenceError), + RtsimError(ron::Error), Other(String), } @@ -41,6 +42,7 @@ impl Display for Error { Self::StreamErr(err) => write!(f, "Stream Error: {}", err), Self::DatabaseErr(err) => write!(f, "Database Error: {}", err), Self::PersistenceErr(err) => write!(f, "Persistence Error: {}", err), + Self::RtsimError(err) => write!(f, "Rtsim Error: {}", err), Self::Other(err) => write!(f, "Error: {}", err), } } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index bddc7423ae..bb9913681e 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -519,6 +519,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt } if should_delete { + /* if let Some(rtsim_entity) = state .ecs() .read_storage::() @@ -530,6 +531,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt .write_resource::() .destroy_entity(rtsim_entity.0); } + */ if let Err(e) = state.delete_entity_recorded(entity) { error!(?e, ?entity, "Failed to delete destroyed entity"); diff --git a/server/src/lib.rs b/server/src/lib.rs index f5f15e6c14..329857f909 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -30,6 +30,7 @@ pub mod persistence; mod pet; pub mod presence; pub mod rtsim; +pub mod rtsim2; pub mod settings; pub mod state_ext; pub mod sys; @@ -548,6 +549,7 @@ impl Server { let connection_handler = ConnectionHandler::new(network, &runtime); // Initiate real-time world simulation + /* #[cfg(feature = "worldgen")] { rtsim::init(&mut state, &world, index.as_index_ref()); @@ -555,6 +557,20 @@ impl Server { } #[cfg(not(feature = "worldgen"))] rtsim::init(&mut state); + */ + + // Init rtsim, loading it from disk if possible + #[cfg(feature = "worldgen")] + { + match rtsim2::RtSim::new(&world, data_dir.to_owned()) { + Ok(rtsim) => state.ecs_mut().insert(rtsim), + Err(err) => { + error!("Failed to load rtsim: {}", err); + return Err(Error::RtsimError(err)); + }, + } + weather::init(&mut state, &world); + } let server_constants = ServerConstants { day_cycle_coefficient: 1440.0 / settings.day_length, @@ -694,11 +710,18 @@ impl Server { add_local_systems(dispatcher_builder); sys::msg::add_server_systems(dispatcher_builder); sys::add_server_systems(dispatcher_builder); + /* #[cfg(feature = "worldgen")] { rtsim::add_server_systems(dispatcher_builder); weather::add_server_systems(dispatcher_builder); } + */ + #[cfg(feature = "worldgen")] + { + rtsim2::add_server_systems(dispatcher_builder); + weather::add_server_systems(dispatcher_builder); + } }, false, Some(&mut state_tick_metrics), @@ -805,6 +828,7 @@ impl Server { }; for entity in to_delete { + /* // Assimilate entities that are part of the real-time world simulation if let Some(rtsim_entity) = self .state @@ -818,6 +842,7 @@ impl Server { .write_resource::() .assimilate_entity(rtsim_entity.0); } + */ if let Err(e) = self.state.delete_entity_recorded(entity) { error!(?e, "Failed to delete agent outside the terrain"); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs new file mode 100644 index 0000000000..11bda686ad --- /dev/null +++ b/server/src/rtsim2/mod.rs @@ -0,0 +1,70 @@ +pub mod tick; + +use common::grid::Grid; +use common_ecs::{dispatch, System}; +use rtsim2::{data::Data, RtState}; +use specs::{DispatcherBuilder, WorldExt}; +use std::{fs::File, io, path::PathBuf, sync::Arc}; +use tracing::info; +use vek::*; +use world::World; + +pub struct RtSim { + file_path: PathBuf, + chunk_states: Grid, // true = loaded + state: RtState, +} + +impl RtSim { + pub fn new(world: &World, data_dir: PathBuf) -> Result { + let file_path = Self::get_file_path(data_dir); + + Ok(Self { + chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| false), + state: RtState { + data: { + info!("Looking for rtsim state in {}...", file_path.display()); + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim state found. Attending to load..."); + Data::from_reader(file)? + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + info!("No rtsim state found. Generating from initial world state..."); + Data::generate(&world) + }, + Err(e) => return Err(e.into()), + } + }, + }, + file_path, + }) + } + + fn get_file_path(mut data_dir: PathBuf) -> PathBuf { + let mut path = std::env::var("VELOREN_RTSIM") + .map(PathBuf::from) + .unwrap_or_else(|_| { + data_dir.push("rtsim"); + data_dir + }); + path.push("state.ron"); + path + } + + pub fn hook_load_chunk(&mut self, key: Vec2) { + if let Some(is_loaded) = self.chunk_states.get_mut(key) { + *is_loaded = true; + } + } + + pub fn hook_unload_chunk(&mut self, key: Vec2) { + if let Some(is_loaded) = self.chunk_states.get_mut(key) { + *is_loaded = false; + } + } +} + +pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { + dispatch::(dispatch_builder, &[]); +} diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs new file mode 100644 index 0000000000..5222dcff0a --- /dev/null +++ b/server/src/rtsim2/tick.rs @@ -0,0 +1,179 @@ +#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out + +use super::*; +use crate::sys::terrain::NpcData; +use common::{ + comp, + event::{EventBus, ServerEvent}, + generation::{BodyBuilder, EntityConfig, EntityInfo}, + resources::{DeltaTime, Time}, +}; +use common_ecs::{Job, Origin, Phase, System}; +use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; +use std::sync::Arc; + +#[derive(Default)] +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Read<'a, DeltaTime>, + Read<'a, Time>, + Read<'a, EventBus>, + WriteExpect<'a, RtSim>, + ReadExpect<'a, Arc>, + ReadExpect<'a, world::IndexOwned>, + ); + + const NAME: &'static str = "rtsim::tick"; + const ORIGIN: Origin = Origin::Server; + const PHASE: Phase = Phase::Create; + + fn run( + _job: &mut Job, + (dt, time, server_event_bus, mut rtsim, world, index): Self::SystemData, + ) { + let rtsim = &mut *rtsim; + // rtsim.tick += 1; + + // Update unloaded rtsim entities, in groups at a time + /* + const TICK_STAGGER: usize = 30; + let entities_per_iteration = rtsim.entities.len() / TICK_STAGGER; + let mut to_reify = Vec::new(); + for (id, entity) in rtsim + .entities + .iter_mut() + .skip((rtsim.tick as usize % TICK_STAGGER) * entities_per_iteration) + .take(entities_per_iteration) + .filter(|(_, e)| !e.is_loaded) + { + if rtsim + .chunk_states + .get(entity.pos.xy()) + .copied() + .unwrap_or(false) + { + to_reify.push(id); + } else { + // Simulate behaviour + if let Some(travel_to) = &entity.controller.travel_to { + // Move towards target at approximate character speed + entity.pos += Vec3::from( + (travel_to.0.xy() - entity.pos.xy()) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * entity.get_body().max_speed_approx() + * entity.controller.speed_factor, + ) * dt; + } + + if let Some(alt) = world + .sim() + .get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) + { + entity.pos.z = alt; + } + } + // entity.tick(&time, &terrain, &world, &index.as_index_ref()); + } + */ + + // Tick entity AI each time if it's loaded + // for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)| + // e.is_loaded) { entity.last_time_ticked = time.0; + // entity.tick(&time, &terrain, &world, &index.as_index_ref()); + // } + + /* + let mut server_emitter = server_event_bus.emitter(); + for id in to_reify { + rtsim.reify_entity(id); + let entity = &rtsim.entities[id]; + let rtsim_entity = Some(RtSimEntity(id)); + + let body = entity.get_body(); + let spawn_pos = terrain + .find_space(entity.pos.map(|e| e.floor() as i32)) + .map(|e| e as f32) + + Vec3::new(0.5, 0.5, body.flying_height()); + + let pos = comp::Pos(spawn_pos); + + let event = if let comp::Body::Ship(ship) = body { + ServerEvent::CreateShip { + pos, + ship, + mountable: false, + agent: Some(comp::Agent::from_body(&body)), + rtsim_entity, + } + } else { + let entity_config_path = entity.get_entity_config(); + let mut loadout_rng = entity.loadout_rng(); + let ad_hoc_loadout = entity.get_adhoc_loadout(); + // Body is rewritten so that body parameters + // are consistent between reifications + let entity_config = EntityConfig::from_asset_expect_owned(entity_config_path) + .with_body(BodyBuilder::Exact(body)); + + let mut entity_info = EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(entity_config_path), &mut loadout_rng) + .with_lazy_loadout(ad_hoc_loadout); + // Merchants can be traded with + if let Some(economy) = entity.get_trade_info(&world, &index) { + entity_info = entity_info + .with_agent_mark(comp::agent::Mark::Merchant) + .with_economy(&economy); + } + match NpcData::from_entity_info(entity_info) { + NpcData::Data { + pos, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + loot, + } => ServerEvent::CreateNpc { + pos, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + anchor: None, + loot, + rtsim_entity, + projectile: None, + }, + // EntityConfig can't represent Waypoints at all + // as of now, and if someone will try to spawn + // rtsim waypoint it is definitely error. + NpcData::Waypoint(_) => unimplemented!(), + } + }; + server_emitter.emit(event); + } + + // Update rtsim with real entity data + for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() { + rtsim + .entities + .get_mut(rtsim_entity.0) + .filter(|e| e.is_loaded) + .map(|entity| { + entity.pos = pos.0; + agent.rtsim_controller = entity.controller.clone(); + }); + } + */ + } +} diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index f2789c6aca..2098dac8bb 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -32,7 +32,7 @@ impl<'a> System<'a> for Sys { Read<'a, EventBus>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, - WriteExpect<'a, RtSim>, + //WriteExpect<'a, RtSim>, ); const NAME: &'static str = "agent"; @@ -41,9 +41,9 @@ impl<'a> System<'a> for Sys { fn run( job: &mut Job, - (read_data, event_bus, mut agents, mut controllers, mut rtsim): Self::SystemData, + (read_data, event_bus, mut agents, mut controllers /* mut rtsim */): Self::SystemData, ) { - let rtsim = &mut *rtsim; + //let rtsim = &mut *rtsim; job.cpu_stats.measure(ParMode::Rayon); ( @@ -161,10 +161,12 @@ impl<'a> System<'a> for Sys { can_fly: body.map_or(false, |b| b.fly_thrust().is_some()), }; let health_fraction = health.map_or(1.0, Health::fraction); + /* let rtsim_entity = read_data .rtsim_entities .get(entity) .and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0)); + */ if traversal_config.can_fly && matches!(body, Some(Body::Ship(_))) { // hack (kinda): Never turn off flight airships @@ -226,7 +228,7 @@ impl<'a> System<'a> for Sys { // inputs. let mut behavior_data = BehaviorData { agent, - rtsim_entity, + // rtsim_entity, agent_data: data, read_data: &read_data, event_emitter: &mut event_emitter, @@ -240,6 +242,7 @@ impl<'a> System<'a> for Sys { debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); }, ); + /* for (agent, rtsim_entity) in (&mut agents, &read_data.rtsim_entities).join() { // Entity must be loaded in as it has an agent component :) // React to all events in the controller @@ -258,5 +261,6 @@ impl<'a> System<'a> for Sys { } } } + */ } } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6a3ebb37a0..eda52faedf 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -41,7 +41,7 @@ pub struct BehaviorData<'a, 'b, 'c> { pub agent: &'a mut Agent, pub agent_data: AgentData<'a>, // TODO: Move rtsim back into AgentData after rtsim2 when it has a separate crate - pub rtsim_entity: Option<&'a RtSimEntity>, + // pub rtsim_entity: Option<&'a RtSimEntity>, pub read_data: &'a ReadData<'a>, pub event_emitter: &'a mut Emitter<'c, ServerEvent>, pub controller: &'a mut Controller, @@ -277,6 +277,7 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { } // Remember this attack if we're an RtSim entity + /* if let Some(attacker_stats) = bdata.rtsim_entity.and(bdata.read_data.stats.get(attacker)) { @@ -284,6 +285,7 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool { .agent .add_fight_to_memory(&attacker_stats.name, bdata.read_data.time.0); } + */ } } } @@ -316,9 +318,11 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool { if let Some(tgt_health) = bdata.read_data.healths.get(target) { // If target is dead, forget them if tgt_health.is_dead { + /* if let Some(tgt_stats) = bdata.rtsim_entity.and(bdata.read_data.stats.get(target)) { bdata.agent.forget_enemy(&tgt_stats.name); } + */ bdata.agent.target = None; return true; } @@ -496,7 +500,7 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool { bdata.controller, bdata.read_data, bdata.event_emitter, - will_ambush(bdata.rtsim_entity, &bdata.agent_data), + will_ambush(/* bdata.rtsim_entity */ None, &bdata.agent_data), ); } else { bdata.agent_data.handle_sounds_heard( @@ -639,7 +643,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, agent_data, - rtsim_entity, + // rtsim_entity, read_data, event_emitter, controller, @@ -746,7 +750,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { controller, read_data, event_emitter, - will_ambush(*rtsim_entity, agent_data), + will_ambush(/* *rtsim_entity */None, agent_data), ); } @@ -764,9 +768,9 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { read_data, event_emitter, rng, - remembers_fight_with(*rtsim_entity, read_data, target), + remembers_fight_with(/* *rtsim_entity */None, read_data, target), ); - remember_fight(*rtsim_entity, read_data, agent, target); + remember_fight(/* *rtsim_entity */ None, read_data, agent, target); } } } @@ -775,6 +779,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { } fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool { + // TODO: implement for rtsim2 agent_data .health .map_or(false, |h| h.current() / h.maximum() > 0.7) @@ -786,6 +791,7 @@ fn remembers_fight_with( read_data: &ReadData, other: EcsEntity, ) -> bool { + // TODO: implement for rtsim2 let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); rtsim_entity.map_or(false, |rtsim_entity| { diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 9d1088287f..3d37cc6d83 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -108,21 +108,17 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { match subject { Subject::Regular => { + /* if let Some(rtsim_entity) = &bdata.rtsim_entity { if matches!(rtsim_entity.kind, RtSimEntityKind::Prisoner) { agent_data.chat_npc("npc-speech-prisoner", event_emitter); - } else if let ( - Some((_travel_to, destination_name)), - Some(rtsim_entity), - ) = - (&agent.rtsim_controller.travel_to, &&bdata.rtsim_entity) - { + } else if let Some((_travel_to, destination_name) = &agent.rtsim_controller.travel_to { let personality = &rtsim_entity.brain.personality; let standard_response_msg = || -> String { if personality.will_ambush { format!( - "I'm heading to {}! Want to come along? We'll \ - make great travel buddies, hehe.", + "I'm heading to {}! Want to come along? We'll make \ + great travel buddies, hehe.", destination_name ) } else if personality @@ -153,118 +149,129 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { )); if rtsim_entity.brain.remembers_character(&tgt_stats.name) { if personality.will_ambush { - "Just follow me a bit more, hehe.".to_string() + format!( + "I'm heading to {}! Want to come along? We'll \ + make great travel buddies, hehe.", + destination_name + ) } else if personality .personality_traits .contains(PersonalityTrait::Extroverted) { - format!( - "Greetings fair {}! It has been far too long \ - since last I saw you. I'm going to {} right \ - now.", - &tgt_stats.name, destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() + if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "Greetings fair {}! It has been far \ + too long since last I saw you. I'm \ + going to {} right now.", + &tgt_stats.name, destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Oh. It's you again.".to_string() + } else { + format!( + "Hi again {}! Unfortunately I'm in a \ + hurry right now. See you!", + &tgt_stats.name + ) + } } else { - format!( - "Hi again {}! Unfortunately I'm in a hurry \ - right now. See you!", - &tgt_stats.name - ) + standard_response_msg() } } else { standard_response_msg() + }; + agent_data.chat_npc(msg, event_emitter); + } + } else*/ + if agent.behavior.can_trade(agent_data.alignment.copied(), by) { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc-speech-merchant_advertisement", + event_emitter, + ); + } else { + let default_msg = "npc-speech-merchant_busy"; + let msg = default_msg/*agent_data.rtsim_entity.map_or(default_msg, |e| { + if e.brain + .personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "npc-speech-merchant_busy_rude" + } else { + default_msg } - } else { - standard_response_msg() + })*/; + agent_data.chat_npc(msg, event_emitter); + } + } else { + let mut rng = thread_rng(); + /*if let Some(extreme_trait) = + agent_data.rtsim_entity.and_then(|e| { + e.brain.personality.random_chat_trait(&mut rng) + }) + { + let msg = match extreme_trait { + PersonalityTrait::Open => { + "npc-speech-villager_open" + }, + PersonalityTrait::Adventurous => { + "npc-speech-villager_adventurous" + }, + PersonalityTrait::Closed => { + "npc-speech-villager_closed" + }, + PersonalityTrait::Conscientious => { + "npc-speech-villager_conscientious" + }, + PersonalityTrait::Busybody => { + "npc-speech-villager_busybody" + }, + PersonalityTrait::Unconscientious => { + "npc-speech-villager_unconscientious" + }, + PersonalityTrait::Extroverted => { + "npc-speech-villager_extroverted" + }, + PersonalityTrait::Introverted => { + "npc-speech-villager_introverted" + }, + PersonalityTrait::Agreeable => { + "npc-speech-villager_agreeable" + }, + PersonalityTrait::Sociable => { + "npc-speech-villager_sociable" + }, + PersonalityTrait::Disagreeable => { + "npc-speech-villager_disagreeable" + }, + PersonalityTrait::Neurotic => { + "npc-speech-villager_neurotic" + }, + PersonalityTrait::Seeker => { + "npc-speech-villager_seeker" + }, + PersonalityTrait::SadLoner => { + "npc-speech-villager_sad_loner" + }, + PersonalityTrait::Worried => { + "npc-speech-villager_worried" + }, + PersonalityTrait::Stable => { + "npc-speech-villager_stable" + }, }; agent_data.chat_npc(msg, event_emitter); - } else if agent - .behavior - .can_trade(agent_data.alignment.copied(), by) + } else*/ { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( - "npc-speech-merchant_advertisement", - event_emitter, - ); - } else { - let default_msg = "npc-speech-merchant_busy"; - let msg = &bdata.rtsim_entity.map_or(default_msg, |e| { - if e.brain - .personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "npc-speech-merchant_busy_rude" - } else { - default_msg - } - }); - agent_data.chat_npc(msg, event_emitter); - } - } else { - let mut rng = thread_rng(); - if let Some(extreme_trait) = &bdata.rtsim_entity.and_then(|e| { - e.brain.personality.random_chat_trait(&mut rng) - }) { - let msg = match extreme_trait { - PersonalityTrait::Open => "npc-speech-villager_open", - PersonalityTrait::Adventurous => { - "npc-speech-villager_adventurous" - }, - PersonalityTrait::Closed => { - "npc-speech-villager_closed" - }, - PersonalityTrait::Conscientious => { - "npc-speech-villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc-speech-villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc-speech-villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc-speech-villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc-speech-villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc-speech-villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc-speech-villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc-speech-villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc-speech-villager_neurotic" - }, - PersonalityTrait::Seeker => { - "npc-speech-villager_seeker" - }, - PersonalityTrait::SadLoner => { - "npc-speech-villager_sad_loner" - }, - PersonalityTrait::Worried => { - "npc-speech-villager_worried" - }, - PersonalityTrait::Stable => { - "npc-speech-villager_stable" - }, - }; - agent_data.chat_npc(msg, event_emitter); - } else { - agent_data.chat_npc("npc-speech-villager", event_emitter); - } + agent_data.chat_npc("npc-speech-villager", event_emitter); } } }, @@ -295,6 +302,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { } }, Subject::Mood => { + /* if let Some(rtsim_entity) = &bdata.rtsim_entity { if !rtsim_entity.brain.remembers_mood() { // TODO: the following code will need a rework to @@ -325,7 +333,9 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { 2 => agent.rtsim_controller.events.push( RtSimEvent::SetMood(Memory { item: MemoryItem::Mood { - state: MoodState::Bad(MoodContext::GoodWeather), + state: MoodState::Bad( + MoodContext::GoodWeather, + ), }, time_to_forget: read_data.time.0 + 86400.0, }), @@ -340,7 +350,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { }; agent_data.chat_npc(msg, event_emitter); } - } + }*/ }, Subject::Location(location) => { if let Some(tgt_pos) = read_data.positions.get(target) { @@ -750,6 +760,7 @@ pub fn handle_inbox_cancel_interactions(bdata: &mut BehaviorData) -> bool { if used { agent.inbox.pop_front(); } + return used; } false } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 3df4ff170e..d7bd86276f 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -10,7 +10,7 @@ use crate::{ chunk_serialize::ChunkSendEntry, client::Client, presence::{Presence, RepositionOnChunkLoad}, - rtsim::RtSim, + rtsim2, settings::Settings, ChunkRequest, Tick, }; @@ -49,6 +49,11 @@ pub type TerrainPersistenceData<'a> = (); pub const SAFE_ZONE_RADIUS: f32 = 200.0; +#[cfg(feature = "worldgen")] +type RtSimData<'a> = WriteExpect<'a, rtsim2::RtSim>; +#[cfg(not(feature = "worldgen"))] +type RtSimData<'a> = (); + /// This system will handle loading generated chunks and unloading /// unneeded chunks. /// 1. Inserts newly generated chunks into the TerrainGrid @@ -73,7 +78,8 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, Write<'a, Vec>, - WriteExpect<'a, RtSim>, + //WriteExpect<'a, RtSim>, + RtSimData<'a>, TerrainPersistenceData<'a>, WriteStorage<'a, Pos>, ReadStorage<'a, Presence>, @@ -105,7 +111,8 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut chunk_requests, - mut rtsim, + //mut rtsim, + mut rtsim2, mut _terrain_persistence, mut positions, presences, @@ -174,7 +181,8 @@ impl<'a> System<'a> for Sys { terrain_changes.modified_chunks.insert(key); } else { terrain_changes.new_chunks.insert(key); - rtsim.hook_load_chunk(key); + #[cfg(feature = "worldgen")] + rtsim2.hook_load_chunk(key); } // Handle chunk supplement @@ -378,7 +386,8 @@ impl<'a> System<'a> for Sys { // TODO: code duplication for chunk insertion between here and state.rs terrain.remove(key).map(|chunk| { terrain_changes.removed_chunks.insert(key); - rtsim.hook_unload_chunk(key); + #[cfg(feature = "worldgen")] + rtsim2.hook_unload_chunk(key); chunk }) }) From d5e324bded1f4cacd4aa42211f54d19d6d1f25b5 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 22 May 2022 19:40:59 +0100 Subject: [PATCH 003/139] Fixed bad comment --- voxygen/src/menu/main/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 21e76f0f9e..26a75bd309 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -148,6 +148,10 @@ impl PlayState for MainMenuState { }, ) .into_owned(), + server::Error::RtsimError(e) => localized_strings + .get("main.servers.rtsim_error") + .to_owned() + .replace("{raw_error}", e.to_string().as_str()), server::Error::Other(e) => localized_strings .get_msg_ctx("main-servers-other_error", &i18n::fluent_args! { "raw_error" => e, From c168ff2f9b4448d90e0c439cbd67dd0152bfb242 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 8 Aug 2022 23:15:45 +0100 Subject: [PATCH 004/139] Added rtsim saving, chunk resources, chunk resource depletion --- Cargo.lock | 27 ++++++ client/src/lib.rs | 1 + common/Cargo.toml | 2 + common/src/generation.rs | 3 + common/src/rtsim.rs | 8 ++ common/src/terrain/block.rs | 21 +++++ common/state/src/state.rs | 25 +++-- rtsim/Cargo.toml | 2 + rtsim/src/data/actor.rs | 5 +- rtsim/src/data/helper.rs | 55 ----------- rtsim/src/data/mod.rs | 10 +- rtsim/src/data/nature.rs | 45 ++++++++- rtsim/src/data/version/actor.rs | 85 ----------------- rtsim/src/data/version/mod.rs | 84 ----------------- rtsim/src/data/version/nature.rs | 17 ---- rtsim/src/gen/mod.rs | 2 +- server/Cargo.toml | 1 + server/src/chunk_generator.rs | 18 +++- server/src/lib.rs | 23 ++++- server/src/rtsim2/mod.rs | 151 +++++++++++++++++++++++++++---- server/src/rtsim2/tick.rs | 11 ++- server/src/state_ext.rs | 7 +- server/src/sys/terrain.rs | 7 +- world/Cargo.toml | 1 + world/src/canvas.rs | 12 ++- world/src/lib.rs | 38 +++++++- 26 files changed, 372 insertions(+), 289 deletions(-) delete mode 100644 rtsim/src/data/helper.rs delete mode 100644 rtsim/src/data/version/actor.rs delete mode 100644 rtsim/src/data/version/mod.rs delete mode 100644 rtsim/src/data/version/nature.rs diff --git a/Cargo.lock b/Cargo.lock index 3a1c24ede9..2ce962ac5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1839,6 +1839,27 @@ dependencies = [ "syn 1.0.100", ] +[[package]] +name = "enum-map" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" +dependencies = [ + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.100", +] + [[package]] name = "enumset" version = "1.0.11" @@ -6655,6 +6676,8 @@ dependencies = [ "crossbeam-utils 0.8.11", "csv", "dot_vox", + "enum-iterator 1.1.3", + "enum-map", "fxhash", "hashbrown 0.12.3", "indexmap", @@ -6888,9 +6911,11 @@ dependencies = [ name = "veloren-rtsim" version = "0.10.0" dependencies = [ + "enum-map", "hashbrown 0.12.3", "ron 0.8.0", "serde", + "vek 0.15.8", "veloren-common", "veloren-world", ] @@ -6907,6 +6932,7 @@ dependencies = [ "chrono-tz", "crossbeam-channel", "drop_guard", + "enum-map", "enumset", "futures-util", "hashbrown 0.12.3", @@ -7120,6 +7146,7 @@ dependencies = [ "csv", "deflate", "enum-iterator 1.1.3", + "enum-map", "fallible-iterator", "flate2", "fxhash", diff --git a/client/src/lib.rs b/client/src/lib.rs index f7c527bb36..9c5ee7d6a5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1855,6 +1855,7 @@ impl Client { true, None, &self.connected_server_constants, + |_, _, _, _| {}, ); // TODO: avoid emitting these in the first place let _ = self diff --git a/common/Cargo.toml b/common/Cargo.toml index 7288871985..8ff5bd5ee9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -26,6 +26,8 @@ common-base = { package = "veloren-common-base", path = "base" } serde = { version = "1.0.110", features = ["derive", "rc"] } # Util +enum-iterator = "1.1.3" +enum-map = "2.4" vek = { version = "0.15.8", features = ["serde"] } cfg-if = "1.0.0" chrono = "0.4.22" diff --git a/common/src/generation.rs b/common/src/generation.rs index 764165c793..e12864c2c8 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -8,7 +8,9 @@ use crate::{ lottery::LootSpec, npc::{self, NPC_NAMES}, trade::SiteInformation, + rtsim, }; +use enum_map::EnumMap; use serde::Deserialize; use vek::*; @@ -449,6 +451,7 @@ impl EntityInfo { #[derive(Default)] pub struct ChunkSupplement { pub entities: Vec, + pub rtsim_max_resources: EnumMap, } impl ChunkSupplement { diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index d8ff8f0423..1dc03a0250 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -4,6 +4,7 @@ // module in `server`. use specs::Component; +use serde::{Serialize, Deserialize}; use vek::*; use crate::comp::dialogue::MoodState; @@ -82,3 +83,10 @@ impl RtSimController { } } } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] +pub enum ChunkResource { + Grass, + Flax, + Cotton, +} diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 6e8669586b..614b424bb6 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -4,6 +4,7 @@ use crate::{ consts::FRIC_GROUND, lottery::LootSpec, make_case_elim, + rtsim, }; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -195,6 +196,26 @@ impl Block { } } + /// Returns the rtsim resource, if any, that this block corresponds to. If you want the scarcity of a block to change with rtsim's resource depletion tracking, you can do so by editing this function. + #[inline] + pub fn get_rtsim_resource(&self) -> Option { + match self.get_sprite()? { + SpriteKind::LongGrass + | SpriteKind::MediumGrass + | SpriteKind::ShortGrass + | SpriteKind::LargeGrass + | SpriteKind::GrassSnow + | SpriteKind::GrassBlue + | SpriteKind::SavannaGrass + | SpriteKind::TallSavannaGrass + | SpriteKind::RedSavannaGrass + | SpriteKind::JungleRedGrass => Some(rtsim::ChunkResource::Grass), + SpriteKind::WildFlax => Some(rtsim::ChunkResource::Flax), + SpriteKind::Cotton => Some(rtsim::ChunkResource::Cotton), + _ => None, + } + } + #[inline] pub fn get_glow(&self) -> Option { match self.kind() { diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 6bc741ee84..1a3cf2b458 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -524,7 +524,12 @@ impl State { } // Apply terrain changes - pub fn apply_terrain_changes(&self) { self.apply_terrain_changes_internal(false); } + pub fn apply_terrain_changes( + &self, + mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + ) { + self.apply_terrain_changes_internal(false, block_update); + } /// `during_tick` is true if and only if this is called from within /// [State::tick]. @@ -534,7 +539,11 @@ impl State { /// from within both the client and the server ticks, right after /// handling terrain messages; currently, client sets it to true and /// server to false. - fn apply_terrain_changes_internal(&self, during_tick: bool) { + fn apply_terrain_changes_internal( + &self, + during_tick: bool, + mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + ) { span!( _guard, "apply_terrain_changes", @@ -575,14 +584,17 @@ impl State { } // Apply block modifications // Only include in `TerrainChanges` if successful - modified_blocks.retain(|pos, block| { - let res = terrain.set(*pos, *block); + modified_blocks.retain(|pos, new_block| { + let res = terrain.map(*pos, |old_block| { + block_update(&self.ecs, *pos, old_block, *new_block); + *new_block + }); if let (&Ok(old_block), true) = (&res, during_tick) { // NOTE: If the changes are applied during the tick, we push the *old* value as // the modified block (since it otherwise can't be recovered after the tick). // Otherwise, the changes will be applied after the tick, so we push the *new* // value. - *block = old_block; + *new_block = old_block; } res.is_ok() }); @@ -597,6 +609,7 @@ impl State { update_terrain_and_regions: bool, mut metrics: Option<&mut StateTickMetrics>, server_constants: &ServerConstants, + block_update: impl FnMut(&specs::World, Vec3, Block, Block), ) { span!(_guard, "tick", "State::tick"); @@ -643,7 +656,7 @@ impl State { drop(guard); if update_terrain_and_regions { - self.apply_terrain_changes_internal(true); + self.apply_terrain_changes_internal(true, block_update); } // Process local events diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 662da3835c..504d1d50cd 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -9,3 +9,5 @@ world = { package = "veloren-world", path = "../world" } ron = "0.8" serde = { version = "1.0.110", features = ["derive"] } hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } +enum-map = { version = "2.4", features = ["serde"] } +vek = { version = "0.15.8", features = ["serde"] } diff --git a/rtsim/src/data/actor.rs b/rtsim/src/data/actor.rs index 81893f29f5..88d3e58ee3 100644 --- a/rtsim/src/data/actor.rs +++ b/rtsim/src/data/actor.rs @@ -1,13 +1,16 @@ use hashbrown::HashMap; +use serde::{Serialize, Deserialize}; -#[derive(Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct ActorId { pub idx: u32, pub gen: u32, } +#[derive(Clone, Serialize, Deserialize)] pub struct Actor {} +#[derive(Clone, Serialize, Deserialize)] pub struct Actors { pub actors: HashMap, } diff --git a/rtsim/src/data/helper.rs b/rtsim/src/data/helper.rs deleted file mode 100644 index 6ea6b221d1..0000000000 --- a/rtsim/src/data/helper.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{ - de::{DeserializeOwned, Error}, - Deserialize, Deserializer, Serialize, Serializer, -}; - -#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)] -pub struct V(pub T); - -impl Serialize for V { - fn serialize(&self, serializer: S) -> Result { - self.0.serialize(serializer) - } -} - -impl<'de, T: Version> Deserialize<'de> for V { - fn deserialize>(deserializer: D) -> Result { - T::try_from_value_compat(ron::Value::deserialize(deserializer)?) - .map(Self) - .map_err(|e| D::Error::custom(e)) - } -} - -impl> Latest for V { - fn to_unversioned(self) -> U { self.0.to_unversioned() } - - fn from_unversioned(x: &U) -> Self { Self(T::from_unversioned(x)) } -} - -pub trait Latest { - fn to_unversioned(self) -> T; - fn from_unversioned(x: &T) -> Self; -} - -pub trait Version: Sized + DeserializeOwned { - type Prev: Version; - - fn migrate(prev: Self::Prev) -> Self; - - fn try_from_value_compat(value: ron::Value) -> Result { - value.clone().into_rust().or_else(|e| { - Ok(Self::migrate( - ::Prev::try_from_value_compat(value).map_err(|_| e)?, - )) - }) - } -} - -#[derive(Deserialize)] -pub enum Bottom {} - -impl Version for Bottom { - type Prev = Self; - - fn migrate(prev: Self::Prev) -> Self { prev } -} diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index fd56da5a5e..d4dd3d97e9 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,6 +1,3 @@ -pub mod helper; -pub mod version; - pub mod actor; pub mod nature; @@ -11,9 +8,10 @@ pub use self::{ use self::helper::Latest; use ron::error::SpannedResult; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; use std::io::{Read, Write}; +#[derive(Clone, Serialize, Deserialize)] pub struct Data { pub nature: Nature, pub actors: Actors, @@ -21,10 +19,10 @@ pub struct Data { impl Data { pub fn from_reader(reader: R) -> SpannedResult { - ron::de::from_reader(reader).map(version::LatestData::to_unversioned) + ron::de::from_reader(reader) } pub fn write_to(&self, writer: W) -> Result<(), ron::Error> { - ron::ser::to_writer(writer, &version::LatestData::from_unversioned(self)) + ron::ser::to_writer(writer, self) } } diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index dbc53fab83..9fccf7ad2a 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -1 +1,44 @@ -pub struct Nature {} +use serde::{Serialize, Deserialize}; +use enum_map::EnumMap; +use common::{ + grid::Grid, + rtsim::ChunkResource, +}; +use world::World; +use vek::*; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Nature { + chunks: Grid, +} + +impl Nature { + pub fn generate(world: &World) -> Self { + Self { + chunks: Grid::populate_from( + world.sim().get_size().map(|e| e as i32), + |pos| Chunk { + res: EnumMap::<_, f32>::default().map(|_, _| 1.0), + }, + ), + } + } + + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.chunks + .get(key) + .map(|c| c.res) + .unwrap_or_default() + } + pub fn set_chunk_resources(&mut self, key: Vec2, res: EnumMap) { + if let Some(chunk) = self.chunks.get_mut(key) { + chunk.res = res; + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Chunk { + res: EnumMap, +} diff --git a/rtsim/src/data/version/actor.rs b/rtsim/src/data/version/actor.rs deleted file mode 100644 index 490e0a14d2..0000000000 --- a/rtsim/src/data/version/actor.rs +++ /dev/null @@ -1,85 +0,0 @@ -use super::*; -use crate::data::{Actor, ActorId, Actors}; -use hashbrown::HashMap; - -// ActorId - -impl Latest for ActorIdV0 { - fn to_unversioned(self) -> ActorId { - ActorId { - idx: self.idx, - gen: self.gen, - } - } - - fn from_unversioned(id: &ActorId) -> Self { - Self { - idx: id.idx, - gen: id.gen, - } - } -} - -#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ActorIdV0 { - pub idx: u32, - pub gen: u32, -} - -impl Version for ActorIdV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} - -// Actor - -impl Latest for ActorV0 { - fn to_unversioned(self) -> Actor { Actor {} } - - fn from_unversioned(actor: &Actor) -> Self { Self {} } -} - -#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ActorV0 {} - -impl Version for ActorV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} - -// Actors - -impl Latest for ActorsV0 { - fn to_unversioned(self) -> Actors { - Actors { - actors: self - .actors - .into_iter() - .map(|(k, v)| (k.to_unversioned(), v.to_unversioned())) - .collect(), - } - } - - fn from_unversioned(actors: &Actors) -> Self { - Self { - actors: actors - .actors - .iter() - .map(|(k, v)| (Latest::from_unversioned(k), Latest::from_unversioned(v))) - .collect(), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct ActorsV0 { - actors: HashMap, V>, -} - -impl Version for ActorsV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/data/version/mod.rs b/rtsim/src/data/version/mod.rs deleted file mode 100644 index cc86307cee..0000000000 --- a/rtsim/src/data/version/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -// # Hey, you! Yes, you! -// -// Don't touch anything in this module, or any sub-modules. No, really. Bad -// stuff will happen. -// -// You're only an exception to this rule if you fulfil the following criteria: -// -// - You *really* understand exactly how the versioning system in `helper.rs` -// works, what assumptions it makes, and how all of this can go badly wrong. -// -// - You are creating a new version of a data structure, and *not* modifying an -// existing one. -// -// - You've thought really carefully about things and you've come to the -// conclusion that there's just no way to add the feature you want to add -// without creating a new version of the data structure in question. -// -// Please note that in *very specific* cases, it is possible to make a change to -// an existing data structure that is backward-compatible. For example, adding a -// new variant to an enum or a new field to a struct (where said field is -// annotated with `#[serde(default)]`) is generally considered to be a -// backward-compatible change. -// -// That said, here's how to make a breaking change to one of the structures in -// this module, or submodules. -// -// 1) Duplicate the latest version of the data structure and the `Version` impl -// for it (later versions should be kept at the top of each file). -// -// 2) Rename the duplicated version, incrementing the version number (i.e: V0 -// becomes V1). -// -// 3) Change the `type Prev =` associated type in the new `Version` impl to the -// previous versions' type. You will need to write an implementation of -// `migrate` that migrates from the old version to the new version. -// -// 4) *Change* the existing `Latest` impl so that it uses the new version you -// have created. -// -// 5) If your data structure is contained within another data structure, you -// will need to similarly update the parent data structure too, also -// following these instructions. -// -// The *golden rule* is that, once merged to master, an old version's type must -// not be changed! - -pub mod actor; -pub mod nature; - -use super::{ - helper::{Bottom, Latest, Version, V}, - Data, -}; -use serde::{Deserialize, Serialize}; - -pub type LatestData = DataV0; - -impl Latest for LatestData { - fn to_unversioned(self) -> Data { - Data { - nature: self.nature.to_unversioned(), - actors: self.actors.to_unversioned(), - } - } - - fn from_unversioned(data: &Data) -> Self { - Self { - nature: Latest::from_unversioned(&data.nature), - actors: Latest::from_unversioned(&data.actors), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct DataV0 { - nature: V, - actors: V, -} - -impl Version for DataV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/data/version/nature.rs b/rtsim/src/data/version/nature.rs deleted file mode 100644 index 1a10f0b929..0000000000 --- a/rtsim/src/data/version/nature.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; -use crate::data::Nature; - -impl Latest for NatureV0 { - fn to_unversioned(self) -> Nature { Nature {} } - - fn from_unversioned(nature: &Nature) -> Self { Self {} } -} - -#[derive(Serialize, Deserialize)] -pub struct NatureV0 {} - -impl Version for NatureV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index d1f9032a42..f1ce167d1d 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -5,7 +5,7 @@ use world::World; impl Data { pub fn generate(world: &World) -> Self { Self { - nature: Nature {}, + nature: Nature::generate(world), actors: Actors { actors: HashMap::default(), }, diff --git a/server/Cargo.toml b/server/Cargo.toml index 6d3659fe7c..38b1ea391f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -64,6 +64,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253 slab = "0.4" rand_distr = "0.4.0" enumset = "1.0.8" +enum-map = "2.4" noise = { version = "0.7", default-features = false } censor = "0.2" diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index e5a28f3315..f5b34e7f4a 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,4 +1,7 @@ -use crate::metrics::ChunkGenMetrics; +use crate::{ + metrics::ChunkGenMetrics, + rtsim2::RtSim, +}; #[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; use common::{ @@ -44,6 +47,10 @@ impl ChunkGenerator { key: Vec2, slowjob_pool: &SlowJobPool, world: Arc, + #[cfg(feature = "worldgen")] + rtsim: &RtSim, + #[cfg(not(feature = "worldgen"))] + rtsim: &(), index: IndexOwned, time: (TimeOfDay, Calendar), ) { @@ -56,10 +63,17 @@ impl ChunkGenerator { v.insert(Arc::clone(&cancel)); let chunk_tx = self.chunk_tx.clone(); self.metrics.chunks_requested.inc(); + + // Get state for this chunk from rtsim + #[cfg(feature = "worldgen")] + let rtsim_resources = Some(rtsim.get_chunk_resources(key)); + #[cfg(not(feature = "worldgen"))] + let rtsim_resources = None; + slowjob_pool.spawn("CHUNK_GENERATOR", move || { let index = index.as_index_ref(); let payload = world - .generate_chunk(index, key, || cancel.load(Ordering::Relaxed), Some(time)) + .generate_chunk(index, key, rtsim_resources, || cancel.load(Ordering::Relaxed), Some(time)) // FIXME: Since only the first entity who cancels a chunk is notified, we end up // delaying chunk re-requests for up to 3 seconds for other clients, which isn't // great. We *could* store all the other requesting clients here, but it could diff --git a/server/src/lib.rs b/server/src/lib.rs index 329857f909..df1dca17e4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -82,7 +82,7 @@ use common::{ rtsim::RtSimEntity, shared_server_config::ServerConstants, slowjob::SlowJobPool, - terrain::{TerrainChunk, TerrainChunkSize}, + terrain::{TerrainChunk, TerrainChunkSize, Block}, vol::RectRasterableVol, }; use common_ecs::run_now; @@ -342,6 +342,7 @@ impl Server { pool.configure("CHUNK_DROP", |_n| 1); pool.configure("CHUNK_GENERATOR", |n| n / 2 + n / 4); pool.configure("CHUNK_SERIALIZER", |n| n / 2); + pool.configure("RTSIM_SAVE", |_| 1); } state .ecs_mut() @@ -700,6 +701,13 @@ impl Server { let before_state_tick = Instant::now(); + fn on_block_update(ecs: &specs::World, wpos: Vec3, old_block: Block, new_block: Block) { + // When a resource block updates, inform rtsim + if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { + ecs.write_resource::().hook_block_update(wpos, old_block, new_block); + } + } + // 4) Tick the server's LocalState. // 5) Fetch any generated `TerrainChunk`s and insert them into the terrain. // in sys/terrain.rs @@ -726,6 +734,7 @@ impl Server { false, Some(&mut state_tick_metrics), &self.server_constants, + on_block_update, ); let before_handle_events = Instant::now(); @@ -749,7 +758,7 @@ impl Server { self.state.update_region_map(); // NOTE: apply_terrain_changes sends the *new* value since it is not being // synchronized during the tick. - self.state.apply_terrain_changes(); + self.state.apply_terrain_changes(on_block_update); let before_sync = Instant::now(); @@ -994,6 +1003,10 @@ impl Server { let mut chunk_generator = ecs.write_resource::(); let client = ecs.read_storage::(); let mut terrain = ecs.write_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); // Cancel all pending chunks. chunk_generator.cancel_all(); @@ -1009,6 +1022,7 @@ impl Server { pos, &slow_jobs, Arc::clone(world), + &rtsim, index.clone(), ( *ecs.read_resource::(), @@ -1172,11 +1186,16 @@ impl Server { pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2) { let ecs = self.state.ecs(); let slow_jobs = ecs.read_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); ecs.write_resource::().generate_chunk( Some(entity), key, &slow_jobs, Arc::clone(&self.world), + &rtsim, self.index.clone(), ( *ecs.read_resource::(), diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 11bda686ad..f0aaaf0693 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -1,17 +1,31 @@ pub mod tick; -use common::grid::Grid; +use common::{ + grid::Grid, + slowjob::SlowJobPool, + rtsim::ChunkResource, + terrain::{TerrainChunk, Block}, + vol::RectRasterableVol, +}; use common_ecs::{dispatch, System}; use rtsim2::{data::Data, RtState}; use specs::{DispatcherBuilder, WorldExt}; -use std::{fs::File, io, path::PathBuf, sync::Arc}; -use tracing::info; +use std::{ + fs::{self, File}, + path::PathBuf, + sync::Arc, + time::Instant, + io::{self, Write}, +}; +use enum_map::EnumMap; +use tracing::{error, warn, info}; use vek::*; use world::World; pub struct RtSim { file_path: PathBuf, - chunk_states: Grid, // true = loaded + last_saved: Option, + chunk_states: Grid>, state: RtState, } @@ -20,20 +34,46 @@ impl RtSim { let file_path = Self::get_file_path(data_dir); Ok(Self { - chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| false), + chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| None), + last_saved: None, state: RtState { data: { info!("Looking for rtsim state in {}...", file_path.display()); - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim state found. Attending to load..."); - Data::from_reader(file)? - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => { - info!("No rtsim state found. Generating from initial world state..."); - Data::generate(&world) - }, - Err(e) => return Err(e.into()), + 'load: { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim state found. Attempting to load..."); + match Data::from_reader(file) { + Ok(data) => { info!("Rtsim state loaded."); break 'load data }, + Err(e) => { + error!("Rtsim state failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("ron_backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!("Failed rtsim state was moved to {}", backup_path.display()); + info!("A fresh rtsim state will now be generated."); + break; + } + i += 1; + } + }, + } + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => + info!("No rtsim state found. Generating from initial world state..."), + Err(e) => return Err(e.into()), + } + + let data = Data::generate(&world); + info!("Rtsim state generated."); + data } }, }, @@ -52,17 +92,88 @@ impl RtSim { path } - pub fn hook_load_chunk(&mut self, key: Vec2) { - if let Some(is_loaded) = self.chunk_states.get_mut(key) { - *is_loaded = true; + pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { + if let Some(chunk_state) = self.chunk_states.get_mut(key) { + *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(is_loaded) = self.chunk_states.get_mut(key) { - *is_loaded = false; + if let Some(chunk_state) = self.chunk_states.get_mut(key) { + *chunk_state = None; } } + + pub fn save(&mut self, slowjob_pool: &SlowJobPool) { + info!("Beginning rtsim state save..."); + let file_path = self.file_path.clone(); + let data = self.state.data.clone(); + info!("Starting rtsim save job..."); + // TODO: Use slow job + // slowjob_pool.spawn("RTSIM_SAVE", move || { + std::thread::spawn(move || { + let tmp_file_name = "state_tmp.ron"; + if let Err(e) = file_path + .parent() + .map(|dir| { + fs::create_dir_all(dir)?; + // We write to a temporary file and then rename to avoid corruption. + Ok(dir.join(tmp_file_name)) + }) + .unwrap_or_else(|| Ok(tmp_file_name.into())) + .and_then(|tmp_file_path| { + Ok((File::create(&tmp_file_path)?, tmp_file_path)) + }) + .map_err(|e: io::Error| ron::Error::from(e)) + .and_then(|(mut file, tmp_file_path)| { + info!("Writing rtsim state to file..."); + data.write_to(&mut file)?; + file.flush()?; + drop(file); + fs::rename(tmp_file_path, file_path)?; + info!("Rtsim state saved."); + Ok(()) + }) + { + error!("Saving rtsim state failed: {}", e); + } + }); + self.last_saved = Some(Instant::now()); + } + + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.state.data.nature.get_chunk_resources(key) + } + pub fn hook_block_update(&mut self, wpos: Vec3, old_block: Block, new_block: Block) { + let key = wpos + .xy() + .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); + if let Some(Some(chunk_state)) = self.chunk_states.get(key) { + let mut chunk_res = self.get_chunk_resources(key); + // Remove resources + if let Some(res) = old_block.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] - 1.0 / chunk_state.max_res[res] as f32).max(0.0); + println!("Subbing {} to resources", 1.0 / chunk_state.max_res[res] as f32); + } + } + // Add resources + if let Some(res) = new_block.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] + 1.0 / chunk_state.max_res[res] as f32).min(1.0); + println!("Added {} to resources", 1.0 / chunk_state.max_res[res] as f32); + } + } + println!("Chunk resources are {:?}", chunk_res); + self.state.data.nature.set_chunk_resources(key, chunk_res); + } + } +} + +struct LoadedChunkState { + // The maximum possible number of each resource in this chunk + max_res: EnumMap, } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 5222dcff0a..4e3e2b1e13 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -7,10 +7,11 @@ use common::{ event::{EventBus, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, + slowjob::SlowJobPool, }; use common_ecs::{Job, Origin, Phase, System}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; #[derive(Default)] pub struct Sys; @@ -22,6 +23,7 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, RtSim>, ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, + ReadExpect<'a, SlowJobPool>, ); const NAME: &'static str = "rtsim::tick"; @@ -30,9 +32,14 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, server_event_bus, mut rtsim, world, index): Self::SystemData, + (dt, time, server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, ) { let rtsim = &mut *rtsim; + + if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { + rtsim.save(&slow_jobs); + } + // rtsim.tick += 1; // Update unloaded rtsim entities, in groups at a time diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 94acc0df25..4d3bdd2b94 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -7,6 +7,7 @@ use crate::{ presence::{Presence, RepositionOnChunkLoad}, settings::Settings, sys::sentinel::DeletedEntities, + rtsim2::RtSim, wiring, BattleModeBuffer, SpawnPoint, }; use common::{ @@ -497,6 +498,10 @@ impl StateExt for State { { let ecs = self.ecs(); let slow_jobs = ecs.write_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); let mut chunk_generator = ecs.write_resource::(); let chunk_pos = self.terrain().pos_key(pos.0.map(|e| e as i32)); @@ -517,7 +522,7 @@ impl StateExt for State { #[cfg(feature = "worldgen")] { let time = (*ecs.read_resource::(), (*ecs.read_resource::()).clone()); - chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), index.clone(), time); + chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), &rtsim, index.clone(), time); } }); } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index d7bd86276f..ce5765c839 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -112,7 +112,7 @@ impl<'a> System<'a> for Sys { mut terrain_changes, mut chunk_requests, //mut rtsim, - mut rtsim2, + mut rtsim, mut _terrain_persistence, mut positions, presences, @@ -137,6 +137,7 @@ impl<'a> System<'a> for Sys { request.key, &slow_jobs, Arc::clone(&world), + &rtsim, index.clone(), (*time_of_day, calendar.clone()), ) @@ -182,7 +183,7 @@ impl<'a> System<'a> for Sys { } else { terrain_changes.new_chunks.insert(key); #[cfg(feature = "worldgen")] - rtsim2.hook_load_chunk(key); + rtsim.hook_load_chunk(key, supplement.rtsim_max_resources); } // Handle chunk supplement @@ -387,7 +388,7 @@ impl<'a> System<'a> for Sys { terrain.remove(key).map(|chunk| { terrain_changes.removed_chunks.insert(key); #[cfg(feature = "worldgen")] - rtsim2.hook_unload_chunk(key); + rtsim.hook_unload_chunk(key); chunk }) }) diff --git a/world/Cargo.toml b/world/Cargo.toml index 9d053952c4..0d8ed510a9 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -21,6 +21,7 @@ common-dynlib = {package = "veloren-common-dynlib", path = "../common/dynlib", o bincode = "1.3.1" bitvec = "1.0.1" enum-iterator = "1.1.3" +enum-map = "2.4" fxhash = "0.2.1" image = { version = "0.24", default-features = false, features = ["png"] } itertools = "0.10" diff --git a/world/src/canvas.rs b/world/src/canvas.rs index 4df2ed8ac1..2a1a2e36ac 100644 --- a/world/src/canvas.rs +++ b/world/src/canvas.rs @@ -141,6 +141,7 @@ pub struct Canvas<'a> { pub(crate) info: CanvasInfo<'a>, pub(crate) chunk: &'a mut TerrainChunk, pub(crate) entities: Vec, + pub(crate) rtsim_resource_blocks: Vec>, } impl<'a> Canvas<'a> { @@ -159,11 +160,20 @@ impl<'a> Canvas<'a> { } pub fn set(&mut self, pos: Vec3, block: Block) { + if block.get_rtsim_resource().is_some() { + self.rtsim_resource_blocks.push(pos); + } let _ = self.chunk.set(pos - self.wpos(), block); } pub fn map(&mut self, pos: Vec3, f: impl FnOnce(Block) -> Block) { - let _ = self.chunk.map(pos - self.wpos(), f); + let _ = self.chunk.map(pos - self.wpos(), |b| { + let new_block = f(b); + if new_block.get_rtsim_resource().is_some() { + self.rtsim_resource_blocks.push(pos); + } + new_block + }); } pub fn set_sprite_cfg(&mut self, pos: Vec3, sprite_cfg: SpriteCfg) { diff --git a/world/src/lib.rs b/world/src/lib.rs index 5b187fcae3..76c4b7c55d 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -53,8 +53,10 @@ use common::{ Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid, }, vol::{ReadVol, RectVolSize, WriteVol}, + rtsim::ChunkResource, }; use common_net::msg::{world_msg, WorldMapMsg}; +use enum_map::EnumMap; use rand::{prelude::*, Rng}; use rand_chacha::ChaCha8Rng; use rayon::iter::ParallelIterator; @@ -235,7 +237,7 @@ impl World { // Unwrapping because generate_chunk only returns err when should_continue evals // to true let (tc, _cs) = self - .generate_chunk(index, chunk_pos, || false, None) + .generate_chunk(index, chunk_pos, None, || false, None) .unwrap(); tc.find_accessible_pos(spawn_wpos, ascending) @@ -246,6 +248,7 @@ impl World { &self, index: IndexRef, chunk_pos: Vec2, + rtsim_resources: Option>, // TODO: misleading name mut should_continue: impl FnMut() -> bool, time: Option<(TimeOfDay, Calendar)>, @@ -377,6 +380,7 @@ impl World { }, chunk: &mut chunk, entities: Vec::new(), + rtsim_resource_blocks: Vec::new(), }; if index.features.train_tracks { @@ -416,9 +420,12 @@ impl World { .iter() .for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng)); + let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks); let mut supplement = ChunkSupplement { - entities: canvas.entities, + entities: std::mem::take(&mut canvas.entities), + rtsim_max_resources: Default::default(), }; + drop(canvas); let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| { let lpos2d = TerrainChunkSize::RECT_SIZE @@ -485,6 +492,33 @@ impl World { // Finally, defragment to minimize space consumption. chunk.defragment(); + // Before we finish, we check candidate rtsim resource blocks, deduplicating positions and only keeping those + // that actually do have resources. Although this looks potentially very expensive, only blocks that are rtsim + // resources (i.e: a relatively small number of sprites) are processed here. + if let Some(rtsim_resources) = rtsim_resources { + rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array()); + rtsim_resource_blocks.dedup(); + for wpos in rtsim_resource_blocks { + chunk.map( + wpos - chunk_wpos2d.with_z(0), + |block| if let Some(res) = block.get_rtsim_resource() { + // Note: this represents the upper limit, not the actual number spanwed, so we increment this before deciding whether we're going to spawn the resource. + supplement.rtsim_max_resources[res] += 1; + // Throw a dice to determine whether this resource should actually spawn + // TODO: Don't throw a dice, try to generate the *exact* correct number + if dynamic_rng.gen_bool(rtsim_resources[res] as f64) { + block + } else { + block.into_vacant() + } + } else { + block + }, + ); + } + } + + Ok((chunk, supplement)) } From a421c1239dbb76429b01a22fc4e6f273062ac031 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 9 Aug 2022 00:06:28 +0100 Subject: [PATCH 005/139] Use BufReader/BufWriter for rtsim2 operations --- server/src/rtsim2/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index f0aaaf0693..515c0dbd7b 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -43,7 +43,7 @@ impl RtSim { match File::open(&file_path) { Ok(file) => { info!("Rtsim state found. Attempting to load..."); - match Data::from_reader(file) { + match Data::from_reader(io::BufReader::new(file)) { Ok(data) => { info!("Rtsim state loaded."); break 'load data }, Err(e) => { error!("Rtsim state failed to load: {}", e); @@ -127,7 +127,7 @@ impl RtSim { .map_err(|e: io::Error| ron::Error::from(e)) .and_then(|(mut file, tmp_file_path)| { info!("Writing rtsim state to file..."); - data.write_to(&mut file)?; + data.write_to(io::BufWriter::new(&mut file))?; file.flush()?; drop(file); fs::rename(tmp_file_path, file_path)?; From 0b06eaec6f0673ba20f9e20720534c01fe9550a0 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 9 Aug 2022 14:44:02 +0100 Subject: [PATCH 006/139] Use MessagePack for more compact rtsim state persistence --- Cargo.lock | 29 +++++++++++++++++++++++++++++ rtsim/Cargo.toml | 1 + rtsim/src/data/mod.rs | 12 +++++++----- server/src/rtsim2/mod.rs | 11 ++++++----- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ce962ac5c..4915092e2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4318,6 +4318,12 @@ dependencies = [ "regex", ] +[[package]] +name = "paste" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -5096,6 +5102,28 @@ dependencies = [ "syn 1.0.100", ] +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25786b0d276110195fa3d6f3f31299900cf71dfbd6c28450f3f58a0e7f7a347e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rodio" version = "0.15.0" @@ -6913,6 +6941,7 @@ version = "0.10.0" dependencies = [ "enum-map", "hashbrown 0.12.3", + "rmp-serde", "ron 0.8.0", "serde", "vek 0.15.8", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 504d1d50cd..96fccf251e 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -11,3 +11,4 @@ serde = { version = "1.0.110", features = ["derive"] } hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } enum-map = { version = "2.4", features = ["serde"] } vek = { version = "0.15.8", features = ["serde"] } +rmp-serde = "1.1.0" diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index d4dd3d97e9..4e8622f294 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -7,7 +7,6 @@ pub use self::{ }; use self::helper::Latest; -use ron::error::SpannedResult; use serde::{Serialize, Deserialize}; use std::io::{Read, Write}; @@ -17,12 +16,15 @@ pub struct Data { pub actors: Actors, } +pub type ReadError = rmp_serde::decode::Error; +pub type WriteError = rmp_serde::encode::Error; + impl Data { - pub fn from_reader(reader: R) -> SpannedResult { - ron::de::from_reader(reader) + pub fn from_reader(reader: R) -> Result { + rmp_serde::decode::from_read(reader) } - pub fn write_to(&self, writer: W) -> Result<(), ron::Error> { - ron::ser::to_writer(writer, self) + pub fn write_to(&self, mut writer: W) -> Result<(), WriteError> { + rmp_serde::encode::write(&mut writer, self) } } diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 515c0dbd7b..2cf6b2b337 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -8,7 +8,7 @@ use common::{ vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; -use rtsim2::{data::Data, RtState}; +use rtsim2::{data::{Data, ReadError}, RtState}; use specs::{DispatcherBuilder, WorldExt}; use std::{ fs::{self, File}, @@ -16,6 +16,7 @@ use std::{ sync::Arc, time::Instant, io::{self, Write}, + error::Error, }; use enum_map::EnumMap; use tracing::{error, warn, info}; @@ -51,7 +52,7 @@ impl RtSim { loop { let mut backup_path = file_path.clone(); backup_path.set_extension(if i == 0 { - format!("ron_backup_{}", i) + format!("backup_{}", i) } else { "ron_backup".to_string() }); @@ -88,7 +89,7 @@ impl RtSim { data_dir.push("rtsim"); data_dir }); - path.push("state.ron"); + path.push("state.dat"); path } @@ -112,7 +113,7 @@ impl RtSim { // TODO: Use slow job // slowjob_pool.spawn("RTSIM_SAVE", move || { std::thread::spawn(move || { - let tmp_file_name = "state_tmp.ron"; + let tmp_file_name = "state_tmp.dat"; if let Err(e) = file_path .parent() .map(|dir| { @@ -124,7 +125,7 @@ impl RtSim { .and_then(|tmp_file_path| { Ok((File::create(&tmp_file_path)?, tmp_file_path)) }) - .map_err(|e: io::Error| ron::Error::from(e)) + .map_err(|e: io::Error| Box::new(e) as Box::) .and_then(|(mut file, tmp_file_path)| { info!("Writing rtsim state to file..."); data.write_to(io::BufWriter::new(&mut file))?; From 9d3dadfabab0396beb63198b9c4bb1e524c33b2b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 10 Aug 2022 00:02:56 +0100 Subject: [PATCH 007/139] Make resource depletion an rtsim rule --- Cargo.lock | 9 ++ rtsim/Cargo.toml | 3 + rtsim/src/data/nature.rs | 3 + rtsim/src/event.rs | 8 ++ rtsim/src/lib.rs | 109 ++++++++++++++- rtsim/src/rule.rs | 21 +++ rtsim/src/rule/example.rs | 19 +++ server/src/lib.rs | 3 +- server/src/rtsim2/event.rs | 12 ++ server/src/rtsim2/mod.rs | 146 +++++++++----------- server/src/rtsim2/rule.rs | 9 ++ server/src/rtsim2/rule/deplete_resources.rs | 47 +++++++ server/src/rtsim2/tick.rs | 2 + 13 files changed, 305 insertions(+), 86 deletions(-) create mode 100644 rtsim/src/event.rs create mode 100644 rtsim/src/rule.rs create mode 100644 rtsim/src/rule/example.rs create mode 100644 server/src/rtsim2/event.rs create mode 100644 server/src/rtsim2/rule.rs create mode 100644 server/src/rtsim2/rule/deplete_resources.rs diff --git a/Cargo.lock b/Cargo.lock index 4915092e2c..efd30ed9ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,12 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + [[package]] name = "app_dirs2" version = "2.5.4" @@ -6939,11 +6945,14 @@ dependencies = [ name = "veloren-rtsim" version = "0.10.0" dependencies = [ + "anymap2", + "atomic_refcell", "enum-map", "hashbrown 0.12.3", "rmp-serde", "ron 0.8.0", "serde", + "tracing", "vek 0.15.8", "veloren-common", "veloren-world", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 96fccf251e..1ffcb48f0d 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -12,3 +12,6 @@ hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } enum-map = { version = "2.4", features = ["serde"] } vek = { version = "0.15.8", features = ["serde"] } rmp-serde = "1.1.0" +anymap2 = "0.13" +tracing = "0.1" +atomic_refcell = "0.1" diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index 9fccf7ad2a..5cb9bea7ef 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -7,6 +7,8 @@ use common::{ use world::World; use vek::*; +/// Represents the state of 'natural' elements of the world such as plant/animal/resource populations, weather systems, +/// etc. #[derive(Clone, Serialize, Deserialize)] pub struct Nature { chunks: Grid, @@ -40,5 +42,6 @@ impl Nature { #[derive(Clone, Serialize, Deserialize)] pub struct Chunk { + /// Represent the 'naturally occurring' resource proportion that exists in this chunk. res: EnumMap, } diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs new file mode 100644 index 0000000000..deb35cc68c --- /dev/null +++ b/rtsim/src/event.rs @@ -0,0 +1,8 @@ +use common::resources::Time; + +pub trait Event: Clone + 'static {} + +#[derive(Clone)] +pub struct OnTick { pub dt: f32 } + +impl Event for OnTick {} diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 975ea54412..1397d99dd8 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,10 +1,107 @@ -pub mod data; -pub mod gen; +#![feature(explicit_generic_args_with_impl_trait)] -use self::data::Data; -use std::sync::Arc; -use world::World; +pub mod data; +pub mod event; +pub mod gen; +pub mod rule; + +pub use self::{ + data::Data, + event::{Event, OnTick}, + rule::{Rule, RuleError}, +}; +use anymap2::SendSyncAnyMap; +use tracing::{info, error}; +use atomic_refcell::AtomicRefCell; +use std::{ + any::type_name, + ops::{Deref, DerefMut}, +}; pub struct RtState { - pub data: Data, + resources: SendSyncAnyMap, + rules: SendSyncAnyMap, + event_handlers: SendSyncAnyMap, +} + +type RuleState = AtomicRefCell; +type EventHandlersOf = Vec>; + +impl RtState { + pub fn new(data: Data) -> Self { + let mut this = Self { + resources: SendSyncAnyMap::new(), + rules: SendSyncAnyMap::new(), + event_handlers: SendSyncAnyMap::new(), + } + .with_resource(data); + + this.start_default_rules(); + + this + } + + pub fn with_resource(mut self, r: R) -> Self { + self.resources.insert(AtomicRefCell::new(r)); + self + } + + fn start_default_rules(&mut self) { + info!("Starting default rtsim rules..."); + self.start_rule::(); + } + + pub fn start_rule(&mut self) { + info!("Initiating '{}' rule...", type_name::()); + match R::start(self) { + Ok(rule) => { self.rules.insert::>(AtomicRefCell::new(rule)); }, + Err(e) => error!("Error when initiating '{}' rule: {}", type_name::(), e), + } + } + + fn rule_mut(&self) -> impl DerefMut + '_ { + self.rules + .get::>() + .unwrap_or_else(|| panic!("Tried to access rule '{}' but it does not exist", type_name::())) + .borrow_mut() + } + + pub fn bind(&mut self, mut f: impl FnMut(&mut R, &RtState, E) + Send + Sync + 'static) { + let f = AtomicRefCell::new(f); + self.event_handlers + .entry::>() + .or_default() + .push(Box::new(move |rtstate, event| { + (f.borrow_mut())(&mut rtstate.rule_mut(), rtstate, event) + })); + } + + pub fn data(&self) -> impl Deref + '_ { self.resource() } + pub fn data_mut(&self) -> impl DerefMut + '_ { self.resource_mut() } + + pub fn resource(&self) -> impl Deref + '_ { + self.resources + .get::>() + .unwrap_or_else(|| panic!("Tried to access resource '{}' but it does not exist", type_name::())) + .borrow() + } + + pub fn resource_mut(&self) -> impl DerefMut + '_ { + self.resources + .get::>() + .unwrap_or_else(|| panic!("Tried to access resource '{}' but it does not exist", type_name::())) + .borrow_mut() + } + + pub fn emit(&mut self, e: E) { + self.event_handlers + .get::>() + .map(|handlers| handlers + .iter() + .for_each(|f| f(self, e.clone()))); + } + + pub fn tick(&mut self, dt: f32) { + self.emit(OnTick { dt }); + } } diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs new file mode 100644 index 0000000000..6f6b701740 --- /dev/null +++ b/rtsim/src/rule.rs @@ -0,0 +1,21 @@ +pub mod example; + +use std::fmt; +use super::RtState; + +#[derive(Debug)] +pub enum RuleError { + NoSuchRule(&'static str), +} + +impl fmt::Display for RuleError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::NoSuchRule(r) => write!(f, "tried to fetch rule state '{}' but it does not exist", r), + } + } +} + +pub trait Rule: Sized + Send + Sync + 'static { + fn start(rtstate: &mut RtState) -> Result; +} diff --git a/rtsim/src/rule/example.rs b/rtsim/src/rule/example.rs new file mode 100644 index 0000000000..d4cda69920 --- /dev/null +++ b/rtsim/src/rule/example.rs @@ -0,0 +1,19 @@ +use tracing::info; +use crate::{ + event::OnTick, + RtState, Rule, RuleError, +}; + +pub struct RuleState; + +impl Rule for RuleState { + fn start(rtstate: &mut RtState) -> Result { + info!("Hello from example rule!"); + + rtstate.bind::(|this, rtstate, event| { + // println!("Tick!"); + }); + + Ok(Self) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index df1dca17e4..ec39cab251 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -7,7 +7,8 @@ let_chains, never_type, option_zip, - unwrap_infallible + unwrap_infallible, + explicit_generic_args_with_impl_trait )] #![feature(hash_drain_filter)] diff --git a/server/src/rtsim2/event.rs b/server/src/rtsim2/event.rs new file mode 100644 index 0000000000..e576c42fcb --- /dev/null +++ b/server/src/rtsim2/event.rs @@ -0,0 +1,12 @@ +use rtsim2::Event; +use common::terrain::Block; +use vek::*; + +#[derive(Clone)] +pub struct OnBlockChange { + pub wpos: Vec3, + pub old: Block, + pub new: Block, +} + +impl Event for OnBlockChange {} diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 2cf6b2b337..f71555afaa 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -1,3 +1,5 @@ +pub mod event; +pub mod rule; pub mod tick; use common::{ @@ -8,7 +10,11 @@ use common::{ vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; -use rtsim2::{data::{Data, ReadError}, RtState}; +use rtsim2::{ + data::{Data, ReadError}, + rule::Rule, + RtState, +}; use specs::{DispatcherBuilder, WorldExt}; use std::{ fs::{self, File}, @@ -19,14 +25,13 @@ use std::{ error::Error, }; use enum_map::EnumMap; -use tracing::{error, warn, info}; +use tracing::{error, warn, info, debug}; use vek::*; use world::World; pub struct RtSim { file_path: PathBuf, last_saved: Option, - chunk_states: Grid>, state: RtState, } @@ -34,52 +39,54 @@ impl RtSim { pub fn new(world: &World, data_dir: PathBuf) -> Result { let file_path = Self::get_file_path(data_dir); - Ok(Self { - chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| None), - last_saved: None, - state: RtState { - data: { - info!("Looking for rtsim state in {}...", file_path.display()); - 'load: { - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim state found. Attempting to load..."); - match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { info!("Rtsim state loaded."); break 'load data }, - Err(e) => { - error!("Rtsim state failed to load: {}", e); - let mut i = 0; - loop { - let mut backup_path = file_path.clone(); - backup_path.set_extension(if i == 0 { - format!("backup_{}", i) - } else { - "ron_backup".to_string() - }); - if !backup_path.exists() { - fs::rename(&file_path, &backup_path)?; - warn!("Failed rtsim state was moved to {}", backup_path.display()); - info!("A fresh rtsim state will now be generated."); - break; - } - i += 1; - } - }, + info!("Looking for rtsim data in {}...", file_path.display()); + let data = 'load: { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim data found. Attempting to load..."); + match Data::from_reader(io::BufReader::new(file)) { + Ok(data) => { info!("Rtsim data loaded."); break 'load data }, + Err(e) => { + error!("Rtsim data failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!("Failed rtsim data was moved to {}", backup_path.display()); + info!("A fresh rtsim data will now be generated."); + break; } - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => - info!("No rtsim state found. Generating from initial world state..."), - Err(e) => return Err(e.into()), - } - - let data = Data::generate(&world); - info!("Rtsim state generated."); - data + i += 1; + } + }, } }, - }, + Err(e) if e.kind() == io::ErrorKind::NotFound => + info!("No rtsim data found. Generating from world..."), + Err(e) => return Err(e.into()), + } + + let data = Data::generate(&world); + info!("Rtsim data generated."); + data + }; + + let mut this = Self { + last_saved: None, + state: RtState::new(data) + .with_resource(ChunkStates(Grid::populate_from(world.sim().get_size().as_(), |_| None))), file_path, - }) + }; + + rule::start_rules(&mut this.state); + + Ok(this) } fn get_file_path(mut data_dir: PathBuf) -> PathBuf { @@ -89,31 +96,31 @@ impl RtSim { data_dir.push("rtsim"); data_dir }); - path.push("state.dat"); + path.push("data.dat"); path } pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { - if let Some(chunk_state) = self.chunk_states.get_mut(key) { + if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk_state) = self.chunk_states.get_mut(key) { + if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { *chunk_state = None; } } pub fn save(&mut self, slowjob_pool: &SlowJobPool) { - info!("Beginning rtsim state save..."); + info!("Saving rtsim data..."); let file_path = self.file_path.clone(); - let data = self.state.data.clone(); - info!("Starting rtsim save job..."); + let data = self.state.data().clone(); + debug!("Starting rtsim data save job..."); // TODO: Use slow job // slowjob_pool.spawn("RTSIM_SAVE", move || { std::thread::spawn(move || { - let tmp_file_name = "state_tmp.dat"; + let tmp_file_name = "data_tmp.dat"; if let Err(e) = file_path .parent() .map(|dir| { @@ -127,16 +134,16 @@ impl RtSim { }) .map_err(|e: io::Error| Box::new(e) as Box::) .and_then(|(mut file, tmp_file_path)| { - info!("Writing rtsim state to file..."); + debug!("Writing rtsim data to file..."); data.write_to(io::BufWriter::new(&mut file))?; file.flush()?; drop(file); fs::rename(tmp_file_path, file_path)?; - info!("Rtsim state saved."); + debug!("Rtsim data saved."); Ok(()) }) { - error!("Saving rtsim state failed: {}", e); + error!("Saving rtsim data failed: {}", e); } }); self.last_saved = Some(Instant::now()); @@ -144,34 +151,15 @@ impl RtSim { // TODO: Clean up this API a bit pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { - self.state.data.nature.get_chunk_resources(key) + self.state.data().nature.get_chunk_resources(key) } - pub fn hook_block_update(&mut self, wpos: Vec3, old_block: Block, new_block: Block) { - let key = wpos - .xy() - .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); - if let Some(Some(chunk_state)) = self.chunk_states.get(key) { - let mut chunk_res = self.get_chunk_resources(key); - // Remove resources - if let Some(res) = old_block.get_rtsim_resource() { - if chunk_state.max_res[res] > 0 { - chunk_res[res] = (chunk_res[res] - 1.0 / chunk_state.max_res[res] as f32).max(0.0); - println!("Subbing {} to resources", 1.0 / chunk_state.max_res[res] as f32); - } - } - // Add resources - if let Some(res) = new_block.get_rtsim_resource() { - if chunk_state.max_res[res] > 0 { - chunk_res[res] = (chunk_res[res] + 1.0 / chunk_state.max_res[res] as f32).min(1.0); - println!("Added {} to resources", 1.0 / chunk_state.max_res[res] as f32); - } - } - println!("Chunk resources are {:?}", chunk_res); - self.state.data.nature.set_chunk_resources(key, chunk_res); - } + pub fn hook_block_update(&mut self, wpos: Vec3, old: Block, new: Block) { + self.state.emit(event::OnBlockChange { wpos, old, new }); } } +struct ChunkStates(pub Grid>); + struct LoadedChunkState { // The maximum possible number of each resource in this chunk max_res: EnumMap, diff --git a/server/src/rtsim2/rule.rs b/server/src/rtsim2/rule.rs new file mode 100644 index 0000000000..41dd191507 --- /dev/null +++ b/server/src/rtsim2/rule.rs @@ -0,0 +1,9 @@ +pub mod deplete_resources; + +use tracing::info; +use rtsim2::RtState; + +pub fn start_rules(rtstate: &mut RtState) { + info!("Starting server rtsim rules..."); + rtstate.start_rule::(); +} diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs new file mode 100644 index 0000000000..fe13a2d190 --- /dev/null +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -0,0 +1,47 @@ +use tracing::info; +use rtsim2::{RtState, Rule, RuleError}; +use crate::rtsim2::{ + event::OnBlockChange, + ChunkStates, +}; +use common::{ + terrain::TerrainChunk, + vol::RectRasterableVol, +}; + +pub struct State; + +impl Rule for State { + fn start(rtstate: &mut RtState) -> Result { + info!("Hello from the resource depletion rule!"); + + rtstate.bind::(|this, rtstate, event| { + let key = event.wpos + .xy() + .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); + if let Some(Some(chunk_state)) = rtstate.resource_mut::().0.get(key) { + let mut chunk_res = rtstate.data().nature.get_chunk_resources(key); + // Remove resources + if let Some(res) = event.old.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 - 1.0) + .round() + .max(0.0) / chunk_state.max_res[res] as f32; + } + } + // Add resources + if let Some(res) = event.new.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + 1.0) + .round() + .max(0.0) / chunk_state.max_res[res] as f32; + } + } + println!("Chunk resources = {:?}", chunk_res); + rtstate.data_mut().nature.set_chunk_resources(key, chunk_res); + } + }); + + Ok(Self) + } +} diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 4e3e2b1e13..bf07399e7c 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -36,6 +36,8 @@ impl<'a> System<'a> for Sys { ) { let rtsim = &mut *rtsim; + rtsim.state.tick(dt.0); + if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { rtsim.save(&slow_jobs); } From 87a6143375d9d837a685b20a7039b3afe434bd92 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 00:40:30 +0100 Subject: [PATCH 008/139] Began adding rtsim2 NPCs, scale command --- Cargo.lock | 2 + common/src/cmd.rs | 5 ++ common/src/rtsim.rs | 3 + common/src/states/behavior.rs | 5 +- common/src/states/climb.rs | 4 +- common/src/states/utils.rs | 24 +++++--- common/systems/src/character_behavior.rs | 7 ++- common/systems/src/phys.rs | 26 ++++---- rtsim/Cargo.toml | 2 + rtsim/src/data/actor.rs | 16 ----- rtsim/src/data/mod.rs | 76 +++++++++++++++++++++--- rtsim/src/data/nature.rs | 15 +++++ rtsim/src/data/npc.rs | 50 ++++++++++++++++ rtsim/src/gen/mod.rs | 42 ++++++++++--- server/src/cmd.rs | 24 ++++++++ server/src/lib.rs | 2 +- server/src/rtsim2/mod.rs | 66 ++++++++++---------- server/src/rtsim2/tick.rs | 36 ++++++++++- voxygen/src/scene/figure/mod.rs | 19 +++--- voxygen/src/scene/mod.rs | 29 ++++++++- 20 files changed, 354 insertions(+), 99 deletions(-) delete mode 100644 rtsim/src/data/actor.rs create mode 100644 rtsim/src/data/npc.rs diff --git a/Cargo.lock b/Cargo.lock index efd30ed9ae..237da3cbc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6949,9 +6949,11 @@ dependencies = [ "atomic_refcell", "enum-map", "hashbrown 0.12.3", + "rand 0.8.5", "rmp-serde", "ron 0.8.0", "serde", + "slotmap 1.0.6", "tracing", "vek 0.15.8", "veloren-common", diff --git a/common/src/cmd.rs b/common/src/cmd.rs index ddc319baf3..7431e7e271 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -298,6 +298,7 @@ pub enum ServerChatCommand { RevokeBuildAll, Safezone, Say, + Scale, ServerPhysics, SetMotd, Ship, @@ -727,6 +728,9 @@ impl ServerChatCommand { ServerChatCommand::Lightning => { cmd(vec![], "Lightning strike at current position", Some(Admin)) }, + ServerChatCommand::Scale => { + cmd(vec![Float("factor", 1.0, Required)], "Scale your character", Some(Admin)) + }, } } @@ -808,6 +812,7 @@ impl ServerChatCommand { ServerChatCommand::DeleteLocation => "delete_location", ServerChatCommand::WeatherZone => "weather_zone", ServerChatCommand::Lightning => "lightning", + ServerChatCommand::Scale => "scale", } } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 1dc03a0250..19990a9d5b 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -86,7 +86,10 @@ impl RtSimController { #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] pub enum ChunkResource { + #[serde(rename = "0")] Grass, + #[serde(rename = "1")] Flax, + #[serde(rename = "2")] Cotton, } diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index 766a1738bc..a117ad19a0 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -6,7 +6,7 @@ use crate::{ ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, Stance, StateUpdate, Stats, - Vel, + Vel, Scale, }, link::Is, mounting::Rider, @@ -123,6 +123,7 @@ pub struct JoinData<'a> { pub pos: &'a Pos, pub vel: &'a Vel, pub ori: &'a Ori, + pub scale: Option<&'a Scale>, pub mass: &'a Mass, pub density: &'a Density, pub dt: &'a DeltaTime, @@ -155,6 +156,7 @@ pub struct JoinStruct<'a> { pub pos: &'a mut Pos, pub vel: &'a mut Vel, pub ori: &'a mut Ori, + pub scale: Option<&'a Scale>, pub mass: &'a Mass, pub density: FlaggedAccessMut<'a, &'a mut Density, Density>, pub energy: FlaggedAccessMut<'a, &'a mut Energy, Energy>, @@ -191,6 +193,7 @@ impl<'a> JoinData<'a> { pos: j.pos, vel: j.vel, ori: j.ori, + scale: j.scale, mass: j.mass, density: &j.density, energy: &j.energy, diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 68c2dd0be0..77ffc8d69d 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -122,10 +122,10 @@ impl CharacterBehavior for Data { // Apply Vertical Climbing Movement match climb { Climb::Down => { - update.vel.0.z += data.dt.0 * (GRAVITY - self.static_data.movement_speed.powi(2)) + update.vel.0.z += data.dt.0 * (GRAVITY - self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) }, Climb::Up => { - update.vel.0.z += data.dt.0 * (GRAVITY + self.static_data.movement_speed.powi(2)) + update.vel.0.z += data.dt.0 * (GRAVITY + self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) }, Climb::Hold => update.vel.0.z += data.dt.0 * GRAVITY, } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 4457f0df51..e266db3864 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -385,7 +385,10 @@ fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) { let accel = if let Some(block) = data.physics.on_ground { // FRIC_GROUND temporarily used to normalize things around expected values - data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND + data.body.base_accel() + * data.scale.map_or(1.0, |s| s.0) + * block.get_traction() + * block.get_friction() / FRIC_GROUND } else { data.body.air_accel() } * efficiency; @@ -435,7 +438,7 @@ pub fn handle_forced_movement( data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * Vec2::from(*data.ori) * strength; + Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * Vec2::from(*data.ori) * strength; } }, ForcedMovement::Reverse(strength) => { @@ -445,7 +448,7 @@ pub fn handle_forced_movement( data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * -Vec2::from(*data.ori) * strength; + Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * -Vec2::from(*data.ori) * strength; } }, ForcedMovement::Sideways(strength) => { @@ -467,7 +470,7 @@ pub fn handle_forced_movement( } }; - update.vel.0 += Vec2::broadcast(data.dt.0) * accel * direction * strength; + update.vel.0 += Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * direction * strength; } }, ForcedMovement::DirectedReverse(strength) => { @@ -516,6 +519,7 @@ pub fn handle_forced_movement( dir.y, vertical, ) + * data.scale.map_or(1.0, |s| s.0) // Multiply decreasing amount linearly over time (with average of 1) * 2.0 * progress // Apply direction @@ -528,8 +532,9 @@ pub fn handle_forced_movement( * (1.0 - data.inputs.look_dir.z.abs()); }, ForcedMovement::Hover { move_input } => { - update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0) - + move_input * data.inputs.move_dir.try_normalized().unwrap_or_default(); + update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0) + move_input + * data.scale.map_or(1.0, |s| s.0) + * data.inputs.move_dir.try_normalized().unwrap_or_default(); }, } } @@ -570,6 +575,7 @@ pub fn handle_orientation( }; // unit is multiples of 180° let half_turns_per_tick = data.body.base_ori_rate() + / data.scale.map_or(1.0, |s| s.0) * efficiency * if data.physics.on_ground.is_some() { 1.0 @@ -605,7 +611,7 @@ fn swim_move( ) -> bool { let efficiency = efficiency * data.stats.move_speed_modifier * data.stats.friction_modifier; if let Some(force) = data.body.swim_thrust() { - let force = efficiency * force; + let force = efficiency * force * data.scale.map_or(1.0, |s| s.0); let mut water_accel = force / data.mass.0; if let Ok(level) = data.skill_set.skill_level(Skill::Swim(SwimSkill::Speed)) { @@ -1068,7 +1074,9 @@ pub fn handle_jump( .map(|impulse| { output_events.emit_local(LocalEvent::Jump( data.entity, - strength * impulse / data.mass.0 * data.stats.move_speed_modifier, + strength * impulse / data.mass.0 + * data.scale.map_or(1.0, |s| s.0.sqrt()) + * data.stats.move_speed_modifier, )); }) .is_some() diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 133c66d869..d4a8e6ca70 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -10,7 +10,7 @@ use common::{ inventory::item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, SkillSet, Stance, - StateUpdate, Stats, Vel, + StateUpdate, Stats, Vel, Scale, }, event::{EventBus, LocalEvent, ServerEvent}, link::Is, @@ -37,6 +37,7 @@ pub struct ReadData<'a> { healths: ReadStorage<'a, Health>, bodies: ReadStorage<'a, Body>, masses: ReadStorage<'a, Mass>, + scales: ReadStorage<'a, Scale>, physics_states: ReadStorage<'a, PhysicsState>, melee_attacks: ReadStorage<'a, Melee>, beams: ReadStorage<'a, Beam>, @@ -116,7 +117,7 @@ impl<'a> System<'a> for Sys { health, body, physics, - (stat, skill_set, active_abilities, is_rider), + (scale, stat, skill_set, active_abilities, is_rider), combo, ) in ( &read_data.entities, @@ -134,6 +135,7 @@ impl<'a> System<'a> for Sys { &read_data.bodies, &read_data.physics_states, ( + read_data.scales.maybe(), &read_data.stats, &read_data.skill_sets, read_data.active_abilities.maybe(), @@ -183,6 +185,7 @@ impl<'a> System<'a> for Sys { pos, vel, ori, + scale, mass, density, energy, diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index a11bbcc053..2e9bf97adc 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1350,19 +1350,15 @@ fn box_voxel_collision + ReadVol>( read: &PhysicsRead, ori: &Ori, ) { - // FIXME: Review these - #![allow( - clippy::cast_precision_loss, - clippy::cast_possible_truncation, - clippy::cast_sign_loss - )] + let scale = read.scales.get(entity).map_or(1.0, |s| s.0); + //prof_span!("box_voxel_collision"); // Convience function to compute the player aabb - fn player_aabb(pos: Vec3, radius: f32, z_range: Range) -> Aabb { + fn player_aabb(pos: Vec3, radius: f32, z_range: Range, scale: f32) -> Aabb { Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start), - max: pos + Vec3::new(radius, radius, z_range.end), + min: pos + Vec3::new(-radius, -radius, z_range.start) * scale, + max: pos + Vec3::new(radius, radius, z_range.end) * scale, } } @@ -1383,8 +1379,9 @@ fn box_voxel_collision + ReadVol>( near_aabb: Aabb, radius: f32, z_range: Range, + scale: f32, ) -> bool { - let player_aabb = player_aabb(pos, radius, z_range); + let player_aabb = player_aabb(pos, radius, z_range, scale); // Calculate the world space near aabb let near_aabb = move_aabb(near_aabb, pos); @@ -1451,7 +1448,7 @@ fn box_voxel_collision + ReadVol>( let try_colliding_block = |pos: &Pos| { //prof_span!("most colliding check"); // Calculate the player's AABB - let player_aabb = player_aabb(pos.0, radius, z_range.clone()); + let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); // Determine the block that we are colliding with most // (based on minimum collision axis) @@ -1501,7 +1498,7 @@ fn box_voxel_collision + ReadVol>( .flatten() { // Calculate the player's AABB - let player_aabb = player_aabb(pos.0, radius, z_range.clone()); + let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); // Find the intrusion vector of the collision let dir = player_aabb.collision_vector_with_aabb(block_aabb); @@ -1547,6 +1544,7 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), + scale, ) } // ...and there is a collision with a block beneath our current hitbox... @@ -1559,6 +1557,7 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), + scale, ) } { // ...block-hop! @@ -1613,6 +1612,7 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), + scale, ) } { //prof_span!("snap!!"); @@ -1630,7 +1630,7 @@ fn box_voxel_collision + ReadVol>( } // Find liquid immersion and wall collision all in one round of iteration - let player_aabb = player_aabb(pos.0, radius, z_range.clone()); + let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); // Calculate the world space near_aabb let near_aabb = move_aabb(near_aabb, pos.0); diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 1ffcb48f0d..da380b2066 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -15,3 +15,5 @@ rmp-serde = "1.1.0" anymap2 = "0.13" tracing = "0.1" atomic_refcell = "0.1" +slotmap = { version = "1.0.6", features = ["serde"] } +rand = { version = "0.8", features = ["small_rng"] } diff --git a/rtsim/src/data/actor.rs b/rtsim/src/data/actor.rs deleted file mode 100644 index 88d3e58ee3..0000000000 --- a/rtsim/src/data/actor.rs +++ /dev/null @@ -1,16 +0,0 @@ -use hashbrown::HashMap; -use serde::{Serialize, Deserialize}; - -#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct ActorId { - pub idx: u32, - pub gen: u32, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Actor {} - -#[derive(Clone, Serialize, Deserialize)] -pub struct Actors { - pub actors: HashMap, -} diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 4e8622f294..c68797db4f 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,19 +1,30 @@ -pub mod actor; +pub mod npc; pub mod nature; pub use self::{ - actor::{Actor, ActorId, Actors}, + npc::{Npc, NpcId, Npcs}, nature::Nature, }; -use self::helper::Latest; -use serde::{Serialize, Deserialize}; -use std::io::{Read, Write}; +use enum_map::{EnumMap, EnumArray, enum_map}; +use serde::{Serialize, Deserialize, ser, de}; +use std::{ + io::{Read, Write}, + marker::PhantomData, + cmp::PartialEq, + fmt, +}; + +#[derive(Copy, Clone, Serialize, Deserialize)] +pub enum Actor { + Npc(NpcId), + Character(common::character::CharacterId), +} #[derive(Clone, Serialize, Deserialize)] pub struct Data { pub nature: Nature, - pub actors: Actors, + pub npcs: Npcs, } pub type ReadError = rmp_serde::decode::Error; @@ -25,6 +36,57 @@ impl Data { } pub fn write_to(&self, mut writer: W) -> Result<(), WriteError> { - rmp_serde::encode::write(&mut writer, self) + rmp_serde::encode::write_named(&mut writer, self) } } + +// fn rugged_ser_enum_map + Serialize, V: PartialEq + Default + Serialize, S: ser::Serializer>(map: &EnumMap, mut ser: S) -> Result { +// ser.collect_map(map +// .iter() +// .filter(|(k, v)| v != &&V::default()) +// .map(|(k, v)| (k, v))) +// } + +fn rugged_ser_enum_map< + K: EnumArray + Serialize, + V: From + PartialEq + Serialize, + S: ser::Serializer, + const DEFAULT: i16, +>(map: &EnumMap, ser: S) -> Result { + ser.collect_map(map + .iter() + .filter(|(k, v)| v != &&V::from(DEFAULT)) + .map(|(k, v)| (k, v))) +} + +fn rugged_de_enum_map< + 'a, + K: EnumArray + EnumArray> + Deserialize<'a>, + V: From + Deserialize<'a>, + D: de::Deserializer<'a>, + const DEFAULT: i16, +>(mut de: D) -> Result, D::Error> { + struct Visitor(PhantomData<(K, V)>); + + impl<'de, K, V, const DEFAULT: i16> de::Visitor<'de> for Visitor + where + K: EnumArray + EnumArray> + Deserialize<'de>, + V: From + Deserialize<'de>, + { + type Value = EnumMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a map") + } + + fn visit_map>(self, mut access: M) -> Result { + let mut entries = EnumMap::default(); + while let Some((key, value)) = access.next_entry()? { + entries[key] = Some(value); + } + Ok(enum_map! { key => entries[key].take().unwrap_or_else(|| V::from(DEFAULT)) }) + } + } + + de.deserialize_map(Visitor::<_, _, DEFAULT>(PhantomData)) +} diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index 5cb9bea7ef..2a7abf8b0f 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -9,6 +9,9 @@ use vek::*; /// Represents the state of 'natural' elements of the world such as plant/animal/resource populations, weather systems, /// etc. +/// +/// Where possible, this data does not define the state of natural aspects of the world, but instead defines +/// 'modifications' that sit on top of the world data generated by initial generation. #[derive(Clone, Serialize, Deserialize)] pub struct Nature { chunks: Grid, @@ -43,5 +46,17 @@ impl Nature { #[derive(Clone, Serialize, Deserialize)] pub struct Chunk { /// Represent the 'naturally occurring' resource proportion that exists in this chunk. + /// + /// 0.0 => None of the resources generated by terrain generation should be present + /// 1.0 => All of the resources generated by terrain generation should be present + /// + /// It's important to understand this this number does not represent the total amount of a resource present in a + /// chunk, nor is it even proportional to the amount of the resource present. To get the total amount of the + /// resource in a chunk, one must first multiply this factor by the amount of 'natural' resources given by terrain + /// generation. This value represents only the variable 'depletion' factor of that resource, which shall change + /// over time as the world evolves and players interact with it. + #[serde(rename = "r")] + #[serde(serialize_with = "crate::data::rugged_ser_enum_map::<_, _, _, 1>")] + #[serde(deserialize_with = "crate::data::rugged_de_enum_map::<_, _, _, 1>")] res: EnumMap, } diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs new file mode 100644 index 0000000000..ce8e0054fa --- /dev/null +++ b/rtsim/src/data/npc.rs @@ -0,0 +1,50 @@ +use hashbrown::HashMap; +use serde::{Serialize, Deserialize}; +use slotmap::HopSlotMap; +use vek::*; +use std::ops::{Deref, DerefMut}; +use common::uid::Uid; + +slotmap::new_key_type! { pub struct NpcId; } + +#[derive(Clone, Serialize, Deserialize)] +pub struct Npc { + pub wpos: Vec3, + #[serde(skip_serializing, skip_deserializing)] + pub mode: NpcMode, +} + +impl Npc { + pub fn at(wpos: Vec3) -> Self { + Self { wpos, mode: NpcMode::Simulated } + } +} + +#[derive(Copy, Clone, Default)] +pub enum NpcMode { + /// The NPC is unloaded and is being simulated via rtsim. + #[default] + Simulated, + /// The NPC has been loaded into the game world as an ECS entity. + Loaded, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Npcs { + pub npcs: HopSlotMap, +} + +impl Npcs { + pub fn spawn(&mut self, npc: Npc) -> NpcId { + self.npcs.insert(npc) + } +} + +impl Deref for Npcs { + type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.npcs } +} + +impl DerefMut for Npcs { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.npcs } +} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index f1ce167d1d..fb2d3829a8 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,14 +1,42 @@ -use crate::data::{Actors, Data, Nature}; +use crate::data::{Npcs, Npc, Data, Nature}; use hashbrown::HashMap; -use world::World; +use rand::prelude::*; +use world::{ + site::SiteKind, + IndexRef, + World, +}; impl Data { - pub fn generate(world: &World) -> Self { - Self { + pub fn generate(index: IndexRef, world: &World) -> Self { + let mut seed = [0; 32]; + seed.iter_mut().zip(&mut index.seed.to_le_bytes()).for_each(|(dst, src)| *dst = *src); + let mut rng = SmallRng::from_seed(seed); + + let mut this = Self { nature: Nature::generate(world), - actors: Actors { - actors: HashMap::default(), - }, + npcs: Npcs { npcs: Default::default() }, + }; + + for (site_id, site) in world + .civs() + .sites + .iter() + .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) + { + match &site.kind { + SiteKind::Refactor(site2) => { + let wpos = site.get_origin() + .map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(site.get_origin()).unwrap_or(0.0)); + // TODO: Better API + this.npcs.spawn(Npc::at(wpos)); + println!("Spawned rtsim NPC at {:?}", wpos); + } + _ => {}, + } } + + this } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9bbc73a86d..b1413ef4f8 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -196,6 +196,7 @@ fn do_command( ServerChatCommand::DeleteLocation => handle_delete_location, ServerChatCommand::WeatherZone => handle_weather_zone, ServerChatCommand::Lightning => handle_lightning, + ServerChatCommand::Scale => handle_scale, }; handler(server, client, target, args, cmd) @@ -3835,3 +3836,26 @@ fn handle_body( Err(action.help_string()) } } + +fn handle_scale( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + if let Some(scale) = parse_cmd_args!(args, f32) { + let _ = server + .state + .ecs_mut() + .write_storage::() + .insert(target, comp::Scale(scale)); + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, format!("Set scale to {}", scale)), + ); + Ok(()) + } else { + Err(action.help_string()) + } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index ec39cab251..c1b8fd37a9 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -564,7 +564,7 @@ impl Server { // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { - match rtsim2::RtSim::new(&world, data_dir.to_owned()) { + match rtsim2::RtSim::new(index.as_index_ref(), &world, data_dir.to_owned()) { Ok(rtsim) => state.ecs_mut().insert(rtsim), Err(err) => { error!("Failed to load rtsim: {}", err); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index f71555afaa..388ba1b6dc 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -27,7 +27,7 @@ use std::{ use enum_map::EnumMap; use tracing::{error, warn, info, debug}; use vek::*; -use world::World; +use world::{IndexRef, World}; pub struct RtSim { file_path: PathBuf, @@ -36,43 +36,47 @@ pub struct RtSim { } impl RtSim { - pub fn new(world: &World, data_dir: PathBuf) -> Result { + pub fn new(index: IndexRef, world: &World, data_dir: PathBuf) -> Result { let file_path = Self::get_file_path(data_dir); info!("Looking for rtsim data in {}...", file_path.display()); let data = 'load: { - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim data found. Attempting to load..."); - match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { info!("Rtsim data loaded."); break 'load data }, - Err(e) => { - error!("Rtsim data failed to load: {}", e); - let mut i = 0; - loop { - let mut backup_path = file_path.clone(); - backup_path.set_extension(if i == 0 { - format!("backup_{}", i) - } else { - "ron_backup".to_string() - }); - if !backup_path.exists() { - fs::rename(&file_path, &backup_path)?; - warn!("Failed rtsim data was moved to {}", backup_path.display()); - info!("A fresh rtsim data will now be generated."); - break; + if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim data found. Attempting to load..."); + match Data::from_reader(io::BufReader::new(file)) { + Ok(data) => { info!("Rtsim data loaded."); break 'load data }, + Err(e) => { + error!("Rtsim data failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!("Failed rtsim data was moved to {}", backup_path.display()); + info!("A fresh rtsim data will now be generated."); + break; + } + i += 1; } - i += 1; - } - }, - } - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => - info!("No rtsim data found. Generating from world..."), - Err(e) => return Err(e.into()), + }, + } + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => + info!("No rtsim data found. Generating from world..."), + Err(e) => return Err(e.into()), + } + } else { + warn!("'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be overwritten)."); } - let data = Data::generate(&world); + let data = Data::generate(index, &world); info!("Rtsim data generated."); data }; diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bf07399e7c..a1a2b906aa 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -10,6 +10,7 @@ use common::{ slowjob::SlowJobPool, }; use common_ecs::{Job, Origin, Phase, System}; +use rtsim2::data::npc::NpcMode; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; @@ -32,8 +33,9 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, + (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, ) { + let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; rtsim.state.tick(dt.0); @@ -42,6 +44,38 @@ impl<'a> System<'a> for Sys { rtsim.save(&slow_jobs); } + let chunk_states = rtsim.state.resource::(); + for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() { + let chunk = npc.wpos + .xy() + .map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32)); + + // Load the NPC into the world if it's in a loaded chunk and is not already loaded + if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { + npc.mode = NpcMode::Loaded; + + println!("Loading in rtsim NPC at {:?}", npc.wpos); + + let body = comp::Body::Object(comp::object::Body::Scarecrow); + emitter.emit(ServerEvent::CreateNpc { + pos: comp::Pos(npc.wpos), + stats: comp::Stats::new("Rtsim NPC".to_string()), + skill_set: comp::SkillSet::default(), + health: None, + poise: comp::Poise::new(body), + inventory: comp::Inventory::with_empty(), + body, + agent: None, + alignment: comp::Alignment::Wild, + scale: comp::Scale(10.0), + anchor: None, + loot: Default::default(), + rtsim_entity: None, // For now, the old one is used! + projectile: None, + }); + } + } + // rtsim.tick += 1; // Update unloaded rtsim entities, in groups at a time diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 3dcbdef1b2..2340e40b5c 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -776,7 +776,7 @@ impl FigureMgr { .enumerate() { // Velocity relative to the current ground - let rel_vel = anim::vek::Vec3::::from(vel.0 - physics.ground_vel); + let rel_vel = anim::vek::Vec3::::from(vel.0 - physics.ground_vel) / scale.map_or(1.0, |s| s.0); let look_dir = controller.map(|c| c.inputs.look_dir).unwrap_or_default(); let is_viewpoint = scene_data.viewpoint_entity == entity; @@ -1005,7 +1005,7 @@ impl FigureMgr { }); // Average velocity relative to the current ground - let rel_avg_vel = state.avg_vel - physics.ground_vel; + let rel_avg_vel = (state.avg_vel - physics.ground_vel) / scale; let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), @@ -6157,6 +6157,7 @@ impl FigureMgr { None, entity, body, + scale.copied(), inventory, false, pos.0, @@ -6238,6 +6239,7 @@ impl FigureMgr { character_state, entity, body, + scale.copied(), inventory, false, pos.0, @@ -6273,9 +6275,10 @@ impl FigureMgr { let character_state = character_state_storage.get(player_entity); let items = ecs.read_storage::(); - if let (Some(pos), Some(body)) = ( + if let (Some(pos), Some(body), scale) = ( ecs.read_storage::().get(player_entity), ecs.read_storage::().get(player_entity), + ecs.read_storage::().get(player_entity), ) { let healths = state.read_storage::(); let health = healths.get(player_entity); @@ -6292,6 +6295,7 @@ impl FigureMgr { character_state, player_entity, body, + scale.copied(), inventory, true, pos.0, @@ -6325,6 +6329,7 @@ impl FigureMgr { character_state: Option<&CharacterState>, entity: EcsEntity, body: &Body, + scale: Option, inventory: Option<&Inventory>, is_viewpoint: bool, pos: Vec3, @@ -6702,8 +6707,8 @@ impl FigureMgr { } { let model_entry = model_entry?; - let figure_low_detail_distance = figure_lod_render_distance * 0.75; - let figure_mid_detail_distance = figure_lod_render_distance * 0.5; + let figure_low_detail_distance = figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.75; + let figure_mid_detail_distance = figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.5; let model = if pos.distance_squared(cam_pos) > figure_low_detail_distance.powi(2) { model_entry.lod_model(2) @@ -7092,7 +7097,7 @@ impl FigureState { self.last_ori = Lerp::lerp(self.last_ori, *ori, 15.0 * dt).normalized(); - self.state_time += dt * state_animation_rate; + self.state_time += dt * state_animation_rate / scale; let mat = { let scale_mat = anim::vek::Mat4::scaling_3d(anim::vek::Vec3::from(*scale)); @@ -7254,7 +7259,7 @@ impl FigureState { // Can potentially overflow if self.avg_vel.magnitude_squared() != 0.0 { - self.acc_vel += (self.avg_vel - *ground_vel).magnitude() * dt; + self.acc_vel += (self.avg_vel - *ground_vel).magnitude() * dt / scale; } else { self.acc_vel = 0.0; } diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index d5d81f10a7..ca10372782 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -526,7 +526,30 @@ impl Scene { .get(scene_data.viewpoint_entity) .map_or(Quaternion::identity(), |ori| ori.to_quat()); - let (is_humanoid, viewpoint_height, viewpoint_eye_height) = ecs + let viewpoint_scale = ecs + .read_storage::() + .get(scene_data.viewpoint_entity) + .map_or(1.0, |scale| scale.0); + + let viewpoint_rolling = ecs + .read_storage::() + .get(scene_data.viewpoint_entity) + .map_or(false, |cs| cs.is_dodge()); + + let is_running = ecs + .read_storage::() + .get(scene_data.viewpoint_entity) + .map(|v| v.0.magnitude_squared() > RUNNING_THRESHOLD.powi(2)) + .unwrap_or(false); + + let on_ground = ecs + .read_storage::() + .get(scene_data.viewpoint_entity) + .map(|p| p.on_ground.is_some()); + + let (is_humanoid, viewpoint_height, viewpoint_eye_height) = scene_data + .state + .ecs() .read_storage::() .get(scene_data.viewpoint_entity) .map_or((false, 1.0, 0.0), |b| { @@ -602,10 +625,10 @@ impl Scene { let tilt = self.camera.get_orientation().y; let dist = self.camera.get_distance(); - Vec3::unit_z() * (up - tilt.min(0.0).sin() * dist * 0.6) + Vec3::unit_z() * (up * viewpoint_scale - tilt.min(0.0).sin() * dist * 0.6) } else { self.figure_mgr - .viewpoint_offset(scene_data, scene_data.viewpoint_entity) + .viewpoint_offset(scene_data, scene_data.viewpoint_entity) * viewpoint_scale }; match self.camera.get_mode() { From f349e99cfbf804faa02c32242ac95108e9e09e4b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 00:48:43 +0100 Subject: [PATCH 009/139] Better camera at smaller scales --- voxygen/src/scene/camera.rs | 6 +++--- voxygen/src/scene/mod.rs | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 68a480f002..5b8b1850e6 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -551,13 +551,13 @@ impl Camera { /// Zoom with the ability to switch between first and third-person mode. /// /// Note that cap > 18237958000000.0 can cause panic due to float overflow - pub fn zoom_switch(&mut self, delta: f32, cap: f32) { + pub fn zoom_switch(&mut self, delta: f32, cap: f32, scale: f32) { if delta > 0_f32 || self.mode != CameraMode::FirstPerson { let t = self.tgt_dist + delta; const MIN_THIRD_PERSON: f32 = 2.35; match self.mode { CameraMode::ThirdPerson => { - if t < MIN_THIRD_PERSON { + if t < MIN_THIRD_PERSON * scale { self.set_mode(CameraMode::FirstPerson); } else { self.tgt_dist = t; @@ -565,7 +565,7 @@ impl Camera { }, CameraMode::FirstPerson => { self.set_mode(CameraMode::ThirdPerson); - self.tgt_dist = MIN_THIRD_PERSON; + self.tgt_dist = MIN_THIRD_PERSON * scale; }, _ => {}, } diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index ca10372782..ab7cf11a62 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -409,15 +409,16 @@ impl Scene { // when zooming in the distance the camera travelles should be based on the // final distance. This is to make sure the camera travelles the // same distance when zooming in and out + let player_scale = client.state().read_component_copied::(client.entity()).map_or(1.0, |s| s.0); if delta < 0.0 { self.camera.zoom_switch( // Thank you Imbris for doing the math delta * (0.05 + self.camera.get_distance() * 0.01) / (1.0 - delta * 0.01), cap, + player_scale, ); } else { - self.camera - .zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01), cap); + self.camera.zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01), cap, player_scale); } true }, From 1dc75182006b54b6edc2d6ac7081e4c7f3e27d47 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 11:05:01 +0100 Subject: [PATCH 010/139] Added rtsim entity unload hook --- common/src/rtsim.rs | 4 +- common/src/states/utils.rs | 16 +-- rtsim/src/data/npc.rs | 2 +- server/src/cmd.rs | 1 + server/src/events/entity_manipulation.rs | 6 +- server/src/lib.rs | 13 +- server/src/rtsim2/mod.rs | 21 ++- server/src/rtsim2/tick.rs | 3 +- server/src/sys/agent.rs | 2 +- server/src/sys/agent/data.rs | 168 +++++++++++++++++++++++ 10 files changed, 209 insertions(+), 27 deletions(-) create mode 100644 server/src/sys/agent/data.rs diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 19990a9d5b..6bfe625fea 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -9,10 +9,10 @@ use vek::*; use crate::comp::dialogue::MoodState; -pub type RtSimId = usize; +slotmap::new_key_type! { pub struct NpcId; } #[derive(Copy, Clone, Debug)] -pub struct RtSimEntity(pub RtSimId); +pub struct RtSimEntity(pub NpcId); impl Component for RtSimEntity { type Storage = specs::VecStorage; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index e266db3864..2d67ef61be 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -386,7 +386,7 @@ fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) { let accel = if let Some(block) = data.physics.on_ground { // FRIC_GROUND temporarily used to normalize things around expected values data.body.base_accel() - * data.scale.map_or(1.0, |s| s.0) + * data.scale.map_or(1.0, |s| s.0.sqrt()) * block.get_traction() * block.get_friction() / FRIC_GROUND } else { @@ -438,7 +438,7 @@ pub fn handle_forced_movement( data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * Vec2::from(*data.ori) * strength; + Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * Vec2::from(*data.ori) * strength; } }, ForcedMovement::Reverse(strength) => { @@ -448,7 +448,7 @@ pub fn handle_forced_movement( data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * -Vec2::from(*data.ori) * strength; + Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * -Vec2::from(*data.ori) * strength; } }, ForcedMovement::Sideways(strength) => { @@ -470,7 +470,7 @@ pub fn handle_forced_movement( } }; - update.vel.0 += Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0) * direction * strength; + update.vel.0 += Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * direction * strength; } }, ForcedMovement::DirectedReverse(strength) => { @@ -519,7 +519,7 @@ pub fn handle_forced_movement( dir.y, vertical, ) - * data.scale.map_or(1.0, |s| s.0) + * data.scale.map_or(1.0, |s| s.0.sqrt()) // Multiply decreasing amount linearly over time (with average of 1) * 2.0 * progress // Apply direction @@ -533,7 +533,7 @@ pub fn handle_forced_movement( }, ForcedMovement::Hover { move_input } => { update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0) + move_input - * data.scale.map_or(1.0, |s| s.0) + * data.scale.map_or(1.0, |s| s.0.sqrt()) * data.inputs.move_dir.try_normalized().unwrap_or_default(); }, } @@ -575,7 +575,7 @@ pub fn handle_orientation( }; // unit is multiples of 180° let half_turns_per_tick = data.body.base_ori_rate() - / data.scale.map_or(1.0, |s| s.0) + / data.scale.map_or(1.0, |s| s.0.sqrt()) * efficiency * if data.physics.on_ground.is_some() { 1.0 @@ -1075,7 +1075,7 @@ pub fn handle_jump( output_events.emit_local(LocalEvent::Jump( data.entity, strength * impulse / data.mass.0 - * data.scale.map_or(1.0, |s| s.0.sqrt()) + * data.scale.map_or(1.0, |s| s.0.powf(0.25)) * data.stats.move_speed_modifier, )); }) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index ce8e0054fa..6f2f0d6151 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -5,7 +5,7 @@ use vek::*; use std::ops::{Deref, DerefMut}; use common::uid::Uid; -slotmap::new_key_type! { pub struct NpcId; } +pub use common::rtsim::NpcId; #[derive(Clone, Serialize, Deserialize)] pub struct Npc { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b1413ef4f8..70c678ff74 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -3845,6 +3845,7 @@ fn handle_scale( action: &ServerChatCommand, ) -> CmdResult<()> { if let Some(scale) = parse_cmd_args!(args, f32) { + let scale = scale.clamped(0.025, 1000.0); let _ = server .state .ecs_mut() diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index bb9913681e..8046e23e9b 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -7,7 +7,7 @@ use crate::{ skillset::SkillGroupKind, BuffKind, BuffSource, PhysicsState, }, - rtsim::RtSim, + // rtsim::RtSim, sys::terrain::SAFE_ZONE_RADIUS, Server, SpawnPoint, StateExt, }; @@ -26,8 +26,8 @@ use common::{ event::{EventBus, ServerEvent}, outcome::{HealthChangeInfo, Outcome}, resources::{Secs, Time}, - rtsim::RtSimEntity, - states::utils::StageSection, + // rtsim::RtSimEntity, + states::utils::{AbilityInfo, StageSection}, terrain::{Block, BlockKind, TerrainGrid}, uid::{Uid, UidAllocator}, util::Dir, diff --git a/server/src/lib.rs b/server/src/lib.rs index c1b8fd37a9..de19d3a7c7 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -30,7 +30,8 @@ pub mod metrics; pub mod persistence; mod pet; pub mod presence; -pub mod rtsim; +// TODO: Remove +//pub mod rtsim; pub mod rtsim2; pub mod settings; pub mod state_ext; @@ -65,7 +66,7 @@ use crate::{ login_provider::LoginProvider, persistence::PersistedComponents, presence::{Presence, RegionSubscription, RepositionOnChunkLoad}, - rtsim::RtSim, + // rtsim::RtSim, state_ext::StateExt, sys::sentinel::DeletedEntities, }; @@ -386,6 +387,7 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); + state.ecs_mut().register::(); // Load banned words list let banned_words = settings.moderation.load_banned_words(data_dir); @@ -838,8 +840,8 @@ impl Server { }; for entity in to_delete { - /* // Assimilate entities that are part of the real-time world simulation + #[cfg(feature = "worldgen")] if let Some(rtsim_entity) = self .state .ecs() @@ -849,10 +851,9 @@ impl Server { { self.state .ecs() - .write_resource::() - .assimilate_entity(rtsim_entity.0); + .write_resource::() + .hook_rtsim_entity_unload(rtsim_entity); } - */ if let Err(e) = self.state.delete_entity_recorded(entity) { error!(?e, "Failed to delete agent outside the terrain"); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 388ba1b6dc..fa73f1fe0c 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -5,13 +5,17 @@ pub mod tick; use common::{ grid::Grid, slowjob::SlowJobPool, - rtsim::ChunkResource, + rtsim::{ChunkResource, RtSimEntity}, terrain::{TerrainChunk, Block}, vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; use rtsim2::{ - data::{Data, ReadError}, + data::{ + npc::NpcMode, + Data, + ReadError, + }, rule::Rule, RtState, }; @@ -116,6 +120,16 @@ impl RtSim { } } + pub fn hook_block_update(&mut self, wpos: Vec3, old: Block, new: Block) { + self.state.emit(event::OnBlockChange { wpos, old, new }); + } + + pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { + if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { + npc.mode = NpcMode::Simulated; + } + } + pub fn save(&mut self, slowjob_pool: &SlowJobPool) { info!("Saving rtsim data..."); let file_path = self.file_path.clone(); @@ -157,9 +171,6 @@ impl RtSim { pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { self.state.data().nature.get_chunk_resources(key) } - pub fn hook_block_update(&mut self, wpos: Vec3, old: Block, new: Block) { - self.state.emit(event::OnBlockChange { wpos, old, new }); - } } struct ChunkStates(pub Grid>); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index a1a2b906aa..1f1fdc0d3d 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -8,6 +8,7 @@ use common::{ generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, slowjob::SlowJobPool, + rtsim::RtSimEntity, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::npc::NpcMode; @@ -70,7 +71,7 @@ impl<'a> System<'a> for Sys { scale: comp::Scale(10.0), anchor: None, loot: Default::default(), - rtsim_entity: None, // For now, the old one is used! + rtsim_entity: Some(RtSimEntity(npc_id)), projectile: None, }); } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 2098dac8bb..d32581f147 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2,7 +2,7 @@ pub mod behavior_tree; pub use server_agent::{action_nodes, attack, consts, data, util}; use crate::{ - rtsim::RtSim, + // rtsim::{entity::PersonalityTrait, RtSim}, sys::agent::{ behavior_tree::{BehaviorData, BehaviorTree}, data::{AgentData, ReadData}, diff --git a/server/src/sys/agent/data.rs b/server/src/sys/agent/data.rs new file mode 100644 index 0000000000..e443e67cff --- /dev/null +++ b/server/src/sys/agent/data.rs @@ -0,0 +1,168 @@ +// use crate::rtsim::Entity as RtSimData; +use common::{ + comp::{ + buff::Buffs, group, item::MaterialStatManifest, ActiveAbilities, Alignment, Body, + CharacterState, Combo, Energy, Health, Inventory, LightEmitter, LootOwner, Ori, + PhysicsState, Pos, Scale, SkillSet, Stats, Vel, + }, + link::Is, + mounting::Mount, + path::TraversalConfig, + resources::{DeltaTime, Time, TimeOfDay}, + // rtsim::RtSimEntity, + terrain::TerrainGrid, + uid::{Uid, UidAllocator}, +}; +use specs::{ + shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, + World, +}; +use std::sync::Arc; + +pub struct AgentData<'a> { + pub entity: &'a EcsEntity, + //pub rtsim_entity: Option<&'a RtSimData>, + pub uid: &'a Uid, + pub pos: &'a Pos, + pub vel: &'a Vel, + pub ori: &'a Ori, + pub energy: &'a Energy, + pub body: Option<&'a Body>, + pub inventory: &'a Inventory, + pub skill_set: &'a SkillSet, + #[allow(dead_code)] // may be useful for pathing + pub physics_state: &'a PhysicsState, + pub alignment: Option<&'a Alignment>, + pub traversal_config: TraversalConfig, + pub scale: f32, + pub damage: f32, + pub light_emitter: Option<&'a LightEmitter>, + pub glider_equipped: bool, + pub is_gliding: bool, + pub health: Option<&'a Health>, + pub char_state: &'a CharacterState, + pub active_abilities: &'a ActiveAbilities, + pub cached_spatial_grid: &'a common::CachedSpatialGrid, + pub msm: &'a MaterialStatManifest, +} + +pub struct TargetData<'a> { + pub pos: &'a Pos, + pub body: Option<&'a Body>, + pub scale: Option<&'a Scale>, +} + +impl<'a> TargetData<'a> { + pub fn new(pos: &'a Pos, body: Option<&'a Body>, scale: Option<&'a Scale>) -> Self { + Self { pos, body, scale } + } +} + +pub struct AttackData { + pub min_attack_dist: f32, + pub dist_sqrd: f32, + pub angle: f32, + pub angle_xy: f32, +} + +impl AttackData { + pub fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) } +} + +#[derive(Eq, PartialEq)] +// When adding a new variant, first decide if it should instead fall under one +// of the pre-existing tactics +pub enum Tactic { + // General tactics + SimpleMelee, + SimpleBackstab, + ElevatedRanged, + Turret, + FixedTurret, + RotatingTurret, + RadialTurret, + + // Tool specific tactics + Axe, + Hammer, + Sword, + Bow, + Staff, + Sceptre, + + // Broad creature tactics + CircleCharge { radius: u32, circle_time: u32 }, + QuadLowRanged, + TailSlap, + QuadLowQuick, + QuadLowBasic, + QuadLowBeam, + QuadMedJump, + QuadMedBasic, + Theropod, + BirdLargeBreathe, + BirdLargeFire, + BirdLargeBasic, + ArthropodMelee, + ArthropodRanged, + ArthropodAmbush, + + // Specific species tactics + Mindflayer, + Minotaur, + ClayGolem, + TidalWarrior, + Yeti, + Harvester, + StoneGolem, + Deadwood, + Mandragora, + WoodGolem, + GnarlingChieftain, + OrganAura, + Dagon, + Cardinal, +} + +#[derive(SystemData)] +pub struct ReadData<'a> { + pub entities: Entities<'a>, + pub uid_allocator: Read<'a, UidAllocator>, + pub dt: Read<'a, DeltaTime>, + pub time: Read<'a, Time>, + pub cached_spatial_grid: Read<'a, common::CachedSpatialGrid>, + pub group_manager: Read<'a, group::GroupManager>, + pub energies: ReadStorage<'a, Energy>, + pub positions: ReadStorage<'a, Pos>, + pub velocities: ReadStorage<'a, Vel>, + pub orientations: ReadStorage<'a, Ori>, + pub scales: ReadStorage<'a, Scale>, + pub healths: ReadStorage<'a, Health>, + pub inventories: ReadStorage<'a, Inventory>, + pub stats: ReadStorage<'a, Stats>, + pub skill_set: ReadStorage<'a, SkillSet>, + pub physics_states: ReadStorage<'a, PhysicsState>, + pub char_states: ReadStorage<'a, CharacterState>, + pub uids: ReadStorage<'a, Uid>, + pub groups: ReadStorage<'a, group::Group>, + pub terrain: ReadExpect<'a, TerrainGrid>, + pub alignments: ReadStorage<'a, Alignment>, + pub bodies: ReadStorage<'a, Body>, + pub is_mounts: ReadStorage<'a, Is>, + pub time_of_day: Read<'a, TimeOfDay>, + pub light_emitter: ReadStorage<'a, LightEmitter>, + #[cfg(feature = "worldgen")] + pub world: ReadExpect<'a, Arc>, + //pub rtsim_entities: ReadStorage<'a, RtSimEntity>, + pub buffs: ReadStorage<'a, Buffs>, + pub combos: ReadStorage<'a, Combo>, + pub active_abilities: ReadStorage<'a, ActiveAbilities>, + pub loot_owners: ReadStorage<'a, LootOwner>, + pub msm: ReadExpect<'a, MaterialStatManifest>, +} + +pub enum Path { + Full, + Separate, + Partial, +} From c856f2625c1300f420216bd0e811e5fdefdf9985 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 15:44:57 +0100 Subject: [PATCH 011/139] Added rtsim sites --- common/src/rtsim.rs | 2 + rtsim/src/data/mod.rs | 10 ++--- rtsim/src/data/npc.rs | 49 ++++++++++++++++++--- rtsim/src/data/site.rs | 47 ++++++++++++++++++++ rtsim/src/event.rs | 15 ++++++- rtsim/src/gen/mod.rs | 40 ++++++++++------- rtsim/src/gen/site.rs | 25 +++++++++++ rtsim/src/lib.rs | 28 +++++++----- rtsim/src/rule.rs | 3 +- rtsim/src/rule/example.rs | 19 -------- rtsim/src/rule/setup.rs | 48 ++++++++++++++++++++ rtsim/src/rule/simulate_npcs.rs | 25 +++++++++++ server/src/events/entity_manipulation.rs | 9 ++-- server/src/lib.rs | 8 +++- server/src/rtsim2/mod.rs | 14 ++++-- server/src/rtsim2/rule.rs | 2 +- server/src/rtsim2/rule/deplete_resources.rs | 18 ++++---- server/src/rtsim2/tick.rs | 8 ++-- voxygen/src/scene/debug.rs | 44 +++++++++++------- voxygen/src/scene/mod.rs | 27 ++++++++---- world/src/civ/mod.rs | 1 + 21 files changed, 333 insertions(+), 109 deletions(-) create mode 100644 rtsim/src/data/site.rs create mode 100644 rtsim/src/gen/site.rs delete mode 100644 rtsim/src/rule/example.rs create mode 100644 rtsim/src/rule/setup.rs create mode 100644 rtsim/src/rule/simulate_npcs.rs diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6bfe625fea..80b541c1c7 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -11,6 +11,8 @@ use crate::comp::dialogue::MoodState; slotmap::new_key_type! { pub struct NpcId; } +slotmap::new_key_type! { pub struct SiteId; } + #[derive(Copy, Clone, Debug)] pub struct RtSimEntity(pub NpcId); diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index c68797db4f..e1a7d66adb 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,8 +1,10 @@ pub mod npc; +pub mod site; pub mod nature; pub use self::{ npc::{Npc, NpcId, Npcs}, + site::{Site, SiteId, Sites}, nature::Nature, }; @@ -25,6 +27,7 @@ pub enum Actor { pub struct Data { pub nature: Nature, pub npcs: Npcs, + pub sites: Sites, } pub type ReadError = rmp_serde::decode::Error; @@ -40,13 +43,6 @@ impl Data { } } -// fn rugged_ser_enum_map + Serialize, V: PartialEq + Default + Serialize, S: ser::Serializer>(map: &EnumMap, mut ser: S) -> Result { -// ser.collect_map(map -// .iter() -// .filter(|(k, v)| v != &&V::default()) -// .map(|(k, v)| (k, v))) -// } - fn rugged_ser_enum_map< K: EnumArray + Serialize, V: From + PartialEq + Serialize, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 6f2f0d6151..c01b3b0081 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,21 +3,47 @@ use serde::{Serialize, Deserialize}; use slotmap::HopSlotMap; use vek::*; use std::ops::{Deref, DerefMut}; -use common::uid::Uid; - +use common::{ + uid::Uid, + store::Id, + rtsim::SiteId, +}; pub use common::rtsim::NpcId; #[derive(Clone, Serialize, Deserialize)] pub struct Npc { - pub wpos: Vec3, + // Persisted state + + /// Represents the location of the NPC. + pub loc: NpcLoc, + + // Unpersisted state + + /// The position of the NPC in the world. Note that this is derived from [`Npc::loc`] and cannot be updated manually + #[serde(skip_serializing, skip_deserializing)] + wpos: Vec3, + /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being + /// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should + /// instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] pub mode: NpcMode, } impl Npc { - pub fn at(wpos: Vec3) -> Self { - Self { wpos, mode: NpcMode::Simulated } + pub fn new(loc: NpcLoc) -> Self { + Self { + loc, + wpos: Vec3::zero(), + mode: NpcMode::Simulated, + } } + + pub fn wpos(&self) -> Vec3 { self.wpos } + + /// You almost certainly *DO NOT* want to use this method. + /// + /// Update the NPC's wpos as a result of routine NPC simulation derived from its location. + pub(crate) fn tick_wpos(&mut self, wpos: Vec3) { self.wpos = wpos; } } #[derive(Copy, Clone, Default)] @@ -29,13 +55,24 @@ pub enum NpcMode { Loaded, } +#[derive(Clone, Serialize, Deserialize)] +pub enum NpcLoc { + Wild { wpos: Vec3 }, + Site { site: SiteId, wpos: Vec3 }, + Travelling { + a: SiteId, + b: SiteId, + frac: f32, + }, +} + #[derive(Clone, Serialize, Deserialize)] pub struct Npcs { pub npcs: HopSlotMap, } impl Npcs { - pub fn spawn(&mut self, npc: Npc) -> NpcId { + pub fn create(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } } diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs new file mode 100644 index 0000000000..6e34be73d0 --- /dev/null +++ b/rtsim/src/data/site.rs @@ -0,0 +1,47 @@ +use hashbrown::HashMap; +use serde::{Serialize, Deserialize}; +use slotmap::HopSlotMap; +use vek::*; +use std::ops::{Deref, DerefMut}; +use common::{ + uid::Uid, + store::Id, +}; +pub use common::rtsim::SiteId; +use world::site::Site as WorldSite; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Site { + pub wpos: Vec2, + + /// The site generated during initial worldgen that this site corresponds to. + /// + /// Eventually, rtsim should replace initial worldgen's site system and this will not be necessary. + /// + /// When setting up rtsim state, we try to 'link' these two definitions of a site: but if initial worldgen has + /// changed, this might not be possible. We try to delete sites that no longer exist during setup, but this is an + /// inherent fallible process. If linking fails, we try to delete the site in rtsim2 in order to avoid an + /// 'orphaned' site. (TODO: create new sites for new initial worldgen sites that come into being too). + #[serde(skip_serializing, skip_deserializing)] + pub world_site: Option>, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Sites { + pub sites: HopSlotMap, +} + +impl Sites { + pub fn create(&mut self, site: Site) -> SiteId { + self.sites.insert(site) + } +} + +impl Deref for Sites { + type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.sites } +} + +impl DerefMut for Sites { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.sites } +} diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index deb35cc68c..298cebf8c8 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -1,8 +1,21 @@ use common::resources::Time; +use world::{World, IndexRef}; +use super::{Rule, RtState}; pub trait Event: Clone + 'static {} +pub struct EventCtx<'a, R: Rule, E: Event> { + pub state: &'a RtState, + pub rule: &'a mut R, + pub event: &'a E, + pub world: &'a World, + pub index: IndexRef<'a>, +} + +#[derive(Clone)] +pub struct OnSetup; +impl Event for OnSetup {} + #[derive(Clone)] pub struct OnTick { pub dt: f32 } - impl Event for OnTick {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index fb2d3829a8..4e0877c240 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,6 +1,14 @@ -use crate::data::{Npcs, Npc, Data, Nature}; +pub mod site; + +use crate::data::{ + npc::{Npcs, Npc, NpcLoc}, + site::{Sites, Site}, + Data, + Nature, +}; use hashbrown::HashMap; use rand::prelude::*; +use tracing::info; use world::{ site::SiteKind, IndexRef, @@ -8,7 +16,7 @@ use world::{ }; impl Data { - pub fn generate(index: IndexRef, world: &World) -> Self { + pub fn generate(world: &World, index: IndexRef) -> Self { let mut seed = [0; 32]; seed.iter_mut().zip(&mut index.seed.to_le_bytes()).for_each(|(dst, src)| *dst = *src); let mut rng = SmallRng::from_seed(seed); @@ -16,25 +24,25 @@ impl Data { let mut this = Self { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default() }, + sites: Sites { sites: Default::default() }, }; - for (site_id, site) in world - .civs() + // Register sites with rtsim + for (world_site_id, _) in index .sites .iter() - .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) { - match &site.kind { - SiteKind::Refactor(site2) => { - let wpos = site.get_origin() - .map(|e| e as f32 + 0.5) - .with_z(world.sim().get_alt_approx(site.get_origin()).unwrap_or(0.0)); - // TODO: Better API - this.npcs.spawn(Npc::at(wpos)); - println!("Spawned rtsim NPC at {:?}", wpos); - } - _ => {}, - } + let site = Site::generate(world_site_id, world, index); + this.sites.create(site); + } + info!("Registering {} rtsim sites from world sites.", this.sites.len()); + + // Spawn some test entities at the sites + for (site_id, site) in this.sites.iter() { + let wpos = site.wpos.map(|e| e as f32) + .with_z(world.sim().get_alt_approx(site.wpos).unwrap_or(0.0)); + this.npcs.create(Npc::new(NpcLoc::Site { site: site_id, wpos })); + println!("Spawned rtsim NPC at {:?}", wpos); } this diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs new file mode 100644 index 0000000000..75a0876611 --- /dev/null +++ b/rtsim/src/gen/site.rs @@ -0,0 +1,25 @@ +use crate::data::Site; +use common::store::Id; +use world::{ + site::Site as WorldSite, + World, + IndexRef, +}; + +impl Site { + pub fn generate(world_site: Id, world: &World, index: IndexRef) -> Self { + // match &world_site.kind { + // SiteKind::Refactor(site2) => { + // let site = Site::generate(world_site_id, world, index); + // println!("Registering rtsim site at {:?}...", site.wpos); + // this.sites.create(site); + // } + // _ => {}, + // } + + Self { + wpos: index.sites.get(world_site).get_origin(), + world_site: Some(world_site), + } + } +} diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 1397d99dd8..84de682836 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -7,9 +7,10 @@ pub mod rule; pub use self::{ data::Data, - event::{Event, OnTick}, + event::{Event, EventCtx, OnTick}, rule::{Rule, RuleError}, }; +use world::{World, IndexRef}; use anymap2::SendSyncAnyMap; use tracing::{info, error}; use atomic_refcell::AtomicRefCell; @@ -25,7 +26,7 @@ pub struct RtState { } type RuleState = AtomicRefCell; -type EventHandlersOf = Vec>; +type EventHandlersOf = Vec>; impl RtState { pub fn new(data: Data) -> Self { @@ -48,7 +49,8 @@ impl RtState { fn start_default_rules(&mut self) { info!("Starting default rtsim rules..."); - self.start_rule::(); + self.start_rule::(); + self.start_rule::(); } pub fn start_rule(&mut self) { @@ -66,13 +68,19 @@ impl RtState { .borrow_mut() } - pub fn bind(&mut self, mut f: impl FnMut(&mut R, &RtState, E) + Send + Sync + 'static) { + pub fn bind(&mut self, mut f: impl FnMut(EventCtx) + Send + Sync + 'static) { let f = AtomicRefCell::new(f); self.event_handlers .entry::>() .or_default() - .push(Box::new(move |rtstate, event| { - (f.borrow_mut())(&mut rtstate.rule_mut(), rtstate, event) + .push(Box::new(move |state, world, index, event| { + (f.borrow_mut())(EventCtx { + state, + rule: &mut state.rule_mut(), + event, + world, + index, + }) })); } @@ -93,15 +101,15 @@ impl RtState { .borrow_mut() } - pub fn emit(&mut self, e: E) { + pub fn emit(&mut self, e: E, world: &World, index: IndexRef) { self.event_handlers .get::>() .map(|handlers| handlers .iter() - .for_each(|f| f(self, e.clone()))); + .for_each(|f| f(self, world, index, &e))); } - pub fn tick(&mut self, dt: f32) { - self.emit(OnTick { dt }); + pub fn tick(&mut self, world: &World, index: IndexRef, dt: f32) { + self.emit(OnTick { dt }, world, index); } } diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs index 6f6b701740..0547d4d277 100644 --- a/rtsim/src/rule.rs +++ b/rtsim/src/rule.rs @@ -1,4 +1,5 @@ -pub mod example; +pub mod setup; +pub mod simulate_npcs; use std::fmt; use super::RtState; diff --git a/rtsim/src/rule/example.rs b/rtsim/src/rule/example.rs deleted file mode 100644 index d4cda69920..0000000000 --- a/rtsim/src/rule/example.rs +++ /dev/null @@ -1,19 +0,0 @@ -use tracing::info; -use crate::{ - event::OnTick, - RtState, Rule, RuleError, -}; - -pub struct RuleState; - -impl Rule for RuleState { - fn start(rtstate: &mut RtState) -> Result { - info!("Hello from example rule!"); - - rtstate.bind::(|this, rtstate, event| { - // println!("Tick!"); - }); - - Ok(Self) - } -} diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs new file mode 100644 index 0000000000..5bdb00a373 --- /dev/null +++ b/rtsim/src/rule/setup.rs @@ -0,0 +1,48 @@ +use tracing::warn; +use crate::{ + data::Site, + event::OnSetup, + RtState, Rule, RuleError, +}; + +/// This rule runs at rtsim startup and broadly acts to perform some primitive migration/sanitisation in order to +/// ensure that the state of rtsim is mostly sensible. +pub struct Setup; + +impl Rule for Setup { + fn start(rtstate: &mut RtState) -> Result { + rtstate.bind::(|ctx| { + // Delete rtsim sites that don't correspond to a world site + ctx.state.data_mut().sites.retain(|site_id, site| { + if let Some((world_site_id, _)) = ctx.index.sites + .iter() + .find(|(_, world_site)| world_site.get_origin() == site.wpos) + { + site.world_site = Some(world_site_id); + true + } else { + warn!("{:?} is no longer valid because the site it was derived from no longer exists. It will now be deleted.", site_id); + false + } + }); + + // Generate rtsim sites for world sites that don't have a corresponding rtsim site yet + for (world_site_id, _) in ctx.index.sites.iter() { + if !ctx.state.data().sites + .values() + .any(|site| site.world_site.expect("Rtsim site not assigned to world site") == world_site_id) + { + warn!("{:?} is new and does not have a corresponding rtsim site. One will now be generated afresh.", world_site_id); + ctx.state + .data_mut() + .sites + .create(Site::generate(world_site_id, ctx.world, ctx.index)); + } + } + + // TODO: Reassign sites for NPCs if they don't have one + }); + + Ok(Self) + } +} diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs new file mode 100644 index 0000000000..dae9ed6502 --- /dev/null +++ b/rtsim/src/rule/simulate_npcs.rs @@ -0,0 +1,25 @@ +use tracing::info; +use crate::{ + data::npc::NpcLoc, + event::OnTick, + RtState, Rule, RuleError, +}; + +pub struct SimulateNpcs; + +impl Rule for SimulateNpcs { + fn start(rtstate: &mut RtState) -> Result { + + rtstate.bind::(|ctx| { + for (_, npc) in ctx.state.data_mut().npcs.iter_mut() { + npc.tick_wpos(match npc.loc { + NpcLoc::Wild { wpos } => wpos, + NpcLoc::Site { site, wpos } => wpos, + NpcLoc::Travelling { a, b, frac } => todo!(), + }); + } + }); + + Ok(Self) + } +} diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 8046e23e9b..3307d80ed8 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -9,6 +9,7 @@ use crate::{ }, // rtsim::RtSim, sys::terrain::SAFE_ZONE_RADIUS, + rtsim2, Server, SpawnPoint, StateExt, }; use authc::Uuid; @@ -26,7 +27,7 @@ use common::{ event::{EventBus, ServerEvent}, outcome::{HealthChangeInfo, Outcome}, resources::{Secs, Time}, - // rtsim::RtSimEntity, + rtsim::RtSimEntity, states::utils::{AbilityInfo, StageSection}, terrain::{Block, BlockKind, TerrainGrid}, uid::{Uid, UidAllocator}, @@ -519,7 +520,6 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt } if should_delete { - /* if let Some(rtsim_entity) = state .ecs() .read_storage::() @@ -528,10 +528,9 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt { state .ecs() - .write_resource::() - .destroy_entity(rtsim_entity.0); + .write_resource::() + .hook_rtsim_entity_delete(rtsim_entity); } - */ if let Err(e) = state.delete_entity_recorded(entity) { error!(?e, ?entity, "Failed to delete destroyed entity"); diff --git a/server/src/lib.rs b/server/src/lib.rs index de19d3a7c7..e7baa3c55e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -707,7 +707,13 @@ impl Server { fn on_block_update(ecs: &specs::World, wpos: Vec3, old_block: Block, new_block: Block) { // When a resource block updates, inform rtsim if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { - ecs.write_resource::().hook_block_update(wpos, old_block, new_block); + ecs.write_resource::().hook_block_update( + &ecs.read_resource::>(), + ecs.read_resource::>().as_index_ref(), + wpos, + old_block, + new_block, + ); } } diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index fa73f1fe0c..a0f6cf6d33 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -17,6 +17,7 @@ use rtsim2::{ ReadError, }, rule::Rule, + event::OnSetup, RtState, }; use specs::{DispatcherBuilder, WorldExt}; @@ -80,7 +81,7 @@ impl RtSim { warn!("'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be overwritten)."); } - let data = Data::generate(index, &world); + let data = Data::generate(&world, index); info!("Rtsim data generated."); data }; @@ -94,6 +95,8 @@ impl RtSim { rule::start_rules(&mut this.state); + this.state.emit(OnSetup, world, index); + Ok(this) } @@ -120,8 +123,8 @@ impl RtSim { } } - pub fn hook_block_update(&mut self, wpos: Vec3, old: Block, new: Block) { - self.state.emit(event::OnBlockChange { wpos, old, new }); + pub fn hook_block_update(&mut self, world: &World, index: IndexRef, wpos: Vec3, old: Block, new: Block) { + self.state.emit(event::OnBlockChange { wpos, old, new }, world, index); } pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { @@ -130,6 +133,11 @@ impl RtSim { } } + pub fn hook_rtsim_entity_delete(&mut self, entity: RtSimEntity) { + // TODO: Emit event on deletion to catch death? + self.state.data_mut().npcs.remove(entity.0); + } + pub fn save(&mut self, slowjob_pool: &SlowJobPool) { info!("Saving rtsim data..."); let file_path = self.file_path.clone(); diff --git a/server/src/rtsim2/rule.rs b/server/src/rtsim2/rule.rs index 41dd191507..2f349b5368 100644 --- a/server/src/rtsim2/rule.rs +++ b/server/src/rtsim2/rule.rs @@ -5,5 +5,5 @@ use rtsim2::RtState; pub fn start_rules(rtstate: &mut RtState) { info!("Starting server rtsim rules..."); - rtstate.start_rule::(); + rtstate.start_rule::(); } diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs index fe13a2d190..1041576977 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -9,20 +9,20 @@ use common::{ vol::RectRasterableVol, }; -pub struct State; +pub struct DepleteResources; -impl Rule for State { +impl Rule for DepleteResources { fn start(rtstate: &mut RtState) -> Result { info!("Hello from the resource depletion rule!"); - rtstate.bind::(|this, rtstate, event| { - let key = event.wpos + rtstate.bind::(|ctx| { + let key = ctx.event.wpos .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); - if let Some(Some(chunk_state)) = rtstate.resource_mut::().0.get(key) { - let mut chunk_res = rtstate.data().nature.get_chunk_resources(key); + if let Some(Some(chunk_state)) = ctx.state.resource_mut::().0.get(key) { + let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key); // Remove resources - if let Some(res) = event.old.get_rtsim_resource() { + if let Some(res) = ctx.event.old.get_rtsim_resource() { if chunk_state.max_res[res] > 0 { chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 - 1.0) .round() @@ -30,7 +30,7 @@ impl Rule for State { } } // Add resources - if let Some(res) = event.new.get_rtsim_resource() { + if let Some(res) = ctx.event.new.get_rtsim_resource() { if chunk_state.max_res[res] > 0 { chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + 1.0) .round() @@ -38,7 +38,7 @@ impl Rule for State { } } println!("Chunk resources = {:?}", chunk_res); - rtstate.data_mut().nature.set_chunk_resources(key, chunk_res); + ctx.state.data_mut().nature.set_chunk_resources(key, chunk_res); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 1f1fdc0d3d..bcd577594b 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -39,7 +39,7 @@ impl<'a> System<'a> for Sys { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - rtsim.state.tick(dt.0); + rtsim.state.tick(&world, index.as_index_ref(), dt.0); if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { rtsim.save(&slow_jobs); @@ -47,7 +47,7 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() { - let chunk = npc.wpos + let chunk = npc.wpos() .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32)); @@ -55,11 +55,9 @@ impl<'a> System<'a> for Sys { if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { npc.mode = NpcMode::Loaded; - println!("Loading in rtsim NPC at {:?}", npc.wpos); - let body = comp::Body::Object(comp::object::Body::Scarecrow); emitter.emit(ServerEvent::CreateNpc { - pos: comp::Pos(npc.wpos), + pos: comp::Pos(npc.wpos()), stats: comp::Stats::new("Rtsim NPC".to_string()), skill_set: comp::SkillSet::default(), health: None, diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index b0fb2959f7..3ab11ba0d5 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -7,7 +7,7 @@ use hashbrown::{HashMap, HashSet}; use tracing::warn; use vek::*; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum DebugShape { Line([Vec3; 2]), Cylinder { @@ -261,7 +261,8 @@ pub struct DebugShapeId(pub u64); pub struct Debug { next_shape_id: DebugShapeId, - pending_shapes: HashMap, + shapes: HashMap, + pending: HashSet, pending_locals: HashMap, pending_deletes: HashSet, models: HashMap, Bound>)>, @@ -272,7 +273,8 @@ impl Debug { pub fn new() -> Debug { Debug { next_shape_id: DebugShapeId(0), - pending_shapes: HashMap::new(), + shapes: HashMap::new(), + pending: HashSet::new(), pending_locals: HashMap::new(), pending_deletes: HashSet::new(), models: HashMap::new(), @@ -286,10 +288,15 @@ impl Debug { if matches!(shape, DebugShape::TrainTrack { .. }) { self.casts_shadow.insert(id); } - self.pending_shapes.insert(id, shape); + self.shapes.insert(id, shape); + self.pending.insert(id); id } + pub fn get_shape(&self, id: DebugShapeId) -> Option<&DebugShape> { + self.shapes.get(&id) + } + pub fn set_context(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4], ori: [f32; 4]) { self.pending_locals.insert(id, (pos, color, ori)); } @@ -297,19 +304,21 @@ impl Debug { pub fn remove_shape(&mut self, id: DebugShapeId) { self.pending_deletes.insert(id); } pub fn maintain(&mut self, renderer: &mut Renderer) { - for (id, shape) in self.pending_shapes.drain() { - if let Some(model) = renderer.create_model(&shape.mesh()) { - let locals = renderer.create_debug_bound_locals(&[DebugLocals { - pos: [0.0; 4], - color: [1.0, 0.0, 0.0, 1.0], - ori: [0.0, 0.0, 0.0, 1.0], - }]); - self.models.insert(id, (model, locals)); - } else { - warn!( - "Failed to create model for debug shape {:?}: {:?}", - id, shape - ); + for id in self.pending.drain() { + if let Some(shape) = self.shapes.get(&id) { + if let Some(model) = renderer.create_model(&shape.mesh()) { + let locals = renderer.create_debug_bound_locals(&[DebugLocals { + pos: [0.0; 4], + color: [1.0, 0.0, 0.0, 1.0], + ori: [0.0, 0.0, 0.0, 1.0], + }]); + self.models.insert(id, (model, locals)); + } else { + warn!( + "Failed to create model for debug shape {:?}: {:?}", + id, shape + ); + } } } for (id, (pos, color, ori)) in self.pending_locals.drain() { @@ -330,6 +339,7 @@ impl Debug { } for id in self.pending_deletes.drain() { self.models.remove(&id); + self.shapes.remove(&id); } } diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index ab7cf11a62..08cccdf9bb 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1465,17 +1465,28 @@ impl Scene { z_min, z_max, } => { + let scale = scale.map_or(1.0, |s| s.0); current_entities.insert(entity); - let s = scale.map_or(1.0, |sc| sc.0); + + let shape = DebugShape::CapsulePrism { + p0: *p0 * scale, + p1: *p1 * scale, + radius: *radius * scale, + height: (*z_max - *z_min) * scale, + }; + + // If this shape no longer matches, remove the old one + if let Some(shape_id) = hitboxes.get(&entity) { + if self.debug.get_shape(*shape_id).map_or(false, |s| s != &shape) { + self.debug.remove_shape(*shape_id); + hitboxes.remove(&entity); + } + } + let shape_id = hitboxes.entry(entity).or_insert_with(|| { - self.debug.add_shape(DebugShape::CapsulePrism { - p0: *p0 * s, - p1: *p1 * s, - radius: *radius * s, - height: (*z_max - *z_min) * s, - }) + self.debug.add_shape(shape) }); - let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min, 0.0]; + let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min * scale, 0.0]; let color = if group == Some(&comp::group::ENEMY) { [1.0, 0.0, 0.0, 0.5] } else if group == Some(&comp::group::NPC) { diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 3f7d6e7ea7..6aaef9b714 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -1430,6 +1430,7 @@ fn find_site_loc( }); } } + debug!("Failed to place site {:?}.", site_kind); None } From f140a94dc6a9aa6e4f86098bdfaa4ce3885faf3e Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 17:06:00 +0100 Subject: [PATCH 012/139] Fixed scaled terrain collisions --- common/systems/src/phys.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 2e9bf97adc..e1291a5712 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1350,15 +1350,16 @@ fn box_voxel_collision + ReadVol>( read: &PhysicsRead, ori: &Ori, ) { - let scale = read.scales.get(entity).map_or(1.0, |s| s.0); + // We cap out scale at 10.0 to prevent an enormous amount of lag + let scale = read.scales.get(entity).map_or(1.0, |s| s.0.min(10.0)); //prof_span!("box_voxel_collision"); // Convience function to compute the player aabb - fn player_aabb(pos: Vec3, radius: f32, z_range: Range, scale: f32) -> Aabb { + fn player_aabb(pos: Vec3, radius: f32, z_range: Range) -> Aabb { Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start) * scale, - max: pos + Vec3::new(radius, radius, z_range.end) * scale, + min: pos + Vec3::new(-radius, -radius, z_range.start), + max: pos + Vec3::new(radius, radius, z_range.end), } } @@ -1381,7 +1382,7 @@ fn box_voxel_collision + ReadVol>( z_range: Range, scale: f32, ) -> bool { - let player_aabb = player_aabb(pos, radius, z_range, scale); + let player_aabb = player_aabb(pos, radius, z_range); // Calculate the world space near aabb let near_aabb = move_aabb(near_aabb, pos); @@ -1407,7 +1408,7 @@ fn box_voxel_collision + ReadVol>( #[allow(clippy::trivially_copy_pass_by_ref)] fn always_hits(_: &Block) -> bool { true } - let (radius, z_min, z_max) = cylinder; + let (radius, z_min, z_max) = (Vec3::from(cylinder) * scale).into_tuple(); // Probe distances let hdist = radius.ceil() as i32; @@ -1448,7 +1449,7 @@ fn box_voxel_collision + ReadVol>( let try_colliding_block = |pos: &Pos| { //prof_span!("most colliding check"); // Calculate the player's AABB - let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); // Determine the block that we are colliding with most // (based on minimum collision axis) @@ -1498,7 +1499,7 @@ fn box_voxel_collision + ReadVol>( .flatten() { // Calculate the player's AABB - let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); // Find the intrusion vector of the collision let dir = player_aabb.collision_vector_with_aabb(block_aabb); @@ -1630,7 +1631,7 @@ fn box_voxel_collision + ReadVol>( } // Find liquid immersion and wall collision all in one round of iteration - let player_aabb = player_aabb(pos.0, radius, z_range.clone(), scale); + let player_aabb = player_aabb(pos.0, radius, z_range.clone()); // Calculate the world space near_aabb let near_aabb = move_aabb(near_aabb, pos.0); From 8ff438bb5bbdc7dfc7de939a7cf0016ef4fed660 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 18:57:13 +0100 Subject: [PATCH 013/139] Smol animals --- common/src/cmd.rs | 1 + common/systems/src/mount.rs | 8 +++++--- server/src/cmd.rs | 6 +++--- voxygen/src/scene/figure/mod.rs | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 7431e7e271..46ff446b85 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -654,6 +654,7 @@ impl ServerChatCommand { Enum("entity", ENTITIES.clone(), Required), Integer("amount", 1, Optional), Boolean("ai", "true".to_string(), Optional), + Float("ai", 1.0, Optional), ], "Spawn a test entity", Some(Admin), diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index 1cfb25e68d..997442f12e 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,5 @@ use common::{ - comp::{Body, Controller, InputKind, Ori, Pos, Vel}, + comp::{Body, Controller, InputKind, Ori, Pos, Vel, Scale}, link::Is, mounting::Mount, uid::UidAllocator, @@ -24,6 +24,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, ReadStorage<'a, Body>, + ReadStorage<'a, Scale>, ); const NAME: &'static str = "mount"; @@ -41,6 +42,7 @@ impl<'a> System<'a> for Sys { mut velocities, mut orientations, bodies, + scales, ): Self::SystemData, ) { // For each mount... @@ -67,8 +69,8 @@ impl<'a> System<'a> for Sys { let vel = velocities.get(entity).copied(); if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) { let mounter_body = bodies.get(rider); - let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset) - + mounter_body.map_or(Vec3::zero(), Body::rider_offset); + let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset) * scales.get(entity).map_or(1.0, |s| s.0) + + mounter_body.map_or(Vec3::zero(), Body::rider_offset) * scales.get(rider).map_or(1.0, |s| s.0); let _ = positions.insert(rider, Pos(pos.0 + ori.to_quat() * mounting_offset)); let _ = orientations.insert(rider, ori); let _ = velocities.insert(rider, vel); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 70c678ff74..a1e486b3f0 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1189,8 +1189,8 @@ fn handle_spawn( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - match parse_cmd_args!(args, String, npc::NpcBody, u32, bool) { - (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai) => { + match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) { + (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai, opt_scale) => { let uid = uid(server, target, "target")?; let alignment = parse_alignment(uid, &opt_align)?; let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50); @@ -1227,7 +1227,7 @@ fn handle_spawn( body, ) .with(comp::Vel(vel)) - .with(body.scale()) + .with(opt_scale.map(comp::Scale).unwrap_or(body.scale())) .with(alignment); if ai { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 2340e40b5c..d1d71dafb9 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -809,7 +809,7 @@ impl FigureMgr { const MIN_PERFECT_RATE_DIST: f32 = 100.0; if (i as u64 + tick) - % (((pos.0.distance_squared(focus_pos).powf(0.25) - MIN_PERFECT_RATE_DIST.sqrt()) + % ((((pos.0.distance_squared(focus_pos) / scale.map_or(1.0, |s| s.0)).powf(0.25) - MIN_PERFECT_RATE_DIST.sqrt()) .max(0.0) / 3.0) as u64) .saturating_add(1) From 558dd99fd3929dc193f2584ec90547ddf6770d0d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 21:54:35 +0100 Subject: [PATCH 014/139] Added basic rtsim NPC simulation, rtsim controller support --- common/src/rtsim.rs | 8 +- rtsim/src/data/mod.rs | 2 + rtsim/src/data/npc.rs | 89 +++++---- rtsim/src/event.rs | 5 +- rtsim/src/gen/mod.rs | 14 +- rtsim/src/lib.rs | 6 +- rtsim/src/rule/simulate_npcs.rs | 36 +++- server/agent/src/action_nodes.rs | 2 +- server/agent/src/data.rs | 3 +- server/src/rtsim2/tick.rs | 170 +++--------------- server/src/sys/agent.rs | 1 + server/src/sys/agent/behavior_tree.rs | 31 ++-- .../sys/agent/behavior_tree/interaction.rs | 1 - 13 files changed, 139 insertions(+), 229 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 80b541c1c7..3852d09190 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -57,7 +57,7 @@ pub struct RtSimController { /// When this field is `Some(..)`, the agent should attempt to make progress /// toward the given location, accounting for obstacles and other /// high-priority situations like being attacked. - pub travel_to: Option<(Vec3, String)>, + pub travel_to: Option>, /// Proportion of full speed to move pub speed_factor: f32, /// Events @@ -75,12 +75,10 @@ impl Default for RtSimController { } impl RtSimController { - pub fn reset(&mut self) { *self = Self::default(); } - pub fn with_destination(pos: Vec3) -> Self { Self { - travel_to: Some((pos, format!("{:0.1?}", pos))), - speed_factor: 0.25, + travel_to: Some(pos), + speed_factor: 0.5, events: Vec::new(), } } diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index e1a7d66adb..ed6bf14210 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -28,6 +28,8 @@ pub struct Data { pub nature: Nature, pub npcs: Npcs, pub sites: Sites, + + pub time: f64, } pub type ReadError = rmp_serde::decode::Error; diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index c01b3b0081..386a0eb1ff 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -2,50 +2,17 @@ use hashbrown::HashMap; use serde::{Serialize, Deserialize}; use slotmap::HopSlotMap; use vek::*; +use rand::prelude::*; use std::ops::{Deref, DerefMut}; use common::{ uid::Uid, store::Id, - rtsim::SiteId, + rtsim::{SiteId, RtSimController}, + comp, }; +use world::util::RandomPerm; pub use common::rtsim::NpcId; -#[derive(Clone, Serialize, Deserialize)] -pub struct Npc { - // Persisted state - - /// Represents the location of the NPC. - pub loc: NpcLoc, - - // Unpersisted state - - /// The position of the NPC in the world. Note that this is derived from [`Npc::loc`] and cannot be updated manually - #[serde(skip_serializing, skip_deserializing)] - wpos: Vec3, - /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being - /// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should - /// instead be derived from the game. - #[serde(skip_serializing, skip_deserializing)] - pub mode: NpcMode, -} - -impl Npc { - pub fn new(loc: NpcLoc) -> Self { - Self { - loc, - wpos: Vec3::zero(), - mode: NpcMode::Simulated, - } - } - - pub fn wpos(&self) -> Vec3 { self.wpos } - - /// You almost certainly *DO NOT* want to use this method. - /// - /// Update the NPC's wpos as a result of routine NPC simulation derived from its location. - pub(crate) fn tick_wpos(&mut self, wpos: Vec3) { self.wpos = wpos; } -} - #[derive(Copy, Clone, Default)] pub enum NpcMode { /// The NPC is unloaded and is being simulated via rtsim. @@ -56,14 +23,46 @@ pub enum NpcMode { } #[derive(Clone, Serialize, Deserialize)] -pub enum NpcLoc { - Wild { wpos: Vec3 }, - Site { site: SiteId, wpos: Vec3 }, - Travelling { - a: SiteId, - b: SiteId, - frac: f32, - }, +pub struct Npc { + // Persisted state + + /// Represents the location of the NPC. + pub seed: u32, + pub wpos: Vec3, + + // Unpersisted state + + /// (wpos, speed_factor) + #[serde(skip_serializing, skip_deserializing)] + pub target: Option<(Vec3, f32)>, + /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being + /// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should + /// instead be derived from the game. + #[serde(skip_serializing, skip_deserializing)] + pub mode: NpcMode, +} + +impl Npc { + const PERM_SPECIES: u32 = 0; + const PERM_BODY: u32 = 1; + + pub fn new(seed: u32, wpos: Vec3) -> Self { + Self { + seed, + wpos, + target: None, + mode: NpcMode::Simulated, + } + } + + pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } + + pub fn get_body(&self) -> comp::Body { + let species = *(&comp::humanoid::ALL_SPECIES) + .choose(&mut self.rng(Self::PERM_SPECIES)) + .unwrap(); + comp::humanoid::Body::random_with(&mut self.rng(Self::PERM_BODY), &species).into() + } } #[derive(Clone, Serialize, Deserialize)] diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index 298cebf8c8..b60b9825f4 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -17,5 +17,8 @@ pub struct OnSetup; impl Event for OnSetup {} #[derive(Clone)] -pub struct OnTick { pub dt: f32 } +pub struct OnTick { + pub dt: f32, + pub time: f64, +} impl Event for OnTick {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 4e0877c240..c0a6ba4f94 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,7 +1,7 @@ pub mod site; use crate::data::{ - npc::{Npcs, Npc, NpcLoc}, + npc::{Npcs, Npc}, site::{Sites, Site}, Data, Nature, @@ -25,6 +25,8 @@ impl Data { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default() }, sites: Sites { sites: Default::default() }, + + time: 0.0, }; // Register sites with rtsim @@ -39,10 +41,12 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() { - let wpos = site.wpos.map(|e| e as f32) - .with_z(world.sim().get_alt_approx(site.wpos).unwrap_or(0.0)); - this.npcs.create(Npc::new(NpcLoc::Site { site: site_id, wpos })); - println!("Spawned rtsim NPC at {:?}", wpos); + for _ in 0..10 { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + let wpos = wpos2d.map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)); + this.npcs.create(Npc::new(rng.gen(), wpos)); + } } this diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 84de682836..5903d7dbaa 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -49,8 +49,8 @@ impl RtState { fn start_default_rules(&mut self) { info!("Starting default rtsim rules..."); - self.start_rule::(); self.start_rule::(); + self.start_rule::(); } pub fn start_rule(&mut self) { @@ -110,6 +110,8 @@ impl RtState { } pub fn tick(&mut self, world: &World, index: IndexRef, dt: f32) { - self.emit(OnTick { dt }, world, index); + self.data_mut().time += dt as f64; + let event = OnTick { dt, time: self.data().time }; + self.emit(event, world, index); } } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index dae9ed6502..206e3ad5a7 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,6 +1,7 @@ use tracing::info; +use vek::*; use crate::{ - data::npc::NpcLoc, + data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError, }; @@ -11,12 +12,33 @@ impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { - for (_, npc) in ctx.state.data_mut().npcs.iter_mut() { - npc.tick_wpos(match npc.loc { - NpcLoc::Wild { wpos } => wpos, - NpcLoc::Site { site, wpos } => wpos, - NpcLoc::Travelling { a, b, frac } => todo!(), - }); + for npc in ctx.state + .data_mut() + .npcs + .values_mut() + .filter(|npc| matches!(npc.mode, NpcMode::Simulated)) + { + let body = npc.get_body(); + + if let Some((target, speed_factor)) = npc.target { + npc.wpos += Vec3::from( + (target.xy() - npc.wpos.xy()) + .try_normalized() + .unwrap_or_else(Vec2::zero) + * body.max_speed_approx() + * speed_factor, + ) * ctx.event.dt; + } + } + + // Do some thinking. TODO: Not here! + for npc in ctx.state + .data_mut() + .npcs + .values_mut() + { + // TODO: Not this + npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0)); } }); diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index cc590e5879..2509c2ac3e 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -212,7 +212,7 @@ impl<'a> AgentData<'a> { } agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; - if let Some((travel_to, _destination)) = &agent.rtsim_controller.travel_to { + if let Some(travel_to) = &agent.rtsim_controller.travel_to { // If it has an rtsim destination and can fly, then it should. // If it is flying and bumps something above it, then it should move down. if self.traversal_config.can_fly diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index c6a1f18eec..8fc80890b0 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -54,6 +54,7 @@ pub struct AgentData<'a> { pub stance: Option<&'a Stance>, pub cached_spatial_grid: &'a common::CachedSpatialGrid, pub msm: &'a MaterialStatManifest, + pub rtsim_entity: Option<&'a RtSimEntity>, } pub struct TargetData<'a> { @@ -236,7 +237,7 @@ pub struct ReadData<'a> { pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, - // pub rtsim_entities: ReadStorage<'a, RtSimEntity>, + pub rtsim_entities: ReadStorage<'a, RtSimEntity>, pub buffs: ReadStorage<'a, Buffs>, pub combos: ReadStorage<'a, Combo>, pub active_abilities: ReadStorage<'a, ActiveAbilities>, diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bcd577594b..55eab5f289 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -8,7 +8,7 @@ use common::{ generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, slowjob::SlowJobPool, - rtsim::RtSimEntity, + rtsim::{RtSimEntity, RtSimController}, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::npc::NpcMode; @@ -26,6 +26,9 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, ReadExpect<'a, SlowJobPool>, + ReadStorage<'a, comp::Pos>, + ReadStorage<'a, RtSimEntity>, + WriteStorage<'a, comp::Agent>, ); const NAME: &'static str = "rtsim::tick"; @@ -34,7 +37,7 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, + (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs, positions, rtsim_entities, mut agents): Self::SystemData, ) { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; @@ -47,7 +50,7 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() { - let chunk = npc.wpos() + let chunk = npc.wpos .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32)); @@ -55,18 +58,18 @@ impl<'a> System<'a> for Sys { if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { npc.mode = NpcMode::Loaded; - let body = comp::Body::Object(comp::object::Body::Scarecrow); + let body = npc.get_body(); emitter.emit(ServerEvent::CreateNpc { - pos: comp::Pos(npc.wpos()), + pos: comp::Pos(npc.wpos), stats: comp::Stats::new("Rtsim NPC".to_string()), skill_set: comp::SkillSet::default(), health: None, poise: comp::Poise::new(body), inventory: comp::Inventory::with_empty(), body, - agent: None, + agent: Some(comp::Agent::from_body(&body)), alignment: comp::Alignment::Wild, - scale: comp::Scale(10.0), + scale: comp::Scale(1.0), anchor: None, loot: Default::default(), rtsim_entity: Some(RtSimEntity(npc_id)), @@ -75,147 +78,24 @@ impl<'a> System<'a> for Sys { } } - // rtsim.tick += 1; - - // Update unloaded rtsim entities, in groups at a time - /* - const TICK_STAGGER: usize = 30; - let entities_per_iteration = rtsim.entities.len() / TICK_STAGGER; - let mut to_reify = Vec::new(); - for (id, entity) in rtsim - .entities - .iter_mut() - .skip((rtsim.tick as usize % TICK_STAGGER) * entities_per_iteration) - .take(entities_per_iteration) - .filter(|(_, e)| !e.is_loaded) - { - if rtsim - .chunk_states - .get(entity.pos.xy()) - .copied() - .unwrap_or(false) - { - to_reify.push(id); - } else { - // Simulate behaviour - if let Some(travel_to) = &entity.controller.travel_to { - // Move towards target at approximate character speed - entity.pos += Vec3::from( - (travel_to.0.xy() - entity.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero) - * entity.get_body().max_speed_approx() - * entity.controller.speed_factor, - ) * dt; - } - - if let Some(alt) = world - .sim() - .get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) - { - entity.pos.z = alt; - } - } - // entity.tick(&time, &terrain, &world, &index.as_index_ref()); - } - */ - - // Tick entity AI each time if it's loaded - // for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)| - // e.is_loaded) { entity.last_time_ticked = time.0; - // entity.tick(&time, &terrain, &world, &index.as_index_ref()); - // } - - /* - let mut server_emitter = server_event_bus.emitter(); - for id in to_reify { - rtsim.reify_entity(id); - let entity = &rtsim.entities[id]; - let rtsim_entity = Some(RtSimEntity(id)); - - let body = entity.get_body(); - let spawn_pos = terrain - .find_space(entity.pos.map(|e| e.floor() as i32)) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, body.flying_height()); - - let pos = comp::Pos(spawn_pos); - - let event = if let comp::Body::Ship(ship) = body { - ServerEvent::CreateShip { - pos, - ship, - mountable: false, - agent: Some(comp::Agent::from_body(&body)), - rtsim_entity, - } - } else { - let entity_config_path = entity.get_entity_config(); - let mut loadout_rng = entity.loadout_rng(); - let ad_hoc_loadout = entity.get_adhoc_loadout(); - // Body is rewritten so that body parameters - // are consistent between reifications - let entity_config = EntityConfig::from_asset_expect_owned(entity_config_path) - .with_body(BodyBuilder::Exact(body)); - - let mut entity_info = EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(entity_config_path), &mut loadout_rng) - .with_lazy_loadout(ad_hoc_loadout); - // Merchants can be traded with - if let Some(economy) = entity.get_trade_info(&world, &index) { - entity_info = entity_info - .with_agent_mark(comp::agent::Mark::Merchant) - .with_economy(&economy); - } - match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => ServerEvent::CreateNpc { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: None, - loot, - rtsim_entity, - projectile: None, - }, - // EntityConfig can't represent Waypoints at all - // as of now, and if someone will try to spawn - // rtsim waypoint it is definitely error. - NpcData::Waypoint(_) => unimplemented!(), - } - }; - server_emitter.emit(event); - } - - // Update rtsim with real entity data - for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() { + // Synchronise rtsim NPC with entity data + for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() { rtsim - .entities + .state + .data_mut() + .npcs .get_mut(rtsim_entity.0) - .filter(|e| e.is_loaded) - .map(|entity| { - entity.pos = pos.0; - agent.rtsim_controller = entity.controller.clone(); + .filter(|npc| matches!(npc.mode, NpcMode::Loaded)) + .map(|npc| { + // Update rtsim NPC state + npc.wpos = pos.0; + + // Update entity state + if let Some(agent) = agent { + agent.rtsim_controller.travel_to = npc.target.map(|(wpos, _)| wpos); + agent.rtsim_controller.speed_factor = npc.target.map_or(1.0, |(_, sf)| sf); + } }); } - */ } } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index d32581f147..adbe5b4bc6 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -206,6 +206,7 @@ impl<'a> System<'a> for Sys { msm: &read_data.msm, poise: read_data.poises.get(entity), stance: read_data.stances.get(entity), + rtsim_entity: read_data.rtsim_entities.get(entity), }; /////////////////////////////////////////////////////////// diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index eda52faedf..261f94034e 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -1,4 +1,4 @@ -use crate::rtsim::Entity as RtSimEntity; +use common::rtsim::RtSimEntity; use common::{ comp::{ agent::{ @@ -40,8 +40,6 @@ mod interaction; pub struct BehaviorData<'a, 'b, 'c> { pub agent: &'a mut Agent, pub agent_data: AgentData<'a>, - // TODO: Move rtsim back into AgentData after rtsim2 when it has a separate crate - // pub rtsim_entity: Option<&'a RtSimEntity>, pub read_data: &'a ReadData<'a>, pub event_emitter: &'a mut Emitter<'c, ServerEvent>, pub controller: &'a mut Controller, @@ -643,7 +641,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { let BehaviorData { agent, agent_data, - // rtsim_entity, read_data, event_emitter, controller, @@ -750,7 +747,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { controller, read_data, event_emitter, - will_ambush(/* *rtsim_entity */None, agent_data), + will_ambush(agent_data.rtsim_entity, agent_data), ); } @@ -768,9 +765,9 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { read_data, event_emitter, rng, - remembers_fight_with(/* *rtsim_entity */None, read_data, target), + remembers_fight_with(agent_data.rtsim_entity, read_data, target), ); - remember_fight(/* *rtsim_entity */ None, read_data, agent, target); + remember_fight(agent_data.rtsim_entity, read_data, agent, target); } } } @@ -780,10 +777,11 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool { // TODO: implement for rtsim2 - agent_data - .health - .map_or(false, |h| h.current() / h.maximum() > 0.7) - && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush) + // agent_data + // .health + // .map_or(false, |h| h.current() / h.maximum() > 0.7) + // && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush) + false } fn remembers_fight_with( @@ -794,11 +792,12 @@ fn remembers_fight_with( // TODO: implement for rtsim2 let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); - rtsim_entity.map_or(false, |rtsim_entity| { - name().map_or(false, |name| { - rtsim_entity.brain.remembers_fight_with_character(&name) - }) - }) + // rtsim_entity.map_or(false, |rtsim_entity| { + // name().map_or(false, |name| { + // rtsim_entity.brain.remembers_fight_with_character(&name) + // }) + // }) + false } /// Remember target. diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 3d37cc6d83..29d2175f7e 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -16,7 +16,6 @@ use rand::{thread_rng, Rng}; use specs::saveload::Marker; use crate::{ - rtsim::entity::{PersonalityTrait, RtSimEntityKind}, sys::agent::util::get_entity_by_id, }; From df63e41a23d46bfdb95b832d64478ab4d8193355 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 11 Aug 2022 22:06:25 +0100 Subject: [PATCH 015/139] Clamp NPCs to surface --- rtsim/src/rule/simulate_npcs.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 206e3ad5a7..b597ee88b7 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -20,6 +20,7 @@ impl Rule for SimulateNpcs { { let body = npc.get_body(); + // Move NPCs if they have a target if let Some((target, speed_factor)) = npc.target { npc.wpos += Vec3::from( (target.xy() - npc.wpos.xy()) @@ -29,6 +30,11 @@ impl Rule for SimulateNpcs { * speed_factor, ) * ctx.event.dt; } + + // Make sure NPCs remain on the surface + npc.wpos.z = ctx.world.sim() + .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) + .unwrap_or(0.0); } // Do some thinking. TODO: Not here! From 587996abb7b4bb1d333dfcfb891530eb56aabe1a Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 12 Aug 2022 13:15:55 +0100 Subject: [PATCH 016/139] Correctly scale glider physics --- common/src/cmd.rs | 2 +- common/src/comp/fluid_dynamics.rs | 27 ++++++++++++++------------- common/src/states/climb.rs | 2 +- common/src/states/utils.rs | 2 +- common/systems/src/phys.rs | 9 +++++++-- server/src/cmd.rs | 17 ++++++++++++++++- server/src/lib.rs | 2 +- 7 files changed, 41 insertions(+), 20 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 46ff446b85..eb304014a0 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -730,7 +730,7 @@ impl ServerChatCommand { cmd(vec![], "Lightning strike at current position", Some(Admin)) }, ServerChatCommand::Scale => { - cmd(vec![Float("factor", 1.0, Required)], "Scale your character", Some(Admin)) + cmd(vec![Float("factor", 1.0, Required), Boolean("reset_mass", true.to_string(), Optional)], "Scale your character", Some(Admin)) }, } } diff --git a/common/src/comp/fluid_dynamics.rs b/common/src/comp/fluid_dynamics.rs index 9124ae501a..c7b3558497 100644 --- a/common/src/comp/fluid_dynamics.rs +++ b/common/src/comp/fluid_dynamics.rs @@ -135,6 +135,7 @@ impl Body { rel_flow: &Vel, fluid_density: f32, wings: Option<&Wings>, + scale: f32, ) -> Vec3 { let v_sq = rel_flow.0.magnitude_squared(); if v_sq < 0.25 { @@ -201,11 +202,11 @@ impl Body { debug_assert!(c_d.is_sign_positive()); debug_assert!(c_l.is_sign_positive() || aoa.is_sign_negative()); - planform_area * (c_l * *lift_dir + c_d * *rel_flow_dir) - + self.parasite_drag() * *rel_flow_dir + planform_area * scale.powf(2.0) * (c_l * *lift_dir + c_d * *rel_flow_dir) + + self.parasite_drag(scale) * *rel_flow_dir }, - _ => self.parasite_drag() * *rel_flow_dir, + _ => self.parasite_drag(scale) * *rel_flow_dir, } } } @@ -214,13 +215,13 @@ impl Body { /// Skin friction is the drag arising from the shear forces between a fluid /// and a surface, while pressure drag is due to flow separation. Both are /// viscous effects. - fn parasite_drag(&self) -> f32 { + fn parasite_drag(&self, scale: f32) -> f32 { // Reference area and drag coefficient assumes best-case scenario of the // orientation producing least amount of drag match self { // Cross-section, head/feet first Body::BipedLarge(_) | Body::BipedSmall(_) | Body::Golem(_) | Body::Humanoid(_) => { - let dim = self.dimensions().xy().map(|a| a * 0.5); + let dim = self.dimensions().xy().map(|a| a * 0.5 * scale); const CD: f32 = 0.7; CD * PI * dim.x * dim.y }, @@ -231,7 +232,7 @@ impl Body { | Body::QuadrupedSmall(_) | Body::QuadrupedLow(_) | Body::Arthropod(_) => { - let dim = self.dimensions().map(|a| a * 0.5); + let dim = self.dimensions().map(|a| a * 0.5 * scale); let cd: f32 = if matches!(self, Body::QuadrupedLow(_)) { 0.7 } else { @@ -242,7 +243,7 @@ impl Body { // Cross-section, zero-lift angle; exclude the wings (width * 0.2) Body::BirdMedium(_) | Body::BirdLarge(_) | Body::Dragon(_) => { - let dim = self.dimensions().map(|a| a * 0.5); + let dim = self.dimensions().map(|a| a * 0.5 * scale); let cd: f32 = match self { // "Field Estimates of Body Drag Coefficient // on the Basis of Dives in Passerine Birds", @@ -256,7 +257,7 @@ impl Body { // Cross-section, zero-lift angle; exclude the fins (width * 0.2) Body::FishMedium(_) | Body::FishSmall(_) => { - let dim = self.dimensions().map(|a| a * 0.5); + let dim = self.dimensions().map(|a| a * 0.5 * scale); // "A Simple Method to Determine Drag Coefficients in Aquatic Animals", // D. Bilo and W. Nachtigall, 1980 const CD: f32 = 0.031; @@ -276,7 +277,7 @@ impl Body { | object::Body::FireworkYellow | object::Body::MultiArrow | object::Body::Dart => { - let dim = self.dimensions().map(|a| a * 0.5); + let dim = self.dimensions().map(|a| a * 0.5 * scale); const CD: f32 = 0.02; CD * PI * dim.x * dim.z }, @@ -295,20 +296,20 @@ impl Body { | object::Body::Pumpkin3 | object::Body::Pumpkin4 | object::Body::Pumpkin5 => { - let dim = self.dimensions().map(|a| a * 0.5); + let dim = self.dimensions().map(|a| a * 0.5 * scale); const CD: f32 = 0.5; CD * PI * dim.x * dim.z }, _ => { - let dim = self.dimensions(); + let dim = self.dimensions().map(|a| a * scale); const CD: f32 = 2.0; CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) }, }, Body::ItemDrop(_) => { - let dim = self.dimensions(); + let dim = self.dimensions().map(|a| a * scale); const CD: f32 = 2.0; CD * (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) }, @@ -316,7 +317,7 @@ impl Body { Body::Ship(_) => { // Airships tend to use the square of the cube root of its volume for // reference area - let dim = self.dimensions(); + let dim = self.dimensions().map(|a| a * scale); (PI / 6.0 * dim.x * dim.y * dim.z).powf(2.0 / 3.0) }, } diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 77ffc8d69d..fa415ce616 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -76,7 +76,7 @@ impl CharacterBehavior for Data { // They've climbed atop something, give them a boost output_events.emit_local(LocalEvent::Jump( data.entity, - CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0, + CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)), )); }; update.character = CharacterState::Idle(idle::Data::default()); diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 2d67ef61be..68c8e06641 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -1075,7 +1075,7 @@ pub fn handle_jump( output_events.emit_local(LocalEvent::Jump( data.entity, strength * impulse / data.mass.0 - * data.scale.map_or(1.0, |s| s.0.powf(0.25)) + * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)) * data.stats.move_speed_modifier, )); }) diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index e1291a5712..755e555872 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -50,6 +50,7 @@ fn integrate_forces( mass: &Mass, fluid: &Fluid, gravity: f32, + scale: Option, ) -> Vel { let dim = body.dimensions(); let height = dim.z; @@ -61,7 +62,7 @@ fn integrate_forces( // Aerodynamic/hydrodynamic forces if !rel_flow.0.is_approx_zero() { debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or()); - let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, wings); + let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, wings, scale.map_or(1.0, |s| s.0)); debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or()); if !impulse.is_approx_zero() { let new_v = vel.0 + impulse / mass.0; @@ -610,6 +611,7 @@ impl<'a> PhysicsData<'a> { &write.physics_states, &read.masses, &read.densities, + read.scales.maybe(), !&read.is_ridings, ) .par_join() @@ -628,6 +630,7 @@ impl<'a> PhysicsData<'a> { physics_state, mass, density, + scale, _, )| { let in_loaded_chunk = read @@ -672,6 +675,7 @@ impl<'a> PhysicsData<'a> { mass, &fluid, GRAVITY, + scale.copied(), ) .0 }, @@ -1438,7 +1442,8 @@ fn box_voxel_collision + ReadVol>( // Don't jump too far at once const MAX_INCREMENTS: usize = 100; // The maximum number of collision tests per tick - let increments = ((pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3).ceil() as usize) + let min_step = (radius / 2.0).min(z_max - z_min).clamped(0.01, 0.3); + let increments = ((pos_delta.map(|e| e.abs()).reduce_partial_max() / min_step).ceil() as usize) .clamped(1, MAX_INCREMENTS); let old_pos = pos.0; for _ in 0..increments { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index a1e486b3f0..ff72e6c8c8 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1228,6 +1228,7 @@ fn handle_spawn( ) .with(comp::Vel(vel)) .with(opt_scale.map(comp::Scale).unwrap_or(body.scale())) + .maybe_with(opt_scale.map(|s| comp::Mass(body.mass().0 * s.powi(3)))) .with(alignment); if ai { @@ -3844,13 +3845,27 @@ fn handle_scale( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - if let Some(scale) = parse_cmd_args!(args, f32) { + if let (Some(scale), reset_mass) = parse_cmd_args!(args, f32, bool) { let scale = scale.clamped(0.025, 1000.0); let _ = server .state .ecs_mut() .write_storage::() .insert(target, comp::Scale(scale)); + if reset_mass.unwrap_or(true) { + if let Some(body) = server + .state + .ecs() + .read_storage::() + .get(target) + { + let _ = server + .state + .ecs() + .write_storage() + .insert(target, comp::Mass(body.mass().0 * scale.powi(3))); + } + } server.notify_client( client, ServerGeneral::server_msg(ChatType::CommandInfo, format!("Set scale to {}", scale)), diff --git a/server/src/lib.rs b/server/src/lib.rs index e7baa3c55e..03c5ef4fa3 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -709,7 +709,7 @@ impl Server { if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { ecs.write_resource::().hook_block_update( &ecs.read_resource::>(), - ecs.read_resource::>().as_index_ref(), + ecs.read_resource::().as_index_ref(), wpos, old_block, new_block, From ac0e62df8e3cc44ea59e9cfd18d95f1f2de7c07d Mon Sep 17 00:00:00 2001 From: IsseW Date: Fri, 12 Aug 2022 17:57:55 +0200 Subject: [PATCH 017/139] tp_npc command --- common/src/cmd.rs | 7 +++++++ server/src/cmd.rs | 20 ++++++++++++++++++++ server/src/rtsim2/mod.rs | 4 ++++ 3 files changed, 31 insertions(+) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index eb304014a0..f074cc6a3e 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -310,6 +310,7 @@ pub enum ServerChatCommand { Tell, Time, Tp, + TpNpc, Unban, Version, Waypoint, @@ -679,6 +680,11 @@ impl ServerChatCommand { "Teleport to another player", Some(Moderator), ), + ServerChatCommand::TpNpc => cmd( + vec![Integer("npc index", 0, Required)], + "Teleport to a npc", + Some(Moderator), + ), ServerChatCommand::Unban => cmd( vec![PlayerName(Required)], "Remove the ban for the given username", @@ -801,6 +807,7 @@ impl ServerChatCommand { ServerChatCommand::Tell => "tell", ServerChatCommand::Time => "time", ServerChatCommand::Tp => "tp", + ServerChatCommand::TpNpc => "tp_npc", ServerChatCommand::Unban => "unban", ServerChatCommand::Version => "version", ServerChatCommand::Waypoint => "waypoint", diff --git a/server/src/cmd.rs b/server/src/cmd.rs index ff72e6c8c8..8132fae612 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -184,6 +184,7 @@ fn do_command( ServerChatCommand::Tell => handle_tell, ServerChatCommand::Time => handle_time, ServerChatCommand::Tp => handle_tp, + ServerChatCommand::TpNpc => handle_tp_npc, ServerChatCommand::Unban => handle_unban, ServerChatCommand::Version => handle_version, ServerChatCommand::Waypoint => handle_waypoint, @@ -1182,6 +1183,25 @@ fn handle_tp( }) } +fn handle_tp_npc( + server: &mut Server, + _client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + use crate::rtsim2::RtSim; + let pos = if let Some(id) = parse_cmd_args!(args, u32) { + // TODO: Take some other identifier than an integer to this command. + server.state.ecs().read_resource::().state().data().npcs.values().nth(id as usize).ok_or(action.help_string())?.wpos + } else { + return Err(action.help_string()); + }; + position_mut(server, target, "target", |target_pos| { + target_pos.0 = pos; + }) +} + fn handle_spawn( server: &mut Server, client: EcsEntity, diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index a0f6cf6d33..748b99b054 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -179,6 +179,10 @@ impl RtSim { pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { self.state.data().nature.get_chunk_resources(key) } + + pub fn state(&self) -> &RtState { + &self.state + } } struct ChunkStates(pub Grid>); From 21f9bcb8e2a9e3544647f9d6d52a6a23a598efa4 Mon Sep 17 00:00:00 2001 From: IsseW Date: Sat, 13 Aug 2022 18:58:10 +0200 Subject: [PATCH 018/139] added professions, and loadouts --- rtsim/src/data/npc.rs | 26 ++++++++++++- rtsim/src/gen/mod.rs | 18 ++++++--- rtsim/src/rule/setup.rs | 13 +++++-- server/src/rtsim2/tick.rs | 79 ++++++++++++++++++++++++++++++--------- 4 files changed, 109 insertions(+), 27 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 386a0eb1ff..bc0594d608 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -13,6 +13,15 @@ use common::{ use world::util::RandomPerm; pub use common::rtsim::NpcId; +#[derive(Clone, Serialize, Deserialize)] +pub enum Profession { + Farmer, + Hunter, + Merchant, + Guard, + Adventurer(u32), +} + #[derive(Copy, Clone, Default)] pub enum NpcMode { /// The NPC is unloaded and is being simulated via rtsim. @@ -30,6 +39,9 @@ pub struct Npc { pub seed: u32, pub wpos: Vec3, + pub profession: Option, + pub home: Option, + // Unpersisted state /// (wpos, speed_factor) @@ -50,12 +62,24 @@ impl Npc { Self { seed, wpos, + profession: None, + home: None, target: None, mode: NpcMode::Simulated, } } - pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } + pub fn with_profession(mut self, profession: Profession) -> Self { + self.profession = Some(profession); + self + } + + pub fn with_home(mut self, home: SiteId) -> Self { + self.home = Some(home); + self + } + + pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) } pub fn get_body(&self) -> comp::Body { let species = *(&comp::humanoid::ALL_SPECIES) diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index c0a6ba4f94..4ecc42ee14 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,7 +1,7 @@ pub mod site; use crate::data::{ - npc::{Npcs, Npc}, + npc::{Npcs, Npc, Profession}, site::{Sites, Site}, Data, Nature, @@ -41,12 +41,20 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() { - for _ in 0..10 { + let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); - let wpos = wpos2d.map(|e| e as f32 + 0.5) - .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)); - this.npcs.create(Npc::new(rng.gen(), wpos)); + wpos2d.map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + for _ in 0..10 { + + this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..10) { + 0 => Profession::Hunter, + 1..=4 => Profession::Farmer, + _ => Profession::Guard, + })); } + this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(Profession::Merchant)); } this diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index 5bdb00a373..ae35d1f27c 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -12,8 +12,9 @@ pub struct Setup; impl Rule for Setup { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { + let data = &mut *ctx.state.data_mut(); // Delete rtsim sites that don't correspond to a world site - ctx.state.data_mut().sites.retain(|site_id, site| { + data.sites.retain(|site_id, site| { if let Some((world_site_id, _)) = ctx.index.sites .iter() .find(|(_, world_site)| world_site.get_origin() == site.wpos) @@ -26,15 +27,19 @@ impl Rule for Setup { } }); + for npc in data.npcs.values_mut() { + // TODO: Consider what to do with homeless npcs. + npc.home = npc.home.filter(|home| data.sites.contains_key(*home)); + } + // Generate rtsim sites for world sites that don't have a corresponding rtsim site yet for (world_site_id, _) in ctx.index.sites.iter() { - if !ctx.state.data().sites + if !data.sites .values() .any(|site| site.world_site.expect("Rtsim site not assigned to world site") == world_site_id) { warn!("{:?} is new and does not have a corresponding rtsim site. One will now be generated afresh.", world_site_id); - ctx.state - .data_mut() + data .sites .create(Site::generate(world_site_id, ctx.world, ctx.index)); } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 55eab5f289..34be4709d8 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -3,17 +3,19 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp, + comp::{self, inventory::loadout::Loadout}, event::{EventBus, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, + rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, - rtsim::{RtSimEntity, RtSimController}, + LoadoutBuilder, }; use common_ecs::{Job, Origin, Phase, System}; -use rtsim2::data::npc::NpcMode; +use rtsim2::data::npc::{NpcMode, Profession}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; +use world::site::settlement::merchant_loadout; #[derive(Default)] pub struct Sys; @@ -37,35 +39,78 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, mut server_event_bus, mut rtsim, world, index, slow_jobs, positions, rtsim_entities, mut agents): Self::SystemData, + ( + dt, + time, + mut server_event_bus, + mut rtsim, + world, + index, + slow_jobs, + positions, + rtsim_entities, + mut agents, + ): Self::SystemData, ) { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; rtsim.state.tick(&world, index.as_index_ref(), dt.0); - if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { + if rtsim + .last_saved + .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) + { rtsim.save(&slow_jobs); } let chunk_states = rtsim.state.resource::(); - for (npc_id, npc) in rtsim.state.data_mut().npcs.iter_mut() { - let chunk = npc.wpos - .xy() - .map2(TerrainChunk::RECT_SIZE, |e, sz| (e as i32).div_euclid(sz as i32)); + let data = &mut *rtsim.state.data_mut(); + for (npc_id, npc) in data.npcs.iter_mut() { + let chunk = npc.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e as i32).div_euclid(sz as i32) + }); - // Load the NPC into the world if it's in a loaded chunk and is not already loaded - if matches!(npc.mode, NpcMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { + // Load the NPC into the world if it's in a loaded chunk and is not already + // loaded + if matches!(npc.mode, NpcMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) + { npc.mode = NpcMode::Loaded; - let body = npc.get_body(); + let mut loadout_builder = LoadoutBuilder::from_default(&body); + let mut rng = npc.rng(3); + + if let Some(ref profession) = npc.profession { + loadout_builder = match profession { + Profession::Guard => loadout_builder + .with_asset_expect("common.loadout.village.guard", &mut rng), + + Profession::Merchant => { + merchant_loadout( + loadout_builder, + npc.home + .and_then(|home| { + let site = data.sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }).as_ref(), + ) + } + + Profession::Farmer | Profession::Hunter => loadout_builder + .with_asset_expect("common.loadout.village.villager", &mut rng), + + Profession::Adventurer(level) => todo!(), + }; + } + emitter.emit(ServerEvent::CreateNpc { pos: comp::Pos(npc.wpos), stats: comp::Stats::new("Rtsim NPC".to_string()), skill_set: comp::SkillSet::default(), health: None, poise: comp::Poise::new(body), - inventory: comp::Inventory::with_empty(), + inventory: comp::Inventory::with_loadout(loadout_builder.build(), body), body, agent: Some(comp::Agent::from_body(&body)), alignment: comp::Alignment::Wild, @@ -79,10 +124,10 @@ impl<'a> System<'a> for Sys { } // Synchronise rtsim NPC with entity data - for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() { - rtsim - .state - .data_mut() + for (pos, rtsim_entity, agent) in + (&positions, &rtsim_entities, (&mut agents).maybe()).join() + { + data .npcs .get_mut(rtsim_entity.0) .filter(|npc| matches!(npc.mode, NpcMode::Loaded)) From ff7478eb01a9893b73657bb417f4cefa3aeae53d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 00:09:21 +0100 Subject: [PATCH 019/139] Factored out NPC AI --- rtsim/src/lib.rs | 1 + rtsim/src/rule.rs | 1 + rtsim/src/rule/npc_ai.rs | 27 +++++++++++++++++++++++++++ rtsim/src/rule/simulate_npcs.rs | 10 ---------- 4 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 rtsim/src/rule/npc_ai.rs diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 5903d7dbaa..9f1ed63bc6 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -51,6 +51,7 @@ impl RtState { info!("Starting default rtsim rules..."); self.start_rule::(); self.start_rule::(); + self.start_rule::(); } pub fn start_rule(&mut self) { diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs index 0547d4d277..611872ab75 100644 --- a/rtsim/src/rule.rs +++ b/rtsim/src/rule.rs @@ -1,5 +1,6 @@ pub mod setup; pub mod simulate_npcs; +pub mod npc_ai; use std::fmt; use super::RtState; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs new file mode 100644 index 0000000000..7c2c994f55 --- /dev/null +++ b/rtsim/src/rule/npc_ai.rs @@ -0,0 +1,27 @@ +use tracing::info; +use vek::*; +use crate::{ + data::npc::NpcMode, + event::OnTick, + RtState, Rule, RuleError, +}; + +pub struct NpcAi; + +impl Rule for NpcAi { + fn start(rtstate: &mut RtState) -> Result { + + rtstate.bind::(|ctx| { + for npc in ctx.state + .data_mut() + .npcs + .values_mut() + { + // TODO: Not this + npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0)); + } + }); + + Ok(Self) + } +} diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index b597ee88b7..44a7313d5a 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -36,16 +36,6 @@ impl Rule for SimulateNpcs { .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); } - - // Do some thinking. TODO: Not here! - for npc in ctx.state - .data_mut() - .npcs - .values_mut() - { - // TODO: Not this - npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0)); - } }); Ok(Self) From ee048ad5a28c6bbb2c418f3472cb31a1e6f7d511 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 00:32:39 +0100 Subject: [PATCH 020/139] Made NPCs talk and trade --- server/src/rtsim2/tick.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 34be4709d8..457a136fda 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -3,13 +3,14 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, inventory::loadout::Loadout}, + comp::{self, inventory::loadout::Loadout, skillset::skills}, event::{EventBus, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, LoadoutBuilder, + SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::npc::{NpcMode, Profession}; @@ -104,16 +105,37 @@ impl<'a> System<'a> for Sys { }; } + let can_speak = npc.profession.is_some(); // TODO: not this + + let trade_for_site = if let Some(Profession::Merchant) = npc.profession { + npc.home.and_then(|home| Some(data.sites.get(home)?.world_site?.id())) + } else { + None + }; + + let skill_set = SkillSetBuilder::default().build(); + let health_level = skill_set + .skill_level(skills::Skill::General(skills::GeneralSkill::HealthIncrease)) + .unwrap_or(0); emitter.emit(ServerEvent::CreateNpc { pos: comp::Pos(npc.wpos), stats: comp::Stats::new("Rtsim NPC".to_string()), - skill_set: comp::SkillSet::default(), - health: None, + skill_set: skill_set, + health: Some(comp::Health::new(body, health_level)), poise: comp::Poise::new(body), inventory: comp::Inventory::with_loadout(loadout_builder.build(), body), body, - agent: Some(comp::Agent::from_body(&body)), - alignment: comp::Alignment::Wild, + agent: Some(comp::Agent::from_body(&body) + .with_behavior( + comp::Behavior::default() + .maybe_with_capabilities(can_speak.then_some(comp::BehaviorCapability::SPEAK)) + .with_trade_site(trade_for_site), + )), + alignment: if can_speak { + comp::Alignment::Npc + } else { + comp::Alignment::Wild + }, scale: comp::Scale(1.0), anchor: None, loot: Default::default(), From 64c56f544d0a13d9c33099cf7aded09135b34de0 Mon Sep 17 00:00:00 2001 From: IsseW Date: Sun, 14 Aug 2022 09:47:49 +0200 Subject: [PATCH 021/139] randomly walk around town --- rtsim/src/rule/npc_ai.rs | 67 ++++++++++++++++++++++++++++++++-------- world/src/site2/mod.rs | 9 +++--- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 7c2c994f55..2d7678375f 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,24 +1,65 @@ +use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; +use rand::seq::IteratorRandom; use tracing::info; use vek::*; -use crate::{ - data::npc::NpcMode, - event::OnTick, - RtState, Rule, RuleError, -}; +use world::site::SiteKind; pub struct NpcAi; impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|ctx| { - for npc in ctx.state - .data_mut() - .npcs - .values_mut() - { - // TODO: Not this - npc.target = Some((npc.wpos + Vec3::new(ctx.event.time.sin() as f32 * 16.0, ctx.event.time.cos() as f32 * 16.0, 0.0), 1.0)); + let data = &mut *ctx.state.data_mut(); + for npc in data.npcs.values_mut() { + if let Some(home_id) = npc + .home + .and_then(|site_id| data.sites.get(site_id)?.world_site) + { + if let Some((target, _)) = npc.target { + if target.distance_squared(npc.wpos) < 1.0 { + npc.target = None; + } + } else { + match &ctx.index.sites.get(home_id).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + let tile = site.wpos_tile_pos(npc.wpos.xy().as_()); + + let mut rng = rand::thread_rng(); + let cardinals = [ + Vec2::unit_x(), + Vec2::unit_y(), + -Vec2::unit_x(), + -Vec2::unit_y(), + ]; + let next_tile = cardinals + .iter() + .map(|c| tile + *c) + .filter(|tile| site.tiles.get(*tile).is_road()).choose(&mut rng).unwrap_or(tile); + + let wpos = + site.tile_center_wpos(next_tile).as_().with_z(npc.wpos.z); + + npc.target = Some((wpos, 1.0)); + }, + _ => { + // No brain T_T + }, + } + } + } else { + // TODO: Don't make homeless people walk around in circles + npc.target = Some(( + npc.wpos + + Vec3::new( + ctx.event.time.sin() as f32 * 16.0, + ctx.event.time.cos() as f32 * 16.0, + 0.0, + ), + 1.0, + )); + } } }); diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 5736adab32..95beeb6a87 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -42,10 +42,11 @@ fn reseed(rng: &mut impl Rng) -> impl Rng { ChaChaRng::from_seed(rng.gen::<[u8; pub struct Site { pub(crate) origin: Vec2, name: String, - tiles: TileGrid, - plots: Store, - plazas: Vec>, - roads: Vec>, + // NOTE: Do we want these to be public? + pub tiles: TileGrid, + pub plots: Store, + pub plazas: Vec>, + pub roads: Vec>, } impl Site { From e204789ce9ce2eebd74aa337f80cf1215bb4e366 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 15:20:22 +0100 Subject: [PATCH 022/139] Persist TimeOfDay with rtsim --- rtsim/src/data/mod.rs | 3 ++- rtsim/src/event.rs | 5 +++-- rtsim/src/gen/mod.rs | 4 ++-- rtsim/src/lib.rs | 7 ++++--- rtsim/src/rule/npc_ai.rs | 4 ++-- server/src/lib.rs | 6 ++++-- server/src/rtsim2/mod.rs | 2 +- server/src/rtsim2/tick.rs | 6 ++++-- 8 files changed, 22 insertions(+), 15 deletions(-) diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index ed6bf14210..84fc342ec5 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -8,6 +8,7 @@ pub use self::{ nature::Nature, }; +use common::resources::TimeOfDay; use enum_map::{EnumMap, EnumArray, enum_map}; use serde::{Serialize, Deserialize, ser, de}; use std::{ @@ -29,7 +30,7 @@ pub struct Data { pub npcs: Npcs, pub sites: Sites, - pub time: f64, + pub time_of_day: TimeOfDay, } pub type ReadError = rmp_serde::decode::Error; diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index b60b9825f4..72932e9668 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -1,4 +1,4 @@ -use common::resources::Time; +use common::resources::{Time, TimeOfDay}; use world::{World, IndexRef}; use super::{Rule, RtState}; @@ -18,7 +18,8 @@ impl Event for OnSetup {} #[derive(Clone)] pub struct OnTick { + pub time_of_day: TimeOfDay, + pub time: Time, pub dt: f32, - pub time: f64, } impl Event for OnTick {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 4ecc42ee14..97fe5b730e 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -26,7 +26,7 @@ impl Data { npcs: Npcs { npcs: Default::default() }, sites: Sites { sites: Default::default() }, - time: 0.0, + time_of_day: Default::default(), }; // Register sites with rtsim @@ -47,7 +47,7 @@ impl Data { .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; for _ in 0..10 { - + this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..10) { 0 => Profession::Hunter, 1..=4 => Profession::Farmer, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 9f1ed63bc6..9152bc9d11 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -10,6 +10,7 @@ pub use self::{ event::{Event, EventCtx, OnTick}, rule::{Rule, RuleError}, }; +use common::resources::{Time, TimeOfDay}; use world::{World, IndexRef}; use anymap2::SendSyncAnyMap; use tracing::{info, error}; @@ -110,9 +111,9 @@ impl RtState { .for_each(|f| f(self, world, index, &e))); } - pub fn tick(&mut self, world: &World, index: IndexRef, dt: f32) { - self.data_mut().time += dt as f64; - let event = OnTick { dt, time: self.data().time }; + pub fn tick(&mut self, world: &World, index: IndexRef, time_of_day: TimeOfDay, time: Time, dt: f32) { + self.data_mut().time_of_day = time_of_day; + let event = OnTick { time_of_day, time, dt }; self.emit(event, world, index); } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 2d7678375f..8579bba26e 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -53,8 +53,8 @@ impl Rule for NpcAi { npc.target = Some(( npc.wpos + Vec3::new( - ctx.event.time.sin() as f32 * 16.0, - ctx.event.time.cos() as f32 * 16.0, + ctx.event.time.0.sin() as f32 * 16.0, + ctx.event.time.0.cos() as f32 * 16.0, 0.0, ), 1.0, diff --git a/server/src/lib.rs b/server/src/lib.rs index 03c5ef4fa3..de44b7edc1 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -66,7 +66,6 @@ use crate::{ login_provider::LoginProvider, persistence::PersistedComponents, presence::{Presence, RegionSubscription, RepositionOnChunkLoad}, - // rtsim::RtSim, state_ext::StateExt, sys::sentinel::DeletedEntities, }; @@ -567,7 +566,10 @@ impl Server { #[cfg(feature = "worldgen")] { match rtsim2::RtSim::new(index.as_index_ref(), &world, data_dir.to_owned()) { - Ok(rtsim) => state.ecs_mut().insert(rtsim), + Ok(rtsim) => { + state.ecs_mut().insert(rtsim.state().data().time_of_day); + state.ecs_mut().insert(rtsim); + }, Err(err) => { error!("Failed to load rtsim: {}", err); return Err(Error::RtsimError(err)); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 748b99b054..6f2faf4357 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -44,7 +44,7 @@ impl RtSim { pub fn new(index: IndexRef, world: &World, data_dir: PathBuf) -> Result { let file_path = Self::get_file_path(data_dir); - info!("Looking for rtsim data in {}...", file_path.display()); + info!("Looking for rtsim data at {}...", file_path.display()); let data = 'load: { if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { match File::open(&file_path) { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 457a136fda..8566d9d653 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -6,7 +6,7 @@ use common::{ comp::{self, inventory::loadout::Loadout, skillset::skills}, event::{EventBus, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, - resources::{DeltaTime, Time}, + resources::{DeltaTime, Time, TimeOfDay}, rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, LoadoutBuilder, @@ -24,6 +24,7 @@ impl<'a> System<'a> for Sys { type SystemData = ( Read<'a, DeltaTime>, Read<'a, Time>, + Read<'a, TimeOfDay>, Read<'a, EventBus>, WriteExpect<'a, RtSim>, ReadExpect<'a, Arc>, @@ -43,6 +44,7 @@ impl<'a> System<'a> for Sys { ( dt, time, + time_of_day, mut server_event_bus, mut rtsim, world, @@ -56,7 +58,7 @@ impl<'a> System<'a> for Sys { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - rtsim.state.tick(&world, index.as_index_ref(), dt.0); + rtsim.state.tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); if rtsim .last_saved From f40cfb4ac32d95ef8afa68560d0f2f6877039fa5 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 16:08:10 +0100 Subject: [PATCH 023/139] Made farmers sell food --- common/src/rtsim.rs | 26 ++++++++++++++++++++ rtsim/src/data/npc.rs | 11 +-------- server/src/rtsim2/tick.rs | 36 +++++++++++++++++----------- world/src/site/settlement/mod.rs | 41 ++++++++++++++++++++++---------- 4 files changed, 78 insertions(+), 36 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 3852d09190..bd5684a6d5 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -93,3 +93,29 @@ pub enum ChunkResource { #[serde(rename = "2")] Cotton, } + +#[derive(Clone, Serialize, Deserialize)] +pub enum Profession { + #[serde(rename = "0")] + Farmer, + #[serde(rename = "1")] + Hunter, + #[serde(rename = "2")] + Merchant, + #[serde(rename = "3")] + Guard, + #[serde(rename = "4")] + Adventurer(u32), +} + +impl Profession { + pub fn to_name(&self) -> String { + match self { + Self::Farmer => "Farmer".to_string(), + Self::Hunter => "Hunter".to_string(), + Self::Merchant => "Merchant".to_string(), + Self::Guard => "Guard".to_string(), + Self::Adventurer(_) => "Adventurer".to_string(), + } + } +} diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bc0594d608..8d6e71f1de 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -11,16 +11,7 @@ use common::{ comp, }; use world::util::RandomPerm; -pub use common::rtsim::NpcId; - -#[derive(Clone, Serialize, Deserialize)] -pub enum Profession { - Farmer, - Hunter, - Merchant, - Guard, - Adventurer(u32), -} +pub use common::rtsim::{NpcId, Profession}; #[derive(Copy, Clone, Default)] pub enum NpcMode { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 8566d9d653..1249db34be 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -9,6 +9,7 @@ use common::{ resources::{DeltaTime, Time, TimeOfDay}, rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, + trade::Good, LoadoutBuilder, SkillSetBuilder, }; @@ -16,7 +17,7 @@ use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::npc::{NpcMode, Profession}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; -use world::site::settlement::merchant_loadout; +use world::site::settlement::{merchant_loadout, trader_loadout}; #[derive(Default)] pub struct Sys; @@ -58,6 +59,7 @@ impl<'a> System<'a> for Sys { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; + rtsim.state.data_mut().time_of_day = *time_of_day; rtsim.state.tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); if rtsim @@ -84,23 +86,26 @@ impl<'a> System<'a> for Sys { let mut loadout_builder = LoadoutBuilder::from_default(&body); let mut rng = npc.rng(3); + let economy = npc.home + .and_then(|home| { + let site = data.sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }); + if let Some(ref profession) = npc.profession { loadout_builder = match profession { Profession::Guard => loadout_builder .with_asset_expect("common.loadout.village.guard", &mut rng), - Profession::Merchant => { - merchant_loadout( - loadout_builder, - npc.home - .and_then(|home| { - let site = data.sites.get(home)?.world_site?; - index.sites.get(site).trade_information(site.id()) - }).as_ref(), - ) - } + Profession::Merchant => merchant_loadout(loadout_builder, economy.as_ref()), - Profession::Farmer | Profession::Hunter => loadout_builder + Profession::Farmer => trader_loadout( + loadout_builder + .with_asset_expect("common.loadout.village.villager", &mut rng), + economy.as_ref(), + |good| matches!(good, Good::Food), + ), + Profession::Hunter => loadout_builder .with_asset_expect("common.loadout.village.villager", &mut rng), Profession::Adventurer(level) => todo!(), @@ -109,7 +114,7 @@ impl<'a> System<'a> for Sys { let can_speak = npc.profession.is_some(); // TODO: not this - let trade_for_site = if let Some(Profession::Merchant) = npc.profession { + let trade_for_site = if let Some(Profession::Merchant | Profession::Farmer) = npc.profession { npc.home.and_then(|home| Some(data.sites.get(home)?.world_site?.id())) } else { None @@ -121,7 +126,10 @@ impl<'a> System<'a> for Sys { .unwrap_or(0); emitter.emit(ServerEvent::CreateNpc { pos: comp::Pos(npc.wpos), - stats: comp::Stats::new("Rtsim NPC".to_string()), + stats: comp::Stats::new(npc.profession + .as_ref() + .map(|p| p.to_name()) + .unwrap_or_else(|| "Rtsim NPC".to_string())), skill_set: skill_set, health: Some(comp::Health::new(body, health_level)), poise: comp::Poise::new(body), diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 79f027b004..c77e8e24b2 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -1011,6 +1011,15 @@ fn humanoid(pos: Vec3, economy: &SiteInformation, dynamic_rng: &mut impl Rn pub fn merchant_loadout( loadout_builder: LoadoutBuilder, economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder + .with_asset_expect("common.loadout.village.merchant", &mut thread_rng()), economy, |_| true) +} + +pub fn trader_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, + mut permitted: impl FnMut(Good) -> bool, ) -> LoadoutBuilder { let rng = &mut thread_rng(); @@ -1021,7 +1030,10 @@ pub fn merchant_loadout( let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack"); let slots = backpack.slots().len() + 4 * bag1.slots().len(); let mut stockmap: HashMap = economy - .map(|e| e.unconsumed_stock.clone()) + .map(|e| e.unconsumed_stock.clone() + .into_iter() + .filter(|(good, _)| permitted(*good)) + .collect()) .unwrap_or_default(); // modify stock for better gameplay @@ -1029,21 +1041,27 @@ pub fn merchant_loadout( // for the players to buy; the `.max` is temporary to ensure that there's some // food for sale at every site, to be used until we have some solution like NPC // houses as a limit on econsim population growth - stockmap - .entry(Good::Food) - .and_modify(|e| *e = e.max(10_000.0)) - .or_insert(10_000.0); + if permitted(Good::Food) { + stockmap + .entry(Good::Food) + .and_modify(|e| *e = e.max(10_000.0)) + .or_insert(10_000.0); + } // Reduce amount of potions so merchants do not oversupply potions. // TODO: Maybe remove when merchants and their inventories are rtsim? // Note: Likely without effect now that potions are counted as food - stockmap - .entry(Good::Potions) - .and_modify(|e| *e = e.powf(0.25)); + if permitted(Good::Potions) { + stockmap + .entry(Good::Potions) + .and_modify(|e| *e = e.powf(0.25)); + } // It's safe to truncate here, because coins clamped to 3000 max // also we don't really want negative values here - stockmap - .entry(Good::Coin) - .and_modify(|e| *e = e.min(rng.gen_range(1000.0..3000.0))); + if permitted(Good::Coin) { + stockmap + .entry(Good::Coin) + .and_modify(|e| *e = e.min(rng.gen_range(1000.0..3000.0))); + } // assume roughly 10 merchants sharing a town's stock (other logic for coins) stockmap .iter_mut() @@ -1073,7 +1091,6 @@ pub fn merchant_loadout( transfer(&mut wares, &mut bag4); loadout_builder - .with_asset_expect("common.loadout.village.merchant", rng) .back(Some(backpack)) .bag(ArmorSlot::Bag1, Some(bag1)) .bag(ArmorSlot::Bag2, Some(bag2)) From afd9ea5462a500c99ef5b64992ba614cda8c4da0 Mon Sep 17 00:00:00 2001 From: IsseW Date: Sun, 14 Aug 2022 17:17:42 +0200 Subject: [PATCH 024/139] site pathing --- Cargo.lock | 1 + rtsim/Cargo.toml | 1 + rtsim/src/rule/npc_ai.rs | 158 +++++++++++++++++++++++++++------- server/src/state_ext.rs | 1 + world/src/site2/mod.rs | 10 +-- world/src/site2/plot/house.rs | 2 +- world/src/site2/tile.rs | 4 +- 7 files changed, 137 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 237da3cbc5..292eded160 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6948,6 +6948,7 @@ dependencies = [ "anymap2", "atomic_refcell", "enum-map", + "fxhash", "hashbrown 0.12.3", "rand 0.8.5", "rmp-serde", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index da380b2066..eb01cb8640 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -17,3 +17,4 @@ tracing = "0.1" atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } +fxhash = "0.2.1" \ No newline at end of file diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8579bba26e..cdd866beb0 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,11 +1,131 @@ -use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; -use rand::seq::IteratorRandom; -use tracing::info; +use std::hash::BuildHasherDefault; + +use crate::{ + event::OnTick, + RtState, Rule, RuleError, +}; +use common::{astar::{Astar, PathResult}, store::Id}; +use fxhash::FxHasher64; +use rand::{seq::IteratorRandom, rngs::SmallRng, SeedableRng}; use vek::*; -use world::site::SiteKind; +use world::{ + site::{Site as WorldSite, SiteKind}, + site2::{self, TileKind}, + IndexRef, +}; pub struct NpcAi; +const NEIGHBOURS: &[Vec2] = &[ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(-1, 0), + Vec2::new(0, -1), + Vec2::new(1, 1), + Vec2::new(-1, 1), + Vec2::new(-1, -1), + Vec2::new(1, -1), +]; +const CARDINALS: &[Vec2] = &[ + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(-1, 0), + Vec2::new(0, -1), +]; + +fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { + let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); + let mut astar = Astar::new( + 100, + start, + &heuristic, + BuildHasherDefault::::default(), + ); + + let transition = |a: &Vec2, b: &Vec2| { + let distance = a.as_::().distance(b.as_()); + let a_tile = site.tiles.get(*a); + let b_tile = site.tiles.get(*b); + + let terrain = match &b_tile.kind { + TileKind::Empty => 5.0, + TileKind::Hazard(_) => 20.0, + TileKind::Field => 12.0, + TileKind::Plaza + | TileKind::Road { .. } => 1.0, + + TileKind::Building + | TileKind::Castle + | TileKind::Wall(_) + | TileKind::Tower(_) + | TileKind::Keep(_) + | TileKind::Gate + | TileKind::GnarlingFortification => 3.0, + }; + let is_door_tile = |plot: Id, tile: Vec2| { + match site.plot(plot).kind() { + site2::PlotKind::House(house) => house.door_tile == tile, + site2::PlotKind::Workshop(_) => true, + _ => false, + } + }; + let building = if a_tile.is_building() && b_tile.is_road() { + a_tile.plot.and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)).unwrap_or(f32::INFINITY) + } else if b_tile.is_building() && a_tile.is_road() { + b_tile.plot.and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)).unwrap_or(f32::INFINITY) + } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { + f32::INFINITY + } else { + 1.0 + }; + + distance * terrain * building + }; + + astar.poll( + 100, + heuristic, + |&tile| NEIGHBOURS.iter().map(move |c| tile + *c), + transition, + |tile| *tile == end, + ) +} + +fn path_town( + wpos: Vec3, + site: Id, + index: IndexRef, + time: f64, + seed: u32, +) -> Option<(Vec3, f32)> { + match &index.sites.get(site).kind { + SiteKind::Refactor(site) | SiteKind::CliffTown(site) | SiteKind::DesertCity(site) => { + let start = site.wpos_tile_pos(wpos.xy().as_()); + + let mut rng = SmallRng::from_seed([(time / 3.0) as u8 ^ seed as u8; 32]); + + let end = site.plots[site.plazas().choose(&mut rng)?].root_tile(); + + if start == end { + return None; + } + + let next_tile = match path_between(start, end, site) { + PathResult::None(p) | PathResult::Exhausted(p) | PathResult::Path(p) => p.into_iter().nth(2), + PathResult::Pending => None, + }.unwrap_or(end); + + let wpos = site.tile_center_wpos(next_tile).as_().with_z(wpos.z); + + Some((wpos, 1.0)) + }, + _ => { + // No brain T_T + None + }, + } +} + impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { @@ -16,37 +136,11 @@ impl Rule for NpcAi { .and_then(|site_id| data.sites.get(site_id)?.world_site) { if let Some((target, _)) = npc.target { - if target.distance_squared(npc.wpos) < 1.0 { + if target.xy().distance_squared(npc.wpos.xy()) < 1.0 { npc.target = None; } } else { - match &ctx.index.sites.get(home_id).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - let tile = site.wpos_tile_pos(npc.wpos.xy().as_()); - - let mut rng = rand::thread_rng(); - let cardinals = [ - Vec2::unit_x(), - Vec2::unit_y(), - -Vec2::unit_x(), - -Vec2::unit_y(), - ]; - let next_tile = cardinals - .iter() - .map(|c| tile + *c) - .filter(|tile| site.tiles.get(*tile).is_road()).choose(&mut rng).unwrap_or(tile); - - let wpos = - site.tile_center_wpos(next_tile).as_().with_z(npc.wpos.z); - - npc.target = Some((wpos, 1.0)); - }, - _ => { - // No brain T_T - }, - } + npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time, npc.seed); } } else { // TODO: Don't make homeless people walk around in circles diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 4d3bdd2b94..626499341f 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -292,6 +292,7 @@ impl StateExt for State { .with(comp::Combo::default()) .with(comp::Auras::default()) .with(comp::Stance::default()) + .with(RepositionOnChunkLoad) } fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder { diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 95beeb6a87..8b46849d7d 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -3,11 +3,11 @@ pub mod plot; mod tile; pub mod util; -use self::tile::{HazardKind, KeepKind, RoofKind, Tile, TileGrid, TileKind, TILE_SIZE}; +use self::tile::{HazardKind, KeepKind, RoofKind, Tile, TileGrid, TILE_SIZE}; pub use self::{ gen::{aabr_with_z, Fill, Painter, Primitive, PrimitiveRef, Structure}, plot::{Plot, PlotKind}, - util::Dir, + util::Dir, tile::TileKind, }; use crate::{ sim::Path, @@ -40,7 +40,7 @@ fn reseed(rng: &mut impl Rng) -> impl Rng { ChaChaRng::from_seed(rng.gen::<[u8; #[derive(Default)] pub struct Site { - pub(crate) origin: Vec2, + pub origin: Vec2, name: String, // NOTE: Do we want these to be public? pub tiles: TileGrid, @@ -110,8 +110,8 @@ impl Site { pub fn bounds(&self) -> Aabr { let border = 1; Aabr { - min: self.origin + self.tile_wpos(self.tiles.bounds.min - border), - max: self.origin + self.tile_wpos(self.tiles.bounds.max + 1 + border), + min: self.tile_wpos(self.tiles.bounds.min - border), + max: self.tile_wpos(self.tiles.bounds.max + 1 + border), } } diff --git a/world/src/site2/plot/house.rs b/world/src/site2/plot/house.rs index 2a54ae1363..380c3af782 100644 --- a/world/src/site2/plot/house.rs +++ b/world/src/site2/plot/house.rs @@ -11,7 +11,7 @@ use vek::*; /// Represents house data generated by the `generate()` method pub struct House { /// Tile position of the door tile - door_tile: Vec2, + pub door_tile: Vec2, /// Axis aligned bounding region of tiles tile_aabr: Aabr, /// Axis aligned bounding region for the house diff --git a/world/src/site2/tile.rs b/world/src/site2/tile.rs index 5a3b951baa..a02849f4fe 100644 --- a/world/src/site2/tile.rs +++ b/world/src/site2/tile.rs @@ -193,8 +193,8 @@ pub enum TileKind { #[derive(Clone, PartialEq)] pub struct Tile { - pub(crate) kind: TileKind, - pub(crate) plot: Option>, + pub kind: TileKind, + pub plot: Option>, pub(crate) hard_alt: Option, } From e08f7d4fa935e0288d541736ece0b93a2789b699 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 16:18:47 +0100 Subject: [PATCH 025/139] Added blacksmith --- common/src/rtsim.rs | 3 +++ rtsim/src/gen/mod.rs | 5 +++-- server/src/rtsim2/tick.rs | 8 +++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index bd5684a6d5..de97dc66bc 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -106,6 +106,8 @@ pub enum Profession { Guard, #[serde(rename = "4")] Adventurer(u32), + #[serde(rename = "5")] + Blacksmith, } impl Profession { @@ -116,6 +118,7 @@ impl Profession { Self::Merchant => "Merchant".to_string(), Self::Guard => "Guard".to_string(), Self::Adventurer(_) => "Adventurer".to_string(), + Self::Blacksmith => "Blacksmith".to_string(), } } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 97fe5b730e..3e14d02963 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -46,11 +46,12 @@ impl Data { wpos2d.map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..10 { + for _ in 0..20 { this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..10) { 0 => Profession::Hunter, - 1..=4 => Profession::Farmer, + 1 => Profession::Blacksmith, + 2..=4 => Profession::Farmer, _ => Profession::Guard, })); } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 1249db34be..88bef6b91b 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -105,6 +105,12 @@ impl<'a> System<'a> for Sys { economy.as_ref(), |good| matches!(good, Good::Food), ), + Profession::Blacksmith => trader_loadout( + loadout_builder + .with_asset_expect("common.loadout.village.blacksmith", &mut rng), + economy.as_ref(), + |good| matches!(good, Good::Tools | Good::Armor), + ), Profession::Hunter => loadout_builder .with_asset_expect("common.loadout.village.villager", &mut rng), @@ -114,7 +120,7 @@ impl<'a> System<'a> for Sys { let can_speak = npc.profession.is_some(); // TODO: not this - let trade_for_site = if let Some(Profession::Merchant | Profession::Farmer) = npc.profession { + let trade_for_site = if let Some(Profession::Merchant | Profession::Farmer | Profession::Blacksmith) = npc.profession { npc.home.and_then(|home| Some(data.sites.get(home)?.world_site?.id())) } else { None From bccbbfa3b9713978a3a0b6de6836ac0b2b544f98 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 16:21:24 +0100 Subject: [PATCH 026/139] Fixed up broken time --- rtsim/src/rule/npc_ai.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index cdd866beb0..136557e727 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -74,7 +74,7 @@ fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes } else if b_tile.is_building() && a_tile.is_road() { b_tile.plot.and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)).unwrap_or(f32::INFINITY) } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { - f32::INFINITY + f32::INFINITY } else { 1.0 }; @@ -140,7 +140,7 @@ impl Rule for NpcAi { npc.target = None; } } else { - npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time, npc.seed); + npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time.0, npc.seed); } } else { // TODO: Don't make homeless people walk around in circles From 6397e283b2eef7c74fbd439a02f332db27cea04d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 14 Aug 2022 16:38:31 +0100 Subject: [PATCH 027/139] Added world settings --- common/src/rtsim.rs | 13 +++++++++++++ rtsim/src/gen/mod.rs | 8 ++++++-- server/src/lib.rs | 4 ++-- server/src/rtsim2/mod.rs | 6 +++--- server/src/settings.rs | 6 +++++- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index de97dc66bc..905a2c0e9b 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -122,3 +122,16 @@ impl Profession { } } } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WorldSettings { + pub start_time: f64, +} + +impl Default for WorldSettings { + fn default() -> Self { + Self { + start_time: 9.0 * 3600.0, // 9am + } + } +} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 3e14d02963..898d49b055 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -9,6 +9,10 @@ use crate::data::{ use hashbrown::HashMap; use rand::prelude::*; use tracing::info; +use common::{ + rtsim::WorldSettings, + resources::TimeOfDay, +}; use world::{ site::SiteKind, IndexRef, @@ -16,7 +20,7 @@ use world::{ }; impl Data { - pub fn generate(world: &World, index: IndexRef) -> Self { + pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self { let mut seed = [0; 32]; seed.iter_mut().zip(&mut index.seed.to_le_bytes()).for_each(|(dst, src)| *dst = *src); let mut rng = SmallRng::from_seed(seed); @@ -26,7 +30,7 @@ impl Data { npcs: Npcs { npcs: Default::default() }, sites: Sites { sites: Default::default() }, - time_of_day: Default::default(), + time_of_day: TimeOfDay(settings.start_time), }; // Register sites with rtsim diff --git a/server/src/lib.rs b/server/src/lib.rs index de44b7edc1..94c8dc7efb 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -459,7 +459,7 @@ impl Server { state.ecs_mut().insert(index.clone()); // Set starting time for the server. - state.ecs_mut().write_resource::().0 = settings.start_time; + state.ecs_mut().write_resource::().0 = settings.world.start_time; // Register trackers sys::sentinel::UpdateTrackers::register(state.ecs_mut()); @@ -565,7 +565,7 @@ impl Server { // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { - match rtsim2::RtSim::new(index.as_index_ref(), &world, data_dir.to_owned()) { + match rtsim2::RtSim::new(&settings.world, index.as_index_ref(), &world, data_dir.to_owned()) { Ok(rtsim) => { state.ecs_mut().insert(rtsim.state().data().time_of_day); state.ecs_mut().insert(rtsim); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 6f2faf4357..2df1720c1c 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -5,7 +5,7 @@ pub mod tick; use common::{ grid::Grid, slowjob::SlowJobPool, - rtsim::{ChunkResource, RtSimEntity}, + rtsim::{ChunkResource, RtSimEntity, WorldSettings}, terrain::{TerrainChunk, Block}, vol::RectRasterableVol, }; @@ -41,7 +41,7 @@ pub struct RtSim { } impl RtSim { - pub fn new(index: IndexRef, world: &World, data_dir: PathBuf) -> Result { + pub fn new(settings: &WorldSettings, index: IndexRef, world: &World, data_dir: PathBuf) -> Result { let file_path = Self::get_file_path(data_dir); info!("Looking for rtsim data at {}...", file_path.display()); @@ -81,7 +81,7 @@ impl RtSim { warn!("'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be overwritten)."); } - let data = Data::generate(&world, index); + let data = Data::generate(settings, &world, index); info!("Rtsim data generated."); data }; diff --git a/server/src/settings.rs b/server/src/settings.rs index 304a95b24e..a01e420e09 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -17,6 +17,7 @@ use chrono::Utc; use common::{ calendar::{Calendar, CalendarEvent}, resources::BattleMode, + rtsim::WorldSettings, }; use core::time::Duration; use portpicker::pick_unused_port; @@ -184,6 +185,9 @@ pub struct Settings { pub gameplay: GameplaySettings, #[serde(default)] pub moderation: ModerationSettings, + + #[serde(default)] + pub world: WorldSettings, } impl Default for Settings { @@ -213,6 +217,7 @@ impl Default for Settings { experimental_terrain_persistence: false, gameplay: GameplaySettings::default(), moderation: ModerationSettings::default(), + world: WorldSettings::default(), } } } @@ -292,7 +297,6 @@ impl Settings { }, server_name: "Singleplayer".to_owned(), max_players: 100, - start_time: 9.0 * 3600.0, max_view_distance: None, client_timeout: Duration::from_secs(180), ..load // Fill in remaining fields from server_settings.ron. From e7798f2a4e4d6d23132de8d442b0105c7be3f677 Mon Sep 17 00:00:00 2001 From: IsseW Date: Sun, 14 Aug 2022 18:09:24 +0200 Subject: [PATCH 028/139] use world alt for pathing --- rtsim/src/rule/npc_ai.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 136557e727..41a649bd63 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -11,7 +11,7 @@ use vek::*; use world::{ site::{Site as WorldSite, SiteKind}, site2::{self, TileKind}, - IndexRef, + IndexRef, World, }; pub struct NpcAi; @@ -97,6 +97,7 @@ fn path_town( index: IndexRef, time: f64, seed: u32, + world: &World, ) -> Option<(Vec3, f32)> { match &index.sites.get(site).kind { SiteKind::Refactor(site) | SiteKind::CliffTown(site) | SiteKind::DesertCity(site) => { @@ -115,7 +116,8 @@ fn path_town( PathResult::Pending => None, }.unwrap_or(end); - let wpos = site.tile_center_wpos(next_tile).as_().with_z(wpos.z); + let wpos = site.tile_center_wpos(next_tile); + let wpos = wpos.as_::().with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0)); Some((wpos, 1.0)) }, @@ -140,7 +142,7 @@ impl Rule for NpcAi { npc.target = None; } } else { - npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time.0, npc.seed); + npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time.0, npc.seed, ctx.world); } } else { // TODO: Don't make homeless people walk around in circles From ca02b5e97c70ac7271699b3d9908c90204b1c7b9 Mon Sep 17 00:00:00 2001 From: IsseW Date: Mon, 15 Aug 2022 12:02:38 +0200 Subject: [PATCH 029/139] cleaner entity creation --- assets/common/entity/village/merchant.ron | 1 + common/src/generation.rs | 13 ++ common/src/rtsim.rs | 12 ++ rtsim/src/gen/mod.rs | 6 +- server/src/rtsim2/tick.rs | 212 ++++++++++++++-------- 5 files changed, 166 insertions(+), 78 deletions(-) diff --git a/assets/common/entity/village/merchant.ron b/assets/common/entity/village/merchant.ron index 13b9d6ccc1..64ae86cb98 100644 --- a/assets/common/entity/village/merchant.ron +++ b/assets/common/entity/village/merchant.ron @@ -6,6 +6,7 @@ loot: LootTable("common.loot_tables.creature.humanoid"), inventory: ( loadout: Inline(( + inherit: Asset("common.loadout.village.merchant"), active_hands: InHands((Choice([ (2, ModularWeapon(tool: Bow, material: Eldwood, hands: None)), (1, ModularWeapon(tool: Sword, material: Steel, hands: None)), diff --git a/common/src/generation.rs b/common/src/generation.rs index e12864c2c8..ef37b72037 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -379,6 +379,12 @@ impl EntityInfo { self.agent_mark = Some(agent_mark); self } + + #[must_use] + pub fn with_maybe_agent_mark(mut self, agent_mark: Option) -> Self { + self.agent_mark = agent_mark; + self + } #[must_use] pub fn with_loot_drop(mut self, loot_drop: LootSpec) -> Self { @@ -441,6 +447,13 @@ impl EntityInfo { self } + /// map contains price+amount + #[must_use] + pub fn with_maybe_economy(mut self, e: Option<&SiteInformation>) -> Self { + self.trading_information = e.cloned(); + self + } + #[must_use] pub fn with_no_flee(mut self) -> Self { self.no_flee = true; diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 905a2c0e9b..20e8717eb5 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -108,6 +108,14 @@ pub enum Profession { Adventurer(u32), #[serde(rename = "5")] Blacksmith, + #[serde(rename = "6")] + Chef, + #[serde(rename = "7")] + Alchemist, + #[serde(rename = "8")] + Pirate, + #[serde(rename = "9")] + Cultist, } impl Profession { @@ -119,6 +127,10 @@ impl Profession { Self::Guard => "Guard".to_string(), Self::Adventurer(_) => "Adventurer".to_string(), Self::Blacksmith => "Blacksmith".to_string(), + Self::Chef => "Chef".to_string(), + Self::Alchemist => "Alchemist".to_string(), + Self::Pirate => "Pirate".to_string(), + Self::Cultist => "Cultist".to_string(), } } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 898d49b055..8c331134cf 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -52,10 +52,12 @@ impl Data { }; for _ in 0..20 { - this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..10) { + this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..15) { 0 => Profession::Hunter, 1 => Profession::Blacksmith, - 2..=4 => Profession::Farmer, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=10 => Profession::Farmer, _ => Profession::Guard, })); } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 88bef6b91b..7d94018efd 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -9,15 +9,114 @@ use common::{ resources::{DeltaTime, Time, TimeOfDay}, rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, - trade::Good, + trade::{Good, SiteInformation}, LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; -use rtsim2::data::npc::{NpcMode, Profession}; +use rtsim2::data::{npc::{NpcMode, Profession}, Npc, Sites}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; -use world::site::settlement::{merchant_loadout, trader_loadout}; +use world::site::settlement::trader_loadout; + +fn humanoid_config(profession: &Profession) -> &'static str { + match profession { + Profession::Farmer | Profession::Hunter => "common.entity.village.villager", + Profession::Merchant => "common.entity.village.merchant", + Profession::Guard => "common.entity.village.guard", + Profession::Adventurer(rank) => match rank { + 0 => "common.entity.world.traveler0", + 1 => "common.entity.world.traveler1", + 2 => "common.entity.world.traveler2", + _ => "common.entity.world.traveler3", + }, + Profession::Blacksmith => "common.entity.village.blacksmith", + Profession::Chef => "common.entity.village.chef", + Profession::Alchemist => "common.entity.village.alchemist", + Profession::Pirate => "common.entity.spot.pirate", + Profession::Cultist => "common.entity.dungeon.tier-5.cultist", + } +} + +fn loadout_default(loadout: LoadoutBuilder, _economy: Option<&SiteInformation>) -> LoadoutBuilder { + loadout +} + +fn merchant_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |_| true) +} + +fn farmer_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn chef_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn blacksmith_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Tools | Good::Armor)) +} + +fn profession_extra_loadout(profession: Option<&Profession>) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { + match profession { + Some(Profession::Merchant) => merchant_loadout, + Some(Profession::Farmer) => farmer_loadout, + Some(Profession::Chef) => chef_loadout, + Some(Profession::Blacksmith) => blacksmith_loadout, + _ => loadout_default, + } +} + +fn profession_agent_mark(profession: Option<&Profession>) -> Option { + match profession { + Some(Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith) => Some(comp::agent::Mark::Merchant), + Some(Profession::Guard) => Some(comp::agent::Mark::Guard), + _ => None, + } +} + +fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { + let body = npc.get_body(); + let pos = comp::Pos(npc.wpos); + + if let Some(ref profession) = npc.profession { + + let economy = npc.home + .and_then(|home| { + let site = sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }); + + let config_asset = humanoid_config(profession); + + let entity_config = EntityConfig::from_asset_expect_owned(config_asset).with_body(BodyBuilder::Exact(body)); + let mut rng = npc.rng(3); + EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(config_asset), &mut rng) + .with_alignment(comp::Alignment::Npc) + .with_maybe_economy(economy.as_ref()) + .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) + .with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref())) + } else { + EntityInfo::at(pos.0) + .with_body(body) + .with_alignment(comp::Alignment::Wild) + .with_name("Rtsim NPC") + } +} #[derive(Default)] pub struct Sys; @@ -46,7 +145,7 @@ impl<'a> System<'a> for Sys { dt, time, time_of_day, - mut server_event_bus, + server_event_bus, mut rtsim, world, index, @@ -82,81 +181,42 @@ impl<'a> System<'a> for Sys { && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { npc.mode = NpcMode::Loaded; - let body = npc.get_body(); - let mut loadout_builder = LoadoutBuilder::from_default(&body); - let mut rng = npc.rng(3); - let economy = npc.home - .and_then(|home| { - let site = data.sites.get(home)?.world_site?; - index.sites.get(site).trade_information(site.id()) - }); + let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - if let Some(ref profession) = npc.profession { - loadout_builder = match profession { - Profession::Guard => loadout_builder - .with_asset_expect("common.loadout.village.guard", &mut rng), - - Profession::Merchant => merchant_loadout(loadout_builder, economy.as_ref()), - - Profession::Farmer => trader_loadout( - loadout_builder - .with_asset_expect("common.loadout.village.villager", &mut rng), - economy.as_ref(), - |good| matches!(good, Good::Food), - ), - Profession::Blacksmith => trader_loadout( - loadout_builder - .with_asset_expect("common.loadout.village.blacksmith", &mut rng), - economy.as_ref(), - |good| matches!(good, Good::Tools | Good::Armor), - ), - Profession::Hunter => loadout_builder - .with_asset_expect("common.loadout.village.villager", &mut rng), - - Profession::Adventurer(level) => todo!(), - }; - } - - let can_speak = npc.profession.is_some(); // TODO: not this - - let trade_for_site = if let Some(Profession::Merchant | Profession::Farmer | Profession::Blacksmith) = npc.profession { - npc.home.and_then(|home| Some(data.sites.get(home)?.world_site?.id())) - } else { - None - }; - - let skill_set = SkillSetBuilder::default().build(); - let health_level = skill_set - .skill_level(skills::Skill::General(skills::GeneralSkill::HealthIncrease)) - .unwrap_or(0); - emitter.emit(ServerEvent::CreateNpc { - pos: comp::Pos(npc.wpos), - stats: comp::Stats::new(npc.profession - .as_ref() - .map(|p| p.to_name()) - .unwrap_or_else(|| "Rtsim NPC".to_string())), - skill_set: skill_set, - health: Some(comp::Health::new(body, health_level)), - poise: comp::Poise::new(body), - inventory: comp::Inventory::with_loadout(loadout_builder.build(), body), - body, - agent: Some(comp::Agent::from_body(&body) - .with_behavior( - comp::Behavior::default() - .maybe_with_capabilities(can_speak.then_some(comp::BehaviorCapability::SPEAK)) - .with_trade_site(trade_for_site), - )), - alignment: if can_speak { - comp::Alignment::Npc - } else { - comp::Alignment::Wild + emitter.emit(match NpcData::from_entity_info(entity_info) { + NpcData::Data { + pos, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + loot, + } => ServerEvent::CreateNpc { + pos, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + anchor: None, + loot, + rtsim_entity: Some(RtSimEntity(npc_id)), + projectile: None, }, - scale: comp::Scale(1.0), - anchor: None, - loot: Default::default(), - rtsim_entity: Some(RtSimEntity(npc_id)), - projectile: None, + // EntityConfig can't represent Waypoints at all + // as of now, and if someone will try to spawn + // rtsim waypoint it is definitely error. + NpcData::Waypoint(_) => unimplemented!(), }); } } From feaaaa9a25e22beffefb60e21578cc861c1d00ec Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 15 Aug 2022 15:00:39 +0100 Subject: [PATCH 030/139] Added initial impl of factions --- common/src/rtsim.rs | 2 ++ rtsim/src/data/faction.rs | 36 ++++++++++++++++++++++++++++++++++++ rtsim/src/data/mod.rs | 3 +++ rtsim/src/data/npc.rs | 17 ++++++++++++----- rtsim/src/data/site.rs | 9 +++++++++ rtsim/src/gen/faction.rs | 13 +++++++++++++ rtsim/src/gen/mod.rs | 39 ++++++++++++++++++++++++++++++--------- rtsim/src/gen/site.rs | 20 +++++++++----------- rtsim/src/rule/setup.rs | 2 +- 9 files changed, 115 insertions(+), 26 deletions(-) create mode 100644 rtsim/src/data/faction.rs create mode 100644 rtsim/src/gen/faction.rs diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 20e8717eb5..4202c36bcd 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -13,6 +13,8 @@ slotmap::new_key_type! { pub struct NpcId; } slotmap::new_key_type! { pub struct SiteId; } +slotmap::new_key_type! { pub struct FactionId; } + #[derive(Copy, Clone, Debug)] pub struct RtSimEntity(pub NpcId); diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs new file mode 100644 index 0000000000..f07c6296e9 --- /dev/null +++ b/rtsim/src/data/faction.rs @@ -0,0 +1,36 @@ +use hashbrown::HashMap; +use serde::{Serialize, Deserialize}; +use slotmap::HopSlotMap; +use vek::*; +use std::ops::{Deref, DerefMut}; +use common::{ + uid::Uid, + store::Id, +}; +use super::Actor; +pub use common::rtsim::FactionId; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Faction { + pub leader: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Factions { + pub factions: HopSlotMap, +} + +impl Factions { + pub fn create(&mut self, faction: Faction) -> FactionId { + self.factions.insert(faction) + } +} + +impl Deref for Factions { + type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.factions } +} + +impl DerefMut for Factions { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.factions } +} diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 84fc342ec5..f193e8eb85 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,3 +1,4 @@ +pub mod faction; pub mod npc; pub mod site; pub mod nature; @@ -5,6 +6,7 @@ pub mod nature; pub use self::{ npc::{Npc, NpcId, Npcs}, site::{Site, SiteId, Sites}, + faction::{Faction, FactionId, Factions}, nature::Nature, }; @@ -29,6 +31,7 @@ pub struct Data { pub nature: Nature, pub npcs: Npcs, pub sites: Sites, + pub factions: Factions, pub time_of_day: TimeOfDay, } diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 8d6e71f1de..ba4b16eac3 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut}; use common::{ uid::Uid, store::Id, - rtsim::{SiteId, RtSimController}, + rtsim::{SiteId, FactionId, RtSimController}, comp, }; use world::util::RandomPerm; @@ -32,6 +32,7 @@ pub struct Npc { pub profession: Option, pub home: Option, + pub faction: Option, // Unpersisted state @@ -55,18 +56,24 @@ impl Npc { wpos, profession: None, home: None, + faction: None, target: None, mode: NpcMode::Simulated, } } - pub fn with_profession(mut self, profession: Profession) -> Self { - self.profession = Some(profession); + pub fn with_profession(mut self, profession: impl Into>) -> Self { + self.profession = profession.into(); self } - pub fn with_home(mut self, home: SiteId) -> Self { - self.home = Some(home); + pub fn with_home(mut self, home: impl Into>) -> Self { + self.home = home.into(); + self + } + + pub fn with_faction(mut self, faction: impl Into>) -> Self { + self.faction = faction.into(); self } diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index 6e34be73d0..e8b3cf1dc4 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -6,6 +6,7 @@ use std::ops::{Deref, DerefMut}; use common::{ uid::Uid, store::Id, + rtsim::FactionId, }; pub use common::rtsim::SiteId; use world::site::Site as WorldSite; @@ -13,6 +14,7 @@ use world::site::Site as WorldSite; #[derive(Clone, Serialize, Deserialize)] pub struct Site { pub wpos: Vec2, + pub faction: Option, /// The site generated during initial worldgen that this site corresponds to. /// @@ -26,6 +28,13 @@ pub struct Site { pub world_site: Option>, } +impl Site { + pub fn with_faction(mut self, faction: impl Into>) -> Self { + self.faction = faction.into(); + self + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct Sites { pub sites: HopSlotMap, diff --git a/rtsim/src/gen/faction.rs b/rtsim/src/gen/faction.rs new file mode 100644 index 0000000000..0027042038 --- /dev/null +++ b/rtsim/src/gen/faction.rs @@ -0,0 +1,13 @@ +use crate::data::Faction; +use vek::*; +use rand::prelude::*; +use world::{ + World, + IndexRef, +}; + +impl Faction { + pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { + Self { leader: None } + } +} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 8c331134cf..f460949dc0 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,17 +1,22 @@ pub mod site; +pub mod faction; use crate::data::{ npc::{Npcs, Npc, Profession}, site::{Sites, Site}, + faction::{Factions, Faction}, Data, Nature, }; use hashbrown::HashMap; use rand::prelude::*; use tracing::info; +use vek::*; use common::{ rtsim::WorldSettings, resources::TimeOfDay, + terrain::TerrainChunkSize, + vol::RectVolSize, }; use world::{ site::SiteKind, @@ -29,16 +34,29 @@ impl Data { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default() }, sites: Sites { sites: Default::default() }, + factions: Factions { factions: Default::default() }, time_of_day: TimeOfDay(settings.start_time), }; + let initial_factions = (0..10) + .map(|_| { + let faction = Faction::generate(world, index, &mut rng); + let wpos = world + .sim() + .get_size() + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| rng.gen_range(0..(e * sz) as i32)); + (wpos, this.factions.create(faction)) + }) + .collect::>(); + info!("Generated {} rtsim factions.", this.factions.len()); + // Register sites with rtsim for (world_site_id, _) in index .sites .iter() { - let site = Site::generate(world_site_id, world, index); + let site = Site::generate(world_site_id, world, index, &initial_factions); this.sites.create(site); } info!("Registering {} rtsim sites from world sites.", this.sites.len()); @@ -52,17 +70,20 @@ impl Data { }; for _ in 0..20 { - this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(match rng.gen_range(0..15) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - _ => Profession::Guard, - })); + this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id).with_profession(match rng.gen_range(0..15) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=10 => Profession::Farmer, + _ => Profession::Guard, + })); } this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(Profession::Merchant)); } + info!("Generated {} rtsim NPCs.", this.npcs.len()); this } diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index 75a0876611..dad713b48c 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,5 +1,6 @@ -use crate::data::Site; +use crate::data::{Site, FactionId}; use common::store::Id; +use vek::*; use world::{ site::Site as WorldSite, World, @@ -7,19 +8,16 @@ use world::{ }; impl Site { - pub fn generate(world_site: Id, world: &World, index: IndexRef) -> Self { - // match &world_site.kind { - // SiteKind::Refactor(site2) => { - // let site = Site::generate(world_site_id, world, index); - // println!("Registering rtsim site at {:?}...", site.wpos); - // this.sites.create(site); - // } - // _ => {}, - // } + pub fn generate(world_site: Id, world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)]) -> Self { + let wpos = index.sites.get(world_site).get_origin(); Self { - wpos: index.sites.get(world_site).get_origin(), + wpos, world_site: Some(world_site), + faction: nearby_factions + .iter() + .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) + .map(|(_, faction)| *faction), } } } diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index ae35d1f27c..cbcda44025 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -41,7 +41,7 @@ impl Rule for Setup { warn!("{:?} is new and does not have a corresponding rtsim site. One will now be generated afresh.", world_site_id); data .sites - .create(Site::generate(world_site_id, ctx.world, ctx.index)); + .create(Site::generate(world_site_id, ctx.world, ctx.index, &[])); } } From 9be6c7b5274f45343815d0ffcf2ec1e5d282a14f Mon Sep 17 00:00:00 2001 From: IsseW Date: Mon, 15 Aug 2022 20:54:01 +0200 Subject: [PATCH 031/139] Pathing between sites. --- Cargo.lock | 1 + common/src/astar.rs | 9 + common/src/path.rs | 2 +- rtsim/Cargo.toml | 3 +- rtsim/src/data/npc.rs | 25 ++- rtsim/src/data/site.rs | 10 +- rtsim/src/gen/mod.rs | 7 +- rtsim/src/rule/npc_ai.rs | 337 ++++++++++++++++++++++++++++---- rtsim/src/rule/simulate_npcs.rs | 8 +- server/src/rtsim2/tick.rs | 3 +- world/src/civ/mod.rs | 13 +- 11 files changed, 365 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 292eded160..25bb316998 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6950,6 +6950,7 @@ dependencies = [ "enum-map", "fxhash", "hashbrown 0.12.3", + "itertools", "rand 0.8.5", "rmp-serde", "ron 0.8.0", diff --git a/common/src/astar.rs b/common/src/astar.rs index 4f39310cd3..37b9b09f95 100644 --- a/common/src/astar.rs +++ b/common/src/astar.rs @@ -45,6 +45,15 @@ impl PathResult { _ => None, } } + + pub fn map(self, f: impl FnOnce(Path) -> Path) -> PathResult { + match self { + PathResult::None(p) => PathResult::None(f(p)), + PathResult::Exhausted(p) => PathResult::Exhausted(f(p)), + PathResult::Path(p) => PathResult::Path(f(p)), + PathResult::Pending => PathResult::Pending, + } + } } #[derive(Clone)] diff --git a/common/src/path.rs b/common/src/path.rs index 439875276b..682a9ff16c 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -19,7 +19,7 @@ use vek::*; #[derive(Clone, Debug)] pub struct Path { - nodes: Vec, + pub nodes: Vec, } impl Default for Path { diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index eb01cb8640..1d7665bcc4 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -17,4 +17,5 @@ tracing = "0.1" atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } -fxhash = "0.2.1" \ No newline at end of file +fxhash = "0.2.1" +itertools = "0.10.3" \ No newline at end of file diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index ba4b16eac3..aaf8924d51 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,14 +3,15 @@ use serde::{Serialize, Deserialize}; use slotmap::HopSlotMap; use vek::*; use rand::prelude::*; -use std::ops::{Deref, DerefMut}; +use std::{ops::{Deref, DerefMut}, collections::VecDeque}; use common::{ uid::Uid, store::Id, rtsim::{SiteId, FactionId, RtSimController}, comp, }; -use world::util::RandomPerm; +use world::{util::RandomPerm, civ::Track}; +use world::site::Site as WorldSite; pub use common::rtsim::{NpcId, Profession}; #[derive(Copy, Clone, Default)] @@ -22,6 +23,19 @@ pub enum NpcMode { Loaded, } +#[derive(Clone)] +pub struct PathData { + pub end: N, + pub path: VecDeque

, + pub repoll: bool, +} + +#[derive(Clone, Default)] +pub struct PathingMemory { + pub intrasite_path: Option<(PathData, Vec2>, Id)>, + pub intersite_path: Option<(PathData<(Id, bool), SiteId>, usize)>, +} + #[derive(Clone, Serialize, Deserialize)] pub struct Npc { // Persisted state @@ -35,6 +49,11 @@ pub struct Npc { pub faction: Option, // Unpersisted state + #[serde(skip_serializing, skip_deserializing)] + pub pathing: PathingMemory, + + #[serde(skip_serializing, skip_deserializing)] + pub current_site: Option, /// (wpos, speed_factor) #[serde(skip_serializing, skip_deserializing)] @@ -57,6 +76,8 @@ impl Npc { profession: None, home: None, faction: None, + pathing: Default::default(), + current_site: None, target: None, mode: NpcMode::Simulated, } diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index e8b3cf1dc4..9189d4a97b 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -38,11 +38,19 @@ impl Site { #[derive(Clone, Serialize, Deserialize)] pub struct Sites { pub sites: HopSlotMap, + + #[serde(skip_serializing, skip_deserializing)] + pub world_site_map: HashMap, SiteId>, } impl Sites { pub fn create(&mut self, site: Site) -> SiteId { - self.sites.insert(site) + let world_site = site.world_site; + let key = self.sites.insert(site); + if let Some(world_site) = world_site { + self.world_site_map.insert(world_site, key); + } + key } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index f460949dc0..e452294ac4 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -33,7 +33,7 @@ impl Data { let mut this = Self { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default() }, - sites: Sites { sites: Default::default() }, + sites: Sites { sites: Default::default(), world_site_map: Default::default() }, factions: Factions { factions: Default::default() }, time_of_day: TimeOfDay(settings.start_time), @@ -72,13 +72,14 @@ impl Data { this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) - .with_home(site_id).with_profession(match rng.gen_range(0..15) { + .with_home(site_id).with_profession(match rng.gen_range(0..20) { 0 => Profession::Hunter, 1 => Profession::Blacksmith, 2 => Profession::Chef, 3 => Profession::Alchemist, 5..=10 => Profession::Farmer, - _ => Profession::Guard, + 11..=15 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), })); } this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(Profession::Merchant)); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 41a649bd63..6eabd789fc 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,14 +1,23 @@ -use std::hash::BuildHasherDefault; +use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ + data::{npc::PathData, Sites}, event::OnTick, RtState, Rule, RuleError, }; -use common::{astar::{Astar, PathResult}, store::Id}; +use common::{ + astar::{Astar, PathResult}, + path::Path, + rtsim::{Profession, SiteId}, + store::Id, + terrain::TerrainChunkSize, +}; use fxhash::FxHasher64; -use rand::{seq::IteratorRandom, rngs::SmallRng, SeedableRng}; +use itertools::Itertools; +use rand::seq::IteratorRandom; use vek::*; use world::{ + civ::{self, Track}, site::{Site as WorldSite, SiteKind}, site2::{self, TileKind}, IndexRef, World, @@ -33,7 +42,7 @@ const CARDINALS: &[Vec2] = &[ Vec2::new(0, -1), ]; -fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { +fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); let mut astar = Astar::new( 100, @@ -51,8 +60,7 @@ fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes TileKind::Empty => 5.0, TileKind::Hazard(_) => 20.0, TileKind::Field => 12.0, - TileKind::Plaza - | TileKind::Road { .. } => 1.0, + TileKind::Plaza | TileKind::Road { .. } => 1.0, TileKind::Building | TileKind::Castle @@ -62,17 +70,21 @@ fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes | TileKind::Gate | TileKind::GnarlingFortification => 3.0, }; - let is_door_tile = |plot: Id, tile: Vec2| { - match site.plot(plot).kind() { - site2::PlotKind::House(house) => house.door_tile == tile, - site2::PlotKind::Workshop(_) => true, - _ => false, - } + let is_door_tile = |plot: Id, tile: Vec2| match site.plot(plot).kind() { + site2::PlotKind::House(house) => house.door_tile == tile, + site2::PlotKind::Workshop(_) => true, + _ => false, }; let building = if a_tile.is_building() && b_tile.is_road() { - a_tile.plot.and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)).unwrap_or(f32::INFINITY) + a_tile + .plot + .and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)) + .unwrap_or(f32::INFINITY) } else if b_tile.is_building() && a_tile.is_road() { - b_tile.plot.and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)).unwrap_or(f32::INFINITY) + b_tile + .plot + .and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)) + .unwrap_or(f32::INFINITY) } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { f32::INFINITY } else { @@ -91,35 +103,98 @@ fn path_between(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes ) } +fn path_between_sites( + start: SiteId, + end: SiteId, + sites: &Sites, + world: &World, +) -> PathResult<(Id, bool)> { + let world_site = |site_id: SiteId| { + let id = sites.get(site_id).and_then(|site| site.world_site)?; + world.civs().sites.recreate_id(id.id()) + }; + + let start = if let Some(start) = world_site(start) { + start + } else { + return PathResult::Pending; + }; + let end = if let Some(end) = world_site(end) { + end + } else { + return PathResult::Pending; + }; + + let get_site = |site: &Id| world.civs().sites.get(*site); + + let end_pos = get_site(&end).center.as_::(); + let heuristic = |site: &Id| get_site(site).center.as_().distance(end_pos); + + let mut astar = Astar::new( + 100, + start, + heuristic, + BuildHasherDefault::::default(), + ); + + let neighbors = |site: &Id| world.civs().neighbors(*site); + + let track_between = |a: Id, b: Id| { + world + .civs() + .tracks + .get(world.civs().track_between(a, b).unwrap().0) + }; + + let transition = |a: &Id, b: &Id| track_between(*a, *b).cost; + + let path = astar.poll(100, heuristic, neighbors, transition, |site| *site == end); + + path.map(|path| { + let path = path + .into_iter() + .tuple_windows::<(_, _)>() + .map(|(a, b)| world.civs().track_between(a, b).unwrap()) + .collect_vec(); + Path { nodes: path } + }) +} + fn path_town( wpos: Vec3, site: Id, index: IndexRef, - time: f64, - seed: u32, - world: &World, -) -> Option<(Vec3, f32)> { + end: impl FnOnce(&site2::Site) -> Option>, +) -> Option, Vec2>> { match &index.sites.get(site).kind { SiteKind::Refactor(site) | SiteKind::CliffTown(site) | SiteKind::DesertCity(site) => { let start = site.wpos_tile_pos(wpos.xy().as_()); - let mut rng = SmallRng::from_seed([(time / 3.0) as u8 ^ seed as u8; 32]); - - let end = site.plots[site.plazas().choose(&mut rng)?].root_tile(); + let end = end(site)?; if start == end { return None; } - let next_tile = match path_between(start, end, site) { - PathResult::None(p) | PathResult::Exhausted(p) | PathResult::Path(p) => p.into_iter().nth(2), - PathResult::Pending => None, - }.unwrap_or(end); + // We pop the first element of the path + fn pop_first(mut queue: VecDeque) -> VecDeque { + queue.pop_front(); + queue + } - let wpos = site.tile_center_wpos(next_tile); - let wpos = wpos.as_::().with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0)); - - Some((wpos, 1.0)) + match path_in_site(start, end, site) { + PathResult::Path(p) => Some(PathData { + end, + path: pop_first(p.nodes.into()), + repoll: false, + }), + PathResult::Exhausted(p) => Some(PathData { + end, + path: pop_first(p.nodes.into()), + repoll: true, + }), + PathResult::None(_) | PathResult::Pending => None, + } }, _ => { // No brain T_T @@ -128,21 +203,213 @@ fn path_town( } } +fn path_towns( + start: SiteId, + end: SiteId, + sites: &Sites, + world: &World, +) -> Option<(PathData<(Id, bool), SiteId>, usize)> { + match path_between_sites(start, end, sites, world) { + PathResult::Exhausted(p) => Some(( + PathData { + end, + path: p.nodes.into(), + repoll: true, + }, + 0, + )), + PathResult::Path(p) => Some(( + PathData { + end, + path: p.nodes.into(), + repoll: false, + }, + 0, + )), + PathResult::Pending | PathResult::None(_) => None, + } +} + impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); + let mut dynamic_rng = rand::thread_rng(); for npc in data.npcs.values_mut() { - if let Some(home_id) = npc - .home - .and_then(|site_id| data.sites.get(site_id)?.world_site) - { + if let Some(home_id) = npc.home { if let Some((target, _)) = npc.target { - if target.xy().distance_squared(npc.wpos.xy()) < 1.0 { + // Walk to the current target + if target.xy().distance_squared(npc.wpos.xy()) < 4.0 { npc.target = None; } } else { - npc.target = path_town(npc.wpos, home_id, ctx.index, ctx.event.time.0, npc.seed, ctx.world); + if let Some((ref mut path, site)) = npc.pathing.intrasite_path { + // If the npc walking in a site and want to reroll (because the path was + // exhausted.) to try to find a complete path. + if path.repoll { + npc.pathing.intrasite_path = + path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) + .map(|path| (path, site)); + } + } + if let Some((ref mut path, site)) = npc.pathing.intrasite_path { + if let Some(next_tile) = path.path.pop_front() { + match &ctx.index.sites.get(site).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + // Set the target to the next node in the path. + let wpos = site.tile_center_wpos(next_tile); + let wpos = wpos.as_::().with_z( + ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), + ); + + npc.target = Some((wpos, 1.0)); + }, + _ => {}, + } + } else { + // If the path is empty, we're done. + npc.pathing.intrasite_path = None; + } + } else if let Some((path, progress)) = { + // Check if we are done with this part of the inter site path. + if let Some((path, progress)) = &mut npc.pathing.intersite_path { + if let Some((track_id, _)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + if *progress >= track.path().len() { + if path.repoll { + // Repoll if last path wasn't complete. + npc.pathing.intersite_path = path_towns( + npc.current_site.unwrap(), + path.end, + &data.sites, + ctx.world, + ); + } else { + // Otherwise just take the next in the calculated path. + path.path.pop_front(); + *progress = 0; + } + } + } + } + &mut npc.pathing.intersite_path + } { + if let Some((track_id, reversed)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + let get_progress = |progress: usize| { + if *reversed { + track.path().len().wrapping_sub(progress + 1) + } else { + progress + } + }; + + let transform_path_pos = |chunk_pos| { + let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); + if let Some(pathdata) = + ctx.world.sim().get_nearest_path(chunk_wpos) + { + pathdata.1.map(|e| e as i32) + } else { + chunk_wpos + } + }; + + // Loop through and skip nodes that are inside a site, and use intra + // site path finding there instead. + let walk_path = loop { + if let Some(chunk_pos) = + track.path().nodes.get(get_progress(*progress)) + { + if let Some((wpos, site_id, site)) = + ctx.world.sim().get(*chunk_pos).and_then(|chunk| { + let site_id = *chunk.sites.first()?; + let wpos = transform_path_pos(*chunk_pos); + match &ctx.index.sites.get(site_id).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) + if !site.wpos_tile(wpos).is_empty() => + { + Some((wpos, site_id, site)) + }, + _ => None, + } + }) + { + if !site.wpos_tile(wpos).is_empty() { + *progress += 1; + } else { + let end = site.wpos_tile_pos(wpos); + npc.pathing.intrasite_path = + path_town(npc.wpos, site_id, ctx.index, |_| { + Some(end) + }) + .map(|path| (path, site_id)); + break false; + } + } else { + break true; + } + } else { + break false; + } + }; + + if walk_path { + // Find the next wpos on the path. + // NOTE: Consider not having this big gap between current + // position and next. For better path finding. Maybe that would + // mean having a float for progress. + let wpos = transform_path_pos( + track.path().nodes[get_progress(*progress)], + ); + let wpos = wpos.as_::().with_z( + ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), + ); + npc.target = Some((wpos, 1.0)); + *progress += 1; + } + } else { + npc.pathing.intersite_path = None; + } + } else { + if matches!(npc.profession, Some(Profession::Adventurer(_))) { + // If the npc is home, choose a random site to go to, otherwise go + // home. + if let Some(start) = npc.current_site { + let end = if home_id == start { + data.sites + .keys() + .filter(|site| *site != home_id) + .choose(&mut dynamic_rng) + .unwrap_or(home_id) + } else { + home_id + }; + npc.pathing.intersite_path = + path_towns(start, end, &data.sites, ctx.world); + } + } else { + // Choose a random plaza in the npcs home site (which should be the + // current here) to go to. + if let Some(home_id) = + data.sites.get(home_id).and_then(|site| site.world_site) + { + npc.pathing.intrasite_path = + path_town(npc.wpos, home_id, ctx.index, |site| { + Some( + site.plots + [site.plazas().choose(&mut dynamic_rng)?] + .root_tile(), + ) + }) + .map(|path| (path, home_id)); + } + } + } } } else { // TODO: Don't make homeless people walk around in circles diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 44a7313d5a..000fa14cb7 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,3 +1,4 @@ +use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use tracing::info; use vek::*; use crate::{ @@ -12,8 +13,8 @@ impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { - for npc in ctx.state - .data_mut() + let data = &mut *ctx.state.data_mut(); + for npc in data .npcs .values_mut() .filter(|npc| matches!(npc.mode, NpcMode::Simulated)) @@ -35,6 +36,9 @@ impl Rule for SimulateNpcs { npc.wpos.z = ctx.world.sim() .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); + npc.current_site = ctx.world.sim().get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()).and_then(|chunk| { + data.sites.world_site_map.get(chunk.sites.first()?).copied() + }); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 7d94018efd..30d28975cf 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -28,7 +28,8 @@ fn humanoid_config(profession: &Profession) -> &'static str { 0 => "common.entity.world.traveler0", 1 => "common.entity.world.traveler1", 2 => "common.entity.world.traveler2", - _ => "common.entity.world.traveler3", + 3 => "common.entity.world.traveler3", + _ => panic!("Not a valid adventurer rank"), }, Profession::Blacksmith => "common.entity.village.blacksmith", Profession::Chef => "common.entity.village.chef", diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 6aaef9b714..eeb7fe5b09 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -628,13 +628,12 @@ impl Civs { } } - /// Return the direct track between two places - pub fn track_between(&self, a: Id, b: Id) -> Option> { + /// Return the direct track between two places, bool if the track should be reversed or not + pub fn track_between(&self, a: Id, b: Id) -> Option<(Id, bool)> { self.track_map .get(&a) - .and_then(|dests| dests.get(&b)) - .or_else(|| self.track_map.get(&b).and_then(|dests| dests.get(&a))) - .copied() + .and_then(|dests| Some((*dests.get(&b)?, false))) + .or_else(|| self.track_map.get(&b).and_then(|dests| Some((*dests.get(&a)?, true)))) } /// Return an iterator over a site's neighbors @@ -665,7 +664,7 @@ impl Civs { }; let neighbors = |p: &Id| self.neighbors(*p); let transition = - |a: &Id, b: &Id| self.tracks.get(self.track_between(*a, *b).unwrap()).cost; + |a: &Id, b: &Id| self.tracks.get(self.track_between(*a, *b).unwrap().0).cost; let satisfied = |p: &Id| *p == b; // We use this hasher (FxHasher64) because // (1) we don't care about DDOS attacks (ruling out SipHash); @@ -1453,7 +1452,7 @@ pub struct Track { /// Cost of using this track relative to other paths. This cost is an /// arbitrary unit and doesn't make sense unless compared to other track /// costs. - cost: f32, + pub cost: f32, path: Path>, } From c026b4d20a620e8b90b79093993e945d728c0036 Mon Sep 17 00:00:00 2001 From: IsseW Date: Tue, 16 Aug 2022 12:09:23 +0200 Subject: [PATCH 032/139] travelers say where they're going --- common/src/rtsim.rs | 3 + rtsim/src/rule/npc_ai.rs | 6 +- rtsim/src/rule/simulate_npcs.rs | 3 - server/src/rtsim2/tick.rs | 80 ++++++---- server/src/sys/agent.rs | 4 +- .../sys/agent/behavior_tree/interaction.rs | 140 +++++++++--------- server/src/sys/agent/data.rs | 5 +- 7 files changed, 134 insertions(+), 107 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 4202c36bcd..8064459c40 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -60,6 +60,7 @@ pub struct RtSimController { /// toward the given location, accounting for obstacles and other /// high-priority situations like being attacked. pub travel_to: Option>, + pub heading_to: Option, /// Proportion of full speed to move pub speed_factor: f32, /// Events @@ -70,6 +71,7 @@ impl Default for RtSimController { fn default() -> Self { Self { travel_to: None, + heading_to: None, speed_factor: 1.0, events: Vec::new(), } @@ -80,6 +82,7 @@ impl RtSimController { pub fn with_destination(pos: Vec3) -> Self { Self { travel_to: Some(pos), + heading_to: None, speed_factor: 0.5, events: Vec::new(), } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 6eabd789fc..9f43a7b265 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -10,7 +10,7 @@ use common::{ path::Path, rtsim::{Profession, SiteId}, store::Id, - terrain::TerrainChunkSize, + terrain::TerrainChunkSize, vol::RectVolSize, }; use fxhash::FxHasher64; use itertools::Itertools; @@ -236,6 +236,10 @@ impl Rule for NpcAi { let data = &mut *ctx.state.data_mut(); let mut dynamic_rng = rand::thread_rng(); for npc in data.npcs.values_mut() { + npc.current_site = ctx.world.sim().get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()).and_then(|chunk| { + data.sites.world_site_map.get(chunk.sites.first()?).copied() + }); + if let Some(home_id) = npc.home { if let Some((target, _)) = npc.target { // Walk to the current target diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 000fa14cb7..e1fb91f856 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -36,9 +36,6 @@ impl Rule for SimulateNpcs { npc.wpos.z = ctx.world.sim() .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); - npc.current_site = ctx.world.sim().get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()).and_then(|chunk| { - data.sites.world_site_map.get(chunk.sites.first()?).copied() - }); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 30d28975cf..0c88b14836 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -10,32 +10,34 @@ use common::{ rtsim::{RtSimController, RtSimEntity}, slowjob::SlowJobPool, trade::{Good, SiteInformation}, - LoadoutBuilder, - SkillSetBuilder, + LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; -use rtsim2::data::{npc::{NpcMode, Profession}, Npc, Sites}; +use rtsim2::data::{ + npc::{NpcMode, Profession}, + Npc, Sites, +}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; use world::site::settlement::trader_loadout; fn humanoid_config(profession: &Profession) -> &'static str { match profession { - Profession::Farmer | Profession::Hunter => "common.entity.village.villager", - Profession::Merchant => "common.entity.village.merchant", - Profession::Guard => "common.entity.village.guard", - Profession::Adventurer(rank) => match rank { - 0 => "common.entity.world.traveler0", - 1 => "common.entity.world.traveler1", - 2 => "common.entity.world.traveler2", - 3 => "common.entity.world.traveler3", - _ => panic!("Not a valid adventurer rank"), - }, - Profession::Blacksmith => "common.entity.village.blacksmith", - Profession::Chef => "common.entity.village.chef", - Profession::Alchemist => "common.entity.village.alchemist", - Profession::Pirate => "common.entity.spot.pirate", - Profession::Cultist => "common.entity.dungeon.tier-5.cultist", + Profession::Farmer | Profession::Hunter => "common.entity.village.villager", + Profession::Merchant => "common.entity.village.merchant", + Profession::Guard => "common.entity.village.guard", + Profession::Adventurer(rank) => match rank { + 0 => "common.entity.world.traveler0", + 1 => "common.entity.world.traveler1", + 2 => "common.entity.world.traveler2", + 3 => "common.entity.world.traveler3", + _ => panic!("Not a valid adventurer rank"), + }, + Profession::Blacksmith => "common.entity.village.blacksmith", + Profession::Chef => "common.entity.village.chef", + Profession::Alchemist => "common.entity.village.alchemist", + Profession::Pirate => "common.entity.spot.pirate", + Profession::Cultist => "common.entity.dungeon.tier-5.cultist", } } @@ -68,10 +70,14 @@ fn blacksmith_loadout( loadout_builder: LoadoutBuilder, economy: Option<&SiteInformation>, ) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Tools | Good::Armor)) + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Tools | Good::Armor) + }) } -fn profession_extra_loadout(profession: Option<&Profession>) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { +fn profession_extra_loadout( + profession: Option<&Profession>, +) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { match profession { Some(Profession::Merchant) => merchant_loadout, Some(Profession::Farmer) => farmer_loadout, @@ -83,7 +89,9 @@ fn profession_extra_loadout(profession: Option<&Profession>) -> fn(LoadoutBuilde fn profession_agent_mark(profession: Option<&Profession>) -> Option { match profession { - Some(Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith) => Some(comp::agent::Mark::Merchant), + Some( + Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith, + ) => Some(comp::agent::Mark::Merchant), Some(Profession::Guard) => Some(comp::agent::Mark::Guard), _ => None, } @@ -94,16 +102,15 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo let pos = comp::Pos(npc.wpos); if let Some(ref profession) = npc.profession { + let economy = npc.home.and_then(|home| { + let site = sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }); - let economy = npc.home - .and_then(|home| { - let site = sites.get(home)?.world_site?; - index.sites.get(site).trade_information(site.id()) - }); - let config_asset = humanoid_config(profession); - let entity_config = EntityConfig::from_asset_expect_owned(config_asset).with_body(BodyBuilder::Exact(body)); + let entity_config = + EntityConfig::from_asset_expect_owned(config_asset).with_body(BodyBuilder::Exact(body)); let mut rng = npc.rng(3); EntityInfo::at(pos.0) .with_entity_config(entity_config, Some(config_asset), &mut rng) @@ -160,7 +167,9 @@ impl<'a> System<'a> for Sys { let rtsim = &mut *rtsim; rtsim.state.data_mut().time_of_day = *time_of_day; - rtsim.state.tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); + rtsim + .state + .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); if rtsim .last_saved @@ -226,8 +235,7 @@ impl<'a> System<'a> for Sys { for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() { - data - .npcs + data.npcs .get_mut(rtsim_entity.0) .filter(|npc| matches!(npc.mode, NpcMode::Loaded)) .map(|npc| { @@ -238,6 +246,16 @@ impl<'a> System<'a> for Sys { if let Some(agent) = agent { agent.rtsim_controller.travel_to = npc.target.map(|(wpos, _)| wpos); agent.rtsim_controller.speed_factor = npc.target.map_or(1.0, |(_, sf)| sf); + agent.rtsim_controller.heading_to = + npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { + Some( + index + .sites + .get(data.sites.get(path.end)?.world_site?) + .name() + .to_string(), + ) + }); } }); } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index adbe5b4bc6..3714e564a8 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -71,6 +71,7 @@ impl<'a> System<'a> for Sys { &mut controllers, read_data.light_emitter.maybe(), read_data.groups.maybe(), + read_data.rtsim_entities.maybe(), !&read_data.is_mounts, ) .par_join() @@ -93,6 +94,7 @@ impl<'a> System<'a> for Sys { controller, light_emitter, group, + rtsim_entity, _, )| { let mut event_emitter = event_bus.emitter(); @@ -180,6 +182,7 @@ impl<'a> System<'a> for Sys { // Package all this agent's data into a convenient struct let data = AgentData { entity: &entity, + rtsim_entity, uid, pos, vel, @@ -206,7 +209,6 @@ impl<'a> System<'a> for Sys { msm: &read_data.msm, poise: read_data.poises.get(entity), stance: read_data.stances.get(entity), - rtsim_entity: read_data.rtsim_entities.get(entity), }; /////////////////////////////////////////////////////////// diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 29d2175f7e..86acde2c6d 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -107,88 +107,90 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { match subject { Subject::Regular => { - /* - if let Some(rtsim_entity) = &bdata.rtsim_entity { - if matches!(rtsim_entity.kind, RtSimEntityKind::Prisoner) { - agent_data.chat_npc("npc-speech-prisoner", event_emitter); - } else if let Some((_travel_to, destination_name) = &agent.rtsim_controller.travel_to { - let personality = &rtsim_entity.brain.personality; - let standard_response_msg = || -> String { + if let Some(destination_name) = &agent.rtsim_controller.heading_to { + let msg = format!( + "I'm heading to {}! Want to come along?", + destination_name + ); + agent_data.chat_npc(msg, event_emitter); + } + /*if let ( + Some((_travel_to, destination_name)), + Some(rtsim_entity), + ) = (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity) + { + let personality = &rtsim_entity.brain.personality; + let standard_response_msg = || -> String { + if personality.will_ambush { + format!( + "I'm heading to {}! Want to come along? We'll make \ + great travel buddies, hehe.", + destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Extroverted) + { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Hrm.".to_string() + } else { + "Hello!".to_string() + } + }; + let msg = if let Some(tgt_stats) = read_data.stats.get(target) { + agent.rtsim_controller.events.push(RtSimEvent::AddMemory( + Memory { + item: MemoryItem::CharacterInteraction { + name: tgt_stats.name.clone(), + }, + time_to_forget: read_data.time.0 + 600.0, + }, + )); + if rtsim_entity.brain.remembers_character(&tgt_stats.name) { if personality.will_ambush { - format!( - "I'm heading to {}! Want to come along? We'll make \ - great travel buddies, hehe.", - destination_name - ) + "Just follow me a bit more, hehe.".to_string() } else if personality .personality_traits .contains(PersonalityTrait::Extroverted) { - format!( - "I'm heading to {}! Want to come along?", - destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Hrm.".to_string() - } else { - "Hello!".to_string() - } - }; - let msg = if let Some(tgt_stats) = read_data.stats.get(target) { - agent.rtsim_controller.events.push(RtSimEvent::AddMemory( - Memory { - item: MemoryItem::CharacterInteraction { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 + 600.0, - }, - )); - if rtsim_entity.brain.remembers_character(&tgt_stats.name) { - if personality.will_ambush { - format!( - "I'm heading to {}! Want to come along? We'll \ - make great travel buddies, hehe.", - destination_name - ) - } else if personality + if personality .personality_traits .contains(PersonalityTrait::Extroverted) { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "Greetings fair {}! It has been far \ - too long since last I saw you. I'm \ - going to {} right now.", - &tgt_stats.name, destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() - } else { - format!( - "Hi again {}! Unfortunately I'm in a \ - hurry right now. See you!", - &tgt_stats.name - ) - } + format!( + "Greetings fair {}! It has been far \ + too long since last I saw you. I'm \ + going to {} right now.", + &tgt_stats.name, destination_name + ) + } else if personality + .personality_traits + .contains(PersonalityTrait::Disagreeable) + { + "Oh. It's you again.".to_string() } else { - standard_response_msg() + format!( + "Hi again {}! Unfortunately I'm in a \ + hurry right now. See you!", + &tgt_stats.name + ) } } else { standard_response_msg() - }; - agent_data.chat_npc(msg, event_emitter); - } + } + } else { + standard_response_msg() + }; + agent_data.chat_npc(msg, event_emitter); } else*/ - if agent.behavior.can_trade(agent_data.alignment.copied(), by) { + else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { if !agent.behavior.is(BehaviorState::TRADING) { controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc( diff --git a/server/src/sys/agent/data.rs b/server/src/sys/agent/data.rs index e443e67cff..2d1e816128 100644 --- a/server/src/sys/agent/data.rs +++ b/server/src/sys/agent/data.rs @@ -9,9 +9,8 @@ use common::{ mounting::Mount, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, - // rtsim::RtSimEntity, terrain::TerrainGrid, - uid::{Uid, UidAllocator}, + uid::{Uid, UidAllocator}, rtsim::RtSimEntity, }; use specs::{ shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, @@ -21,6 +20,7 @@ use std::sync::Arc; pub struct AgentData<'a> { pub entity: &'a EcsEntity, + pub rtsim_entity: Option<&'a RtSimEntity>, //pub rtsim_entity: Option<&'a RtSimData>, pub uid: &'a Uid, pub pos: &'a Pos, @@ -153,6 +153,7 @@ pub struct ReadData<'a> { pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, + pub rtsim_entity: ReadStorage<'a, RtSimEntity>, //pub rtsim_entities: ReadStorage<'a, RtSimEntity>, pub buffs: ReadStorage<'a, Buffs>, pub combos: ReadStorage<'a, Combo>, From 539c482cff459ce6e2c51d57a1e40a26d6a44d42 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 16 Aug 2022 12:49:50 +0100 Subject: [PATCH 033/139] Capped out NPC movement at higher dts --- rtsim/src/rule/simulate_npcs.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index e1fb91f856..b761f35599 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -23,13 +23,15 @@ impl Rule for SimulateNpcs { // Move NPCs if they have a target if let Some((target, speed_factor)) = npc.target { - npc.wpos += Vec3::from( - (target.xy() - npc.wpos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero) - * body.max_speed_approx() - * speed_factor, - ) * ctx.event.dt; + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5.powi(2) { + npc.wpos += (diff + * (body.max_speed_approx() * speed_factor * ctx.event.dt / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } } // Make sure NPCs remain on the surface From 9b48faba7e3f42573af558d63bdec2a483f4efa0 Mon Sep 17 00:00:00 2001 From: IsseW Date: Tue, 16 Aug 2022 16:03:45 +0200 Subject: [PATCH 034/139] fix compilation error --- rtsim/src/rule/simulate_npcs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index b761f35599..baebf45c27 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -26,7 +26,7 @@ impl Rule for SimulateNpcs { let diff = target.xy() - npc.wpos.xy(); let dist2 = diff.magnitude_squared(); - if dist2 > 0.5.powi(2) { + if dist2 > 0.5f32.powi(2) { npc.wpos += (diff * (body.max_speed_approx() * speed_factor * ctx.event.dt / dist2.sqrt()) .min(1.0)) From 63f1ac0e311c0035c60bb4a9eefbeff33bde5ce2 Mon Sep 17 00:00:00 2001 From: IsseW Date: Tue, 23 Aug 2022 17:16:09 +0200 Subject: [PATCH 035/139] trade with alchemists --- server/src/rtsim2/tick.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 0c88b14836..20345a8667 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -75,6 +75,15 @@ fn blacksmith_loadout( }) } +fn alchemist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Potions) + }) +} + fn profession_extra_loadout( profession: Option<&Profession>, ) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { @@ -83,6 +92,7 @@ fn profession_extra_loadout( Some(Profession::Farmer) => farmer_loadout, Some(Profession::Chef) => chef_loadout, Some(Profession::Blacksmith) => blacksmith_loadout, + Some(Profession::Alchemist) => alchemist_loadout, _ => loadout_default, } } @@ -90,7 +100,7 @@ fn profession_extra_loadout( fn profession_agent_mark(profession: Option<&Profession>) -> Option { match profession { Some( - Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith, + Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith | Profession::Alchemist, ) => Some(comp::agent::Mark::Merchant), Some(Profession::Guard) => Some(comp::agent::Mark::Guard), _ => None, From 3a52cc1fa3ca7c58872a79d7e76c8420279ed204 Mon Sep 17 00:00:00 2001 From: TaylorNAlbarnaz Date: Fri, 26 Aug 2022 23:45:17 -0300 Subject: [PATCH 036/139] NPCs walk in when pathing in intrasite --- rtsim/src/data/npc.rs | 5 +++-- rtsim/src/rule/npc_ai.rs | 20 ++++++++++++++------ rtsim/src/rule/simulate_npcs.rs | 4 ++-- server/src/rtsim2/tick.rs | 4 ++-- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index aaf8924d51..36c5d2ae16 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -57,7 +57,8 @@ pub struct Npc { /// (wpos, speed_factor) #[serde(skip_serializing, skip_deserializing)] - pub target: Option<(Vec3, f32)>, + pub goto: Option<(Vec3, f32)>, + /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being /// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should /// instead be derived from the game. @@ -78,7 +79,7 @@ impl Npc { faction: None, pathing: Default::default(), current_site: None, - target: None, + goto: None, mode: NpcMode::Simulated, } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 9f43a7b265..0b6d50937a 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -241,12 +241,19 @@ impl Rule for NpcAi { }); if let Some(home_id) = npc.home { - if let Some((target, _)) = npc.target { + if let Some((target, _)) = npc.goto { // Walk to the current target if target.xy().distance_squared(npc.wpos.xy()) < 4.0 { - npc.target = None; + npc.goto = None; } } else { + // Walk slower when pathing in a site, and faster when between sites + if npc.pathing.intersite_path.is_none() { + npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 0.7)); + } else { + npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 1.0)); + } + if let Some((ref mut path, site)) = npc.pathing.intrasite_path { // If the npc walking in a site and want to reroll (because the path was // exhausted.) to try to find a complete path. @@ -256,6 +263,7 @@ impl Rule for NpcAi { .map(|path| (path, site)); } } + if let Some((ref mut path, site)) = npc.pathing.intrasite_path { if let Some(next_tile) = path.path.pop_front() { match &ctx.index.sites.get(site).kind { @@ -268,7 +276,7 @@ impl Rule for NpcAi { ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), ); - npc.target = Some((wpos, 1.0)); + npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); }, _ => {}, } @@ -373,7 +381,7 @@ impl Rule for NpcAi { let wpos = wpos.as_::().with_z( ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), ); - npc.target = Some((wpos, 1.0)); + npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); *progress += 1; } } else { @@ -417,14 +425,14 @@ impl Rule for NpcAi { } } else { // TODO: Don't make homeless people walk around in circles - npc.target = Some(( + npc.goto = Some(( npc.wpos + Vec3::new( ctx.event.time.0.sin() as f32 * 16.0, ctx.event.time.0.cos() as f32 * 16.0, 0.0, ), - 1.0, + 0.7, )); } } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index baebf45c27..34fa6e8d6d 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -21,8 +21,8 @@ impl Rule for SimulateNpcs { { let body = npc.get_body(); - // Move NPCs if they have a target - if let Some((target, speed_factor)) = npc.target { + // Move NPCs if they have a target destination + if let Some((target, speed_factor)) = npc.goto { let diff = target.xy() - npc.wpos.xy(); let dist2 = diff.magnitude_squared(); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 20345a8667..bc62d60cc5 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -254,8 +254,8 @@ impl<'a> System<'a> for Sys { // Update entity state if let Some(agent) = agent { - agent.rtsim_controller.travel_to = npc.target.map(|(wpos, _)| wpos); - agent.rtsim_controller.speed_factor = npc.target.map_or(1.0, |(_, sf)| sf); + agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos); + agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); agent.rtsim_controller.heading_to = npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { Some( From e8b489a71a79d69115780998946e41bfaf066620 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 3 Sep 2022 10:47:18 +0100 Subject: [PATCH 037/139] sync --- common/systems/src/character_behavior.rs | 4 +- common/systems/src/mount.rs | 8 +- common/systems/src/phys.rs | 8 +- rtsim/src/data/faction.rs | 20 ++--- rtsim/src/data/mod.rs | 35 +++++--- rtsim/src/data/nature.rs | 54 ++++++------ rtsim/src/data/npc.rs | 47 +++++------ rtsim/src/data/site.rs | 36 ++++---- rtsim/src/event.rs | 4 +- rtsim/src/gen/faction.rs | 7 +- rtsim/src/gen/mod.rs | 92 ++++++++++++--------- rtsim/src/gen/site.rs | 15 ++-- rtsim/src/lib.rs | 58 +++++++++---- rtsim/src/rule.rs | 8 +- rtsim/src/rule/npc_ai.rs | 13 +-- rtsim/src/rule/setup.rs | 42 ++++++---- rtsim/src/rule/simulate_npcs.rs | 16 ++-- server/src/chunk_generator.rs | 11 +-- server/src/cmd.rs | 23 ++++-- server/src/events/entity_manipulation.rs | 3 +- server/src/lib.rs | 19 ++++- server/src/rtsim2/event.rs | 2 +- server/src/rtsim2/mod.rs | 75 ++++++++++------- server/src/rtsim2/rule.rs | 2 +- server/src/rtsim2/rule/deplete_resources.rs | 27 +++--- server/src/rtsim2/tick.rs | 6 +- server/src/state_ext.rs | 2 +- server/src/sys/agent.rs | 5 +- server/src/sys/agent/data.rs | 4 +- voxygen/src/menu/main/mod.rs | 7 +- voxygen/src/scene/debug.rs | 4 +- voxygen/src/scene/figure/mod.rs | 14 ++-- voxygen/src/scene/mod.rs | 23 ++++-- world/src/civ/mod.rs | 14 +++- world/src/lib.rs | 21 ++--- world/src/site/settlement/mod.rs | 18 ++-- world/src/site2/mod.rs | 3 +- 37 files changed, 435 insertions(+), 315 deletions(-) diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index d4a8e6ca70..28c3393df6 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -9,8 +9,8 @@ use common::{ character_state::OutputEvents, inventory::item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, - Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, SkillSet, Stance, - StateUpdate, Stats, Vel, Scale, + Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, Stance, + StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, link::Is, diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index 997442f12e..25ee2ae644 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,5 @@ use common::{ - comp::{Body, Controller, InputKind, Ori, Pos, Vel, Scale}, + comp::{Body, Controller, InputKind, Ori, Pos, Scale, Vel}, link::Is, mounting::Mount, uid::UidAllocator, @@ -69,8 +69,10 @@ impl<'a> System<'a> for Sys { let vel = velocities.get(entity).copied(); if let (Some(pos), Some(ori), Some(vel)) = (pos, ori, vel) { let mounter_body = bodies.get(rider); - let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset) * scales.get(entity).map_or(1.0, |s| s.0) - + mounter_body.map_or(Vec3::zero(), Body::rider_offset) * scales.get(rider).map_or(1.0, |s| s.0); + let mounting_offset = body.map_or(Vec3::unit_z(), Body::mount_offset) + * scales.get(entity).map_or(1.0, |s| s.0) + + mounter_body.map_or(Vec3::zero(), Body::rider_offset) + * scales.get(rider).map_or(1.0, |s| s.0); let _ = positions.insert(rider, Pos(pos.0 + ori.to_quat() * mounting_offset)); let _ = orientations.insert(rider, ori); let _ = velocities.insert(rider, vel); diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 755e555872..6c738fa675 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -62,7 +62,13 @@ fn integrate_forces( // Aerodynamic/hydrodynamic forces if !rel_flow.0.is_approx_zero() { debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or()); - let impulse = dt.0 * body.aerodynamic_forces(&rel_flow, fluid_density.0, wings, scale.map_or(1.0, |s| s.0)); + let impulse = dt.0 + * body.aerodynamic_forces( + &rel_flow, + fluid_density.0, + wings, + scale.map_or(1.0, |s| s.0), + ); debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or()); if !impulse.is_approx_zero() { let new_v = vel.0 + impulse / mass.0; diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs index f07c6296e9..d27c294e00 100644 --- a/rtsim/src/data/faction.rs +++ b/rtsim/src/data/faction.rs @@ -1,14 +1,11 @@ -use hashbrown::HashMap; -use serde::{Serialize, Deserialize}; -use slotmap::HopSlotMap; -use vek::*; -use std::ops::{Deref, DerefMut}; -use common::{ - uid::Uid, - store::Id, -}; use super::Actor; pub use common::rtsim::FactionId; +use common::{store::Id, uid::Uid}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; +use slotmap::HopSlotMap; +use std::ops::{Deref, DerefMut}; +use vek::*; #[derive(Clone, Serialize, Deserialize)] pub struct Faction { @@ -21,13 +18,12 @@ pub struct Factions { } impl Factions { - pub fn create(&mut self, faction: Faction) -> FactionId { - self.factions.insert(faction) - } + pub fn create(&mut self, faction: Faction) -> FactionId { self.factions.insert(faction) } } impl Deref for Factions { type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.factions } } diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index f193e8eb85..df4c219ef0 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,23 +1,26 @@ pub mod faction; +pub mod nature; pub mod npc; pub mod site; -pub mod nature; pub use self::{ - npc::{Npc, NpcId, Npcs}, - site::{Site, SiteId, Sites}, faction::{Faction, FactionId, Factions}, nature::Nature, + npc::{Npc, NpcId, Npcs}, + site::{Site, SiteId, Sites}, }; use common::resources::TimeOfDay; -use enum_map::{EnumMap, EnumArray, enum_map}; -use serde::{Serialize, Deserialize, ser, de}; +use enum_map::{enum_map, EnumArray, EnumMap}; +use serde::{ + de::{self, Error as _}, + ser, Deserialize, Serialize, +}; use std::{ - io::{Read, Write}, - marker::PhantomData, cmp::PartialEq, fmt, + io::{Read, Write}, + marker::PhantomData, }; #[derive(Copy, Clone, Serialize, Deserialize)] @@ -54,11 +57,15 @@ fn rugged_ser_enum_map< V: From + PartialEq + Serialize, S: ser::Serializer, const DEFAULT: i16, ->(map: &EnumMap, ser: S) -> Result { - ser.collect_map(map - .iter() - .filter(|(k, v)| v != &&V::from(DEFAULT)) - .map(|(k, v)| (k, v))) +>( + map: &EnumMap, + ser: S, +) -> Result { + ser.collect_map( + map.iter() + .filter(|(k, v)| v != &&V::from(DEFAULT)) + .map(|(k, v)| (k, v)), + ) } fn rugged_de_enum_map< @@ -67,7 +74,9 @@ fn rugged_de_enum_map< V: From + Deserialize<'a>, D: de::Deserializer<'a>, const DEFAULT: i16, ->(mut de: D) -> Result, D::Error> { +>( + mut de: D, +) -> Result, D::Error> { struct Visitor(PhantomData<(K, V)>); impl<'de, K, V, const DEFAULT: i16> de::Visitor<'de> for Visitor diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index 2a7abf8b0f..2325ad6dbc 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -1,17 +1,15 @@ -use serde::{Serialize, Deserialize}; +use common::{grid::Grid, rtsim::ChunkResource}; use enum_map::EnumMap; -use common::{ - grid::Grid, - rtsim::ChunkResource, -}; -use world::World; +use serde::{Deserialize, Serialize}; use vek::*; +use world::World; -/// Represents the state of 'natural' elements of the world such as plant/animal/resource populations, weather systems, -/// etc. +/// Represents the state of 'natural' elements of the world such as +/// plant/animal/resource populations, weather systems, etc. /// -/// Where possible, this data does not define the state of natural aspects of the world, but instead defines -/// 'modifications' that sit on top of the world data generated by initial generation. +/// Where possible, this data does not define the state of natural aspects of +/// the world, but instead defines 'modifications' that sit on top of the world +/// data generated by initial generation. #[derive(Clone, Serialize, Deserialize)] pub struct Nature { chunks: Grid, @@ -20,22 +18,17 @@ pub struct Nature { impl Nature { pub fn generate(world: &World) -> Self { Self { - chunks: Grid::populate_from( - world.sim().get_size().map(|e| e as i32), - |pos| Chunk { - res: EnumMap::<_, f32>::default().map(|_, _| 1.0), - }, - ), + chunks: Grid::populate_from(world.sim().get_size().map(|e| e as i32), |pos| Chunk { + res: EnumMap::<_, f32>::default().map(|_, _| 1.0), + }), } } // TODO: Clean up this API a bit pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { - self.chunks - .get(key) - .map(|c| c.res) - .unwrap_or_default() + self.chunks.get(key).map(|c| c.res).unwrap_or_default() } + pub fn set_chunk_resources(&mut self, key: Vec2, res: EnumMap) { if let Some(chunk) = self.chunks.get_mut(key) { chunk.res = res; @@ -45,16 +38,21 @@ impl Nature { #[derive(Clone, Serialize, Deserialize)] pub struct Chunk { - /// Represent the 'naturally occurring' resource proportion that exists in this chunk. + /// Represent the 'naturally occurring' resource proportion that exists in + /// this chunk. /// - /// 0.0 => None of the resources generated by terrain generation should be present - /// 1.0 => All of the resources generated by terrain generation should be present + /// 0.0 => None of the resources generated by terrain generation should be + /// present 1.0 => All of the resources generated by terrain generation + /// should be present /// - /// It's important to understand this this number does not represent the total amount of a resource present in a - /// chunk, nor is it even proportional to the amount of the resource present. To get the total amount of the - /// resource in a chunk, one must first multiply this factor by the amount of 'natural' resources given by terrain - /// generation. This value represents only the variable 'depletion' factor of that resource, which shall change - /// over time as the world evolves and players interact with it. + /// It's important to understand this this number does not represent the + /// total amount of a resource present in a chunk, nor is it even + /// proportional to the amount of the resource present. To get the total + /// amount of the resource in a chunk, one must first multiply this + /// factor by the amount of 'natural' resources given by terrain + /// generation. This value represents only the variable 'depletion' factor + /// of that resource, which shall change over time as the world evolves + /// and players interact with it. #[serde(rename = "r")] #[serde(serialize_with = "crate::data::rugged_ser_enum_map::<_, _, _, 1>")] #[serde(deserialize_with = "crate::data::rugged_de_enum_map::<_, _, _, 1>")] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 36c5d2ae16..bd97d8d097 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,18 +1,20 @@ -use hashbrown::HashMap; -use serde::{Serialize, Deserialize}; -use slotmap::HopSlotMap; -use vek::*; -use rand::prelude::*; -use std::{ops::{Deref, DerefMut}, collections::VecDeque}; -use common::{ - uid::Uid, - store::Id, - rtsim::{SiteId, FactionId, RtSimController}, - comp, -}; -use world::{util::RandomPerm, civ::Track}; -use world::site::Site as WorldSite; pub use common::rtsim::{NpcId, Profession}; +use common::{ + comp, + rtsim::{FactionId, RtSimController, SiteId}, + store::Id, + uid::Uid, +}; +use hashbrown::HashMap; +use rand::prelude::*; +use serde::{Deserialize, Serialize}; +use slotmap::HopSlotMap; +use std::{ + collections::VecDeque, + ops::{Deref, DerefMut}, +}; +use vek::*; +use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; #[derive(Copy, Clone, Default)] pub enum NpcMode { @@ -39,7 +41,6 @@ pub struct PathingMemory { #[derive(Clone, Serialize, Deserialize)] pub struct Npc { // Persisted state - /// Represents the location of the NPC. pub seed: u32, pub wpos: Vec3, @@ -58,17 +59,18 @@ pub struct Npc { /// (wpos, speed_factor) #[serde(skip_serializing, skip_deserializing)] pub goto: Option<(Vec3, f32)>, - - /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the server, loaded corresponds to being - /// within a loaded chunk). When in loaded mode, the interactions of the NPC should not be simulated but should - /// instead be derived from the game. + + /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the + /// server, loaded corresponds to being within a loaded chunk). When in + /// loaded mode, the interactions of the NPC should not be simulated but + /// should instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] pub mode: NpcMode, } impl Npc { - const PERM_SPECIES: u32 = 0; const PERM_BODY: u32 = 1; + const PERM_SPECIES: u32 = 0; pub fn new(seed: u32, wpos: Vec3) -> Self { Self { @@ -115,13 +117,12 @@ pub struct Npcs { } impl Npcs { - pub fn create(&mut self, npc: Npc) -> NpcId { - self.npcs.insert(npc) - } + pub fn create(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } } impl Deref for Npcs { type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.npcs } } diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index 9189d4a97b..f1edf2645e 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -1,14 +1,10 @@ -use hashbrown::HashMap; -use serde::{Serialize, Deserialize}; -use slotmap::HopSlotMap; -use vek::*; -use std::ops::{Deref, DerefMut}; -use common::{ - uid::Uid, - store::Id, - rtsim::FactionId, -}; pub use common::rtsim::SiteId; +use common::{rtsim::FactionId, store::Id, uid::Uid}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; +use slotmap::HopSlotMap; +use std::ops::{Deref, DerefMut}; +use vek::*; use world::site::Site as WorldSite; #[derive(Clone, Serialize, Deserialize)] @@ -16,14 +12,19 @@ pub struct Site { pub wpos: Vec2, pub faction: Option, - /// The site generated during initial worldgen that this site corresponds to. + /// The site generated during initial worldgen that this site corresponds + /// to. /// - /// Eventually, rtsim should replace initial worldgen's site system and this will not be necessary. + /// Eventually, rtsim should replace initial worldgen's site system and this + /// will not be necessary. /// - /// When setting up rtsim state, we try to 'link' these two definitions of a site: but if initial worldgen has - /// changed, this might not be possible. We try to delete sites that no longer exist during setup, but this is an - /// inherent fallible process. If linking fails, we try to delete the site in rtsim2 in order to avoid an - /// 'orphaned' site. (TODO: create new sites for new initial worldgen sites that come into being too). + /// When setting up rtsim state, we try to 'link' these two definitions of a + /// site: but if initial worldgen has changed, this might not be + /// possible. We try to delete sites that no longer exist during setup, but + /// this is an inherent fallible process. If linking fails, we try to + /// delete the site in rtsim2 in order to avoid an 'orphaned' site. + /// (TODO: create new sites for new initial worldgen sites that come into + /// being too). #[serde(skip_serializing, skip_deserializing)] pub world_site: Option>, } @@ -38,7 +39,7 @@ impl Site { #[derive(Clone, Serialize, Deserialize)] pub struct Sites { pub sites: HopSlotMap, - + #[serde(skip_serializing, skip_deserializing)] pub world_site_map: HashMap, SiteId>, } @@ -56,6 +57,7 @@ impl Sites { impl Deref for Sites { type Target = HopSlotMap; + fn deref(&self) -> &Self::Target { &self.sites } } diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index 72932e9668..1ae5cbb6bb 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -1,6 +1,6 @@ +use super::{RtState, Rule}; use common::resources::{Time, TimeOfDay}; -use world::{World, IndexRef}; -use super::{Rule, RtState}; +use world::{IndexRef, World}; pub trait Event: Clone + 'static {} diff --git a/rtsim/src/gen/faction.rs b/rtsim/src/gen/faction.rs index 0027042038..ed09d71594 100644 --- a/rtsim/src/gen/faction.rs +++ b/rtsim/src/gen/faction.rs @@ -1,10 +1,7 @@ use crate::data::Faction; -use vek::*; use rand::prelude::*; -use world::{ - World, - IndexRef, -}; +use vek::*; +use world::{IndexRef, World}; impl Faction { pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index e452294ac4..ee0ffe8b49 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -1,40 +1,41 @@ -pub mod site; pub mod faction; +pub mod site; use crate::data::{ - npc::{Npcs, Npc, Profession}, - site::{Sites, Site}, - faction::{Factions, Faction}, - Data, - Nature, + faction::{Faction, Factions}, + npc::{Npc, Npcs, Profession}, + site::{Site, Sites}, + Data, Nature, +}; +use common::{ + resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, vol::RectVolSize, }; use hashbrown::HashMap; use rand::prelude::*; use tracing::info; use vek::*; -use common::{ - rtsim::WorldSettings, - resources::TimeOfDay, - terrain::TerrainChunkSize, - vol::RectVolSize, -}; -use world::{ - site::SiteKind, - IndexRef, - World, -}; +use world::{site::SiteKind, IndexRef, World}; impl Data { pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self { let mut seed = [0; 32]; - seed.iter_mut().zip(&mut index.seed.to_le_bytes()).for_each(|(dst, src)| *dst = *src); + seed.iter_mut() + .zip(&mut index.seed.to_le_bytes()) + .for_each(|(dst, src)| *dst = *src); let mut rng = SmallRng::from_seed(seed); let mut this = Self { nature: Nature::generate(world), - npcs: Npcs { npcs: Default::default() }, - sites: Sites { sites: Default::default(), world_site_map: Default::default() }, - factions: Factions { factions: Default::default() }, + npcs: Npcs { + npcs: Default::default(), + }, + sites: Sites { + sites: Default::default(), + world_site_map: Default::default(), + }, + factions: Factions { + factions: Default::default(), + }, time_of_day: TimeOfDay(settings.start_time), }; @@ -45,44 +46,53 @@ impl Data { let wpos = world .sim() .get_size() - .map2(TerrainChunkSize::RECT_SIZE, |e, sz| rng.gen_range(0..(e * sz) as i32)); + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + rng.gen_range(0..(e * sz) as i32) + }); (wpos, this.factions.create(faction)) }) .collect::>(); info!("Generated {} rtsim factions.", this.factions.len()); // Register sites with rtsim - for (world_site_id, _) in index - .sites - .iter() - { + for (world_site_id, _) in index.sites.iter() { let site = Site::generate(world_site_id, world, index, &initial_factions); this.sites.create(site); } - info!("Registering {} rtsim sites from world sites.", this.sites.len()); + info!( + "Registering {} rtsim sites from world sites.", + this.sites.len() + ); // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() { let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); - wpos2d.map(|e| e as f32 + 0.5) + wpos2d + .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; for _ in 0..20 { - - this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)) - .with_faction(site.faction) - .with_home(site_id).with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - 11..=15 => Profession::Guard, - _ => Profession::Adventurer(rng.gen_range(0..=3)), - })); + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=10 => Profession::Farmer, + 11..=15 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), + }), + ); } - this.npcs.create(Npc::new(rng.gen(), rand_wpos(&mut rng)).with_home(site_id).with_profession(Profession::Merchant)); + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_home(site_id) + .with_profession(Profession::Merchant), + ); } info!("Generated {} rtsim NPCs.", this.npcs.len()); diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index dad713b48c..30d079362f 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,14 +1,15 @@ -use crate::data::{Site, FactionId}; +use crate::data::{FactionId, Site}; use common::store::Id; use vek::*; -use world::{ - site::Site as WorldSite, - World, - IndexRef, -}; +use world::{site::Site as WorldSite, IndexRef, World}; impl Site { - pub fn generate(world_site: Id, world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)]) -> Self { + pub fn generate( + world_site: Id, + world: &World, + index: IndexRef, + nearby_factions: &[(Vec2, FactionId)], + ) -> Self { let wpos = index.sites.get(world_site).get_origin(); Self { diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 9152bc9d11..0a15a841bc 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -10,15 +10,15 @@ pub use self::{ event::{Event, EventCtx, OnTick}, rule::{Rule, RuleError}, }; -use common::resources::{Time, TimeOfDay}; -use world::{World, IndexRef}; use anymap2::SendSyncAnyMap; -use tracing::{info, error}; use atomic_refcell::AtomicRefCell; +use common::resources::{Time, TimeOfDay}; use std::{ any::type_name, ops::{Deref, DerefMut}, }; +use tracing::{error, info}; +use world::{IndexRef, World}; pub struct RtState { resources: SendSyncAnyMap, @@ -36,7 +36,7 @@ impl RtState { rules: SendSyncAnyMap::new(), event_handlers: SendSyncAnyMap::new(), } - .with_resource(data); + .with_resource(data); this.start_default_rules(); @@ -58,7 +58,9 @@ impl RtState { pub fn start_rule(&mut self) { info!("Initiating '{}' rule...", type_name::()); match R::start(self) { - Ok(rule) => { self.rules.insert::>(AtomicRefCell::new(rule)); }, + Ok(rule) => { + self.rules.insert::>(AtomicRefCell::new(rule)); + }, Err(e) => error!("Error when initiating '{}' rule: {}", type_name::(), e), } } @@ -66,11 +68,19 @@ impl RtState { fn rule_mut(&self) -> impl DerefMut + '_ { self.rules .get::>() - .unwrap_or_else(|| panic!("Tried to access rule '{}' but it does not exist", type_name::())) + .unwrap_or_else(|| { + panic!( + "Tried to access rule '{}' but it does not exist", + type_name::() + ) + }) .borrow_mut() } - pub fn bind(&mut self, mut f: impl FnMut(EventCtx) + Send + Sync + 'static) { + pub fn bind( + &mut self, + mut f: impl FnMut(EventCtx) + Send + Sync + 'static, + ) { let f = AtomicRefCell::new(f); self.event_handlers .entry::>() @@ -87,33 +97,53 @@ impl RtState { } pub fn data(&self) -> impl Deref + '_ { self.resource() } + pub fn data_mut(&self) -> impl DerefMut + '_ { self.resource_mut() } pub fn resource(&self) -> impl Deref + '_ { self.resources .get::>() - .unwrap_or_else(|| panic!("Tried to access resource '{}' but it does not exist", type_name::())) + .unwrap_or_else(|| { + panic!( + "Tried to access resource '{}' but it does not exist", + type_name::() + ) + }) .borrow() } pub fn resource_mut(&self) -> impl DerefMut + '_ { self.resources .get::>() - .unwrap_or_else(|| panic!("Tried to access resource '{}' but it does not exist", type_name::())) + .unwrap_or_else(|| { + panic!( + "Tried to access resource '{}' but it does not exist", + type_name::() + ) + }) .borrow_mut() } pub fn emit(&mut self, e: E, world: &World, index: IndexRef) { self.event_handlers .get::>() - .map(|handlers| handlers - .iter() - .for_each(|f| f(self, world, index, &e))); + .map(|handlers| handlers.iter().for_each(|f| f(self, world, index, &e))); } - pub fn tick(&mut self, world: &World, index: IndexRef, time_of_day: TimeOfDay, time: Time, dt: f32) { + pub fn tick( + &mut self, + world: &World, + index: IndexRef, + time_of_day: TimeOfDay, + time: Time, + dt: f32, + ) { self.data_mut().time_of_day = time_of_day; - let event = OnTick { time_of_day, time, dt }; + let event = OnTick { + time_of_day, + time, + dt, + }; self.emit(event, world, index); } } diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs index 611872ab75..5444d0a2c7 100644 --- a/rtsim/src/rule.rs +++ b/rtsim/src/rule.rs @@ -1,9 +1,9 @@ +pub mod npc_ai; pub mod setup; pub mod simulate_npcs; -pub mod npc_ai; -use std::fmt; use super::RtState; +use std::fmt; #[derive(Debug)] pub enum RuleError { @@ -13,7 +13,9 @@ pub enum RuleError { impl fmt::Display for RuleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Self::NoSuchRule(r) => write!(f, "tried to fetch rule state '{}' but it does not exist", r), + Self::NoSuchRule(r) => { + write!(f, "tried to fetch rule state '{}' but it does not exist", r) + }, } } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 0b6d50937a..8f5a02fbf9 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -10,7 +10,8 @@ use common::{ path::Path, rtsim::{Profession, SiteId}, store::Id, - terrain::TerrainChunkSize, vol::RectVolSize, + terrain::TerrainChunkSize, + vol::RectVolSize, }; use fxhash::FxHasher64; use itertools::Itertools; @@ -236,10 +237,12 @@ impl Rule for NpcAi { let data = &mut *ctx.state.data_mut(); let mut dynamic_rng = rand::thread_rng(); for npc in data.npcs.values_mut() { - npc.current_site = ctx.world.sim().get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()).and_then(|chunk| { - data.sites.world_site_map.get(chunk.sites.first()?).copied() - }); - + npc.current_site = ctx + .world + .sim() + .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) + .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); + if let Some(home_id) = npc.home { if let Some((target, _)) = npc.goto { // Walk to the current target diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index cbcda44025..eebec822e5 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -1,12 +1,9 @@ +use crate::{data::Site, event::OnSetup, RtState, Rule, RuleError}; use tracing::warn; -use crate::{ - data::Site, - event::OnSetup, - RtState, Rule, RuleError, -}; -/// This rule runs at rtsim startup and broadly acts to perform some primitive migration/sanitisation in order to -/// ensure that the state of rtsim is mostly sensible. +/// This rule runs at rtsim startup and broadly acts to perform some primitive +/// migration/sanitisation in order to ensure that the state of rtsim is mostly +/// sensible. pub struct Setup; impl Rule for Setup { @@ -15,14 +12,20 @@ impl Rule for Setup { let data = &mut *ctx.state.data_mut(); // Delete rtsim sites that don't correspond to a world site data.sites.retain(|site_id, site| { - if let Some((world_site_id, _)) = ctx.index.sites + if let Some((world_site_id, _)) = ctx + .index + .sites .iter() .find(|(_, world_site)| world_site.get_origin() == site.wpos) { site.world_site = Some(world_site_id); true } else { - warn!("{:?} is no longer valid because the site it was derived from no longer exists. It will now be deleted.", site_id); + warn!( + "{:?} is no longer valid because the site it was derived from no longer \ + exists. It will now be deleted.", + site_id + ); false } }); @@ -32,15 +35,20 @@ impl Rule for Setup { npc.home = npc.home.filter(|home| data.sites.contains_key(*home)); } - // Generate rtsim sites for world sites that don't have a corresponding rtsim site yet + // Generate rtsim sites for world sites that don't have a corresponding rtsim + // site yet for (world_site_id, _) in ctx.index.sites.iter() { - if !data.sites - .values() - .any(|site| site.world_site.expect("Rtsim site not assigned to world site") == world_site_id) - { - warn!("{:?} is new and does not have a corresponding rtsim site. One will now be generated afresh.", world_site_id); - data - .sites + if !data.sites.values().any(|site| { + site.world_site + .expect("Rtsim site not assigned to world site") + == world_site_id + }) { + warn!( + "{:?} is new and does not have a corresponding rtsim site. One will now \ + be generated afresh.", + world_site_id + ); + data.sites .create(Site::generate(world_site_id, ctx.world, ctx.index, &[])); } } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 34fa6e8d6d..b2761cc404 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,17 +1,12 @@ +use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use tracing::info; use vek::*; -use crate::{ - data::npc::NpcMode, - event::OnTick, - RtState, Rule, RuleError, -}; pub struct SimulateNpcs; impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); for npc in data @@ -28,14 +23,17 @@ impl Rule for SimulateNpcs { if dist2 > 0.5f32.powi(2) { npc.wpos += (diff - * (body.max_speed_approx() * speed_factor * ctx.event.dt / dist2.sqrt()) - .min(1.0)) + * (body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) .with_z(0.0); } } // Make sure NPCs remain on the surface - npc.wpos.z = ctx.world.sim() + npc.wpos.z = ctx + .world + .sim() .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); } diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index f5b34e7f4a..c5ca62be7d 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,9 +1,6 @@ -use crate::{ - metrics::ChunkGenMetrics, - rtsim2::RtSim, -}; #[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; +use crate::{metrics::ChunkGenMetrics, rtsim2::RtSim}; use common::{ calendar::Calendar, generation::ChunkSupplement, resources::TimeOfDay, slowjob::SlowJobPool, terrain::TerrainChunk, @@ -47,10 +44,8 @@ impl ChunkGenerator { key: Vec2, slowjob_pool: &SlowJobPool, world: Arc, - #[cfg(feature = "worldgen")] - rtsim: &RtSim, - #[cfg(not(feature = "worldgen"))] - rtsim: &(), + #[cfg(feature = "worldgen")] rtsim: &RtSim, + #[cfg(not(feature = "worldgen"))] rtsim: &(), index: IndexOwned, time: (TimeOfDay, Calendar), ) { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 8132fae612..3edf3fcc5c 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1192,9 +1192,19 @@ fn handle_tp_npc( ) -> CmdResult<()> { use crate::rtsim2::RtSim; let pos = if let Some(id) = parse_cmd_args!(args, u32) { - // TODO: Take some other identifier than an integer to this command. - server.state.ecs().read_resource::().state().data().npcs.values().nth(id as usize).ok_or(action.help_string())?.wpos - } else { + // TODO: Take some other identifier than an integer to this command. + server + .state + .ecs() + .read_resource::() + .state() + .data() + .npcs + .values() + .nth(id as usize) + .ok_or(action.help_string())? + .wpos + } else { return Err(action.help_string()); }; position_mut(server, target, "target", |target_pos| { @@ -3873,12 +3883,7 @@ fn handle_scale( .write_storage::() .insert(target, comp::Scale(scale)); if reset_mass.unwrap_or(true) { - if let Some(body) = server - .state - .ecs() - .read_storage::() - .get(target) - { + if let Some(body) = server.state.ecs().read_storage::().get(target) { let _ = server .state .ecs() diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 3307d80ed8..0c9944731d 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -7,9 +7,8 @@ use crate::{ skillset::SkillGroupKind, BuffKind, BuffSource, PhysicsState, }, - // rtsim::RtSim, - sys::terrain::SAFE_ZONE_RADIUS, rtsim2, + sys::terrain::SAFE_ZONE_RADIUS, Server, SpawnPoint, StateExt, }; use authc::Uuid; diff --git a/server/src/lib.rs b/server/src/lib.rs index 94c8dc7efb..7deaa9e7c4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -83,7 +83,7 @@ use common::{ rtsim::RtSimEntity, shared_server_config::ServerConstants, slowjob::SlowJobPool, - terrain::{TerrainChunk, TerrainChunkSize, Block}, + terrain::{Block, TerrainChunk, TerrainChunkSize}, vol::RectRasterableVol, }; use common_ecs::run_now; @@ -565,7 +565,12 @@ impl Server { // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { - match rtsim2::RtSim::new(&settings.world, index.as_index_ref(), &world, data_dir.to_owned()) { + match rtsim2::RtSim::new( + &settings.world, + index.as_index_ref(), + &world, + data_dir.to_owned(), + ) { Ok(rtsim) => { state.ecs_mut().insert(rtsim.state().data().time_of_day); state.ecs_mut().insert(rtsim); @@ -706,9 +711,15 @@ impl Server { let before_state_tick = Instant::now(); - fn on_block_update(ecs: &specs::World, wpos: Vec3, old_block: Block, new_block: Block) { + fn on_block_update( + ecs: &specs::World, + wpos: Vec3, + old_block: Block, + new_block: Block, + ) { // When a resource block updates, inform rtsim - if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { + if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() + { ecs.write_resource::().hook_block_update( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), diff --git a/server/src/rtsim2/event.rs b/server/src/rtsim2/event.rs index e576c42fcb..958bf458fa 100644 --- a/server/src/rtsim2/event.rs +++ b/server/src/rtsim2/event.rs @@ -1,5 +1,5 @@ -use rtsim2::Event; use common::terrain::Block; +use rtsim2::Event; use vek::*; #[derive(Clone)] diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 2df1720c1c..45eba51d8f 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -4,33 +4,29 @@ pub mod tick; use common::{ grid::Grid, - slowjob::SlowJobPool, rtsim::{ChunkResource, RtSimEntity, WorldSettings}, - terrain::{TerrainChunk, Block}, + slowjob::SlowJobPool, + terrain::{Block, TerrainChunk}, vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; +use enum_map::EnumMap; use rtsim2::{ - data::{ - npc::NpcMode, - Data, - ReadError, - }, - rule::Rule, + data::{npc::NpcMode, Data, ReadError}, event::OnSetup, + rule::Rule, RtState, }; use specs::{DispatcherBuilder, WorldExt}; use std::{ + error::Error, fs::{self, File}, + io::{self, Write}, path::PathBuf, sync::Arc, time::Instant, - io::{self, Write}, - error::Error, }; -use enum_map::EnumMap; -use tracing::{error, warn, info, debug}; +use tracing::{debug, error, info, warn}; use vek::*; use world::{IndexRef, World}; @@ -41,7 +37,12 @@ pub struct RtSim { } impl RtSim { - pub fn new(settings: &WorldSettings, index: IndexRef, world: &World, data_dir: PathBuf) -> Result { + pub fn new( + settings: &WorldSettings, + index: IndexRef, + world: &World, + data_dir: PathBuf, + ) -> Result { let file_path = Self::get_file_path(data_dir); info!("Looking for rtsim data at {}...", file_path.display()); @@ -51,7 +52,10 @@ impl RtSim { Ok(file) => { info!("Rtsim data found. Attempting to load..."); match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { info!("Rtsim data loaded."); break 'load data }, + Ok(data) => { + info!("Rtsim data loaded."); + break 'load data; + }, Err(e) => { error!("Rtsim data failed to load: {}", e); let mut i = 0; @@ -64,7 +68,10 @@ impl RtSim { }); if !backup_path.exists() { fs::rename(&file_path, &backup_path)?; - warn!("Failed rtsim data was moved to {}", backup_path.display()); + warn!( + "Failed rtsim data was moved to {}", + backup_path.display() + ); info!("A fresh rtsim data will now be generated."); break; } @@ -73,12 +80,16 @@ impl RtSim { }, } }, - Err(e) if e.kind() == io::ErrorKind::NotFound => - info!("No rtsim data found. Generating from world..."), + Err(e) if e.kind() == io::ErrorKind::NotFound => { + info!("No rtsim data found. Generating from world...") + }, Err(e) => return Err(e.into()), } } else { - warn!("'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be overwritten)."); + warn!( + "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \ + overwritten)." + ); } let data = Data::generate(settings, &world, index); @@ -88,8 +99,10 @@ impl RtSim { let mut this = Self { last_saved: None, - state: RtState::new(data) - .with_resource(ChunkStates(Grid::populate_from(world.sim().get_size().as_(), |_| None))), + state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from( + world.sim().get_size().as_(), + |_| None, + ))), file_path, }; @@ -123,8 +136,16 @@ impl RtSim { } } - pub fn hook_block_update(&mut self, world: &World, index: IndexRef, wpos: Vec3, old: Block, new: Block) { - self.state.emit(event::OnBlockChange { wpos, old, new }, world, index); + pub fn hook_block_update( + &mut self, + world: &World, + index: IndexRef, + wpos: Vec3, + old: Block, + new: Block, + ) { + self.state + .emit(event::OnBlockChange { wpos, old, new }, world, index); } pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { @@ -155,10 +176,8 @@ impl RtSim { Ok(dir.join(tmp_file_name)) }) .unwrap_or_else(|| Ok(tmp_file_name.into())) - .and_then(|tmp_file_path| { - Ok((File::create(&tmp_file_path)?, tmp_file_path)) - }) - .map_err(|e: io::Error| Box::new(e) as Box::) + .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) + .map_err(|e: io::Error| Box::new(e) as Box) .and_then(|(mut file, tmp_file_path)| { debug!("Writing rtsim data to file..."); data.write_to(io::BufWriter::new(&mut file))?; @@ -180,9 +199,7 @@ impl RtSim { self.state.data().nature.get_chunk_resources(key) } - pub fn state(&self) -> &RtState { - &self.state - } + pub fn state(&self) -> &RtState { &self.state } } struct ChunkStates(pub Grid>); diff --git a/server/src/rtsim2/rule.rs b/server/src/rtsim2/rule.rs index 2f349b5368..9b73ea9165 100644 --- a/server/src/rtsim2/rule.rs +++ b/server/src/rtsim2/rule.rs @@ -1,7 +1,7 @@ pub mod deplete_resources; -use tracing::info; use rtsim2::RtState; +use tracing::info; pub fn start_rules(rtstate: &mut RtState) { info!("Starting server rtsim rules..."); diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs index 1041576977..05bfe43789 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -1,13 +1,7 @@ -use tracing::info; +use crate::rtsim2::{event::OnBlockChange, ChunkStates}; +use common::{terrain::TerrainChunk, vol::RectRasterableVol}; use rtsim2::{RtState, Rule, RuleError}; -use crate::rtsim2::{ - event::OnBlockChange, - ChunkStates, -}; -use common::{ - terrain::TerrainChunk, - vol::RectRasterableVol, -}; +use tracing::info; pub struct DepleteResources; @@ -16,7 +10,9 @@ impl Rule for DepleteResources { info!("Hello from the resource depletion rule!"); rtstate.bind::(|ctx| { - let key = ctx.event.wpos + let key = ctx + .event + .wpos .xy() .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); if let Some(Some(chunk_state)) = ctx.state.resource_mut::().0.get(key) { @@ -26,7 +22,8 @@ impl Rule for DepleteResources { if chunk_state.max_res[res] > 0 { chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 - 1.0) .round() - .max(0.0) / chunk_state.max_res[res] as f32; + .max(0.0) + / chunk_state.max_res[res] as f32; } } // Add resources @@ -34,11 +31,15 @@ impl Rule for DepleteResources { if chunk_state.max_res[res] > 0 { chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + 1.0) .round() - .max(0.0) / chunk_state.max_res[res] as f32; + .max(0.0) + / chunk_state.max_res[res] as f32; } } println!("Chunk resources = {:?}", chunk_res); - ctx.state.data_mut().nature.set_chunk_resources(key, chunk_res); + ctx.state + .data_mut() + .nature + .set_chunk_resources(key, chunk_res); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bc62d60cc5..bc794b7722 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -100,7 +100,11 @@ fn profession_extra_loadout( fn profession_agent_mark(profession: Option<&Profession>) -> Option { match profession { Some( - Profession::Merchant | Profession::Farmer | Profession::Chef | Profession::Blacksmith | Profession::Alchemist, + Profession::Merchant + | Profession::Farmer + | Profession::Chef + | Profession::Blacksmith + | Profession::Alchemist, ) => Some(comp::agent::Mark::Merchant), Some(Profession::Guard) => Some(comp::agent::Mark::Guard), _ => None, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 626499341f..b58167357e 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -5,9 +5,9 @@ use crate::{ persistence::PersistedComponents, pet::restore_pet, presence::{Presence, RepositionOnChunkLoad}, + rtsim2::RtSim, settings::Settings, sys::sentinel::DeletedEntities, - rtsim2::RtSim, wiring, BattleModeBuffer, SpawnPoint, }; use common::{ diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 3714e564a8..bcea774b24 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1,12 +1,9 @@ pub mod behavior_tree; pub use server_agent::{action_nodes, attack, consts, data, util}; -use crate::{ - // rtsim::{entity::PersonalityTrait, RtSim}, - sys::agent::{ +use crate::sys::agent::{ behavior_tree::{BehaviorData, BehaviorTree}, data::{AgentData, ReadData}, - }, }; use common::{ comp::{ diff --git a/server/src/sys/agent/data.rs b/server/src/sys/agent/data.rs index 2d1e816128..0b13b2524d 100644 --- a/server/src/sys/agent/data.rs +++ b/server/src/sys/agent/data.rs @@ -9,8 +9,9 @@ use common::{ mounting::Mount, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, + rtsim::RtSimEntity, terrain::TerrainGrid, - uid::{Uid, UidAllocator}, rtsim::RtSimEntity, + uid::{Uid, UidAllocator}, }; use specs::{ shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, @@ -154,7 +155,6 @@ pub struct ReadData<'a> { #[cfg(feature = "worldgen")] pub world: ReadExpect<'a, Arc>, pub rtsim_entity: ReadStorage<'a, RtSimEntity>, - //pub rtsim_entities: ReadStorage<'a, RtSimEntity>, pub buffs: ReadStorage<'a, Buffs>, pub combos: ReadStorage<'a, Combo>, pub active_abilities: ReadStorage<'a, ActiveAbilities>, diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index 26a75bd309..cfbea304fe 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -149,9 +149,10 @@ impl PlayState for MainMenuState { ) .into_owned(), server::Error::RtsimError(e) => localized_strings - .get("main.servers.rtsim_error") - .to_owned() - .replace("{raw_error}", e.to_string().as_str()), + .get_msg_ctx("main-servers-rtsim_error", &i18n::fluent_args! { + "raw_error" => e.to_string(), + }) + .into_owned(), server::Error::Other(e) => localized_strings .get_msg_ctx("main-servers-other_error", &i18n::fluent_args! { "raw_error" => e, diff --git a/voxygen/src/scene/debug.rs b/voxygen/src/scene/debug.rs index 3ab11ba0d5..bfb94015bb 100644 --- a/voxygen/src/scene/debug.rs +++ b/voxygen/src/scene/debug.rs @@ -293,9 +293,7 @@ impl Debug { id } - pub fn get_shape(&self, id: DebugShapeId) -> Option<&DebugShape> { - self.shapes.get(&id) - } + pub fn get_shape(&self, id: DebugShapeId) -> Option<&DebugShape> { self.shapes.get(&id) } pub fn set_context(&mut self, id: DebugShapeId, pos: [f32; 4], color: [f32; 4], ori: [f32; 4]) { self.pending_locals.insert(id, (pos, color, ori)); diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index d1d71dafb9..7d2bb1cb49 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -776,7 +776,8 @@ impl FigureMgr { .enumerate() { // Velocity relative to the current ground - let rel_vel = anim::vek::Vec3::::from(vel.0 - physics.ground_vel) / scale.map_or(1.0, |s| s.0); + let rel_vel = anim::vek::Vec3::::from(vel.0 - physics.ground_vel) + / scale.map_or(1.0, |s| s.0); let look_dir = controller.map(|c| c.inputs.look_dir).unwrap_or_default(); let is_viewpoint = scene_data.viewpoint_entity == entity; @@ -809,8 +810,9 @@ impl FigureMgr { const MIN_PERFECT_RATE_DIST: f32 = 100.0; if (i as u64 + tick) - % ((((pos.0.distance_squared(focus_pos) / scale.map_or(1.0, |s| s.0)).powf(0.25) - MIN_PERFECT_RATE_DIST.sqrt()) - .max(0.0) + % ((((pos.0.distance_squared(focus_pos) / scale.map_or(1.0, |s| s.0)).powf(0.25) + - MIN_PERFECT_RATE_DIST.sqrt()) + .max(0.0) / 3.0) as u64) .saturating_add(1) != 0 @@ -6707,8 +6709,10 @@ impl FigureMgr { } { let model_entry = model_entry?; - let figure_low_detail_distance = figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.75; - let figure_mid_detail_distance = figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.5; + let figure_low_detail_distance = + figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.75; + let figure_mid_detail_distance = + figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.5; let model = if pos.distance_squared(cam_pos) > figure_low_detail_distance.powi(2) { model_entry.lod_model(2) diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 08cccdf9bb..08c031a542 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -409,7 +409,10 @@ impl Scene { // when zooming in the distance the camera travelles should be based on the // final distance. This is to make sure the camera travelles the // same distance when zooming in and out - let player_scale = client.state().read_component_copied::(client.entity()).map_or(1.0, |s| s.0); + let player_scale = client + .state() + .read_component_copied::(client.entity()) + .map_or(1.0, |s| s.0); if delta < 0.0 { self.camera.zoom_switch( // Thank you Imbris for doing the math @@ -418,7 +421,11 @@ impl Scene { player_scale, ); } else { - self.camera.zoom_switch(delta * (0.05 + self.camera.get_distance() * 0.01), cap, player_scale); + self.camera.zoom_switch( + delta * (0.05 + self.camera.get_distance() * 0.01), + cap, + player_scale, + ); } true }, @@ -1477,15 +1484,19 @@ impl Scene { // If this shape no longer matches, remove the old one if let Some(shape_id) = hitboxes.get(&entity) { - if self.debug.get_shape(*shape_id).map_or(false, |s| s != &shape) { + if self + .debug + .get_shape(*shape_id) + .map_or(false, |s| s != &shape) + { self.debug.remove_shape(*shape_id); hitboxes.remove(&entity); } } - let shape_id = hitboxes.entry(entity).or_insert_with(|| { - self.debug.add_shape(shape) - }); + let shape_id = hitboxes + .entry(entity) + .or_insert_with(|| self.debug.add_shape(shape)); let hb_pos = [pos.0.x, pos.0.y, pos.0.z + *z_min * scale, 0.0]; let color = if group == Some(&comp::group::ENEMY) { [1.0, 0.0, 0.0, 0.5] diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index eeb7fe5b09..c456da2f17 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -628,12 +628,17 @@ impl Civs { } } - /// Return the direct track between two places, bool if the track should be reversed or not + /// Return the direct track between two places, bool if the track should be + /// reversed or not pub fn track_between(&self, a: Id, b: Id) -> Option<(Id, bool)> { self.track_map .get(&a) .and_then(|dests| Some((*dests.get(&b)?, false))) - .or_else(|| self.track_map.get(&b).and_then(|dests| Some((*dests.get(&a)?, true)))) + .or_else(|| { + self.track_map + .get(&b) + .and_then(|dests| Some((*dests.get(&a)?, true))) + }) } /// Return an iterator over a site's neighbors @@ -663,8 +668,9 @@ impl Civs { .sqrt() }; let neighbors = |p: &Id| self.neighbors(*p); - let transition = - |a: &Id, b: &Id| self.tracks.get(self.track_between(*a, *b).unwrap().0).cost; + let transition = |a: &Id, b: &Id| { + self.tracks.get(self.track_between(*a, *b).unwrap().0).cost + }; let satisfied = |p: &Id| *p == b; // We use this hasher (FxHasher64) because // (1) we don't care about DDOS attacks (ruling out SipHash); diff --git a/world/src/lib.rs b/world/src/lib.rs index 76c4b7c55d..b50ff1d250 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -49,11 +49,11 @@ use common::{ generation::{ChunkSupplement, EntityInfo}, lod, resources::TimeOfDay, + rtsim::ChunkResource, terrain::{ Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid, }, vol::{ReadVol, RectVolSize, WriteVol}, - rtsim::ChunkResource, }; use common_net::msg::{world_msg, WorldMapMsg}; use enum_map::EnumMap; @@ -492,17 +492,19 @@ impl World { // Finally, defragment to minimize space consumption. chunk.defragment(); - // Before we finish, we check candidate rtsim resource blocks, deduplicating positions and only keeping those - // that actually do have resources. Although this looks potentially very expensive, only blocks that are rtsim + // Before we finish, we check candidate rtsim resource blocks, deduplicating + // positions and only keeping those that actually do have resources. + // Although this looks potentially very expensive, only blocks that are rtsim // resources (i.e: a relatively small number of sprites) are processed here. if let Some(rtsim_resources) = rtsim_resources { rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array()); rtsim_resource_blocks.dedup(); for wpos in rtsim_resource_blocks { - chunk.map( - wpos - chunk_wpos2d.with_z(0), - |block| if let Some(res) = block.get_rtsim_resource() { - // Note: this represents the upper limit, not the actual number spanwed, so we increment this before deciding whether we're going to spawn the resource. + chunk.map(wpos - chunk_wpos2d.with_z(0), |block| { + if let Some(res) = block.get_rtsim_resource() { + // Note: this represents the upper limit, not the actual number spanwed, so + // we increment this before deciding whether we're going to spawn the + // resource. supplement.rtsim_max_resources[res] += 1; // Throw a dice to determine whether this resource should actually spawn // TODO: Don't throw a dice, try to generate the *exact* correct number @@ -513,12 +515,11 @@ impl World { } } else { block - }, - ); + } + }); } } - Ok((chunk, supplement)) } diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index c77e8e24b2..1c2b16178f 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -1012,8 +1012,11 @@ pub fn merchant_loadout( loadout_builder: LoadoutBuilder, economy: Option<&SiteInformation>, ) -> LoadoutBuilder { - trader_loadout(loadout_builder - .with_asset_expect("common.loadout.village.merchant", &mut thread_rng()), economy, |_| true) + trader_loadout( + loadout_builder.with_asset_expect("common.loadout.village.merchant", &mut thread_rng()), + economy, + |_| true, + ) } pub fn trader_loadout( @@ -1030,10 +1033,13 @@ pub fn trader_loadout( let mut bag4 = Item::new_from_asset_expect("common.items.armor.misc.bag.sturdy_red_backpack"); let slots = backpack.slots().len() + 4 * bag1.slots().len(); let mut stockmap: HashMap = economy - .map(|e| e.unconsumed_stock.clone() - .into_iter() - .filter(|(good, _)| permitted(*good)) - .collect()) + .map(|e| { + e.unconsumed_stock + .clone() + .into_iter() + .filter(|(good, _)| permitted(*good)) + .collect() + }) .unwrap_or_default(); // modify stock for better gameplay diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 8b46849d7d..9da82b9d44 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -7,7 +7,8 @@ use self::tile::{HazardKind, KeepKind, RoofKind, Tile, TileGrid, TILE_SIZE}; pub use self::{ gen::{aabr_with_z, Fill, Painter, Primitive, PrimitiveRef, Structure}, plot::{Plot, PlotKind}, - util::Dir, tile::TileKind, + tile::TileKind, + util::Dir, }; use crate::{ sim::Path, From 1b439d08978d5504a40567f4394e74137a9fbaba Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 5 Sep 2022 02:21:11 +0100 Subject: [PATCH 038/139] New behaviour tree system for rtsim2 --- rtsim/Cargo.toml | 2 +- rtsim/src/data/npc.rs | 135 ++++++++- rtsim/src/gen/mod.rs | 14 +- rtsim/src/lib.rs | 2 +- rtsim/src/rule/npc_ai.rs | 511 +++++++++++++++++++------------- rtsim/src/rule/simulate_npcs.rs | 7 + server/src/rtsim2/tick.rs | 21 +- 7 files changed, 464 insertions(+), 228 deletions(-) diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 1d7665bcc4..321558b7b3 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -18,4 +18,4 @@ atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } fxhash = "0.2.1" -itertools = "0.10.3" \ No newline at end of file +itertools = "0.10.3" diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bd97d8d097..a5d6f3dc93 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -11,7 +11,8 @@ use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ collections::VecDeque, - ops::{Deref, DerefMut}, + ops::{Deref, DerefMut, ControlFlow}, + any::Any, }; use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; @@ -38,7 +39,113 @@ pub struct PathingMemory { pub intersite_path: Option<(PathData<(Id, bool), SiteId>, usize)>, } -#[derive(Clone, Serialize, Deserialize)] +pub struct Controller { + pub goto: Option<(Vec3, f32)>, +} + +#[derive(Default)] +pub struct TaskState { + state: Option>, +} + +pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(()); +pub const FINISH: ControlFlow<()> = ControlFlow::Continue(()); + +pub trait Task: PartialEq + Clone + Send + Sync + 'static { + type State: Send + Sync; + type Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()>; + + fn then(self, other: B) -> Then { + Then(self, other) + } + + fn repeat(self) -> Repeat { + Repeat(self) + } +} + +#[derive(Clone, PartialEq)] +pub struct Then(A, B); + +impl Task for Then + where B: for<'a> Task = A::Ctx<'a>> +{ + type State = Result; // TODO: Use `Either` instead + type Ctx<'a> = A::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + Ok(self.0.begin(ctx)) + } + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + match state { + Ok(a_state) => { + self.0.run(a_state, ctx, controller)?; + *state = Err(self.1.begin(ctx)); + CONTINUE + }, + Err(b_state) => self.1.run(b_state, ctx, controller), + } + } +} + +#[derive(Clone, PartialEq)] +pub struct Repeat(A); + +impl Task for Repeat { + type State = A::State; + type Ctx<'a> = A::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + self.0.begin(ctx) + } + + fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + self.0.run(state, ctx, controller)?; + *state = self.0.begin(ctx); + CONTINUE + } +} + +impl TaskState { + pub fn perform<'a, T: Task>( + &mut self, + task: T, + ctx: &T::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + type StateOf = (T, ::State); + + let mut state = if let Some(state) = self.state + .take() + .and_then(|state| state + .downcast::>() + .ok() + .filter(|state| state.0 == task)) + { + state + } else { + let mut state = task.begin(ctx); + Box::new((task, state)) + }; + + let res = state.0.run(&mut state.1, ctx, controller); + + self.state = if matches!(res, ControlFlow::Break(())) { + Some(state) + } else { + None + }; + + res + } +} + +#[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state /// Represents the location of the NPC. @@ -50,8 +157,6 @@ pub struct Npc { pub faction: Option, // Unpersisted state - #[serde(skip_serializing, skip_deserializing)] - pub pathing: PathingMemory, #[serde(skip_serializing, skip_deserializing)] pub current_site: Option, @@ -66,6 +171,26 @@ pub struct Npc { /// should instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] pub mode: NpcMode, + + #[serde(skip_serializing, skip_deserializing)] + pub task_state: Option, +} + +impl Clone for Npc { + fn clone(&self) -> Self { + Self { + seed: self.seed, + wpos: self.wpos, + profession: self.profession.clone(), + home: self.home, + faction: self.faction, + // Not persisted + current_site: Default::default(), + goto: Default::default(), + mode: Default::default(), + task_state: Default::default(), + } + } } impl Npc { @@ -79,10 +204,10 @@ impl Npc { profession: None, home: None, faction: None, - pathing: Default::default(), current_site: None, goto: None, mode: NpcMode::Simulated, + task_state: Default::default(), } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index ee0ffe8b49..e2bcd7565b 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -77,13 +77,13 @@ impl Data { Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - 11..=15 => Profession::Guard, + .with_profession(match rng.gen_range(0..17) { + // 0 => Profession::Hunter, + // 1 => Profession::Blacksmith, + // 2 => Profession::Chef, + // 3 => Profession::Alchemist, + // 5..=10 => Profession::Farmer, + // 11..=15 => Profession::Guard, _ => Profession::Adventurer(rng.gen_range(0..=3)), }), ); diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 0a15a841bc..51cecf6575 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(explicit_generic_args_with_impl_trait)] +#![feature(explicit_generic_args_with_impl_trait, generic_associated_types, never_type, try_blocks)] pub mod data; pub mod event; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8f5a02fbf9..103a528873 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,9 +1,9 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ - data::{npc::PathData, Sites}, + data::{npc::{PathData, PathingMemory, Npc, Task, TaskState, Controller, CONTINUE, FINISH}, Sites}, event::OnTick, - RtState, Rule, RuleError, + RtState, Rule, RuleError, EventCtx, }; use common::{ astar::{Astar, PathResult}, @@ -15,7 +15,7 @@ use common::{ }; use fxhash::FxHasher64; use itertools::Itertools; -use rand::seq::IteratorRandom; +use rand::prelude::*; use vek::*; use world::{ civ::{self, Track}, @@ -23,6 +23,11 @@ use world::{ site2::{self, TileKind}, IndexRef, World, }; +use std::{ + ops::ControlFlow, + marker::PhantomData, + any::{Any, TypeId}, +}; pub struct NpcAi; @@ -46,7 +51,7 @@ const CARDINALS: &[Vec2] = &[ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); let mut astar = Astar::new( - 100, + 250, start, &heuristic, BuildHasherDefault::::default(), @@ -69,7 +74,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes | TileKind::Tower(_) | TileKind::Keep(_) | TileKind::Gate - | TileKind::GnarlingFortification => 3.0, + | TileKind::GnarlingFortification => 20.0, }; let is_door_tile = |plot: Id, tile: Vec2| match site.plot(plot).kind() { site2::PlotKind::House(house) => house.door_tile == tile, @@ -96,7 +101,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes }; astar.poll( - 100, + 250, heuristic, |&tile| NEIGHBOURS.iter().map(move |c| tile + *c), transition, @@ -132,7 +137,7 @@ fn path_between_sites( let heuristic = |site: &Id| get_site(site).center.as_().distance(end_pos); let mut astar = Astar::new( - 100, + 250, start, heuristic, BuildHasherDefault::::default(), @@ -149,7 +154,7 @@ fn path_between_sites( let transition = |a: &Id, b: &Id| track_between(*a, *b).cost; - let path = astar.poll(100, heuristic, neighbors, transition, |site| *site == end); + let path = astar.poll(250, heuristic, neighbors, transition, |site| *site == end); path.map(|path| { let path = path @@ -231,216 +236,314 @@ fn path_towns( } } +const MAX_STEP: f32 = 32.0; + impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|ctx| { - let data = &mut *ctx.state.data_mut(); - let mut dynamic_rng = rand::thread_rng(); - for npc in data.npcs.values_mut() { - npc.current_site = ctx - .world - .sim() - .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) - .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); + rtstate.bind::(|mut ctx| { + let npc_ids = ctx.state.data().npcs.keys().collect::>(); - if let Some(home_id) = npc.home { - if let Some((target, _)) = npc.goto { - // Walk to the current target - if target.xy().distance_squared(npc.wpos.xy()) < 4.0 { - npc.goto = None; - } - } else { - // Walk slower when pathing in a site, and faster when between sites - if npc.pathing.intersite_path.is_none() { - npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 0.7)); - } else { - npc.goto = Some((npc.goto.map_or(npc.wpos, |(wpos, _)| wpos), 1.0)); - } + for npc_id in npc_ids { + let mut task_state = ctx.state.data_mut().npcs[npc_id].task_state.take().unwrap_or_default(); - if let Some((ref mut path, site)) = npc.pathing.intrasite_path { - // If the npc walking in a site and want to reroll (because the path was - // exhausted.) to try to find a complete path. - if path.repoll { - npc.pathing.intrasite_path = - path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) - .map(|path| (path, site)); - } - } + let (controller, task_state) = { + let data = &*ctx.state.data(); + let npc = &data.npcs[npc_id]; - if let Some((ref mut path, site)) = npc.pathing.intrasite_path { - if let Some(next_tile) = path.path.pop_front() { - match &ctx.index.sites.get(site).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - // Set the target to the next node in the path. - let wpos = site.tile_center_wpos(next_tile); - let wpos = wpos.as_::().with_z( - ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), - ); + let mut controller = Controller { goto: npc.goto }; - npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); - }, - _ => {}, - } - } else { - // If the path is empty, we're done. - npc.pathing.intrasite_path = None; - } - } else if let Some((path, progress)) = { - // Check if we are done with this part of the inter site path. - if let Some((path, progress)) = &mut npc.pathing.intersite_path { - if let Some((track_id, _)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - if *progress >= track.path().len() { - if path.repoll { - // Repoll if last path wasn't complete. - npc.pathing.intersite_path = path_towns( - npc.current_site.unwrap(), - path.end, - &data.sites, - ctx.world, - ); - } else { - // Otherwise just take the next in the calculated path. - path.path.pop_front(); - *progress = 0; - } - } - } - } - &mut npc.pathing.intersite_path - } { - if let Some((track_id, reversed)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - let get_progress = |progress: usize| { - if *reversed { - track.path().len().wrapping_sub(progress + 1) - } else { - progress - } - }; + let action: ControlFlow<()> = try { + if matches!(npc.profession, Some(Profession::Adventurer(_))) { + if let Some(home) = npc.home { + // Travel between random nearby sites + let task = generate(move |(npc, ctx): &(&Npc, &EventCtx<_, _>)| { + let tgt_site = ctx.state.data().sites + .iter() + .filter(|(site_id, site)| npc + .current_site + .map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25)) + .min_by_key(|(_, site)| site.wpos.as_().distance(npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id) + .unwrap_or(home); - let transform_path_pos = |chunk_pos| { - let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); - if let Some(pathdata) = - ctx.world.sim().get_nearest_path(chunk_wpos) - { - pathdata.1.map(|e| e as i32) - } else { - chunk_wpos - } - }; + TravelToSite(tgt_site) + }) + .repeat(); - // Loop through and skip nodes that are inside a site, and use intra - // site path finding there instead. - let walk_path = loop { - if let Some(chunk_pos) = - track.path().nodes.get(get_progress(*progress)) - { - if let Some((wpos, site_id, site)) = - ctx.world.sim().get(*chunk_pos).and_then(|chunk| { - let site_id = *chunk.sites.first()?; - let wpos = transform_path_pos(*chunk_pos); - match &ctx.index.sites.get(site_id).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) - if !site.wpos_tile(wpos).is_empty() => - { - Some((wpos, site_id, site)) - }, - _ => None, - } - }) - { - if !site.wpos_tile(wpos).is_empty() { - *progress += 1; - } else { - let end = site.wpos_tile_pos(wpos); - npc.pathing.intrasite_path = - path_town(npc.wpos, site_id, ctx.index, |_| { - Some(end) - }) - .map(|path| (path, site_id)); - break false; - } - } else { - break true; - } - } else { - break false; - } - }; - - if walk_path { - // Find the next wpos on the path. - // NOTE: Consider not having this big gap between current - // position and next. For better path finding. Maybe that would - // mean having a float for progress. - let wpos = transform_path_pos( - track.path().nodes[get_progress(*progress)], - ); - let wpos = wpos.as_::().with_z( - ctx.world.sim().get_alt_approx(wpos).unwrap_or(0.0), - ); - npc.goto = Some((wpos, npc.goto.map_or(1.0, |(_, sf)| sf))); - *progress += 1; - } - } else { - npc.pathing.intersite_path = None; + task_state.perform(task, &(&*npc, &ctx), &mut controller)?; } } else { - if matches!(npc.profession, Some(Profession::Adventurer(_))) { - // If the npc is home, choose a random site to go to, otherwise go - // home. - if let Some(start) = npc.current_site { - let end = if home_id == start { - data.sites - .keys() - .filter(|site| *site != home_id) - .choose(&mut dynamic_rng) - .unwrap_or(home_id) - } else { - home_id - }; - npc.pathing.intersite_path = - path_towns(start, end, &data.sites, ctx.world); - } - } else { - // Choose a random plaza in the npcs home site (which should be the - // current here) to go to. - if let Some(home_id) = - data.sites.get(home_id).and_then(|site| site.world_site) - { - npc.pathing.intrasite_path = - path_town(npc.wpos, home_id, ctx.index, |site| { - Some( - site.plots - [site.plazas().choose(&mut dynamic_rng)?] - .root_tile(), - ) - }) - .map(|path| (path, home_id)); - } - } + controller.goto = None; + + // // Choose a random plaza in the npcs home site (which should be the + // // current here) to go to. + // if let Some(home_id) = + // data.sites.get(home_id).and_then(|site| site.world_site) + // { + // npc.pathing.intrasite_path = + // path_town(npc.wpos, home_id, ctx.index, |site| { + // Some( + // site.plots + // [site.plazas().choose(&mut dynamic_rng)?] + // .root_tile(), + // ) + // }) + // .map(|path| (path, home_id)); + // } } - } - } else { - // TODO: Don't make homeless people walk around in circles - npc.goto = Some(( - npc.wpos - + Vec3::new( - ctx.event.time.0.sin() as f32 * 16.0, - ctx.event.time.0.cos() as f32 * 16.0, - 0.0, - ), - 0.7, - )); - } + }; + + (controller, task_state) + }; + + ctx.state.data_mut().npcs[npc_id].goto = controller.goto; + ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state); } }); Ok(Self) } } + +#[derive(Clone)] +pub struct Generate(F, PhantomData); + +impl PartialEq for Generate { + fn eq(&self, _: &Self) -> bool { true } +} + +pub fn generate(f: F) -> Generate { Generate(f, PhantomData) } + +impl Task for Generate + where F: Clone + Send + Sync + 'static + for<'a> Fn(&T::Ctx<'a>) -> T +{ + type State = (T::State, T); + type Ctx<'a> = T::Ctx<'a>; + + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { + let task = (self.0)(ctx); + (task.begin(ctx), task) + } + + fn run<'a>( + &self, + (state, task): &mut Self::State, + ctx: &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + task.run(state, ctx, controller) + } +} + +#[derive(Clone, PartialEq)] +struct Goto(Vec2, f32); + +impl Task for Goto { + type State = (Vec2, f32); + type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + + fn begin<'a>(&self, (_npc, _ctx): &Self::Ctx<'a>) -> Self::State { (self.0, self.1) } + + fn run<'a>( + &self, + (tgt, speed_factor): &mut Self::State, + (npc, ctx): &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + if npc.wpos.xy().distance_squared(*tgt) < 2f32.powi(2) { + controller.goto = None; + FINISH + } else { + let dist = npc.wpos.xy().distance(*tgt); + let step = dist.min(32.0); + let next_tgt = npc.wpos.xy() + (*tgt - npc.wpos.xy()) / dist * step; + + if npc.goto.map_or(true, |(tgt, _)| tgt.xy().distance_squared(next_tgt) > (step * 0.5).powi(2)) || npc.wpos.xy().distance_squared(next_tgt) < (step * 0.5).powi(2) { + controller.goto = Some((next_tgt.with_z(ctx.world.sim().get_alt_approx(next_tgt.map(|e| e as i32)).unwrap_or(0.0)), *speed_factor)); + } + CONTINUE + } + } +} + +#[derive(Clone, PartialEq)] +struct TravelToSite(SiteId); + +impl Task for TravelToSite { + type State = (PathingMemory, TaskState); + type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + + fn begin<'a>(&self, (npc, ctx): &Self::Ctx<'a>) -> Self::State { + (PathingMemory::default(), TaskState::default()) + } + + fn run<'a>( + &self, + (pathing, task_state): &mut Self::State, + (npc, ctx): &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { + if let Some(current_site) = npc.current_site { + if pathing.intersite_path.is_none() { + pathing.intersite_path = path_towns( + current_site, + self.0, + &ctx.state.data().sites, + ctx.world, + ); + if pathing.intersite_path.is_none() { + return FINISH; + } + } + } + + if let Some((ref mut path, site)) = pathing.intrasite_path { + // If the npc walking in a site and want to reroll (because the path was + // exhausted.) to try to find a complete path. + if path.repoll { + pathing.intrasite_path = + path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) + .map(|path| (path, site)); + } + } + + if let Some((ref mut path, site)) = pathing.intrasite_path { + if let Some(next_tile) = path.path.front() { + match &ctx.index.sites.get(site).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + // Set the target to the next node in the path. + let wpos = site.tile_center_wpos(*next_tile); + task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 1.0), &(npc, ctx), controller)?; + path.path.pop_front(); + return CONTINUE; + }, + _ => {}, + } + } else { + // If the path is empty, we're done. + pathing.intrasite_path = None; + } + } + + if let Some((path, progress)) = { + if let Some((path, progress)) = &mut pathing.intersite_path { + if let Some((track_id, _)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + if *progress >= track.path().len() { + if path.repoll { + // Repoll if last path wasn't complete. + pathing.intersite_path = path_towns( + npc.current_site.unwrap(), + path.end, + &ctx.state.data().sites, + ctx.world, + ); + } else { + // Otherwise just take the next in the calculated path. + path.path.pop_front(); + *progress = 0; + } + } + } + } + &mut pathing.intersite_path + } { + if let Some((track_id, reversed)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + let get_progress = |progress: usize| { + if *reversed { + track.path().len().wrapping_sub(progress + 1) + } else { + progress + } + }; + + let transform_path_pos = |chunk_pos| { + let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); + if let Some(pathdata) = + ctx.world.sim().get_nearest_path(chunk_wpos) + { + pathdata.1.map(|e| e as i32) + } else { + chunk_wpos + } + }; + + // Loop through and skip nodes that are inside a site, and use intra + // site path finding there instead. + let walk_path = if let Some(chunk_pos) = + track.path().nodes.get(get_progress(*progress)) + { + if let Some((wpos, site_id, site)) = + ctx.world.sim().get(*chunk_pos).and_then(|chunk| { + let site_id = *chunk.sites.first()?; + let wpos = transform_path_pos(*chunk_pos); + match &ctx.index.sites.get(site_id).kind { + SiteKind::Refactor(site) + | SiteKind::CliffTown(site) + | SiteKind::DesertCity(site) => { + Some((wpos, site_id, site)) + }, + _ => None, + } + }) + { + if pathing.intrasite_path.is_none() { + let end = site.wpos_tile_pos(wpos); + pathing.intrasite_path = + path_town(npc.wpos, site_id, ctx.index, |_| { + Some(end) + }) + .map(|path| (path, site_id)); + } + if site.wpos_tile(wpos).is_obstacle() { + *progress += 1; + pathing.intrasite_path = None; + false + } else { + true + } + } else { + true + } + } else { + false + }; + + if walk_path { + // Find the next wpos on the path. + // NOTE: Consider not having this big gap between current + // position and next. For better path finding. Maybe that would + // mean having a float for progress. + let wpos = transform_path_pos( + track.path().nodes[get_progress(*progress)], + ); + task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller)?; + *progress += 1; + return CONTINUE; + } + } else { + pathing.intersite_path = None; + } + } + + let world_site = |site_id: SiteId| { + let id = ctx.state.data().sites.get(site_id).and_then(|site| site.world_site)?; + ctx.world.civs().sites.recreate_id(id.id()) + }; + + if let Some(site_wpos) = world_site(self.0) + .map(|home| TerrainChunkSize::center_wpos(ctx.world.civs().sites.get(home).center)) + { + if site_wpos.map(|e| e as f32 + 0.5).distance_squared(npc.wpos.xy()) < 16f32.powi(2) { + FINISH + } else { + task_state.perform(Goto(site_wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller) + } + } else { + FINISH + } + } +} diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index b2761cc404..06a816b6dd 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -36,6 +36,13 @@ impl Rule for SimulateNpcs { .sim() .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) .unwrap_or(0.0); + + // Update the NPC's current site, if any + npc.current_site = ctx + .world + .sim() + .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) + .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); } }); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index bc794b7722..ff8435f711 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -260,16 +260,17 @@ impl<'a> System<'a> for Sys { if let Some(agent) = agent { agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos); agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); - agent.rtsim_controller.heading_to = - npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { - Some( - index - .sites - .get(data.sites.get(path.end)?.world_site?) - .name() - .to_string(), - ) - }); + // TODO: + // agent.rtsim_controller.heading_to = + // npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { + // Some( + // index + // .sites + // .get(data.sites.get(path.end)?.world_site?) + // .name() + // .to_string(), + // ) + // }); } }); } From 7e9474ab703af92d7c7e5b32e4e943ba1d9ec5f7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 5 Sep 2022 15:03:21 +0100 Subject: [PATCH 039/139] Overhauled rtsim2 pathfinding with TravelTo --- rtsim/src/data/npc.rs | 59 +++-- rtsim/src/gen/mod.rs | 14 +- rtsim/src/gen/site.rs | 27 +- rtsim/src/lib.rs | 7 +- rtsim/src/rule/npc_ai.rs | 530 +++++++++++++++++++++----------------- server/src/rtsim2/tick.rs | 6 +- world/src/site2/tile.rs | 10 +- 7 files changed, 366 insertions(+), 287 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index a5d6f3dc93..13fb620fd2 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -10,9 +10,9 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ - collections::VecDeque, - ops::{Deref, DerefMut, ControlFlow}, any::Any, + collections::VecDeque, + ops::{ControlFlow, Deref, DerefMut}, }; use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; @@ -57,31 +57,37 @@ pub trait Task: PartialEq + Clone + Send + Sync + 'static { fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; - fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()>; + fn run<'a>( + &self, + state: &mut Self::State, + ctx: &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()>; - fn then(self, other: B) -> Then { - Then(self, other) - } + fn then(self, other: B) -> Then { Then(self, other) } - fn repeat(self) -> Repeat { - Repeat(self) - } + fn repeat(self) -> Repeat { Repeat(self) } } #[derive(Clone, PartialEq)] pub struct Then(A, B); impl Task for Then - where B: for<'a> Task = A::Ctx<'a>> +where + B: for<'a> Task = A::Ctx<'a>>, { - type State = Result; // TODO: Use `Either` instead + // TODO: Use `Either` instead type Ctx<'a> = A::Ctx<'a>; + type State = Result; - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { - Ok(self.0.begin(ctx)) - } + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { Ok(self.0.begin(ctx)) } - fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + fn run<'a>( + &self, + state: &mut Self::State, + ctx: &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { match state { Ok(a_state) => { self.0.run(a_state, ctx, controller)?; @@ -97,14 +103,17 @@ impl Task for Then pub struct Repeat(A); impl Task for Repeat { - type State = A::State; type Ctx<'a> = A::Ctx<'a>; + type State = A::State; - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { - self.0.begin(ctx) - } + fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { self.0.begin(ctx) } - fn run<'a>(&self, state: &mut Self::State, ctx: &Self::Ctx<'a>, controller: &mut Controller) -> ControlFlow<()> { + fn run<'a>( + &self, + state: &mut Self::State, + ctx: &Self::Ctx<'a>, + controller: &mut Controller, + ) -> ControlFlow<()> { self.0.run(state, ctx, controller)?; *state = self.0.begin(ctx); CONTINUE @@ -120,13 +129,12 @@ impl TaskState { ) -> ControlFlow<()> { type StateOf = (T, ::State); - let mut state = if let Some(state) = self.state - .take() - .and_then(|state| state + let mut state = if let Some(state) = self.state.take().and_then(|state| { + state .downcast::>() .ok() - .filter(|state| state.0 == task)) - { + .filter(|state| state.0 == task) + }) { state } else { let mut state = task.begin(ctx); @@ -157,7 +165,6 @@ pub struct Npc { pub faction: Option, // Unpersisted state - #[serde(skip_serializing, skip_deserializing)] pub current_site: Option, diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index e2bcd7565b..ee0ffe8b49 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -77,13 +77,13 @@ impl Data { Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match rng.gen_range(0..17) { - // 0 => Profession::Hunter, - // 1 => Profession::Blacksmith, - // 2 => Profession::Chef, - // 3 => Profession::Alchemist, - // 5..=10 => Profession::Farmer, - // 11..=15 => Profession::Guard, + .with_profession(match rng.gen_range(0..20) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=10 => Profession::Farmer, + 11..=15 => Profession::Guard, _ => Profession::Adventurer(rng.gen_range(0..=3)), }), ); diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index 30d079362f..d871360ca3 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,24 +1,35 @@ use crate::data::{FactionId, Site}; use common::store::Id; use vek::*; -use world::{site::Site as WorldSite, IndexRef, World}; +use world::{ + site::{Site as WorldSite, SiteKind}, + IndexRef, World, +}; impl Site { pub fn generate( - world_site: Id, + world_site_id: Id, world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)], ) -> Self { - let wpos = index.sites.get(world_site).get_origin(); + let world_site = index.sites.get(world_site_id); + let wpos = world_site.get_origin(); Self { wpos, - world_site: Some(world_site), - faction: nearby_factions - .iter() - .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) - .map(|(_, faction)| *faction), + world_site: Some(world_site_id), + faction: if matches!( + &world_site.kind, + SiteKind::Refactor(_) | SiteKind::CliffTown(_) | SiteKind::DesertCity(_) + ) { + nearby_factions + .iter() + .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) + .map(|(_, faction)| *faction) + } else { + None + }, } } } diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 51cecf6575..2b569f264e 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,4 +1,9 @@ -#![feature(explicit_generic_args_with_impl_trait, generic_associated_types, never_type, try_blocks)] +#![feature( + explicit_generic_args_with_impl_trait, + generic_associated_types, + never_type, + try_blocks +)] pub mod data; pub mod event; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 103a528873..159dac08ae 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,9 +1,12 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ - data::{npc::{PathData, PathingMemory, Npc, Task, TaskState, Controller, CONTINUE, FINISH}, Sites}, + data::{ + npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH}, + Sites, + }, event::OnTick, - RtState, Rule, RuleError, EventCtx, + EventCtx, RtState, Rule, RuleError, }; use common::{ astar::{Astar, PathResult}, @@ -16,6 +19,11 @@ use common::{ use fxhash::FxHasher64; use itertools::Itertools; use rand::prelude::*; +use std::{ + any::{Any, TypeId}, + marker::PhantomData, + ops::ControlFlow, +}; use vek::*; use world::{ civ::{self, Track}, @@ -23,24 +31,9 @@ use world::{ site2::{self, TileKind}, IndexRef, World, }; -use std::{ - ops::ControlFlow, - marker::PhantomData, - any::{Any, TypeId}, -}; pub struct NpcAi; -const NEIGHBOURS: &[Vec2] = &[ - Vec2::new(1, 0), - Vec2::new(0, 1), - Vec2::new(-1, 0), - Vec2::new(0, -1), - Vec2::new(1, 1), - Vec2::new(-1, 1), - Vec2::new(-1, -1), - Vec2::new(1, -1), -]; const CARDINALS: &[Vec2] = &[ Vec2::new(1, 0), Vec2::new(0, 1), @@ -51,7 +44,7 @@ const CARDINALS: &[Vec2] = &[ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); let mut astar = Astar::new( - 250, + 1000, start, &heuristic, BuildHasherDefault::::default(), @@ -63,9 +56,9 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes let b_tile = site.tiles.get(*b); let terrain = match &b_tile.kind { - TileKind::Empty => 5.0, - TileKind::Hazard(_) => 20.0, - TileKind::Field => 12.0, + TileKind::Empty => 3.0, + TileKind::Hazard(_) => 50.0, + TileKind::Field => 8.0, TileKind::Plaza | TileKind::Road { .. } => 1.0, TileKind::Building @@ -74,7 +67,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes | TileKind::Tower(_) | TileKind::Keep(_) | TileKind::Gate - | TileKind::GnarlingFortification => 20.0, + | TileKind::GnarlingFortification => 5.0, }; let is_door_tile = |plot: Id, tile: Vec2| match site.plot(plot).kind() { site2::PlotKind::House(house) => house.door_tile == tile, @@ -85,27 +78,27 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes a_tile .plot .and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)) - .unwrap_or(f32::INFINITY) + .unwrap_or(10000.0) } else if b_tile.is_building() && a_tile.is_road() { b_tile .plot .and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)) - .unwrap_or(f32::INFINITY) + .unwrap_or(10000.0) } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { - f32::INFINITY + 10000.0 } else { 1.0 }; - distance * terrain * building + distance * terrain + building }; astar.poll( - 250, + 1000, heuristic, - |&tile| NEIGHBOURS.iter().map(move |c| tile + *c), + |&tile| CARDINALS.iter().map(move |c| tile + *c), transition, - |tile| *tile == end, + |tile| *tile == end || site.tiles.get_known(*tile).is_none(), ) } @@ -183,20 +176,20 @@ fn path_town( } // We pop the first element of the path - fn pop_first(mut queue: VecDeque) -> VecDeque { - queue.pop_front(); - queue - } + // fn pop_first(mut queue: VecDeque) -> VecDeque { + // queue.pop_front(); + // queue + // } match path_in_site(start, end, site) { PathResult::Path(p) => Some(PathData { end, - path: pop_first(p.nodes.into()), + path: p.nodes.into(), //pop_first(p.nodes.into()), repoll: false, }), PathResult::Exhausted(p) => Some(PathData { end, - path: pop_first(p.nodes.into()), + path: p.nodes.into(), //pop_first(p.nodes.into()), repoll: true, }), PathResult::None(_) | PathResult::Pending => None, @@ -244,7 +237,10 @@ impl Rule for NpcAi { let npc_ids = ctx.state.data().npcs.keys().collect::>(); for npc_id in npc_ids { - let mut task_state = ctx.state.data_mut().npcs[npc_id].task_state.take().unwrap_or_default(); + let mut task_state = ctx.state.data_mut().npcs[npc_id] + .task_state + .take() + .unwrap_or_default(); let (controller, task_state) = { let data = &*ctx.state.data(); @@ -256,40 +252,81 @@ impl Rule for NpcAi { if matches!(npc.profession, Some(Profession::Adventurer(_))) { if let Some(home) = npc.home { // Travel between random nearby sites - let task = generate(move |(npc, ctx): &(&Npc, &EventCtx<_, _>)| { - let tgt_site = ctx.state.data().sites - .iter() - .filter(|(site_id, site)| npc - .current_site - .map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25)) - .min_by_key(|(_, site)| site.wpos.as_().distance(npc.wpos.xy()) as i32) - .map(|(site_id, _)| site_id) - .unwrap_or(home); + let task = generate( + move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { + // Choose a random site that's fairly close by + let tgt_site = ctx + .state + .data() + .sites + .iter() + .filter(|(site_id, site)| { + site.faction.is_some() + && npc + .current_site + .map_or(true, |cs| *site_id != cs) + && thread_rng().gen_bool(0.25) + }) + .min_by_key(|(_, site)| { + site.wpos.as_().distance(npc.wpos.xy()) as i32 + }) + .map(|(site_id, _)| site_id) + .unwrap_or(home); - TravelToSite(tgt_site) - }) - .repeat(); + let wpos = ctx + .state + .data() + .sites + .get(tgt_site) + .map_or(npc.wpos.xy(), |site| site.wpos.as_()); - task_state.perform(task, &(&*npc, &ctx), &mut controller)?; + TravelTo { + wpos, + use_paths: true, + } + }, + ) + .repeat(); + + task_state.perform( + task, + &(npc_id, &*npc, &ctx), + &mut controller, + )?; } } else { - controller.goto = None; - // // Choose a random plaza in the npcs home site (which should be the // // current here) to go to. - // if let Some(home_id) = - // data.sites.get(home_id).and_then(|site| site.world_site) - // { - // npc.pathing.intrasite_path = - // path_town(npc.wpos, home_id, ctx.index, |site| { - // Some( - // site.plots - // [site.plazas().choose(&mut dynamic_rng)?] - // .root_tile(), - // ) - // }) - // .map(|path| (path, home_id)); - // } + let task = + generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { + let data = ctx.state.data(); + let site2 = + npc.home.and_then(|home| data.sites.get(home)).and_then( + |home| match &ctx.index.sites.get(home.world_site?).kind + { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + }, + ); + + let wpos = site2 + .and_then(|site2| { + let plaza = &site2.plots + [site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + .unwrap_or(npc.wpos.xy()); + + TravelTo { + wpos, + use_paths: true, + } + }) + .repeat(); + + task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; } }; @@ -315,10 +352,11 @@ impl PartialEq for Generate { pub fn generate(f: F) -> Generate { Generate(f, PhantomData) } impl Task for Generate - where F: Clone + Send + Sync + 'static + for<'a> Fn(&T::Ctx<'a>) -> T +where + F: Clone + Send + Sync + 'static + for<'a> Fn(&T::Ctx<'a>) -> T, { - type State = (T::State, T); type Ctx<'a> = T::Ctx<'a>; + type State = (T::State, T); fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { let task = (self.0)(ctx); @@ -336,30 +374,53 @@ impl Task for Generate } #[derive(Clone, PartialEq)] -struct Goto(Vec2, f32); +pub struct Goto { + wpos: Vec2, + speed_factor: f32, + finish_dist: f32, +} + +pub fn goto(wpos: Vec2) -> Goto { + Goto { + wpos, + speed_factor: 1.0, + finish_dist: 1.0, + } +} impl Task for Goto { - type State = (Vec2, f32); type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + type State = (); - fn begin<'a>(&self, (_npc, _ctx): &Self::Ctx<'a>) -> Self::State { (self.0, self.1) } + fn begin<'a>(&self, (_npc, _ctx): &Self::Ctx<'a>) -> Self::State {} fn run<'a>( &self, - (tgt, speed_factor): &mut Self::State, + (): &mut Self::State, (npc, ctx): &Self::Ctx<'a>, controller: &mut Controller, ) -> ControlFlow<()> { - if npc.wpos.xy().distance_squared(*tgt) < 2f32.powi(2) { + if npc.wpos.xy().distance_squared(self.wpos) < self.finish_dist.powi(2) { controller.goto = None; FINISH } else { - let dist = npc.wpos.xy().distance(*tgt); + let dist = npc.wpos.xy().distance(self.wpos); let step = dist.min(32.0); - let next_tgt = npc.wpos.xy() + (*tgt - npc.wpos.xy()) / dist * step; + let next_tgt = npc.wpos.xy() + (self.wpos - npc.wpos.xy()) / dist * step; - if npc.goto.map_or(true, |(tgt, _)| tgt.xy().distance_squared(next_tgt) > (step * 0.5).powi(2)) || npc.wpos.xy().distance_squared(next_tgt) < (step * 0.5).powi(2) { - controller.goto = Some((next_tgt.with_z(ctx.world.sim().get_alt_approx(next_tgt.map(|e| e as i32)).unwrap_or(0.0)), *speed_factor)); + if npc.goto.map_or(true, |(tgt, _)| { + tgt.xy().distance_squared(next_tgt) > (step * 0.5).powi(2) + }) || npc.wpos.xy().distance_squared(next_tgt) < (step * 0.5).powi(2) + { + controller.goto = Some(( + next_tgt.with_z( + ctx.world + .sim() + .get_alt_approx(next_tgt.map(|e| e as i32)) + .unwrap_or(0.0), + ), + self.speed_factor, + )); } CONTINUE } @@ -367,181 +428,172 @@ impl Task for Goto { } #[derive(Clone, PartialEq)] -struct TravelToSite(SiteId); +pub struct TravelTo { + wpos: Vec2, + use_paths: bool, +} -impl Task for TravelToSite { - type State = (PathingMemory, TaskState); - type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); +pub enum TravelStage { + Goto(Vec2), + SiteToSite { + path: PathData<(Id, bool), SiteId>, + progress: usize, + }, + IntraSite { + path: PathData, Vec2>, + site: Id, + }, +} - fn begin<'a>(&self, (npc, ctx): &Self::Ctx<'a>) -> Self::State { - (PathingMemory::default(), TaskState::default()) +impl Task for TravelTo { + type Ctx<'a> = (NpcId, &'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); + type State = (VecDeque, TaskState); + + fn begin<'a>(&self, (_npc_id, npc, ctx): &Self::Ctx<'a>) -> Self::State { + if self.use_paths { + let a = npc.wpos.xy(); + let b = self.wpos; + + let data = ctx.state.data(); + let nearest_in_dir = |wpos: Vec2, end: Vec2| { + let dist = wpos.distance(end); + data.sites + .iter() + // TODO: faction.is_some() is currently used as a proxy for whether the site likely has paths, don't do this + .filter(|(site_id, site)| site.faction.is_some() && end.distance(site.wpos.as_()) < dist * 1.2) + .min_by_key(|(_, site)| site.wpos.as_().distance(wpos) as i32) + }; + if let Some((site_a, site_b)) = nearest_in_dir(a, b).zip(nearest_in_dir(b, a)) { + if site_a.0 != site_b.0 { + if let Some((path, progress)) = + path_towns(site_a.0, site_b.0, &ctx.state.data().sites, ctx.world) + { + return ( + [ + TravelStage::Goto(site_a.1.wpos.as_()), + TravelStage::SiteToSite { path, progress }, + TravelStage::Goto(b), + ] + .into_iter() + .collect(), + TaskState::default(), + ); + } + } + } + } + ( + [TravelStage::Goto(self.wpos)].into_iter().collect(), + TaskState::default(), + ) } fn run<'a>( &self, - (pathing, task_state): &mut Self::State, - (npc, ctx): &Self::Ctx<'a>, + (stages, task_state): &mut Self::State, + (npc_id, npc, ctx): &Self::Ctx<'a>, controller: &mut Controller, ) -> ControlFlow<()> { - if let Some(current_site) = npc.current_site { - if pathing.intersite_path.is_none() { - pathing.intersite_path = path_towns( - current_site, - self.0, - &ctx.state.data().sites, - ctx.world, - ); - if pathing.intersite_path.is_none() { - return FINISH; - } - } - } - - if let Some((ref mut path, site)) = pathing.intrasite_path { - // If the npc walking in a site and want to reroll (because the path was - // exhausted.) to try to find a complete path. - if path.repoll { - pathing.intrasite_path = - path_town(npc.wpos, site, ctx.index, |_| Some(path.end)) - .map(|path| (path, site)); - } - } - - if let Some((ref mut path, site)) = pathing.intrasite_path { - if let Some(next_tile) = path.path.front() { - match &ctx.index.sites.get(site).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - // Set the target to the next node in the path. - let wpos = site.tile_center_wpos(*next_tile); - task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 1.0), &(npc, ctx), controller)?; - path.path.pop_front(); - return CONTINUE; - }, - _ => {}, - } - } else { - // If the path is empty, we're done. - pathing.intrasite_path = None; - } - } - - if let Some((path, progress)) = { - if let Some((path, progress)) = &mut pathing.intersite_path { - if let Some((track_id, _)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - if *progress >= track.path().len() { - if path.repoll { - // Repoll if last path wasn't complete. - pathing.intersite_path = path_towns( - npc.current_site.unwrap(), - path.end, - &ctx.state.data().sites, - ctx.world, - ); - } else { - // Otherwise just take the next in the calculated path. - path.path.pop_front(); - *progress = 0; - } - } - } - } - &mut pathing.intersite_path - } { - if let Some((track_id, reversed)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - let get_progress = |progress: usize| { - if *reversed { - track.path().len().wrapping_sub(progress + 1) - } else { - progress - } - }; - - let transform_path_pos = |chunk_pos| { - let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); - if let Some(pathdata) = - ctx.world.sim().get_nearest_path(chunk_wpos) - { - pathdata.1.map(|e| e as i32) - } else { - chunk_wpos - } - }; - - // Loop through and skip nodes that are inside a site, and use intra - // site path finding there instead. - let walk_path = if let Some(chunk_pos) = - track.path().nodes.get(get_progress(*progress)) - { - if let Some((wpos, site_id, site)) = - ctx.world.sim().get(*chunk_pos).and_then(|chunk| { - let site_id = *chunk.sites.first()?; - let wpos = transform_path_pos(*chunk_pos); - match &ctx.index.sites.get(site_id).kind { - SiteKind::Refactor(site) - | SiteKind::CliffTown(site) - | SiteKind::DesertCity(site) => { - Some((wpos, site_id, site)) - }, - _ => None, - } - }) - { - if pathing.intrasite_path.is_none() { - let end = site.wpos_tile_pos(wpos); - pathing.intrasite_path = - path_town(npc.wpos, site_id, ctx.index, |_| { - Some(end) - }) - .map(|path| (path, site_id)); - } - if site.wpos_tile(wpos).is_obstacle() { - *progress += 1; - pathing.intrasite_path = None; - false - } else { - true - } - } else { - true - } - } else { - false - }; - - if walk_path { - // Find the next wpos on the path. - // NOTE: Consider not having this big gap between current - // position and next. For better path finding. Maybe that would - // mean having a float for progress. - let wpos = transform_path_pos( - track.path().nodes[get_progress(*progress)], - ); - task_state.perform(Goto(wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller)?; - *progress += 1; - return CONTINUE; - } - } else { - pathing.intersite_path = None; - } - } - - let world_site = |site_id: SiteId| { - let id = ctx.state.data().sites.get(site_id).and_then(|site| site.world_site)?; - ctx.world.civs().sites.recreate_id(id.id()) + let get_site2 = |site| match &ctx.index.sites.get(site).kind { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, }; - if let Some(site_wpos) = world_site(self.0) - .map(|home| TerrainChunkSize::center_wpos(ctx.world.civs().sites.get(home).center)) - { - if site_wpos.map(|e| e as f32 + 0.5).distance_squared(npc.wpos.xy()) < 16f32.powi(2) { - FINISH - } else { - task_state.perform(Goto(site_wpos.map(|e| e as f32 + 0.5), 0.8), &(npc, ctx), controller) + if let Some(stage) = stages.front_mut() { + match stage { + TravelStage::Goto(wpos) => { + task_state.perform(goto(*wpos), &(npc, ctx), controller)?; + stages.pop_front(); + }, + TravelStage::IntraSite { path, site } => { + if npc + .current_site + .and_then(|site| ctx.state.data().sites.get(site)?.world_site) + == Some(*site) + { + if let Some(next_tile) = path.path.front() { + task_state.perform( + Goto { + wpos: get_site2(*site) + .expect( + "intrasite path should only be started on a site2 site", + ) + .tile_center_wpos(*next_tile) + .as_() + + 0.5, + speed_factor: 0.6, + finish_dist: 1.0, + }, + &(npc, ctx), + controller, + )?; + path.path.pop_front(); + return CONTINUE; + } + } + task_state.perform(goto(self.wpos), &(npc, ctx), controller)?; + stages.pop_front(); + }, + TravelStage::SiteToSite { path, progress } => { + if let Some((track_id, reversed)) = path.path.front() { + let track = ctx.world.civs().tracks.get(*track_id); + if *progress >= track.path().len() { + // We finished this track section, move to the next one + path.path.pop_front(); + *progress = 0; + } else { + let next_node_idx = if *reversed { + track.path().len().saturating_sub(*progress + 1) + } else { + *progress + }; + let next_node = track.path().nodes[next_node_idx]; + + let transform_path_pos = |chunk_pos| { + let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); + if let Some(pathdata) = ctx.world.sim().get_nearest_path(chunk_wpos) + { + pathdata.1.map(|e| e as i32) + } else { + chunk_wpos + } + }; + + task_state.perform( + Goto { + wpos: transform_path_pos(next_node).as_() + 0.5, + speed_factor: 1.0, + finish_dist: 10.0, + }, + &(npc, ctx), + controller, + )?; + *progress += 1; + } + } else { + stages.pop_front(); + } + }, } + + if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { + let data = ctx.state.data(); + if let Some((site2, site)) = npc + .current_site + .and_then(|current_site| data.sites.get(current_site)) + .and_then(|site| site.world_site) + .and_then(|site| Some((get_site2(site)?, site))) + { + let end = site2.wpos_tile_pos(self.wpos.as_()); + if let Some(path) = path_town(npc.wpos, site, ctx.index, |_| Some(end)) { + stages.push_front(TravelStage::IntraSite { path, site }); + } + } + } + + CONTINUE } else { FINISH } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index ff8435f711..ec27cf38dd 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -262,11 +262,13 @@ impl<'a> System<'a> for Sys { agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); // TODO: // agent.rtsim_controller.heading_to = - // npc.pathing.intersite_path.as_ref().and_then(|(path, _)| { + // npc.pathing.intersite_path.as_ref(). + // and_then(|(path, _)| { // Some( // index // .sites - // .get(data.sites.get(path.end)?.world_site?) + // + // .get(data.sites.get(path.end)?.world_site?) // .name() // .to_string(), // ) diff --git a/world/src/site2/tile.rs b/world/src/site2/tile.rs index a02849f4fe..137038e9dd 100644 --- a/world/src/site2/tile.rs +++ b/world/src/site2/tile.rs @@ -25,9 +25,7 @@ impl Default for TileGrid { } impl TileGrid { - pub fn get(&self, tpos: Vec2) -> &Tile { - static EMPTY: Tile = Tile::empty(); - + pub fn get_known(&self, tpos: Vec2) -> Option<&Tile> { let tpos = tpos + TILE_RADIUS as i32; self.zones .get(tpos.map(|e| e.div_euclid(ZONE_SIZE as i32))) @@ -36,7 +34,11 @@ impl TileGrid { .get(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32))) }) .and_then(|tile| tile.as_ref()) - .unwrap_or(&EMPTY) + } + + pub fn get(&self, tpos: Vec2) -> &Tile { + static EMPTY: Tile = Tile::empty(); + self.get_known(tpos).unwrap_or(&EMPTY) } // WILL NOT EXPAND BOUNDS! From 0b4d3c9e20dffa6c5355532fa0bf74c505e5ee16 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 30 Oct 2022 00:12:28 +0100 Subject: [PATCH 040/139] Fixed scaling of airships --- common/src/states/utils.rs | 2 +- common/systems/src/phys.rs | 18 ++++++++++-------- server/src/state_ext.rs | 1 - voxygen/anim/src/ship/mod.rs | 3 ++- voxygen/src/scene/figure/mod.rs | 22 +++++++++++++++++----- 5 files changed, 30 insertions(+), 16 deletions(-) diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 68c8e06641..df50cd136b 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -220,7 +220,7 @@ impl Body { _ => 2.0, }, Body::Ship(ship) if ship.has_water_thrust() => 0.1, - Body::Ship(_) => 0.035, + Body::Ship(_) => 0.12, Body::Arthropod(_) => 3.5, } } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 6c738fa675..b1f2a63a2c 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1102,19 +1102,21 @@ impl<'a> PhysicsData<'a> { // TODO: Cache the matrices here to avoid recomputing - let transform_last_from = Mat4::::translation_3d( - previous_cache_other.pos.unwrap_or(*pos_other).0 - - previous_cache.pos.unwrap_or(Pos(wpos)).0, - ) * Mat4::from( - previous_cache_other.ori, - ) * Mat4::::translation_3d( - voxel_collider.translation, - ); + let transform_last_from = + Mat4::::translation_3d( + previous_cache_other.pos.unwrap_or(*pos_other).0 + - previous_cache.pos.unwrap_or(Pos(wpos)).0, + ) * Mat4::from(previous_cache_other.ori) + * Mat4::::scaling_3d(previous_cache_other.scale) + * Mat4::::translation_3d( + voxel_collider.translation, + ); let transform_last_to = transform_last_from.inverted(); let transform_from = Mat4::::translation_3d(pos_other.0 - wpos) * Mat4::from(ori_other.to_quat()) + * Mat4::::scaling_3d(previous_cache_other.scale) * Mat4::::translation_3d( voxel_collider.translation, ); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index b58167357e..e6a953013e 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -351,7 +351,6 @@ impl StateExt for State { .with(body.density()) .with(make_collider(ship)) .with(body) - .with(comp::Scale(comp::ship::AIRSHIP_SCALE)) .with(comp::Controller::default()) .with(Inventory::with_empty()) .with(comp::CharacterState::default()) diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs index 81c9b33f2c..d1d0c4e679 100644 --- a/voxygen/anim/src/ship/mod.rs +++ b/voxygen/anim/src/ship/mod.rs @@ -31,7 +31,8 @@ impl Skeleton for ShipSkeleton { buf: &mut [FigureBoneData; super::MAX_BONE_COUNT], body: Self::Body, ) -> Offsets { - let scale_mat = Mat4::scaling_3d(1.0 / 11.0); + // Ships are normal scale + let scale_mat = Mat4::scaling_3d(1.0); let bone0_mat = base_mat * scale_mat * Mat4::::from(self.bone0); diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 7d2bb1cb49..e7eeac8d0c 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -33,7 +33,7 @@ use common::{ comp::{ inventory::slot::EquipSlot, item::{tool::AbilityContext, Hands, ItemKind, ToolKind}, - Body, CharacterState, Collider, Controller, Health, Inventory, Item, ItemKey, Last, + ship, Body, CharacterState, Collider, Controller, Health, Inventory, Item, ItemKey, Last, LightAnimation, LightEmitter, Ori, PhysicsState, PoiseState, Pos, Scale, SkillSet, Stance, Vel, }, @@ -6709,10 +6709,22 @@ impl FigureMgr { } { let model_entry = model_entry?; - let figure_low_detail_distance = - figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.75; - let figure_mid_detail_distance = - figure_lod_render_distance * scale.map_or(1.0, |s| s.0) * 0.5; + let figure_low_detail_distance = figure_lod_render_distance + * if matches!(body, Body::Ship(_)) { + ship::AIRSHIP_SCALE + } else { + 1.0 + } + * scale.map_or(1.0, |s| s.0) + * 0.75; + let figure_mid_detail_distance = figure_lod_render_distance + * if matches!(body, Body::Ship(_)) { + ship::AIRSHIP_SCALE + } else { + 1.0 + } + * scale.map_or(1.0, |s| s.0) + * 0.5; let model = if pos.distance_squared(cam_pos) > figure_low_detail_distance.powi(2) { model_entry.lod_model(2) From 558b5f7c3a1472eda1feba9201a88f84503984c2 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 30 Oct 2022 21:11:30 +0000 Subject: [PATCH 041/139] Attempted generator-driven AI --- rtsim/src/data/npc.rs | 117 ++++++++++++++++++++++++++++++- rtsim/src/gen/mod.rs | 6 +- rtsim/src/lib.rs | 6 +- rtsim/src/rule/npc_ai.rs | 145 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 266 insertions(+), 8 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 13fb620fd2..9df7e31d46 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -10,12 +10,16 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ - any::Any, + any::{Any, TypeId}, collections::VecDeque, - ops::{ControlFlow, Deref, DerefMut}, + ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState}, + sync::{Arc, atomic::{AtomicPtr, Ordering}}, + pin::Pin, + marker::PhantomData, }; use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; +use crate::rule::npc_ai; #[derive(Copy, Clone, Default)] pub enum NpcMode { @@ -153,6 +157,110 @@ impl TaskState { } } +pub unsafe trait Context { + // TODO: Somehow we need to enforce this bound, I think? + // Hence, this trait is unsafe for now. + type Ty<'a>;// where for<'a> Self::Ty<'a>: 'a; +} + +pub struct Data(Arc>, PhantomData); + +impl Clone for Data { + fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) } +} + +impl Data { + pub fn with(&mut self, f: impl FnOnce(&mut C::Ty<'_>) -> R) -> R { + let ptr = self.0.swap(std::ptr::null_mut(), Ordering::Acquire); + if ptr.is_null() { + panic!("Data pointer was null, you probably tried to access data recursively") + } else { + // Safety: We have exclusive access to the pointer within this scope. + // TODO: Do we need a panic guard here? + let r = f(unsafe { &mut *(ptr as *mut C::Ty<'_>) }); + self.0.store(ptr, Ordering::Release); + r + } + } +} + +pub type Priority = usize; + +pub struct TaskBox { + task: Option<( + TypeId, + Box, Yield = A, Return = ()> + Unpin + Send + Sync>, + Priority, + )>, + data: Data, +} + +impl TaskBox { + pub fn new(data: Data) -> Self { + Self { + task: None, + data, + } + } + + #[must_use] + pub fn finish(&mut self, prio: Priority) -> ControlFlow { + if let Some((_, task, _)) = &mut self.task.as_mut().filter(|(_, _, p)| *p <= prio) { + match Pin::new(task).resume(self.data.clone()) { + GeneratorState::Yielded(action) => ControlFlow::Break(action), + GeneratorState::Complete(_) => { + self.task = None; + ControlFlow::Continue(()) + }, + } + } else { + ControlFlow::Continue(()) + } + } + + #[must_use] + pub fn perform, Yield = A, Return = ()> + Unpin + Any + Send + Sync>( + &mut self, + prio: Priority, + task: T, + ) -> ControlFlow { + let ty = TypeId::of::(); + if self.task.as_mut().filter(|(ty1, _, _)| *ty1 == ty).is_none() { + self.task = Some((ty, Box::new(task), prio)); + }; + + self.finish(prio) + } +} + +pub struct Brain { + task: Box, Yield = A, Return = !> + Unpin + Send + Sync>, + data: Data, +} + +impl Brain { + pub fn new, Yield = A, Return = !> + Unpin + Any + Send + Sync>(task: T) -> Self { + Self { + task: Box::new(task), + data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData), + } + } + + pub fn tick( + &mut self, + ctx_ref: &mut C::Ty<'_>, + ) -> A { + self.data.0.store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst); + match Pin::new(&mut self.task).resume(self.data.clone()) { + GeneratorState::Yielded(action) => { + self.data.0.store(std::ptr::null_mut(), Ordering::Release); + action + }, + GeneratorState::Complete(ret) => match ret {}, + } + } +} + #[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state @@ -181,6 +289,9 @@ pub struct Npc { #[serde(skip_serializing, skip_deserializing)] pub task_state: Option, + + #[serde(skip_serializing, skip_deserializing)] + pub brain: Option>>, } impl Clone for Npc { @@ -196,6 +307,7 @@ impl Clone for Npc { goto: Default::default(), mode: Default::default(), task_state: Default::default(), + brain: Default::default(), } } } @@ -215,6 +327,7 @@ impl Npc { goto: None, mode: NpcMode::Simulated, task_state: Default::default(), + brain: Some(npc_ai::brain()), } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index ee0ffe8b49..7ff58bd0b6 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -65,19 +65,19 @@ impl Data { ); // Spawn some test entities at the sites - for (site_id, site) in this.sites.iter() { + for (site_id, site) in this.sites.iter().take(1) { let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); wpos2d .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..20 { + for _ in 0..1 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { + .with_profession(match 1/*rng.gen_range(0..20)*/ { 0 => Profession::Hunter, 1 => Profession::Blacksmith, 2 => Profession::Chef, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 2b569f264e..db14aab7b5 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,8 +1,10 @@ #![feature( - explicit_generic_args_with_impl_trait, generic_associated_types, never_type, - try_blocks + try_blocks, + generator_trait, + generators, + trait_alias )] pub mod data; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 159dac08ae..aeeba1b070 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -2,7 +2,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ data::{ - npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH}, + npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH, TaskBox, Brain, Data, Context}, Sites, }, event::OnTick, @@ -241,6 +241,10 @@ impl Rule for NpcAi { .task_state .take() .unwrap_or_default(); + let mut brain = ctx.state.data_mut().npcs[npc_id] + .brain + .take() + .unwrap_or_else(brain); let (controller, task_state) = { let data = &*ctx.state.data(); @@ -295,6 +299,13 @@ impl Rule for NpcAi { )?; } } else { + brain.tick(&mut NpcData { + ctx: &ctx, + npc, + npc_id, + controller: &mut controller, + }); + /* // // Choose a random plaza in the npcs home site (which should be the // // current here) to go to. let task = @@ -327,6 +338,7 @@ impl Rule for NpcAi { .repeat(); task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; + */ } }; @@ -335,6 +347,7 @@ impl Rule for NpcAi { ctx.state.data_mut().npcs[npc_id].goto = controller.goto; ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state); + ctx.state.data_mut().npcs[npc_id].brain = Some(brain); } }); @@ -599,3 +612,133 @@ impl Task for TravelTo { } } } + +/* +let data = ctx.state.data(); +let site2 = + npc.home.and_then(|home| data.sites.get(home)).and_then( + |home| match &ctx.index.sites.get(home.world_site?).kind + { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + }, + ); + +let wpos = site2 + .and_then(|site2| { + let plaza = &site2.plots + [site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + .unwrap_or(npc.wpos.xy()); + +TravelTo { + wpos, + use_paths: true, +} +*/ + +trait IsTask = core::ops::Generator>, Yield = (), Return = ()> + Any + Send + Sync; + +pub struct NpcData<'a> { + ctx: &'a EventCtx<'a, NpcAi, OnTick>, + npc_id: NpcId, + npc: &'a Npc, + controller: &'a mut Controller, +} + +unsafe impl Context for NpcData<'static> { + type Ty<'a> = NpcData<'a>; +} + +pub fn brain() -> Brain> { + Brain::new(|mut data: Data| { + let mut task = TaskBox::<_, ()>::new(data.clone()); + + loop { + println!("Started"); + while let ControlFlow::Break(end) = task.finish(0) { + yield end; + } + + // Choose a new plaza in the NPC's home site to path towards + let path = data.with(|d| { + let data = d.ctx.state.data(); + + let current_site = data.sites.get(d.npc.current_site?)?; + let site2 = match &d.ctx.index.sites.get(current_site.world_site?).kind { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + }?; + + let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; + let end_wpos = site2.tile_center_wpos(plaza.root_tile()); + + if end_wpos.as_::().distance(d.npc.wpos.xy()) < 32.0 { + return None; + } + + let start = site2.wpos_tile_pos(d.npc.wpos.xy().as_()); + let end = site2.wpos_tile_pos(plaza.root_tile()); + + let path = match path_in_site(start, end, site2) { + PathResult::Path(path) => path, + _ => return None, + }; + println!("CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", path.len(), start, end, d.npc_id); + Some((current_site.world_site?, path)) + }); + + if let Some((site, path)) = path { + println!("Begin path"); + task.perform(0, walk_path(site, path)); + } else { + println!("No path, waiting..."); + for _ in 0..100 { + yield (); + } + println!("Waited."); + } + } + }) +} + +fn walk_path(site: Id, path: Path>) -> impl IsTask { + move |mut data: Data| { + for tile in path { + println!("TILE"); + let wpos = data.with(|d| match &d.ctx.index.sites.get(site).kind { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + } + .expect("intrasite path should only be started on a site2 site") + .tile_center_wpos(tile) + .as_() + + 0.5); + + println!("Walking to next tile... tile wpos = {:?} npc wpos = {:?}", wpos, data.with(|d| d.npc.wpos)); + while data.with(|d| d.npc.wpos.xy().distance_squared(wpos) > 2.0) { + data.with(|d| d.controller.goto = Some(( + wpos.with_z(d.ctx.world + .sim() + .get_alt_approx(wpos.map(|e| e as i32)) + .unwrap_or(0.0)), + 1.0, + ))); + yield (); + } + } + + println!("Waiting.."); + for _ in 0..100 { + yield (); + } + println!("Waited."); + } +} From acecc62d40667ceece7297cbe7ef763bcf36e880 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 1 Jan 2023 16:59:12 +0000 Subject: [PATCH 042/139] sync --- rtsim/src/data/npc.rs | 40 +++++++------ rtsim/src/rule/npc_ai.rs | 59 +++++++++++++------ server/src/rtsim2/tick.rs | 2 +- server/src/sys/agent.rs | 4 +- server/src/sys/agent/behavior_tree.rs | 2 +- .../sys/agent/behavior_tree/interaction.rs | 4 +- voxygen/src/scene/mod.rs | 2 +- 7 files changed, 68 insertions(+), 45 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 9df7e31d46..929980adae 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,3 +1,4 @@ +use crate::rule::npc_ai; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, @@ -12,14 +13,16 @@ use slotmap::HopSlotMap; use std::{ any::{Any, TypeId}, collections::VecDeque, - ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState}, - sync::{Arc, atomic::{AtomicPtr, Ordering}}, - pin::Pin, marker::PhantomData, + ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState}, + pin::Pin, + sync::{ + atomic::{AtomicPtr, Ordering}, + Arc, + }, }; use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; -use crate::rule::npc_ai; #[derive(Copy, Clone, Default)] pub enum NpcMode { @@ -160,7 +163,7 @@ impl TaskState { pub unsafe trait Context { // TODO: Somehow we need to enforce this bound, I think? // Hence, this trait is unsafe for now. - type Ty<'a>;// where for<'a> Self::Ty<'a>: 'a; + type Ty<'a>; // where for<'a> Self::Ty<'a>: 'a; } pub struct Data(Arc>, PhantomData); @@ -196,12 +199,7 @@ pub struct TaskBox { } impl TaskBox { - pub fn new(data: Data) -> Self { - Self { - task: None, - data, - } - } + pub fn new(data: Data) -> Self { Self { task: None, data } } #[must_use] pub fn finish(&mut self, prio: Priority) -> ControlFlow { @@ -225,7 +223,12 @@ impl TaskBox { task: T, ) -> ControlFlow { let ty = TypeId::of::(); - if self.task.as_mut().filter(|(ty1, _, _)| *ty1 == ty).is_none() { + if self + .task + .as_mut() + .filter(|(ty1, _, _)| *ty1 == ty) + .is_none() + { self.task = Some((ty, Box::new(task), prio)); }; @@ -239,18 +242,19 @@ pub struct Brain { } impl Brain { - pub fn new, Yield = A, Return = !> + Unpin + Any + Send + Sync>(task: T) -> Self { + pub fn new, Yield = A, Return = !> + Unpin + Any + Send + Sync>( + task: T, + ) -> Self { Self { task: Box::new(task), data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData), } } - pub fn tick( - &mut self, - ctx_ref: &mut C::Ty<'_>, - ) -> A { - self.data.0.store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst); + pub fn tick(&mut self, ctx_ref: &mut C::Ty<'_>) -> A { + self.data + .0 + .store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst); match Pin::new(&mut self.task).resume(self.data.clone()) { GeneratorState::Yielded(action) => { self.data.0.store(std::ptr::null_mut(), Ordering::Release); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index aeeba1b070..0702b1165c 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -2,7 +2,10 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ data::{ - npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH, TaskBox, Brain, Data, Context}, + npc::{ + Brain, Context, Controller, Data, Npc, NpcId, PathData, PathingMemory, Task, TaskBox, + TaskState, CONTINUE, FINISH, + }, Sites, }, event::OnTick, @@ -59,7 +62,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes TileKind::Empty => 3.0, TileKind::Hazard(_) => 50.0, TileKind::Field => 8.0, - TileKind::Plaza | TileKind::Road { .. } => 1.0, + TileKind::Plaza | TileKind::Road { .. } | TileKind::Path => 1.0, TileKind::Building | TileKind::Castle @@ -640,7 +643,8 @@ TravelTo { } */ -trait IsTask = core::ops::Generator>, Yield = (), Return = ()> + Any + Send + Sync; +trait IsTask = + core::ops::Generator>, Yield = (), Return = ()> + Any + Send + Sync; pub struct NpcData<'a> { ctx: &'a EventCtx<'a, NpcAi, OnTick>, @@ -689,7 +693,13 @@ pub fn brain() -> Brain> { PathResult::Path(path) => path, _ => return None, }; - println!("CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", path.len(), start, end, d.npc_id); + println!( + "CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", + path.len(), + start, + end, + d.npc_id + ); Some((current_site.world_site?, path)) }); @@ -711,26 +721,37 @@ fn walk_path(site: Id, path: Path>) -> impl IsTask { move |mut data: Data| { for tile in path { println!("TILE"); - let wpos = data.with(|d| match &d.ctx.index.sites.get(site).kind { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - } + let wpos = data.with(|d| { + match &d.ctx.index.sites.get(site).kind { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + } .expect("intrasite path should only be started on a site2 site") .tile_center_wpos(tile) .as_() - + 0.5); + + 0.5 + }); - println!("Walking to next tile... tile wpos = {:?} npc wpos = {:?}", wpos, data.with(|d| d.npc.wpos)); + println!( + "Walking to next tile... tile wpos = {:?} npc wpos = {:?}", + wpos, + data.with(|d| d.npc.wpos) + ); while data.with(|d| d.npc.wpos.xy().distance_squared(wpos) > 2.0) { - data.with(|d| d.controller.goto = Some(( - wpos.with_z(d.ctx.world - .sim() - .get_alt_approx(wpos.map(|e| e as i32)) - .unwrap_or(0.0)), - 1.0, - ))); + data.with(|d| { + d.controller.goto = Some(( + wpos.with_z( + d.ctx + .world + .sim() + .get_alt_approx(wpos.map(|e| e as i32)) + .unwrap_or(0.0), + ), + 1.0, + )) + }); yield (); } } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index ec27cf38dd..03e4327ab9 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -267,7 +267,7 @@ impl<'a> System<'a> for Sys { // Some( // index // .sites - // + // // .get(data.sites.get(path.end)?.world_site?) // .name() // .to_string(), diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index bcea774b24..e0c3d3deff 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -2,8 +2,8 @@ pub mod behavior_tree; pub use server_agent::{action_nodes, attack, consts, data, util}; use crate::sys::agent::{ - behavior_tree::{BehaviorData, BehaviorTree}, - data::{AgentData, ReadData}, + behavior_tree::{BehaviorData, BehaviorTree}, + data::{AgentData, ReadData}, }; use common::{ comp::{ diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 261f94034e..6a8cad8ef6 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -1,4 +1,3 @@ -use common::rtsim::RtSimEntity; use common::{ comp::{ agent::{ @@ -10,6 +9,7 @@ use common::{ }, event::{Emitter, ServerEvent}, path::TraversalConfig, + rtsim::RtSimEntity, }; use rand::{prelude::ThreadRng, thread_rng, Rng}; use specs::{ diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 86acde2c6d..e438fe9d86 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -15,9 +15,7 @@ use common::{ use rand::{thread_rng, Rng}; use specs::saveload::Marker; -use crate::{ - sys::agent::util::get_entity_by_id, -}; +use crate::sys::agent::util::get_entity_by_id; use super::{BehaviorData, BehaviorTree}; diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 08c031a542..87129c41d0 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -636,7 +636,7 @@ impl Scene { Vec3::unit_z() * (up * viewpoint_scale - tilt.min(0.0).sin() * dist * 0.6) } else { self.figure_mgr - .viewpoint_offset(scene_data, scene_data.viewpoint_entity) * viewpoint_scale + .viewpoint_offset(scene_data, scene_data.viewpoint_entity) }; match self.camera.get_mode() { From b2f92e4a6c9dcc6487074634f69ab3ddc27521c1 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 4 Jan 2023 11:25:39 +0000 Subject: [PATCH 043/139] Switch to combinator-driven NPC AI API --- rtsim/src/data/npc.rs | 441 ++++++++++++++++++------------- rtsim/src/gen/mod.rs | 4 +- rtsim/src/lib.rs | 4 +- rtsim/src/rule/npc_ai.rs | 120 ++++++++- server/agent/src/action_nodes.rs | 7 +- 5 files changed, 376 insertions(+), 200 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 929980adae..d0ee7b90a2 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,4 +1,4 @@ -use crate::rule::npc_ai; +use crate::rule::npc_ai::NpcCtx; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, @@ -50,221 +50,295 @@ pub struct Controller { pub goto: Option<(Vec3, f32)>, } -#[derive(Default)] -pub struct TaskState { - state: Option>, +impl Controller { + pub fn idle() -> Self { Self { goto: None } } } -pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(()); -pub const FINISH: ControlFlow<()> = ControlFlow::Continue(()); +pub trait Action: Any + Send + Sync { + /// Returns `true` if the action should be considered the 'same' (i.e: + /// achieving the same objective) as another. In general, the AI system + /// will try to avoid switching (and therefore restarting) tasks when the + /// new task is the 'same' as the old one. + // TODO: Figure out a way to compare actions based on their 'intention': i.e: + // two pathing actions should be considered equivalent if their destination + // is the same regardless of the progress they've each made. + fn is_same(&self, other: &Self) -> bool + where + Self: Sized; + fn dyn_is_same_sized(&self, other: &dyn Action) -> bool + where + Self: Sized, + { + match (other as &dyn Any).downcast_ref::() { + Some(other) => self.is_same(other), + None => false, + } + } + fn dyn_is_same(&self, other: &dyn Action) -> bool; + // Reset the action to its initial state so it can be restarted + fn reset(&mut self); -pub trait Task: PartialEq + Clone + Send + Sync + 'static { - type State: Send + Sync; - type Ctx<'a>; + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow; - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; - - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()>; - - fn then(self, other: B) -> Then { Then(self, other) } - - fn repeat(self) -> Repeat { Repeat(self) } + fn then, R1>(self, other: A1) -> Then + where + Self: Sized, + { + Then { + a0: self, + a0_finished: false, + a1: other, + phantom: PhantomData, + } + } + fn repeat(self) -> Repeat + where + Self: Sized, + { + Repeat(self, PhantomData) + } + fn stop_if bool>(self, f: F) -> StopIf + where + Self: Sized, + { + StopIf(self, f) + } + fn map R1, R1>(self, f: F) -> Map + where + Self: Sized, + { + Map(self, f, PhantomData) + } } -#[derive(Clone, PartialEq)] -pub struct Then(A, B); +// Now -impl Task for Then -where - B: for<'a> Task = A::Ctx<'a>>, +#[derive(Copy, Clone)] +pub struct Now(F, Option); + +impl A + Send + Sync + 'static, A: Action> + Action for Now { - // TODO: Use `Either` instead - type Ctx<'a> = A::Ctx<'a>; - type State = Result; + // TODO: This doesn't compare?! + fn is_same(&self, other: &Self) -> bool { true } - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { Ok(self.0.begin(ctx)) } + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - match state { - Ok(a_state) => { - self.0.run(a_state, ctx, controller)?; - *state = Err(self.1.begin(ctx)); - CONTINUE - }, - Err(b_state) => self.1.run(b_state, ctx, controller), - } + fn reset(&mut self) { self.1 = None; } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + (self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx) } } -#[derive(Clone, PartialEq)] -pub struct Repeat(A); - -impl Task for Repeat { - type Ctx<'a> = A::Ctx<'a>; - type State = A::State; - - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { self.0.begin(ctx) } - - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - self.0.run(state, ctx, controller)?; - *state = self.0.begin(ctx); - CONTINUE - } +pub fn now(f: F) -> Now +where + F: FnMut(&mut NpcCtx) -> A, +{ + Now(f, None) } -impl TaskState { - pub fn perform<'a, T: Task>( - &mut self, - task: T, - ctx: &T::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - type StateOf = (T, ::State); +// Just - let mut state = if let Some(state) = self.state.take().and_then(|state| { - state - .downcast::>() - .ok() - .filter(|state| state.0 == task) - }) { - state - } else { - let mut state = task.begin(ctx); - Box::new((task, state)) - }; +#[derive(Copy, Clone)] +pub struct Just(F, PhantomData); - let res = state.0.run(&mut state.1, ctx, controller); +impl R + Send + Sync + 'static> Action + for Just +{ + fn is_same(&self, other: &Self) -> bool { true } - self.state = if matches!(res, ControlFlow::Break(())) { - Some(state) - } else { - None - }; + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - res - } + fn reset(&mut self) {} + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { ControlFlow::Break((self.0)(ctx)) } } -pub unsafe trait Context { - // TODO: Somehow we need to enforce this bound, I think? - // Hence, this trait is unsafe for now. - type Ty<'a>; // where for<'a> Self::Ty<'a>: 'a; +pub fn just(mut f: F) -> Just +where + F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, +{ + Just(f, PhantomData) } -pub struct Data(Arc>, PhantomData); - -impl Clone for Data { - fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) } -} - -impl Data { - pub fn with(&mut self, f: impl FnOnce(&mut C::Ty<'_>) -> R) -> R { - let ptr = self.0.swap(std::ptr::null_mut(), Ordering::Acquire); - if ptr.is_null() { - panic!("Data pointer was null, you probably tried to access data recursively") - } else { - // Safety: We have exclusive access to the pointer within this scope. - // TODO: Do we need a panic guard here? - let r = f(unsafe { &mut *(ptr as *mut C::Ty<'_>) }); - self.0.store(ptr, Ordering::Release); - r - } - } -} +// Tree pub type Priority = usize; -pub struct TaskBox { - task: Option<( - TypeId, - Box, Yield = A, Return = ()> + Unpin + Send + Sync>, - Priority, - )>, - data: Data, +const URGENT: Priority = 0; +const CASUAL: Priority = 1; + +pub struct Node(Box>, Priority); + +pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } +pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } + +pub struct Tree { + next: F, + prev: Option>, + interrupt: bool, } -impl TaskBox { - pub fn new(data: Data) -> Self { Self { task: None, data } } +impl Node + Send + Sync + 'static, R: 'static> Action + for Tree +{ + fn is_same(&self, other: &Self) -> bool { true } - #[must_use] - pub fn finish(&mut self, prio: Priority) -> ControlFlow { - if let Some((_, task, _)) = &mut self.task.as_mut().filter(|(_, _, p)| *p <= prio) { - match Pin::new(task).resume(self.data.clone()) { - GeneratorState::Yielded(action) => ControlFlow::Break(action), - GeneratorState::Complete(_) => { - self.task = None; - ControlFlow::Continue(()) - }, - } - } else { - ControlFlow::Continue(()) - } - } + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - #[must_use] - pub fn perform, Yield = A, Return = ()> + Unpin + Any + Send + Sync>( - &mut self, - prio: Priority, - task: T, - ) -> ControlFlow { - let ty = TypeId::of::(); - if self - .task - .as_mut() - .filter(|(ty1, _, _)| *ty1 == ty) - .is_none() - { - self.task = Some((ty, Box::new(task), prio)); + fn reset(&mut self) { self.prev = None; } + + // TODO: Reset `next` too? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + let new = (self.next)(ctx); + + let prev = match &mut self.prev { + Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => { + prev + }, + _ => self.prev.insert(new), }; - self.finish(prio) - } -} - -pub struct Brain { - task: Box, Yield = A, Return = !> + Unpin + Send + Sync>, - data: Data, -} - -impl Brain { - pub fn new, Yield = A, Return = !> + Unpin + Any + Send + Sync>( - task: T, - ) -> Self { - Self { - task: Box::new(task), - data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData), - } - } - - pub fn tick(&mut self, ctx_ref: &mut C::Ty<'_>) -> A { - self.data - .0 - .store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst); - match Pin::new(&mut self.task).resume(self.data.clone()) { - GeneratorState::Yielded(action) => { - self.data.0.store(std::ptr::null_mut(), Ordering::Release); - action + match prev.0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(r) => { + self.prev = None; + ControlFlow::Break(r) }, - GeneratorState::Complete(ret) => match ret {}, } } } +pub fn choose(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: false, + } +} + +pub fn watch(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: true, + } +} + +// Then + +#[derive(Copy, Clone)] +pub struct Then { + a0: A0, + a0_finished: bool, + a1: A1, + phantom: PhantomData, +} + +impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync + 'static> + Action for Then +{ + fn is_same(&self, other: &Self) -> bool { + self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1) + } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { + self.a0.reset(); + self.a0_finished = false; + self.a1.reset(); + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + if !self.a0_finished { + match self.a0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(_) => self.a0_finished = true, + } + } + self.a1.tick(ctx) + } +} + +// Repeat + +#[derive(Copy, Clone)] +pub struct Repeat(A, PhantomData); + +impl> Action for Repeat { + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + match self.0.tick(ctx) { + ControlFlow::Continue(()) => ControlFlow::Continue(()), + ControlFlow::Break(_) => { + self.0.reset(); + ControlFlow::Continue(()) + }, + } + } +} + +// StopIf + +#[derive(Copy, Clone)] +pub struct StopIf(A, F); + +impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action> + for StopIf +{ + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { + if (self.1)(ctx) { + ControlFlow::Break(None) + } else { + self.0.tick(ctx).map_break(Some) + } + } +} + +// Map + +#[derive(Copy, Clone)] +pub struct Map(A, F, PhantomData); + +impl, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1> + Action for Map +{ + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + self.0.tick(ctx).map_break(&mut self.1) + } +} + +pub struct Brain { + pub(crate) action: Box>, +} + #[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state @@ -292,10 +366,7 @@ pub struct Npc { pub mode: NpcMode, #[serde(skip_serializing, skip_deserializing)] - pub task_state: Option, - - #[serde(skip_serializing, skip_deserializing)] - pub brain: Option>>, + pub brain: Option, } impl Clone for Npc { @@ -310,7 +381,6 @@ impl Clone for Npc { current_site: Default::default(), goto: Default::default(), mode: Default::default(), - task_state: Default::default(), brain: Default::default(), } } @@ -330,8 +400,7 @@ impl Npc { current_site: None, goto: None, mode: NpcMode::Simulated, - task_state: Default::default(), - brain: Some(npc_ai::brain()), + brain: None, } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 7ff58bd0b6..49f34c8c0f 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -72,12 +72,12 @@ impl Data { .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..1 { + for _ in 0..10 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match 1/*rng.gen_range(0..20)*/ { + .with_profession(match rng.gen_range(0..20) { 0 => Profession::Hunter, 1 => Profession::Blacksmith, 2 => Profession::Chef, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index db14aab7b5..dcf24c9e76 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -4,7 +4,9 @@ try_blocks, generator_trait, generators, - trait_alias + trait_alias, + trait_upcasting, + control_flow_enum )] pub mod data; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 0702b1165c..798670a5fe 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -3,8 +3,8 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ data::{ npc::{ - Brain, Context, Controller, Data, Npc, NpcId, PathData, PathingMemory, Task, TaskBox, - TaskState, CONTINUE, FINISH, + casual, choose, just, now, urgent, Action, Brain, Controller, Npc, NpcId, PathData, + PathingMemory, }, Sites, }, @@ -240,21 +240,27 @@ impl Rule for NpcAi { let npc_ids = ctx.state.data().npcs.keys().collect::>(); for npc_id in npc_ids { - let mut task_state = ctx.state.data_mut().npcs[npc_id] - .task_state - .take() - .unwrap_or_default(); let mut brain = ctx.state.data_mut().npcs[npc_id] .brain .take() - .unwrap_or_else(brain); + .unwrap_or_else(|| Brain { + action: Box::new(think().repeat()), + }); - let (controller, task_state) = { + let controller = { let data = &*ctx.state.data(); let npc = &data.npcs[npc_id]; let mut controller = Controller { goto: npc.goto }; + brain.action.tick(&mut NpcCtx { + ctx: &ctx, + npc, + npc_id, + controller: &mut controller, + }); + + /* let action: ControlFlow<()> = try { if matches!(npc.profession, Some(Profession::Adventurer(_))) { if let Some(home) = npc.home { @@ -344,12 +350,12 @@ impl Rule for NpcAi { */ } }; + */ - (controller, task_state) + controller }; ctx.state.data_mut().npcs[npc_id].goto = controller.goto; - ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state); ctx.state.data_mut().npcs[npc_id].brain = Some(brain); } }); @@ -358,6 +364,99 @@ impl Rule for NpcAi { } } +pub struct NpcCtx<'a> { + ctx: &'a EventCtx<'a, NpcAi, OnTick>, + npc_id: NpcId, + npc: &'a Npc, + controller: &'a mut Controller, +} + +fn idle() -> impl Action + Clone { just(|ctx| *ctx.controller = Controller::idle()) } + +fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { + const STEP_DIST: f32 = 10.0; + just(move |ctx| { + let rpos = wpos - ctx.npc.wpos; + let len = rpos.magnitude(); + ctx.controller.goto = Some(( + ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST), + speed_factor, + )); + }) +} + +fn goto(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { + const MIN_DIST: f32 = 1.0; + + move_toward(wpos, speed_factor) + .repeat() + .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < MIN_DIST.powi(2)) + .map(|_| {}) +} + +// Seconds +fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { + let end = ctx.ctx.event.time.0 + time; + move |ctx| ctx.ctx.event.time.0 > end +} + +fn think() -> impl Action { + choose(|ctx| { + if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { + // Choose a random site that's fairly close by + let site_wpos2d = ctx + .ctx + .state + .data() + .sites + .iter() + .filter(|(site_id, site)| { + site.faction.is_some() + && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) + && thread_rng().gen_bool(0.25) + }) + .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id) + .and_then(|tgt_site| { + ctx.ctx + .state + .data() + .sites + .get(tgt_site) + .map(|site| site.wpos) + }); + + if let Some(site_wpos2d) = site_wpos2d { + // Walk toward the site + casual(goto( + site_wpos2d.map(|e| e as f32 + 0.5).with_z( + ctx.ctx + .world + .sim() + .get_alt_approx(site_wpos2d.as_()) + .unwrap_or(0.0), + ), + 1.0, + )) + } else { + casual(idle()) + } + } else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) { + casual(idle()) + } else { + casual( + now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0)) + .then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0))) + .repeat() + .stop_if(timeout(ctx, 10.0)) + .then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0)))) + .map(|_| {}), + ) + } + }) +} + +/* #[derive(Clone)] pub struct Generate(F, PhantomData); @@ -763,3 +862,4 @@ fn walk_path(site: Id, path: Path>) -> impl IsTask { println!("Waited."); } } +*/ diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 2509c2ac3e..e4907eb356 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -229,11 +229,16 @@ impl<'a> AgentData<'a> { controller.push_cancel_input(InputKind::Fly) } + let chase_tgt = *travel_to/*read_data.terrain + .try_find_space(travel_to.as_()) + .map(|pos| pos.as_()) + .unwrap_or(*travel_to)*/; + if let Some((bearing, speed)) = agent.chaser.chase( &*read_data.terrain, self.pos.0, self.vel.0, - *travel_to, + chase_tgt, TraversalConfig { min_tgt_dist: 1.25, ..self.traversal_config From 9413a56c13f921e4c061ebde22708145ccc1aaf5 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 01:50:51 +0000 Subject: [PATCH 044/139] Added sequence combinator, NPC site-site pathfinding --- rtsim/src/data/npc.rs | 82 ++++++++++++++++++++++++++++++ rtsim/src/lib.rs | 3 +- rtsim/src/rule/npc_ai.rs | 106 +++++++++++++++++++++++++++------------ 3 files changed, 158 insertions(+), 33 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index d0ee7b90a2..8e5e6b0085 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -109,6 +109,28 @@ pub trait Action: Any + Send + Sync { { Map(self, f, PhantomData) } + fn boxed(self) -> Box> + where + Self: Sized, + { + Box::new(self) + } +} + +impl Action for Box> { + fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { + match (other as &dyn Any).downcast_ref::() { + Some(other) => self.is_same(other), + None => false, + } + } + + fn reset(&mut self) { (**self).reset(); } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { (**self).tick(ctx) } } // Now @@ -164,6 +186,23 @@ where Just(f, PhantomData) } +// Finish + +#[derive(Copy, Clone)] +pub struct Finish; + +impl Action<()> for Finish { + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) {} + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } +} + +pub fn finish() -> Finish { Finish } + // Tree pub type Priority = usize; @@ -293,6 +332,49 @@ impl> Action for Repeat { } } +// Sequence + +#[derive(Copy, Clone)] +pub struct Sequence(I, I, Option, PhantomData); + +impl + Clone + Send + Sync + 'static, A: Action> + Action<()> for Sequence +{ + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { + self.0 = self.1.clone(); + self.2 = None; + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { + let item = if let Some(prev) = &mut self.2 { + prev + } else { + match self.0.next() { + Some(next) => self.2.insert(next), + None => return ControlFlow::Break(()), + } + }; + + if let ControlFlow::Break(_) = item.tick(ctx) { + self.2 = None; + } + + ControlFlow::Continue(()) + } +} + +pub fn seq(iter: I) -> Sequence +where + I: Iterator + Clone, + A: Action, +{ + Sequence(iter.clone(), iter, None, PhantomData) +} + // StopIf #[derive(Copy, Clone)] diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index dcf24c9e76..6252d7d0aa 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -6,7 +6,8 @@ generators, trait_alias, trait_upcasting, - control_flow_enum + control_flow_enum, + let_chains )] pub mod data; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 798670a5fe..447fe21db1 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -3,8 +3,8 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ data::{ npc::{ - casual, choose, just, now, urgent, Action, Brain, Controller, Npc, NpcId, PathData, - PathingMemory, + casual, choose, finish, just, now, seq, urgent, watch, Action, Brain, Controller, Npc, + NpcId, PathData, PathingMemory, }, Sites, }, @@ -371,10 +371,16 @@ pub struct NpcCtx<'a> { controller: &'a mut Controller, } -fn idle() -> impl Action + Clone { just(|ctx| *ctx.controller = Controller::idle()) } +fn pass() -> impl Action { just(|ctx| {}) } -fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { - const STEP_DIST: f32 = 10.0; +fn idle_wait() -> impl Action { + just(|ctx| *ctx.controller = Controller::idle()) + .repeat() + .map(|_| ()) +} + +fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action { + const STEP_DIST: f32 = 16.0; just(move |ctx| { let rpos = wpos - ctx.npc.wpos; let len = rpos.magnitude(); @@ -385,8 +391,8 @@ fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { }) } -fn goto(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { - const MIN_DIST: f32 = 1.0; +fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { + const MIN_DIST: f32 = 2.0; move_toward(wpos, speed_factor) .repeat() @@ -394,6 +400,61 @@ fn goto(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { .map(|_| {}) } +fn goto_2d(wpos2d: Vec2, speed_factor: f32) -> impl Action { + const MIN_DIST: f32 = 2.0; + + now(move |ctx| { + let wpos = wpos2d.with_z( + ctx.ctx + .world + .sim() + .get_alt_approx(wpos2d.as_()) + .unwrap_or(0.0), + ); + goto(wpos, speed_factor) + }) +} + +fn path_to_site(tgt_site: SiteId) -> impl Action { + now(move |ctx| { + let sites = &ctx.ctx.state.data().sites; + + // If we can, try to find a path to the site via tracks + if let Some(current_site) = ctx.npc.current_site + && let Some((mut tracks, _)) = path_towns(current_site, tgt_site, sites, ctx.ctx.world) + { + seq(tracks + .path + .into_iter() + .map(|(track_id, reversed)| now(move |ctx| { + let track_len = ctx.ctx.world.civs().tracks.get(track_id).path().len(); + seq(if reversed { + itertools::Either::Left((0..track_len).rev()) + } else { + itertools::Either::Right(0..track_len) + } + .map(move |node_idx| now(move |ctx| { + let track = ctx.ctx.world.civs().tracks.get(track_id); + let next_node = track.path().nodes[node_idx]; + + let chunk_wpos = TerrainChunkSize::center_wpos(next_node); + let path_wpos2d = ctx.ctx.world.sim() + .get_nearest_path(chunk_wpos) + .map_or(chunk_wpos, |(_, wpos, _, _)| wpos.as_()); + + goto_2d(path_wpos2d.as_(), 1.0) + }))) + }))) + .boxed() + } else if let Some(site) = sites.get(tgt_site) { + // If all else fails, just walk toward the site in a straight line + goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0).boxed() + } else { + pass().boxed() + } + }) +} + // Seconds fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { let end = ctx.ctx.event.time.0 + time; @@ -404,7 +465,7 @@ fn think() -> impl Action { choose(|ctx| { if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { // Choose a random site that's fairly close by - let site_wpos2d = ctx + if let Some(tgt_site) = ctx .ctx .state .data() @@ -417,39 +478,20 @@ fn think() -> impl Action { }) .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .map(|(site_id, _)| site_id) - .and_then(|tgt_site| { - ctx.ctx - .state - .data() - .sites - .get(tgt_site) - .map(|site| site.wpos) - }); - - if let Some(site_wpos2d) = site_wpos2d { - // Walk toward the site - casual(goto( - site_wpos2d.map(|e| e as f32 + 0.5).with_z( - ctx.ctx - .world - .sim() - .get_alt_approx(site_wpos2d.as_()) - .unwrap_or(0.0), - ), - 1.0, - )) + { + casual(path_to_site(tgt_site)) } else { - casual(idle()) + casual(pass()) } } else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) { - casual(idle()) + casual(idle_wait()) } else { casual( now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0)) .then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0))) .repeat() .stop_if(timeout(ctx, 10.0)) - .then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0)))) + .then(now(|ctx| idle_wait().stop_if(timeout(ctx, 5.0)))) .map(|_| {}), ) } From 8f7b11f12f1dfb2127713a4138e7aadd4dc9b4f1 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 03:08:00 +0000 Subject: [PATCH 045/139] Improved AI code structure, added docs --- rtsim/src/ai/mod.rs | 583 +++++++++++++++++++++++++++++++++++ rtsim/src/data/npc.rs | 369 +--------------------- rtsim/src/lib.rs | 1 + rtsim/src/rule/npc_ai.rs | 651 +++++++-------------------------------- 4 files changed, 689 insertions(+), 915 deletions(-) create mode 100644 rtsim/src/ai/mod.rs diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs new file mode 100644 index 0000000000..642240fb19 --- /dev/null +++ b/rtsim/src/ai/mod.rs @@ -0,0 +1,583 @@ +use crate::{ + data::npc::{Controller, Npc, NpcId}, + RtState, +}; +use common::resources::{Time, TimeOfDay}; +use std::{any::Any, marker::PhantomData, ops::ControlFlow}; +use world::{IndexRef, World}; + +/// 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 +/// through this struct. +pub struct NpcCtx<'a> { + pub state: &'a RtState, + pub world: &'a World, + pub index: IndexRef<'a>, + + pub time_of_day: TimeOfDay, + pub time: Time, + + pub npc_id: NpcId, + pub npc: &'a Npc, + pub controller: &'a mut Controller, +} + +/// A trait that describes 'actions': long-running tasks performed by rtsim +/// NPCs. These can be as simple as walking in a straight line between two +/// locations or as complex as taking part in an adventure with players or +/// performing an entire daily work schedule. +/// +/// Actions are built up from smaller sub-actions via the combinator methods +/// defined on this trait, and with the standalone functions in this module. +/// Using these combinators, in a similar manner to using the [`Iterator`] API, +/// it is possible to construct arbitrarily complex actions including behaviour +/// trees (see [`choose`] and [`watch`]) and other forms of moment-by-moment +/// decision-making. +/// +/// On completion, actions may produce a value, denoted by the type parameter +/// `R`. For example, an action may communicate whether it was successful or +/// unsuccessful through this completion value. +/// +/// 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 +/// dev team first. +pub trait Action: Any + Send + Sync { + /// Returns `true` if the action should be considered the 'same' (i.e: + /// achieving the same objective) as another. In general, the AI system + /// will try to avoid switching (and therefore restarting) tasks when the + /// new task is the 'same' as the old one. + // TODO: Figure out a way to compare actions based on their 'intention': i.e: + // two pathing actions should be considered equivalent if their destination + // is the same regardless of the progress they've each made. + fn is_same(&self, other: &Self) -> bool + where + Self: Sized; + + /// Like [`Action::is_same`], but allows for dynamic dispatch. + fn dyn_is_same_sized(&self, other: &dyn Action) -> bool + where + Self: Sized, + { + match (other as &dyn Any).downcast_ref::() { + Some(other) => self.is_same(other), + None => false, + } + } + + /// Like [`Action::is_same`], but allows for dynamic dispatch. + fn dyn_is_same(&self, other: &dyn Action) -> bool; + + /// Reset the action to its initial state such that it can be repeated. + fn reset(&mut self); + + /// Perform the action for the current tick. + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow; + + /// Create an action that chains together two sub-actions, one after the + /// other. + /// + /// # Example + /// + /// ```ignore + /// // Walk toward an enemy NPC and, once done, attack the enemy NPC + /// goto(enemy_npc).then(attack(enemy_npc)) + /// ``` + fn then, R1>(self, other: A1) -> Then + where + Self: Sized, + { + Then { + a0: self, + a0_finished: false, + a1: other, + phantom: PhantomData, + } + } + + /// Create an action that repeats a sub-action indefinitely. + /// + /// # Example + /// + /// ```ignore + /// // Endlessly collect flax from the environment + /// find_and_collect(ChunkResource::Flax).repeat() + /// ``` + fn repeat(self) -> Repeat + where + Self: Sized, + { + Repeat(self, PhantomData) + } + + /// Stop the sub-action suddenly if a condition is reached. + /// + /// # Example + /// + /// ```ignore + /// // Keep going on adventures until your 111th birthday + /// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0) + /// ``` + fn stop_if bool>(self, f: F) -> StopIf + where + Self: Sized, + { + StopIf(self, f) + } + + /// Map the completion value of this action to something else. + fn map R1, R1>(self, f: F) -> Map + where + Self: Sized, + { + Map(self, f, PhantomData) + } + + /// Box the action. Often used to perform type erasure, such as when you + /// want to return one of many actions (each with different types) from + /// the same function. + /// + /// # Example + /// + /// ```ignore + /// // Error! Type mismatch between branches + /// if npc.is_too_tired() { + /// goto(npc.home) + /// } else { + /// go_on_an_adventure() + /// } + /// + /// // All fine + /// if npc.is_too_tired() { + /// goto(npc.home).boxed() + /// } else { + /// go_on_an_adventure().boxed() + /// } + /// ``` + fn boxed(self) -> Box> + where + Self: Sized, + { + Box::new(self) + } +} + +impl Action for Box> { + fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { + match (other as &dyn Any).downcast_ref::() { + Some(other) => self.is_same(other), + None => false, + } + } + + fn reset(&mut self) { (**self).reset(); } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { (**self).tick(ctx) } +} + +// Now + +/// See [`now`]. +#[derive(Copy, Clone)] +pub struct Now(F, Option); + +impl A + Send + Sync + 'static, A: Action> + Action for Now +{ + // TODO: This doesn't compare?! + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { self.1 = None; } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + (self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx) + } +} + +/// Start a new action based on the state of the world (`ctx`) at the moment the +/// action is started. +/// +/// If you're in a situation where you suddenly find yourself needing `ctx`, you +/// probably want to use this. +/// +/// # Example +/// +/// ```ignore +/// // An action that makes an NPC immediately travel to its *current* home +/// now(|ctx| goto(ctx.npc.home)) +/// ``` +pub fn now(f: F) -> Now +where + F: FnMut(&mut NpcCtx) -> A, +{ + Now(f, None) +} + +// Just + +/// See [`just`]. +#[derive(Copy, Clone)] +pub struct Just(F, PhantomData); + +impl R + Send + Sync + 'static> Action + for Just +{ + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) {} + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { ControlFlow::Break((self.0)(ctx)) } +} + +/// An action that executes some code just once when performed. +/// +/// If you want to execute this code on every tick, consider combining it with +/// [`Action::repeat`]. +/// +/// # Example +/// +/// ```ignore +/// // Make the current NPC say 'Hello, world!' exactly once +/// just(|ctx| ctx.controller.say("Hello, world!")) +/// ``` +pub fn just(mut f: F) -> Just +where + F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, +{ + Just(f, PhantomData) +} + +// Finish + +/// See [`finish`]. +#[derive(Copy, Clone)] +pub struct Finish; + +impl Action<()> for Finish { + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) {} + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } +} + +/// An action that immediately finishes without doing anything. +/// +/// This action is useless by itself, but becomes useful when combined with +/// actions that make decisions. +/// +/// # Example +/// +/// ```ignore +/// now(|ctx| { +/// if ctx.npc.is_tired() { +/// sleep().boxed() // If we're tired, sleep +/// } else if ctx.npc.is_hungry() { +/// eat().boxed() // If we're hungry, eat +/// } else { +/// finish().boxed() // Otherwise, do nothing +/// } +/// }) +/// ``` +pub fn finish() -> Finish { Finish } + +// Tree + +pub type Priority = usize; + +pub const URGENT: Priority = 0; +pub const IMPORTANT: Priority = 1; +pub const CASUAL: Priority = 2; + +pub struct Node(Box>, Priority); + +/// Perform an action with [`URGENT`] priority (see [`choose`]). +pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } + +/// Perform an action with [`IMPORTANT`] priority (see [`choose`]). +pub fn important, R>(a: A) -> Node { Node(Box::new(a), IMPORTANT) } + +/// Perform an action with [`CASUAL`] priority (see [`choose`]). +pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } + +/// See [`choose`] and [`watch`]. +pub struct Tree { + next: F, + prev: Option>, + interrupt: bool, +} + +impl Node + Send + Sync + 'static, R: 'static> Action + for Tree +{ + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { self.prev = None; } + + // TODO: Reset `next` too? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + let new = (self.next)(ctx); + + let prev = match &mut self.prev { + Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => { + prev + }, + _ => self.prev.insert(new), + }; + + match prev.0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(r) => { + self.prev = None; + ControlFlow::Break(r) + }, + } + } +} + +/// An action that allows implementing a decision tree, with action +/// prioritisation. +/// +/// The inner function will be run every tick to decide on an action. When an +/// action is chosen, it will be performed until completed *UNLESS* an action +/// with a more urgent priority is chosen in a subsequent tick. [`choose`] tries +/// to commit to actions when it can: only more urgent actions will interrupt an +/// action that's currently being performed. If you want something that's more +/// eager to switch actions, see [`watch`]. +/// +/// # Example +/// +/// ```ignore +/// choose(|ctx| { +/// if ctx.npc.is_being_attacked() { +/// urgent(combat()) // If we're in danger, do something! +/// } else if ctx.npc.is_hungry() { +/// important(eat()) // If we're hungry, eat +/// } else { +/// casual(idle()) // Otherwise, do nothing +/// } +/// }) +/// ``` +pub fn choose(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: false, + } +} + +/// An action that allows implementing a decision tree, with action +/// prioritisation. +/// +/// The inner function will be run every tick to decide on an action. When an +/// action is chosen, it will be performed until completed unless a different +/// action is chosen in a subsequent tick. [`watch`] is very unfocussed and will +/// happily switch between actions rapidly between ticks if conditions change. +/// If you want something that tends to commit to actions until they are +/// completed, see [`choose`]. +/// +/// # Example +/// +/// ```ignore +/// choose(|ctx| { +/// if ctx.npc.is_being_attacked() { +/// urgent(combat()) // If we're in danger, do something! +/// } else if ctx.npc.is_hungry() { +/// important(eat()) // If we're hungry, eat +/// } else { +/// casual(idle()) // Otherwise, do nothing +/// } +/// }) +/// ``` +pub fn watch(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: true, + } +} + +// Then + +/// See [`Action::then`]. +#[derive(Copy, Clone)] +pub struct Then { + a0: A0, + a0_finished: bool, + a1: A1, + phantom: PhantomData, +} + +impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync + 'static> + Action for Then +{ + fn is_same(&self, other: &Self) -> bool { + self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1) + } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { + self.a0.reset(); + self.a0_finished = false; + self.a1.reset(); + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + if !self.a0_finished { + match self.a0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(_) => self.a0_finished = true, + } + } + self.a1.tick(ctx) + } +} + +// Repeat + +/// See [`Action::repeat`]. +#[derive(Copy, Clone)] +pub struct Repeat(A, PhantomData); + +impl> Action for Repeat { + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + match self.0.tick(ctx) { + ControlFlow::Continue(()) => ControlFlow::Continue(()), + ControlFlow::Break(_) => { + self.0.reset(); + ControlFlow::Continue(()) + }, + } + } +} + +// Sequence + +/// See [`seq`]. +#[derive(Copy, Clone)] +pub struct Sequence(I, I, Option, PhantomData); + +impl + Clone + Send + Sync + 'static, A: Action> + Action<()> for Sequence +{ + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { + self.0 = self.1.clone(); + self.2 = None; + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { + let item = if let Some(prev) = &mut self.2 { + prev + } else { + match self.0.next() { + Some(next) => self.2.insert(next), + None => return ControlFlow::Break(()), + } + }; + + if let ControlFlow::Break(_) = item.tick(ctx) { + self.2 = None; + } + + ControlFlow::Continue(()) + } +} + +/// An action that consumes and performs an iterator of actions in sequence, one +/// after another. +/// +/// # Example +/// +/// ```ignore +/// // A list of enemies we should attack in turn +/// let enemies = vec![ +/// ugly_goblin, +/// stinky_troll, +/// rude_dwarf, +/// ]; +/// +/// // Attack each enemy, one after another +/// seq(enemies +/// .into_iter() +/// .map(|enemy| attack(enemy))) +/// ``` +pub fn seq(iter: I) -> Sequence +where + I: Iterator + Clone, + A: Action, +{ + Sequence(iter.clone(), iter, None, PhantomData) +} + +// StopIf + +/// See [`Action::stop_if`]. +#[derive(Copy, Clone)] +pub struct StopIf(A, F); + +impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action> + for StopIf +{ + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { + if (self.1)(ctx) { + ControlFlow::Break(None) + } else { + self.0.tick(ctx).map_break(Some) + } + } +} + +// Map + +/// See [`Action::map`]. +#[derive(Copy, Clone)] +pub struct Map(A, F, PhantomData); + +impl, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1> + Action for Map +{ + 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 reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + self.0.tick(ctx).map_break(&mut self.1) + } +} diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 8e5e6b0085..8547cf265c 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,4 +1,4 @@ -use crate::rule::npc_ai::NpcCtx; +use crate::ai::{Action, NpcCtx}; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, @@ -11,10 +11,8 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ - any::{Any, TypeId}, collections::VecDeque, - marker::PhantomData, - ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState}, + ops::{Deref, DerefMut, Generator, GeneratorState}, pin::Pin, sync::{ atomic::{AtomicPtr, Ordering}, @@ -54,369 +52,6 @@ impl Controller { pub fn idle() -> Self { Self { goto: None } } } -pub trait Action: Any + Send + Sync { - /// Returns `true` if the action should be considered the 'same' (i.e: - /// achieving the same objective) as another. In general, the AI system - /// will try to avoid switching (and therefore restarting) tasks when the - /// new task is the 'same' as the old one. - // TODO: Figure out a way to compare actions based on their 'intention': i.e: - // two pathing actions should be considered equivalent if their destination - // is the same regardless of the progress they've each made. - fn is_same(&self, other: &Self) -> bool - where - Self: Sized; - fn dyn_is_same_sized(&self, other: &dyn Action) -> bool - where - Self: Sized, - { - match (other as &dyn Any).downcast_ref::() { - Some(other) => self.is_same(other), - None => false, - } - } - fn dyn_is_same(&self, other: &dyn Action) -> bool; - // Reset the action to its initial state so it can be restarted - fn reset(&mut self); - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow; - - fn then, R1>(self, other: A1) -> Then - where - Self: Sized, - { - Then { - a0: self, - a0_finished: false, - a1: other, - phantom: PhantomData, - } - } - fn repeat(self) -> Repeat - where - Self: Sized, - { - Repeat(self, PhantomData) - } - fn stop_if bool>(self, f: F) -> StopIf - where - Self: Sized, - { - StopIf(self, f) - } - fn map R1, R1>(self, f: F) -> Map - where - Self: Sized, - { - Map(self, f, PhantomData) - } - fn boxed(self) -> Box> - where - Self: Sized, - { - Box::new(self) - } -} - -impl Action for Box> { - fn is_same(&self, other: &Self) -> bool { (**self).dyn_is_same(other) } - - fn dyn_is_same(&self, other: &dyn Action) -> bool { - match (other as &dyn Any).downcast_ref::() { - Some(other) => self.is_same(other), - None => false, - } - } - - fn reset(&mut self) { (**self).reset(); } - - // TODO: Reset closure state? - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { (**self).tick(ctx) } -} - -// Now - -#[derive(Copy, Clone)] -pub struct Now(F, Option); - -impl A + Send + Sync + 'static, A: Action> - Action for Now -{ - // TODO: This doesn't compare?! - fn is_same(&self, other: &Self) -> bool { true } - - fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) { self.1 = None; } - - // TODO: Reset closure state? - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { - (self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx) - } -} - -pub fn now(f: F) -> Now -where - F: FnMut(&mut NpcCtx) -> A, -{ - Now(f, None) -} - -// Just - -#[derive(Copy, Clone)] -pub struct Just(F, PhantomData); - -impl R + Send + Sync + 'static> Action - for Just -{ - fn is_same(&self, other: &Self) -> bool { true } - - fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) {} - - // TODO: Reset closure state? - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { ControlFlow::Break((self.0)(ctx)) } -} - -pub fn just(mut f: F) -> Just -where - F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, -{ - Just(f, PhantomData) -} - -// Finish - -#[derive(Copy, Clone)] -pub struct Finish; - -impl Action<()> for Finish { - fn is_same(&self, other: &Self) -> bool { true } - - fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) {} - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } -} - -pub fn finish() -> Finish { Finish } - -// Tree - -pub type Priority = usize; - -const URGENT: Priority = 0; -const CASUAL: Priority = 1; - -pub struct Node(Box>, Priority); - -pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } -pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } - -pub struct Tree { - next: F, - prev: Option>, - interrupt: bool, -} - -impl Node + Send + Sync + 'static, R: 'static> Action - for Tree -{ - fn is_same(&self, other: &Self) -> bool { true } - - fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) { self.prev = None; } - - // TODO: Reset `next` too? - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { - let new = (self.next)(ctx); - - let prev = match &mut self.prev { - Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => { - prev - }, - _ => self.prev.insert(new), - }; - - match prev.0.tick(ctx) { - ControlFlow::Continue(()) => return ControlFlow::Continue(()), - ControlFlow::Break(r) => { - self.prev = None; - ControlFlow::Break(r) - }, - } - } -} - -pub fn choose(f: F) -> impl Action -where - F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, -{ - Tree { - next: f, - prev: None, - interrupt: false, - } -} - -pub fn watch(f: F) -> impl Action -where - F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, -{ - Tree { - next: f, - prev: None, - interrupt: true, - } -} - -// Then - -#[derive(Copy, Clone)] -pub struct Then { - a0: A0, - a0_finished: bool, - a1: A1, - phantom: PhantomData, -} - -impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync + 'static> - Action for Then -{ - fn is_same(&self, other: &Self) -> bool { - self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1) - } - - fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) { - self.a0.reset(); - self.a0_finished = false; - self.a1.reset(); - } - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { - if !self.a0_finished { - match self.a0.tick(ctx) { - ControlFlow::Continue(()) => return ControlFlow::Continue(()), - ControlFlow::Break(_) => self.a0_finished = true, - } - } - self.a1.tick(ctx) - } -} - -// Repeat - -#[derive(Copy, Clone)] -pub struct Repeat(A, PhantomData); - -impl> Action for Repeat { - 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 reset(&mut self) { self.0.reset(); } - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { - match self.0.tick(ctx) { - ControlFlow::Continue(()) => ControlFlow::Continue(()), - ControlFlow::Break(_) => { - self.0.reset(); - ControlFlow::Continue(()) - }, - } - } -} - -// Sequence - -#[derive(Copy, Clone)] -pub struct Sequence(I, I, Option, PhantomData); - -impl + Clone + Send + Sync + 'static, A: Action> - Action<()> for Sequence -{ - fn is_same(&self, other: &Self) -> bool { true } - - fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } - - fn reset(&mut self) { - self.0 = self.1.clone(); - self.2 = None; - } - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { - let item = if let Some(prev) = &mut self.2 { - prev - } else { - match self.0.next() { - Some(next) => self.2.insert(next), - None => return ControlFlow::Break(()), - } - }; - - if let ControlFlow::Break(_) = item.tick(ctx) { - self.2 = None; - } - - ControlFlow::Continue(()) - } -} - -pub fn seq(iter: I) -> Sequence -where - I: Iterator + Clone, - A: Action, -{ - Sequence(iter.clone(), iter, None, PhantomData) -} - -// StopIf - -#[derive(Copy, Clone)] -pub struct StopIf(A, F); - -impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action> - for StopIf -{ - 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 reset(&mut self) { self.0.reset(); } - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { - if (self.1)(ctx) { - ControlFlow::Break(None) - } else { - self.0.tick(ctx).map_break(Some) - } - } -} - -// Map - -#[derive(Copy, Clone)] -pub struct Map(A, F, PhantomData); - -impl, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1> - Action for Map -{ - 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 reset(&mut self) { self.0.reset(); } - - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { - self.0.tick(ctx).map_break(&mut self.1) - } -} - pub struct Brain { pub(crate) action: Box>, } diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 6252d7d0aa..cc73f3ec92 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -10,6 +10,7 @@ let_chains )] +pub mod ai; pub mod data; pub mod event; pub mod gen; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 447fe21db1..0b847aed00 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,11 +1,9 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ + ai::{casual, choose, finish, just, now, seq, urgent, watch, Action, NpcCtx}, data::{ - npc::{ - casual, choose, finish, just, now, seq, urgent, watch, Action, Brain, Controller, Npc, - NpcId, PathData, PathingMemory, - }, + npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory}, Sites, }, event::OnTick, @@ -254,7 +252,11 @@ impl Rule for NpcAi { let mut controller = Controller { goto: npc.goto }; brain.action.tick(&mut NpcCtx { - ctx: &ctx, + state: ctx.state, + world: ctx.world, + index: ctx.index, + time_of_day: ctx.event.time_of_day, + time: ctx.event.time, npc, npc_id, controller: &mut controller, @@ -262,93 +264,46 @@ impl Rule for NpcAi { /* let action: ControlFlow<()> = try { - if matches!(npc.profession, Some(Profession::Adventurer(_))) { - if let Some(home) = npc.home { - // Travel between random nearby sites - let task = generate( - move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { - // Choose a random site that's fairly close by - let tgt_site = ctx - .state - .data() - .sites - .iter() - .filter(|(site_id, site)| { - site.faction.is_some() - && npc - .current_site - .map_or(true, |cs| *site_id != cs) - && thread_rng().gen_bool(0.25) - }) - .min_by_key(|(_, site)| { - site.wpos.as_().distance(npc.wpos.xy()) as i32 - }) - .map(|(site_id, _)| site_id) - .unwrap_or(home); + brain.tick(&mut NpcData { + ctx: &ctx, + npc, + npc_id, + controller: &mut controller, + }); + /* + // // Choose a random plaza in the npcs home site (which should be the + // // current here) to go to. + let task = + generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { + let data = ctx.state.data(); + let site2 = + npc.home.and_then(|home| data.sites.get(home)).and_then( + |home| match &ctx.index.sites.get(home.world_site?).kind + { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + }, + ); - let wpos = ctx - .state - .data() - .sites - .get(tgt_site) - .map_or(npc.wpos.xy(), |site| site.wpos.as_()); + let wpos = site2 + .and_then(|site2| { + let plaza = &site2.plots + [site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + .unwrap_or(npc.wpos.xy()); - TravelTo { - wpos, - use_paths: true, - } - }, - ) - .repeat(); + TravelTo { + wpos, + use_paths: true, + } + }) + .repeat(); - task_state.perform( - task, - &(npc_id, &*npc, &ctx), - &mut controller, - )?; - } - } else { - brain.tick(&mut NpcData { - ctx: &ctx, - npc, - npc_id, - controller: &mut controller, - }); - /* - // // Choose a random plaza in the npcs home site (which should be the - // // current here) to go to. - let task = - generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { - let data = ctx.state.data(); - let site2 = - npc.home.and_then(|home| data.sites.get(home)).and_then( - |home| match &ctx.index.sites.get(home.world_site?).kind - { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }, - ); - - let wpos = site2 - .and_then(|site2| { - let plaza = &site2.plots - [site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - .unwrap_or(npc.wpos.xy()); - - TravelTo { - wpos, - use_paths: true, - } - }) - .repeat(); - - task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; - */ - } + task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; + */ }; */ @@ -364,23 +319,13 @@ impl Rule for NpcAi { } } -pub struct NpcCtx<'a> { - ctx: &'a EventCtx<'a, NpcAi, OnTick>, - npc_id: NpcId, - npc: &'a Npc, - controller: &'a mut Controller, -} +fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()) } -fn pass() -> impl Action { just(|ctx| {}) } - -fn idle_wait() -> impl Action { - just(|ctx| *ctx.controller = Controller::idle()) - .repeat() - .map(|_| ()) -} - -fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action { +/// Try to walk toward a 3D position without caring for obstacles. +fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { const STEP_DIST: f32 = 16.0; + const GOAL_DIST: f32 = 2.0; + just(move |ctx| { let rpos = wpos - ctx.npc.wpos; let len = rpos.magnitude(); @@ -389,76 +334,78 @@ fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action { speed_factor, )); }) + .repeat() + .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2)) + .map(|_| {}) } -fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { - const MIN_DIST: f32 = 2.0; - - move_toward(wpos, speed_factor) - .repeat() - .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < MIN_DIST.powi(2)) - .map(|_| {}) -} - +/// Try to walk toward a 2D position on the terrain without caring for +/// obstacles. fn goto_2d(wpos2d: Vec2, speed_factor: f32) -> impl Action { const MIN_DIST: f32 = 2.0; now(move |ctx| { - let wpos = wpos2d.with_z( - ctx.ctx - .world - .sim() - .get_alt_approx(wpos2d.as_()) - .unwrap_or(0.0), - ); + let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); goto(wpos, speed_factor) }) } -fn path_to_site(tgt_site: SiteId) -> impl Action { +/// Try to travel to a site. Where practical, paths will be taken. +fn travel_to_site(tgt_site: SiteId) -> impl Action { now(move |ctx| { - let sites = &ctx.ctx.state.data().sites; + let sites = &ctx.state.data().sites; - // If we can, try to find a path to the site via tracks + // If we're currently in a site, try to find a path to the target site via + // tracks if let Some(current_site) = ctx.npc.current_site - && let Some((mut tracks, _)) = path_towns(current_site, tgt_site, sites, ctx.ctx.world) + && let Some((mut tracks, _)) = path_towns(current_site, tgt_site, sites, ctx.world) { + // For every track in the path we discovered between the sites... seq(tracks .path .into_iter() + // ...traverse the nodes of that path. .map(|(track_id, reversed)| now(move |ctx| { - let track_len = ctx.ctx.world.civs().tracks.get(track_id).path().len(); + let track_len = ctx.world.civs().tracks.get(track_id).path().len(); + // Tracks can be traversed backward (i.e: from end to beginning). Account for this. seq(if reversed { itertools::Either::Left((0..track_len).rev()) } else { itertools::Either::Right(0..track_len) } .map(move |node_idx| now(move |ctx| { - let track = ctx.ctx.world.civs().tracks.get(track_id); - let next_node = track.path().nodes[node_idx]; + // Find the centre of the track node's chunk + let node_chunk_wpos = TerrainChunkSize::center_wpos(ctx.world + .civs() + .tracks + .get(track_id) + .path() + .nodes[node_idx]); - let chunk_wpos = TerrainChunkSize::center_wpos(next_node); - let path_wpos2d = ctx.ctx.world.sim() - .get_nearest_path(chunk_wpos) - .map_or(chunk_wpos, |(_, wpos, _, _)| wpos.as_()); + // Refine the node position a bit more based on local path information + let node_wpos = ctx.world.sim() + .get_nearest_path(node_chunk_wpos) + .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()); - goto_2d(path_wpos2d.as_(), 1.0) + // Walk toward the node + goto_2d(node_wpos.as_(), 1.0) }))) }))) .boxed() } else if let Some(site) = sites.get(tgt_site) { - // If all else fails, just walk toward the site in a straight line + // If all else fails, just walk toward the target site in a straight line goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0).boxed() } else { - pass().boxed() + // If we can't find a way to get to the site at all, there's nothing more to be done + finish().boxed() } }) } // Seconds fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { - let end = ctx.ctx.event.time.0 + time; - move |ctx| ctx.ctx.event.time.0 > end + let end = ctx.time.0 + time; + move |ctx| ctx.time.0 > end } fn think() -> impl Action { @@ -466,12 +413,13 @@ fn think() -> impl Action { if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { // Choose a random site that's fairly close by if let Some(tgt_site) = ctx - .ctx .state .data() .sites .iter() .filter(|(site_id, site)| { + // TODO: faction.is_some() is used as a proxy for whether the site likely has + // paths, don't do this site.faction.is_some() && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25) @@ -479,429 +427,36 @@ fn think() -> impl Action { .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .map(|(site_id, _)| site_id) { - casual(path_to_site(tgt_site)) + casual(travel_to_site(tgt_site)) } else { - casual(pass()) + casual(finish()) } } else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) { - casual(idle_wait()) + casual(idle()) } else { casual( now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0)) .then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0))) .repeat() .stop_if(timeout(ctx, 10.0)) - .then(now(|ctx| idle_wait().stop_if(timeout(ctx, 5.0)))) + .then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0)))) .map(|_| {}), ) } }) } -/* -#[derive(Clone)] -pub struct Generate(F, PhantomData); - -impl PartialEq for Generate { - fn eq(&self, _: &Self) -> bool { true } -} - -pub fn generate(f: F) -> Generate { Generate(f, PhantomData) } - -impl Task for Generate -where - F: Clone + Send + Sync + 'static + for<'a> Fn(&T::Ctx<'a>) -> T, -{ - type Ctx<'a> = T::Ctx<'a>; - type State = (T::State, T); - - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { - let task = (self.0)(ctx); - (task.begin(ctx), task) - } - - fn run<'a>( - &self, - (state, task): &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - task.run(state, ctx, controller) - } -} - -#[derive(Clone, PartialEq)] -pub struct Goto { - wpos: Vec2, - speed_factor: f32, - finish_dist: f32, -} - -pub fn goto(wpos: Vec2) -> Goto { - Goto { - wpos, - speed_factor: 1.0, - finish_dist: 1.0, - } -} - -impl Task for Goto { - type Ctx<'a> = (&'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); - type State = (); - - fn begin<'a>(&self, (_npc, _ctx): &Self::Ctx<'a>) -> Self::State {} - - fn run<'a>( - &self, - (): &mut Self::State, - (npc, ctx): &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - if npc.wpos.xy().distance_squared(self.wpos) < self.finish_dist.powi(2) { - controller.goto = None; - FINISH - } else { - let dist = npc.wpos.xy().distance(self.wpos); - let step = dist.min(32.0); - let next_tgt = npc.wpos.xy() + (self.wpos - npc.wpos.xy()) / dist * step; - - if npc.goto.map_or(true, |(tgt, _)| { - tgt.xy().distance_squared(next_tgt) > (step * 0.5).powi(2) - }) || npc.wpos.xy().distance_squared(next_tgt) < (step * 0.5).powi(2) - { - controller.goto = Some(( - next_tgt.with_z( - ctx.world - .sim() - .get_alt_approx(next_tgt.map(|e| e as i32)) - .unwrap_or(0.0), - ), - self.speed_factor, - )); - } - CONTINUE - } - } -} - -#[derive(Clone, PartialEq)] -pub struct TravelTo { - wpos: Vec2, - use_paths: bool, -} - -pub enum TravelStage { - Goto(Vec2), - SiteToSite { - path: PathData<(Id, bool), SiteId>, - progress: usize, - }, - IntraSite { - path: PathData, Vec2>, - site: Id, - }, -} - -impl Task for TravelTo { - type Ctx<'a> = (NpcId, &'a Npc, &'a EventCtx<'a, NpcAi, OnTick>); - type State = (VecDeque, TaskState); - - fn begin<'a>(&self, (_npc_id, npc, ctx): &Self::Ctx<'a>) -> Self::State { - if self.use_paths { - let a = npc.wpos.xy(); - let b = self.wpos; - - let data = ctx.state.data(); - let nearest_in_dir = |wpos: Vec2, end: Vec2| { - let dist = wpos.distance(end); - data.sites - .iter() - // TODO: faction.is_some() is currently used as a proxy for whether the site likely has paths, don't do this - .filter(|(site_id, site)| site.faction.is_some() && end.distance(site.wpos.as_()) < dist * 1.2) - .min_by_key(|(_, site)| site.wpos.as_().distance(wpos) as i32) - }; - if let Some((site_a, site_b)) = nearest_in_dir(a, b).zip(nearest_in_dir(b, a)) { - if site_a.0 != site_b.0 { - if let Some((path, progress)) = - path_towns(site_a.0, site_b.0, &ctx.state.data().sites, ctx.world) - { - return ( - [ - TravelStage::Goto(site_a.1.wpos.as_()), - TravelStage::SiteToSite { path, progress }, - TravelStage::Goto(b), - ] - .into_iter() - .collect(), - TaskState::default(), - ); - } - } - } - } - ( - [TravelStage::Goto(self.wpos)].into_iter().collect(), - TaskState::default(), - ) - } - - fn run<'a>( - &self, - (stages, task_state): &mut Self::State, - (npc_id, npc, ctx): &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - let get_site2 = |site| match &ctx.index.sites.get(site).kind { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }; - - if let Some(stage) = stages.front_mut() { - match stage { - TravelStage::Goto(wpos) => { - task_state.perform(goto(*wpos), &(npc, ctx), controller)?; - stages.pop_front(); - }, - TravelStage::IntraSite { path, site } => { - if npc - .current_site - .and_then(|site| ctx.state.data().sites.get(site)?.world_site) - == Some(*site) - { - if let Some(next_tile) = path.path.front() { - task_state.perform( - Goto { - wpos: get_site2(*site) - .expect( - "intrasite path should only be started on a site2 site", - ) - .tile_center_wpos(*next_tile) - .as_() - + 0.5, - speed_factor: 0.6, - finish_dist: 1.0, - }, - &(npc, ctx), - controller, - )?; - path.path.pop_front(); - return CONTINUE; - } - } - task_state.perform(goto(self.wpos), &(npc, ctx), controller)?; - stages.pop_front(); - }, - TravelStage::SiteToSite { path, progress } => { - if let Some((track_id, reversed)) = path.path.front() { - let track = ctx.world.civs().tracks.get(*track_id); - if *progress >= track.path().len() { - // We finished this track section, move to the next one - path.path.pop_front(); - *progress = 0; - } else { - let next_node_idx = if *reversed { - track.path().len().saturating_sub(*progress + 1) - } else { - *progress - }; - let next_node = track.path().nodes[next_node_idx]; - - let transform_path_pos = |chunk_pos| { - let chunk_wpos = TerrainChunkSize::center_wpos(chunk_pos); - if let Some(pathdata) = ctx.world.sim().get_nearest_path(chunk_wpos) - { - pathdata.1.map(|e| e as i32) - } else { - chunk_wpos - } - }; - - task_state.perform( - Goto { - wpos: transform_path_pos(next_node).as_() + 0.5, - speed_factor: 1.0, - finish_dist: 10.0, - }, - &(npc, ctx), - controller, - )?; - *progress += 1; - } - } else { - stages.pop_front(); - } - }, - } - - if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { - let data = ctx.state.data(); - if let Some((site2, site)) = npc - .current_site - .and_then(|current_site| data.sites.get(current_site)) - .and_then(|site| site.world_site) - .and_then(|site| Some((get_site2(site)?, site))) - { - let end = site2.wpos_tile_pos(self.wpos.as_()); - if let Some(path) = path_town(npc.wpos, site, ctx.index, |_| Some(end)) { - stages.push_front(TravelStage::IntraSite { path, site }); - } - } - } - - CONTINUE - } else { - FINISH - } - } -} - -/* -let data = ctx.state.data(); -let site2 = - npc.home.and_then(|home| data.sites.get(home)).and_then( - |home| match &ctx.index.sites.get(home.world_site?).kind - { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }, - ); - -let wpos = site2 - .and_then(|site2| { - let plaza = &site2.plots - [site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - .unwrap_or(npc.wpos.xy()); - -TravelTo { - wpos, - use_paths: true, -} -*/ - -trait IsTask = - core::ops::Generator>, Yield = (), Return = ()> + Any + Send + Sync; - -pub struct NpcData<'a> { - ctx: &'a EventCtx<'a, NpcAi, OnTick>, - npc_id: NpcId, - npc: &'a Npc, - controller: &'a mut Controller, -} - -unsafe impl Context for NpcData<'static> { - type Ty<'a> = NpcData<'a>; -} - -pub fn brain() -> Brain> { - Brain::new(|mut data: Data| { - let mut task = TaskBox::<_, ()>::new(data.clone()); - - loop { - println!("Started"); - while let ControlFlow::Break(end) = task.finish(0) { - yield end; - } - - // Choose a new plaza in the NPC's home site to path towards - let path = data.with(|d| { - let data = d.ctx.state.data(); - - let current_site = data.sites.get(d.npc.current_site?)?; - let site2 = match &d.ctx.index.sites.get(current_site.world_site?).kind { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }?; - - let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; - let end_wpos = site2.tile_center_wpos(plaza.root_tile()); - - if end_wpos.as_::().distance(d.npc.wpos.xy()) < 32.0 { - return None; - } - - let start = site2.wpos_tile_pos(d.npc.wpos.xy().as_()); - let end = site2.wpos_tile_pos(plaza.root_tile()); - - let path = match path_in_site(start, end, site2) { - PathResult::Path(path) => path, - _ => return None, - }; - println!( - "CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", - path.len(), - start, - end, - d.npc_id - ); - Some((current_site.world_site?, path)) - }); - - if let Some((site, path)) = path { - println!("Begin path"); - task.perform(0, walk_path(site, path)); - } else { - println!("No path, waiting..."); - for _ in 0..100 { - yield (); - } - println!("Waited."); - } - } - }) -} - -fn walk_path(site: Id, path: Path>) -> impl IsTask { - move |mut data: Data| { - for tile in path { - println!("TILE"); - let wpos = data.with(|d| { - match &d.ctx.index.sites.get(site).kind { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - } - .expect("intrasite path should only be started on a site2 site") - .tile_center_wpos(tile) - .as_() - + 0.5 - }); - - println!( - "Walking to next tile... tile wpos = {:?} npc wpos = {:?}", - wpos, - data.with(|d| d.npc.wpos) - ); - while data.with(|d| d.npc.wpos.xy().distance_squared(wpos) > 2.0) { - data.with(|d| { - d.controller.goto = Some(( - wpos.with_z( - d.ctx - .world - .sim() - .get_alt_approx(wpos.map(|e| e as i32)) - .unwrap_or(0.0), - ), - 1.0, - )) - }); - yield (); - } - } - - println!("Waiting.."); - for _ in 0..100 { - yield (); - } - println!("Waited."); - } -} -*/ +// if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { +// let data = ctx.state.data(); +// if let Some((site2, site)) = npc +// .current_site +// .and_then(|current_site| data.sites.get(current_site)) +// .and_then(|site| site.world_site) +// .and_then(|site| Some((get_site2(site)?, site))) +// { +// let end = site2.wpos_tile_pos(self.wpos.as_()); +// if let Some(path) = path_town(npc.wpos, site, ctx.index, |_| +// Some(end)) { stages.push_front(TravelStage::IntraSite { path, +// site }); } +// } +// } From ac83cfc4a33a8dce79d15d28ce5930e9a7498f57 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 13:15:09 +0000 Subject: [PATCH 046/139] More interesting idle behaviours --- rtsim/src/gen/mod.rs | 9 +++- rtsim/src/rule/npc_ai.rs | 112 +++++++++++++++++++++++++++------------ world/src/site/mod.rs | 19 +++++++ 3 files changed, 104 insertions(+), 36 deletions(-) diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 49f34c8c0f..eaf7e09ee0 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -65,14 +65,19 @@ impl Data { ); // Spawn some test entities at the sites - for (site_id, site) in this.sites.iter().take(1) { + for (site_id, site) in this.sites.iter() + // TODO: Stupid + .filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) + .skip(1) + .take(1) + { let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); wpos2d .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..10 { + for _ in 0..16 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 0b847aed00..e36bbabb65 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,7 +1,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ - ai::{casual, choose, finish, just, now, seq, urgent, watch, Action, NpcCtx}, + ai::{casual, choose, finish, important, just, now, seq, urgent, watch, Action, NpcCtx}, data::{ npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory}, Sites, @@ -208,24 +208,18 @@ fn path_towns( end: SiteId, sites: &Sites, world: &World, -) -> Option<(PathData<(Id, bool), SiteId>, usize)> { +) -> Option, bool), SiteId>> { match path_between_sites(start, end, sites, world) { - PathResult::Exhausted(p) => Some(( - PathData { - end, - path: p.nodes.into(), - repoll: true, - }, - 0, - )), - PathResult::Path(p) => Some(( - PathData { - end, - path: p.nodes.into(), - repoll: false, - }, - 0, - )), + PathResult::Exhausted(p) => Some(PathData { + end, + path: p.nodes.into(), + repoll: true, + }), + PathResult::Path(p) => Some(PathData { + end, + path: p.nodes.into(), + repoll: false, + }), PathResult::Pending | PathResult::None(_) => None, } } @@ -323,16 +317,28 @@ fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()) } /// Try to walk toward a 3D position without caring for obstacles. fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { - const STEP_DIST: f32 = 16.0; + const STEP_DIST: f32 = 24.0; + const WAYPOINT_DIST: f32 = 12.0; const GOAL_DIST: f32 = 2.0; + let mut waypoint = None; + just(move |ctx| { let rpos = wpos - ctx.npc.wpos; let len = rpos.magnitude(); - ctx.controller.goto = Some(( - ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST), - speed_factor, - )); + + // If we're close to the next waypoint, complete it + if waypoint.map_or(false, |waypoint: Vec3| { + ctx.npc.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2) + }) { + waypoint = None; + } + + // Get the next waypoint on the route toward the goal + let waypoint = + waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST)); + + ctx.controller.goto = Some((*waypoint, speed_factor)); }) .repeat() .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2)) @@ -358,7 +364,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { // If we're currently in a site, try to find a path to the target site via // tracks if let Some(current_site) = ctx.npc.current_site - && let Some((mut tracks, _)) = path_towns(current_site, tgt_site, sites, ctx.world) + && let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world) { // For every track in the path we discovered between the sites... seq(tracks @@ -408,6 +414,53 @@ fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + move |ctx| ctx.time.0 > end } +fn villager() -> impl Action { + choose(|ctx| { + if let Some(home) = ctx.npc.home { + if ctx.npc.current_site != Some(home) { + // Travel home if we're not there already + important(travel_to_site(home)) + } else if matches!( + ctx.npc.profession, + Some(Profession::Merchant | Profession::Blacksmith) + ) { + // Trade professions just walk between town plazas + casual(now(move |ctx| { + // Choose a plaza in the NPC's home site to walk to + if let Some(plaza_wpos) = ctx + .state + .data() + .sites + .get(home) + .and_then(|home| ctx.index.sites.get(home.world_site?).site2()) + .and_then(|site2| { + let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + { + // Walk to the plaza... + goto_2d(plaza_wpos, 0.5) + // ...then wait for some time before moving on + .then(now(|ctx| { + let wait_time = thread_rng().gen_range(10.0..30.0); + idle().repeat().stop_if(timeout(ctx, wait_time)) + })) + .map(|_| ()) + .boxed() + } else { + // No plazas? :( + finish().boxed() + } + })) + } else { + casual(idle()) + } + } else { + casual(finish()) // Nothing to do if we're homeless! + } + }) +} + fn think() -> impl Action { choose(|ctx| { if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { @@ -431,17 +484,8 @@ fn think() -> impl Action { } else { casual(finish()) } - } else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) { - casual(idle()) } else { - casual( - now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0)) - .then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0))) - .repeat() - .stop_if(timeout(ctx, 10.0)) - .then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0)))) - .map(|_| {}), - ) + casual(villager()) } }) } diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs index 0c65c6280f..b25d7e2e7c 100644 --- a/world/src/site/mod.rs +++ b/world/src/site/mod.rs @@ -305,6 +305,25 @@ impl Site { | SiteKind::Settlement(_) ) } + + /// Return the inner site2 site, if this site has one. + // TODO: Remove all of this when site1 gets removed. + pub fn site2(&self) -> Option<&site2::Site> { + match &self.kind { + SiteKind::Settlement(_) => None, + SiteKind::Dungeon(site2) => Some(site2), + SiteKind::Castle(_) => None, + SiteKind::Refactor(site2) => Some(site2), + SiteKind::CliffTown(site2) => Some(site2), + SiteKind::SavannahPit(site2) => Some(site2), + SiteKind::Tree(_) => None, + SiteKind::DesertCity(site2) => Some(site2), + SiteKind::ChapelSite(site2) => Some(site2), + SiteKind::GiantTree(site2) => Some(site2), + SiteKind::Gnarling(site2) => Some(site2), + SiteKind::Bridge(site2) => Some(site2), + } + } } impl SiteKind { From 2b3f0737d032bae7f70e63a074a314de095f7b20 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 15:09:32 +0000 Subject: [PATCH 047/139] Added npc_info, action backtraces --- common/src/cmd.rs | 9 +++- common/src/rtsim.rs | 2 +- rtsim/src/ai/mod.rs | 90 +++++++++++++++++++++++++++++++ rtsim/src/data/npc.rs | 4 +- rtsim/src/rule/npc_ai.rs | 113 ++++++++++++++++++++++++++++----------- server/src/cmd.rs | 48 +++++++++++++++++ 6 files changed, 230 insertions(+), 36 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index f074cc6a3e..a80ba24f6c 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -311,6 +311,7 @@ pub enum ServerChatCommand { Time, Tp, TpNpc, + NpcInfo, Unban, Version, Waypoint, @@ -682,7 +683,12 @@ impl ServerChatCommand { ), ServerChatCommand::TpNpc => cmd( vec![Integer("npc index", 0, Required)], - "Teleport to a npc", + "Teleport to an rtsim npc", + Some(Moderator), + ), + ServerChatCommand::NpcInfo => cmd( + vec![Integer("npc index", 0, Required)], + "Display information about an rtsim NPC", Some(Moderator), ), ServerChatCommand::Unban => cmd( @@ -808,6 +814,7 @@ impl ServerChatCommand { ServerChatCommand::Time => "time", ServerChatCommand::Tp => "tp", ServerChatCommand::TpNpc => "tp_npc", + ServerChatCommand::NpcInfo => "npc_info", ServerChatCommand::Unban => "unban", ServerChatCommand::Version => "version", ServerChatCommand::Waypoint => "waypoint", diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 8064459c40..b672f073aa 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -99,7 +99,7 @@ pub enum ChunkResource { Cotton, } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum Profession { #[serde(rename = "0")] Farmer, diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 642240fb19..05f4fd3c40 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -67,6 +67,10 @@ pub trait Action: Any + Send + Sync { /// Like [`Action::is_same`], but allows for dynamic dispatch. fn dyn_is_same(&self, other: &dyn Action) -> bool; + /// Generate a backtrace for the action. The action should recursively push + /// all of the tasks it is currently performing. + fn backtrace(&self, bt: &mut Vec); + /// Reset the action to its initial state such that it can be repeated. fn reset(&mut self); @@ -159,6 +163,21 @@ pub trait Action: Any + Send + Sync { { Box::new(self) } + + /// Add debugging information to the action that will be visible when using + /// the `/npc_info` command. + /// + /// # Example + /// + /// ```ignore + /// goto(npc.home).debug(|| "Going home") + /// ``` + fn debug(self, mk_info: F) -> Debug + where + Self: Sized, + { + Debug(self, mk_info, PhantomData) + } } impl Action for Box> { @@ -171,6 +190,8 @@ impl Action for Box> { } } + fn backtrace(&self, bt: &mut Vec) { (**self).backtrace(bt) } + fn reset(&mut self) { (**self).reset(); } // TODO: Reset closure state? @@ -191,6 +212,14 @@ impl A + Send + Sync + 'stati fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { + if let Some(action) = &self.1 { + action.backtrace(bt); + } else { + bt.push("".to_string()); + } + } + fn reset(&mut self) { self.1 = None; } // TODO: Reset closure state? @@ -231,6 +260,8 @@ impl R + Send + Sync + 'stati fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) {} + fn reset(&mut self) {} // TODO: Reset closure state? @@ -266,6 +297,8 @@ impl Action<()> for Finish { fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) {} + fn reset(&mut self) {} fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } @@ -324,6 +357,14 @@ impl Node + Send + Sync + 'static, R: 'static> Actio fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { + if let Some(prev) = &self.prev { + prev.0.backtrace(bt); + } else { + bt.push("".to_string()); + } + } + fn reset(&mut self) { self.prev = None; } // TODO: Reset `next` too? @@ -435,6 +476,14 @@ impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { + if self.a0_finished { + self.a1.backtrace(bt); + } else { + self.a0.backtrace(bt); + } + } + fn reset(&mut self) { self.a0.reset(); self.a0_finished = false; @@ -463,6 +512,8 @@ impl> Action for Repeat { fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { self.0.backtrace(bt); } + fn reset(&mut self) { self.0.reset(); } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { @@ -489,6 +540,14 @@ impl + Clone + Send + Sync + 'st fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { + if let Some(action) = &self.2 { + action.backtrace(bt); + } else { + bt.push("".to_string()); + } + } + fn reset(&mut self) { self.0 = self.1.clone(); self.2 = None; @@ -551,6 +610,8 @@ impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Act fn dyn_is_same(&self, other: &dyn Action>) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { self.0.backtrace(bt); } + fn reset(&mut self) { self.0.reset(); } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { @@ -575,9 +636,38 @@ impl, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + ' fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + fn backtrace(&self, bt: &mut Vec) { self.0.backtrace(bt); } + fn reset(&mut self) { self.0.reset(); } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { self.0.tick(ctx).map_break(&mut self.1) } } + +// Debug + +/// See [`Action::debug`]. +#[derive(Copy, Clone)] +pub struct Debug(A, F, PhantomData); + +impl< + A: Action, + F: Fn() -> T + Send + Sync + 'static, + R: Send + Sync + 'static, + T: Send + Sync + std::fmt::Display + 'static, +> Action for Debug +{ + 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 backtrace(&self, bt: &mut Vec) { + bt.push((self.1)().to_string()); + self.0.backtrace(bt); + } + + fn reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { self.0.tick(ctx) } +} diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 8547cf265c..ecee1af62f 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -22,7 +22,7 @@ use std::{ use vek::*; use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub enum NpcMode { /// The NPC is unloaded and is being simulated via rtsim. #[default] @@ -53,7 +53,7 @@ impl Controller { } pub struct Brain { - pub(crate) action: Box>, + pub action: Box>, } #[derive(Serialize, Deserialize)] diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index e36bbabb65..480dd62185 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -15,6 +15,7 @@ use common::{ rtsim::{Profession, SiteId}, store::Id, terrain::TerrainChunkSize, + time::DayPeriod, vol::RectVolSize, }; use fxhash::FxHasher64; @@ -29,7 +30,7 @@ use vek::*; use world::{ civ::{self, Track}, site::{Site as WorldSite, SiteKind}, - site2::{self, TileKind}, + site2::{self, PlotKind, TileKind}, IndexRef, World, }; @@ -313,7 +314,7 @@ impl Rule for NpcAi { } } -fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()) } +fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") } /// Try to walk toward a 3D position without caring for obstacles. fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { @@ -342,6 +343,7 @@ fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { }) .repeat() .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2)) + .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z)) .map(|_| {}) } @@ -366,12 +368,14 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { if let Some(current_site) = ctx.npc.current_site && let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world) { + let track_count = tracks.path.len(); // For every track in the path we discovered between the sites... seq(tracks .path .into_iter() + .enumerate() // ...traverse the nodes of that path. - .map(|(track_id, reversed)| now(move |ctx| { + .map(move |(i, (track_id, reversed))| now(move |ctx| { let track_len = ctx.world.civs().tracks.get(track_id).path().len(); // Tracks can be traversed backward (i.e: from end to beginning). Account for this. seq(if reversed { @@ -379,7 +383,8 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { } else { itertools::Either::Right(0..track_len) } - .map(move |node_idx| now(move |ctx| { + .enumerate() + .map(move |(i, node_idx)| now(move |ctx| { // Find the centre of the track node's chunk let node_chunk_wpos = TerrainChunkSize::center_wpos(ctx.world .civs() @@ -395,8 +400,10 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { // Walk toward the node goto_2d(node_wpos.as_(), 1.0) + .debug(move || format!("traversing track node ({}/{})", i + 1, track_len)) }))) - }))) + }) + .debug(move || format!("travel via track {:?} ({}/{})", track_id, i + 1, track_count)))) .boxed() } else if let Some(site) = sites.get(tgt_site) { // If all else fails, just walk toward the target site in a straight line @@ -406,12 +413,43 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { finish().boxed() } }) + .debug(move || format!("travel_to_site {:?}", tgt_site)) } // Seconds -fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { - let end = ctx.time.0 + time; - move |ctx| ctx.time.0 > end +fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { + let mut timeout = None; + move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time) +} + +fn adventure() -> impl Action { + now(|ctx| { + // Choose a random site that's fairly close by + if let Some(tgt_site) = ctx + .state + .data() + .sites + .iter() + .filter(|(site_id, site)| { + // TODO: faction.is_some() is used as a proxy for whether the site likely has + // paths, don't do this + site.faction.is_some() + && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) + && thread_rng().gen_bool(0.25) + }) + .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id) + { + // Travel to the site + travel_to_site(tgt_site) + // Stop for a few minutes + .then(villager().stop_if(timeout(60.0 * 3.0))) + .map(|_| ()) + .boxed() + } else { + finish().boxed() + } + }) } fn villager() -> impl Action { @@ -419,7 +457,34 @@ fn villager() -> impl Action { if let Some(home) = ctx.npc.home { if ctx.npc.current_site != Some(home) { // Travel home if we're not there already - important(travel_to_site(home)) + urgent(travel_to_site(home).debug(move || format!("travel home"))) + } else if DayPeriod::from(ctx.time_of_day.0).is_dark() { + important(now(move |ctx| { + if let Some(house_wpos) = ctx + .state + .data() + .sites + .get(home) + .and_then(|home| ctx.index.sites.get(home.world_site?).site2()) + .and_then(|site2| { + // Find a house + let house = site2 + .plots() + .filter(|p| matches!(p.kind(), PlotKind::House(_))) + .choose(&mut thread_rng())?; + Some(site2.tile_center_wpos(house.root_tile()).as_()) + }) + { + goto_2d(house_wpos, 0.5) + .debug(|| "walk to house") + .then(idle().repeat().debug(|| "wait in house")) + .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) + .map(|_| ()) + .boxed() + } else { + finish().boxed() + } + })) } else if matches!( ctx.npc.profession, Some(Profession::Merchant | Profession::Blacksmith) @@ -440,11 +505,13 @@ fn villager() -> impl Action { { // Walk to the plaza... goto_2d(plaza_wpos, 0.5) + .debug(|| "walk to plaza") // ...then wait for some time before moving on - .then(now(|ctx| { + .then({ let wait_time = thread_rng().gen_range(10.0..30.0); - idle().repeat().stop_if(timeout(ctx, wait_time)) - })) + idle().repeat().stop_if(timeout(wait_time)) + .debug(|| "wait at plaza") + }) .map(|_| ()) .boxed() } else { @@ -459,31 +526,13 @@ fn villager() -> impl Action { casual(finish()) // Nothing to do if we're homeless! } }) + .debug(move || format!("villager")) } fn think() -> impl Action { choose(|ctx| { if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { - // Choose a random site that's fairly close by - if let Some(tgt_site) = ctx - .state - .data() - .sites - .iter() - .filter(|(site_id, site)| { - // TODO: faction.is_some() is used as a proxy for whether the site likely has - // paths, don't do this - site.faction.is_some() - && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) - && thread_rng().gen_bool(0.25) - }) - .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) - .map(|(site_id, _)| site_id) - { - casual(travel_to_site(tgt_site)) - } else { - casual(finish()) - } + casual(adventure()) } else { casual(villager()) } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3edf3fcc5c..3a887e7513 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -185,6 +185,7 @@ fn do_command( ServerChatCommand::Time => handle_time, ServerChatCommand::Tp => handle_tp, ServerChatCommand::TpNpc => handle_tp_npc, + ServerChatCommand::NpcInfo => handle_npc_info, ServerChatCommand::Unban => handle_unban, ServerChatCommand::Version => handle_version, ServerChatCommand::Waypoint => handle_waypoint, @@ -1212,6 +1213,53 @@ fn handle_tp_npc( }) } +fn handle_npc_info( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + use crate::rtsim2::RtSim; + if let Some(id) = parse_cmd_args!(args, u32) { + // TODO: Take some other identifier than an integer to this command. + let rtsim = server.state.ecs().read_resource::(); + let data = rtsim.state().data(); + let npc = data + .npcs + .values() + .nth(id as usize) + .ok_or_else(|| format!("No NPC has index {}", id))?; + + let mut info = String::new(); + + let _ = writeln!(&mut info, "-- General Information --"); + let _ = writeln!(&mut info, "Seed: {}", npc.seed); + let _ = writeln!(&mut info, "Profession: {:?}", npc.profession); + let _ = writeln!(&mut info, "Home: {:?}", npc.home); + let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode); + let _ = writeln!(&mut info, "-- Action State --"); + if let Some(brain) = &npc.brain { + let mut bt = Vec::new(); + brain.action.backtrace(&mut bt); + for (i, action) in bt.into_iter().enumerate() { + let _ = writeln!(&mut info, "[{}] {}", i, action); + } + } else { + let _ = writeln!(&mut info, ""); + } + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, info), + ); + + Ok(()) + } else { + Err(action.help_string()) + } +} + fn handle_spawn( server: &mut Server, client: EcsEntity, From 84eb7b0653ae7550be1f9c6fdda17d9e984cdbee Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 17:15:33 +0000 Subject: [PATCH 048/139] Made adventurers explore sites before moving on --- rtsim/src/rule/npc_ai.rs | 124 +++++++++++++++++--------------- rtsim/src/rule/simulate_npcs.rs | 55 +++++++------- server/src/cmd.rs | 2 + 3 files changed, 95 insertions(+), 86 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 480dd62185..aee8047f85 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -423,7 +423,7 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { } fn adventure() -> impl Action { - now(|ctx| { + choose(|ctx| { // Choose a random site that's fairly close by if let Some(tgt_site) = ctx .state @@ -441,33 +441,43 @@ fn adventure() -> impl Action { .map(|(site_id, _)| site_id) { // Travel to the site - travel_to_site(tgt_site) + important( + travel_to_site(tgt_site) // Stop for a few minutes - .then(villager().stop_if(timeout(60.0 * 3.0))) + .then(villager(tgt_site).repeat().stop_if(timeout(60.0 * 3.0))) .map(|_| ()) - .boxed() + .boxed(), + ) } else { - finish().boxed() + casual(finish().boxed()) } }) + .debug(move || format!("adventure")) } -fn villager() -> impl Action { - choose(|ctx| { - if let Some(home) = ctx.npc.home { - if ctx.npc.current_site != Some(home) { - // Travel home if we're not there already - urgent(travel_to_site(home).debug(move || format!("travel home"))) - } else if DayPeriod::from(ctx.time_of_day.0).is_dark() { - important(now(move |ctx| { +fn villager(visiting_site: SiteId) -> impl Action { + choose(move |ctx| { + if ctx.npc.current_site != Some(visiting_site) { + let npc_home = ctx.npc.home; + // Travel to the site we're supposed to be in + urgent(travel_to_site(visiting_site).debug(move || { + if npc_home == Some(visiting_site) { + format!("travel home") + } else { + format!("travel to visiting site") + } + })) + } else if DayPeriod::from(ctx.time_of_day.0).is_dark() { + important( + now(move |ctx| { if let Some(house_wpos) = ctx .state .data() .sites - .get(home) - .and_then(|home| ctx.index.sites.get(home.world_site?).site2()) + .get(visiting_site) + .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) .and_then(|site2| { - // Find a house + // Find a house in the site we're visiting let house = site2 .plots() .filter(|p| matches!(p.kind(), PlotKind::House(_))) @@ -484,57 +494,55 @@ fn villager() -> impl Action { } else { finish().boxed() } - })) - } else if matches!( - ctx.npc.profession, - Some(Profession::Merchant | Profession::Blacksmith) - ) { - // Trade professions just walk between town plazas - casual(now(move |ctx| { - // Choose a plaza in the NPC's home site to walk to - if let Some(plaza_wpos) = ctx - .state - .data() - .sites - .get(home) - .and_then(|home| ctx.index.sites.get(home.world_site?).site2()) - .and_then(|site2| { - let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - { - // Walk to the plaza... - goto_2d(plaza_wpos, 0.5) - .debug(|| "walk to plaza") - // ...then wait for some time before moving on - .then({ - let wait_time = thread_rng().gen_range(10.0..30.0); - idle().repeat().stop_if(timeout(wait_time)) - .debug(|| "wait at plaza") - }) - .map(|_| ()) - .boxed() - } else { - // No plazas? :( - finish().boxed() - } - })) - } else { - casual(idle()) - } + }) + .debug(|| "find somewhere to sleep"), + ) } else { - casual(finish()) // Nothing to do if we're homeless! + casual(now(move |ctx| { + // Choose a plaza in the site we're visiting to walk to + if let Some(plaza_wpos) = ctx + .state + .data() + .sites + .get(visiting_site) + .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) + .and_then(|site2| { + let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + { + // Walk to the plaza... + goto_2d(plaza_wpos, 0.5) + .debug(|| "walk to plaza") + // ...then wait for some time before moving on + .then({ + let wait_time = thread_rng().gen_range(10.0..30.0); + idle().repeat().stop_if(timeout(wait_time)) + .debug(|| "wait at plaza") + }) + .map(|_| ()) + .boxed() + } else { + // No plazas? :( + finish().boxed() + } + })) } }) - .debug(move || format!("villager")) + .debug(move || format!("villager at site {:?}", visiting_site)) } fn think() -> impl Action { choose(|ctx| { - if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { + if matches!( + ctx.npc.profession, + Some(Profession::Adventurer(_) | Profession::Merchant) + ) { casual(adventure()) + } else if let Some(home) = ctx.npc.home { + casual(villager(home)) } else { - casual(villager()) + casual(finish()) // Homeless } }) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 06a816b6dd..4bb83f02f6 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -9,40 +9,39 @@ impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); - for npc in data - .npcs - .values_mut() - .filter(|npc| matches!(npc.mode, NpcMode::Simulated)) - { - let body = npc.get_body(); - - // Move NPCs if they have a target destination - if let Some((target, speed_factor)) = npc.goto { - let diff = target.xy() - npc.wpos.xy(); - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - npc.wpos += (diff - * (body.max_speed_approx() * speed_factor * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); - } - } - - // Make sure NPCs remain on the surface - npc.wpos.z = ctx - .world - .sim() - .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) - .unwrap_or(0.0); - + for npc in data.npcs.values_mut() { // Update the NPC's current site, if any npc.current_site = ctx .world .sim() .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); + + // Simulate the NPC's movement and interactions + if matches!(npc.mode, NpcMode::Simulated) { + let body = npc.get_body(); + + // Move NPCs if they have a target destination + if let Some((target, speed_factor)) = npc.goto { + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + npc.wpos += (diff + * (body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } + } + + // Make sure NPCs remain on the surface + npc.wpos.z = ctx + .world + .sim() + .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) + .unwrap_or(0.0); + } } }); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3a887e7513..6f7cf7b884 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1237,6 +1237,8 @@ fn handle_npc_info( let _ = writeln!(&mut info, "Seed: {}", npc.seed); let _ = writeln!(&mut info, "Profession: {:?}", npc.profession); let _ = writeln!(&mut info, "Home: {:?}", npc.home); + let _ = writeln!(&mut info, "-- Status --"); + let _ = writeln!(&mut info, "Current site: {:?}", npc.current_site); let _ = writeln!(&mut info, "Current mode: {:?}", npc.mode); let _ = writeln!(&mut info, "-- Action State --"); if let Some(brain) = &npc.brain { From 2aa6ced3572060ca0369575fc1cef977bb2748c5 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 17:22:36 +0000 Subject: [PATCH 049/139] Removed patrol origin from humanoid NPCs --- rtsim/src/rule/npc_ai.rs | 4 +++- server/src/rtsim2/tick.rs | 13 ------------- server/src/sys/terrain.rs | 22 +++++++++++++--------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index aee8047f85..f9c609391f 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -467,7 +467,9 @@ fn villager(visiting_site: SiteId) -> impl Action { format!("travel to visiting site") } })) - } else if DayPeriod::from(ctx.time_of_day.0).is_dark() { + } else if DayPeriod::from(ctx.time_of_day.0).is_dark() + && !matches!(ctx.npc.profession, Some(Profession::Guard)) + { important( now(move |ctx| { if let Some(house_wpos) = ctx diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 03e4327ab9..421a6b24b3 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -260,19 +260,6 @@ impl<'a> System<'a> for Sys { if let Some(agent) = agent { agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos); agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); - // TODO: - // agent.rtsim_controller.heading_to = - // npc.pathing.intersite_path.as_ref(). - // and_then(|(path, _)| { - // Some( - // index - // .sites - // - // .get(data.sites.get(path.end)?.world_site?) - // .name() - // .to_string(), - // ) - // }); } }); } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index ce5765c839..ea6fd4952e 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -513,15 +513,19 @@ impl NpcData { }; let agent = has_agency.then(|| { - comp::Agent::from_body(&body) - .with_behavior( - Behavior::default() - .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK)) - .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE)) - .with_trade_site(trade_for_site), - ) - .with_patrol_origin(pos) - .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee) + let mut agent = comp::Agent::from_body(&body).with_behavior( + Behavior::default() + .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK)) + .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE)) + .with_trade_site(trade_for_site), + ); + + // Non-humanoids get a patrol origin to stop them moving too far + if !matches!(body, comp::Body::Humanoid(_)) { + agent = agent.with_patrol_origin(pos); + } + + agent.with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee) }); let agent = if matches!(alignment, comp::Alignment::Enemy) From 077da13a5f186df2743a33445773ed35e1f25e45 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 20:25:32 +0000 Subject: [PATCH 050/139] Improved herbalist, hunter, farmer, added cultist factions --- assets/common/entity/village/farmer.ron | 23 +++++++ assets/common/entity/village/herbalist.ron | 21 +++++++ assets/common/entity/village/hunter.ron | 23 +++++++ assets/common/loadout/village/farmer.ron | 30 +++++++++ assets/common/loadout/village/herbalist.ron | 26 ++++++++ assets/common/loadout/village/hunter.ron | 28 +++++++++ common/src/rtsim.rs | 3 + rtsim/src/data/faction.rs | 1 + rtsim/src/gen/faction.rs | 5 +- rtsim/src/gen/mod.rs | 67 ++++++++++++++------- rtsim/src/gen/site.rs | 34 ++++++++--- rtsim/src/rule/npc_ai.rs | 31 ++++++---- rtsim/src/rule/setup.rs | 9 ++- server/src/rtsim2/tick.rs | 21 ++++++- 14 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 assets/common/entity/village/farmer.ron create mode 100644 assets/common/entity/village/herbalist.ron create mode 100644 assets/common/entity/village/hunter.ron create mode 100644 assets/common/loadout/village/farmer.ron create mode 100644 assets/common/loadout/village/herbalist.ron create mode 100644 assets/common/loadout/village/hunter.ron diff --git a/assets/common/entity/village/farmer.ron b/assets/common/entity/village/farmer.ron new file mode 100644 index 0000000000..77c8f1469c --- /dev/null +++ b/assets/common/entity/village/farmer.ron @@ -0,0 +1,23 @@ +#![enable(implicit_some)] +( + name: Name("Farmer"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.farmer"), + active_hands: InHands((Choice([ + (1, Item("common.items.weapons.tool.hoe")), + (1, Item("common.items.weapons.tool.rake")), + (1, Item("common.items.weapons.tool.shovel-0")), + (1, Item("common.items.weapons.tool.shovel-1")), + ]), None)), + )), + items: [ + (10, "common.items.food.cheese"), + (10, "common.items.food.plainsalad"), + ], + ), + meta: [], +) diff --git a/assets/common/entity/village/herbalist.ron b/assets/common/entity/village/herbalist.ron new file mode 100644 index 0000000000..db8bea4a03 --- /dev/null +++ b/assets/common/entity/village/herbalist.ron @@ -0,0 +1,21 @@ +#![enable(implicit_some)] +( + name: Name("Herbalist"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.herbalist"), + active_hands: InHands((Choice([ + (1, Item("common.items.weapons.tool.hoe")), + (1, Item("common.items.weapons.tool.rake")), + ]), None)), + )), + items: [ + (10, "common.items.food.cheese"), + (10, "common.items.food.plainsalad"), + ], + ), + meta: [], +) diff --git a/assets/common/entity/village/hunter.ron b/assets/common/entity/village/hunter.ron new file mode 100644 index 0000000000..d0304584c2 --- /dev/null +++ b/assets/common/entity/village/hunter.ron @@ -0,0 +1,23 @@ +#![enable(implicit_some)] +( + name: Name("Hunter"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.hunter"), + active_hands: InHands((Choice([ + (8, ModularWeapon(tool: Bow, material: Wood, hands: None)), + (4, ModularWeapon(tool: Bow, material: Bamboo, hands: None)), + (2, ModularWeapon(tool: Bow, material: Hardwood, hands: None)), + (2, ModularWeapon(tool: Bow, material: Ironwood, hands: None)), + (1, ModularWeapon(tool: Bow, material: Eldwood, hands: None)), + ]), None)), + )), + items: [ + (10, "common.items.consumable.potion_big"), + ], + ), + meta: [], +) diff --git a/assets/common/loadout/village/farmer.ron b/assets/common/loadout/village/farmer.ron new file mode 100644 index 0000000000..14f646383a --- /dev/null +++ b/assets/common/loadout/village/farmer.ron @@ -0,0 +1,30 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (3, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.bamboo_twig")), + (2, None), + ]), + chest: Choice([ + (1, Item("common.items.armor.misc.chest.worker_green_0")), + (1, Item("common.items.armor.misc.chest.worker_green_1")), + (1, Item("common.items.armor.misc.chest.worker_red_0")), + (1, Item("common.items.armor.misc.chest.worker_red_1")), + (1, Item("common.items.armor.misc.chest.worker_purple_0")), + (1, Item("common.items.armor.misc.chest.worker_purple_1")), + (1, Item("common.items.armor.misc.chest.worker_yellow_0")), + (1, Item("common.items.armor.misc.chest.worker_yellow_1")), + (1, Item("common.items.armor.misc.chest.worker_orange_0")), + (1, Item("common.items.armor.misc.chest.worker_orange_1")), + ]), + legs: Choice([ + (1, Item("common.items.armor.misc.pants.worker_blue")), + (1, Item("common.items.armor.misc.pants.worker_brown")), + ]), + feet: Choice([ + (1, Item("common.items.armor.misc.foot.sandals")), + (1, Item("common.items.armor.cloth_blue.foot")), + ]), +) diff --git a/assets/common/loadout/village/herbalist.ron b/assets/common/loadout/village/herbalist.ron new file mode 100644 index 0000000000..e41a4f4fdb --- /dev/null +++ b/assets/common/loadout/village/herbalist.ron @@ -0,0 +1,26 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (3, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.hood")), + (2, None), + ]), + chest: Choice([ + (1, Item("common.items.armor.twigs.chest")), + (1, Item("common.items.armor.twigsflowers.chest")), + (1, Item("common.items.armor.twigsleaves.chest")), + ]), + legs: Choice([ + (1, Item("common.items.armor.twigs.pants")), + (1, Item("common.items.armor.twigsflowers.pants")), + (1, Item("common.items.armor.twigsleaves.pants")), + ]), + feet: Choice([ + (1, Item("common.items.armor.twigs.foot")), + (1, Item("common.items.armor.twigsflowers.foot")), + (1, Item("common.items.armor.twigsleaves.foot")), + (1, Item("common.items.armor.misc.foot.sandals")), + ]), +) diff --git a/assets/common/loadout/village/hunter.ron b/assets/common/loadout/village/hunter.ron new file mode 100644 index 0000000000..8a4933751d --- /dev/null +++ b/assets/common/loadout/village/hunter.ron @@ -0,0 +1,28 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Choice([ + (6, None), + (2, Item("common.items.armor.misc.head.straw")), + (3, Item("common.items.armor.misc.head.hood")), + (3, Item("common.items.armor.misc.head.hood_dark")), + ]), + chest: Choice([ + (1, Item("common.items.armor.hide.leather.chest")), + (1, Item("common.items.armor.hide.rawhide.chest")), + (1, Item("common.items.armor.hide.primal.chest")), + ]), + legs: Choice([ + (1, Item("common.items.armor.hide.leather.pants")), + (1, Item("common.items.armor.hide.rawhide.pants")), + (1, Item("common.items.armor.hide.primal.pants")), + ]), + feet: Choice([ + (1, None), + (2, Item("common.items.armor.misc.foot.sandals")), + (4, Item("common.items.armor.hide.leather.foot")), + (4, Item("common.items.armor.hide.rawhide.foot")), + (4, Item("common.items.armor.hide.primal.foot")), + ]), +) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index b672f073aa..8e34c97726 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -121,6 +121,8 @@ pub enum Profession { Pirate, #[serde(rename = "9")] Cultist, + #[serde(rename = "10")] + Herbalist, } impl Profession { @@ -136,6 +138,7 @@ impl Profession { Self::Alchemist => "Alchemist".to_string(), Self::Pirate => "Pirate".to_string(), Self::Cultist => "Cultist".to_string(), + Self::Herbalist => "Herbalist".to_string(), } } } diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs index d27c294e00..921d0cad58 100644 --- a/rtsim/src/data/faction.rs +++ b/rtsim/src/data/faction.rs @@ -10,6 +10,7 @@ use vek::*; #[derive(Clone, Serialize, Deserialize)] pub struct Faction { pub leader: Option, + pub good_or_evil: bool, // TODO: Very stupid, get rid of this } #[derive(Clone, Serialize, Deserialize)] diff --git a/rtsim/src/gen/faction.rs b/rtsim/src/gen/faction.rs index ed09d71594..b7a4c073dc 100644 --- a/rtsim/src/gen/faction.rs +++ b/rtsim/src/gen/faction.rs @@ -5,6 +5,9 @@ use world::{IndexRef, World}; impl Faction { pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { - Self { leader: None } + Self { + leader: None, + good_or_evil: rng.gen(), + } } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index eaf7e09ee0..e54a396fee 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -40,7 +40,7 @@ impl Data { time_of_day: TimeOfDay(settings.start_time), }; - let initial_factions = (0..10) + let initial_factions = (0..16) .map(|_| { let faction = Faction::generate(world, index, &mut rng); let wpos = world @@ -56,7 +56,13 @@ impl Data { // Register sites with rtsim for (world_site_id, _) in index.sites.iter() { - let site = Site::generate(world_site_id, world, index, &initial_factions); + let site = Site::generate( + world_site_id, + world, + index, + &initial_factions, + &this.factions, + ); this.sites.create(site); } info!( @@ -66,32 +72,51 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() - // TODO: Stupid - .filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) - .skip(1) - .take(1) + // TODO: Stupid + // .filter(|(_, site)| site.world_site.map_or(false, |ws| + // matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) + // .take(1) { + let good_or_evil = site + .faction + .and_then(|f| this.factions.get(f)) + .map_or(true, |f| f.good_or_evil); + let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); wpos2d .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..16 { - this.npcs.create( - Npc::new(rng.gen(), rand_wpos(&mut rng)) - .with_faction(site.faction) - .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=10 => Profession::Farmer, - 11..=15 => Profession::Guard, - _ => Profession::Adventurer(rng.gen_range(0..=3)), - }), - ); + if good_or_evil { + for _ in 0..32 { + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=8 => Profession::Farmer, + 9..=10 => Profession::Herbalist, + 11..=15 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), + }), + ); + } + } else { + for _ in 0..5 { + this.npcs.create( + Npc::new(rng.gen(), rand_wpos(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + _ => Profession::Cultist, + }), + ); + } } this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index d871360ca3..8325146ac4 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,4 +1,4 @@ -use crate::data::{FactionId, Site}; +use crate::data::{FactionId, Factions, Site}; use common::store::Id; use vek::*; use world::{ @@ -12,24 +12,42 @@ impl Site { world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)], + factions: &Factions, ) -> Self { let world_site = index.sites.get(world_site_id); let wpos = world_site.get_origin(); + // TODO: This is stupid, do better + let good_or_evil = match &world_site.kind { + // Good + SiteKind::Refactor(_) + | SiteKind::CliffTown(_) + | SiteKind::DesertCity(_) + | SiteKind::SavannahPit(_) => Some(true), + // Evil + SiteKind::Dungeon(_) | SiteKind::ChapelSite(_) | SiteKind::Gnarling(_) => Some(false), + // Neutral + SiteKind::Settlement(_) + | SiteKind::Castle(_) + | SiteKind::Tree(_) + | SiteKind::GiantTree(_) + | SiteKind::Bridge(_) => None, + }; + Self { wpos, world_site: Some(world_site_id), - faction: if matches!( - &world_site.kind, - SiteKind::Refactor(_) | SiteKind::CliffTown(_) | SiteKind::DesertCity(_) - ) { + faction: good_or_evil.and_then(|good_or_evil| { nearby_factions .iter() + .filter(|(_, faction)| { + factions + .get(*faction) + .map_or(false, |f| f.good_or_evil == good_or_evil) + }) .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) .map(|(_, faction)| *faction) - } else { - None - }, + }), } } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index f9c609391f..9fd1e5d2c3 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -317,10 +317,9 @@ impl Rule for NpcAi { fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") } /// Try to walk toward a 3D position without caring for obstacles. -fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { +fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { const STEP_DIST: f32 = 24.0; const WAYPOINT_DIST: f32 = 12.0; - const GOAL_DIST: f32 = 2.0; let mut waypoint = None; @@ -342,19 +341,19 @@ fn goto(wpos: Vec3, speed_factor: f32) -> impl Action { ctx.controller.goto = Some((*waypoint, speed_factor)); }) .repeat() - .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < GOAL_DIST.powi(2)) + .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z)) .map(|_| {}) } /// Try to walk toward a 2D position on the terrain without caring for /// obstacles. -fn goto_2d(wpos2d: Vec2, speed_factor: f32) -> impl Action { +fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action { const MIN_DIST: f32 = 2.0; now(move |ctx| { let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); - goto(wpos, speed_factor) + goto(wpos, speed_factor, goal_dist) }) } @@ -399,7 +398,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()); // Walk toward the node - goto_2d(node_wpos.as_(), 1.0) + goto_2d(node_wpos.as_(), 1.0, 8.0) .debug(move || format!("traversing track node ({}/{})", i + 1, track_len)) }))) }) @@ -407,7 +406,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { .boxed() } else if let Some(site) = sites.get(tgt_site) { // If all else fails, just walk toward the target site in a straight line - goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0).boxed() + goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0, 8.0).boxed() } else { // If we can't find a way to get to the site at all, there's nothing more to be done finish().boxed() @@ -431,10 +430,16 @@ fn adventure() -> impl Action { .sites .iter() .filter(|(site_id, site)| { - // TODO: faction.is_some() is used as a proxy for whether the site likely has - // paths, don't do this - site.faction.is_some() - && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) + // Only path toward towns + matches!( + site.world_site.map(|ws| &ctx.index.sites.get(ws).kind), + Some( + SiteKind::Refactor(_) + | SiteKind::CliffTown(_) + | SiteKind::SavannahPit(_) + | SiteKind::DesertCity(_) + ), + ) && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) && thread_rng().gen_bool(0.25) }) .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) @@ -487,7 +492,7 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - goto_2d(house_wpos, 0.5) + goto_2d(house_wpos, 0.5, 1.0) .debug(|| "walk to house") .then(idle().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) @@ -514,7 +519,7 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { // Walk to the plaza... - goto_2d(plaza_wpos, 0.5) + goto_2d(plaza_wpos, 0.5, 8.0) .debug(|| "walk to plaza") // ...then wait for some time before moving on .then({ diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index eebec822e5..c6636ef6c1 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -48,8 +48,13 @@ impl Rule for Setup { be generated afresh.", world_site_id ); - data.sites - .create(Site::generate(world_site_id, ctx.world, ctx.index, &[])); + data.sites.create(Site::generate( + world_site_id, + ctx.world, + ctx.index, + &[], + &data.factions, + )); } } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 421a6b24b3..586c6a9f48 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -23,7 +23,9 @@ use world::site::settlement::trader_loadout; fn humanoid_config(profession: &Profession) -> &'static str { match profession { - Profession::Farmer | Profession::Hunter => "common.entity.village.villager", + Profession::Farmer => "common.entity.village.farmer", + Profession::Hunter => "common.entity.village.hunter", + Profession::Herbalist => "common.entity.village.herbalist", Profession::Merchant => "common.entity.village.merchant", Profession::Guard => "common.entity.village.guard", Profession::Adventurer(rank) => match rank { @@ -59,6 +61,15 @@ fn farmer_loadout( trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) } +fn herbalist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Ingredients) + }) +} + fn chef_loadout( loadout_builder: LoadoutBuilder, economy: Option<&SiteInformation>, @@ -90,6 +101,7 @@ fn profession_extra_loadout( match profession { Some(Profession::Merchant) => merchant_loadout, Some(Profession::Farmer) => farmer_loadout, + Some(Profession::Herbalist) => herbalist_loadout, Some(Profession::Chef) => chef_loadout, Some(Profession::Blacksmith) => blacksmith_loadout, Some(Profession::Alchemist) => alchemist_loadout, @@ -102,6 +114,7 @@ fn profession_agent_mark(profession: Option<&Profession>) -> Option EntityInfo let mut rng = npc.rng(3); EntityInfo::at(pos.0) .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(comp::Alignment::Npc) + .with_alignment(if matches!(profession, Profession::Cultist) { + comp::Alignment::Enemy + } else { + comp::Alignment::Npc + }) .with_maybe_economy(economy.as_ref()) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref())) From c4032ee0243f02c5c7391383fa2e550c256d0d40 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 21:31:20 +0000 Subject: [PATCH 051/139] Parallelised rtsim NPC AI --- Cargo.lock | 1 + rtsim/Cargo.toml | 1 + rtsim/src/gen/mod.rs | 4 +- rtsim/src/rule/npc_ai.rs | 154 ++++++++++++++++++++------------------- 4 files changed, 85 insertions(+), 75 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25bb316998..7b04dd4df1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6952,6 +6952,7 @@ dependencies = [ "hashbrown 0.12.3", "itertools", "rand 0.8.5", + "rayon", "rmp-serde", "ron 0.8.0", "serde", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 321558b7b3..fa2fd17800 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -19,3 +19,4 @@ slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } fxhash = "0.2.1" itertools = "0.10.3" +rayon = "1.5" diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index e54a396fee..a62f9c9ec6 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -89,7 +89,7 @@ impl Data { .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; if good_or_evil { - for _ in 0..32 { + for _ in 0..250 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) @@ -107,7 +107,7 @@ impl Data { ); } } else { - for _ in 0..5 { + for _ in 0..15 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 9fd1e5d2c3..1e64132a47 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -21,6 +21,7 @@ use common::{ use fxhash::FxHasher64; use itertools::Itertools; use rand::prelude::*; +use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use std::{ any::{Any, TypeId}, marker::PhantomData, @@ -230,84 +231,91 @@ const MAX_STEP: f32 = 32.0; impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|mut ctx| { - let npc_ids = ctx.state.data().npcs.keys().collect::>(); - - for npc_id in npc_ids { - let mut brain = ctx.state.data_mut().npcs[npc_id] - .brain - .take() - .unwrap_or_else(|| Brain { - action: Box::new(think().repeat()), - }); - - let controller = { - let data = &*ctx.state.data(); - let npc = &data.npcs[npc_id]; - - let mut controller = Controller { goto: npc.goto }; - - brain.action.tick(&mut NpcCtx { - state: ctx.state, - world: ctx.world, - index: ctx.index, - time_of_day: ctx.event.time_of_day, - time: ctx.event.time, - npc, - npc_id, - controller: &mut controller, - }); - - /* - let action: ControlFlow<()> = try { - brain.tick(&mut NpcData { - ctx: &ctx, - npc, - npc_id, - controller: &mut controller, + let mut npc_data = { + let mut data = ctx.state.data_mut(); + data.npcs + .iter_mut() + .map(|(npc_id, npc)| { + let controller = Controller { goto: npc.goto }; + let brain = npc.brain.take().unwrap_or_else(|| Brain { + action: Box::new(think().repeat()), }); - /* - // // Choose a random plaza in the npcs home site (which should be the - // // current here) to go to. - let task = - generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { - let data = ctx.state.data(); - let site2 = - npc.home.and_then(|home| data.sites.get(home)).and_then( - |home| match &ctx.index.sites.get(home.world_site?).kind - { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }, - ); + (npc_id, controller, brain) + }) + .collect::>() + }; - let wpos = site2 - .and_then(|site2| { - let plaza = &site2.plots - [site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - .unwrap_or(npc.wpos.xy()); + { + let data = &*ctx.state.data(); - TravelTo { - wpos, - use_paths: true, - } - }) - .repeat(); + npc_data + .par_iter_mut() + .for_each(|(npc_id, controller, brain)| { + let npc = &data.npcs[*npc_id]; - task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; - */ - }; - */ - - controller - }; - - ctx.state.data_mut().npcs[npc_id].goto = controller.goto; - ctx.state.data_mut().npcs[npc_id].brain = Some(brain); + brain.action.tick(&mut NpcCtx { + state: ctx.state, + world: ctx.world, + index: ctx.index, + time_of_day: ctx.event.time_of_day, + time: ctx.event.time, + npc, + npc_id: *npc_id, + controller, + }); + }); } + + let mut data = ctx.state.data_mut(); + for (npc_id, controller, brain) in npc_data { + data.npcs[npc_id].goto = controller.goto; + data.npcs[npc_id].brain = Some(brain); + } + + /* + let action: ControlFlow<()> = try { + brain.tick(&mut NpcData { + ctx: &ctx, + npc, + npc_id, + controller: &mut controller, + }); + /* + // // Choose a random plaza in the npcs home site (which should be the + // // current here) to go to. + let task = + generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { + let data = ctx.state.data(); + let site2 = + npc.home.and_then(|home| data.sites.get(home)).and_then( + |home| match &ctx.index.sites.get(home.world_site?).kind + { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + }, + ); + + let wpos = site2 + .and_then(|site2| { + let plaza = &site2.plots + [site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + .unwrap_or(npc.wpos.xy()); + + TravelTo { + wpos, + use_paths: true, + } + }) + .repeat(); + + task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; + */ + }; + */ }); Ok(Self) From 94390331e0c182dda46aec69274398e54ce4b848 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 5 Jan 2023 23:15:43 +0000 Subject: [PATCH 052/139] Rtsim death event --- rtsim/src/event.rs | 8 +++++++- rtsim/src/gen/mod.rs | 7 ++++--- server/src/events/entity_manipulation.rs | 11 +++++++++-- server/src/rtsim2/mod.rs | 12 +++++++++--- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/rtsim/src/event.rs b/rtsim/src/event.rs index 1ae5cbb6bb..c1a32a85e0 100644 --- a/rtsim/src/event.rs +++ b/rtsim/src/event.rs @@ -1,4 +1,4 @@ -use super::{RtState, Rule}; +use crate::{data::NpcId, RtState, Rule}; use common::resources::{Time, TimeOfDay}; use world::{IndexRef, World}; @@ -23,3 +23,9 @@ pub struct OnTick { pub dt: f32, } impl Event for OnTick {} + +#[derive(Clone)] +pub struct OnDeath { + pub npc_id: NpcId, +} +impl Event for OnDeath {} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index a62f9c9ec6..2f90c2c805 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -77,10 +77,11 @@ impl Data { // matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) // .take(1) { - let good_or_evil = site + let Some(good_or_evil) = site .faction .and_then(|f| this.factions.get(f)) - .map_or(true, |f| f.good_or_evil); + .map(|f| f.good_or_evil) + else { continue }; let rand_wpos = |rng: &mut SmallRng| { let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); @@ -89,7 +90,7 @@ impl Data { .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; if good_or_evil { - for _ in 0..250 { + for _ in 0..64 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 0c9944731d..f596e94292 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -43,7 +43,7 @@ use rand_distr::Distribution; use specs::{ join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, }; -use std::{collections::HashMap, iter, time::Duration}; +use std::{collections::HashMap, iter, sync::Arc, time::Duration}; use tracing::{debug, error}; use vek::{Vec2, Vec3}; @@ -528,7 +528,14 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt state .ecs() .write_resource::() - .hook_rtsim_entity_delete(rtsim_entity); + .hook_rtsim_entity_delete( + &state.ecs().read_resource::>(), + state + .ecs() + .read_resource::() + .as_index_ref(), + rtsim_entity, + ); } if let Err(e) = state.delete_entity_recorded(entity) { diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 45eba51d8f..8ee595bb8f 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -13,7 +13,7 @@ use common_ecs::{dispatch, System}; use enum_map::EnumMap; use rtsim2::{ data::{npc::NpcMode, Data, ReadError}, - event::OnSetup, + event::{OnDeath, OnSetup}, rule::Rule, RtState, }; @@ -154,8 +154,14 @@ impl RtSim { } } - pub fn hook_rtsim_entity_delete(&mut self, entity: RtSimEntity) { - // TODO: Emit event on deletion to catch death? + pub fn hook_rtsim_entity_delete( + &mut self, + world: &World, + index: IndexRef, + entity: RtSimEntity, + ) { + // Should entity deletion be death? They're not exactly the same thing... + self.state.emit(OnDeath { npc_id: entity.0 }, world, index); self.state.data_mut().npcs.remove(entity.0); } From bb96e9236203ed0abb1dc38d0a679a314c3411a7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 6 Jan 2023 01:16:15 +0000 Subject: [PATCH 053/139] Track almost all collectable sprites, added resource replenishment --- common/src/cmd.rs | 7 ++ common/src/rtsim.rs | 20 +++++- common/src/terrain/block.rs | 76 ++++++++++++++++++++- rtsim/src/lib.rs | 1 + rtsim/src/rule.rs | 1 + rtsim/src/rule/replenish_resources.rs | 41 +++++++++++ server/src/cmd.rs | 56 +++++++++++++++ server/src/rtsim2/mod.rs | 6 +- server/src/rtsim2/rule/deplete_resources.rs | 5 +- world/src/layer/scatter.rs | 7 +- 10 files changed, 205 insertions(+), 15 deletions(-) create mode 100644 rtsim/src/rule/replenish_resources.rs diff --git a/common/src/cmd.rs b/common/src/cmd.rs index a80ba24f6c..06621bc5d7 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -312,6 +312,7 @@ pub enum ServerChatCommand { Tp, TpNpc, NpcInfo, + RtsimChunk, Unban, Version, Waypoint, @@ -691,6 +692,11 @@ impl ServerChatCommand { "Display information about an rtsim NPC", Some(Moderator), ), + ServerChatCommand::RtsimChunk => cmd( + vec![], + "Display information about the current chunk from rtsim", + Some(Moderator), + ), ServerChatCommand::Unban => cmd( vec![PlayerName(Required)], "Remove the ban for the given username", @@ -815,6 +821,7 @@ impl ServerChatCommand { ServerChatCommand::Tp => "tp", ServerChatCommand::TpNpc => "tp_npc", ServerChatCommand::NpcInfo => "npc_info", + ServerChatCommand::RtsimChunk => "rtsim_chunk", ServerChatCommand::Unban => "unban", ServerChatCommand::Version => "version", ServerChatCommand::Waypoint => "waypoint", diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 8e34c97726..ce29f9630b 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -94,9 +94,25 @@ pub enum ChunkResource { #[serde(rename = "0")] Grass, #[serde(rename = "1")] - Flax, + Flower, #[serde(rename = "2")] - Cotton, + Fruit, + #[serde(rename = "3")] + Vegetable, + #[serde(rename = "4")] + Mushroom, + #[serde(rename = "5")] + Loot, // Chests, boxes, potions, etc. + #[serde(rename = "6")] + Plant, // Flax, cotton, wheat, corn, etc. + #[serde(rename = "7")] + Stone, + #[serde(rename = "8")] + Wood, // Twigs, logs, bamboo, etc. + #[serde(rename = "9")] + Gem, // Amethyst, diamond, etc. + #[serde(rename = "a")] + Ore, // Iron, copper, etc. } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 614b424bb6..ba8a8379d1 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -200,6 +200,37 @@ impl Block { #[inline] pub fn get_rtsim_resource(&self) -> Option { match self.get_sprite()? { + SpriteKind::Stones => Some(rtsim::ChunkResource::Stone), + SpriteKind::Twigs + | SpriteKind::Wood + | SpriteKind::Bamboo + | SpriteKind::Hardwood + | SpriteKind::Ironwood + | SpriteKind::Frostwood + | SpriteKind::Eldwood => Some(rtsim::ChunkResource::Wood), + SpriteKind::Amethyst + | SpriteKind::Ruby + | SpriteKind::Sapphire + | SpriteKind::Emerald + | SpriteKind::Topaz + | SpriteKind::Diamond + | SpriteKind::AmethystSmall + | SpriteKind::TopazSmall + | SpriteKind::DiamondSmall + | SpriteKind::RubySmall + | SpriteKind::EmeraldSmall + | SpriteKind::SapphireSmall + | SpriteKind::CrystalHigh + | SpriteKind::CrystalLow => Some(rtsim::ChunkResource::Gem), + SpriteKind::Bloodstone + | SpriteKind::Coal + | SpriteKind::Cobalt + | SpriteKind::Copper + | SpriteKind::Iron + | SpriteKind::Tin + | SpriteKind::Silver + | SpriteKind::Gold => Some(rtsim::ChunkResource::Ore), + SpriteKind::LongGrass | SpriteKind::MediumGrass | SpriteKind::ShortGrass @@ -209,9 +240,48 @@ impl Block { | SpriteKind::SavannaGrass | SpriteKind::TallSavannaGrass | SpriteKind::RedSavannaGrass - | SpriteKind::JungleRedGrass => Some(rtsim::ChunkResource::Grass), - SpriteKind::WildFlax => Some(rtsim::ChunkResource::Flax), - SpriteKind::Cotton => Some(rtsim::ChunkResource::Cotton), + | SpriteKind::JungleRedGrass + | SpriteKind::Fern => Some(rtsim::ChunkResource::Grass), + SpriteKind::BlueFlower + | SpriteKind::PinkFlower + | SpriteKind::PurpleFlower + | SpriteKind::RedFlower + | SpriteKind::WhiteFlower + | SpriteKind::YellowFlower + | SpriteKind::Sunflower + | SpriteKind::Moonbell + | SpriteKind::Pyrebloom => Some(rtsim::ChunkResource::Flower), + SpriteKind::Reed + | SpriteKind::Flax + | SpriteKind::WildFlax + | SpriteKind::Cotton + | SpriteKind::Corn + | SpriteKind::WheatYellow + | SpriteKind::WheatGreen => Some(rtsim::ChunkResource::Plant), + SpriteKind::Apple + | SpriteKind::Pumpkin + | SpriteKind::Beehive // TODO: Not a fruit, but kind of acts like one + | SpriteKind::Coconut => Some(rtsim::ChunkResource::Fruit), + SpriteKind::Cabbage + | SpriteKind::Carrot + | SpriteKind::Tomato + | SpriteKind::Radish + | SpriteKind::Turnip => Some(rtsim::ChunkResource::Vegetable), + SpriteKind::Mushroom + | SpriteKind::CaveMushroom + | SpriteKind::CeilingMushroom => Some(rtsim::ChunkResource::Mushroom), + + SpriteKind::Chest + | SpriteKind::ChestBuried + | SpriteKind::PotionMinor + | SpriteKind::DungeonChest0 + | SpriteKind::DungeonChest1 + | SpriteKind::DungeonChest2 + | SpriteKind::DungeonChest3 + | SpriteKind::DungeonChest4 + | SpriteKind::DungeonChest5 + | SpriteKind::CoralChest + | SpriteKind::Crate => Some(rtsim::ChunkResource::Loot), _ => None, } } diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index cc73f3ec92..1ae1622b8d 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -62,6 +62,7 @@ impl RtState { fn start_default_rules(&mut self) { info!("Starting default rtsim rules..."); self.start_rule::(); + self.start_rule::(); self.start_rule::(); self.start_rule::(); } diff --git a/rtsim/src/rule.rs b/rtsim/src/rule.rs index 5444d0a2c7..a58dd20493 100644 --- a/rtsim/src/rule.rs +++ b/rtsim/src/rule.rs @@ -1,4 +1,5 @@ pub mod npc_ai; +pub mod replenish_resources; pub mod setup; pub mod simulate_npcs; diff --git a/rtsim/src/rule/replenish_resources.rs b/rtsim/src/rule/replenish_resources.rs new file mode 100644 index 0000000000..e1838f8883 --- /dev/null +++ b/rtsim/src/rule/replenish_resources.rs @@ -0,0 +1,41 @@ +use crate::{event::OnTick, RtState, Rule, RuleError}; +use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use rand::prelude::*; +use tracing::info; +use vek::*; + +pub struct ReplenishResources; + +/// Take 1 hour to replenish resources entirely. Makes farming unviable, but +/// probably still poorly balanced. +// TODO: Different rates for different resources? +// TODO: Non-renewable resources? +pub const REPLENISH_TIME: f32 = 60.0 * 60.0; +/// How many chunks should be replenished per tick? +pub const REPLENISH_PER_TICK: usize = 100000; + +impl Rule for ReplenishResources { + fn start(rtstate: &mut RtState) -> Result { + rtstate.bind::(|ctx| { + let world_size = ctx.world.sim().get_size(); + let mut data = ctx.state.data_mut(); + + // How much should be replenished for each chosen chunk to hit our target + // replenishment rate? + let replenish_amount = world_size.product() as f32 * ctx.event.dt + / REPLENISH_TIME + / REPLENISH_PER_TICK as f32; + for _ in 0..REPLENISH_PER_TICK { + let key = world_size.map(|e| thread_rng().gen_range(0..e as i32)); + + let mut res = data.nature.get_chunk_resources(key); + for (_, res) in &mut res { + *res = (*res + replenish_amount).clamp(0.0, 1.0); + } + data.nature.set_chunk_resources(key, res); + } + }); + + Ok(Self) + } +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 6f7cf7b884..e724894f88 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -186,6 +186,7 @@ fn do_command( ServerChatCommand::Tp => handle_tp, ServerChatCommand::TpNpc => handle_tp_npc, ServerChatCommand::NpcInfo => handle_npc_info, + ServerChatCommand::RtsimChunk => handle_rtsim_chunk, ServerChatCommand::Unban => handle_unban, ServerChatCommand::Version => handle_version, ServerChatCommand::Waypoint => handle_waypoint, @@ -1262,6 +1263,61 @@ fn handle_npc_info( } } +fn handle_rtsim_chunk( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + use crate::rtsim2::{ChunkStates, RtSim}; + let pos = position(server, target, "target")?; + + let chunk_key = pos.0.xy().map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { + e as i32 / sz as i32 + }); + + let rtsim = server.state.ecs().read_resource::(); + let data = rtsim.state().data(); + + let chunk_states = rtsim.state().resource::(); + let chunk_state = match chunk_states.0.get(chunk_key) { + Some(Some(chunk_state)) => chunk_state, + Some(None) => return Err(format!("Chunk {}, {} not loaded", chunk_key.x, chunk_key.y)), + None => { + return Err(format!( + "Chunk {}, {} not within map bounds", + chunk_key.x, chunk_key.y + )); + }, + }; + + let mut info = String::new(); + let _ = writeln!( + &mut info, + "-- Chunk {}, {} Resources --", + chunk_key.x, chunk_key.y + ); + for (res, frac) in data.nature.get_chunk_resources(chunk_key) { + let total = chunk_state.max_res[res]; + let _ = writeln!( + &mut info, + "{:?}: {} / {} ({}%)", + res, + frac * total as f32, + total, + frac * 100.0 + ); + } + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, info), + ); + + Ok(()) +} + fn handle_spawn( server: &mut Server, client: EcsEntity, diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 8ee595bb8f..7d8ee64ce7 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -208,11 +208,11 @@ impl RtSim { pub fn state(&self) -> &RtState { &self.state } } -struct ChunkStates(pub Grid>); +pub struct ChunkStates(pub Grid>); -struct LoadedChunkState { +pub struct LoadedChunkState { // The maximum possible number of each resource in this chunk - max_res: EnumMap, + pub max_res: EnumMap, } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs index 05bfe43789..810af3aaed 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -1,14 +1,11 @@ use crate::rtsim2::{event::OnBlockChange, ChunkStates}; use common::{terrain::TerrainChunk, vol::RectRasterableVol}; use rtsim2::{RtState, Rule, RuleError}; -use tracing::info; pub struct DepleteResources; impl Rule for DepleteResources { fn start(rtstate: &mut RtState) -> Result { - info!("Hello from the resource depletion rule!"); - rtstate.bind::(|ctx| { let key = ctx .event @@ -35,7 +32,7 @@ impl Rule for DepleteResources { / chunk_state.max_res[res] as f32; } } - println!("Chunk resources = {:?}", chunk_res); + //println!("Chunk resources = {:?}", chunk_res); ctx.state .data_mut() .nature diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index e59de70cc3..936c204f3f 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -1,4 +1,4 @@ -use crate::{column::ColumnSample, sim::SimChunk, Canvas, CONFIG}; +use crate::{column::ColumnSample, sim::SimChunk, util::RandomField, Canvas, CONFIG}; use common::{ calendar::{Calendar, CalendarEvent}, terrain::{Block, BlockKind, SpriteKind}, @@ -13,7 +13,7 @@ pub fn close(x: f32, tgt: f32, falloff: f32) -> f32 { (1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.125) } -/// Returns a decimal value between 0 and 1. +/// Returns a decimal value between 0 and 1. /// The density is maximum at the middle of the highest and the lowest allowed /// altitudes, and zero otherwise. Quadratic curve. /// @@ -1071,7 +1071,8 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng, calendar: Optio }) .unwrap_or(density); if density > 0.0 - && rng.gen::() < density //RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density) + // Now deterministic, chunk resources are tracked by rtsim + && /*rng.gen::() < density*/ RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density) && matches!(&water_mode, Underwater | Floating) == underwater { Some((*kind, water_mode)) From 191f3622925490fa03192380ea9a9fff4d40c0e9 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 12 Jan 2023 15:02:53 +0000 Subject: [PATCH 054/139] Incremental point traversal --- rtsim/src/ai/mod.rs | 92 ++++++++++++++++- rtsim/src/gen/mod.rs | 8 +- rtsim/src/rule/npc_ai.rs | 213 ++++++++++++++++++++++++++------------- 3 files changed, 236 insertions(+), 77 deletions(-) diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 05f4fd3c40..2c7e072583 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -194,10 +194,42 @@ impl Action for Box> { fn reset(&mut self) { (**self).reset(); } - // TODO: Reset closure state? fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { (**self).tick(ctx) } } +impl, B: Action> Action for itertools::Either { + fn is_same(&self, other: &Self) -> bool { + match (self, other) { + (itertools::Either::Left(x), itertools::Either::Left(y)) => x.is_same(y), + (itertools::Either::Right(x), itertools::Either::Right(y)) => x.is_same(y), + _ => false, + } + } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn backtrace(&self, bt: &mut Vec) { + match self { + itertools::Either::Left(x) => x.backtrace(bt), + itertools::Either::Right(x) => x.backtrace(bt), + } + } + + fn reset(&mut self) { + match self { + itertools::Either::Left(x) => x.reset(), + itertools::Either::Right(x) => x.reset(), + } + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + match self { + itertools::Either::Left(x) => x.tick(ctx), + itertools::Either::Right(x) => x.tick(ctx), + } + } +} + // Now /// See [`now`]. @@ -220,6 +252,7 @@ impl A + Send + Sync + 'stati } } + // TODO: Reset closure? fn reset(&mut self) { self.1 = None; } // TODO: Reset closure state? @@ -247,6 +280,62 @@ where Now(f, None) } +// Until + +/// See [`now`]. +#[derive(Copy, Clone)] +pub struct Until(F, Option, PhantomData); + +impl< + R: Send + Sync + 'static, + F: FnMut(&mut NpcCtx) -> Option + Send + Sync + 'static, + A: Action, +> Action<()> for Until +{ + // TODO: This doesn't compare?! + fn is_same(&self, other: &Self) -> bool { true } + + fn dyn_is_same(&self, other: &dyn Action<()>) -> bool { self.dyn_is_same_sized(other) } + + fn backtrace(&self, bt: &mut Vec) { + if let Some(action) = &self.1 { + action.backtrace(bt); + } else { + bt.push("".to_string()); + } + } + + // TODO: Reset closure? + fn reset(&mut self) { self.1 = None; } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { + match &mut self.1 { + Some(x) => match x.tick(ctx) { + ControlFlow::Continue(()) => ControlFlow::Continue(()), + ControlFlow::Break(_) => { + self.1 = None; + ControlFlow::Continue(()) + }, + }, + None => match (self.0)(ctx) { + Some(x) => { + self.1 = Some(x); + ControlFlow::Continue(()) + }, + None => ControlFlow::Break(()), + }, + } + } +} + +pub fn until(f: F) -> Until +where + F: FnMut(&mut NpcCtx) -> Option, +{ + Until(f, None, PhantomData) +} + // Just /// See [`just`]. @@ -262,6 +351,7 @@ impl R + Send + Sync + 'stati fn backtrace(&self, bt: &mut Vec) {} + // TODO: Reset closure? fn reset(&mut self) {} // TODO: Reset closure state? diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 2f90c2c805..cd5b01e0b7 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -73,9 +73,9 @@ impl Data { // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() // TODO: Stupid - // .filter(|(_, site)| site.world_site.map_or(false, |ws| - // matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) - // .take(1) + .filter(|(_, site)| site.world_site.map_or(false, |ws| + matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) + .take(1) { let Some(good_or_evil) = site .faction @@ -90,7 +90,7 @@ impl Data { .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; if good_or_evil { - for _ in 0..64 { + for _ in 0..32 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 1e64132a47..b45c4d1055 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,7 +1,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ - ai::{casual, choose, finish, important, just, now, seq, urgent, watch, Action, NpcCtx}, + ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx}, data::{ npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory}, Sites, @@ -162,46 +162,31 @@ fn path_between_sites( }) } -fn path_town( - wpos: Vec3, +fn path_site( + start: Vec2, + end: Vec2, site: Id, index: IndexRef, - end: impl FnOnce(&site2::Site) -> Option>, -) -> Option, Vec2>> { - match &index.sites.get(site).kind { - SiteKind::Refactor(site) | SiteKind::CliffTown(site) | SiteKind::DesertCity(site) => { - let start = site.wpos_tile_pos(wpos.xy().as_()); +) -> Option>> { + if let Some(site) = index.sites.get(site).site2() { + let start = site.wpos_tile_pos(start.as_()); - let end = end(site)?; + let end = site.wpos_tile_pos(end.as_()); - if start == end { - return None; - } + let nodes = match path_in_site(start, end, site) { + PathResult::Path(p) => p.nodes, + PathResult::Exhausted(p) => p.nodes, + PathResult::None(_) | PathResult::Pending => return None, + }; - // We pop the first element of the path - // fn pop_first(mut queue: VecDeque) -> VecDeque { - // queue.pop_front(); - // queue - // } - - match path_in_site(start, end, site) { - PathResult::Path(p) => Some(PathData { - end, - path: p.nodes.into(), //pop_first(p.nodes.into()), - repoll: false, - }), - PathResult::Exhausted(p) => Some(PathData { - end, - path: p.nodes.into(), //pop_first(p.nodes.into()), - repoll: true, - }), - PathResult::None(_) | PathResult::Pending => None, - } - }, - _ => { - // No brain T_T - None - }, + Some( + nodes + .into_iter() + .map(|tile| site.tile_center_wpos(tile).as_() + 0.5) + .collect(), + ) + } else { + None } } @@ -365,6 +350,58 @@ fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action }) } +fn traverse_points(mut next_point: F) -> impl Action<()> +where + F: FnMut(&mut NpcCtx) -> Option> + Send + Sync + 'static, +{ + until(move |ctx| { + let wpos = next_point(ctx)?; + + let wpos_site = |wpos: Vec2| { + ctx.world + .sim() + .get(wpos.as_::() / TerrainChunkSize::RECT_SIZE.as_()) + .and_then(|chunk| chunk.sites.first().copied()) + }; + + // If we're traversing within a site, to intra-site pathfinding + if let Some(site) = wpos_site(wpos) { + let mut site_exit = wpos; + while let Some(next) = next_point(ctx).filter(|next| wpos_site(*next) == Some(site)) { + site_exit = next; + } + + // println!("[NPC {:?}] Pathing in site...", ctx.npc_id); + if let Some(path) = path_site(wpos, site_exit, site, ctx.index) { + // println!("[NPC {:?}] Found path of length {} from {:?} to {:?}!", ctx.npc_id, + // path.len(), wpos, site_exit); + Some(itertools::Either::Left( + seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))) + .then(goto_2d(site_exit, 1.0, 8.0)), + )) + } else { + // println!("[NPC {:?}] No path", ctx.npc_id); + Some(itertools::Either::Right(goto_2d(site_exit, 1.0, 8.0))) + } + } else { + Some(itertools::Either::Right(goto_2d(wpos, 1.0, 8.0))) + } + }) +} + +/// Try to travel to a site. Where practical, paths will be taken. +fn travel_to_point(wpos: Vec2) -> impl Action { + now(move |ctx| { + const WAYPOINT: f32 = 24.0; + let start = ctx.npc.wpos.xy(); + let diff = wpos - start; + let n = (diff.magnitude() / WAYPOINT).max(1.0); + let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); + traverse_points(move |_| points.next()) + }) + .debug(|| "travel to point") +} + /// Try to travel to a site. Where practical, paths will be taken. fn travel_to_site(tgt_site: SiteId) -> impl Action { now(move |ctx| { @@ -376,51 +413,83 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { && let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world) { let track_count = tracks.path.len(); - // For every track in the path we discovered between the sites... - seq(tracks - .path + + let mut nodes = tracks.path .into_iter() - .enumerate() - // ...traverse the nodes of that path. - .map(move |(i, (track_id, reversed))| now(move |ctx| { - let track_len = ctx.world.civs().tracks.get(track_id).path().len(); - // Tracks can be traversed backward (i.e: from end to beginning). Account for this. - seq(if reversed { - itertools::Either::Left((0..track_len).rev()) - } else { - itertools::Either::Right(0..track_len) - } - .enumerate() - .map(move |(i, node_idx)| now(move |ctx| { - // Find the centre of the track node's chunk - let node_chunk_wpos = TerrainChunkSize::center_wpos(ctx.world - .civs() - .tracks - .get(track_id) - .path() - .nodes[node_idx]); + .flat_map(move |(track_id, reversed)| (0..) + .map(move |node_idx| (node_idx, track_id, reversed))); - // Refine the node position a bit more based on local path information - let node_wpos = ctx.world.sim() - .get_nearest_path(node_chunk_wpos) - .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()); + traverse_points(move |ctx| { + let (node_idx, track_id, reversed) = nodes.next()?; + let nodes = &ctx.world.civs().tracks.get(track_id).path().nodes; - // Walk toward the node - goto_2d(node_wpos.as_(), 1.0, 8.0) - .debug(move || format!("traversing track node ({}/{})", i + 1, track_len)) - }))) - }) - .debug(move || format!("travel via track {:?} ({}/{})", track_id, i + 1, track_count)))) + // Handle the case where we walk paths backward + let idx = if reversed { + nodes.len().checked_sub(node_idx + 1) + } else { + Some(node_idx) + }; + + if let Some(node) = idx.and_then(|idx| nodes.get(idx)) { + // Find the centre of the track node's chunk + let node_chunk_wpos = TerrainChunkSize::center_wpos(*node); + + // Refine the node position a bit more based on local path information + Some(ctx.world.sim() + .get_nearest_path(node_chunk_wpos) + .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()) + .as_::()) + } else { + None + } + }) .boxed() + + // For every track in the path we discovered between the sites... + // seq(tracks + // .path + // .into_iter() + // .enumerate() + // // ...traverse the nodes of that path. + // .map(move |(i, (track_id, reversed))| now(move |ctx| { + // let track_len = ctx.world.civs().tracks.get(track_id).path().len(); + // // Tracks can be traversed backward (i.e: from end to beginning). Account for this. + // seq(if reversed { + // itertools::Either::Left((0..track_len).rev()) + // } else { + // itertools::Either::Right(0..track_len) + // } + // .enumerate() + // .map(move |(i, node_idx)| now(move |ctx| { + // // Find the centre of the track node's chunk + // let node_chunk_wpos = TerrainChunkSize::center_wpos(ctx.world + // .civs() + // .tracks + // .get(track_id) + // .path() + // .nodes[node_idx]); + + // // Refine the node position a bit more based on local path information + // let node_wpos = ctx.world.sim() + // .get_nearest_path(node_chunk_wpos) + // .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_()); + + // // Walk toward the node + // goto_2d(node_wpos.as_(), 1.0, 8.0) + // .debug(move || format!("traversing track node ({}/{})", i + 1, track_len)) + // }))) + // }) + // .debug(move || format!("travel via track {:?} ({}/{})", track_id, i + 1, track_count)))) + // .boxed() } else if let Some(site) = sites.get(tgt_site) { // If all else fails, just walk toward the target site in a straight line - goto_2d(site.wpos.map(|e| e as f32 + 0.5), 1.0, 8.0).boxed() + travel_to_point(site.wpos.map(|e| e as f32 + 0.5)).boxed() } else { // If we can't find a way to get to the site at all, there's nothing more to be done finish().boxed() } }) - .debug(move || format!("travel_to_site {:?}", tgt_site)) + .debug(move || format!("travel_to_site {:?}", tgt_site)) } // Seconds @@ -500,7 +569,7 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - goto_2d(house_wpos, 0.5, 1.0) + travel_to_point(house_wpos) .debug(|| "walk to house") .then(idle().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) @@ -527,7 +596,7 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { // Walk to the plaza... - goto_2d(plaza_wpos, 0.5, 8.0) + travel_to_point(plaza_wpos) .debug(|| "walk to plaza") // ...then wait for some time before moving on .then({ From 28ebdbbe74aefbca9242e548df159e2854b30d69 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 14 Jan 2023 23:40:59 +0100 Subject: [PATCH 055/139] fix mount controller --- common/systems/src/lib.rs | 2 +- common/systems/src/mount.rs | 22 ++++++++++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/common/systems/src/lib.rs b/common/systems/src/lib.rs index a0e18fcf5f..1c4e108177 100644 --- a/common/systems/src/lib.rs +++ b/common/systems/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(btree_drain_filter)] +#![feature(drain_filter)] #![allow(clippy::option_map_unit_fn)] mod aura; diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index 25ee2ae644..ee1945d7f1 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,5 @@ use common::{ - comp::{Body, Controller, InputKind, Ori, Pos, Scale, Vel}, + comp::{Body, Controller, InputKind, Ori, Pos, Scale, Vel, ControlAction}, link::Is, mounting::Mount, uid::UidAllocator, @@ -48,17 +48,18 @@ impl<'a> System<'a> for Sys { // For each mount... for (entity, is_mount, body) in (&entities, &is_mounts, bodies.maybe()).join() { // ...find the rider... - let Some((inputs, queued_inputs, rider)) = uid_allocator + let Some((inputs, actions, rider)) = uid_allocator .retrieve_entity_internal(is_mount.rider.id()) .and_then(|rider| { controllers .get_mut(rider) .map(|c| { - let queued_inputs = c.queued_inputs - // TODO: Formalise ways to pass inputs to mounts - .drain_filter(|i, _| matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll)) - .collect(); - (c.inputs.clone(), queued_inputs, rider) + let actions = c.actions.drain_filter(|action| match action { + ControlAction::StartInput { input: i, .. } + | ControlAction::CancelInput(i) => matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll), + _ => false + }).collect(); + (c.inputs.clone(), actions, rider) }) }) else { continue }; @@ -79,11 +80,8 @@ impl<'a> System<'a> for Sys { } // ...and apply the rider's inputs to the mount's controller. if let Some(controller) = controllers.get_mut(entity) { - *controller = Controller { - inputs, - queued_inputs, - ..Default::default() - } + controller.inputs = inputs; + controller.actions = actions; } } } From a6b2f04518edaa089241034d60acdfb692a7f6c2 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 14 Jan 2023 23:43:15 +0100 Subject: [PATCH 056/139] use push_basic_input instead of actions.push --- server/agent/src/attack.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index 9d5151edb6..031e134609 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -2313,9 +2313,7 @@ impl<'a> AgentData<'a> { { agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] = 0.0; } else if agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] < 1.0 { - controller - .actions - .push(ControlAction::basic_input(InputKind::Primary)); + controller.push_basic_input(InputKind::Primary); agent.action_state.timers[ActionStateTimers::TimerOrganAura as usize] += read_data.dt.0; } else { From a7588e274d4a2d08bd54a5052813aceba62ac9b5 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 14 Jan 2023 23:47:05 +0100 Subject: [PATCH 057/139] clean up mounting --- common/src/comp/pet.rs | 1 + common/src/mounting.rs | 29 ++++++++++------------------- server/src/events/interaction.rs | 9 +++++++-- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/common/src/comp/pet.rs b/common/src/comp/pet.rs index 8f8a308513..ac617be380 100644 --- a/common/src/comp/pet.rs +++ b/common/src/comp/pet.rs @@ -94,6 +94,7 @@ pub fn is_mountable(mount: &Body, rider: Option<&Body>) -> bool { | quadruped_low::Species::Elbst | quadruped_low::Species::Tortoise ), + Body::Ship(_) => true, _ => false, } } diff --git a/common/src/mounting.rs b/common/src/mounting.rs index 6d95da5fd0..39461c1b07 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -1,6 +1,5 @@ use crate::{ comp, - comp::{pet::is_mountable, Body}, link::{Is, Link, LinkHandle, Role}, terrain::TerrainGrid, uid::{Uid, UidAllocator}, @@ -29,6 +28,7 @@ pub struct Mounting { pub rider: Uid, } +#[derive(Debug)] pub enum MountingError { NoSuchEntity, NotMountable, @@ -39,7 +39,6 @@ impl Link for Mounting { Read<'a, UidAllocator>, WriteStorage<'a, Is>, WriteStorage<'a, Is>, - WriteStorage<'a, Body>, ); type DeleteData<'a> = ( Read<'a, UidAllocator>, @@ -60,7 +59,7 @@ impl Link for Mounting { fn create( this: &LinkHandle, - (uid_allocator, mut is_mounts, mut is_riders, body): Self::CreateData<'_>, + (uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>, ) -> Result<(), Self::Error> { let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); @@ -68,23 +67,15 @@ impl Link for Mounting { // Forbid self-mounting Err(MountingError::NotMountable) } else if let Some((mount, rider)) = entity(this.mount).zip(entity(this.rider)) { - if let Some(mount_body) = body.get(mount) { - if is_mountable(mount_body, body.get(rider)) { - let can_mount_with = - |entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none(); + let can_mount_with = + |entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none(); - // Ensure that neither mount or rider are already part of a mounting - // relationship - if can_mount_with(mount) && can_mount_with(rider) { - let _ = is_mounts.insert(mount, this.make_role()); - let _ = is_riders.insert(rider, this.make_role()); - Ok(()) - } else { - Err(MountingError::NotMountable) - } - } else { - Err(MountingError::NotMountable) - } + // Ensure that neither mount or rider are already part of a mounting + // relationship + if can_mount_with(mount) && can_mount_with(rider) { + let _ = is_mounts.insert(mount, this.make_role()); + let _ = is_riders.insert(rider, this.make_role()); + Ok(()) } else { Err(MountingError::NotMountable) } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 0aad278d92..c1edd8a6af 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -10,7 +10,7 @@ use common::{ inventory::slot::EquipSlot, loot_owner::LootOwnerKind, tool::ToolKind, - Inventory, LootOwner, Pos, SkillGroupKind, + Inventory, LootOwner, Pos, SkillGroupKind, pet::is_mountable, }, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, event::EventBus, @@ -119,7 +119,12 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, ); - if is_pet { + let can_ride = state.ecs() + .read_storage() + .get(mount) + .map_or(false, |mount_body| is_mountable(mount_body, state.ecs().read_storage().get(rider))); + + if is_pet && can_ride { drop(uids); drop(healths); let _ = state.link(Mounting { From 1a117f13311163ffb0651f0164bbc783008b6258 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 14 Jan 2023 23:49:43 +0100 Subject: [PATCH 058/139] rtsim vehicles --- assets/common/entity/village/captain.ron | 19 +++ assets/common/loadout/village/captain.ron | 26 ++++ common/src/comp/agent.rs | 9 +- common/src/event.rs | 99 +++++++++--- common/src/generation.rs | 21 +-- common/src/rtsim.rs | 12 ++ common/src/states/basic_summon.rs | 39 +++-- rtsim/src/data/mod.rs | 11 +- rtsim/src/data/npc.rs | 175 ++++++++++++++++++++-- rtsim/src/gen/mod.rs | 20 ++- rtsim/src/rule/npc_ai.rs | 131 +++++++++++++++- rtsim/src/rule/simulate_npcs.rs | 161 +++++++++++++++++--- server/agent/src/data.rs | 3 +- server/src/cmd.rs | 9 +- server/src/events/entity_creation.rs | 88 ++++++----- server/src/events/mod.rs | 44 ++---- server/src/lib.rs | 16 +- server/src/rtsim2/mod.rs | 12 +- server/src/rtsim2/tick.rs | 136 +++++++++++++---- server/src/state_ext.rs | 5 - server/src/sys/agent.rs | 29 +++- server/src/sys/terrain.rs | 24 ++- 22 files changed, 843 insertions(+), 246 deletions(-) create mode 100644 assets/common/entity/village/captain.ron create mode 100644 assets/common/loadout/village/captain.ron diff --git a/assets/common/entity/village/captain.ron b/assets/common/entity/village/captain.ron new file mode 100644 index 0000000000..0043093d54 --- /dev/null +++ b/assets/common/entity/village/captain.ron @@ -0,0 +1,19 @@ +#![enable(implicit_some)] +( + name: Name("Captain"), + body: RandomWith("humanoid"), + alignment: Alignment(Npc), + loot: LootTable("common.loot_tables.creature.humanoid"), + inventory: ( + loadout: Inline(( + inherit: Asset("common.loadout.village.captain"), + active_hands: InHands((ModularWeapon(tool: Sword, material: Orichalcum, hands: Two), None)), + )), + items: [ + (10, "common.items.food.cheese"), + (10, "common.items.food.plainsalad"), + (10, "common.items.consumable.potion_med"), + ], + ), + meta: [], +) diff --git a/assets/common/loadout/village/captain.ron b/assets/common/loadout/village/captain.ron new file mode 100644 index 0000000000..c6617a5879 --- /dev/null +++ b/assets/common/loadout/village/captain.ron @@ -0,0 +1,26 @@ +// Christmas event +//(1.0, Some(Item("common.items.calendar.christmas.armor.misc.head.woolly_wintercap"))), +#![enable(implicit_some)] +( + head: Item("common.items.armor.pirate.hat"), + shoulders: Item("common.items.armor.mail.orichalcum.shoulder"), + chest: Item("common.items.armor.mail.orichalcum.chest"), + gloves: Item("common.items.armor.mail.orichalcum.hand"), + back: Choice([ + (1, Item("common.items.armor.misc.back.backpack")), + (1, Item("common.items.npc_armor.back.backpack_blue")), + (1, Item("common.items.armor.mail.orichalcum.back")), + (1, None), + ]), + belt: Item("common.items.armor.mail.orichalcum.belt"), + legs: Item("common.items.armor.mail.orichalcum.pants"), + feet: Item("common.items.armor.mail.orichalcum.foot"), + lantern: Choice([ + (1, Item("common.items.lantern.black_0")), + (1, Item("common.items.lantern.blue_0")), + (1, Item("common.items.lantern.green_0")), + (1, Item("common.items.lantern.red_0")), + (1, Item("common.items.lantern.geode_purp")), + (1, Item("common.items.boss_drops.lantern")), + ]), +) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index ab02458c9c..5af82f0c82 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -904,21 +904,20 @@ impl, Vec3) -> f32, const NUM_SAMPLES: usize> PidController /// Get the PID coefficients associated with some Body, since it will likely /// need to be tuned differently for each body type -pub fn pid_coefficients(body: &Body) -> (f32, f32, f32) { +pub fn pid_coefficients(body: &Body) -> Option<(f32, f32, f32)> { match body { Body::Ship(ship::Body::DefaultAirship) => { let kp = 1.0; let ki = 0.1; let kd = 1.2; - (kp, ki, kd) + Some((kp, ki, kd)) }, Body::Ship(ship::Body::AirBalloon) => { let kp = 1.0; let ki = 0.1; let kd = 0.8; - (kp, ki, kd) + Some((kp, ki, kd)) }, - // default to a pure-proportional controller, which is the first step when tuning - _ => (1.0, 0.0, 0.0), + _ => None, } } diff --git a/common/src/event.rs b/common/src/event.rs index 0eab390bbf..7914d6a21e 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -8,7 +8,7 @@ use crate::{ }, lottery::LootSpec, outcome::Outcome, - rtsim::RtSimEntity, + rtsim::{RtSimEntity, RtSimVehicle}, terrain::SpriteKind, trade::{TradeAction, TradeId}, uid::Uid, @@ -42,6 +42,83 @@ pub struct UpdateCharacterMetadata { pub skill_set_persistence_load_error: Option, } +pub struct NpcBuilder { + pub stats: comp::Stats, + pub skill_set: comp::SkillSet, + pub health: Option, + pub poise: comp::Poise, + pub inventory: comp::inventory::Inventory, + pub body: comp::Body, + pub agent: Option, + pub alignment: comp::Alignment, + pub scale: comp::Scale, + pub anchor: Option, + pub loot: LootSpec, + pub rtsim_entity: Option, + pub projectile: Option, +} + +impl NpcBuilder { + pub fn new(stats: comp::Stats, body: comp::Body, alignment: comp::Alignment) -> Self { + Self { + stats, + skill_set: comp::SkillSet::default(), + health: None, + poise: comp::Poise::new(body.clone()), + inventory: comp::Inventory::with_empty(), + body, + agent: None, + alignment, + scale: comp::Scale(1.0), + anchor: None, + loot: LootSpec::Nothing, + rtsim_entity: None, + projectile: None, + } + } + + pub fn with_health(mut self, health: impl Into>) -> Self { + self.health = health.into(); + self + } + pub fn with_poise(mut self, poise: comp::Poise) -> Self { + self.poise = poise; + self + } + pub fn with_agent(mut self, agent: impl Into>) -> Self { + self.agent = agent.into(); + self + } + pub fn with_anchor(mut self, anchor: comp::Anchor) -> Self { + self.anchor = Some(anchor); + self + } + pub fn with_rtsim(mut self, rtsim: RtSimEntity) -> Self { + self.rtsim_entity = Some(rtsim); + self + } + pub fn with_projectile(mut self, projectile: impl Into>) -> Self { + self.projectile = projectile.into(); + self + } + pub fn with_scale(mut self, scale: comp::Scale) -> Self { + self.scale = scale; + self + } + pub fn with_inventory(mut self, inventory: comp::Inventory) -> Self { + self.inventory = inventory; + self + } + pub fn with_skill_set(mut self, skill_set: comp::SkillSet) -> Self { + self.skill_set = skill_set; + self + } + pub fn with_loot(mut self, loot: LootSpec) -> Self { + self.loot = loot; + self + } +} + #[allow(clippy::large_enum_variant)] // TODO: Pending review in #587 #[derive(strum::EnumDiscriminants)] #[strum_discriminants(repr(usize))] @@ -137,26 +214,14 @@ pub enum ServerEvent { // TODO: to avoid breakage when adding new fields, perhaps have an `NpcBuilder` type? CreateNpc { pos: Pos, - stats: comp::Stats, - skill_set: comp::SkillSet, - health: Option, - poise: comp::Poise, - inventory: comp::inventory::Inventory, - body: comp::Body, - agent: Option, - alignment: comp::Alignment, - scale: comp::Scale, - anchor: Option, - loot: LootSpec, - rtsim_entity: Option, - projectile: Option, + npc: NpcBuilder, }, CreateShip { pos: Pos, ship: comp::ship::Body, - mountable: bool, - agent: Option, - rtsim_entity: Option, + rtsim_entity: Option, + driver: Option, + passangers: Vec, }, CreateWaypoint(Vec3), ClientDisconnect(EcsEntity, DisconnectReason), diff --git a/common/src/generation.rs b/common/src/generation.rs index ef37b72037..041f7a4b8e 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -375,14 +375,8 @@ impl EntityInfo { } #[must_use] - pub fn with_agent_mark(mut self, agent_mark: agent::Mark) -> Self { - self.agent_mark = Some(agent_mark); - self - } - - #[must_use] - pub fn with_maybe_agent_mark(mut self, agent_mark: Option) -> Self { - self.agent_mark = agent_mark; + pub fn with_agent_mark(mut self, agent_mark: impl Into>) -> Self { + self.agent_mark = agent_mark.into(); self } @@ -442,15 +436,8 @@ impl EntityInfo { /// map contains price+amount #[must_use] - pub fn with_economy(mut self, e: &SiteInformation) -> Self { - self.trading_information = Some(e.clone()); - self - } - - /// map contains price+amount - #[must_use] - pub fn with_maybe_economy(mut self, e: Option<&SiteInformation>) -> Self { - self.trading_information = e.cloned(); + pub fn with_economy<'a>(mut self, e: impl Into>) -> Self { + self.trading_information = e.into().cloned(); self } diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index ce29f9630b..5874fd8999 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -11,6 +11,8 @@ use crate::comp::dialogue::MoodState; slotmap::new_key_type! { pub struct NpcId; } +slotmap::new_key_type! { pub struct VehicleId; } + slotmap::new_key_type! { pub struct SiteId; } slotmap::new_key_type! { pub struct FactionId; } @@ -22,6 +24,13 @@ impl Component for RtSimEntity { type Storage = specs::VecStorage; } +#[derive(Copy, Clone, Debug)] +pub struct RtSimVehicle(pub VehicleId); + +impl Component for RtSimVehicle { + type Storage = specs::VecStorage; +} + #[derive(Clone, Debug)] pub enum RtSimEvent { AddMemory(Memory), @@ -139,6 +148,8 @@ pub enum Profession { Cultist, #[serde(rename = "10")] Herbalist, + #[serde(rename = "11")] + Captain, } impl Profession { @@ -155,6 +166,7 @@ impl Profession { Self::Pirate => "Pirate".to_string(), Self::Cultist => "Cultist".to_string(), Self::Herbalist => "Herbalist".to_string(), + Self::Captain => "Captain".to_string(), } } } diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 810644dea2..2f56ab02de 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -6,7 +6,7 @@ use crate::{ skillset::skills, Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, }, - event::{LocalEvent, ServerEvent}, + event::{LocalEvent, ServerEvent, NpcBuilder}, outcome::Outcome, skillset_builder::{self, SkillSetBuilder}, states::{ @@ -174,27 +174,22 @@ impl CharacterBehavior for Data { // Send server event to create npc output_events.emit_server(ServerEvent::CreateNpc { pos: comp::Pos(collision_vector - Vec3::unit_z() * obstacle_z), - stats, - skill_set, - health, - poise: comp::Poise::new(body), - inventory: comp::Inventory::with_loadout(loadout, body), - body, - agent: Some( - comp::Agent::from_body(&body) - .with_behavior(Behavior::from(BehaviorCapability::SPEAK)) - .with_no_flee_if(true), - ), - alignment: comp::Alignment::Owned(*data.uid), - scale: self - .static_data - .summon_info - .scale - .unwrap_or(comp::Scale(1.0)), - anchor: None, - loot: crate::lottery::LootSpec::Nothing, - rtsim_entity: None, - projectile, + npc: NpcBuilder::new(stats, body, comp::Alignment::Owned(*data.uid)) + .with_skill_set(skill_set) + .with_health(health) + .with_inventory(comp::Inventory::with_loadout(loadout, body)) + .with_agent( + comp::Agent::from_body(&body) + .with_behavior(Behavior::from(BehaviorCapability::SPEAK)) + .with_no_flee_if(true) + ) + .with_scale( + self + .static_data + .summon_info + .scale + .unwrap_or(comp::Scale(1.0)) + ).with_projectile(projectile) }); // Send local event used for frontend shenanigans diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index df4c219ef0..d3bb51a484 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -23,12 +23,21 @@ use std::{ marker::PhantomData, }; -#[derive(Copy, Clone, Serialize, Deserialize)] +#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum Actor { Npc(NpcId), Character(common::character::CharacterId), } +impl Actor { + pub fn npc(&self) -> Option { + match self { + Actor::Npc(id) => Some(*id), + Actor::Character(_) => None, + } + } +} + #[derive(Clone, Serialize, Deserialize)] pub struct Data { pub nature: Nature, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index ecee1af62f..ff6a513318 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -2,9 +2,10 @@ use crate::ai::{Action, NpcCtx}; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, - rtsim::{FactionId, RtSimController, SiteId}, + grid::Grid, + rtsim::{FactionId, RtSimController, SiteId, VehicleId}, store::Id, - uid::Uid, + uid::Uid, vol::RectVolSize, }; use hashbrown::HashMap; use rand::prelude::*; @@ -20,10 +21,12 @@ use std::{ }, }; use vek::*; -use world::{civ::Track, site::Site as WorldSite, util::RandomPerm}; +use world::{civ::Track, site::Site as WorldSite, util::{RandomPerm, LOCALITY}}; + +use super::Actor; #[derive(Copy, Clone, Debug, Default)] -pub enum NpcMode { +pub enum SimulationMode { /// The NPC is unloaded and is being simulated via rtsim. #[default] Simulated, @@ -44,12 +47,24 @@ pub struct PathingMemory { pub intersite_path: Option<(PathData<(Id, bool), SiteId>, usize)>, } +#[derive(Clone, Copy)] +pub enum NpcAction { + /// (wpos, speed_factor) + Goto(Vec3, f32), +} + pub struct Controller { - pub goto: Option<(Vec3, f32)>, + pub action: Option, } impl Controller { - pub fn idle() -> Self { Self { goto: None } } + pub fn idle() -> Self { Self { action: None } } + + pub fn goto(wpos: Vec3, speed_factor: f32) -> Self { + Self { + action: Some(NpcAction::Goto(wpos, speed_factor)), + } + } } pub struct Brain { @@ -67,20 +82,23 @@ pub struct Npc { pub home: Option, pub faction: Option, + pub riding: Option, + // Unpersisted state #[serde(skip_serializing, skip_deserializing)] + pub chunk_pos: Option>, + #[serde(skip_serializing, skip_deserializing)] pub current_site: Option, - /// (wpos, speed_factor) #[serde(skip_serializing, skip_deserializing)] - pub goto: Option<(Vec3, f32)>, + pub action: Option, /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// server, loaded corresponds to being within a loaded chunk). When in /// loaded mode, the interactions of the NPC should not be simulated but /// should instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] - pub mode: NpcMode, + pub mode: SimulationMode, #[serde(skip_serializing, skip_deserializing)] pub brain: Option, @@ -94,9 +112,11 @@ impl Clone for Npc { profession: self.profession.clone(), home: self.home, faction: self.faction, + riding: self.riding.clone(), // Not persisted + chunk_pos: None, current_site: Default::default(), - goto: Default::default(), + action: Default::default(), mode: Default::default(), brain: Default::default(), } @@ -114,9 +134,11 @@ impl Npc { profession: None, home: None, faction: None, + riding: None, + chunk_pos: None, current_site: None, - goto: None, - mode: NpcMode::Simulated, + action: None, + mode: SimulationMode::Simulated, brain: None, } } @@ -131,6 +153,26 @@ impl Npc { self } + pub fn steering(mut self, vehicle: impl Into>) -> Self { + self.riding = vehicle.into().map(|vehicle| { + Riding { + vehicle, + steering: true, + } + }); + self + } + + pub fn riding(mut self, vehicle: impl Into>) -> Self { + self.riding = vehicle.into().map(|vehicle| { + Riding { + vehicle, + steering: false, + } + }); + self + } + pub fn with_faction(mut self, faction: impl Into>) -> Self { self.faction = faction.into(); self @@ -147,12 +189,115 @@ impl Npc { } #[derive(Clone, Serialize, Deserialize)] -pub struct Npcs { - pub npcs: HopSlotMap, +pub struct Riding { + pub vehicle: VehicleId, + pub steering: bool, } +#[derive(Clone, Serialize, Deserialize)] +pub enum VehicleKind { + Airship, + Boat, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Vehicle { + pub wpos: Vec3, + + pub kind: VehicleKind, + + #[serde(skip_serializing, skip_deserializing)] + pub chunk_pos: Option>, + + #[serde(skip_serializing, skip_deserializing)] + pub driver: Option, + + #[serde(skip_serializing, skip_deserializing)] + // TODO: Find a way to detect riders when the vehicle is loaded + pub riders: Vec, + + /// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on the + /// server, loaded corresponds to being within a loaded chunk). When in + /// loaded mode, the interactions of the Vehicle should not be simulated but + /// should instead be derived from the game. + #[serde(skip_serializing, skip_deserializing)] + pub mode: SimulationMode, +} + +impl Vehicle { + pub fn new(wpos: Vec3, kind: VehicleKind) -> Self { + Self { + wpos, + kind, + chunk_pos: None, + driver: None, + riders: Vec::new(), + mode: SimulationMode::Simulated, + } + } + pub fn get_ship(&self) -> comp::ship::Body { + match self.kind { + VehicleKind::Airship => comp::ship::Body::DefaultAirship, + VehicleKind::Boat => comp::ship::Body::Galleon, + } + } + + pub fn get_body(&self) -> comp::Body { + comp::Body::Ship(self.get_ship()) + } + + /// Max speed in block/s + pub fn get_speed(&self) -> f32 { + match self.kind { + VehicleKind::Airship => 15.0, + VehicleKind::Boat => 13.0, + } + } +} + +#[derive(Default, Clone, Serialize, Deserialize)] +pub struct GridCell { + pub npcs: Vec, + pub vehicles: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Npcs { + pub npcs: HopSlotMap, + pub vehicles: HopSlotMap, + #[serde(skip, default = "construct_npc_grid")] + pub npc_grid: Grid, +} + +fn construct_npc_grid() -> Grid { Grid::new(Vec2::zero(), Default::default()) } + impl Npcs { - pub fn create(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } + pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } + + pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { self.vehicles.insert(vehicle) } + + /// Queries nearby npcs, not garantueed to work if radius > 32.0 + pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { + let chunk_pos = wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); + let r_sqr = radius * radius; + LOCALITY + .into_iter() + .filter_map(move |neighbor| { + self + .npc_grid + .get(chunk_pos + neighbor) + .map(|cell| { + cell.npcs.iter() + .copied() + .filter(|npc| { + self.npcs.get(*npc) + .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) + }) + .collect::>() + }) + }) + .flatten() + } } impl Deref for Npcs { diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index cd5b01e0b7..368c532697 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -3,12 +3,13 @@ pub mod site; use crate::data::{ faction::{Faction, Factions}, - npc::{Npc, Npcs, Profession}, + npc::{Npc, Npcs, Profession, Vehicle, VehicleKind}, site::{Site, Sites}, Data, Nature, }; use common::{ - resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, vol::RectVolSize, + grid::Grid, resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, + vol::RectVolSize, }; use hashbrown::HashMap; use rand::prelude::*; @@ -28,6 +29,8 @@ impl Data { nature: Nature::generate(world), npcs: Npcs { npcs: Default::default(), + vehicles: Default::default(), + npc_grid: Grid::new(Vec2::zero(), Default::default()), }, sites: Sites { sites: Default::default(), @@ -75,7 +78,6 @@ impl Data { // TODO: Stupid .filter(|(_, site)| site.world_site.map_or(false, |ws| matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) - .take(1) { let Some(good_or_evil) = site .faction @@ -91,7 +93,7 @@ impl Data { }; if good_or_evil { for _ in 0..32 { - this.npcs.create( + this.npcs.create_npc( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) @@ -109,7 +111,7 @@ impl Data { } } else { for _ in 0..15 { - this.npcs.create( + this.npcs.create_npc( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) @@ -119,12 +121,18 @@ impl Data { ); } } - this.npcs.create( + this.npcs.create_npc( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_home(site_id) .with_profession(Profession::Merchant), ); + + let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0; + let vehicle_id = this.npcs.create_vehicle(Vehicle::new(wpos, VehicleKind::Airship)); + + this.npcs.create_npc(Npc::new(rng.gen(), wpos).with_home(site_id).with_profession(Profession::Captain).steering(vehicle_id)); } + info!("Generated {} rtsim NPCs.", this.npcs.len()); this diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index b45c4d1055..c8e7ce4c53 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -3,7 +3,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx}, data::{ - npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory}, + npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory, VehicleKind}, Sites, }, event::OnTick, @@ -14,7 +14,7 @@ use common::{ path::Path, rtsim::{Profession, SiteId}, store::Id, - terrain::TerrainChunkSize, + terrain::{TerrainChunkSize, SiteKindMeta}, time::DayPeriod, vol::RectVolSize, }; @@ -32,7 +32,7 @@ use world::{ civ::{self, Track}, site::{Site as WorldSite, SiteKind}, site2::{self, PlotKind, TileKind}, - IndexRef, World, + IndexRef, World, util::NEIGHBORS, }; pub struct NpcAi; @@ -221,7 +221,7 @@ impl Rule for NpcAi { data.npcs .iter_mut() .map(|(npc_id, npc)| { - let controller = Controller { goto: npc.goto }; + let controller = Controller { action: npc.action }; let brain = npc.brain.take().unwrap_or_else(|| Brain { action: Box::new(think().repeat()), }); @@ -253,7 +253,7 @@ impl Rule for NpcAi { let mut data = ctx.state.data_mut(); for (npc_id, controller, brain) in npc_data { - data.npcs[npc_id].goto = controller.goto; + data.npcs[npc_id].action = controller.action; data.npcs[npc_id].brain = Some(brain); } @@ -331,7 +331,7 @@ fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { let waypoint = waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST)); - ctx.controller.goto = Some((*waypoint, speed_factor)); + *ctx.controller = Controller::goto(*waypoint, speed_factor); }) .repeat() .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) @@ -616,9 +616,126 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(move || format!("villager at site {:?}", visiting_site)) } +fn follow(npc: NpcId, distance: f32) -> impl Action { + const STEP_DIST: f32 = 1.0; + now(move |ctx| { + if let Some(npc) = ctx.state.data().npcs.get(npc) { + let d = npc.wpos.xy() - ctx.npc.wpos.xy(); + let len = d.magnitude(); + let dir = d / len; + let wpos = ctx.npc.wpos.xy() + dir * STEP_DIST.min(len - distance); + goto_2d(wpos, 1.0, distance).boxed() + } else { + // The npc we're trying to follow doesn't exist. + finish().boxed() + } + }) + .repeat() + .debug(move || format!("Following npc({npc:?})")) + .map(|_| {}) +} + +fn chunk_path(from: Vec2, to: Vec2, chunk_height: impl Fn(Vec2) -> Option) -> Box { + let heuristics = |(p, _): &(Vec2, i32)| p.distance_squared(to) as f32; + let start = (from, chunk_height(from).unwrap()); + let mut astar = Astar::new( + 1000, + start, + heuristics, + BuildHasherDefault::::default(), + ); + + let path = astar.poll( + 1000, + heuristics, + |&(p, _)| NEIGHBORS.into_iter().map(move |n| p + n).filter_map(|p| Some((p, chunk_height(p)?))), + |(p0, h0), (p1, h1)| { + let diff = ((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32); + + diff.magnitude_squared() + }, + |(e, _)| *e == to + ); + let path = match path { + PathResult::Exhausted(p) | PathResult::Path(p) => p, + _ => return finish().boxed(), + }; + let len = path.len(); + seq( + path + .into_iter() + .enumerate() + .map(move |(i, (chunk_pos, height))| { + let wpos = TerrainChunkSize::center_wpos(chunk_pos).with_z(height).as_(); + goto(wpos, 1.0, 5.0).debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}")) + }) + ).boxed() +} + +fn pilot() -> impl Action { + // Travel between different towns in a straight line + now(|ctx| { + let data = &*ctx.state.data(); + let site = data.sites.iter() + .filter(|(id, _)| Some(*id) != ctx.npc.current_site) + .filter(|(_, site)| { + site.world_site + .and_then(|site| ctx.index.sites.get(site).kind.convert_to_meta()) + .map_or(false, |meta| matches!(meta, SiteKindMeta::Settlement(_))) + }) + .choose(&mut thread_rng()); + if let Some((_id, site)) = site { + let start_chunk = ctx.npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::(); + chunk_path(start_chunk, end_chunk, |chunk| { + ctx.world.sim().get_alt_approx(TerrainChunkSize::center_wpos(chunk)).map(|f| { + (f + 150.0) as i32 + }) + }) + } else { + finish().boxed() + } + }) + .repeat() + .map(|_| ()) +} + +fn captain() -> impl Action { + // For now just randomly travel the sea + now(|ctx| { + let chunk = ctx.npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + if let Some(chunk) = NEIGHBORS + .into_iter() + .map(|neighbor| chunk + neighbor) + .filter(|neighbor| ctx.world.sim().get(*neighbor).map_or(false, |c| c.river.river_kind.is_some())) + .choose(&mut thread_rng()) + { + let wpos = TerrainChunkSize::center_wpos(chunk); + let wpos = wpos.as_().with_z(ctx.world.sim().get_interpolated(wpos, |chunk| chunk.water_alt).unwrap_or(0.0)); + goto(wpos, 0.7, 5.0).boxed() + } else { + idle().boxed() + } + }) + .repeat().map(|_| ()) +} + fn think() -> impl Action { choose(|ctx| { - if matches!( + if let Some(riding) = &ctx.npc.riding { + if riding.steering { + if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) { + match vehicle.kind { + VehicleKind::Airship => important(pilot()), + VehicleKind::Boat => important(captain()), + } + } else { + casual(finish()) + } + } else { + important(idle()) + } + } else if matches!( ctx.npc.profession, Some(Profession::Adventurer(_) | Profession::Merchant) ) { diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 4bb83f02f6..b65e5f5f1c 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,5 +1,9 @@ -use crate::{data::npc::NpcMode, event::OnTick, RtState, Rule, RuleError}; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; +use crate::{ + data::npc::SimulationMode, + event::{OnSetup, OnTick}, + RtState, Rule, RuleError, +}; +use common::{terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid}; use tracing::info; use vek::*; @@ -7,9 +11,46 @@ pub struct SimulateNpcs; impl Rule for SimulateNpcs { fn start(rtstate: &mut RtState) -> Result { + rtstate.bind::(|ctx| { + let data = &mut *ctx.state.data_mut(); + data.npcs.npc_grid = Grid::new(ctx.world.sim().get_size().as_(), Default::default()); + + for (npc_id, npc) in data.npcs.npcs.iter() { + if let Some(ride) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { + let actor = crate::data::Actor::Npc(npc_id); + vehicle.riders.push(actor); + if ride.steering { + if vehicle.driver.replace(actor).is_some() { + panic!("Replaced driver"); + } + } + } + } + } + }); rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); - for npc in data.npcs.values_mut() { + for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { + let chunk_pos = + vehicle.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + if vehicle.chunk_pos != Some(chunk_pos) { + if let Some(cell) = vehicle + .chunk_pos + .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) + { + if let Some(index) = cell.vehicles.iter().position(|id| *id == vehicle_id) { + cell.vehicles.swap_remove(index); + } + } + vehicle.chunk_pos = Some(chunk_pos); + if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { + cell.vehicles.push(vehicle_id); + } + } + + } + for (npc_id, npc) in data.npcs.npcs.iter_mut() { // Update the NPC's current site, if any npc.current_site = ctx .world @@ -17,30 +58,102 @@ impl Rule for SimulateNpcs { .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); - // Simulate the NPC's movement and interactions - if matches!(npc.mode, NpcMode::Simulated) { - let body = npc.get_body(); - - // Move NPCs if they have a target destination - if let Some((target, speed_factor)) = npc.goto { - let diff = target.xy() - npc.wpos.xy(); - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - npc.wpos += (diff - * (body.max_speed_approx() * speed_factor * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); + let chunk_pos = + npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + if npc.chunk_pos != Some(chunk_pos) { + if let Some(cell) = npc + .chunk_pos + .and_then(|chunk_pos| data.npcs.npc_grid.get_mut(chunk_pos)) + { + if let Some(index) = cell.npcs.iter().position(|id| *id == npc_id) { + cell.npcs.swap_remove(index); } } + npc.chunk_pos = Some(chunk_pos); + if let Some(cell) = data.npcs.npc_grid.get_mut(chunk_pos) { + cell.npcs.push(npc_id); + } + } + + // Simulate the NPC's movement and interactions + if matches!(npc.mode, SimulationMode::Simulated) { + let body = npc.get_body(); + + if let Some(riding) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { + if let Some(action) = npc.action && riding.steering { + match action { + crate::data::npc::NpcAction::Goto(target, speed_factor) => { + let diff = target.xy() - vehicle.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + let mut wpos = vehicle.wpos + (diff + * (vehicle.get_speed() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + + let is_valid = match vehicle.kind { + crate::data::npc::VehicleKind::Airship => true, + crate::data::npc::VehicleKind::Boat => { + let chunk_pos = wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some()) + }, + }; + + if is_valid { + match vehicle.kind { + crate::data::npc::VehicleKind::Airship => { + if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) { + wpos.z = alt; + } + }, + crate::data::npc::VehicleKind::Boat => { + wpos.z = ctx + .world + .sim() + .get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt) + .unwrap_or(0.0); + }, + } + vehicle.wpos = wpos; + } + } + } + } + } + npc.wpos = vehicle.wpos; + } else { + // Vehicle doens't exist anymore + npc.riding = None; + } + } + // Move NPCs if they have a target destination + else if let Some(action) = npc.action { + match action { + crate::data::npc::NpcAction::Goto(target, speed_factor) => { + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + npc.wpos += (diff + * (body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } + }, + } + + // Make sure NPCs remain on the surface + npc.wpos.z = ctx + .world + .sim() + .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) + .unwrap_or(0.0); + } - // Make sure NPCs remain on the surface - npc.wpos.z = ctx - .world - .sim() - .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) - .unwrap_or(0.0); } } }); diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 8fc80890b0..10f3739f33 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -10,7 +10,7 @@ use common::{ Vel, }, link::Is, - mounting::Mount, + mounting::{Mount, Rider}, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, rtsim::RtSimEntity, @@ -233,6 +233,7 @@ pub struct ReadData<'a> { pub alignments: ReadStorage<'a, Alignment>, pub bodies: ReadStorage<'a, Body>, pub is_mounts: ReadStorage<'a, Is>, + pub is_riders: ReadStorage<'a, Is>, pub time_of_day: Read<'a, TimeOfDay>, pub light_emitter: ReadStorage<'a, LightEmitter>, #[cfg(feature = "worldgen")] diff --git a/server/src/cmd.rs b/server/src/cmd.rs index e724894f88..dc14b666d5 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1480,7 +1480,7 @@ fn handle_spawn_airship( let ship = comp::ship::Body::random_airship_with(&mut rng); let mut builder = server .state - .create_ship(pos, ship, |ship| ship.make_collider(), true) + .create_ship(pos, ship, |ship| ship.make_collider()) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, @@ -1488,7 +1488,7 @@ fn handle_spawn_airship( animated: true, }); if let Some(pos) = destination { - let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)); + let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) .with_destination(pos) @@ -1528,7 +1528,7 @@ fn handle_spawn_ship( let ship = comp::ship::Body::random_ship_with(&mut rng); let mut builder = server .state - .create_ship(pos, ship, |ship| ship.make_collider(), true) + .create_ship(pos, ship, |ship| ship.make_collider()) .with(LightEmitter { col: Rgb::new(1.0, 0.65, 0.2), strength: 2.0, @@ -1536,7 +1536,7 @@ fn handle_spawn_ship( animated: true, }); if let Some(pos) = destination { - let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)); + let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) .with_destination(pos) @@ -1581,7 +1581,6 @@ fn handle_make_volume( comp::Pos(pos.0 + Vec3::unit_z() * 50.0), ship, move |_| collider, - true, ) .build(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index dd9173948b..5601d755b9 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -14,14 +14,14 @@ use common::{ LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, TradingBehavior, Vel, WaypointArea, }, - event::{EventBus, UpdateCharacterMetadata}, + event::{EventBus, UpdateCharacterMetadata, NpcBuilder}, lottery::LootSpec, outcome::Outcome, resources::{Secs, Time}, - rtsim::RtSimEntity, + rtsim::{RtSimEntity, RtSimVehicle}, uid::Uid, util::Dir, - ViewDistances, + ViewDistances, mounting::Mounting, }; use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use specs::{Builder, Entity as EcsEntity, WorldExt}; @@ -94,60 +94,47 @@ pub fn handle_loaded_character_data( pub fn handle_create_npc( server: &mut Server, pos: Pos, - stats: Stats, - skill_set: SkillSet, - health: Option, - poise: Poise, - inventory: Inventory, - body: Body, - agent: impl Into>, - alignment: Alignment, - scale: Scale, - loot: LootSpec, - home_chunk: Option, - rtsim_entity: Option, - projectile: Option, -) { + mut npc: NpcBuilder, +) -> EcsEntity { let entity = server .state - .create_npc(pos, stats, skill_set, health, poise, inventory, body) - .with(scale); + .create_npc(pos, npc.stats, npc.skill_set, npc.health, npc.poise, npc.inventory, npc.body) + .with(npc.scale); - let mut agent = agent.into(); - if let Some(agent) = &mut agent { - if let Alignment::Owned(_) = &alignment { + if let Some(agent) = &mut npc.agent { + if let Alignment::Owned(_) = &npc.alignment { agent.behavior.allow(BehaviorCapability::TRADE); agent.behavior.trading_behavior = TradingBehavior::AcceptFood; } } - let entity = entity.with(alignment); + let entity = entity.with(npc.alignment); - let entity = if let Some(agent) = agent { + let entity = if let Some(agent) = npc.agent { entity.with(agent) } else { entity }; - let entity = if let Some(drop_item) = loot.to_item() { + let entity = if let Some(drop_item) = npc.loot.to_item() { entity.with(ItemDrop(drop_item)) } else { entity }; - let entity = if let Some(home_chunk) = home_chunk { + let entity = if let Some(home_chunk) = npc.anchor { entity.with(home_chunk) } else { entity }; - let entity = if let Some(rtsim_entity) = rtsim_entity { + let entity = if let Some(rtsim_entity) = npc.rtsim_entity { entity.with(rtsim_entity) } else { entity }; - let entity = if let Some(projectile) = projectile { + let entity = if let Some(projectile) = npc.projectile { entity.with(projectile) } else { entity @@ -156,7 +143,7 @@ pub fn handle_create_npc( let new_entity = entity.build(); // Add to group system if a pet - if let comp::Alignment::Owned(owner_uid) = alignment { + if let comp::Alignment::Owned(owner_uid) = npc.alignment { let state = server.state(); let clients = state.ecs().read_storage::(); let uids = state.ecs().read_storage::(); @@ -187,7 +174,7 @@ pub fn handle_create_npc( }, ); } - } else if let Some(group) = match alignment { + } else if let Some(group) = match npc.alignment { Alignment::Wild => None, Alignment::Passive => None, Alignment::Enemy => Some(comp::group::ENEMY), @@ -196,19 +183,23 @@ pub fn handle_create_npc( } { let _ = server.state.ecs().write_storage().insert(new_entity, group); } + + new_entity } pub fn handle_create_ship( server: &mut Server, pos: Pos, ship: comp::ship::Body, - mountable: bool, - agent: Option, - rtsim_entity: Option, + rtsim_vehicle: Option, + driver: Option, + passangers: Vec, + ) { let mut entity = server .state - .create_ship(pos, ship, |ship| ship.make_collider(), mountable); + .create_ship(pos, ship, |ship| ship.make_collider()); + /* if let Some(mut agent) = agent { let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } @@ -216,10 +207,33 @@ pub fn handle_create_ship( agent.with_position_pid_controller(PidController::new(kp, ki, kd, pos.0, 0.0, pure_z)); entity = entity.with(agent); } - if let Some(rtsim_entity) = rtsim_entity { - entity = entity.with(rtsim_entity); + */ + if let Some(rtsim_vehicle) = rtsim_vehicle { + entity = entity.with(rtsim_vehicle); + } + let entity = entity.build(); + + + if let Some(driver) = driver { + let npc_entity = handle_create_npc(server, pos, driver); + + let uids = server.state.ecs().read_storage::(); + if let (Some(rider_uid), Some(mount_uid)) = + (uids.get(npc_entity).copied(), uids.get(entity).copied()) + { + drop(uids); + server.state.link(Mounting { + mount: mount_uid, + rider: rider_uid, + }).expect("Failed to link driver to ship"); + } else { + panic!("Couldn't get Uid from newly created ship and npc"); + } + } + + for passanger in passangers { + handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passanger); } - entity.build(); } pub fn handle_shoot( diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 01bcd58e3e..8637890b87 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -190,43 +190,21 @@ impl Server { }, ServerEvent::CreateNpc { pos, - stats, - skill_set, - health, - poise, - inventory, - body, - agent, - alignment, - scale, - anchor: home_chunk, - loot, - rtsim_entity, - projectile, - } => handle_create_npc( - self, - pos, - stats, - skill_set, - health, - poise, - inventory, - body, - agent, - alignment, - scale, - loot, - home_chunk, - rtsim_entity, - projectile, - ), + npc, + } => { + handle_create_npc( + self, + pos, + npc, + ); + }, ServerEvent::CreateShip { pos, ship, - mountable, - agent, rtsim_entity, - } => handle_create_ship(self, pos, ship, mountable, agent, rtsim_entity), + driver, + passangers, + } => handle_create_ship(self, pos, ship, rtsim_entity, driver, passangers), ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos), ServerEvent::ClientDisconnect(entity, reason) => { frontend_events.push(handle_client_disconnect(self, entity, reason, false)) diff --git a/server/src/lib.rs b/server/src/lib.rs index 7deaa9e7c4..44332db921 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -80,8 +80,8 @@ use common::{ comp, event::{EventBus, ServerEvent}, resources::{BattleMode, GameMode, Time, TimeOfDay}, - rtsim::RtSimEntity, shared_server_config::ServerConstants, + rtsim::{RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, terrain::{Block, TerrainChunk, TerrainChunkSize}, vol::RectRasterableVol, @@ -387,6 +387,7 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); + state.ecs_mut().register::(); // Load banned words list let banned_words = settings.moderation.load_banned_words(data_dir); @@ -873,6 +874,19 @@ impl Server { .write_resource::() .hook_rtsim_entity_unload(rtsim_entity); } + #[cfg(feature = "worldgen")] + if let Some(rtsim_vehicle) = self + .state + .ecs() + .read_storage::() + .get(entity) + .copied() + { + self.state + .ecs() + .write_resource::() + .hook_rtsim_vehicle_unload(rtsim_vehicle); + } if let Err(e) = self.state.delete_entity_recorded(entity) { error!(?e, "Failed to delete agent outside the terrain"); diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 7d8ee64ce7..1bbb35e863 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -4,7 +4,7 @@ pub mod tick; use common::{ grid::Grid, - rtsim::{ChunkResource, RtSimEntity, WorldSettings}, + rtsim::{ChunkResource, RtSimEntity, WorldSettings, RtSimVehicle}, slowjob::SlowJobPool, terrain::{Block, TerrainChunk}, vol::RectRasterableVol, @@ -12,7 +12,7 @@ use common::{ use common_ecs::{dispatch, System}; use enum_map::EnumMap; use rtsim2::{ - data::{npc::NpcMode, Data, ReadError}, + data::{npc::SimulationMode, Data, ReadError}, event::{OnDeath, OnSetup}, rule::Rule, RtState, @@ -150,7 +150,13 @@ impl RtSim { pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { - npc.mode = NpcMode::Simulated; + npc.mode = SimulationMode::Simulated; + } + } + + pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { + if let Some(vehicle) = self.state.data_mut().npcs.vehicles.get_mut(entity.0) { + vehicle.mode = SimulationMode::Simulated; } } diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 586c6a9f48..7136dc5692 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -3,19 +3,19 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, inventory::loadout::Loadout, skillset::skills}, - event::{EventBus, ServerEvent}, + comp::{self, inventory::loadout::Loadout, skillset::skills, Body, Agent}, + event::{EventBus, ServerEvent, NpcBuilder}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{RtSimController, RtSimEntity}, + rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, trade::{Good, SiteInformation}, LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::{ - npc::{NpcMode, Profession}, - Npc, Sites, + npc::{SimulationMode, Profession}, + Npc, Sites, Actor, }; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; @@ -26,6 +26,7 @@ fn humanoid_config(profession: &Profession) -> &'static str { Profession::Farmer => "common.entity.village.farmer", Profession::Hunter => "common.entity.village.hunter", Profession::Herbalist => "common.entity.village.herbalist", + Profession::Captain => "common.entity.village.captain", Profession::Merchant => "common.entity.village.merchant", Profession::Guard => "common.entity.village.guard", Profession::Adventurer(rank) => match rank { @@ -146,9 +147,9 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo } else { comp::Alignment::Npc }) - .with_maybe_economy(economy.as_ref()) + .with_economy(economy.as_ref()) .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) - .with_maybe_agent_mark(profession_agent_mark(npc.profession.as_ref())) + .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) } else { EntityInfo::at(pos.0) .with_body(body) @@ -171,6 +172,7 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, SlowJobPool>, ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, + ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, ); @@ -191,6 +193,7 @@ impl<'a> System<'a> for Sys { slow_jobs, positions, rtsim_entities, + rtsim_vehicles, mut agents, ): Self::SystemData, ) { @@ -211,18 +214,79 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); let data = &mut *rtsim.state.data_mut(); - for (npc_id, npc) in data.npcs.iter_mut() { + + for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { + let chunk = vehicle.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e as i32).div_euclid(sz as i32) + }); + + if matches!(vehicle.mode, SimulationMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) + { + vehicle.mode = SimulationMode::Loaded; + + let mut actor_info = |actor: Actor| { + let npc_id = actor.npc()?; + let npc = data.npcs.npcs.get_mut(npc_id)?; + if matches!(npc.mode, SimulationMode::Simulated) { + npc.mode = SimulationMode::Loaded; + let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); + + Some(match NpcData::from_entity_info(entity_info) { + NpcData::Data { + pos: _, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + loot, + } => NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), + // EntityConfig can't represent Waypoints at all + // as of now, and if someone will try to spawn + // rtsim waypoint it is definitely error. + NpcData::Waypoint(_) => unimplemented!(), + }) + } else { + error!("Npc is loaded but vehicle is unloaded"); + None + } + }; + + emitter.emit(ServerEvent::CreateShip { + pos: comp::Pos(vehicle.wpos), + ship: vehicle.get_ship(), + // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), + rtsim_entity: Some(RtSimVehicle(vehicle_id)), + driver: vehicle.driver.and_then(&mut actor_info), + passangers: vehicle.riders.iter().copied().filter(|actor| vehicle.driver != Some(*actor)).filter_map(actor_info).collect(), + }); + } + } + + for (npc_id, npc) in data.npcs.npcs.iter_mut() { let chunk = npc.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { (e as i32).div_euclid(sz as i32) }); // Load the NPC into the world if it's in a loaded chunk and is not already // loaded - if matches!(npc.mode, NpcMode::Simulated) + if matches!(npc.mode, SimulationMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { - npc.mode = NpcMode::Loaded; - + npc.mode = SimulationMode::Loaded; let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); emitter.emit(match NpcData::from_entity_info(entity_info) { @@ -240,19 +304,15 @@ impl<'a> System<'a> for Sys { loot, } => ServerEvent::CreateNpc { pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: None, - loot, - rtsim_entity: Some(RtSimEntity(npc_id)), - projectile: None, + npc: NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), }, // EntityConfig can't represent Waypoints at all // as of now, and if someone will try to spawn @@ -262,21 +322,43 @@ impl<'a> System<'a> for Sys { } } + // Synchronise rtsim NPC with entity data + for (pos, rtsim_vehicle) in + (&positions, &rtsim_vehicles).join() + { + data.npcs.vehicles + .get_mut(rtsim_vehicle.0) + .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) + .map(|vehicle| { + // Update rtsim NPC state + vehicle.wpos = pos.0; + }); + } + // Synchronise rtsim NPC with entity data for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, (&mut agents).maybe()).join() { data.npcs .get_mut(rtsim_entity.0) - .filter(|npc| matches!(npc.mode, NpcMode::Loaded)) + .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) .map(|npc| { // Update rtsim NPC state npc.wpos = pos.0; // Update entity state if let Some(agent) = agent { - agent.rtsim_controller.travel_to = npc.goto.map(|(wpos, _)| wpos); - agent.rtsim_controller.speed_factor = npc.goto.map_or(1.0, |(_, sf)| sf); + if let Some(action) = npc.action { + match action { + rtsim2::data::npc::NpcAction::Goto(wpos, sf) => { + agent.rtsim_controller.travel_to = Some(wpos); + agent.rtsim_controller.speed_factor = sf; + }, + } + } else { + agent.rtsim_controller.travel_to = None; + agent.rtsim_controller.speed_factor = 1.0; + } } }); } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index e6a953013e..1e151dd792 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -65,7 +65,6 @@ pub trait StateExt { pos: comp::Pos, ship: comp::ship::Body, make_collider: F, - mountable: bool, ) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( @@ -338,7 +337,6 @@ impl StateExt for State { pos: comp::Pos, ship: comp::ship::Body, make_collider: F, - mountable: bool, ) -> EcsEntityBuilder { let body = comp::Body::Ship(ship); let builder = self @@ -362,9 +360,6 @@ impl StateExt for State { .with(comp::ActiveAbilities::default()) .with(comp::Combo::default()); - if mountable { - // TODO: Re-add mounting check - } builder } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index e0c3d3deff..dde39a5319 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -1,5 +1,6 @@ pub mod behavior_tree; pub use server_agent::{action_nodes, attack, consts, data, util}; +use vek::Vec3; use crate::sys::agent::{ behavior_tree::{BehaviorData, BehaviorTree}, @@ -18,7 +19,7 @@ use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::thread_rng; use rayon::iter::ParallelIterator; -use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage}; +use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage, saveload::MarkerAllocator}; /// This system will allow NPCs to modify their controller #[derive(Default)] @@ -70,6 +71,7 @@ impl<'a> System<'a> for Sys { read_data.groups.maybe(), read_data.rtsim_entities.maybe(), !&read_data.is_mounts, + read_data.is_riders.maybe(), ) .par_join() .for_each_init( @@ -93,10 +95,16 @@ impl<'a> System<'a> for Sys { group, rtsim_entity, _, + is_rider, )| { let mut event_emitter = event_bus.emitter(); let mut rng = thread_rng(); + // The entity that is moving, if riding it's the mount, otherwise it's itself + let moving_entity = is_rider.and_then(|is_rider| read_data.uid_allocator.retrieve_entity_internal(is_rider.mount.into())).unwrap_or(entity); + + let moving_body = read_data.bodies.get(moving_entity); + // Hack, replace with better system when groups are more sophisticated // Override alignment if in a group unless entity is owned already let alignment = if matches!( @@ -139,8 +147,17 @@ impl<'a> System<'a> for Sys { Some(CharacterState::GlideWield(_) | CharacterState::Glide(_)) ) && physics_state.on_ground.is_none(); - if let Some(pid) = agent.position_pid_controller.as_mut() { + if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients) { + if agent.position_pid_controller.as_ref().map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd)) { + agent.position_pid_controller = None; + } + let pid = agent.position_pid_controller.get_or_insert_with(|| { + fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } + comp::PidController::new(kp, ki, kd, pos.0, 0.0, pure_z) + }); pid.add_measurement(read_data.time.0, pos.0); + } else { + agent.position_pid_controller = None; } // This controls how picky NPCs are about their pathfinding. @@ -149,15 +166,15 @@ impl<'a> System<'a> for Sys { // (especially since they would otherwise get stuck on // obstacles that smaller entities would not). let node_tolerance = scale * 1.5; - let slow_factor = body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0); + let slow_factor = moving_body.map_or(0.0, |b| b.base_accel() / 250.0).min(1.0); let traversal_config = TraversalConfig { node_tolerance, slow_factor, on_ground: physics_state.on_ground.is_some(), in_liquid: physics_state.in_liquid().is_some(), min_tgt_dist: 1.0, - can_climb: body.map_or(false, Body::can_climb), - can_fly: body.map_or(false, |b| b.fly_thrust().is_some()), + can_climb: moving_body.map_or(false, Body::can_climb), + can_fly: moving_body.map_or(false, |b| b.fly_thrust().is_some()), }; let health_fraction = health.map_or(1.0, Health::fraction); /* @@ -167,7 +184,7 @@ impl<'a> System<'a> for Sys { .and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0)); */ - if traversal_config.can_fly && matches!(body, Some(Body::Ship(_))) { + if traversal_config.can_fly && matches!(moving_body, Some(Body::Ship(_))) { // hack (kinda): Never turn off flight airships // since it results in stuttering and falling back to the ground. // diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index ea6fd4952e..c864352fcd 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -19,7 +19,7 @@ use common::{ comp::{ self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, }, - event::{EventBus, ServerEvent}, + event::{EventBus, ServerEvent, NpcBuilder}, generation::EntityInfo, lottery::LootSpec, resources::{Time, TimeOfDay}, @@ -217,19 +217,15 @@ impl<'a> System<'a> for Sys { } => { server_emitter.emit(ServerEvent::CreateNpc { pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: Some(comp::Anchor::Chunk(key)), - loot, - rtsim_entity: None, - projectile: None, + npc: NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_anchor(comp::Anchor::Chunk(key)) + .with_loot(loot) }); }, } From 64bd11d34ad27db67ec9f01cf34c0bcff2d9b01e Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 8 Mar 2023 22:40:16 +0100 Subject: [PATCH 059/139] use wpos_to_cpos --- server/src/cmd.rs | 4 +--- server/src/rtsim2/rule/deplete_resources.rs | 5 ++--- server/src/rtsim2/tick.rs | 10 +++------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index dc14b666d5..8ad10cdfc3 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1273,9 +1273,7 @@ fn handle_rtsim_chunk( use crate::rtsim2::{ChunkStates, RtSim}; let pos = position(server, target, "target")?; - let chunk_key = pos.0.xy().map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { - e as i32 / sz as i32 - }); + let chunk_key = pos.0.xy().as_::().wpos_to_cpos(); let rtsim = server.state.ecs().read_resource::(); let data = rtsim.state().data(); diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs index 810af3aaed..1bc0667b40 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -1,5 +1,5 @@ use crate::rtsim2::{event::OnBlockChange, ChunkStates}; -use common::{terrain::TerrainChunk, vol::RectRasterableVol}; +use common::{terrain::{TerrainChunk, CoordinateConversions}, vol::RectRasterableVol}; use rtsim2::{RtState, Rule, RuleError}; pub struct DepleteResources; @@ -10,8 +10,7 @@ impl Rule for DepleteResources { let key = ctx .event .wpos - .xy() - .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); + .xy().wpos_to_cpos(); if let Some(Some(chunk_state)) = ctx.state.resource_mut::().0.get(key) { let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key); // Remove resources diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 7136dc5692..fb53d80eb5 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -10,7 +10,7 @@ use common::{ rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, + LoadoutBuilder, SkillSetBuilder, terrain::CoordinateConversions, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::{ @@ -216,9 +216,7 @@ impl<'a> System<'a> for Sys { let data = &mut *rtsim.state.data_mut(); for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { - let chunk = vehicle.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e as i32).div_euclid(sz as i32) - }); + let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); if matches!(vehicle.mode, SimulationMode::Simulated) && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) @@ -277,9 +275,7 @@ impl<'a> System<'a> for Sys { } for (npc_id, npc) in data.npcs.npcs.iter_mut() { - let chunk = npc.wpos.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e as i32).div_euclid(sz as i32) - }); + let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); // Load the NPC into the world if it's in a loaded chunk and is not already // loaded From 259bb6fce47016de008ce144daf3702d010299ba Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 8 Mar 2023 23:31:16 +0100 Subject: [PATCH 060/139] fix phys test --- common/systems/tests/character_state.rs | 1 + common/systems/tests/phys/basic.rs | 1 + common/systems/tests/phys/utils.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/common/systems/tests/character_state.rs b/common/systems/tests/character_state.rs index 425a2ab157..8f1dcbf58f 100644 --- a/common/systems/tests/character_state.rs +++ b/common/systems/tests/character_state.rs @@ -84,6 +84,7 @@ mod tests { None, // Dummy ServerConstants &ServerConstants::default(), + |_, _, _, _| {}, ); } diff --git a/common/systems/tests/phys/basic.rs b/common/systems/tests/phys/basic.rs index e7eabcbfd0..dbeab553ea 100644 --- a/common/systems/tests/phys/basic.rs +++ b/common/systems/tests/phys/basic.rs @@ -19,6 +19,7 @@ fn simple_run() { false, None, &ServerConstants::default(), + |_, _, _, _| {}, ); } diff --git a/common/systems/tests/phys/utils.rs b/common/systems/tests/phys/utils.rs index 3d6ac32cd8..0fd4f08515 100644 --- a/common/systems/tests/phys/utils.rs +++ b/common/systems/tests/phys/utils.rs @@ -66,6 +66,7 @@ pub fn tick(state: &mut State, dt: Duration) { false, None, &ServerConstants::default(), + |_, _, _, _| {}, ); } From dda1be58d4a68abf845f74cb439154910e206e42 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 9 Mar 2023 10:20:05 +0100 Subject: [PATCH 061/139] big birds! --- common/src/generation.rs | 6 + common/systems/src/mount.rs | 2 +- rtsim/src/data/npc.rs | 103 ++++++------- rtsim/src/gen/mod.rs | 70 +++++++-- rtsim/src/rule/npc_ai.rs | 154 ++++++++++++++++---- rtsim/src/rule/simulate_npcs.rs | 24 +-- server/src/cmd.rs | 14 +- server/src/events/entity_creation.rs | 36 +++-- server/src/events/interaction.rs | 10 +- server/src/events/mod.rs | 11 +- server/src/rtsim2/mod.rs | 2 +- server/src/rtsim2/rule/deplete_resources.rs | 10 +- server/src/rtsim2/tick.rs | 87 ++++++----- server/src/sys/agent.rs | 19 ++- server/src/sys/terrain.rs | 4 +- world/src/sim/mod.rs | 7 + 16 files changed, 370 insertions(+), 189 deletions(-) diff --git a/common/src/generation.rs b/common/src/generation.rs index 041f7a4b8e..9db9769c91 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -446,6 +446,12 @@ impl EntityInfo { self.no_flee = true; self } + + #[must_use] + pub fn with_loadout(mut self, loadout: LoadoutBuilder) -> Self { + self.loadout = loadout; + self + } } #[derive(Default)] diff --git a/common/systems/src/mount.rs b/common/systems/src/mount.rs index ee1945d7f1..b8d70b37b9 100644 --- a/common/systems/src/mount.rs +++ b/common/systems/src/mount.rs @@ -1,5 +1,5 @@ use common::{ - comp::{Body, Controller, InputKind, Ori, Pos, Scale, Vel, ControlAction}, + comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel}, link::Is, mounting::Mount, uid::UidAllocator, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index ff6a513318..21ee9b4903 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -5,7 +5,8 @@ use common::{ grid::Grid, rtsim::{FactionId, RtSimController, SiteId, VehicleId}, store::Id, - uid::Uid, vol::RectVolSize, + uid::Uid, + vol::RectVolSize, }; use hashbrown::HashMap; use rand::prelude::*; @@ -21,7 +22,11 @@ use std::{ }, }; use vek::*; -use world::{civ::Track, site::Site as WorldSite, util::{RandomPerm, LOCALITY}}; +use world::{ + civ::Track, + site::Site as WorldSite, + util::{RandomPerm, LOCALITY}, +}; use super::Actor; @@ -78,6 +83,7 @@ pub struct Npc { pub seed: u32, pub wpos: Vec3, + pub body: comp::Body, pub profession: Option, pub home: Option, pub faction: Option, @@ -113,6 +119,7 @@ impl Clone for Npc { home: self.home, faction: self.faction, riding: self.riding.clone(), + body: self.body, // Not persisted chunk_pos: None, current_site: Default::default(), @@ -124,13 +131,11 @@ impl Clone for Npc { } impl Npc { - const PERM_BODY: u32 = 1; - const PERM_SPECIES: u32 = 0; - - pub fn new(seed: u32, wpos: Vec3) -> Self { + pub fn new(seed: u32, wpos: Vec3, body: comp::Body) -> Self { Self { seed, wpos, + body, profession: None, home: None, faction: None, @@ -154,21 +159,17 @@ impl Npc { } pub fn steering(mut self, vehicle: impl Into>) -> Self { - self.riding = vehicle.into().map(|vehicle| { - Riding { - vehicle, - steering: true, - } + self.riding = vehicle.into().map(|vehicle| Riding { + vehicle, + steering: true, }); self } pub fn riding(mut self, vehicle: impl Into>) -> Self { - self.riding = vehicle.into().map(|vehicle| { - Riding { - vehicle, - steering: false, - } + self.riding = vehicle.into().map(|vehicle| Riding { + vehicle, + steering: false, }); self } @@ -179,13 +180,6 @@ impl Npc { } pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed.wrapping_add(perm)) } - - pub fn get_body(&self) -> comp::Body { - let species = *(&comp::humanoid::ALL_SPECIES) - .choose(&mut self.rng(Self::PERM_SPECIES)) - .unwrap(); - comp::humanoid::Body::random_with(&mut self.rng(Self::PERM_BODY), &species).into() - } } #[derive(Clone, Serialize, Deserialize)] @@ -204,7 +198,7 @@ pub enum VehicleKind { pub struct Vehicle { pub wpos: Vec3, - pub kind: VehicleKind, + pub body: comp::ship::Body, #[serde(skip_serializing, skip_deserializing)] pub chunk_pos: Option>, @@ -216,41 +210,36 @@ pub struct Vehicle { // TODO: Find a way to detect riders when the vehicle is loaded pub riders: Vec, - /// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on the - /// server, loaded corresponds to being within a loaded chunk). When in - /// loaded mode, the interactions of the Vehicle should not be simulated but - /// should instead be derived from the game. + /// Whether the Vehicle is in simulated or loaded mode (when rtsim is run on + /// the server, loaded corresponds to being within a loaded chunk). When + /// in loaded mode, the interactions of the Vehicle should not be + /// simulated but should instead be derived from the game. #[serde(skip_serializing, skip_deserializing)] pub mode: SimulationMode, } impl Vehicle { - pub fn new(wpos: Vec3, kind: VehicleKind) -> Self { + pub fn new(wpos: Vec3, body: comp::ship::Body) -> Self { Self { wpos, - kind, + body, chunk_pos: None, driver: None, riders: Vec::new(), mode: SimulationMode::Simulated, } } - pub fn get_ship(&self) -> comp::ship::Body { - match self.kind { - VehicleKind::Airship => comp::ship::Body::DefaultAirship, - VehicleKind::Boat => comp::ship::Body::Galleon, - } - } - pub fn get_body(&self) -> comp::Body { - comp::Body::Ship(self.get_ship()) - } + pub fn get_body(&self) -> comp::Body { comp::Body::Ship(self.body) } /// Max speed in block/s pub fn get_speed(&self) -> f32 { - match self.kind { - VehicleKind::Airship => 15.0, - VehicleKind::Boat => 13.0, + match self.body { + comp::ship::Body::DefaultAirship => 15.0, + comp::ship::Body::AirBalloon => 16.0, + comp::ship::Body::SailBoat => 12.0, + comp::ship::Body::Galleon => 13.0, + _ => 10.0, } } } @@ -274,27 +263,29 @@ fn construct_npc_grid() -> Grid { Grid::new(Vec2::zero(), Default::def impl Npcs { pub fn create_npc(&mut self, npc: Npc) -> NpcId { self.npcs.insert(npc) } - pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { self.vehicles.insert(vehicle) } + pub fn create_vehicle(&mut self, vehicle: Vehicle) -> VehicleId { + self.vehicles.insert(vehicle) + } /// Queries nearby npcs, not garantueed to work if radius > 32.0 pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { - let chunk_pos = wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); + let chunk_pos = + wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); let r_sqr = radius * radius; LOCALITY .into_iter() .filter_map(move |neighbor| { - self - .npc_grid - .get(chunk_pos + neighbor) - .map(|cell| { - cell.npcs.iter() - .copied() - .filter(|npc| { - self.npcs.get(*npc) - .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) - }) - .collect::>() - }) + self.npc_grid.get(chunk_pos + neighbor).map(|cell| { + cell.npcs + .iter() + .copied() + .filter(|npc| { + self.npcs + .get(*npc) + .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) + }) + .collect::>() + }) }) .flatten() } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 368c532697..37de8f34d4 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -8,7 +8,11 @@ use crate::data::{ Data, Nature, }; use common::{ - grid::Grid, resources::TimeOfDay, rtsim::WorldSettings, terrain::TerrainChunkSize, + comp::{self, Body}, + grid::Grid, + resources::TimeOfDay, + rtsim::WorldSettings, + terrain::TerrainChunkSize, vol::RectVolSize, }; use hashbrown::HashMap; @@ -72,12 +76,12 @@ impl Data { "Registering {} rtsim sites from world sites.", this.sites.len() ); - + /* // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() // TODO: Stupid .filter(|(_, site)| site.world_site.map_or(false, |ws| - matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) .skip(1) + matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) { let Some(good_or_evil) = site .faction @@ -91,10 +95,17 @@ impl Data { .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; + let random_humanoid = |rng: &mut SmallRng| { + let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); + Body::Humanoid(comp::humanoid::Body::random_with( + rng, + species, + )) + }; if good_or_evil { for _ in 0..32 { this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng)) + Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_faction(site.faction) .with_home(site_id) .with_profession(match rng.gen_range(0..20) { @@ -112,7 +123,7 @@ impl Data { } else { for _ in 0..15 { this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng)) + Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_faction(site.faction) .with_home(site_id) .with_profession(match rng.gen_range(0..20) { @@ -122,17 +133,54 @@ impl Data { } } this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng)) + Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_home(site_id) .with_profession(Profession::Merchant), ); - let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0; - let vehicle_id = this.npcs.create_vehicle(Vehicle::new(wpos, VehicleKind::Airship)); - - this.npcs.create_npc(Npc::new(rng.gen(), wpos).with_home(site_id).with_profession(Profession::Captain).steering(vehicle_id)); - } + if rng.gen_bool(0.4) { + let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0; + let vehicle_id = this + .npcs + .create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship)); + this.npcs.create_npc( + Npc::new(rng.gen(), wpos, random_humanoid(&mut rng)) + .with_home(site_id) + .with_profession(Profession::Captain) + .steering(vehicle_id), + ); + } + } + */ + for (site_id, site) in this.sites.iter() + // TODO: Stupid + .filter(|(_, site)| site.world_site.map_or(false, |ws| + matches!(&index.sites.get(ws).kind, SiteKind::Dungeon(_)))) + { + let rand_wpos = |rng: &mut SmallRng| { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + wpos2d + .map(|e| e as f32 + 0.5) + .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + + let species = [ + comp::body::bird_large::Species::Phoenix, + comp::body::bird_large::Species::Cockatrice, + comp::body::bird_large::Species::Roc, + ] + .choose(&mut rng) + .unwrap(); + this.npcs.create_npc( + Npc::new( + rng.gen(), + rand_wpos(&mut rng), + Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)), + ) + .with_home(site_id), + ); + } info!("Generated {} rtsim NPCs.", this.npcs.len()); this diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index c8e7ce4c53..99db92dd5e 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -14,7 +14,7 @@ use common::{ path::Path, rtsim::{Profession, SiteId}, store::Id, - terrain::{TerrainChunkSize, SiteKindMeta}, + terrain::{SiteKindMeta, TerrainChunkSize}, time::DayPeriod, vol::RectVolSize, }; @@ -32,7 +32,8 @@ use world::{ civ::{self, Track}, site::{Site as WorldSite, SiteKind}, site2::{self, PlotKind, TileKind}, - IndexRef, World, util::NEIGHBORS, + util::NEIGHBORS, + IndexRef, World, }; pub struct NpcAi; @@ -329,7 +330,11 @@ fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { // Get the next waypoint on the route toward the goal let waypoint = - waypoint.get_or_insert_with(|| ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST)); + waypoint.get_or_insert_with(|| { + let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST); + + wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()).unwrap_or(wpos.z)) + }); *ctx.controller = Controller::goto(*waypoint, speed_factor); }) @@ -635,7 +640,11 @@ fn follow(npc: NpcId, distance: f32) -> impl Action { .map(|_| {}) } -fn chunk_path(from: Vec2, to: Vec2, chunk_height: impl Fn(Vec2) -> Option) -> Box { +fn chunk_path( + from: Vec2, + to: Vec2, + chunk_height: impl Fn(Vec2) -> Option, +) -> Box { let heuristics = |(p, _): &(Vec2, i32)| p.distance_squared(to) as f32; let start = (from, chunk_height(from).unwrap()); let mut astar = Astar::new( @@ -648,35 +657,45 @@ fn chunk_path(from: Vec2, to: Vec2, chunk_height: impl Fn(Vec2) - let path = astar.poll( 1000, heuristics, - |&(p, _)| NEIGHBORS.into_iter().map(move |n| p + n).filter_map(|p| Some((p, chunk_height(p)?))), + |&(p, _)| { + NEIGHBORS + .into_iter() + .map(move |n| p + n) + .filter_map(|p| Some((p, chunk_height(p)?))) + }, |(p0, h0), (p1, h1)| { - let diff = ((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32); + let diff = + ((p0 - p1).as_() * TerrainChunkSize::RECT_SIZE.as_()).with_z((h0 - h1) as f32); diff.magnitude_squared() }, - |(e, _)| *e == to + |(e, _)| *e == to, ); let path = match path { PathResult::Exhausted(p) | PathResult::Path(p) => p, _ => return finish().boxed(), }; let len = path.len(); - seq( - path - .into_iter() - .enumerate() - .map(move |(i, (chunk_pos, height))| { - let wpos = TerrainChunkSize::center_wpos(chunk_pos).with_z(height).as_(); - goto(wpos, 1.0, 5.0).debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}")) - }) - ).boxed() + seq(path + .into_iter() + .enumerate() + .map(move |(i, (chunk_pos, height))| { + let wpos = TerrainChunkSize::center_wpos(chunk_pos) + .with_z(height) + .as_(); + goto(wpos, 1.0, 5.0) + .debug(move || format!("chunk path {i}/{len} chunk: {chunk_pos}, height: {height}")) + })) + .boxed() } fn pilot() -> impl Action { // Travel between different towns in a straight line now(|ctx| { let data = &*ctx.state.data(); - let site = data.sites.iter() + let site = data + .sites + .iter() .filter(|(id, _)| Some(*id) != ctx.npc.current_site) .filter(|(_, site)| { site.world_site @@ -685,12 +704,14 @@ fn pilot() -> impl Action { }) .choose(&mut thread_rng()); if let Some((_id, site)) = site { - let start_chunk = ctx.npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); + let start_chunk = + ctx.npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); let end_chunk = site.wpos / TerrainChunkSize::RECT_SIZE.as_::(); - chunk_path(start_chunk, end_chunk, |chunk| { - ctx.world.sim().get_alt_approx(TerrainChunkSize::center_wpos(chunk)).map(|f| { - (f + 150.0) as i32 - }) + chunk_path(start_chunk, end_chunk, |chunk| { + ctx.world + .sim() + .get_alt_approx(TerrainChunkSize::center_wpos(chunk)) + .map(|f| (f + 150.0) as i32) }) } else { finish().boxed() @@ -707,27 +728,42 @@ fn captain() -> impl Action { if let Some(chunk) = NEIGHBORS .into_iter() .map(|neighbor| chunk + neighbor) - .filter(|neighbor| ctx.world.sim().get(*neighbor).map_or(false, |c| c.river.river_kind.is_some())) + .filter(|neighbor| { + ctx.world + .sim() + .get(*neighbor) + .map_or(false, |c| c.river.river_kind.is_some()) + }) .choose(&mut thread_rng()) { let wpos = TerrainChunkSize::center_wpos(chunk); - let wpos = wpos.as_().with_z(ctx.world.sim().get_interpolated(wpos, |chunk| chunk.water_alt).unwrap_or(0.0)); + let wpos = wpos.as_().with_z( + ctx.world + .sim() + .get_interpolated(wpos, |chunk| chunk.water_alt) + .unwrap_or(0.0), + ); goto(wpos, 0.7, 5.0).boxed() } else { idle().boxed() } }) - .repeat().map(|_| ()) + .repeat() + .map(|_| ()) } -fn think() -> impl Action { +fn humanoid() -> impl Action { choose(|ctx| { if let Some(riding) = &ctx.npc.riding { if riding.steering { if let Some(vehicle) = ctx.state.data().npcs.vehicles.get(riding.vehicle) { - match vehicle.kind { - VehicleKind::Airship => important(pilot()), - VehicleKind::Boat => important(captain()), + match vehicle.body { + common::comp::ship::Body::DefaultAirship + | common::comp::ship::Body::AirBalloon => important(pilot()), + common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { + important(captain()) + }, + _ => casual(idle()), } } else { casual(finish()) @@ -748,6 +784,66 @@ fn think() -> impl Action { }) } +fn bird_large() -> impl Action { + choose(|ctx| { + let data = ctx.state.data(); + if let Some(home) = ctx.npc.home { + let is_home = ctx.npc.current_site.map_or(false, |site| home == site); + if is_home { + if let Some((id, site)) = data + .sites + .iter() + .filter(|(id, site)| { + *id != home + && site.world_site.map_or(false, |site| { + matches!(ctx.index.sites.get(site).kind, SiteKind::Dungeon(_)) + }) + }) + .choose(&mut thread_rng()) + { + casual(goto( + site.wpos.as_::().with_z( + ctx.world + .sim() + .get_surface_alt_approx(site.wpos) + .unwrap_or(0.0) + + ctx.npc.body.flying_height(), + ), + 1.0, + 20.0, + )) + } else { + casual(idle()) + } + } else if let Some(site) = data.sites.get(home) { + casual(goto( + site.wpos.as_::().with_z( + ctx.world + .sim() + .get_surface_alt_approx(site.wpos) + .unwrap_or(0.0) + + ctx.npc.body.flying_height(), + ), + 1.0, + 20.0, + )) + } else { + casual(idle()) + } + } else { + casual(idle()) + } + }) +} + +fn think() -> impl Action { + choose(|ctx| match ctx.npc.body { + common::comp::Body::Humanoid(_) => casual(humanoid()), + common::comp::Body::BirdLarge(_) => casual(bird_large()), + _ => casual(idle()), + }) +} + // if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { // let data = ctx.state.data(); // if let Some((site2, site)) = npc diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index b65e5f5f1c..15674d6324 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -3,7 +3,7 @@ use crate::{ event::{OnSetup, OnTick}, RtState, Rule, RuleError, }; -use common::{terrain::TerrainChunkSize, vol::RectVolSize, grid::Grid}; +use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize}; use tracing::info; use vek::*; @@ -77,8 +77,6 @@ impl Rule for SimulateNpcs { // Simulate the NPC's movement and interactions if matches!(npc.mode, SimulationMode::Simulated) { - let body = npc.get_body(); - if let Some(riding) = &npc.riding { if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { if let Some(action) = npc.action && riding.steering { @@ -94,28 +92,30 @@ impl Rule for SimulateNpcs { .min(1.0)) .with_z(0.0); - let is_valid = match vehicle.kind { - crate::data::npc::VehicleKind::Airship => true, - crate::data::npc::VehicleKind::Boat => { + let is_valid = match vehicle.body { + common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => true, + common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { let chunk_pos = wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some()) }, + _ => false, }; if is_valid { - match vehicle.kind { - crate::data::npc::VehicleKind::Airship => { + match vehicle.body { + common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => { if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) { wpos.z = alt; } }, - crate::data::npc::VehicleKind::Boat => { + common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { wpos.z = ctx .world .sim() .get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt) .unwrap_or(0.0); }, + _ => {}, } vehicle.wpos = wpos; } @@ -138,7 +138,7 @@ impl Rule for SimulateNpcs { if dist2 > 0.5f32.powi(2) { npc.wpos += (diff - * (body.max_speed_approx() * speed_factor * ctx.event.dt + * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt / dist2.sqrt()) .min(1.0)) .with_z(0.0); @@ -150,8 +150,8 @@ impl Rule for SimulateNpcs { npc.wpos.z = ctx .world .sim() - .get_alt_approx(npc.wpos.xy().map(|e| e as i32)) - .unwrap_or(0.0); + .get_surface_alt_approx(npc.wpos.xy().map(|e| e as i32)) + .unwrap_or(0.0) + npc.body.flying_height(); } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 8ad10cdfc3..0c310ce1b9 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1486,7 +1486,8 @@ fn handle_spawn_airship( animated: true, }); if let Some(pos) = destination { - let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); + let (kp, ki, kd) = + comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) .with_destination(pos) @@ -1534,7 +1535,8 @@ fn handle_spawn_ship( animated: true, }); if let Some(pos) = destination { - let (kp, ki, kd) = comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); + let (kp, ki, kd) = + comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) .with_destination(pos) @@ -1575,11 +1577,9 @@ fn handle_make_volume( }; server .state - .create_ship( - comp::Pos(pos.0 + Vec3::unit_z() * 50.0), - ship, - move |_| collider, - ) + .create_ship(comp::Pos(pos.0 + Vec3::unit_z() * 50.0), ship, move |_| { + collider + }) .build(); server.notify_client( diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 5601d755b9..f399b3d3ec 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -14,14 +14,15 @@ use common::{ LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, TradingBehavior, Vel, WaypointArea, }, - event::{EventBus, UpdateCharacterMetadata, NpcBuilder}, + event::{EventBus, NpcBuilder, UpdateCharacterMetadata}, lottery::LootSpec, + mounting::Mounting, outcome::Outcome, resources::{Secs, Time}, rtsim::{RtSimEntity, RtSimVehicle}, uid::Uid, util::Dir, - ViewDistances, mounting::Mounting, + ViewDistances, }; use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use specs::{Builder, Entity as EcsEntity, WorldExt}; @@ -91,14 +92,18 @@ pub fn handle_loaded_character_data( server.notify_client(entity, ServerGeneral::CharacterDataLoadResult(Ok(metadata))); } -pub fn handle_create_npc( - server: &mut Server, - pos: Pos, - mut npc: NpcBuilder, -) -> EcsEntity { +pub fn handle_create_npc(server: &mut Server, pos: Pos, mut npc: NpcBuilder) -> EcsEntity { let entity = server .state - .create_npc(pos, npc.stats, npc.skill_set, npc.health, npc.poise, npc.inventory, npc.body) + .create_npc( + pos, + npc.stats, + npc.skill_set, + npc.health, + npc.poise, + npc.inventory, + npc.body, + ) .with(npc.scale); if let Some(agent) = &mut npc.agent { @@ -194,7 +199,6 @@ pub fn handle_create_ship( rtsim_vehicle: Option, driver: Option, passangers: Vec, - ) { let mut entity = server .state @@ -213,19 +217,21 @@ pub fn handle_create_ship( } let entity = entity.build(); - if let Some(driver) = driver { let npc_entity = handle_create_npc(server, pos, driver); let uids = server.state.ecs().read_storage::(); if let (Some(rider_uid), Some(mount_uid)) = - (uids.get(npc_entity).copied(), uids.get(entity).copied()) + (uids.get(npc_entity).copied(), uids.get(entity).copied()) { drop(uids); - server.state.link(Mounting { - mount: mount_uid, - rider: rider_uid, - }).expect("Failed to link driver to ship"); + server + .state + .link(Mounting { + mount: mount_uid, + rider: rider_uid, + }) + .expect("Failed to link driver to ship"); } else { panic!("Couldn't get Uid from newly created ship and npc"); } diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index c1edd8a6af..8a56355aad 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -9,8 +9,9 @@ use common::{ dialogue::Subject, inventory::slot::EquipSlot, loot_owner::LootOwnerKind, + pet::is_mountable, tool::ToolKind, - Inventory, LootOwner, Pos, SkillGroupKind, pet::is_mountable, + Inventory, LootOwner, Pos, SkillGroupKind, }, consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME}, event::EventBus, @@ -119,10 +120,13 @@ pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) { Some(comp::Alignment::Owned(owner)) if *owner == rider_uid, ); - let can_ride = state.ecs() + let can_ride = state + .ecs() .read_storage() .get(mount) - .map_or(false, |mount_body| is_mountable(mount_body, state.ecs().read_storage().get(rider))); + .map_or(false, |mount_body| { + is_mountable(mount_body, state.ecs().read_storage().get(rider)) + }); if is_pet && can_ride { drop(uids); diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 8637890b87..a3b23044d4 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -188,15 +188,8 @@ impl Server { ServerEvent::ExitIngame { entity } => { handle_exit_ingame(self, entity, false); }, - ServerEvent::CreateNpc { - pos, - npc, - } => { - handle_create_npc( - self, - pos, - npc, - ); + ServerEvent::CreateNpc { pos, npc } => { + handle_create_npc(self, pos, npc); }, ServerEvent::CreateShip { pos, diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 1bbb35e863..3073939e6f 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -4,7 +4,7 @@ pub mod tick; use common::{ grid::Grid, - rtsim::{ChunkResource, RtSimEntity, WorldSettings, RtSimVehicle}, + rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, slowjob::SlowJobPool, terrain::{Block, TerrainChunk}, vol::RectRasterableVol, diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim2/rule/deplete_resources.rs index 1bc0667b40..ebea23e721 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim2/rule/deplete_resources.rs @@ -1,5 +1,8 @@ use crate::rtsim2::{event::OnBlockChange, ChunkStates}; -use common::{terrain::{TerrainChunk, CoordinateConversions}, vol::RectRasterableVol}; +use common::{ + terrain::{CoordinateConversions, TerrainChunk}, + vol::RectRasterableVol, +}; use rtsim2::{RtState, Rule, RuleError}; pub struct DepleteResources; @@ -7,10 +10,7 @@ pub struct DepleteResources; impl Rule for DepleteResources { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { - let key = ctx - .event - .wpos - .xy().wpos_to_cpos(); + let key = ctx.event.wpos.xy().wpos_to_cpos(); if let Some(Some(chunk_state)) = ctx.state.resource_mut::().0.get(key) { let mut chunk_res = ctx.state.data().nature.get_chunk_resources(key); // Remove resources diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index fb53d80eb5..2c2e498332 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -3,19 +3,20 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, inventory::loadout::Loadout, skillset::skills, Body, Agent}, - event::{EventBus, ServerEvent, NpcBuilder}, + comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, + event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, + terrain::CoordinateConversions, trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, terrain::CoordinateConversions, + LoadoutBuilder, SkillSetBuilder, lottery::LootSpec, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::{ - npc::{SimulationMode, Profession}, - Npc, Sites, Actor, + npc::{Profession, SimulationMode}, + Actor, Npc, Sites, }; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; @@ -126,9 +127,9 @@ fn profession_agent_mark(profession: Option<&Profession>) -> Option EntityInfo { - let body = npc.get_body(); let pos = comp::Pos(npc.wpos); + let mut rng = npc.rng(3); if let Some(ref profession) = npc.profession { let economy = npc.home.and_then(|home| { let site = sites.get(home)?.world_site?; @@ -137,9 +138,8 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo let config_asset = humanoid_config(profession); - let entity_config = - EntityConfig::from_asset_expect_owned(config_asset).with_body(BodyBuilder::Exact(body)); - let mut rng = npc.rng(3); + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); EntityInfo::at(pos.0) .with_entity_config(entity_config, Some(config_asset), &mut rng) .with_alignment(if matches!(profession, Profession::Cultist) { @@ -151,10 +151,23 @@ fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) } else { + let config_asset = match npc.body { + Body::BirdLarge(body) => match body.species { + comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", + comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", + comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", + // Wildcard match used here as there is an array above + // which limits what species are used + _ => unimplemented!(), + }, + _ => unimplemented!(), + }; + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); + EntityInfo::at(pos.0) - .with_body(body) + .with_entity_config(entity_config, Some(config_asset), &mut rng) .with_alignment(comp::Alignment::Wild) - .with_name("Rtsim NPC") } } @@ -228,7 +241,8 @@ impl<'a> System<'a> for Sys { let npc = data.npcs.npcs.get_mut(npc_id)?; if matches!(npc.mode, SimulationMode::Simulated) { npc.mode = SimulationMode::Loaded; - let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); + let entity_info = + get_npc_entity_info(npc, &data.sites, index.as_index_ref()); Some(match NpcData::from_entity_info(entity_info) { NpcData::Data { @@ -244,14 +258,14 @@ impl<'a> System<'a> for Sys { scale, loot, } => NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), // EntityConfig can't represent Waypoints at all // as of now, and if someone will try to spawn // rtsim waypoint it is definitely error. @@ -265,11 +279,17 @@ impl<'a> System<'a> for Sys { emitter.emit(ServerEvent::CreateShip { pos: comp::Pos(vehicle.wpos), - ship: vehicle.get_ship(), + ship: vehicle.body, // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), rtsim_entity: Some(RtSimVehicle(vehicle_id)), driver: vehicle.driver.and_then(&mut actor_info), - passangers: vehicle.riders.iter().copied().filter(|actor| vehicle.driver != Some(*actor)).filter_map(actor_info).collect(), + passangers: vehicle + .riders + .iter() + .copied() + .filter(|actor| vehicle.driver != Some(*actor)) + .filter_map(actor_info) + .collect(), }); } } @@ -301,14 +321,14 @@ impl<'a> System<'a> for Sys { } => ServerEvent::CreateNpc { pos, npc: NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), }, // EntityConfig can't represent Waypoints at all // as of now, and if someone will try to spawn @@ -319,10 +339,9 @@ impl<'a> System<'a> for Sys { } // Synchronise rtsim NPC with entity data - for (pos, rtsim_vehicle) in - (&positions, &rtsim_vehicles).join() - { - data.npcs.vehicles + for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() { + data.npcs + .vehicles .get_mut(rtsim_vehicle.0) .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) .map(|vehicle| { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index dde39a5319..9fbd288978 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -19,7 +19,7 @@ use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::thread_rng; use rayon::iter::ParallelIterator; -use specs::{Join, ParJoin, Read, WriteExpect, WriteStorage, saveload::MarkerAllocator}; +use specs::{saveload::MarkerAllocator, Join, ParJoin, Read, WriteExpect, WriteStorage}; /// This system will allow NPCs to modify their controller #[derive(Default)] @@ -101,7 +101,13 @@ impl<'a> System<'a> for Sys { let mut rng = thread_rng(); // The entity that is moving, if riding it's the mount, otherwise it's itself - let moving_entity = is_rider.and_then(|is_rider| read_data.uid_allocator.retrieve_entity_internal(is_rider.mount.into())).unwrap_or(entity); + let moving_entity = is_rider + .and_then(|is_rider| { + read_data + .uid_allocator + .retrieve_entity_internal(is_rider.mount.into()) + }) + .unwrap_or(entity); let moving_body = read_data.bodies.get(moving_entity); @@ -147,8 +153,13 @@ impl<'a> System<'a> for Sys { Some(CharacterState::GlideWield(_) | CharacterState::Glide(_)) ) && physics_state.on_ground.is_none(); - if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients) { - if agent.position_pid_controller.as_ref().map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd)) { + if let Some((kp, ki, kd)) = moving_body.and_then(comp::agent::pid_coefficients) + { + if agent + .position_pid_controller + .as_ref() + .map_or(false, |pid| (pid.kp, pid.ki, pid.kd) != (kp, ki, kd)) + { agent.position_pid_controller = None; } let pid = agent.position_pid_controller.get_or_insert_with(|| { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index c864352fcd..c9c5e8be78 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -19,7 +19,7 @@ use common::{ comp::{ self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, }, - event::{EventBus, ServerEvent, NpcBuilder}, + event::{EventBus, NpcBuilder, ServerEvent}, generation::EntityInfo, lottery::LootSpec, resources::{Time, TimeOfDay}, @@ -225,7 +225,7 @@ impl<'a> System<'a> for Sys { .with_agent(agent) .with_scale(scale) .with_anchor(comp::Anchor::Chunk(key)) - .with_loot(loot) + .with_loot(loot), }); }, } diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index fa499f46ae..cc8173d168 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -1914,6 +1914,13 @@ impl WorldSim { } } + /// Get the altitude of the surface, could be water or ground. + pub fn get_surface_alt_approx(&self, wpos: Vec2) -> Option { + self.get_interpolated(wpos, |chunk| chunk.alt) + .zip(self.get_interpolated(wpos, |chunk| chunk.water_alt)) + .map(|(alt, water_alt)| alt.max(water_alt)) + } + pub fn get_alt_approx(&self, wpos: Vec2) -> Option { self.get_interpolated(wpos, |chunk| chunk.alt) } From adb2e1ba854d627b98007b69a9ec718e270de544 Mon Sep 17 00:00:00 2001 From: Isse Date: Thu, 9 Mar 2023 16:07:06 +0100 Subject: [PATCH 062/139] very simple repopulation --- assets/common/entity/village/captain.ron | 4 +- rtsim/src/data/mod.rs | 9 ++ rtsim/src/data/site.rs | 11 ++- rtsim/src/gen/mod.rs | 8 +- rtsim/src/gen/site.rs | 2 + rtsim/src/rule/npc_ai.rs | 14 +-- rtsim/src/rule/simulate_npcs.rs | 106 +++++++++++++++++++++-- server/src/cmd.rs | 21 ++++- server/src/rtsim2/tick.rs | 3 +- 9 files changed, 157 insertions(+), 21 deletions(-) diff --git a/assets/common/entity/village/captain.ron b/assets/common/entity/village/captain.ron index 0043093d54..f6b247e641 100644 --- a/assets/common/entity/village/captain.ron +++ b/assets/common/entity/village/captain.ron @@ -15,5 +15,7 @@ (10, "common.items.consumable.potion_med"), ], ), - meta: [], + meta: [ + SkillSetAsset("common.skillset.preset.rank5.fullskill"), + ], ) diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index d3bb51a484..1e7cbe093e 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -52,6 +52,15 @@ pub type ReadError = rmp_serde::decode::Error; pub type WriteError = rmp_serde::encode::Error; impl Data { + pub fn spawn_npc(&mut self, npc: Npc) -> NpcId { + let home = npc.home; + let id = self.npcs.create_npc(npc); + if let Some(home) = home.and_then(|home| self.sites.get_mut(home)) { + home.population.insert(id); + } + id + } + pub fn from_reader(reader: R) -> Result { rmp_serde::decode::from_read(reader) } diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index f1edf2645e..40b6d2b89e 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -1,6 +1,10 @@ pub use common::rtsim::SiteId; -use common::{rtsim::FactionId, store::Id, uid::Uid}; -use hashbrown::HashMap; +use common::{ + rtsim::{FactionId, NpcId}, + store::Id, + uid::Uid, +}; +use hashbrown::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::ops::{Deref, DerefMut}; @@ -27,6 +31,9 @@ pub struct Site { /// being too). #[serde(skip_serializing, skip_deserializing)] pub world_site: Option>, + + #[serde(skip_serializing, skip_deserializing)] + pub population: HashSet, } impl Site { diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 37de8f34d4..7bcb159f8b 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -76,7 +76,6 @@ impl Data { "Registering {} rtsim sites from world sites.", this.sites.len() ); - /* // Spawn some test entities at the sites for (site_id, site) in this.sites.iter() // TODO: Stupid @@ -97,10 +96,7 @@ impl Data { }; let random_humanoid = |rng: &mut SmallRng| { let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); - Body::Humanoid(comp::humanoid::Body::random_with( - rng, - species, - )) + Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) }; if good_or_evil { for _ in 0..32 { @@ -152,7 +148,7 @@ impl Data { ); } } - */ + for (site_id, site) in this.sites.iter() // TODO: Stupid .filter(|(_, site)| site.world_site.map_or(false, |ws| diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index 8325146ac4..03559b0e63 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -1,5 +1,6 @@ use crate::data::{FactionId, Factions, Site}; use common::store::Id; +use hashbrown::HashSet; use vek::*; use world::{ site::{Site as WorldSite, SiteKind}, @@ -48,6 +49,7 @@ impl Site { .min_by_key(|(faction_wpos, _)| faction_wpos.distance_squared(wpos)) .map(|(_, faction)| *faction) }), + population: HashSet::new(), } } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 99db92dd5e..936adc2cb4 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -329,12 +329,16 @@ fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { } // Get the next waypoint on the route toward the goal - let waypoint = - waypoint.get_or_insert_with(|| { - let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST); + let waypoint = waypoint.get_or_insert_with(|| { + let wpos = ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST); - wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()).unwrap_or(wpos.z)) - }); + wpos.with_z( + ctx.world + .sim() + .get_surface_alt_approx(wpos.xy().as_()) + .unwrap_or(wpos.z), + ) + }); *ctx.controller = Controller::goto(*waypoint, speed_factor); }) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 15674d6324..0a95edb1eb 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -1,11 +1,17 @@ use crate::{ - data::npc::SimulationMode, - event::{OnSetup, OnTick}, + data::{npc::SimulationMode, Npc}, + event::{OnDeath, OnSetup, OnTick}, RtState, Rule, RuleError, }; -use common::{grid::Grid, terrain::TerrainChunkSize, vol::RectVolSize}; -use tracing::info; -use vek::*; +use common::{ + comp::{self, Body}, + grid::Grid, + terrain::TerrainChunkSize, + vol::RectVolSize, +}; +use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; +use tracing::warn; +use world::site::SiteKind; pub struct SimulateNpcs; @@ -27,8 +33,98 @@ impl Rule for SimulateNpcs { } } } + + if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { + home.population.insert(npc_id); + } } }); + + rtstate.bind::(|ctx| { + let data = &mut *ctx.state.data_mut(); + let npc_id = ctx.event.npc_id; + let Some(npc) = data.npcs.get(npc_id) else { + return; + }; + if let Some(home) = npc.home.and_then(|home| data.sites.get_mut(home)) { + home.population.remove(&npc_id); + } + let mut rng = rand::thread_rng(); + match npc.body { + Body::Humanoid(_) => { + if let Some((site_id, site)) = data + .sites + .iter() + .filter(|(id, site)| { + Some(*id) != npc.home + && site.faction == npc.faction + && site.world_site.map_or(false, |s| { + matches!(ctx.index.sites.get(s).kind, SiteKind::Refactor(_)) + }) + }) + .min_by_key(|(_, site)| site.population.len()) + { + let rand_wpos = |rng: &mut ThreadRng| { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + wpos2d + .map(|e| e as f32 + 0.5) + .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + let random_humanoid = |rng: &mut ThreadRng| { + let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); + Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) + }; + data.spawn_npc( + Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) + .with_home(site_id) + .with_faction(npc.faction) + .with_profession(npc.profession.clone()), + ); + } else { + warn!("No site found for respawning humaniod"); + } + }, + Body::BirdLarge(_) => { + if let Some((site_id, site)) = data + .sites + .iter() + .filter(|(id, site)| { + Some(*id) != npc.home + && site.world_site.map_or(false, |s| { + matches!(ctx.index.sites.get(s).kind, SiteKind::Dungeon(_)) + }) + }) + .min_by_key(|(_, site)| site.population.len()) + { + let rand_wpos = |rng: &mut ThreadRng| { + let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + wpos2d + .map(|e| e as f32 + 0.5) + .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) + }; + let species = [ + comp::body::bird_large::Species::Phoenix, + comp::body::bird_large::Species::Cockatrice, + comp::body::bird_large::Species::Roc, + ] + .choose(&mut rng) + .unwrap(); + data.npcs.create_npc( + Npc::new( + rng.gen(), + rand_wpos(&mut rng), + Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)), + ) + .with_home(site_id), + ); + } else { + warn!("No site found for respawning bird"); + } + }, + _ => unimplemented!(), + } + }); + rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 0c310ce1b9..047318a164 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1997,7 +1997,26 @@ fn handle_kill_npcs( true }; - should_kill.then_some(entity) + if should_kill { + if let Some(rtsim_entity) = ecs + .read_storage::() + .get(entity) + .copied() + { + ecs + .write_resource::() + .hook_rtsim_entity_delete( + &ecs.read_resource::>(), + ecs + .read_resource::() + .as_index_ref(), + rtsim_entity, + ); + } + Some(entity) + } else { + None + } }) .collect::>() }; diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 2c2e498332..47dd8b60d5 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -6,12 +6,13 @@ use common::{ comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, + lottery::LootSpec, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, terrain::CoordinateConversions, trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, lottery::LootSpec, + LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim2::data::{ From 7ac6c6b453af337a20e2af7016e51668a74d628c Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Mar 2023 10:59:34 +0100 Subject: [PATCH 063/139] fix warnings in rtsim --- common/src/cmd.rs | 11 ++++++--- common/src/event.rs | 9 ++++++++ common/src/generation.rs | 2 +- common/src/rtsim.rs | 2 +- common/src/states/basic_summon.rs | 16 ++++++------- common/src/states/behavior.rs | 4 ++-- common/src/states/climb.rs | 11 ++++++--- common/src/states/utils.rs | 33 ++++++++++++++++++--------- common/src/terrain/block.rs | 7 +++--- rtsim/src/ai/mod.rs | 20 ++++++++-------- rtsim/src/data/faction.rs | 2 -- rtsim/src/data/mod.rs | 9 +++----- rtsim/src/data/nature.rs | 2 +- rtsim/src/data/npc.rs | 13 +++-------- rtsim/src/data/site.rs | 1 - rtsim/src/gen/faction.rs | 3 +-- rtsim/src/gen/mod.rs | 3 +-- rtsim/src/gen/site.rs | 2 +- rtsim/src/lib.rs | 3 +-- rtsim/src/rule/npc_ai.rs | 24 +++++++------------ rtsim/src/rule/replenish_resources.rs | 3 --- rtsim/src/rule/simulate_npcs.rs | 4 +++- server/src/cmd.rs | 7 ++---- server/src/sys/agent.rs | 3 +-- 24 files changed, 98 insertions(+), 96 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 06621bc5d7..fb2b8068dc 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -747,9 +747,14 @@ impl ServerChatCommand { ServerChatCommand::Lightning => { cmd(vec![], "Lightning strike at current position", Some(Admin)) }, - ServerChatCommand::Scale => { - cmd(vec![Float("factor", 1.0, Required), Boolean("reset_mass", true.to_string(), Optional)], "Scale your character", Some(Admin)) - }, + ServerChatCommand::Scale => cmd( + vec![ + Float("factor", 1.0, Required), + Boolean("reset_mass", true.to_string(), Optional), + ], + "Scale your character", + Some(Admin), + ), } } diff --git a/common/src/event.rs b/common/src/event.rs index 7914d6a21e..629746b934 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -81,38 +81,47 @@ impl NpcBuilder { self.health = health.into(); self } + pub fn with_poise(mut self, poise: comp::Poise) -> Self { self.poise = poise; self } + pub fn with_agent(mut self, agent: impl Into>) -> Self { self.agent = agent.into(); self } + pub fn with_anchor(mut self, anchor: comp::Anchor) -> Self { self.anchor = Some(anchor); self } + pub fn with_rtsim(mut self, rtsim: RtSimEntity) -> Self { self.rtsim_entity = Some(rtsim); self } + pub fn with_projectile(mut self, projectile: impl Into>) -> Self { self.projectile = projectile.into(); self } + pub fn with_scale(mut self, scale: comp::Scale) -> Self { self.scale = scale; self } + pub fn with_inventory(mut self, inventory: comp::Inventory) -> Self { self.inventory = inventory; self } + pub fn with_skill_set(mut self, skill_set: comp::SkillSet) -> Self { self.skill_set = skill_set; self } + pub fn with_loot(mut self, loot: LootSpec) -> Self { self.loot = loot; self diff --git a/common/src/generation.rs b/common/src/generation.rs index 9db9769c91..00ac1f7b29 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -7,8 +7,8 @@ use crate::{ }, lottery::LootSpec, npc::{self, NPC_NAMES}, - trade::SiteInformation, rtsim, + trade::SiteInformation, }; use enum_map::EnumMap; use serde::Deserialize; diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 5874fd8999..0d32bc48bb 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,8 +3,8 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. +use serde::{Deserialize, Serialize}; use specs::Component; -use serde::{Serialize, Deserialize}; use vek::*; use crate::comp::dialogue::MoodState; diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 2f56ab02de..316985b89f 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -6,7 +6,7 @@ use crate::{ skillset::skills, Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, }, - event::{LocalEvent, ServerEvent, NpcBuilder}, + event::{LocalEvent, NpcBuilder, ServerEvent}, outcome::Outcome, skillset_builder::{self, SkillSetBuilder}, states::{ @@ -181,15 +181,15 @@ impl CharacterBehavior for Data { .with_agent( comp::Agent::from_body(&body) .with_behavior(Behavior::from(BehaviorCapability::SPEAK)) - .with_no_flee_if(true) + .with_no_flee_if(true), ) .with_scale( - self - .static_data - .summon_info - .scale - .unwrap_or(comp::Scale(1.0)) - ).with_projectile(projectile) + self.static_data + .summon_info + .scale + .unwrap_or(comp::Scale(1.0)), + ) + .with_projectile(projectile), }); // Send local event used for frontend shenanigans diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index a117ad19a0..d97e1bf92d 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -5,8 +5,8 @@ use crate::{ item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, - InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, SkillSet, Stance, StateUpdate, Stats, - Vel, Scale, + InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate, Stats, + Vel, }, link::Is, mounting::Rider, diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index fa415ce616..6ca51d0337 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -76,7 +76,8 @@ impl CharacterBehavior for Data { // They've climbed atop something, give them a boost output_events.emit_local(LocalEvent::Jump( data.entity, - CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0 * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)), + CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0 + * data.scale.map_or(1.0, |s| s.0.powf(13.0).powf(0.25)), )); }; update.character = CharacterState::Idle(idle::Data::default()); @@ -122,10 +123,14 @@ impl CharacterBehavior for Data { // Apply Vertical Climbing Movement match climb { Climb::Down => { - update.vel.0.z += data.dt.0 * (GRAVITY - self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) + update.vel.0.z += data.dt.0 + * (GRAVITY + - self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) }, Climb::Up => { - update.vel.0.z += data.dt.0 * (GRAVITY + self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) + update.vel.0.z += data.dt.0 + * (GRAVITY + + self.static_data.movement_speed.powi(2) * data.scale.map_or(1.0, |s| s.0)) }, Climb::Hold => update.vel.0.z += data.dt.0 * GRAVITY, } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index df50cd136b..085832b890 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -388,7 +388,8 @@ fn basic_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) { data.body.base_accel() * data.scale.map_or(1.0, |s| s.0.sqrt()) * block.get_traction() - * block.get_friction() / FRIC_GROUND + * block.get_friction() + / FRIC_GROUND } else { data.body.air_accel() } * efficiency; @@ -437,8 +438,11 @@ pub fn handle_forced_movement( // FRIC_GROUND temporarily used to normalize things around expected values data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { - update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * Vec2::from(*data.ori) * strength; + update.vel.0 += Vec2::broadcast(data.dt.0) + * accel + * data.scale.map_or(1.0, |s| s.0.sqrt()) + * Vec2::from(*data.ori) + * strength; } }, ForcedMovement::Reverse(strength) => { @@ -447,8 +451,11 @@ pub fn handle_forced_movement( // FRIC_GROUND temporarily used to normalize things around expected values data.body.base_accel() * block.get_traction() * block.get_friction() / FRIC_GROUND }) { - update.vel.0 += - Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * -Vec2::from(*data.ori) * strength; + update.vel.0 += Vec2::broadcast(data.dt.0) + * accel + * data.scale.map_or(1.0, |s| s.0.sqrt()) + * -Vec2::from(*data.ori) + * strength; } }, ForcedMovement::Sideways(strength) => { @@ -470,7 +477,11 @@ pub fn handle_forced_movement( } }; - update.vel.0 += Vec2::broadcast(data.dt.0) * accel * data.scale.map_or(1.0, |s| s.0.sqrt()) * direction * strength; + update.vel.0 += Vec2::broadcast(data.dt.0) + * accel + * data.scale.map_or(1.0, |s| s.0.sqrt()) + * direction + * strength; } }, ForcedMovement::DirectedReverse(strength) => { @@ -532,9 +543,10 @@ pub fn handle_forced_movement( * (1.0 - data.inputs.look_dir.z.abs()); }, ForcedMovement::Hover { move_input } => { - update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0) + move_input - * data.scale.map_or(1.0, |s| s.0.sqrt()) - * data.inputs.move_dir.try_normalized().unwrap_or_default(); + update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0) + + move_input + * data.scale.map_or(1.0, |s| s.0.sqrt()) + * data.inputs.move_dir.try_normalized().unwrap_or_default(); }, } } @@ -574,8 +586,7 @@ pub fn handle_orientation( .map_or_else(|| to_horizontal_fast(data.ori), |dir| dir.into()) }; // unit is multiples of 180° - let half_turns_per_tick = data.body.base_ori_rate() - / data.scale.map_or(1.0, |s| s.0.sqrt()) + let half_turns_per_tick = data.body.base_ori_rate() / data.scale.map_or(1.0, |s| s.0.sqrt()) * efficiency * if data.physics.on_ground.is_some() { 1.0 diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index ba8a8379d1..9ca94ee0e6 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -3,8 +3,7 @@ use crate::{ comp::{fluid_dynamics::LiquidKind, tool::ToolKind}, consts::FRIC_GROUND, lottery::LootSpec, - make_case_elim, - rtsim, + make_case_elim, rtsim, }; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -196,7 +195,9 @@ impl Block { } } - /// Returns the rtsim resource, if any, that this block corresponds to. If you want the scarcity of a block to change with rtsim's resource depletion tracking, you can do so by editing this function. + /// Returns the rtsim resource, if any, that this block corresponds to. If + /// you want the scarcity of a block to change with rtsim's resource + /// depletion tracking, you can do so by editing this function. #[inline] pub fn get_rtsim_resource(&self) -> Option { match self.get_sprite()? { diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 2c7e072583..1232935a62 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -240,7 +240,7 @@ impl A + Send + Sync + 'stati Action for Now { // 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) } @@ -293,7 +293,7 @@ impl< > Action<()> for Until { // 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) } @@ -345,11 +345,11 @@ pub struct Just(F, PhantomData); impl R + Send + Sync + 'static> Action for Just { - 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 backtrace(&self, bt: &mut Vec) {} + fn backtrace(&self, _bt: &mut Vec) {} // TODO: Reset closure? fn reset(&mut self) {} @@ -369,7 +369,7 @@ impl R + Send + Sync + 'stati /// // Make the current NPC say 'Hello, world!' exactly once /// just(|ctx| ctx.controller.say("Hello, world!")) /// ``` -pub fn just(mut f: F) -> Just +pub fn just(f: F) -> Just where F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, { @@ -383,15 +383,15 @@ where pub struct Finish; impl Action<()> 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 backtrace(&self, bt: &mut Vec) {} + fn backtrace(&self, _bt: &mut Vec) {} fn reset(&mut self) {} - fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } + fn tick(&mut self, _ctx: &mut NpcCtx) -> ControlFlow<()> { ControlFlow::Break(()) } } /// An action that immediately finishes without doing anything. @@ -443,7 +443,7 @@ pub struct Tree { impl Node + Send + Sync + 'static, R: 'static> Action for Tree { - 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) } @@ -626,7 +626,7 @@ pub struct Sequence(I, I, Option, PhantomData); impl + Clone + Send + Sync + 'static, A: Action> Action<()> for Sequence { - 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) } diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs index 921d0cad58..65322e5097 100644 --- a/rtsim/src/data/faction.rs +++ b/rtsim/src/data/faction.rs @@ -1,7 +1,5 @@ use super::Actor; pub use common::rtsim::FactionId; -use common::{store::Id, uid::Uid}; -use hashbrown::HashMap; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::ops::{Deref, DerefMut}; diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 1e7cbe093e..3f2bcae678 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -12,10 +12,7 @@ pub use self::{ use common::resources::TimeOfDay; use enum_map::{enum_map, EnumArray, EnumMap}; -use serde::{ - de::{self, Error as _}, - ser, Deserialize, Serialize, -}; +use serde::{de, ser, Deserialize, Serialize}; use std::{ cmp::PartialEq, fmt, @@ -81,7 +78,7 @@ fn rugged_ser_enum_map< ) -> Result { ser.collect_map( map.iter() - .filter(|(k, v)| v != &&V::from(DEFAULT)) + .filter(|(_, v)| v != &&V::from(DEFAULT)) .map(|(k, v)| (k, v)), ) } @@ -93,7 +90,7 @@ fn rugged_de_enum_map< D: de::Deserializer<'a>, const DEFAULT: i16, >( - mut de: D, + de: D, ) -> Result, D::Error> { struct Visitor(PhantomData<(K, V)>); diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index 2325ad6dbc..c94ca5fae9 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -18,7 +18,7 @@ pub struct Nature { impl Nature { pub fn generate(world: &World) -> Self { Self { - chunks: Grid::populate_from(world.sim().get_size().map(|e| e as i32), |pos| Chunk { + chunks: Grid::populate_from(world.sim().get_size().map(|e| e as i32), |_| Chunk { res: EnumMap::<_, f32>::default().map(|_, _| 1.0), }), } diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 21ee9b4903..bd676b7dfe 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,25 +1,18 @@ -use crate::ai::{Action, NpcCtx}; +use crate::ai::Action; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, grid::Grid, - rtsim::{FactionId, RtSimController, SiteId, VehicleId}, + rtsim::{FactionId, SiteId, VehicleId}, store::Id, - uid::Uid, vol::RectVolSize, }; -use hashbrown::HashMap; use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ collections::VecDeque, - ops::{Deref, DerefMut, Generator, GeneratorState}, - pin::Pin, - sync::{ - atomic::{AtomicPtr, Ordering}, - Arc, - }, + ops::{Deref, DerefMut}, }; use vek::*; use world::{ diff --git a/rtsim/src/data/site.rs b/rtsim/src/data/site.rs index 40b6d2b89e..57fe387ca6 100644 --- a/rtsim/src/data/site.rs +++ b/rtsim/src/data/site.rs @@ -2,7 +2,6 @@ pub use common::rtsim::SiteId; use common::{ rtsim::{FactionId, NpcId}, store::Id, - uid::Uid, }; use hashbrown::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; diff --git a/rtsim/src/gen/faction.rs b/rtsim/src/gen/faction.rs index b7a4c073dc..9c9a80077e 100644 --- a/rtsim/src/gen/faction.rs +++ b/rtsim/src/gen/faction.rs @@ -1,10 +1,9 @@ use crate::data::Faction; use rand::prelude::*; -use vek::*; use world::{IndexRef, World}; impl Faction { - pub fn generate(world: &World, index: IndexRef, rng: &mut impl Rng) -> Self { + pub fn generate(_world: &World, _index: IndexRef, rng: &mut impl Rng) -> Self { Self { leader: None, good_or_evil: rng.gen(), diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 7bcb159f8b..a777aa07a3 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -3,7 +3,7 @@ pub mod site; use crate::data::{ faction::{Faction, Factions}, - npc::{Npc, Npcs, Profession, Vehicle, VehicleKind}, + npc::{Npc, Npcs, Profession, Vehicle}, site::{Site, Sites}, Data, Nature, }; @@ -15,7 +15,6 @@ use common::{ terrain::TerrainChunkSize, vol::RectVolSize, }; -use hashbrown::HashMap; use rand::prelude::*; use tracing::info; use vek::*; diff --git a/rtsim/src/gen/site.rs b/rtsim/src/gen/site.rs index 03559b0e63..e6ef2b3d1d 100644 --- a/rtsim/src/gen/site.rs +++ b/rtsim/src/gen/site.rs @@ -10,7 +10,7 @@ use world::{ impl Site { pub fn generate( world_site_id: Id, - world: &World, + _world: &World, index: IndexRef, nearby_factions: &[(Vec2, FactionId)], factions: &Factions, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index 1ae1622b8d..f24a449f4a 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -1,5 +1,4 @@ #![feature( - generic_associated_types, never_type, try_blocks, generator_trait, @@ -91,7 +90,7 @@ impl RtState { pub fn bind( &mut self, - mut f: impl FnMut(EventCtx) + Send + Sync + 'static, + f: impl FnMut(EventCtx) + Send + Sync + 'static, ) { let f = AtomicRefCell::new(f); self.event_handlers diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 936adc2cb4..bb87d3b028 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,13 +1,13 @@ -use std::{collections::VecDeque, hash::BuildHasherDefault}; +use std::hash::BuildHasherDefault; use crate::{ - ai::{casual, choose, finish, important, just, now, seq, until, urgent, watch, Action, NpcCtx}, + ai::{casual, choose, finish, important, just, now, seq, until, urgent, Action, NpcCtx}, data::{ - npc::{Brain, Controller, Npc, NpcId, PathData, PathingMemory, VehicleKind}, + npc::{Brain, Controller, PathData}, Sites, }, event::OnTick, - EventCtx, RtState, Rule, RuleError, + RtState, Rule, RuleError, }; use common::{ astar::{Astar, PathResult}, @@ -22,11 +22,6 @@ use fxhash::FxHasher64; use itertools::Itertools; use rand::prelude::*; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; -use std::{ - any::{Any, TypeId}, - marker::PhantomData, - ops::ControlFlow, -}; use vek::*; use world::{ civ::{self, Track}, @@ -212,11 +207,9 @@ fn path_towns( } } -const MAX_STEP: f32 = 32.0; - impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { - rtstate.bind::(|mut ctx| { + rtstate.bind::(|ctx| { let mut npc_data = { let mut data = ctx.state.data_mut(); data.npcs @@ -351,8 +344,6 @@ fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { /// Try to walk toward a 2D position on the terrain without caring for /// obstacles. fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action { - const MIN_DIST: f32 = 2.0; - now(move |ctx| { let wpos = wpos2d.with_z(ctx.world.sim().get_alt_approx(wpos2d.as_()).unwrap_or(0.0)); goto(wpos, speed_factor, goal_dist) @@ -421,7 +412,6 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { if let Some(current_site) = ctx.npc.current_site && let Some(tracks) = path_towns(current_site, tgt_site, sites, ctx.world) { - let track_count = tracks.path.len(); let mut nodes = tracks.path .into_iter() @@ -625,6 +615,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(move || format!("villager at site {:?}", visiting_site)) } +/* fn follow(npc: NpcId, distance: f32) -> impl Action { const STEP_DIST: f32 = 1.0; now(move |ctx| { @@ -643,6 +634,7 @@ fn follow(npc: NpcId, distance: f32) -> impl Action { .debug(move || format!("Following npc({npc:?})")) .map(|_| {}) } +*/ fn chunk_path( from: Vec2, @@ -794,7 +786,7 @@ fn bird_large() -> impl Action { if let Some(home) = ctx.npc.home { let is_home = ctx.npc.current_site.map_or(false, |site| home == site); if is_home { - if let Some((id, site)) = data + if let Some((_, site)) = data .sites .iter() .filter(|(id, site)| { diff --git a/rtsim/src/rule/replenish_resources.rs b/rtsim/src/rule/replenish_resources.rs index e1838f8883..3aff5256e9 100644 --- a/rtsim/src/rule/replenish_resources.rs +++ b/rtsim/src/rule/replenish_resources.rs @@ -1,8 +1,5 @@ use crate::{event::OnTick, RtState, Rule, RuleError}; -use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use rand::prelude::*; -use tracing::info; -use vek::*; pub struct ReplenishResources; diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 0a95edb1eb..12f6794382 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -113,7 +113,9 @@ impl Rule for SimulateNpcs { Npc::new( rng.gen(), rand_wpos(&mut rng), - Body::BirdLarge(comp::body::bird_large::Body::random_with(&mut rng, species)), + Body::BirdLarge(comp::body::bird_large::Body::random_with( + &mut rng, species, + )), ) .with_home(site_id), ); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 047318a164..5cade6e2a9 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -2003,13 +2003,10 @@ fn handle_kill_npcs( .get(entity) .copied() { - ecs - .write_resource::() + ecs.write_resource::() .hook_rtsim_entity_delete( &ecs.read_resource::>(), - ecs - .read_resource::() - .as_index_ref(), + ecs.read_resource::().as_index_ref(), rtsim_entity, ); } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 9fbd288978..ec85a61727 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -13,13 +13,12 @@ use common::{ }, event::{EventBus, ServerEvent}, path::TraversalConfig, - rtsim::RtSimEvent, }; use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::thread_rng; use rayon::iter::ParallelIterator; -use specs::{saveload::MarkerAllocator, Join, ParJoin, Read, WriteExpect, WriteStorage}; +use specs::{saveload::MarkerAllocator, Join, ParJoin, Read, WriteStorage}; /// This system will allow NPCs to modify their controller #[derive(Default)] From 1c0fdf922875a67c022a9243155c5ecf375ca81a Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Mar 2023 13:59:55 +0100 Subject: [PATCH 064/139] rtsim personalities --- Cargo.lock | 1 + common/src/rtsim.rs | 120 ++++++++ rtsim/src/data/npc.rs | 12 +- rtsim/src/gen/mod.rs | 6 +- server/agent/Cargo.toml | 5 +- server/agent/src/action_nodes.rs | 3 +- server/src/rtsim2/tick.rs | 1 + server/src/sys/agent/behavior_tree.rs | 11 - .../sys/agent/behavior_tree/interaction.rs | 284 ++++++++---------- 9 files changed, 268 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b04dd4df1..fa46bcc324 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7032,6 +7032,7 @@ dependencies = [ "veloren-common-base", "veloren-common-dynlib", "veloren-common-ecs", + "veloren-rtsim", ] [[package]] diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 0d32bc48bb..e50dc9b920 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,8 +3,10 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. +use rand::{Rng, seq::IteratorRandom}; use serde::{Deserialize, Serialize}; use specs::Component; +use strum::{EnumIter, IntoEnumIterator}; use vek::*; use crate::comp::dialogue::MoodState; @@ -54,6 +56,121 @@ pub enum MemoryItem { Mood { state: MoodState }, } + +#[derive(EnumIter, Clone, Copy)] +pub enum PersonalityTrait { + Open, + Adventurous, + Closed, + Conscientious, + Busybody, + Unconscientious, + Extroverted, + Introverted, + Agreeable, + Sociable, + Disagreeable, + Neurotic, + Seeker, + Worried, + SadLoner, + Stable, +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub struct Personality { + openness: u8, + conscientiousness: u8, + extraversion: u8, + agreeableness: u8, + neuroticism: u8, +} + +fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 { + let l = max - min; + min + rng.gen_range(0..=l / 3) + rng.gen_range(0..=l / 3 + l % 3 % 2) + rng.gen_range(0..=l / 3 + l % 3 / 2) +} + +impl Personality { + pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD; + pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20; + pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20; + pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN; + const MIN: u8 = 0; + pub const MID: u8 = (Self::MAX - Self::MIN) / 2; + const MAX: u8 = 255; + + fn distributed_value(rng: &mut impl Rng) -> u8 { + distributed(Self::MIN, Self::MAX, rng) + } + + pub fn random(rng: &mut impl Rng) -> Self { + Self { + openness: Self::distributed_value(rng), + conscientiousness: Self::distributed_value(rng), + extraversion: Self::distributed_value(rng), + agreeableness: Self::distributed_value(rng), + neuroticism: Self::distributed_value(rng), + } + } + + pub fn random_evil(rng: &mut impl Rng) -> Self { + Self { + openness: Self::distributed_value(rng), + extraversion: Self::distributed_value(rng), + neuroticism: Self::distributed_value(rng), + agreeableness: distributed(0, Self::LOW_THRESHOLD - 1, rng), + conscientiousness: distributed(0, Self::LOW_THRESHOLD - 1, rng), + } + } + + pub fn random_good(rng: &mut impl Rng) -> Self { + Self { + openness: Self::distributed_value(rng), + extraversion: Self::distributed_value(rng), + neuroticism: Self::distributed_value(rng), + agreeableness: Self::distributed_value(rng), + conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng), + } + } + + pub fn is(&self, trait_: PersonalityTrait) -> bool { + match trait_ { + PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD, + PersonalityTrait::Adventurous => self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID, + PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD, + PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD, + PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD, + PersonalityTrait::Unconscientious => self.conscientiousness < Personality::LOW_THRESHOLD, + PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD, + PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD, + PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD, + PersonalityTrait::Sociable => self.agreeableness > Personality::HIGH_THRESHOLD && self.extraversion > Personality::MID, + PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD, + PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD, + PersonalityTrait::Seeker => self.neuroticism > Personality::HIGH_THRESHOLD && self.openness > Personality::LITTLE_HIGH, + PersonalityTrait::Worried => self.neuroticism > Personality::HIGH_THRESHOLD && self.agreeableness > Personality::LITTLE_HIGH, + PersonalityTrait::SadLoner => self.neuroticism > Personality::HIGH_THRESHOLD && self.extraversion < Personality::LITTLE_LOW, + PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD, + } + } + + pub fn chat_trait(&self, rng: &mut impl Rng) -> Option { + PersonalityTrait::iter().filter(|t| self.is(*t)).choose(rng) + } + + pub fn will_ambush(&self) -> bool { + self.agreeableness < Self::LOW_THRESHOLD + && self.conscientiousness < Self::LOW_THRESHOLD + } +} + +impl Default for Personality { + fn default() -> Self { + Self { openness: Personality::MID, conscientiousness: Personality::MID, extraversion: Personality::MID, agreeableness: Personality::MID, neuroticism: Personality::MID } + } +} + /// This type is the map route through which the rtsim (real-time simulation) /// aspect of the game communicates with the rest of the game. It is analagous /// to `comp::Controller` in that it provides a consistent interface for @@ -69,6 +186,7 @@ pub struct RtSimController { /// toward the given location, accounting for obstacles and other /// high-priority situations like being attacked. pub travel_to: Option>, + pub personality: Personality, pub heading_to: Option, /// Proportion of full speed to move pub speed_factor: f32, @@ -80,6 +198,7 @@ impl Default for RtSimController { fn default() -> Self { Self { travel_to: None, + personality:Personality::default(), heading_to: None, speed_factor: 1.0, events: Vec::new(), @@ -91,6 +210,7 @@ impl RtSimController { pub fn with_destination(pos: Vec3) -> Self { Self { travel_to: Some(pos), + personality:Personality::default(), heading_to: None, speed_factor: 0.5, events: Vec::new(), diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bd676b7dfe..bb68a6c742 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,7 +3,7 @@ pub use common::rtsim::{NpcId, Profession}; use common::{ comp, grid::Grid, - rtsim::{FactionId, SiteId, VehicleId}, + rtsim::{FactionId, SiteId, VehicleId, Personality}, store::Id, vol::RectVolSize, }; @@ -80,9 +80,10 @@ pub struct Npc { pub profession: Option, pub home: Option, pub faction: Option, - pub riding: Option, + pub personality: Personality, + // Unpersisted state #[serde(skip_serializing, skip_deserializing)] pub chunk_pos: Option>, @@ -113,6 +114,7 @@ impl Clone for Npc { faction: self.faction, riding: self.riding.clone(), body: self.body, + personality: self.personality, // Not persisted chunk_pos: None, current_site: Default::default(), @@ -129,6 +131,7 @@ impl Npc { seed, wpos, body, + personality: Personality::default(), profession: None, home: None, faction: None, @@ -141,6 +144,11 @@ impl Npc { } } + pub fn with_personality(mut self, personality: Personality) -> Self { + self.personality = personality; + self + } + pub fn with_profession(mut self, profession: impl Into>) -> Self { self.profession = profession.into(); self diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index a777aa07a3..8bc2677125 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -11,7 +11,7 @@ use common::{ comp::{self, Body}, grid::Grid, resources::TimeOfDay, - rtsim::WorldSettings, + rtsim::{WorldSettings, Personality}, terrain::TerrainChunkSize, vol::RectVolSize, }; @@ -103,6 +103,7 @@ impl Data { Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_faction(site.faction) .with_home(site_id) + .with_personality(Personality::random(&mut rng)) .with_profession(match rng.gen_range(0..20) { 0 => Profession::Hunter, 1 => Profession::Blacksmith, @@ -119,6 +120,7 @@ impl Data { for _ in 0..15 { this.npcs.create_npc( Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) + .with_personality(Personality::random_evil(&mut rng)) .with_faction(site.faction) .with_home(site_id) .with_profession(match rng.gen_range(0..20) { @@ -130,6 +132,7 @@ impl Data { this.npcs.create_npc( Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) .with_home(site_id) + .with_personality(Personality::random_good(&mut rng)) .with_profession(Profession::Merchant), ); @@ -143,6 +146,7 @@ impl Data { Npc::new(rng.gen(), wpos, random_humanoid(&mut rng)) .with_home(site_id) .with_profession(Profession::Captain) + .with_personality(Personality::random_good(&mut rng)) .steering(vehicle_id), ); } diff --git a/server/agent/Cargo.toml b/server/agent/Cargo.toml index 9b2ee49c14..9009caae63 100644 --- a/server/agent/Cargo.toml +++ b/server/agent/Cargo.toml @@ -9,10 +9,11 @@ use-dyn-lib = ["common-dynlib"] be-dyn-lib = [] [dependencies] -common = {package = "veloren-common", path = "../../common"} +common = { package = "veloren-common", path = "../../common"} common-base = { package = "veloren-common-base", path = "../../common/base" } common-ecs = { package = "veloren-common-ecs", path = "../../common/ecs" } -common-dynlib = {package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true} +common-dynlib = { package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true} +rtsim = { package = "veloren-rtsim", path = "../../rtsim" } specs = { version = "0.18", features = ["shred-derive"] } vek = { version = "0.15.8", features = ["serde"] } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index e4907eb356..115cc5fb11 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -650,7 +650,6 @@ impl<'a> AgentData<'a> { controller: &mut Controller, read_data: &ReadData, event_emitter: &mut Emitter, - will_ambush: bool, ) { enum ActionStateTimers { TimerChooseTarget = 0, @@ -673,7 +672,7 @@ impl<'a> AgentData<'a> { .get(entity) .map_or(false, |eu| eu != self.uid) }; - if will_ambush + if agent.rtsim_controller.personality.will_ambush() && self_different_from_entity() && !self.passive_towards(entity, read_data) { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 47dd8b60d5..4949c4d959 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -364,6 +364,7 @@ impl<'a> System<'a> for Sys { // Update entity state if let Some(agent) = agent { + agent.rtsim_controller.personality = npc.personality; if let Some(action) = npc.action { match action { rtsim2::data::npc::NpcAction::Goto(wpos, sf) => { diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6a8cad8ef6..5c262bf3ef 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -498,7 +498,6 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool { bdata.controller, bdata.read_data, bdata.event_emitter, - will_ambush(/* bdata.rtsim_entity */ None, &bdata.agent_data), ); } else { bdata.agent_data.handle_sounds_heard( @@ -747,7 +746,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { controller, read_data, event_emitter, - will_ambush(agent_data.rtsim_entity, agent_data), ); } @@ -775,15 +773,6 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { false } -fn will_ambush(rtsim_entity: Option<&RtSimEntity>, agent_data: &AgentData) -> bool { - // TODO: implement for rtsim2 - // agent_data - // .health - // .map_or(false, |h| h.current() / h.maximum() > 0.7) - // && rtsim_entity.map_or(false, |re| re.brain.personality.will_ambush) - false -} - fn remembers_fight_with( rtsim_entity: Option<&RtSimEntity>, read_data: &ReadData, diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index e438fe9d86..cb7ef5b431 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -2,14 +2,14 @@ use common::{ comp::{ agent::{AgentEvent, Target, TimerAction}, compass::{Direction, Distance}, - dialogue::{MoodContext, MoodState, Subject}, + dialogue::Subject, inventory::item::{ItemTag, MaterialStatManifest}, invite::{InviteKind, InviteResponse}, tool::AbilityMap, BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, }, event::ServerEvent, - rtsim::{Memory, MemoryItem, RtSimEvent}, + rtsim::{Memory, MemoryItem, RtSimEvent, PersonalityTrait}, trade::{TradeAction, TradePhase, TradeResult}, }; use rand::{thread_rng, Rng}; @@ -105,172 +105,142 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { match subject { Subject::Regular => { - if let Some(destination_name) = &agent.rtsim_controller.heading_to { - let msg = format!( - "I'm heading to {}! Want to come along?", - destination_name - ); - agent_data.chat_npc(msg, event_emitter); - } - /*if let ( - Some((_travel_to, destination_name)), - Some(rtsim_entity), - ) = (&agent.rtsim_controller.travel_to, &agent_data.rtsim_entity) - { - let personality = &rtsim_entity.brain.personality; - let standard_response_msg = || -> String { - if personality.will_ambush { - format!( - "I'm heading to {}! Want to come along? We'll make \ - great travel buddies, hehe.", - destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) - { - format!( - "I'm heading to {}! Want to come along?", - destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Hrm.".to_string() - } else { - "Hello!".to_string() - } - }; - let msg = if let Some(tgt_stats) = read_data.stats.get(target) { - agent.rtsim_controller.events.push(RtSimEvent::AddMemory( - Memory { - item: MemoryItem::CharacterInteraction { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 + 600.0, + if let Some(tgt_stats) = read_data.stats.get(target) { + agent.rtsim_controller.events.push(RtSimEvent::AddMemory( + Memory { + item: MemoryItem::CharacterInteraction { + name: tgt_stats.name.clone(), }, - )); - if rtsim_entity.brain.remembers_character(&tgt_stats.name) { - if personality.will_ambush { - "Just follow me a bit more, hehe.".to_string() - } else if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) + time_to_forget: read_data.time.0 + 600.0, + }, + )); + if let Some(destination_name) = &agent.rtsim_controller.heading_to { + let personality = &agent.rtsim_controller.personality; + let standard_response_msg = || -> String { + if personality.will_ambush() { + format!( + "I'm heading to {}! Want to come along? We'll make \ + great travel buddies, hehe.", + destination_name + ) + } else if personality.is(PersonalityTrait::Extroverted) { - if personality - .personality_traits - .contains(PersonalityTrait::Extroverted) + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } else if personality.is(PersonalityTrait::Disagreeable) + { + "Hrm.".to_string() + } else { + "Hello!".to_string() + } + }; + let msg = if false /* TODO: Remembers character */ { + if personality.will_ambush() { + "Just follow me a bit more, hehe.".to_string() + } else if personality.is(PersonalityTrait::Extroverted) { - format!( - "Greetings fair {}! It has been far \ - too long since last I saw you. I'm \ - going to {} right now.", - &tgt_stats.name, destination_name - ) - } else if personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() + if personality.is(PersonalityTrait::Extroverted) + { + format!( + "Greetings fair {}! It has been far \ + too long since last I saw you. I'm \ + going to {} right now.", + &tgt_stats.name, destination_name + ) + } else if personality.is(PersonalityTrait::Disagreeable) + { + "Oh. It's you again.".to_string() + } else { + format!( + "Hi again {}! Unfortunately I'm in a \ + hurry right now. See you!", + &tgt_stats.name + ) + } } else { - format!( - "Hi again {}! Unfortunately I'm in a \ - hurry right now. See you!", - &tgt_stats.name - ) + standard_response_msg() } } else { standard_response_msg() - } + }; + agent_data.chat_npc(msg, event_emitter); + } + /*else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); + agent_data.chat_npc( + "npc-speech-merchant_advertisement", + event_emitter, + ); } else { - standard_response_msg() - }; - agent_data.chat_npc(msg, event_emitter); - } else*/ - else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( - "npc-speech-merchant_advertisement", - event_emitter, - ); - } else { - let default_msg = "npc-speech-merchant_busy"; - let msg = default_msg/*agent_data.rtsim_entity.map_or(default_msg, |e| { - if e.brain - .personality - .personality_traits - .contains(PersonalityTrait::Disagreeable) - { + let default_msg = "npc-speech-merchant_busy"; + let msg = if agent.rtsim_controller.personality.is(PersonalityTrait::Disagreeable) { "npc-speech-merchant_busy_rude" } else { default_msg - } - })*/; - agent_data.chat_npc(msg, event_emitter); - } - } else { - let mut rng = thread_rng(); - /*if let Some(extreme_trait) = - agent_data.rtsim_entity.and_then(|e| { - e.brain.personality.random_chat_trait(&mut rng) - }) - { - let msg = match extreme_trait { - PersonalityTrait::Open => { - "npc-speech-villager_open" - }, - PersonalityTrait::Adventurous => { - "npc-speech-villager_adventurous" - }, - PersonalityTrait::Closed => { - "npc-speech-villager_closed" - }, - PersonalityTrait::Conscientious => { - "npc-speech-villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc-speech-villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc-speech-villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc-speech-villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc-speech-villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc-speech-villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc-speech-villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc-speech-villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc-speech-villager_neurotic" - }, - PersonalityTrait::Seeker => { - "npc-speech-villager_seeker" - }, - PersonalityTrait::SadLoner => { - "npc-speech-villager_sad_loner" - }, - PersonalityTrait::Worried => { - "npc-speech-villager_worried" - }, - PersonalityTrait::Stable => { - "npc-speech-villager_stable" - }, - }; - agent_data.chat_npc(msg, event_emitter); - } else*/ - { - agent_data.chat_npc("npc-speech-villager", event_emitter); + }; + agent_data.chat_npc(msg, event_emitter); + } + }*/ else { + let mut rng = thread_rng(); + if let Some(extreme_trait) = agent.rtsim_controller.personality.chat_trait(&mut rng) + { + let msg = match extreme_trait { + PersonalityTrait::Open => { + "npc-speech-villager_open" + }, + PersonalityTrait::Adventurous => { + "npc-speech-villager_adventurous" + }, + PersonalityTrait::Closed => { + "npc-speech-villager_closed" + }, + PersonalityTrait::Conscientious => { + "npc-speech-villager_conscientious" + }, + PersonalityTrait::Busybody => { + "npc-speech-villager_busybody" + }, + PersonalityTrait::Unconscientious => { + "npc-speech-villager_unconscientious" + }, + PersonalityTrait::Extroverted => { + "npc-speech-villager_extroverted" + }, + PersonalityTrait::Introverted => { + "npc-speech-villager_introverted" + }, + PersonalityTrait::Agreeable => { + "npc-speech-villager_agreeable" + }, + PersonalityTrait::Sociable => { + "npc-speech-villager_sociable" + }, + PersonalityTrait::Disagreeable => { + "npc-speech-villager_disagreeable" + }, + PersonalityTrait::Neurotic => { + "npc-speech-villager_neurotic" + }, + PersonalityTrait::Seeker => { + "npc-speech-villager_seeker" + }, + PersonalityTrait::SadLoner => { + "npc-speech-villager_sad_loner" + }, + PersonalityTrait::Worried => { + "npc-speech-villager_worried" + }, + PersonalityTrait::Stable => { + "npc-speech-villager_stable" + }, + }; + agent_data.chat_npc(msg, event_emitter); + } else { + agent_data.chat_npc("npc-speech-villager", event_emitter); + } } } }, From f5c1e07642f711f4c4d1581c3210697a9c944dd2 Mon Sep 17 00:00:00 2001 From: Isse Date: Wed, 22 Mar 2023 20:23:03 +0100 Subject: [PATCH 065/139] apply personality to repopulated npcs --- rtsim/src/rule/simulate_npcs.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 12f6794382..081b3d634d 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -7,7 +7,7 @@ use common::{ comp::{self, Body}, grid::Grid, terrain::TerrainChunkSize, - vol::RectVolSize, + vol::RectVolSize, rtsim::Personality, }; use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; use tracing::warn; @@ -76,6 +76,7 @@ impl Rule for SimulateNpcs { }; data.spawn_npc( Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) + .with_personality(Personality::random(&mut rng)) .with_home(site_id) .with_faction(npc.faction) .with_profession(npc.profession.clone()), From 66710d5bc241f3283464c597f9fcdca7d6b1e3bb Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Mar 2023 22:09:39 +0100 Subject: [PATCH 066/139] NPCs that live in stale sites just idle instead --- rtsim/src/data/npc.rs | 2 +- rtsim/src/gen/mod.rs | 2 +- rtsim/src/rule/npc_ai.rs | 20 +++++++++++++++++--- rtsim/src/rule/simulate_npcs.rs | 7 +++++-- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index bb68a6c742..6c0c26e6f1 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,7 +3,7 @@ pub use common::rtsim::{NpcId, Profession}; use common::{ comp, grid::Grid, - rtsim::{FactionId, SiteId, VehicleId, Personality}, + rtsim::{FactionId, Personality, SiteId, VehicleId}, store::Id, vol::RectVolSize, }; diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 8bc2677125..bbe7d43d6c 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -11,7 +11,7 @@ use common::{ comp::{self, Body}, grid::Grid, resources::TimeOfDay, - rtsim::{WorldSettings, Personality}, + rtsim::{Personality, WorldSettings}, terrain::TerrainChunkSize, vol::RectVolSize, }; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index bb87d3b028..1e0977b681 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -350,7 +350,7 @@ fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action }) } -fn traverse_points(mut next_point: F) -> impl Action<()> +fn traverse_points(mut next_point: F) -> impl Action where F: FnMut(&mut NpcCtx) -> Option> + Send + Sync + 'static, { @@ -397,7 +397,11 @@ fn travel_to_point(wpos: Vec2) -> impl Action { let diff = wpos - start; let n = (diff.magnitude() / WAYPOINT).max(1.0); let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); - traverse_points(move |_| points.next()) + if diff.magnitude() > 1.0 { + traverse_points(move |_| points.next()).boxed() + } else { + finish().boxed() + } }) .debug(|| "travel to point") } @@ -538,7 +542,17 @@ fn adventure() -> impl Action { fn villager(visiting_site: SiteId) -> impl Action { choose(move |ctx| { - if ctx.npc.current_site != Some(visiting_site) { + if ctx + .state + .data() + .sites + .get(visiting_site) + .map_or(true, |s| s.world_site.is_none()) + { + casual( + idle().debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"), + ) + } else if ctx.npc.current_site != Some(visiting_site) { let npc_home = ctx.npc.home; // Travel to the site we're supposed to be in urgent(travel_to_site(visiting_site).debug(move || { diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 081b3d634d..2703c1d8b9 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -6,8 +6,9 @@ use crate::{ use common::{ comp::{self, Body}, grid::Grid, + rtsim::Personality, terrain::TerrainChunkSize, - vol::RectVolSize, rtsim::Personality, + vol::RectVolSize, }; use rand::{rngs::ThreadRng, seq::SliceRandom, Rng}; use tracing::warn; @@ -155,7 +156,9 @@ impl Rule for SimulateNpcs { .world .sim() .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) - .and_then(|chunk| data.sites.world_site_map.get(chunk.sites.first()?).copied()); + .and_then(|chunk| chunk.sites + .iter() + .find_map(|site| data.sites.world_site_map.get(site).copied())); let chunk_pos = npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); From 64324262c72da069a67b1922ac3ecf0e06918587 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Mar 2023 23:20:52 +0100 Subject: [PATCH 067/139] Added /rtsim_purge command --- common/src/cmd.rs | 23 ++++-- common/src/rtsim.rs | 60 +++++++++----- common/src/states/behavior.rs | 4 +- common/systems/src/character_behavior.rs | 4 +- rtsim/src/data/mod.rs | 2 + rtsim/src/gen/mod.rs | 1 + server/src/cmd.rs | 39 ++++++++- server/src/lib.rs | 11 ++- server/src/rtsim2/mod.rs | 22 +++++- server/src/rtsim2/tick.rs | 4 +- server/src/sys/agent/behavior_tree.rs | 7 +- .../sys/agent/behavior_tree/interaction.rs | 79 +++++++++---------- 12 files changed, 171 insertions(+), 85 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index fb2b8068dc..270b60377a 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -310,8 +310,9 @@ pub enum ServerChatCommand { Tell, Time, Tp, - TpNpc, - NpcInfo, + RtsimTp, + RtsimInfo, + RtsimPurge, RtsimChunk, Unban, Version, @@ -682,16 +683,25 @@ impl ServerChatCommand { "Teleport to another player", Some(Moderator), ), - ServerChatCommand::TpNpc => cmd( + ServerChatCommand::RtsimTp => cmd( vec![Integer("npc index", 0, Required)], "Teleport to an rtsim npc", Some(Moderator), ), - ServerChatCommand::NpcInfo => cmd( + ServerChatCommand::RtsimInfo => cmd( vec![Integer("npc index", 0, Required)], "Display information about an rtsim NPC", Some(Moderator), ), + ServerChatCommand::RtsimPurge => cmd( + vec![Boolean( + "whether purging of rtsim data should occur on next startup", + true.to_string(), + Required, + )], + "Purge rtsim data on next startup", + Some(Admin), + ), ServerChatCommand::RtsimChunk => cmd( vec![], "Display information about the current chunk from rtsim", @@ -824,8 +834,9 @@ impl ServerChatCommand { ServerChatCommand::Tell => "tell", ServerChatCommand::Time => "time", ServerChatCommand::Tp => "tp", - ServerChatCommand::TpNpc => "tp_npc", - ServerChatCommand::NpcInfo => "npc_info", + ServerChatCommand::RtsimTp => "rtsim_tp", + ServerChatCommand::RtsimInfo => "rtsim_info", + ServerChatCommand::RtsimPurge => "rtsim_purge", ServerChatCommand::RtsimChunk => "rtsim_chunk", ServerChatCommand::Unban => "unban", ServerChatCommand::Version => "version", diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index e50dc9b920..9fef179a51 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,7 +3,7 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. -use rand::{Rng, seq::IteratorRandom}; +use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; use strum::{EnumIter, IntoEnumIterator}; @@ -56,7 +56,6 @@ pub enum MemoryItem { Mood { state: MoodState }, } - #[derive(EnumIter, Clone, Copy)] pub enum PersonalityTrait { Open, @@ -88,7 +87,9 @@ pub struct Personality { fn distributed(min: u8, max: u8, rng: &mut impl Rng) -> u8 { let l = max - min; - min + rng.gen_range(0..=l / 3) + rng.gen_range(0..=l / 3 + l % 3 % 2) + rng.gen_range(0..=l / 3 + l % 3 / 2) + min + rng.gen_range(0..=l / 3) + + rng.gen_range(0..=l / 3 + l % 3 % 2) + + rng.gen_range(0..=l / 3 + l % 3 / 2) } impl Personality { @@ -96,13 +97,11 @@ impl Personality { pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20; pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20; pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN; - const MIN: u8 = 0; - pub const MID: u8 = (Self::MAX - Self::MIN) / 2; const MAX: u8 = 255; + pub const MID: u8 = (Self::MAX - Self::MIN) / 2; + const MIN: u8 = 0; - fn distributed_value(rng: &mut impl Rng) -> u8 { - distributed(Self::MIN, Self::MAX, rng) - } + fn distributed_value(rng: &mut impl Rng) -> u8 { distributed(Self::MIN, Self::MAX, rng) } pub fn random(rng: &mut impl Rng) -> Self { Self { @@ -130,27 +129,43 @@ impl Personality { extraversion: Self::distributed_value(rng), neuroticism: Self::distributed_value(rng), agreeableness: Self::distributed_value(rng), - conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng), + conscientiousness: distributed(Self::LOW_THRESHOLD, Self::MAX, rng), } } pub fn is(&self, trait_: PersonalityTrait) -> bool { match trait_ { PersonalityTrait::Open => self.openness > Personality::HIGH_THRESHOLD, - PersonalityTrait::Adventurous => self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID, + PersonalityTrait::Adventurous => { + self.openness > Personality::HIGH_THRESHOLD && self.neuroticism < Personality::MID + }, PersonalityTrait::Closed => self.openness < Personality::LOW_THRESHOLD, PersonalityTrait::Conscientious => self.conscientiousness > Personality::HIGH_THRESHOLD, PersonalityTrait::Busybody => self.agreeableness < Personality::LOW_THRESHOLD, - PersonalityTrait::Unconscientious => self.conscientiousness < Personality::LOW_THRESHOLD, + PersonalityTrait::Unconscientious => { + self.conscientiousness < Personality::LOW_THRESHOLD + }, PersonalityTrait::Extroverted => self.extraversion > Personality::HIGH_THRESHOLD, PersonalityTrait::Introverted => self.extraversion < Personality::LOW_THRESHOLD, PersonalityTrait::Agreeable => self.agreeableness > Personality::HIGH_THRESHOLD, - PersonalityTrait::Sociable => self.agreeableness > Personality::HIGH_THRESHOLD && self.extraversion > Personality::MID, + PersonalityTrait::Sociable => { + self.agreeableness > Personality::HIGH_THRESHOLD + && self.extraversion > Personality::MID + }, PersonalityTrait::Disagreeable => self.agreeableness < Personality::LOW_THRESHOLD, PersonalityTrait::Neurotic => self.neuroticism > Personality::HIGH_THRESHOLD, - PersonalityTrait::Seeker => self.neuroticism > Personality::HIGH_THRESHOLD && self.openness > Personality::LITTLE_HIGH, - PersonalityTrait::Worried => self.neuroticism > Personality::HIGH_THRESHOLD && self.agreeableness > Personality::LITTLE_HIGH, - PersonalityTrait::SadLoner => self.neuroticism > Personality::HIGH_THRESHOLD && self.extraversion < Personality::LITTLE_LOW, + PersonalityTrait::Seeker => { + self.neuroticism > Personality::HIGH_THRESHOLD + && self.openness > Personality::LITTLE_HIGH + }, + PersonalityTrait::Worried => { + self.neuroticism > Personality::HIGH_THRESHOLD + && self.agreeableness > Personality::LITTLE_HIGH + }, + PersonalityTrait::SadLoner => { + self.neuroticism > Personality::HIGH_THRESHOLD + && self.extraversion < Personality::LITTLE_LOW + }, PersonalityTrait::Stable => self.neuroticism < Personality::LOW_THRESHOLD, } } @@ -160,14 +175,19 @@ impl Personality { } pub fn will_ambush(&self) -> bool { - self.agreeableness < Self::LOW_THRESHOLD - && self.conscientiousness < Self::LOW_THRESHOLD + self.agreeableness < Self::LOW_THRESHOLD && self.conscientiousness < Self::LOW_THRESHOLD } } impl Default for Personality { fn default() -> Self { - Self { openness: Personality::MID, conscientiousness: Personality::MID, extraversion: Personality::MID, agreeableness: Personality::MID, neuroticism: Personality::MID } + Self { + openness: Personality::MID, + conscientiousness: Personality::MID, + extraversion: Personality::MID, + agreeableness: Personality::MID, + neuroticism: Personality::MID, + } } } @@ -198,7 +218,7 @@ impl Default for RtSimController { fn default() -> Self { Self { travel_to: None, - personality:Personality::default(), + personality: Personality::default(), heading_to: None, speed_factor: 1.0, events: Vec::new(), @@ -210,7 +230,7 @@ impl RtSimController { pub fn with_destination(pos: Vec3) -> Self { Self { travel_to: Some(pos), - personality:Personality::default(), + personality: Personality::default(), heading_to: None, speed_factor: 0.5, events: Vec::new(), diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index d97e1bf92d..dd52911ca8 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -5,8 +5,8 @@ use crate::{ item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, - InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate, Stats, - Vel, + InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate, + Stats, Vel, }, link::Is, mounting::Rider, diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 28c3393df6..aaad927eaa 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -9,8 +9,8 @@ use common::{ character_state::OutputEvents, inventory::item::{tool::AbilityMap, MaterialStatManifest}, ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, - Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, Stance, - StateUpdate, Stats, Vel, + Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, + Stance, StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, link::Is, diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 3f2bcae678..5b537c1ac6 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -43,6 +43,8 @@ pub struct Data { pub factions: Factions, pub time_of_day: TimeOfDay, + // If true, rtsim data will be ignored (and, hence, overwritten on next save) on load. + pub should_purge: bool, } pub type ReadError = rmp_serde::decode::Error; diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index bbe7d43d6c..c3977eb851 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -44,6 +44,7 @@ impl Data { }, time_of_day: TimeOfDay(settings.start_time), + should_purge: false, }; let initial_factions = (0..16) diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5cade6e2a9..82eb3345bf 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -184,8 +184,9 @@ fn do_command( ServerChatCommand::Tell => handle_tell, ServerChatCommand::Time => handle_time, ServerChatCommand::Tp => handle_tp, - ServerChatCommand::TpNpc => handle_tp_npc, - ServerChatCommand::NpcInfo => handle_npc_info, + ServerChatCommand::RtsimTp => handle_rtsim_tp, + ServerChatCommand::RtsimInfo => handle_rtsim_info, + ServerChatCommand::RtsimPurge => handle_rtsim_purge, ServerChatCommand::RtsimChunk => handle_rtsim_chunk, ServerChatCommand::Unban => handle_unban, ServerChatCommand::Version => handle_version, @@ -1185,7 +1186,7 @@ fn handle_tp( }) } -fn handle_tp_npc( +fn handle_rtsim_tp( server: &mut Server, _client: EcsEntity, target: EcsEntity, @@ -1214,7 +1215,7 @@ fn handle_tp_npc( }) } -fn handle_npc_info( +fn handle_rtsim_info( server: &mut Server, client: EcsEntity, target: EcsEntity, @@ -1263,6 +1264,36 @@ fn handle_npc_info( } } +fn handle_rtsim_purge( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + use crate::rtsim2::RtSim; + if let Some(should_purge) = parse_cmd_args!(args, bool) { + server + .state + .ecs() + .write_resource::() + .set_should_purge(should_purge); + server.notify_client( + client, + ServerGeneral::server_msg( + ChatType::CommandInfo, + format!( + "Rtsim data {} be purged on next startup", + if should_purge { "WILL" } else { "will NOT" }, + ), + ), + ); + Ok(()) + } else { + return Err(action.help_string()); + } +} + fn handle_rtsim_chunk( server: &mut Server, client: EcsEntity, diff --git a/server/src/lib.rs b/server/src/lib.rs index 44332db921..0a5ddb03b7 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -80,8 +80,8 @@ use common::{ comp, event::{EventBus, ServerEvent}, resources::{BattleMode, GameMode, Time, TimeOfDay}, - shared_server_config::ServerConstants, rtsim::{RtSimEntity, RtSimVehicle}, + shared_server_config::ServerConstants, slowjob::SlowJobPool, terrain::{Block, TerrainChunk, TerrainChunkSize}, vol::RectRasterableVol, @@ -1463,6 +1463,15 @@ impl Drop for Server { info!("Unloading terrain persistence..."); terrain_persistence.unload_all() }); + + #[cfg(feature = "worldgen")] + { + info!("Saving rtsim state..."); + self.state + .ecs() + .write_resource::() + .save(true); + } } } diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 3073939e6f..a4bffb6164 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -54,7 +54,14 @@ impl RtSim { match Data::from_reader(io::BufReader::new(file)) { Ok(data) => { info!("Rtsim data loaded."); - break 'load data; + if data.should_purge { + warn!( + "The should_purge flag was set on the rtsim data, \ + generating afresh" + ); + } else { + break 'load data; + } }, Err(e) => { error!("Rtsim data failed to load: {}", e); @@ -171,14 +178,14 @@ impl RtSim { self.state.data_mut().npcs.remove(entity.0); } - pub fn save(&mut self, slowjob_pool: &SlowJobPool) { + pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { info!("Saving rtsim data..."); let file_path = self.file_path.clone(); let data = self.state.data().clone(); debug!("Starting rtsim data save job..."); // TODO: Use slow job // slowjob_pool.spawn("RTSIM_SAVE", move || { - std::thread::spawn(move || { + let handle = std::thread::spawn(move || { let tmp_file_name = "data_tmp.dat"; if let Err(e) = file_path .parent() @@ -203,6 +210,11 @@ impl RtSim { error!("Saving rtsim data failed: {}", e); } }); + + if wait_until_finished { + handle.join(); + } + self.last_saved = Some(Instant::now()); } @@ -212,6 +224,10 @@ impl RtSim { } pub fn state(&self) -> &RtState { &self.state } + + pub fn set_should_purge(&mut self, should_purge: bool) { + self.state.data_mut().should_purge = should_purge; + } } pub struct ChunkStates(pub Grid>); diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 4949c4d959..b6a93d7f20 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -223,7 +223,9 @@ impl<'a> System<'a> for Sys { .last_saved .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { - rtsim.save(&slow_jobs); + // TODO: Use slow jobs + let _ = slow_jobs; + rtsim.save(/* &slow_jobs, */ false); } let chunk_states = rtsim.state.resource::(); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 5c262bf3ef..5d0a6ef07c 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -741,12 +741,7 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; if !in_aggro_range && is_time_to_retarget { - agent_data.choose_target( - agent, - controller, - read_data, - event_emitter, - ); + agent_data.choose_target(agent, controller, read_data, event_emitter); } if aggro_on { diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index cb7ef5b431..f477037ff0 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -9,7 +9,7 @@ use common::{ BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, }, event::ServerEvent, - rtsim::{Memory, MemoryItem, RtSimEvent, PersonalityTrait}, + rtsim::{Memory, MemoryItem, PersonalityTrait, RtSimEvent}, trade::{TradeAction, TradePhase, TradeResult}, }; use rand::{thread_rng, Rng}; @@ -106,65 +106,64 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { match subject { Subject::Regular => { if let Some(tgt_stats) = read_data.stats.get(target) { - agent.rtsim_controller.events.push(RtSimEvent::AddMemory( - Memory { + agent + .rtsim_controller + .events + .push(RtSimEvent::AddMemory(Memory { item: MemoryItem::CharacterInteraction { name: tgt_stats.name.clone(), }, time_to_forget: read_data.time.0 + 600.0, - }, - )); + })); if let Some(destination_name) = &agent.rtsim_controller.heading_to { let personality = &agent.rtsim_controller.personality; let standard_response_msg = || -> String { if personality.will_ambush() { format!( - "I'm heading to {}! Want to come along? We'll make \ - great travel buddies, hehe.", + "I'm heading to {}! Want to come along? We'll \ + make great travel buddies, hehe.", destination_name ) - } else if personality.is(PersonalityTrait::Extroverted) - { + } else if personality.is(PersonalityTrait::Extroverted) { format!( "I'm heading to {}! Want to come along?", destination_name ) - } else if personality.is(PersonalityTrait::Disagreeable) - { + } else if personality.is(PersonalityTrait::Disagreeable) { "Hrm.".to_string() } else { "Hello!".to_string() } }; - let msg = if false /* TODO: Remembers character */ { - if personality.will_ambush() { - "Just follow me a bit more, hehe.".to_string() - } else if personality.is(PersonalityTrait::Extroverted) + let msg = if false + /* TODO: Remembers character */ + { + if personality.will_ambush() { + "Just follow me a bit more, hehe.".to_string() + } else if personality.is(PersonalityTrait::Extroverted) { + if personality.is(PersonalityTrait::Extroverted) { + format!( + "Greetings fair {}! It has been far too long \ + since last I saw you. I'm going to {} right \ + now.", + &tgt_stats.name, destination_name + ) + } else if personality.is(PersonalityTrait::Disagreeable) { - if personality.is(PersonalityTrait::Extroverted) - { - format!( - "Greetings fair {}! It has been far \ - too long since last I saw you. I'm \ - going to {} right now.", - &tgt_stats.name, destination_name - ) - } else if personality.is(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() - } else { - format!( - "Hi again {}! Unfortunately I'm in a \ - hurry right now. See you!", - &tgt_stats.name - ) - } + "Oh. It's you again.".to_string() } else { - standard_response_msg() + format!( + "Hi again {}! Unfortunately I'm in a hurry \ + right now. See you!", + &tgt_stats.name + ) } } else { standard_response_msg() - }; + } + } else { + standard_response_msg() + }; agent_data.chat_npc(msg, event_emitter); } /*else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { @@ -183,14 +182,14 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { }; agent_data.chat_npc(msg, event_emitter); } - }*/ else { + }*/ + else { let mut rng = thread_rng(); - if let Some(extreme_trait) = agent.rtsim_controller.personality.chat_trait(&mut rng) + if let Some(extreme_trait) = + agent.rtsim_controller.personality.chat_trait(&mut rng) { let msg = match extreme_trait { - PersonalityTrait::Open => { - "npc-speech-villager_open" - }, + PersonalityTrait::Open => "npc-speech-villager_open", PersonalityTrait::Adventurous => { "npc-speech-villager_adventurous" }, From 5062920b5c18c4bcda0bebaa1cc7ad510f9f3459 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 31 Mar 2023 23:57:01 +0100 Subject: [PATCH 068/139] Better NPC spawning --- rtsim/src/gen/mod.rs | 107 ++++++++++++++++++++++++++------------- rtsim/src/rule/npc_ai.rs | 17 ++++--- server/src/rtsim2/mod.rs | 2 +- 3 files changed, 83 insertions(+), 43 deletions(-) diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index c3977eb851..757cc68ef0 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -18,7 +18,7 @@ use common::{ use rand::prelude::*; use tracing::info; use vek::*; -use world::{site::SiteKind, IndexRef, World}; +use world::{site::SiteKind, site2::PlotKind, IndexRef, World}; impl Data { pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self { @@ -77,10 +77,16 @@ impl Data { this.sites.len() ); // Spawn some test entities at the sites - for (site_id, site) in this.sites.iter() - // TODO: Stupid - .filter(|(_, site)| site.world_site.map_or(false, |ws| - matches!(&index.sites.get(ws).kind, SiteKind::Refactor(_)))) + for (site_id, site, site2) in this.sites.iter() + // TODO: Stupid. Only find site2 towns + .filter_map(|(site_id, site)| Some((site_id, site, site.world_site + .and_then(|ws| match &index.sites.get(ws).kind { + SiteKind::Refactor(site2) + | SiteKind::CliffTown(site2) + | SiteKind::SavannahPit(site2) + | SiteKind::DesertCity(site2) => Some(site2), + _ => None, + })?))) { let Some(good_or_evil) = site .faction @@ -88,8 +94,13 @@ impl Data { .map(|f| f.good_or_evil) else { continue }; - let rand_wpos = |rng: &mut SmallRng| { - let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10)); + let rand_wpos = |rng: &mut SmallRng, matches_plot: fn(&PlotKind) -> bool| { + let wpos2d = site2 + .plots() + .filter(|plot| matches_plot(plot.kind())) + .choose(&mut thread_rng()) + .map(|plot| site2.tile_center_wpos(plot.root_tile())) + .unwrap_or_else(|| site.wpos.map(|e| e + rng.gen_range(-10..10))); wpos2d .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) @@ -98,47 +109,71 @@ impl Data { let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap(); Body::Humanoid(comp::humanoid::Body::random_with(rng, species)) }; + let matches_buildings = (|kind: &PlotKind| { + matches!( + kind, + PlotKind::House(_) | PlotKind::Workshop(_) | PlotKind::Plaza + ) + }) as _; + let matches_plazas = (|kind: &PlotKind| matches!(kind, PlotKind::Plaza)) as _; if good_or_evil { - for _ in 0..32 { + for _ in 0..site2.plots().len() { this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) - .with_faction(site.faction) - .with_home(site_id) - .with_personality(Personality::random(&mut rng)) - .with_profession(match rng.gen_range(0..20) { - 0 => Profession::Hunter, - 1 => Profession::Blacksmith, - 2 => Profession::Chef, - 3 => Profession::Alchemist, - 5..=8 => Profession::Farmer, - 9..=10 => Profession::Herbalist, - 11..=15 => Profession::Guard, - _ => Profession::Adventurer(rng.gen_range(0..=3)), - }), + Npc::new( + rng.gen(), + rand_wpos(&mut rng, matches_buildings), + random_humanoid(&mut rng), + ) + .with_faction(site.faction) + .with_home(site_id) + .with_personality(Personality::random(&mut rng)) + .with_profession(match rng.gen_range(0..20) { + 0 => Profession::Hunter, + 1 => Profession::Blacksmith, + 2 => Profession::Chef, + 3 => Profession::Alchemist, + 5..=8 => Profession::Farmer, + 9..=10 => Profession::Herbalist, + 11..=16 => Profession::Guard, + _ => Profession::Adventurer(rng.gen_range(0..=3)), + }), ); } } else { for _ in 0..15 { this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) - .with_personality(Personality::random_evil(&mut rng)) - .with_faction(site.faction) - .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - _ => Profession::Cultist, - }), + Npc::new( + rng.gen(), + rand_wpos(&mut rng, matches_buildings), + random_humanoid(&mut rng), + ) + .with_personality(Personality::random_evil(&mut rng)) + .with_faction(site.faction) + .with_home(site_id) + .with_profession(match rng.gen_range(0..20) { + _ => Profession::Cultist, + }), + ); + } + } + // Merchants + if good_or_evil { + for _ in 0..(site2.plots().len() / 6) + 1 { + this.npcs.create_npc( + Npc::new( + rng.gen(), + rand_wpos(&mut rng, matches_plazas), + random_humanoid(&mut rng), + ) + .with_home(site_id) + .with_personality(Personality::random_good(&mut rng)) + .with_profession(Profession::Merchant), ); } } - this.npcs.create_npc( - Npc::new(rng.gen(), rand_wpos(&mut rng), random_humanoid(&mut rng)) - .with_home(site_id) - .with_personality(Personality::random_good(&mut rng)) - .with_profession(Profession::Merchant), - ); if rng.gen_bool(0.4) { - let wpos = rand_wpos(&mut rng) + Vec3::unit_z() * 50.0; + let wpos = rand_wpos(&mut rng, matches_plazas) + Vec3::unit_z() * 50.0; let vehicle_id = this .npcs .create_vehicle(Vehicle::new(wpos, comp::body::ship::Body::DefaultAirship)); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 1e0977b681..5e5df3c3a8 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -395,13 +395,13 @@ fn travel_to_point(wpos: Vec2) -> impl Action { const WAYPOINT: f32 = 24.0; let start = ctx.npc.wpos.xy(); let diff = wpos - start; + // if diff.magnitude() > 1.0 { let n = (diff.magnitude() / WAYPOINT).max(1.0); let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); - if diff.magnitude() > 1.0 { - traverse_points(move |_| points.next()).boxed() - } else { - finish().boxed() - } + traverse_points(move |_| points.next()).boxed() + // } else { + // finish().boxed() + // } }) .debug(|| "travel to point") } @@ -525,11 +525,16 @@ fn adventure() -> impl Action { .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .map(|(site_id, _)| site_id) { + let wait_time = if matches!(ctx.npc.profession, Some(Profession::Merchant)) { + 60.0 * 15.0 + } else { + 60.0 * 3.0 + }; // Travel to the site important( travel_to_site(tgt_site) // Stop for a few minutes - .then(villager(tgt_site).repeat().stop_if(timeout(60.0 * 3.0))) + .then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .map(|_| ()) .boxed(), ) diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index a4bffb6164..bb9ffb763b 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -212,7 +212,7 @@ impl RtSim { }); if wait_until_finished { - handle.join(); + handle.join().expect("Save thread failed to join"); } self.last_saved = Some(Instant::now()); From 6035234c6eaa579ae768509196290759d2afa051 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 00:20:14 +0100 Subject: [PATCH 069/139] Removed old rtsim --- server/Cargo.toml | 2 +- server/src/chunk_generator.rs | 2 +- server/src/cmd.rs | 10 +- server/src/events/entity_manipulation.rs | 4 +- server/src/lib.rs | 23 +- server/src/rtsim/chunks.rs | 34 - server/src/rtsim/entity.rs | 1074 ----------------- server/src/{rtsim2 => rtsim}/event.rs | 2 +- server/src/rtsim/load_chunks.rs | 20 - server/src/rtsim/mod.rs | 592 ++++----- server/src/{rtsim2 => rtsim}/rule.rs | 2 +- .../rule/deplete_resources.rs | 4 +- server/src/rtsim/tick.rs | 425 +++++-- server/src/rtsim/unload_chunks.rs | 43 - server/src/rtsim2/mod.rs | 242 ---- server/src/rtsim2/tick.rs | 385 ------ server/src/state_ext.rs | 2 +- server/src/sys/terrain.rs | 4 +- 18 files changed, 542 insertions(+), 2328 deletions(-) delete mode 100644 server/src/rtsim/chunks.rs delete mode 100644 server/src/rtsim/entity.rs rename server/src/{rtsim2 => rtsim}/event.rs (90%) delete mode 100644 server/src/rtsim/load_chunks.rs rename server/src/{rtsim2 => rtsim}/rule.rs (90%) rename server/src/{rtsim2 => rtsim}/rule/deplete_resources.rs (94%) delete mode 100644 server/src/rtsim/unload_chunks.rs delete mode 100644 server/src/rtsim2/mod.rs delete mode 100644 server/src/rtsim2/tick.rs diff --git a/server/Cargo.toml b/server/Cargo.toml index 38b1ea391f..54347b0473 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -23,7 +23,7 @@ common-state = { package = "veloren-common-state", path = "../common/state" } common-systems = { package = "veloren-common-systems", path = "../common/systems" } common-net = { package = "veloren-common-net", path = "../common/net" } world = { package = "veloren-world", path = "../world" } -rtsim2 = { package = "veloren-rtsim", path = "../rtsim" } +rtsim = { package = "veloren-rtsim", path = "../rtsim" } network = { package = "veloren-network", path = "../network", features = ["metrics", "compression", "quic"], default-features = false } server-agent = {package = "veloren-server-agent", path = "agent"} diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index c5ca62be7d..c76eb6b75e 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; -use crate::{metrics::ChunkGenMetrics, rtsim2::RtSim}; +use crate::{metrics::ChunkGenMetrics, rtsim::RtSim}; use common::{ calendar::Calendar, generation::ChunkSupplement, resources::TimeOfDay, slowjob::SlowJobPool, terrain::TerrainChunk, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 82eb3345bf..3b3025e750 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1193,7 +1193,7 @@ fn handle_rtsim_tp( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; let pos = if let Some(id) = parse_cmd_args!(args, u32) { // TODO: Take some other identifier than an integer to this command. server @@ -1222,7 +1222,7 @@ fn handle_rtsim_info( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; if let Some(id) = parse_cmd_args!(args, u32) { // TODO: Take some other identifier than an integer to this command. let rtsim = server.state.ecs().read_resource::(); @@ -1271,7 +1271,7 @@ fn handle_rtsim_purge( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::RtSim; + use crate::rtsim::RtSim; if let Some(should_purge) = parse_cmd_args!(args, bool) { server .state @@ -1301,7 +1301,7 @@ fn handle_rtsim_chunk( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - use crate::rtsim2::{ChunkStates, RtSim}; + use crate::rtsim::{ChunkStates, RtSim}; let pos = position(server, target, "target")?; let chunk_key = pos.0.xy().as_::().wpos_to_cpos(); @@ -2034,7 +2034,7 @@ fn handle_kill_npcs( .get(entity) .copied() { - ecs.write_resource::() + ecs.write_resource::() .hook_rtsim_entity_delete( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index f596e94292..eff23180f2 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -7,7 +7,7 @@ use crate::{ skillset::SkillGroupKind, BuffKind, BuffSource, PhysicsState, }, - rtsim2, + rtsim, sys::terrain::SAFE_ZONE_RADIUS, Server, SpawnPoint, StateExt, }; @@ -527,7 +527,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt { state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_entity_delete( &state.ecs().read_resource::>(), state diff --git a/server/src/lib.rs b/server/src/lib.rs index 0a5ddb03b7..8946299f7e 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -30,9 +30,7 @@ pub mod metrics; pub mod persistence; mod pet; pub mod presence; -// TODO: Remove -//pub mod rtsim; -pub mod rtsim2; +pub mod rtsim; pub mod settings; pub mod state_ext; pub mod sys; @@ -566,7 +564,7 @@ impl Server { // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { - match rtsim2::RtSim::new( + match rtsim::RtSim::new( &settings.world, index.as_index_ref(), &world, @@ -721,7 +719,7 @@ impl Server { // When a resource block updates, inform rtsim if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { - ecs.write_resource::().hook_block_update( + ecs.write_resource::().hook_block_update( &ecs.read_resource::>(), ecs.read_resource::().as_index_ref(), wpos, @@ -750,7 +748,7 @@ impl Server { */ #[cfg(feature = "worldgen")] { - rtsim2::add_server_systems(dispatcher_builder); + rtsim::add_server_systems(dispatcher_builder); weather::add_server_systems(dispatcher_builder); } }, @@ -871,7 +869,7 @@ impl Server { { self.state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_entity_unload(rtsim_entity); } #[cfg(feature = "worldgen")] @@ -884,7 +882,7 @@ impl Server { { self.state .ecs() - .write_resource::() + .write_resource::() .hook_rtsim_vehicle_unload(rtsim_vehicle); } @@ -1039,7 +1037,7 @@ impl Server { let client = ecs.read_storage::(); let mut terrain = ecs.write_resource::(); #[cfg(feature = "worldgen")] - let rtsim = ecs.read_resource::(); + let rtsim = ecs.read_resource::(); #[cfg(not(feature = "worldgen"))] let rtsim = (); @@ -1222,7 +1220,7 @@ impl Server { let ecs = self.state.ecs(); let slow_jobs = ecs.read_resource::(); #[cfg(feature = "worldgen")] - let rtsim = ecs.read_resource::(); + let rtsim = ecs.read_resource::(); #[cfg(not(feature = "worldgen"))] let rtsim = (); ecs.write_resource::().generate_chunk( @@ -1467,10 +1465,7 @@ impl Drop for Server { #[cfg(feature = "worldgen")] { info!("Saving rtsim state..."); - self.state - .ecs() - .write_resource::() - .save(true); + self.state.ecs().write_resource::().save(true); } } } diff --git a/server/src/rtsim/chunks.rs b/server/src/rtsim/chunks.rs deleted file mode 100644 index 481c8cab71..0000000000 --- a/server/src/rtsim/chunks.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::*; -use ::world::util::Grid; - -pub struct Chunks { - chunks: Grid, - pub chunks_to_load: Vec>, - pub chunks_to_unload: Vec>, -} - -impl Chunks { - pub fn new(size: Vec2) -> Self { - Chunks { - chunks: Grid::populate_from(size.map(|e| e as i32), |_| Chunk { is_loaded: false }), - chunks_to_load: Vec::new(), - chunks_to_unload: Vec::new(), - } - } - - pub fn chunk(&self, key: Vec2) -> Option<&Chunk> { self.chunks.get(key) } - - pub fn size(&self) -> Vec2 { self.chunks.size().map(|e| e as u32) } - - pub fn chunk_mut(&mut self, key: Vec2) -> Option<&mut Chunk> { self.chunks.get_mut(key) } - - pub fn chunk_at(&self, pos: Vec2) -> Option<&Chunk> { - self.chunks.get(pos.map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e.floor() as i32).div_euclid(sz as i32) - })) - } -} - -pub struct Chunk { - pub is_loaded: bool, -} diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs deleted file mode 100644 index 57d5ecfcc0..0000000000 --- a/server/src/rtsim/entity.rs +++ /dev/null @@ -1,1074 +0,0 @@ -use super::*; -use common::{ - resources::Time, - rtsim::{Memory, MemoryItem}, - store::Id, - terrain::TerrainGrid, - trade, LoadoutBuilder, -}; -use enumset::*; -use rand_distr::{Distribution, Normal}; -use std::f32::consts::PI; -use tracing::warn; -use world::{ - civ::{Site, Track}, - util::RandomPerm, - IndexRef, World, -}; - -pub struct Entity { - pub is_loaded: bool, - pub pos: Vec3, - pub seed: u32, - pub last_time_ticked: f64, - pub controller: RtSimController, - pub kind: RtSimEntityKind, - pub brain: Brain, -} - -#[derive(Clone, Copy, strum::EnumIter, PartialEq, Eq)] -pub enum RtSimEntityKind { - Wanderer, - Cultist, - Villager, - TownGuard, - Merchant, - Blacksmith, - Chef, - Alchemist, - Prisoner, -} - -const BIRD_MEDIUM_ROSTER: &[comp::bird_medium::Species] = &[ - // Disallows flightless birds - comp::bird_medium::Species::Duck, - comp::bird_medium::Species::Goose, - comp::bird_medium::Species::Parrot, - comp::bird_medium::Species::Eagle, -]; - -const BIRD_LARGE_ROSTER: &[comp::bird_large::Species] = &[ - // Wyverns not included until proper introduction - comp::bird_large::Species::Phoenix, - comp::bird_large::Species::Cockatrice, - comp::bird_large::Species::Roc, -]; - -const PERM_SPECIES: u32 = 0; -const PERM_BODY: u32 = 1; -const PERM_LOADOUT: u32 = 2; -const PERM_LEVEL: u32 = 3; -const PERM_GENUS: u32 = 4; -const PERM_TRADE: u32 = 5; - -impl Entity { - pub fn rng(&self, perm: u32) -> impl Rng { RandomPerm::new(self.seed + perm) } - - pub fn loadout_rng(&self) -> impl Rng { self.rng(PERM_LOADOUT) } - - pub fn get_body(&self) -> comp::Body { - match self.kind { - RtSimEntityKind::Wanderer => { - match self.rng(PERM_GENUS).gen::() { - // we want 5% airships, 45% birds, 50% humans - x if x < 0.05 => { - comp::ship::Body::random_airship_with(&mut self.rng(PERM_BODY)).into() - }, - x if x < 0.45 => { - let species = *BIRD_MEDIUM_ROSTER - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_medium::Body::random_with(&mut self.rng(PERM_BODY), &species) - .into() - }, - x if x < 0.50 => { - let species = *BIRD_LARGE_ROSTER - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::bird_large::Body::random_with(&mut self.rng(PERM_BODY), &species) - .into() - }, - _ => { - let species = *comp::humanoid::ALL_SPECIES - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() - }, - } - }, - RtSimEntityKind::Cultist - | RtSimEntityKind::Villager - | RtSimEntityKind::TownGuard - | RtSimEntityKind::Chef - | RtSimEntityKind::Alchemist - | RtSimEntityKind::Blacksmith - | RtSimEntityKind::Prisoner - | RtSimEntityKind::Merchant => { - let species = *comp::humanoid::ALL_SPECIES - .choose(&mut self.rng(PERM_SPECIES)) - .unwrap(); - comp::humanoid::Body::random_with(&mut self.rng(PERM_BODY), &species).into() - }, - } - } - - pub fn get_trade_info( - &self, - world: &World, - index: &world::IndexOwned, - ) -> Option { - let site = match self.kind { - /* - // Travelling merchants (don't work for some reason currently) - RtSimEntityKind::Wanderer if self.rng(PERM_TRADE).gen_bool(0.5) => { - match self.brain.route { - Travel::Path { target_id, .. } => Some(target_id), - _ => None, - } - }, - */ - RtSimEntityKind::Merchant => self.brain.begin_site(), - _ => None, - }?; - - let site = world.civs().sites[site].site_tmp?; - index.sites[site].trade_information(site.id()) - } - - pub fn get_entity_config(&self) -> &str { - match self.get_body() { - comp::Body::Humanoid(_) => { - let rank = match self.rng(PERM_LEVEL).gen_range::(0..=20) { - 0..=2 => TravelerRank::Rank0, - 3..=9 => TravelerRank::Rank1, - 10..=17 => TravelerRank::Rank2, - 18.. => TravelerRank::Rank3, - }; - humanoid_config(self.kind, rank) - }, - comp::Body::BirdMedium(b) => bird_medium_config(b), - comp::Body::BirdLarge(b) => bird_large_config(b), - _ => unimplemented!(), - } - } - - /// Escape hatch for runtime creation of loadout not covered by entity - /// config. - // NOTE: Signature is part of interface of EntityInfo - pub fn get_adhoc_loadout( - &self, - ) -> fn(LoadoutBuilder, Option<&trade::SiteInformation>) -> LoadoutBuilder { - let kind = self.kind; - - if let RtSimEntityKind::Merchant = kind { - |l, trade| l.with_creator(world::site::settlement::merchant_loadout, trade) - } else { - |l, _| l - } - } - - pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { - self.brain.route = match self.brain.route.clone() { - Travel::Lost => { - match self.get_body() { - comp::Body::Humanoid(_) => { - if let Some(nearest_site_id) = world - .civs() - .sites - .iter() - .filter(|s| s.1.is_settlement() || s.1.is_castle()) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32 - }) - .map(|(id, _)| id) - { - // The path choosing code works best when Humanoids can assume they are - // in a town that has at least one path. If the Human isn't in a town - // with at least one path, we need to get them to a town that does. - let nearest_site = &world.civs().sites[nearest_site_id]; - let site_wpos = - nearest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - site_wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - if dist < 64_u32.pow(2) { - Travel::InSite { - site_id: nearest_site_id, - } - } else { - Travel::Direct { - target_id: nearest_site_id, - } - } - } else { - // Somehow no nearest site could be found - // Logically this should never happen, but if it does the rtsim entity - // will just sit tight - warn!("Nearest site could not be found"); - Travel::Lost - } - }, - comp::Body::Ship(_) => { - if let Some((target_id, site)) = world - .civs() - .sites - .iter() - .filter(|s| match self.get_body() { - comp::Body::Ship(_) => s.1.is_settlement(), - _ => s.1.is_dungeon(), - }) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000_000 } else { 0 } - }) - { - let mut rng = thread_rng(); - if let (Ok(normalpos), Ok(normaloff)) = - (Normal::new(0.0, 64.0), Normal::new(0.0, 256.0)) - { - let mut path = Vec::>::default(); - let target_site_pos = - site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e * sz as i32 + sz as i32 / 2) as f32 - }); - let offset_site_pos = - target_site_pos.map(|v| v + normalpos.sample(&mut rng)); - let offset_dir = (offset_site_pos - self.pos.xy()).normalized(); - let dist = (offset_site_pos - self.pos.xy()).magnitude(); - let midpoint = self.pos.xy() + offset_dir * (dist / 2.0); - let perp_dir = offset_dir.rotated_z(PI / 2.0); - let offset = normaloff.sample(&mut rng); - let inbetween_pos = midpoint + (perp_dir * offset); - - path.push(inbetween_pos.map(|e| e as i32)); - path.push(target_site_pos.map(|e| e as i32)); - - Travel::CustomPath { - target_id, - path, - progress: 0, - } - } else { - Travel::Direct { target_id } - } - } else { - Travel::Lost - } - }, - _ => { - if let Some(target_id) = world - .civs() - .sites - .iter() - .filter(|s| match self.get_body() { - comp::Body::Ship(_) => s.1.is_settlement(), - _ => s.1.is_dungeon(), - }) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = - wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - { - Travel::Direct { target_id } - } else { - Travel::Lost - } - }, - } - }, - Travel::InSite { site_id } => { - if !self.get_body().is_humanoid() { - // Non humanoids don't care if they start at a site - Travel::Lost - } else if let Some(target_id) = world - .civs() - .neighbors(site_id) - .filter(|sid| { - let site = world.civs().sites.get(*sid); - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist > 96_u32.pow(2) - }) - .filter(|sid| { - if let Some(last_visited) = self.brain.last_visited { - *sid != last_visited - } else { - true - } - }) - .choose(&mut thread_rng()) - { - if let Some(track_id) = world.civs().track_between(site_id, target_id) { - self.brain.last_visited = Some(site_id); - Travel::Path { - target_id, - track_id, - progress: 0, - reversed: false, - } - } else { - // This should never trigger, since neighbors returns a list of sites for - // which a track exists going from the current town. - warn!("Could not get track after selecting from neighbor list"); - self.brain.last_visited = Some(site_id); - Travel::Direct { target_id } - } - } else if let Some(target_id) = world - .civs() - .sites - .iter() - .filter(|s| s.1.is_settlement() | s.1.is_castle()) - .filter(|_| thread_rng().gen_range(0i32..4) == 0) - .min_by_key(|(_, site)| { - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - dist + if dist < 96_u32.pow(2) { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - { - // This code should only trigger when no paths out of the current town exist. - // The traveller will attempt to directly travel to another town - self.brain.last_visited = Some(site_id); - Travel::Direct { target_id } - } else { - // No paths we're picked, so stay in town. This will cause direct travel on the - // next tick. - self.brain.last_visited = Some(site_id); - Travel::InSite { site_id } - } - }, - Travel::Direct { target_id } => { - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - let wpos = site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - if dist < 64_u32.pow(2) { - Travel::InSite { site_id: target_id } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::Direct { target_id } - } - }, - Travel::CustomPath { - target_id, - path, - progress, - } => { - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - if let Some(wpos) = &path.get(progress) { - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - if dist < 16_u32.pow(2) { - if progress + 1 < path.len() { - Travel::CustomPath { - target_id, - path, - progress: progress + 1, - } - } else { - Travel::InSite { site_id: target_id } - } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::CustomPath { - target_id, - path, - progress, - } - } - } else { - Travel::Direct { target_id } - } - }, - Travel::Path { - target_id, - track_id, - progress, - reversed, - } => { - let track = &world.civs().tracks.get(track_id); - let site = &world.civs().sites[target_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - let nth = if reversed { - track.path().len() - progress - 1 - } else { - progress - }; - - if let Some(sim_pos) = track.path().iter().nth(nth) { - let chunkpos = sim_pos.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let wpos = if let Some(pathdata) = world.sim().get_nearest_path(chunkpos) { - pathdata.1.map(|e| e as i32) - } else { - chunkpos - }; - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - match dist { - d if d < 16_u32.pow(2) => { - if progress + 1 >= track.path().len() { - Travel::Direct { target_id } - } else { - Travel::Path { - target_id, - track_id, - progress: progress + 1, - reversed, - } - } - }, - d if d > 256_u32.pow(2) => { - if !reversed && progress == 0 { - Travel::Path { - target_id, - track_id, - progress: 0, - reversed: true, - } - } else { - Travel::Lost - } - }, - _ => { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) - as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.70; - Travel::Path { - target_id, - track_id, - progress, - reversed, - } - }, - } - } else { - // This code should never trigger. If we've gone outside the bounds of the - // tracks vec then a logic bug has occured. I actually had - // an off by one error that caused this to trigger and - // resulted in travellers getting stuck in towns. - warn!("Progress out of bounds while following track"); - Travel::Lost - } - }, - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } => { - // Destination site is home if raid is complete, else it is target site - let dest_site = if raid_complete { - &world.civs().sites[home_id] - } else { - &world.civs().sites[target_id] - }; - let destination_name = dest_site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - - let wpos = dest_site.center.map2(TerrainChunk::RECT_SIZE, |e, sz| { - e * sz as i32 + sz as i32 / 2 - }); - let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - - // Once at site, stay for a bit, then move to other site - if dist < 128_u32.pow(2) { - // If time_to_move is not set yet, use current time, ceiling to nearest multiple - // of 100, and then add another 100. - let time_to_move = if time_to_move.is_none() { - // Time increment is how long raiders stay at a site about. Is longer for - // home site and shorter for target site. - let time_increment = if raid_complete { 600.0 } else { 60.0 }; - Some((time.0 / time_increment).ceil() * time_increment + time_increment) - } else { - time_to_move - }; - - // If the time has come to move, flip raid bool - if time_to_move.map_or(false, |t| time.0 > t) { - Travel::DirectRaid { - target_id, - home_id, - raid_complete: !raid_complete, - time_to_move: None, - } - } else { - let theta = (time.0 / 30.0).floor() as f32 * self.seed as f32; - // Otherwise wander around site (or "plunder" if target site) - let travel_to = - wpos.map(|e| e as f32) + Vec2::new(theta.cos(), theta.sin()) * 100.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.75; - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } - } - } else { - let travel_to = self.pos.xy() - + Vec3::from( - (wpos.map(|e| e as f32 + 0.5) - self.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero), - ) * 64.0; - let travel_to_alt = world - .sim() - .get_alt_approx(travel_to.map(|e| e as i32)) - .unwrap_or(0.0) as i32; - let travel_to = terrain - .find_space(Vec3::new( - travel_to.x as i32, - travel_to.y as i32, - travel_to_alt, - )) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, 0.0); - - self.controller.travel_to = Some((travel_to, destination_name)); - self.controller.speed_factor = 0.90; - Travel::DirectRaid { - target_id, - home_id, - raid_complete, - time_to_move, - } - } - }, - Travel::Idle => Travel::Idle, - }; - - // Forget old memories - self.brain - .memories - .retain(|memory| memory.time_to_forget > time.0); - } -} - -#[derive(Clone, Debug)] -pub enum Travel { - // The initial state all entities start in, and a fallback for when a state has stopped making - // sense. Non humanoids will always revert to this state after reaching their goal since the - // current site they are in doesn't change their behavior. - Lost, - // When an rtsim entity reaches a site it will switch to this state to restart their - // pathfinding from the beginning. Useful when the entity needs to know its current site to - // decide their next target. - InSite { - site_id: Id, - }, - // Move directly to a target site. Used by birds mostly, but also by humands who cannot find a - // path. - Direct { - target_id: Id, - }, - // Follow a custom path to reach the destination. Airships define a custom path to reduce the - // chance of collisions. - CustomPath { - target_id: Id, - path: Vec>, - progress: usize, - }, - // Follow a track defined in the track_map to reach a site. Humanoids do this whenever - // possible. - Path { - target_id: Id, - track_id: Id, - progress: usize, - reversed: bool, - }, - // Move directly towards a target site, then head back to a home territory - DirectRaid { - target_id: Id, - home_id: Id, - raid_complete: bool, - time_to_move: Option, - }, - // For testing purposes - Idle, -} - -// Based on https://en.wikipedia.org/wiki/Big_Five_personality_traits -pub struct PersonalityBase { - openness: u8, - conscientiousness: u8, - extraversion: u8, - agreeableness: u8, - neuroticism: u8, -} - -impl PersonalityBase { - /* All thresholds here are arbitrary "seems right" values. The goal is for - * most NPCs to have some kind of distinguishing trait - something - * interesting about them. We want to avoid Joe Averages. But we also - * don't want everyone to be completely weird. - */ - pub fn to_personality(&self) -> Personality { - let will_ambush = self.agreeableness < Personality::LOW_THRESHOLD - && self.conscientiousness < Personality::LOW_THRESHOLD; - let mut chat_traits: EnumSet = EnumSet::new(); - if self.openness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Open); - if self.neuroticism < Personality::MID { - chat_traits.insert(PersonalityTrait::Adventurous); - } - } else if self.openness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Closed); - } - if self.conscientiousness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Conscientious); - if self.agreeableness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Busybody); - } - } else if self.conscientiousness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Unconscientious); - } - if self.extraversion > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Extroverted); - } else if self.extraversion < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Introverted); - } - if self.agreeableness > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Agreeable); - if self.extraversion > Personality::MID { - chat_traits.insert(PersonalityTrait::Sociable); - } - } else if self.agreeableness < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Disagreeable); - } - if self.neuroticism > Personality::HIGH_THRESHOLD { - chat_traits.insert(PersonalityTrait::Neurotic); - if self.openness > Personality::LITTLE_HIGH { - chat_traits.insert(PersonalityTrait::Seeker); - } - if self.agreeableness > Personality::LITTLE_HIGH { - chat_traits.insert(PersonalityTrait::Worried); - } - if self.extraversion < Personality::LITTLE_LOW { - chat_traits.insert(PersonalityTrait::SadLoner); - } - } else if self.neuroticism < Personality::LOW_THRESHOLD { - chat_traits.insert(PersonalityTrait::Stable); - } - Personality { - personality_traits: chat_traits, - will_ambush, - } - } -} - -pub struct Personality { - pub personality_traits: EnumSet, - pub will_ambush: bool, -} - -#[derive(EnumSetType)] -pub enum PersonalityTrait { - Open, - Adventurous, - Closed, - Conscientious, - Busybody, - Unconscientious, - Extroverted, - Introverted, - Agreeable, - Sociable, - Disagreeable, - Neurotic, - Seeker, - Worried, - SadLoner, - Stable, -} - -impl Personality { - pub const HIGH_THRESHOLD: u8 = Self::MAX - Self::LOW_THRESHOLD; - pub const LITTLE_HIGH: u8 = Self::MID + (Self::MAX - Self::MIN) / 20; - pub const LITTLE_LOW: u8 = Self::MID - (Self::MAX - Self::MIN) / 20; - pub const LOW_THRESHOLD: u8 = (Self::MAX - Self::MIN) / 5 * 2 + Self::MIN; - const MAX: u8 = 100; - pub const MID: u8 = (Self::MAX - Self::MIN) / 2; - const MIN: u8 = 0; - - pub fn random_chat_trait(&self, rng: &mut impl Rng) -> Option { - self.personality_traits.into_iter().choose(rng) - } - - pub fn random_trait_value_bounded(rng: &mut impl Rng, min: u8, max: u8) -> u8 { - let max_third = max / 3; - let min_third = min / 3; - rng.gen_range(min_third..=max_third) - + rng.gen_range(min_third..=max_third) - + rng.gen_range((min - 2 * min_third)..=(max - 2 * max_third)) - } - - pub fn random_trait_value(rng: &mut impl Rng) -> u8 { - Self::random_trait_value_bounded(rng, Self::MIN, Self::MAX) - } - - pub fn random(rng: &mut impl Rng) -> Personality { - let mut random_value = - || rng.gen_range(0..=33) + rng.gen_range(0..=34) + rng.gen_range(0..=33); - let base = PersonalityBase { - openness: random_value(), - conscientiousness: random_value(), - extraversion: random_value(), - agreeableness: random_value(), - neuroticism: random_value(), - }; - base.to_personality() - } -} - -pub struct Brain { - pub begin: Option>, - pub tgt: Option>, - pub route: Travel, - pub last_visited: Option>, - pub memories: Vec, - pub personality: Personality, -} - -impl Brain { - pub fn idle(rng: &mut impl Rng) -> Self { - Self { - begin: None, - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn raid(home_id: Id, target_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: None, - tgt: None, - route: Travel::DirectRaid { - target_id, - home_id, - raid_complete: false, - time_to_move: None, - }, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn villager(home_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn merchant(home_id: Id, rng: &mut impl Rng) -> Self { - // Merchants are generally extraverted and agreeable - let extraversion_bias = (Personality::MAX - Personality::MIN) / 10 * 3; - let extraversion = - Personality::random_trait_value_bounded(rng, extraversion_bias, Personality::MAX); - let agreeableness_bias = extraversion_bias / 2; - let agreeableness = - Personality::random_trait_value_bounded(rng, agreeableness_bias, Personality::MAX); - let personality_base = PersonalityBase { - openness: Personality::random_trait_value(rng), - conscientiousness: Personality::random_trait_value(rng), - extraversion, - agreeableness, - neuroticism: Personality::random_trait_value(rng), - }; - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: personality_base.to_personality(), - } - } - - pub fn town_guard(home_id: Id, rng: &mut impl Rng) -> Self { - Self { - begin: Some(home_id), - tgt: None, - route: Travel::Idle, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(rng), - } - } - - pub fn begin_site(&self) -> Option> { self.begin } - - pub fn add_memory(&mut self, memory: Memory) { self.memories.push(memory); } - - pub fn forget_enemy(&mut self, to_forget: &str) { - self.memories.retain(|memory| { - !matches!( - &memory.item, - MemoryItem::CharacterFight {name, ..} if name == to_forget) - }) - } - - pub fn remembers_mood(&self) -> bool { - self.memories - .iter() - .any(|memory| matches!(&memory.item, MemoryItem::Mood { .. })) - } - - pub fn set_mood(&mut self, memory: Memory) { - if let MemoryItem::Mood { .. } = memory.item { - if self.remembers_mood() { - while let Some(position) = self - .memories - .iter() - .position(|mem| matches!(&mem.item, MemoryItem::Mood { .. })) - { - self.memories.remove(position); - } - } - self.add_memory(memory); - }; - } - - pub fn get_mood(&self) -> Option<&Memory> { - self.memories - .iter() - .find(|memory| matches!(&memory.item, MemoryItem::Mood { .. })) - } - - pub fn remembers_character(&self, name_to_remember: &str) -> bool { - self.memories.iter().any(|memory| { - matches!( - &memory.item, - MemoryItem::CharacterInteraction { name, .. } if name == name_to_remember) - }) - } - - pub fn remembers_fight_with_character(&self, name_to_remember: &str) -> bool { - self.memories.iter().any(|memory| { - matches!( - &memory.item, - MemoryItem::CharacterFight { name, .. } if name == name_to_remember) - }) - } -} - -#[derive(strum::EnumIter)] -enum TravelerRank { - Rank0, - Rank1, - Rank2, - Rank3, -} - -fn humanoid_config(kind: RtSimEntityKind, rank: TravelerRank) -> &'static str { - match kind { - RtSimEntityKind::Cultist => "common.entity.dungeon.tier-5.cultist", - RtSimEntityKind::Wanderer => match rank { - TravelerRank::Rank0 => "common.entity.world.traveler0", - TravelerRank::Rank1 => "common.entity.world.traveler1", - TravelerRank::Rank2 => "common.entity.world.traveler2", - TravelerRank::Rank3 => "common.entity.world.traveler3", - }, - RtSimEntityKind::Villager => "common.entity.village.villager", - RtSimEntityKind::TownGuard => "common.entity.village.guard", - RtSimEntityKind::Merchant => "common.entity.village.merchant", - RtSimEntityKind::Blacksmith => "common.entity.village.blacksmith", - RtSimEntityKind::Chef => "common.entity.village.chef", - RtSimEntityKind::Alchemist => "common.entity.village.alchemist", - RtSimEntityKind::Prisoner => "common.entity.dungeon.sea_chapel.prisoner", - } -} - -fn bird_medium_config(body: comp::bird_medium::Body) -> &'static str { - match body.species { - comp::bird_medium::Species::Duck => "common.entity.wild.peaceful.duck", - comp::bird_medium::Species::Chicken => "common.entity.wild.peaceful.chicken", - comp::bird_medium::Species::Goose => "common.entity.wild.peaceful.goose", - comp::bird_medium::Species::Peacock => "common.entity.wild.peaceful.peacock", - comp::bird_medium::Species::Eagle => "common.entity.wild.peaceful.eagle", - comp::bird_medium::Species::SnowyOwl => "common.entity.wild.peaceful.snowy_owl", - comp::bird_medium::Species::HornedOwl => "common.entity.wild.peaceful.horned_owl", - comp::bird_medium::Species::Parrot => "common.entity.wild.peaceful.parrot", - _ => unimplemented!(), - } -} - -fn bird_large_config(body: comp::bird_large::Body) -> &'static str { - match body.species { - comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", - comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", - comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", - // Wildcard match used here as there is an array above - // which limits what species are used - _ => unimplemented!(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use common::generation::EntityInfo; - use strum::IntoEnumIterator; - - // Brief, Incomplete and Mostly Wrong Test that all entity configs do exist. - // - // NOTE: Doesn't checks for ships, because we don't produce entity configs - // for them yet. - #[test] - fn test_entity_configs() { - let dummy_pos = Vec3::new(0.0, 0.0, 0.0); - let mut dummy_rng = thread_rng(); - // Bird Large test - for bird_large_species in BIRD_LARGE_ROSTER { - let female_body = comp::bird_large::Body { - species: *bird_large_species, - body_type: comp::bird_large::BodyType::Female, - }; - let male_body = comp::bird_large::Body { - species: *bird_large_species, - body_type: comp::bird_large::BodyType::Male, - }; - - let female_config = bird_large_config(female_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(female_config, &mut dummy_rng)); - let male_config = bird_large_config(male_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(male_config, &mut dummy_rng)); - } - // Bird Medium test - for bird_med_species in BIRD_MEDIUM_ROSTER { - let female_body = comp::bird_medium::Body { - species: *bird_med_species, - body_type: comp::bird_medium::BodyType::Female, - }; - let male_body = comp::bird_medium::Body { - species: *bird_med_species, - body_type: comp::bird_medium::BodyType::Male, - }; - - let female_config = bird_medium_config(female_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(female_config, &mut dummy_rng)); - let male_config = bird_medium_config(male_body); - drop(EntityInfo::at(dummy_pos).with_asset_expect(male_config, &mut dummy_rng)); - } - // Humanoid test - for kind in RtSimEntityKind::iter() { - for rank in TravelerRank::iter() { - let config = humanoid_config(kind, rank); - drop(EntityInfo::at(dummy_pos).with_asset_expect(config, &mut dummy_rng)); - } - } - } -} diff --git a/server/src/rtsim2/event.rs b/server/src/rtsim/event.rs similarity index 90% rename from server/src/rtsim2/event.rs rename to server/src/rtsim/event.rs index 958bf458fa..50261c98ab 100644 --- a/server/src/rtsim2/event.rs +++ b/server/src/rtsim/event.rs @@ -1,5 +1,5 @@ use common::terrain::Block; -use rtsim2::Event; +use rtsim::Event; use vek::*; #[derive(Clone)] diff --git a/server/src/rtsim/load_chunks.rs b/server/src/rtsim/load_chunks.rs deleted file mode 100644 index 301037cdae..0000000000 --- a/server/src/rtsim/load_chunks.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::*; -use common::event::{EventBus, ServerEvent}; -use common_ecs::{Job, Origin, Phase, System}; -use specs::{Read, WriteExpect}; - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = (Read<'a, EventBus>, WriteExpect<'a, RtSim>); - - const NAME: &'static str = "rtsim::load_chunks"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run(_job: &mut Job, (_server_event_bus, mut rtsim): Self::SystemData) { - for _chunk in std::mem::take(&mut rtsim.chunks.chunks_to_load) { - // TODO - } - } -} diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 21a6fc0365..6b8f50e755 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -1,414 +1,242 @@ -#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out +pub mod event; +pub mod rule; +pub mod tick; -mod chunks; -pub(crate) mod entity; -mod load_chunks; -mod tick; -mod unload_chunks; - -use crate::rtsim::entity::{Personality, Travel}; - -use self::chunks::Chunks; use common::{ - comp, - rtsim::{Memory, RtSimController, RtSimEntity, RtSimId}, - terrain::TerrainChunk, + grid::Grid, + rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, + slowjob::SlowJobPool, + terrain::{Block, TerrainChunk}, vol::RectRasterableVol, }; use common_ecs::{dispatch, System}; -use common_state::State; -use rand::prelude::*; -use slab::Slab; +use enum_map::EnumMap; +use rtsim::{ + data::{npc::SimulationMode, Data, ReadError}, + event::{OnDeath, OnSetup}, + rule::Rule, + RtState, +}; use specs::{DispatcherBuilder, WorldExt}; +use std::{ + error::Error, + fs::{self, File}, + io::{self, Write}, + path::PathBuf, + sync::Arc, + time::Instant, +}; +use tracing::{debug, error, info, warn}; use vek::*; - -pub use self::entity::{Brain, Entity, RtSimEntityKind}; +use world::{IndexRef, World}; pub struct RtSim { - tick: u64, - chunks: Chunks, - entities: Slab, + file_path: PathBuf, + last_saved: Option, + state: RtState, } impl RtSim { - pub fn new(world_chunk_size: Vec2) -> Self { - Self { - tick: 0, - chunks: Chunks::new(world_chunk_size), - entities: Slab::new(), - } + pub fn new( + settings: &WorldSettings, + index: IndexRef, + world: &World, + data_dir: PathBuf, + ) -> Result { + let file_path = Self::get_file_path(data_dir); + + info!("Looking for rtsim data at {}...", file_path.display()); + let data = 'load: { + if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim data found. Attempting to load..."); + match Data::from_reader(io::BufReader::new(file)) { + Ok(data) => { + info!("Rtsim data loaded."); + if data.should_purge { + warn!( + "The should_purge flag was set on the rtsim data, \ + generating afresh" + ); + } else { + break 'load data; + } + }, + Err(e) => { + error!("Rtsim data failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!( + "Failed rtsim data was moved to {}", + backup_path.display() + ); + info!("A fresh rtsim data will now be generated."); + break; + } + i += 1; + } + }, + } + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => { + info!("No rtsim data found. Generating from world...") + }, + Err(e) => return Err(e.into()), + } + } else { + warn!( + "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \ + overwritten)." + ); + } + + let data = Data::generate(settings, &world, index); + info!("Rtsim data generated."); + data + }; + + let mut this = Self { + last_saved: None, + state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from( + world.sim().get_size().as_(), + |_| None, + ))), + file_path, + }; + + rule::start_rules(&mut this.state); + + this.state.emit(OnSetup, world, index); + + Ok(this) } - pub fn hook_load_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.chunks.chunk_mut(key) { - if !chunk.is_loaded { - chunk.is_loaded = true; - self.chunks.chunks_to_load.push(key); - } + fn get_file_path(mut data_dir: PathBuf) -> PathBuf { + let mut path = std::env::var("VELOREN_RTSIM") + .map(PathBuf::from) + .unwrap_or_else(|_| { + data_dir.push("rtsim"); + data_dir + }); + path.push("data.dat"); + path + } + + pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { + if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { + *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk) = self.chunks.chunk_mut(key) { - if chunk.is_loaded { - chunk.is_loaded = false; - self.chunks.chunks_to_unload.push(key); + if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { + *chunk_state = None; + } + } + + pub fn hook_block_update( + &mut self, + world: &World, + index: IndexRef, + wpos: Vec3, + old: Block, + new: Block, + ) { + self.state + .emit(event::OnBlockChange { wpos, old, new }, world, index); + } + + pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { + if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { + npc.mode = SimulationMode::Simulated; + } + } + + pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { + if let Some(vehicle) = self.state.data_mut().npcs.vehicles.get_mut(entity.0) { + vehicle.mode = SimulationMode::Simulated; + } + } + + pub fn hook_rtsim_entity_delete( + &mut self, + world: &World, + index: IndexRef, + entity: RtSimEntity, + ) { + // Should entity deletion be death? They're not exactly the same thing... + self.state.emit(OnDeath { npc_id: entity.0 }, world, index); + self.state.data_mut().npcs.remove(entity.0); + } + + pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { + info!("Saving rtsim data..."); + let file_path = self.file_path.clone(); + let data = self.state.data().clone(); + debug!("Starting rtsim data save job..."); + // TODO: Use slow job + // slowjob_pool.spawn("RTSIM_SAVE", move || { + let handle = std::thread::spawn(move || { + let tmp_file_name = "data_tmp.dat"; + if let Err(e) = file_path + .parent() + .map(|dir| { + fs::create_dir_all(dir)?; + // We write to a temporary file and then rename to avoid corruption. + Ok(dir.join(tmp_file_name)) + }) + .unwrap_or_else(|| Ok(tmp_file_name.into())) + .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) + .map_err(|e: io::Error| Box::new(e) as Box) + .and_then(|(mut file, tmp_file_path)| { + debug!("Writing rtsim data to file..."); + data.write_to(io::BufWriter::new(&mut file))?; + file.flush()?; + drop(file); + fs::rename(tmp_file_path, file_path)?; + debug!("Rtsim data saved."); + Ok(()) + }) + { + error!("Saving rtsim data failed: {}", e); } + }); + + if wait_until_finished { + handle.join().expect("Save thread failed to join"); } + + self.last_saved = Some(Instant::now()); } - pub fn assimilate_entity(&mut self, entity: RtSimId) { - // tracing::info!("Assimilated rtsim entity {}", entity); - self.entities.get_mut(entity).map(|e| e.is_loaded = false); + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.state.data().nature.get_chunk_resources(key) } - pub fn reify_entity(&mut self, entity: RtSimId) { - // tracing::info!("Reified rtsim entity {}", entity); - self.entities.get_mut(entity).map(|e| e.is_loaded = true); - } + pub fn state(&self) -> &RtState { &self.state } - pub fn update_entity(&mut self, entity: RtSimId, pos: Vec3) { - self.entities.get_mut(entity).map(|e| e.pos = pos); + pub fn set_should_purge(&mut self, should_purge: bool) { + self.state.data_mut().should_purge = should_purge; } +} - pub fn destroy_entity(&mut self, entity: RtSimId) { - // tracing::info!("Destroyed rtsim entity {}", entity); - self.entities.remove(entity); - } +pub struct ChunkStates(pub Grid>); - pub fn get_entity(&self, entity: RtSimId) -> Option<&Entity> { self.entities.get(entity) } - - pub fn insert_entity_memory(&mut self, entity: RtSimId, memory: Memory) { - self.entities - .get_mut(entity) - .map(|entity| entity.brain.add_memory(memory)); - } - - pub fn forget_entity_enemy(&mut self, entity: RtSimId, name: &str) { - if let Some(entity) = self.entities.get_mut(entity) { - entity.brain.forget_enemy(name); - } - } - - pub fn set_entity_mood(&mut self, entity: RtSimId, memory: Memory) { - self.entities - .get_mut(entity) - .map(|entity| entity.brain.set_mood(memory)); - } +pub struct LoadedChunkState { + // The maximum possible number of each resource in this chunk + pub max_res: EnumMap, } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { - dispatch::(dispatch_builder, &[]); - dispatch::(dispatch_builder, &[&unload_chunks::Sys::sys_name()]); - dispatch::(dispatch_builder, &[ - &load_chunks::Sys::sys_name(), - &unload_chunks::Sys::sys_name(), - ]); -} - -pub fn init( - state: &mut State, - #[cfg(feature = "worldgen")] world: &world::World, - #[cfg(feature = "worldgen")] index: world::IndexRef, -) { - #[cfg(feature = "worldgen")] - let mut rtsim = RtSim::new(world.sim().get_size()); - #[cfg(not(feature = "worldgen"))] - let mut rtsim = RtSim::new(Vec2::new(40, 40)); - - // TODO: Determine number of rtsim entities based on things like initial site - // populations rather than world size - #[cfg(feature = "worldgen")] - { - for _ in 0..world.sim().get_size().product() / 400 { - let pos = rtsim - .chunks - .size() - .map2(TerrainChunk::RECT_SIZE, |sz, chunk_sz| { - thread_rng().gen_range(0..sz * chunk_sz) as i32 - }); - - rtsim.entities.insert(Entity { - is_loaded: false, - pos: Vec3::from(pos.map(|e| e as f32)), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Wanderer, - brain: Brain { - begin: None, - tgt: None, - route: Travel::Lost, - last_visited: None, - memories: Vec::new(), - personality: Personality::random(&mut thread_rng()), - }, - }); - } - for (site_id, site) in world - .civs() - .sites - .iter() - .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) - { - use world::site::SiteKind; - match &site.kind { - #[allow(clippy::single_match)] - SiteKind::Dungeon(dungeon) => match dungeon.dungeon_difficulty() { - Some(5) => { - let pos = site.get_origin(); - if let Some(nearest_village) = world - .civs() - .sites - .iter() - .filter(|(_, site)| site.is_settlement()) - .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - wpos.map(|e| e as f32) - .distance_squared(pos.map(|x| x as f32)) - as u32 - }) - .map(|(id, _)| id) - { - for _ in 0..25 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: Vec3::from(pos.map(|e| e as f32)), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Cultist, - brain: Brain::raid(site_id, nearest_village, &mut thread_rng()), - }); - } - } - }, - _ => {}, - }, - SiteKind::Refactor(site2) => { - // villagers - for _ in 0..site.economy.population().min(site2.plots().len() as f32) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(plot.root_tile()) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Villager, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - - // guards - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::TownGuard, - brain: Brain::town_guard(site_id, &mut thread_rng()), - }); - } - - // merchants - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::CliffTown(site2) => { - for _ in 0..(site2.plazas().len() as f32 * 1.5) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::SavannahPit(site2) => { - for _ in 0..4 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .filter(|plot| { - matches!(plot.kind(), world::site2::PlotKind::SavannahPit(_)) - }) - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos( - plot.root_tile() - + Vec2::new( - thread_rng().gen_range(-5..5), - thread_rng().gen_range(-5..5), - ), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::DesertCity(site2) => { - // villagers - for _ in 0..(site2.plazas().len() as f32 * 1.5) as usize { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(plot.root_tile()) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Villager, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - - // guards - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::TownGuard, - brain: Brain::town_guard(site_id, &mut thread_rng()), - }); - } - - // merchants - for _ in 0..site2.plazas().len() { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plazas() - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |p| { - site2.tile_center_wpos(site2.plot(p).root_tile()) - + Vec2::new( - thread_rng().gen_range(-8..9), - thread_rng().gen_range(-8..9), - ) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Merchant, - brain: Brain::merchant(site_id, &mut thread_rng()), - }); - } - }, - SiteKind::ChapelSite(site2) => { - // prisoners - for _ in 0..10 { - rtsim.entities.insert(Entity { - is_loaded: false, - pos: site2 - .plots() - .filter(|plot| { - matches!(plot.kind(), world::site2::PlotKind::SeaChapel(_)) - }) - .choose(&mut thread_rng()) - .map_or(site.get_origin(), |plot| { - site2.tile_center_wpos(Vec2::new( - plot.root_tile().x, - plot.root_tile().y + 4, - )) - }) - .with_z(0) - .map(|e| e as f32), - seed: thread_rng().gen(), - controller: RtSimController::default(), - last_time_ticked: 0.0, - kind: RtSimEntityKind::Prisoner, - brain: Brain::villager(site_id, &mut thread_rng()), - }); - } - }, - _ => {}, - } - } - } - - state.ecs_mut().insert(rtsim); - state.ecs_mut().register::(); - tracing::info!("Initiated real-time world simulation"); + dispatch::(dispatch_builder, &[]); } diff --git a/server/src/rtsim2/rule.rs b/server/src/rtsim/rule.rs similarity index 90% rename from server/src/rtsim2/rule.rs rename to server/src/rtsim/rule.rs index 9b73ea9165..2c499c1c60 100644 --- a/server/src/rtsim2/rule.rs +++ b/server/src/rtsim/rule.rs @@ -1,6 +1,6 @@ pub mod deplete_resources; -use rtsim2::RtState; +use rtsim::RtState; use tracing::info; pub fn start_rules(rtstate: &mut RtState) { diff --git a/server/src/rtsim2/rule/deplete_resources.rs b/server/src/rtsim/rule/deplete_resources.rs similarity index 94% rename from server/src/rtsim2/rule/deplete_resources.rs rename to server/src/rtsim/rule/deplete_resources.rs index ebea23e721..6414296012 100644 --- a/server/src/rtsim2/rule/deplete_resources.rs +++ b/server/src/rtsim/rule/deplete_resources.rs @@ -1,9 +1,9 @@ -use crate::rtsim2::{event::OnBlockChange, ChunkStates}; +use crate::rtsim::{event::OnBlockChange, ChunkStates}; use common::{ terrain::{CoordinateConversions, TerrainChunk}, vol::RectRasterableVol, }; -use rtsim2::{RtState, Rule, RuleError}; +use rtsim::{RtState, Rule, RuleError}; pub struct DepleteResources; diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 6b6571904f..63913c4267 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,29 +3,190 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp, - event::{EventBus, ServerEvent}, + comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, + event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, - resources::{DeltaTime, Time}, - terrain::TerrainGrid, + lottery::LootSpec, + resources::{DeltaTime, Time, TimeOfDay}, + rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, + slowjob::SlowJobPool, + terrain::CoordinateConversions, + trade::{Good, SiteInformation}, + LoadoutBuilder, SkillSetBuilder, }; use common_ecs::{Job, Origin, Phase, System}; +use rtsim::data::{ + npc::{Profession, SimulationMode}, + Actor, Npc, Sites, +}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; +use world::site::settlement::trader_loadout; + +fn humanoid_config(profession: &Profession) -> &'static str { + match profession { + Profession::Farmer => "common.entity.village.farmer", + Profession::Hunter => "common.entity.village.hunter", + Profession::Herbalist => "common.entity.village.herbalist", + Profession::Captain => "common.entity.village.captain", + Profession::Merchant => "common.entity.village.merchant", + Profession::Guard => "common.entity.village.guard", + Profession::Adventurer(rank) => match rank { + 0 => "common.entity.world.traveler0", + 1 => "common.entity.world.traveler1", + 2 => "common.entity.world.traveler2", + 3 => "common.entity.world.traveler3", + _ => panic!("Not a valid adventurer rank"), + }, + Profession::Blacksmith => "common.entity.village.blacksmith", + Profession::Chef => "common.entity.village.chef", + Profession::Alchemist => "common.entity.village.alchemist", + Profession::Pirate => "common.entity.spot.pirate", + Profession::Cultist => "common.entity.dungeon.tier-5.cultist", + } +} + +fn loadout_default(loadout: LoadoutBuilder, _economy: Option<&SiteInformation>) -> LoadoutBuilder { + loadout +} + +fn merchant_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |_| true) +} + +fn farmer_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn herbalist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Ingredients) + }) +} + +fn chef_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) +} + +fn blacksmith_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Tools | Good::Armor) + }) +} + +fn alchemist_loadout( + loadout_builder: LoadoutBuilder, + economy: Option<&SiteInformation>, +) -> LoadoutBuilder { + trader_loadout(loadout_builder, economy, |good| { + matches!(good, Good::Potions) + }) +} + +fn profession_extra_loadout( + profession: Option<&Profession>, +) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { + match profession { + Some(Profession::Merchant) => merchant_loadout, + Some(Profession::Farmer) => farmer_loadout, + Some(Profession::Herbalist) => herbalist_loadout, + Some(Profession::Chef) => chef_loadout, + Some(Profession::Blacksmith) => blacksmith_loadout, + Some(Profession::Alchemist) => alchemist_loadout, + _ => loadout_default, + } +} + +fn profession_agent_mark(profession: Option<&Profession>) -> Option { + match profession { + Some( + Profession::Merchant + | Profession::Farmer + | Profession::Herbalist + | Profession::Chef + | Profession::Blacksmith + | Profession::Alchemist, + ) => Some(comp::agent::Mark::Merchant), + Some(Profession::Guard) => Some(comp::agent::Mark::Guard), + _ => None, + } +} + +fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { + let pos = comp::Pos(npc.wpos); + + let mut rng = npc.rng(3); + if let Some(ref profession) = npc.profession { + let economy = npc.home.and_then(|home| { + let site = sites.get(home)?.world_site?; + index.sites.get(site).trade_information(site.id()) + }); + + let config_asset = humanoid_config(profession); + + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); + EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(config_asset), &mut rng) + .with_alignment(if matches!(profession, Profession::Cultist) { + comp::Alignment::Enemy + } else { + comp::Alignment::Npc + }) + .with_economy(economy.as_ref()) + .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) + .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) + } else { + let config_asset = match npc.body { + Body::BirdLarge(body) => match body.species { + comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", + comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", + comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", + // Wildcard match used here as there is an array above + // which limits what species are used + _ => unimplemented!(), + }, + _ => unimplemented!(), + }; + let entity_config = EntityConfig::from_asset_expect_owned(config_asset) + .with_body(BodyBuilder::Exact(npc.body)); + + EntityInfo::at(pos.0) + .with_entity_config(entity_config, Some(config_asset), &mut rng) + .with_alignment(comp::Alignment::Wild) + } +} #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( - Read<'a, Time>, Read<'a, DeltaTime>, + Read<'a, Time>, + Read<'a, TimeOfDay>, Read<'a, EventBus>, WriteExpect<'a, RtSim>, - ReadExpect<'a, TerrainGrid>, ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, + ReadExpect<'a, SlowJobPool>, ReadStorage<'a, comp::Pos>, ReadStorage<'a, RtSimEntity>, + ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, ); @@ -36,114 +197,118 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, ( + dt, time, - _dt, + time_of_day, server_event_bus, mut rtsim, - terrain, world, index, + slow_jobs, positions, rtsim_entities, + rtsim_vehicles, mut agents, ): Self::SystemData, ) { + let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - rtsim.tick += 1; - // Update unloaded rtsim entities, in groups at a time - const TICK_STAGGER: usize = 30; - let entities_per_iteration = rtsim.entities.len() / TICK_STAGGER; - let mut to_reify = Vec::new(); - for (id, entity) in rtsim - .entities - .iter_mut() - .skip((rtsim.tick as usize % TICK_STAGGER) * entities_per_iteration) - .take(entities_per_iteration) - .filter(|(_, e)| !e.is_loaded) + rtsim.state.data_mut().time_of_day = *time_of_day; + rtsim + .state + .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); + + if rtsim + .last_saved + .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { - // Calculating dt ourselves because the dt provided to this fn was since the - // last frame, not since the last iteration that these entities acted - let dt = (time.0 - entity.last_time_ticked) as f32; - entity.last_time_ticked = time.0; + // TODO: Use slow jobs + let _ = slow_jobs; + rtsim.save(/* &slow_jobs, */ false); + } - if rtsim - .chunks - .chunk_at(entity.pos.xy()) - .map(|c| c.is_loaded) - .unwrap_or(false) + let chunk_states = rtsim.state.resource::(); + let data = &mut *rtsim.state.data_mut(); + + for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { + let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); + + if matches!(vehicle.mode, SimulationMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) { - to_reify.push(id); - } else { - // Simulate behaviour - if let Some(travel_to) = &entity.controller.travel_to { - // Move towards target at approximate character speed - entity.pos += Vec3::from( - (travel_to.0.xy() - entity.pos.xy()) - .try_normalized() - .unwrap_or_else(Vec2::zero) - * entity.get_body().max_speed_approx() - * entity.controller.speed_factor, - ) * dt; - } + vehicle.mode = SimulationMode::Loaded; - if let Some(alt) = world - .sim() - .get_alt_approx(entity.pos.xy().map(|e| e.floor() as i32)) - { - entity.pos.z = alt; - } + let mut actor_info = |actor: Actor| { + let npc_id = actor.npc()?; + let npc = data.npcs.npcs.get_mut(npc_id)?; + if matches!(npc.mode, SimulationMode::Simulated) { + npc.mode = SimulationMode::Loaded; + let entity_info = + get_npc_entity_info(npc, &data.sites, index.as_index_ref()); + + Some(match NpcData::from_entity_info(entity_info) { + NpcData::Data { + pos: _, + stats, + skill_set, + health, + poise, + inventory, + agent, + body, + alignment, + scale, + loot, + } => NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), + // EntityConfig can't represent Waypoints at all + // as of now, and if someone will try to spawn + // rtsim waypoint it is definitely error. + NpcData::Waypoint(_) => unimplemented!(), + }) + } else { + error!("Npc is loaded but vehicle is unloaded"); + None + } + }; + + emitter.emit(ServerEvent::CreateShip { + pos: comp::Pos(vehicle.wpos), + ship: vehicle.body, + // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), + rtsim_entity: Some(RtSimVehicle(vehicle_id)), + driver: vehicle.driver.and_then(&mut actor_info), + passangers: vehicle + .riders + .iter() + .copied() + .filter(|actor| vehicle.driver != Some(*actor)) + .filter_map(actor_info) + .collect(), + }); } - entity.tick(&time, &terrain, &world, &index.as_index_ref()); } - // Tick entity AI each time if it's loaded - for (_, entity) in rtsim.entities.iter_mut().filter(|(_, e)| e.is_loaded) { - entity.last_time_ticked = time.0; - entity.tick(&time, &terrain, &world, &index.as_index_ref()); - } + for (npc_id, npc) in data.npcs.npcs.iter_mut() { + let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); - let mut server_emitter = server_event_bus.emitter(); - for id in to_reify { - rtsim.reify_entity(id); - let entity = &rtsim.entities[id]; - let rtsim_entity = Some(RtSimEntity(id)); + // Load the NPC into the world if it's in a loaded chunk and is not already + // loaded + if matches!(npc.mode, SimulationMode::Simulated) + && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) + { + npc.mode = SimulationMode::Loaded; + let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - let body = entity.get_body(); - let spawn_pos = terrain - .find_space(entity.pos.map(|e| e.floor() as i32)) - .map(|e| e as f32) - + Vec3::new(0.5, 0.5, body.flying_height()); - - let pos = comp::Pos(spawn_pos); - - let event = if let comp::Body::Ship(ship) = body { - ServerEvent::CreateShip { - pos, - ship, - mountable: false, - agent: Some(comp::Agent::from_body(&body)), - rtsim_entity, - } - } else { - let entity_config_path = entity.get_entity_config(); - let mut loadout_rng = entity.loadout_rng(); - let ad_hoc_loadout = entity.get_adhoc_loadout(); - // Body is rewritten so that body parameters - // are consistent between reifications - let entity_config = EntityConfig::from_asset_expect_owned(entity_config_path) - .with_body(BodyBuilder::Exact(body)); - - let mut entity_info = EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(entity_config_path), &mut loadout_rng) - .with_lazy_loadout(ad_hoc_loadout); - // Merchants can be traded with - if let Some(economy) = entity.get_trade_info(&world, &index) { - entity_info = entity_info - .with_agent_mark(comp::agent::Mark::Merchant) - .with_economy(&economy); - } - match NpcData::from_entity_info(entity_info) { + emitter.emit(match NpcData::from_entity_info(entity_info) { NpcData::Data { pos, stats, @@ -158,38 +323,62 @@ impl<'a> System<'a> for Sys { loot, } => ServerEvent::CreateNpc { pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - anchor: None, - loot, - rtsim_entity, - projectile: None, + npc: NpcBuilder::new(stats, body, alignment) + .with_skill_set(skill_set) + .with_health(health) + .with_poise(poise) + .with_inventory(inventory) + .with_agent(agent) + .with_scale(scale) + .with_loot(loot) + .with_rtsim(RtSimEntity(npc_id)), }, // EntityConfig can't represent Waypoints at all // as of now, and if someone will try to spawn // rtsim waypoint it is definitely error. NpcData::Waypoint(_) => unimplemented!(), - } - }; - server_emitter.emit(event); + }); + } } - // Update rtsim with real entity data - for (pos, rtsim_entity, agent) in (&positions, &rtsim_entities, &mut agents).join() { - rtsim - .entities + // Synchronise rtsim NPC with entity data + for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() { + data.npcs + .vehicles + .get_mut(rtsim_vehicle.0) + .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) + .map(|vehicle| { + // Update rtsim NPC state + vehicle.wpos = pos.0; + }); + } + + // Synchronise rtsim NPC with entity data + for (pos, rtsim_entity, agent) in + (&positions, &rtsim_entities, (&mut agents).maybe()).join() + { + data.npcs .get_mut(rtsim_entity.0) - .filter(|e| e.is_loaded) - .map(|entity| { - entity.pos = pos.0; - agent.rtsim_controller = entity.controller.clone(); + .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) + .map(|npc| { + // Update rtsim NPC state + npc.wpos = pos.0; + + // Update entity state + if let Some(agent) = agent { + agent.rtsim_controller.personality = npc.personality; + if let Some(action) = npc.action { + match action { + rtsim::data::npc::NpcAction::Goto(wpos, sf) => { + agent.rtsim_controller.travel_to = Some(wpos); + agent.rtsim_controller.speed_factor = sf; + }, + } + } else { + agent.rtsim_controller.travel_to = None; + agent.rtsim_controller.speed_factor = 1.0; + } + } }); } } diff --git a/server/src/rtsim/unload_chunks.rs b/server/src/rtsim/unload_chunks.rs deleted file mode 100644 index 5433164dc3..0000000000 --- a/server/src/rtsim/unload_chunks.rs +++ /dev/null @@ -1,43 +0,0 @@ -use super::*; -use common::{ - comp::Pos, - event::{EventBus, ServerEvent}, - terrain::TerrainGrid, -}; -use common_ecs::{Job, Origin, Phase, System}; -use specs::{Entities, Read, ReadExpect, ReadStorage, WriteExpect}; - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = ( - Read<'a, EventBus>, - WriteExpect<'a, RtSim>, - ReadExpect<'a, TerrainGrid>, - Entities<'a>, - ReadStorage<'a, RtSimEntity>, - ReadStorage<'a, Pos>, - ); - - const NAME: &'static str = "rtsim::unload_chunks"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run( - _job: &mut Job, - ( - _server_event_bus, - mut rtsim, - _terrain_grid, - _entities, - _rtsim_entities, - _positions, - ): Self::SystemData, - ) { - let chunks = std::mem::take(&mut rtsim.chunks.chunks_to_unload); - - for _chunk in chunks { - // TODO - } - } -} diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs deleted file mode 100644 index bb9ffb763b..0000000000 --- a/server/src/rtsim2/mod.rs +++ /dev/null @@ -1,242 +0,0 @@ -pub mod event; -pub mod rule; -pub mod tick; - -use common::{ - grid::Grid, - rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, - slowjob::SlowJobPool, - terrain::{Block, TerrainChunk}, - vol::RectRasterableVol, -}; -use common_ecs::{dispatch, System}; -use enum_map::EnumMap; -use rtsim2::{ - data::{npc::SimulationMode, Data, ReadError}, - event::{OnDeath, OnSetup}, - rule::Rule, - RtState, -}; -use specs::{DispatcherBuilder, WorldExt}; -use std::{ - error::Error, - fs::{self, File}, - io::{self, Write}, - path::PathBuf, - sync::Arc, - time::Instant, -}; -use tracing::{debug, error, info, warn}; -use vek::*; -use world::{IndexRef, World}; - -pub struct RtSim { - file_path: PathBuf, - last_saved: Option, - state: RtState, -} - -impl RtSim { - pub fn new( - settings: &WorldSettings, - index: IndexRef, - world: &World, - data_dir: PathBuf, - ) -> Result { - let file_path = Self::get_file_path(data_dir); - - info!("Looking for rtsim data at {}...", file_path.display()); - let data = 'load: { - if std::env::var("RTSIM_NOLOAD").map_or(true, |v| v != "1") { - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim data found. Attempting to load..."); - match Data::from_reader(io::BufReader::new(file)) { - Ok(data) => { - info!("Rtsim data loaded."); - if data.should_purge { - warn!( - "The should_purge flag was set on the rtsim data, \ - generating afresh" - ); - } else { - break 'load data; - } - }, - Err(e) => { - error!("Rtsim data failed to load: {}", e); - let mut i = 0; - loop { - let mut backup_path = file_path.clone(); - backup_path.set_extension(if i == 0 { - format!("backup_{}", i) - } else { - "ron_backup".to_string() - }); - if !backup_path.exists() { - fs::rename(&file_path, &backup_path)?; - warn!( - "Failed rtsim data was moved to {}", - backup_path.display() - ); - info!("A fresh rtsim data will now be generated."); - break; - } - i += 1; - } - }, - } - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => { - info!("No rtsim data found. Generating from world...") - }, - Err(e) => return Err(e.into()), - } - } else { - warn!( - "'RTSIM_NOLOAD' is set, skipping loading of rtsim state (old state will be \ - overwritten)." - ); - } - - let data = Data::generate(settings, &world, index); - info!("Rtsim data generated."); - data - }; - - let mut this = Self { - last_saved: None, - state: RtState::new(data).with_resource(ChunkStates(Grid::populate_from( - world.sim().get_size().as_(), - |_| None, - ))), - file_path, - }; - - rule::start_rules(&mut this.state); - - this.state.emit(OnSetup, world, index); - - Ok(this) - } - - fn get_file_path(mut data_dir: PathBuf) -> PathBuf { - let mut path = std::env::var("VELOREN_RTSIM") - .map(PathBuf::from) - .unwrap_or_else(|_| { - data_dir.push("rtsim"); - data_dir - }); - path.push("data.dat"); - path - } - - pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { - if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { - *chunk_state = Some(LoadedChunkState { max_res }); - } - } - - pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(chunk_state) = self.state.resource_mut::().0.get_mut(key) { - *chunk_state = None; - } - } - - pub fn hook_block_update( - &mut self, - world: &World, - index: IndexRef, - wpos: Vec3, - old: Block, - new: Block, - ) { - self.state - .emit(event::OnBlockChange { wpos, old, new }, world, index); - } - - pub fn hook_rtsim_entity_unload(&mut self, entity: RtSimEntity) { - if let Some(npc) = self.state.data_mut().npcs.get_mut(entity.0) { - npc.mode = SimulationMode::Simulated; - } - } - - pub fn hook_rtsim_vehicle_unload(&mut self, entity: RtSimVehicle) { - if let Some(vehicle) = self.state.data_mut().npcs.vehicles.get_mut(entity.0) { - vehicle.mode = SimulationMode::Simulated; - } - } - - pub fn hook_rtsim_entity_delete( - &mut self, - world: &World, - index: IndexRef, - entity: RtSimEntity, - ) { - // Should entity deletion be death? They're not exactly the same thing... - self.state.emit(OnDeath { npc_id: entity.0 }, world, index); - self.state.data_mut().npcs.remove(entity.0); - } - - pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { - info!("Saving rtsim data..."); - let file_path = self.file_path.clone(); - let data = self.state.data().clone(); - debug!("Starting rtsim data save job..."); - // TODO: Use slow job - // slowjob_pool.spawn("RTSIM_SAVE", move || { - let handle = std::thread::spawn(move || { - let tmp_file_name = "data_tmp.dat"; - if let Err(e) = file_path - .parent() - .map(|dir| { - fs::create_dir_all(dir)?; - // We write to a temporary file and then rename to avoid corruption. - Ok(dir.join(tmp_file_name)) - }) - .unwrap_or_else(|| Ok(tmp_file_name.into())) - .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) - .map_err(|e: io::Error| Box::new(e) as Box) - .and_then(|(mut file, tmp_file_path)| { - debug!("Writing rtsim data to file..."); - data.write_to(io::BufWriter::new(&mut file))?; - file.flush()?; - drop(file); - fs::rename(tmp_file_path, file_path)?; - debug!("Rtsim data saved."); - Ok(()) - }) - { - error!("Saving rtsim data failed: {}", e); - } - }); - - if wait_until_finished { - handle.join().expect("Save thread failed to join"); - } - - self.last_saved = Some(Instant::now()); - } - - // TODO: Clean up this API a bit - pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { - self.state.data().nature.get_chunk_resources(key) - } - - pub fn state(&self) -> &RtState { &self.state } - - pub fn set_should_purge(&mut self, should_purge: bool) { - self.state.data_mut().should_purge = should_purge; - } -} - -pub struct ChunkStates(pub Grid>); - -pub struct LoadedChunkState { - // The maximum possible number of each resource in this chunk - pub max_res: EnumMap, -} - -pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { - dispatch::(dispatch_builder, &[]); -} diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs deleted file mode 100644 index b6a93d7f20..0000000000 --- a/server/src/rtsim2/tick.rs +++ /dev/null @@ -1,385 +0,0 @@ -#![allow(dead_code)] // TODO: Remove this when rtsim is fleshed out - -use super::*; -use crate::sys::terrain::NpcData; -use common::{ - comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, - event::{EventBus, NpcBuilder, ServerEvent}, - generation::{BodyBuilder, EntityConfig, EntityInfo}, - lottery::LootSpec, - resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, - slowjob::SlowJobPool, - terrain::CoordinateConversions, - trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, -}; -use common_ecs::{Job, Origin, Phase, System}; -use rtsim2::data::{ - npc::{Profession, SimulationMode}, - Actor, Npc, Sites, -}; -use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::{sync::Arc, time::Duration}; -use world::site::settlement::trader_loadout; - -fn humanoid_config(profession: &Profession) -> &'static str { - match profession { - Profession::Farmer => "common.entity.village.farmer", - Profession::Hunter => "common.entity.village.hunter", - Profession::Herbalist => "common.entity.village.herbalist", - Profession::Captain => "common.entity.village.captain", - Profession::Merchant => "common.entity.village.merchant", - Profession::Guard => "common.entity.village.guard", - Profession::Adventurer(rank) => match rank { - 0 => "common.entity.world.traveler0", - 1 => "common.entity.world.traveler1", - 2 => "common.entity.world.traveler2", - 3 => "common.entity.world.traveler3", - _ => panic!("Not a valid adventurer rank"), - }, - Profession::Blacksmith => "common.entity.village.blacksmith", - Profession::Chef => "common.entity.village.chef", - Profession::Alchemist => "common.entity.village.alchemist", - Profession::Pirate => "common.entity.spot.pirate", - Profession::Cultist => "common.entity.dungeon.tier-5.cultist", - } -} - -fn loadout_default(loadout: LoadoutBuilder, _economy: Option<&SiteInformation>) -> LoadoutBuilder { - loadout -} - -fn merchant_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |_| true) -} - -fn farmer_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) -} - -fn herbalist_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Ingredients) - }) -} - -fn chef_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| matches!(good, Good::Food)) -} - -fn blacksmith_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Tools | Good::Armor) - }) -} - -fn alchemist_loadout( - loadout_builder: LoadoutBuilder, - economy: Option<&SiteInformation>, -) -> LoadoutBuilder { - trader_loadout(loadout_builder, economy, |good| { - matches!(good, Good::Potions) - }) -} - -fn profession_extra_loadout( - profession: Option<&Profession>, -) -> fn(LoadoutBuilder, Option<&SiteInformation>) -> LoadoutBuilder { - match profession { - Some(Profession::Merchant) => merchant_loadout, - Some(Profession::Farmer) => farmer_loadout, - Some(Profession::Herbalist) => herbalist_loadout, - Some(Profession::Chef) => chef_loadout, - Some(Profession::Blacksmith) => blacksmith_loadout, - Some(Profession::Alchemist) => alchemist_loadout, - _ => loadout_default, - } -} - -fn profession_agent_mark(profession: Option<&Profession>) -> Option { - match profession { - Some( - Profession::Merchant - | Profession::Farmer - | Profession::Herbalist - | Profession::Chef - | Profession::Blacksmith - | Profession::Alchemist, - ) => Some(comp::agent::Mark::Merchant), - Some(Profession::Guard) => Some(comp::agent::Mark::Guard), - _ => None, - } -} - -fn get_npc_entity_info(npc: &Npc, sites: &Sites, index: IndexRef) -> EntityInfo { - let pos = comp::Pos(npc.wpos); - - let mut rng = npc.rng(3); - if let Some(ref profession) = npc.profession { - let economy = npc.home.and_then(|home| { - let site = sites.get(home)?.world_site?; - index.sites.get(site).trade_information(site.id()) - }); - - let config_asset = humanoid_config(profession); - - let entity_config = EntityConfig::from_asset_expect_owned(config_asset) - .with_body(BodyBuilder::Exact(npc.body)); - EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(if matches!(profession, Profession::Cultist) { - comp::Alignment::Enemy - } else { - comp::Alignment::Npc - }) - .with_economy(economy.as_ref()) - .with_lazy_loadout(profession_extra_loadout(npc.profession.as_ref())) - .with_agent_mark(profession_agent_mark(npc.profession.as_ref())) - } else { - let config_asset = match npc.body { - Body::BirdLarge(body) => match body.species { - comp::bird_large::Species::Phoenix => "common.entity.wild.peaceful.phoenix", - comp::bird_large::Species::Cockatrice => "common.entity.wild.aggressive.cockatrice", - comp::bird_large::Species::Roc => "common.entity.wild.aggressive.roc", - // Wildcard match used here as there is an array above - // which limits what species are used - _ => unimplemented!(), - }, - _ => unimplemented!(), - }; - let entity_config = EntityConfig::from_asset_expect_owned(config_asset) - .with_body(BodyBuilder::Exact(npc.body)); - - EntityInfo::at(pos.0) - .with_entity_config(entity_config, Some(config_asset), &mut rng) - .with_alignment(comp::Alignment::Wild) - } -} - -#[derive(Default)] -pub struct Sys; -impl<'a> System<'a> for Sys { - type SystemData = ( - Read<'a, DeltaTime>, - Read<'a, Time>, - Read<'a, TimeOfDay>, - Read<'a, EventBus>, - WriteExpect<'a, RtSim>, - ReadExpect<'a, Arc>, - ReadExpect<'a, world::IndexOwned>, - ReadExpect<'a, SlowJobPool>, - ReadStorage<'a, comp::Pos>, - ReadStorage<'a, RtSimEntity>, - ReadStorage<'a, RtSimVehicle>, - WriteStorage<'a, comp::Agent>, - ); - - const NAME: &'static str = "rtsim::tick"; - const ORIGIN: Origin = Origin::Server; - const PHASE: Phase = Phase::Create; - - fn run( - _job: &mut Job, - ( - dt, - time, - time_of_day, - server_event_bus, - mut rtsim, - world, - index, - slow_jobs, - positions, - rtsim_entities, - rtsim_vehicles, - mut agents, - ): Self::SystemData, - ) { - let mut emitter = server_event_bus.emitter(); - let rtsim = &mut *rtsim; - - rtsim.state.data_mut().time_of_day = *time_of_day; - rtsim - .state - .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); - - if rtsim - .last_saved - .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) - { - // TODO: Use slow jobs - let _ = slow_jobs; - rtsim.save(/* &slow_jobs, */ false); - } - - let chunk_states = rtsim.state.resource::(); - let data = &mut *rtsim.state.data_mut(); - - for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { - let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); - - if matches!(vehicle.mode, SimulationMode::Simulated) - && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) - { - vehicle.mode = SimulationMode::Loaded; - - let mut actor_info = |actor: Actor| { - let npc_id = actor.npc()?; - let npc = data.npcs.npcs.get_mut(npc_id)?; - if matches!(npc.mode, SimulationMode::Simulated) { - npc.mode = SimulationMode::Loaded; - let entity_info = - get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - - Some(match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos: _, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), - // EntityConfig can't represent Waypoints at all - // as of now, and if someone will try to spawn - // rtsim waypoint it is definitely error. - NpcData::Waypoint(_) => unimplemented!(), - }) - } else { - error!("Npc is loaded but vehicle is unloaded"); - None - } - }; - - emitter.emit(ServerEvent::CreateShip { - pos: comp::Pos(vehicle.wpos), - ship: vehicle.body, - // agent: None,//Some(Agent::from_body(&Body::Ship(ship))), - rtsim_entity: Some(RtSimVehicle(vehicle_id)), - driver: vehicle.driver.and_then(&mut actor_info), - passangers: vehicle - .riders - .iter() - .copied() - .filter(|actor| vehicle.driver != Some(*actor)) - .filter_map(actor_info) - .collect(), - }); - } - } - - for (npc_id, npc) in data.npcs.npcs.iter_mut() { - let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); - - // Load the NPC into the world if it's in a loaded chunk and is not already - // loaded - if matches!(npc.mode, SimulationMode::Simulated) - && chunk_states.0.get(chunk).map_or(false, |c| c.is_some()) - { - npc.mode = SimulationMode::Loaded; - let entity_info = get_npc_entity_info(npc, &data.sites, index.as_index_ref()); - - emitter.emit(match NpcData::from_entity_info(entity_info) { - NpcData::Data { - pos, - stats, - skill_set, - health, - poise, - inventory, - agent, - body, - alignment, - scale, - loot, - } => ServerEvent::CreateNpc { - pos, - npc: NpcBuilder::new(stats, body, alignment) - .with_skill_set(skill_set) - .with_health(health) - .with_poise(poise) - .with_inventory(inventory) - .with_agent(agent) - .with_scale(scale) - .with_loot(loot) - .with_rtsim(RtSimEntity(npc_id)), - }, - // EntityConfig can't represent Waypoints at all - // as of now, and if someone will try to spawn - // rtsim waypoint it is definitely error. - NpcData::Waypoint(_) => unimplemented!(), - }); - } - } - - // Synchronise rtsim NPC with entity data - for (pos, rtsim_vehicle) in (&positions, &rtsim_vehicles).join() { - data.npcs - .vehicles - .get_mut(rtsim_vehicle.0) - .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) - .map(|vehicle| { - // Update rtsim NPC state - vehicle.wpos = pos.0; - }); - } - - // Synchronise rtsim NPC with entity data - for (pos, rtsim_entity, agent) in - (&positions, &rtsim_entities, (&mut agents).maybe()).join() - { - data.npcs - .get_mut(rtsim_entity.0) - .filter(|npc| matches!(npc.mode, SimulationMode::Loaded)) - .map(|npc| { - // Update rtsim NPC state - npc.wpos = pos.0; - - // Update entity state - if let Some(agent) = agent { - agent.rtsim_controller.personality = npc.personality; - if let Some(action) = npc.action { - match action { - rtsim2::data::npc::NpcAction::Goto(wpos, sf) => { - agent.rtsim_controller.travel_to = Some(wpos); - agent.rtsim_controller.speed_factor = sf; - }, - } - } else { - agent.rtsim_controller.travel_to = None; - agent.rtsim_controller.speed_factor = 1.0; - } - } - }); - } - } -} diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 1e151dd792..a939bd113a 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -5,7 +5,7 @@ use crate::{ persistence::PersistedComponents, pet::restore_pet, presence::{Presence, RepositionOnChunkLoad}, - rtsim2::RtSim, + rtsim::RtSim, settings::Settings, sys::sentinel::DeletedEntities, wiring, BattleModeBuffer, SpawnPoint, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index c9c5e8be78..d401221aae 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -10,7 +10,7 @@ use crate::{ chunk_serialize::ChunkSendEntry, client::Client, presence::{Presence, RepositionOnChunkLoad}, - rtsim2, + rtsim, settings::Settings, ChunkRequest, Tick, }; @@ -50,7 +50,7 @@ pub type TerrainPersistenceData<'a> = (); pub const SAFE_ZONE_RADIUS: f32 = 200.0; #[cfg(feature = "worldgen")] -type RtSimData<'a> = WriteExpect<'a, rtsim2::RtSim>; +type RtSimData<'a> = WriteExpect<'a, rtsim::RtSim>; #[cfg(not(feature = "worldgen"))] type RtSimData<'a> = (); From ea007ff70272124226afff9d25b114b0be43c187 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 00:56:06 +0100 Subject: [PATCH 070/139] Cleaning up --- common/state/src/state.rs | 2 +- common/systems/src/phys.rs | 4 -- rtsim/src/rule/npc_ai.rs | 72 ++-------------------- server/src/cmd.rs | 8 +-- server/src/events/entity_creation.rs | 9 +-- server/src/events/entity_manipulation.rs | 2 +- server/src/lib.rs | 5 +- server/src/rtsim/mod.rs | 12 ++-- server/src/rtsim/rule/deplete_resources.rs | 9 +-- server/src/rtsim/tick.rs | 7 +-- server/src/sys/agent/behavior_tree.rs | 8 +-- voxygen/src/scene/mod.rs | 16 ----- world/src/layer/scatter.rs | 2 +- world/src/lib.rs | 2 +- 14 files changed, 31 insertions(+), 127 deletions(-) diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 1a3cf2b458..20ac10d6d4 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -526,7 +526,7 @@ impl State { // Apply terrain changes pub fn apply_terrain_changes( &self, - mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + block_update: impl FnMut(&specs::World, Vec3, Block, Block), ) { self.apply_terrain_changes_internal(false, block_update); } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index b1f2a63a2c..6bf2cf9707 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -1392,7 +1392,6 @@ fn box_voxel_collision + ReadVol>( near_aabb: Aabb, radius: f32, z_range: Range, - scale: f32, ) -> bool { let player_aabb = player_aabb(pos, radius, z_range); @@ -1558,7 +1557,6 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), - scale, ) } // ...and there is a collision with a block beneath our current hitbox... @@ -1571,7 +1569,6 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), - scale, ) } { // ...block-hop! @@ -1626,7 +1623,6 @@ fn box_voxel_collision + ReadVol>( near_aabb, radius, z_range.clone(), - scale, ) } { //prof_span!("snap!!"); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 5e5df3c3a8..81bbde6949 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -210,6 +210,8 @@ fn path_towns( impl Rule for NpcAi { fn start(rtstate: &mut RtState) -> Result { rtstate.bind::(|ctx| { + // Temporarily take the brains of NPCs out of their heads to appease the borrow + // checker let mut npc_data = { let mut data = ctx.state.data_mut(); data.npcs @@ -224,6 +226,7 @@ impl Rule for NpcAi { .collect::>() }; + // Do a little thinking { let data = &*ctx.state.data(); @@ -245,56 +248,12 @@ impl Rule for NpcAi { }); } + // Reinsert NPC brains let mut data = ctx.state.data_mut(); for (npc_id, controller, brain) in npc_data { data.npcs[npc_id].action = controller.action; data.npcs[npc_id].brain = Some(brain); } - - /* - let action: ControlFlow<()> = try { - brain.tick(&mut NpcData { - ctx: &ctx, - npc, - npc_id, - controller: &mut controller, - }); - /* - // // Choose a random plaza in the npcs home site (which should be the - // // current here) to go to. - let task = - generate(move |(_, npc, ctx): &(NpcId, &Npc, &EventCtx<_, _>)| { - let data = ctx.state.data(); - let site2 = - npc.home.and_then(|home| data.sites.get(home)).and_then( - |home| match &ctx.index.sites.get(home.world_site?).kind - { - SiteKind::Refactor(site2) - | SiteKind::CliffTown(site2) - | SiteKind::DesertCity(site2) => Some(site2), - _ => None, - }, - ); - - let wpos = site2 - .and_then(|site2| { - let plaza = &site2.plots - [site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - .unwrap_or(npc.wpos.xy()); - - TravelTo { - wpos, - use_paths: true, - } - }) - .repeat(); - - task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?; - */ - }; - */ }); Ok(Self) @@ -371,16 +330,12 @@ where site_exit = next; } - // println!("[NPC {:?}] Pathing in site...", ctx.npc_id); if let Some(path) = path_site(wpos, site_exit, site, ctx.index) { - // println!("[NPC {:?}] Found path of length {} from {:?} to {:?}!", ctx.npc_id, - // path.len(), wpos, site_exit); Some(itertools::Either::Left( seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))) .then(goto_2d(site_exit, 1.0, 8.0)), )) } else { - // println!("[NPC {:?}] No path", ctx.npc_id); Some(itertools::Either::Right(goto_2d(site_exit, 1.0, 8.0))) } } else { @@ -395,13 +350,9 @@ fn travel_to_point(wpos: Vec2) -> impl Action { const WAYPOINT: f32 = 24.0; let start = ctx.npc.wpos.xy(); let diff = wpos - start; - // if diff.magnitude() > 1.0 { let n = (diff.magnitude() / WAYPOINT).max(1.0); let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); traverse_points(move |_| points.next()).boxed() - // } else { - // finish().boxed() - // } }) .debug(|| "travel to point") } @@ -858,18 +809,3 @@ fn think() -> impl Action { _ => casual(idle()), }) } - -// if !matches!(stages.front(), Some(TravelStage::IntraSite { .. })) { -// let data = ctx.state.data(); -// if let Some((site2, site)) = npc -// .current_site -// .and_then(|current_site| data.sites.get(current_site)) -// .and_then(|site| site.world_site) -// .and_then(|site| Some((get_site2(site)?, site))) -// { -// let end = site2.wpos_tile_pos(self.wpos.as_()); -// if let Some(path) = path_town(npc.wpos, site, ctx.index, |_| -// Some(end)) { stages.push_front(TravelStage::IntraSite { path, -// site }); } -// } -// } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 3b3025e750..e2b0857dd4 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1218,7 +1218,7 @@ fn handle_rtsim_tp( fn handle_rtsim_info( server: &mut Server, client: EcsEntity, - target: EcsEntity, + _target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { @@ -1267,7 +1267,7 @@ fn handle_rtsim_info( fn handle_rtsim_purge( server: &mut Server, client: EcsEntity, - target: EcsEntity, + _target: EcsEntity, args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { @@ -1298,8 +1298,8 @@ fn handle_rtsim_chunk( server: &mut Server, client: EcsEntity, target: EcsEntity, - args: Vec, - action: &ServerChatCommand, + _args: Vec, + _action: &ServerChatCommand, ) -> CmdResult<()> { use crate::rtsim::{ChunkStates, RtSim}; let pos = position(server, target, "target")?; diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index f399b3d3ec..d28b392a73 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -6,20 +6,17 @@ use common::{ character::CharacterId, comp::{ self, - agent::pid_coefficients, aura::{Aura, AuraKind, AuraTarget}, beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, - shockwave, Agent, Alignment, Anchor, BehaviorCapability, Body, Health, Inventory, ItemDrop, - LightEmitter, Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, - TradingBehavior, Vel, WaypointArea, + shockwave, Alignment, BehaviorCapability, Body, ItemDrop, LightEmitter, Object, Ori, Pos, + Projectile, TradingBehavior, Vel, WaypointArea, }, event::{EventBus, NpcBuilder, UpdateCharacterMetadata}, - lottery::LootSpec, mounting::Mounting, outcome::Outcome, resources::{Secs, Time}, - rtsim::{RtSimEntity, RtSimVehicle}, + rtsim::RtSimVehicle, uid::Uid, util::Dir, ViewDistances, diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index eff23180f2..4530376071 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -27,7 +27,7 @@ use common::{ outcome::{HealthChangeInfo, Outcome}, resources::{Secs, Time}, rtsim::RtSimEntity, - states::utils::{AbilityInfo, StageSection}, + states::utils::StageSection, terrain::{Block, BlockKind, TerrainGrid}, uid::{Uid, UidAllocator}, util::Dir, diff --git a/server/src/lib.rs b/server/src/lib.rs index 8946299f7e..3ae719da85 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -7,8 +7,7 @@ let_chains, never_type, option_zip, - unwrap_infallible, - explicit_generic_args_with_impl_trait + unwrap_infallible )] #![feature(hash_drain_filter)] @@ -1464,7 +1463,7 @@ impl Drop for Server { #[cfg(feature = "worldgen")] { - info!("Saving rtsim state..."); + debug!("Saving rtsim state..."); self.state.ecs().write_resource::().save(true); } } diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 6b8f50e755..1bda5169b4 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -5,25 +5,21 @@ pub mod tick; use common::{ grid::Grid, rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, - slowjob::SlowJobPool, - terrain::{Block, TerrainChunk}, - vol::RectRasterableVol, + terrain::Block, }; -use common_ecs::{dispatch, System}; +use common_ecs::dispatch; use enum_map::EnumMap; use rtsim::{ - data::{npc::SimulationMode, Data, ReadError}, + data::{npc::SimulationMode, Data}, event::{OnDeath, OnSetup}, - rule::Rule, RtState, }; -use specs::{DispatcherBuilder, WorldExt}; +use specs::DispatcherBuilder; use std::{ error::Error, fs::{self, File}, io::{self, Write}, path::PathBuf, - sync::Arc, time::Instant, }; use tracing::{debug, error, info, warn}; diff --git a/server/src/rtsim/rule/deplete_resources.rs b/server/src/rtsim/rule/deplete_resources.rs index 6414296012..967a77d1c4 100644 --- a/server/src/rtsim/rule/deplete_resources.rs +++ b/server/src/rtsim/rule/deplete_resources.rs @@ -1,8 +1,5 @@ use crate::rtsim::{event::OnBlockChange, ChunkStates}; -use common::{ - terrain::{CoordinateConversions, TerrainChunk}, - vol::RectRasterableVol, -}; +use common::terrain::CoordinateConversions; use rtsim::{RtState, Rule, RuleError}; pub struct DepleteResources; @@ -22,7 +19,7 @@ impl Rule for DepleteResources { / chunk_state.max_res[res] as f32; } } - // Add resources + // Replenish resources if let Some(res) = ctx.event.new.get_rtsim_resource() { if chunk_state.max_res[res] > 0 { chunk_res[res] = (chunk_res[res] * chunk_state.max_res[res] as f32 + 1.0) @@ -31,7 +28,7 @@ impl Rule for DepleteResources { / chunk_state.max_res[res] as f32; } } - //println!("Chunk resources = {:?}", chunk_res); + ctx.state .data_mut() .nature diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 63913c4267..443957a097 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,16 +3,15 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, inventory::loadout::Loadout, skillset::skills, Agent, Body}, + comp::{self, Body}, event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, - lottery::LootSpec, resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{RtSimController, RtSimEntity, RtSimVehicle}, + rtsim::{RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, terrain::CoordinateConversions, trade::{Good, SiteInformation}, - LoadoutBuilder, SkillSetBuilder, + LoadoutBuilder, }; use common_ecs::{Job, Origin, Phase, System}; use rtsim::data::{ diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 5d0a6ef07c..ce2fd6891a 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -769,12 +769,12 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { } fn remembers_fight_with( - rtsim_entity: Option<&RtSimEntity>, - read_data: &ReadData, - other: EcsEntity, + _rtsim_entity: Option<&RtSimEntity>, + _read_data: &ReadData, + _other: EcsEntity, ) -> bool { // TODO: implement for rtsim2 - let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); + // let name = || read_data.stats.get(other).map(|stats| stats.name.clone()); // rtsim_entity.map_or(false, |rtsim_entity| { // name().map_or(false, |name| { diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 87129c41d0..7a6ff8bbdc 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -539,22 +539,6 @@ impl Scene { .get(scene_data.viewpoint_entity) .map_or(1.0, |scale| scale.0); - let viewpoint_rolling = ecs - .read_storage::() - .get(scene_data.viewpoint_entity) - .map_or(false, |cs| cs.is_dodge()); - - let is_running = ecs - .read_storage::() - .get(scene_data.viewpoint_entity) - .map(|v| v.0.magnitude_squared() > RUNNING_THRESHOLD.powi(2)) - .unwrap_or(false); - - let on_ground = ecs - .read_storage::() - .get(scene_data.viewpoint_entity) - .map(|p| p.on_ground.is_some()); - let (is_humanoid, viewpoint_height, viewpoint_eye_height) = scene_data .state .ecs() diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index 936c204f3f..5ed0f832ed 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -30,7 +30,7 @@ pub fn density_factor_by_altitude(lower_limit: f32, altitude: f32, upper_limit: const MUSH_FACT: f32 = 1.0e-4; // To balance things around the mushroom spawning rate const GRASS_FACT: f32 = 1.0e-3; // To balance things around the grass spawning rate const DEPTH_WATER_NORM: f32 = 15.0; // Water depth at which regular underwater sprites start spawning -pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng, calendar: Option<&Calendar>) { +pub fn apply_scatter_to(canvas: &mut Canvas, _rng: &mut impl Rng, calendar: Option<&Calendar>) { enum WaterMode { Underwater, Floating, diff --git a/world/src/lib.rs b/world/src/lib.rs index b50ff1d250..5207ea16fa 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -500,7 +500,7 @@ impl World { rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array()); rtsim_resource_blocks.dedup(); for wpos in rtsim_resource_blocks { - chunk.map(wpos - chunk_wpos2d.with_z(0), |block| { + let _ = chunk.map(wpos - chunk_wpos2d.with_z(0), |block| { if let Some(res) = block.get_rtsim_resource() { // Note: this represents the upper limit, not the actual number spanwed, so // we increment this before deciding whether we're going to spawn the From 8ba68e30f313fa5f9fac0540e62670d1c3f7c2cc Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 01:25:35 +0100 Subject: [PATCH 071/139] Merchants no longer buy/sell things they don't know the price of --- common/src/trade.rs | 23 ++-- .../sys/agent/behavior_tree/interaction.rs | 121 +++++++++++------- 2 files changed, 86 insertions(+), 58 deletions(-) diff --git a/common/src/trade.rs b/common/src/trade.rs index 496c05358e..a3408d09a2 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -381,15 +381,16 @@ impl SitePrices { inventories: &[Option; 2], who: usize, reduce: bool, - ) -> f32 { + ) -> Option { offers[who] .iter() .map(|(slot, amount)| { inventories[who] .as_ref() - .and_then(|ri| { - ri.inventory.get(slot).map(|item| { - if let Some(vec) = TradePricing::get_materials(&item.name.as_ref()) { + .map(|ri| { + let item = ri.inventory.get(slot)?; + if let Some(vec) = TradePricing::get_materials(&item.name.as_ref()) { + Some( vec.iter() .map(|(amount2, material)| { self.values.get(material).copied().unwrap_or_default() @@ -397,15 +398,15 @@ impl SitePrices { * (if reduce { material.trade_margin() } else { 1.0 }) }) .sum::() - * (*amount as f32) - } else { - 0.0 - } - }) + * (*amount as f32), + ) + } else { + None + } }) - .unwrap_or_default() + .unwrap_or(Some(0.0)) }) - .sum() + .try_fold(0.0, |a, p| Some(a + p?)) } } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index f477037ff0..a64ff80afa 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -524,57 +524,84 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); match agent.behavior.trading_behavior { TradingBehavior::RequireBalanced { .. } => { - let balance0: f32 = - prices.balance(&pending.offers, &inventories, 1 - who, true); - let balance1: f32 = prices.balance(&pending.offers, &inventories, who, false); - if balance0 >= balance1 { - // If the trade is favourable to us, only send an accept message if we're - // not already accepting (since otherwise, spam-clicking the accept button - // results in lagging and moving to the review phase of an unfavorable trade - // (although since the phase is included in the message, this shouldn't - // result in fully accepting an unfavourable trade)) - if !pending.accept_flags[who] && !pending.is_empty_trade() { - event_emitter.emit(ServerEvent::ProcessTradeAction( - *agent_data.entity, - tradeid, - TradeAction::Accept(pending.phase), - )); - tracing::trace!(?tradeid, ?balance0, ?balance1, "Accept Pending Trade"); - } - } else { - if balance1 > 0.0 { - let msg = format!( - "That only covers {:.0}% of my costs!", - (balance0 / balance1 * 100.0).floor() - ); - if let Some(tgt_data) = &agent.target { - // If talking with someone in particular, "tell" it only to them - if let Some(with) = read_data.uids.get(tgt_data.target) { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_tell(*agent_data.uid, *with, msg), - )); - } else { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_say(*agent_data.uid, msg), + let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true); + let balance1 = prices.balance(&pending.offers, &inventories, who, false); + match (balance0, balance1) { + (_, None) => { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + format!("I'm not willing to sell that item"), + ))) + }, + (None, _) => { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + format!("I'm not willing to buy that item"), + ))) + }, + (Some(balance0), Some(balance1)) => { + if balance0 >= balance1 { + // If the trade is favourable to us, only send an accept message if + // we're not already accepting + // (since otherwise, spam-clicking the accept button + // results in lagging and moving to the review phase of an + // unfavorable trade (although since + // the phase is included in the message, this shouldn't + // result in fully accepting an unfavourable trade)) + if !pending.accept_flags[who] && !pending.is_empty_trade() { + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Accept(pending.phase), )); + tracing::trace!( + ?tradeid, + ?balance0, + ?balance1, + "Accept Pending Trade" + ); } } else { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - msg, - ))); + if balance1 > 0.0 { + let msg = format!( + "That only covers {:.0}% of my costs!", + (balance0 / balance1 * 100.0).floor() + ); + if let Some(tgt_data) = &agent.target { + // If talking with someone in particular, "tell" it only to + // them + if let Some(with) = read_data.uids.get(tgt_data.target) { + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc_tell( + *agent_data.uid, + *with, + msg, + ), + )); + } else { + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc_say(*agent_data.uid, msg), + )); + } + } else { + event_emitter.emit(ServerEvent::Chat( + UnresolvedChatMsg::npc_say(*agent_data.uid, msg), + )); + } + } + if pending.phase != TradePhase::Mutate { + // we got into the review phase but without balanced goods, + // decline + agent.behavior.unset(BehaviorState::TRADING); + agent.target = None; + event_emitter.emit(ServerEvent::ProcessTradeAction( + *agent_data.entity, + tradeid, + TradeAction::Decline, + )); + } } - } - if pending.phase != TradePhase::Mutate { - // we got into the review phase but without balanced goods, decline - agent.behavior.unset(BehaviorState::TRADING); - agent.target = None; - event_emitter.emit(ServerEvent::ProcessTradeAction( - *agent_data.entity, - tradeid, - TradeAction::Decline, - )); - } + }, } }, TradingBehavior::AcceptFood => { From b022076a5ce9c3341650b21b2fb69918d2b8865d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 12:50:04 +0100 Subject: [PATCH 072/139] Fallback for non-trades --- voxygen/src/hud/mod.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 4d62bee649..f3102fed6e 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -4035,14 +4035,12 @@ impl Hud { remove, quantity: &mut u32| { if let Some(prices) = prices { - let balance0 = - prices.balance(&trade.offers, &r_inventories, who, true); - let balance1 = prices.balance( - &trade.offers, - &r_inventories, - 1 - who, - false, - ); + let balance0 = prices + .balance(&trade.offers, &r_inventories, who, true) + .unwrap_or(0.0); // TODO: Don't default to 0 here? + let balance1 = prices + .balance(&trade.offers, &r_inventories, 1 - who, false) + .unwrap_or(0.0); // TODO: Don't default to 0 here? if let Some(item) = inventory.get(slot) { if let Some(materials) = TradePricing::get_materials(&item.item_definition_id()) From d53b344c237ba72af595cd928d570df5dcaa55c6 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 1 Apr 2023 14:09:41 +0200 Subject: [PATCH 073/139] make merchants use tell, and general cleanup --- common/src/event.rs | 2 +- common/src/trade.rs | 25 +++---- rtsim/src/ai/mod.rs | 2 +- rtsim/src/gen/mod.rs | 4 +- rtsim/src/lib.rs | 6 +- rtsim/src/rule/npc_ai.rs | 12 +-- rtsim/src/rule/simulate_npcs.rs | 6 +- server/src/cmd.rs | 2 +- server/src/rtsim/mod.rs | 2 +- .../sys/agent/behavior_tree/interaction.rs | 72 ++++++------------ voxygen/src/hud/mod.rs | 74 ++++++++++--------- 11 files changed, 90 insertions(+), 117 deletions(-) diff --git a/common/src/event.rs b/common/src/event.rs index 629746b934..80d1c952a2 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -64,7 +64,7 @@ impl NpcBuilder { stats, skill_set: comp::SkillSet::default(), health: None, - poise: comp::Poise::new(body.clone()), + poise: comp::Poise::new(body), inventory: comp::Inventory::with_empty(), body, agent: None, diff --git a/common/src/trade.rs b/common/src/trade.rs index a3408d09a2..3b3ca0f7eb 100644 --- a/common/src/trade.rs +++ b/common/src/trade.rs @@ -389,20 +389,17 @@ impl SitePrices { .as_ref() .map(|ri| { let item = ri.inventory.get(slot)?; - if let Some(vec) = TradePricing::get_materials(&item.name.as_ref()) { - Some( - vec.iter() - .map(|(amount2, material)| { - self.values.get(material).copied().unwrap_or_default() - * *amount2 - * (if reduce { material.trade_margin() } else { 1.0 }) - }) - .sum::() - * (*amount as f32), - ) - } else { - None - } + let vec = TradePricing::get_materials(&item.name.as_ref())?; + Some( + vec.iter() + .map(|(amount2, material)| { + self.values.get(material).copied().unwrap_or_default() + * *amount2 + * (if reduce { material.trade_margin() } else { 1.0 }) + }) + .sum::() + * (*amount as f32), + ) }) .unwrap_or(Some(0.0)) }) diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 1232935a62..2d27002298 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -469,7 +469,7 @@ impl Node + Send + Sync + 'static, R: 'static> Actio }; match prev.0.tick(ctx) { - ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Continue(()) => ControlFlow::Continue(()), ControlFlow::Break(r) => { self.prev = None; ControlFlow::Break(r) diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 757cc68ef0..19ba5e692b 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -150,9 +150,7 @@ impl Data { .with_personality(Personality::random_evil(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match rng.gen_range(0..20) { - _ => Profession::Cultist, - }), + .with_profession(Profession::Cultist), ); } } diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index f24a449f4a..3643dc458e 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -136,9 +136,9 @@ impl RtState { } pub fn emit(&mut self, e: E, world: &World, index: IndexRef) { - self.event_handlers - .get::>() - .map(|handlers| handlers.iter().for_each(|f| f(self, world, index, &e))); + if let Some(handlers) = self.event_handlers.get::>() { + handlers.iter().for_each(|f| f(self, world, index, &e)); + } } pub fn tick( diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 81bbde6949..2050f42fd7 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -45,7 +45,7 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes let mut astar = Astar::new( 1000, start, - &heuristic, + heuristic, BuildHasherDefault::::default(), ); @@ -76,12 +76,12 @@ fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathRes let building = if a_tile.is_building() && b_tile.is_road() { a_tile .plot - .and_then(|plot| is_door_tile(plot, *a).then(|| 1.0)) + .and_then(|plot| is_door_tile(plot, *a).then_some(1.0)) .unwrap_or(10000.0) } else if b_tile.is_building() && a_tile.is_road() { b_tile .plot - .and_then(|plot| is_door_tile(plot, *b).then(|| 1.0)) + .and_then(|plot| is_door_tile(plot, *b).then_some(1.0)) .unwrap_or(10000.0) } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot { 10000.0 @@ -493,7 +493,7 @@ fn adventure() -> impl Action { casual(finish().boxed()) } }) - .debug(move || format!("adventure")) + .debug(move || "adventure") } fn villager(visiting_site: SiteId) -> impl Action { @@ -513,9 +513,9 @@ fn villager(visiting_site: SiteId) -> impl Action { // Travel to the site we're supposed to be in urgent(travel_to_site(visiting_site).debug(move || { if npc_home == Some(visiting_site) { - format!("travel home") + "travel home".to_string() } else { - format!("travel to visiting site") + "travel to visiting site".to_string() } })) } else if DayPeriod::from(ctx.time_of_day.0).is_dark() diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 2703c1d8b9..620825ac2c 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -27,10 +27,8 @@ impl Rule for SimulateNpcs { if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { let actor = crate::data::Actor::Npc(npc_id); vehicle.riders.push(actor); - if ride.steering { - if vehicle.driver.replace(actor).is_some() { - panic!("Replaced driver"); - } + if ride.steering && vehicle.driver.replace(actor).is_some() { + panic!("Replaced driver"); } } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index e2b0857dd4..acc753ce1a 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1290,7 +1290,7 @@ fn handle_rtsim_purge( ); Ok(()) } else { - return Err(action.help_string()); + Err(action.help_string()) } } diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 1bda5169b4..d9f5d8db63 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -95,7 +95,7 @@ impl RtSim { ); } - let data = Data::generate(settings, &world, index); + let data = Data::generate(settings, world, index); info!("Rtsim data generated."); data }; diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index a64ff80afa..45b05a843f 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -165,25 +165,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { standard_response_msg() }; agent_data.chat_npc(msg, event_emitter); - } - /*else if agent.behavior.can_trade(agent_data.alignment.copied(), by) { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc( - "npc-speech-merchant_advertisement", - event_emitter, - ); - } else { - let default_msg = "npc-speech-merchant_busy"; - let msg = if agent.rtsim_controller.personality.is(PersonalityTrait::Disagreeable) { - "npc-speech-merchant_busy_rude" - } else { - default_msg - }; - agent_data.chat_npc(msg, event_emitter); - } - }*/ - else { + } else { let mut rng = thread_rng(); if let Some(extreme_trait) = agent.rtsim_controller.personality.chat_trait(&mut rng) @@ -522,22 +504,36 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { let (tradeid, pending, prices, inventories) = *boxval; if agent.behavior.is(BehaviorState::TRADING) { let who = usize::from(!agent.behavior.is(BehaviorState::TRADING_ISSUER)); + let mut message = |msg| { + if let Some(with) = agent + .target + .as_ref() + .and_then(|tgt_data| read_data.uids.get(tgt_data.target)) + { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_tell( + *agent_data.uid, + *with, + msg, + ))); + } else { + event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( + *agent_data.uid, + msg, + ))); + } + }; match agent.behavior.trading_behavior { TradingBehavior::RequireBalanced { .. } => { let balance0 = prices.balance(&pending.offers, &inventories, 1 - who, true); let balance1 = prices.balance(&pending.offers, &inventories, who, false); match (balance0, balance1) { (_, None) => { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - format!("I'm not willing to sell that item"), - ))) + let msg = "I'm not willing to sell that item".to_string(); + message(msg); }, (None, _) => { - event_emitter.emit(ServerEvent::Chat(UnresolvedChatMsg::npc_say( - *agent_data.uid, - format!("I'm not willing to buy that item"), - ))) + let msg = "I'm not willing to buy that item".to_string(); + message(msg); }, (Some(balance0), Some(balance1)) => { if balance0 >= balance1 { @@ -567,27 +563,7 @@ pub fn handle_inbox_update_pending_trade(bdata: &mut BehaviorData) -> bool { "That only covers {:.0}% of my costs!", (balance0 / balance1 * 100.0).floor() ); - if let Some(tgt_data) = &agent.target { - // If talking with someone in particular, "tell" it only to - // them - if let Some(with) = read_data.uids.get(tgt_data.target) { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_tell( - *agent_data.uid, - *with, - msg, - ), - )); - } else { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_say(*agent_data.uid, msg), - )); - } - } else { - event_emitter.emit(ServerEvent::Chat( - UnresolvedChatMsg::npc_say(*agent_data.uid, msg), - )); - } + message(msg); } if pending.phase != TradePhase::Mutate { // we got into the review phase but without balanced goods, diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index f3102fed6e..342d470dc9 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -4035,44 +4035,48 @@ impl Hud { remove, quantity: &mut u32| { if let Some(prices) = prices { - let balance0 = prices + if let Some((balance0, balance1)) = prices .balance(&trade.offers, &r_inventories, who, true) - .unwrap_or(0.0); // TODO: Don't default to 0 here? - let balance1 = prices - .balance(&trade.offers, &r_inventories, 1 - who, false) - .unwrap_or(0.0); // TODO: Don't default to 0 here? - if let Some(item) = inventory.get(slot) { - if let Some(materials) = - TradePricing::get_materials(&item.item_definition_id()) - { - let unit_price: f32 = materials - .iter() - .map(|e| { - prices - .values - .get(&e.1) - .cloned() - .unwrap_or_default() - * e.0 - * (if ours { - e.1.trade_margin() - } else { - 1.0 - }) - }) - .sum(); + .zip(prices.balance( + &trade.offers, + &r_inventories, + 1 - who, + false, + )) + { + if let Some(item) = inventory.get(slot) { + if let Some(materials) = TradePricing::get_materials( + &item.item_definition_id(), + ) { + let unit_price: f32 = materials + .iter() + .map(|e| { + prices + .values + .get(&e.1) + .cloned() + .unwrap_or_default() + * e.0 + * (if ours { + e.1.trade_margin() + } else { + 1.0 + }) + }) + .sum(); - let mut float_delta = if ours ^ remove { - (balance1 - balance0) / unit_price - } else { - (balance0 - balance1) / unit_price - }; - if ours ^ remove { - float_delta = float_delta.ceil(); - } else { - float_delta = float_delta.floor(); + let mut float_delta = if ours ^ remove { + (balance1 - balance0) / unit_price + } else { + (balance0 - balance1) / unit_price + }; + if ours ^ remove { + float_delta = float_delta.ceil(); + } else { + float_delta = float_delta.floor(); + } + *quantity = float_delta.max(0.0) as u32; } - *quantity = float_delta.max(0.0) as u32; } } } From 71039b56e677386062290e5e52bdbaea29c2e552 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 1 Apr 2023 14:26:11 +0200 Subject: [PATCH 074/139] fix examples --- world/examples/chunk_compression_benchmarks.rs | 2 +- world/examples/world_block_statistics.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/world/examples/chunk_compression_benchmarks.rs b/world/examples/chunk_compression_benchmarks.rs index f4ff72c75b..8186d5bf44 100644 --- a/world/examples/chunk_compression_benchmarks.rs +++ b/world/examples/chunk_compression_benchmarks.rs @@ -755,7 +755,7 @@ fn main() { .map(|v| v + sitepos.as_()) .enumerate() { - let chunk = world.generate_chunk(index.as_index_ref(), spiralpos, || false, None); + let chunk = world.generate_chunk(index.as_index_ref(), spiralpos, None, || false, None); if let Ok((chunk, _)) = chunk { let uncompressed = bincode::serialize(&chunk).unwrap(); let n = uncompressed.len(); diff --git a/world/examples/world_block_statistics.rs b/world/examples/world_block_statistics.rs index 41e9ae4fa4..71ba59c3f8 100644 --- a/world/examples/world_block_statistics.rs +++ b/world/examples/world_block_statistics.rs @@ -100,7 +100,7 @@ fn generate(db_path: &str, ymin: Option, ymax: Option) -> Result<(), B println!("Generating chunk at ({}, {})", x, y); let start_time = SystemTime::now(); if let Ok((chunk, _supplement)) = - world.generate_chunk(index.as_index_ref(), Vec2::new(x, y), || false, None) + world.generate_chunk(index.as_index_ref(), Vec2::new(x, y), None, || false, None) { let end_time = SystemTime::now(); // TODO: can kiddo be made to work without the `Float` bound, so we can use From 57efd245739492d6380846b6e0170b366cd17588 Mon Sep 17 00:00:00 2001 From: Isse Date: Sat, 1 Apr 2023 14:46:45 +0200 Subject: [PATCH 075/139] fix bench --- voxygen/benches/meshing_benchmark.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voxygen/benches/meshing_benchmark.rs b/voxygen/benches/meshing_benchmark.rs index e6af81a14e..9d6d250def 100644 --- a/voxygen/benches/meshing_benchmark.rs +++ b/voxygen/benches/meshing_benchmark.rs @@ -38,7 +38,9 @@ pub fn criterion_benchmark(c: &mut Criterion) { .map(|pos| { ( pos, - world.generate_chunk(index, pos, || false, None).unwrap(), + world + .generate_chunk(index, pos, None, || false, None) + .unwrap(), ) }) .for_each(|(key, chunk)| { From f911e6e5b62b7320646eabfcc64499010679790d Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 18:36:26 +0100 Subject: [PATCH 076/139] Regenerate world site mapping on setup --- rtsim/src/rule/npc_ai.rs | 2 +- rtsim/src/rule/setup.rs | 3 ++- rtsim/src/rule/simulate_npcs.rs | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 2050f42fd7..428e193071 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -352,7 +352,7 @@ fn travel_to_point(wpos: Vec2) -> impl Action { let diff = wpos - start; let n = (diff.magnitude() / WAYPOINT).max(1.0); let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); - traverse_points(move |_| points.next()).boxed() + traverse_points(move |_| points.next()) }) .debug(|| "travel to point") } diff --git a/rtsim/src/rule/setup.rs b/rtsim/src/rule/setup.rs index c6636ef6c1..55ae2cb507 100644 --- a/rtsim/src/rule/setup.rs +++ b/rtsim/src/rule/setup.rs @@ -11,7 +11,7 @@ impl Rule for Setup { rtstate.bind::(|ctx| { let data = &mut *ctx.state.data_mut(); // Delete rtsim sites that don't correspond to a world site - data.sites.retain(|site_id, site| { + data.sites.sites.retain(|site_id, site| { if let Some((world_site_id, _)) = ctx .index .sites @@ -19,6 +19,7 @@ impl Rule for Setup { .find(|(_, world_site)| world_site.get_origin() == site.wpos) { site.world_site = Some(world_site_id); + data.sites.world_site_map.insert(world_site_id, site_id); true } else { warn!( diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 620825ac2c..1498a7a3cf 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -146,7 +146,6 @@ impl Rule for SimulateNpcs { cell.vehicles.push(vehicle_id); } } - } for (npc_id, npc) in data.npcs.npcs.iter_mut() { // Update the NPC's current site, if any From 8d91ebb23e6b57dc597b0bf2c4cfa7c4852b4a64 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 1 Apr 2023 18:44:50 +0100 Subject: [PATCH 077/139] Don't aim character when drinking --- common/src/comp/character_state.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 9e2ea21973..1c3bf62e55 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -257,7 +257,6 @@ impl CharacterState { | CharacterState::Shockwave(_) | CharacterState::BasicBeam(_) | CharacterState::Stunned(_) - | CharacterState::UseItem(_) | CharacterState::Wielding(_) | CharacterState::Talk | CharacterState::FinisherMelee(_) From 364255c7fe4b6dfc6fe2aec1090a4e07df9c9890 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 2 Apr 2023 18:37:38 +0100 Subject: [PATCH 078/139] Allowed rtsim NPCs to greet nearby actors --- Cargo.lock | 1 + client/src/lib.rs | 6 +- common/net/src/msg/client.rs | 2 +- common/net/src/msg/mod.rs | 15 -- common/net/src/msg/server.rs | 2 +- common/src/comp/agent.rs | 19 +- common/src/comp/mod.rs | 3 + common/src/comp/presence.rs | 128 +++++++++++++ common/src/rtsim.rs | 54 +++--- rtsim/src/data/faction.rs | 3 +- rtsim/src/data/mod.rs | 15 -- rtsim/src/data/npc.rs | 52 +++--- rtsim/src/rule/npc_ai.rs | 36 +++- rtsim/src/rule/simulate_npcs.rs | 152 +++++++++------- server/agent/Cargo.toml | 1 + server/agent/src/action_nodes.rs | 16 +- server/agent/src/data.rs | 29 ++- server/src/cmd.rs | 5 +- server/src/events/player.rs | 6 +- server/src/lib.rs | 6 +- server/src/presence.rs | 111 ------------ server/src/rtsim/tick.rs | 18 +- server/src/state_ext.rs | 6 +- server/src/sys/agent/behavior_tree.rs | 82 +++++++-- .../sys/agent/behavior_tree/interaction.rs | 12 +- server/src/sys/agent/data.rs | 169 ------------------ server/src/sys/chunk_serialize.rs | 3 +- server/src/sys/entity_sync.rs | 8 +- server/src/sys/msg/character_screen.rs | 3 +- server/src/sys/msg/in_game.rs | 6 +- server/src/sys/msg/terrain.rs | 4 +- server/src/sys/persistence.rs | 6 +- server/src/sys/subscription.rs | 4 +- server/src/sys/terrain.rs | 12 +- server/src/sys/terrain_sync.rs | 7 +- voxygen/src/hud/mod.rs | 4 +- voxygen/src/scene/mod.rs | 3 +- voxygen/src/session/mod.rs | 8 +- 38 files changed, 453 insertions(+), 564 deletions(-) create mode 100644 common/src/comp/presence.rs delete mode 100644 server/src/sys/agent/data.rs diff --git a/Cargo.lock b/Cargo.lock index fa46bcc324..17a61eee1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7032,6 +7032,7 @@ dependencies = [ "veloren-common-base", "veloren-common-dynlib", "veloren-common-ecs", + "veloren-common-net", "veloren-rtsim", ] diff --git a/client/src/lib.rs b/client/src/lib.rs index 9c5ee7d6a5..665f806f36 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -30,7 +30,7 @@ use common::{ slot::{EquipSlot, InvSlotId, Slot}, CharacterState, ChatMode, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, InputKind, InventoryAction, InventoryEvent, InventoryUpdateEvent, - MapMarkerChange, UtteranceKind, + MapMarkerChange, PresenceKind, UtteranceKind, }, event::{EventBus, LocalEvent, UpdateCharacterMetadata}, grid::Grid, @@ -59,8 +59,8 @@ use common_net::{ self, world_msg::{EconomyInfo, PoiInfo, SiteId, SiteInfo}, ChatTypeContext, ClientGeneral, ClientMsg, ClientRegister, ClientType, DisconnectReason, - InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, PresenceKind, - RegisterError, ServerGeneral, ServerInit, ServerRegisterAnswer, + InviteAnswer, Notification, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, + ServerGeneral, ServerInit, ServerRegisterAnswer, }, sync::WorldSyncExt, }; diff --git a/common/net/src/msg/client.rs b/common/net/src/msg/client.rs index a599b9117a..f752db223d 100644 --- a/common/net/src/msg/client.rs +++ b/common/net/src/msg/client.rs @@ -101,7 +101,7 @@ impl ClientMsg { &self, c_type: ClientType, registered: bool, - presence: Option, + presence: Option, ) -> bool { match self { ClientMsg::Type(t) => c_type == *t, diff --git a/common/net/src/msg/mod.rs b/common/net/src/msg/mod.rs index 429ef7f0d1..316f7429ba 100644 --- a/common/net/src/msg/mod.rs +++ b/common/net/src/msg/mod.rs @@ -19,23 +19,8 @@ pub use self::{ }, world_msg::WorldMapMsg, }; -use common::character::CharacterId; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum PresenceKind { - Spectator, - Character(CharacterId), - Possessor, -} - -impl PresenceKind { - /// Check if the presence represents a control of a character, and thus - /// certain in-game messages from the client such as control inputs - /// should be handled. - pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum PingMsg { Ping, diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 4d158d5345..65d5d6d856 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -296,7 +296,7 @@ impl ServerMsg { &self, c_type: ClientType, registered: bool, - presence: Option, + presence: Option, ) -> bool { match self { ServerMsg::Info(_) | ServerMsg::Init(_) | ServerMsg::RegisterAnswer(_) => { diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 5af82f0c82..71d4113585 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -4,7 +4,7 @@ use crate::{ quadruped_small, ship, Body, UtteranceKind, }, path::Chaser, - rtsim::{Memory, MemoryItem, RtSimController, RtSimEvent}, + rtsim::RtSimController, trade::{PendingTrade, ReducedInventory, SiteId, SitePrices, TradeId, TradeResult}, uid::Uid, }; @@ -713,23 +713,6 @@ impl Agent { } pub fn allowed_to_speak(&self) -> bool { self.behavior.can(BehaviorCapability::SPEAK) } - - pub fn forget_enemy(&mut self, target_name: &str) { - self.rtsim_controller - .events - .push(RtSimEvent::ForgetEnemy(target_name.to_owned())); - } - - pub fn add_fight_to_memory(&mut self, target_name: &str, time: f64) { - self.rtsim_controller - .events - .push(RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterFight { - name: target_name.to_owned(), - }, - time_to_forget: time + 300.0, - })); - } } impl Component for Agent { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 82a7b1e314..a66a22f130 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -38,6 +38,8 @@ pub mod loot_owner; #[cfg(not(target_arch = "wasm32"))] mod player; #[cfg(not(target_arch = "wasm32"))] pub mod poise; #[cfg(not(target_arch = "wasm32"))] +pub mod presence; +#[cfg(not(target_arch = "wasm32"))] pub mod projectile; #[cfg(not(target_arch = "wasm32"))] pub mod shockwave; @@ -107,6 +109,7 @@ pub use self::{ player::DisconnectReason, player::{AliasError, Player, MAX_ALIAS_LEN}, poise::{Poise, PoiseChange, PoiseState}, + presence::{Presence, PresenceKind}, projectile::{Projectile, ProjectileConstructor}, shockwave::{Shockwave, ShockwaveHitEntities}, skillset::{ diff --git a/common/src/comp/presence.rs b/common/src/comp/presence.rs new file mode 100644 index 0000000000..fcb7588993 --- /dev/null +++ b/common/src/comp/presence.rs @@ -0,0 +1,128 @@ +use crate::{character::CharacterId, ViewDistances}; +use serde::{Deserialize, Serialize}; +use specs::Component; +use std::time::{Duration, Instant}; +use vek::*; + +#[derive(Debug)] +pub struct Presence { + pub terrain_view_distance: ViewDistance, + pub entity_view_distance: ViewDistance, + pub kind: PresenceKind, + pub lossy_terrain_compression: bool, +} + +impl Presence { + pub fn new(view_distances: ViewDistances, kind: PresenceKind) -> Self { + let now = Instant::now(); + Self { + terrain_view_distance: ViewDistance::new(view_distances.terrain, now), + entity_view_distance: ViewDistance::new(view_distances.entity, now), + kind, + lossy_terrain_compression: false, + } + } +} + +impl Component for Presence { + type Storage = specs::DenseVecStorage; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PresenceKind { + Spectator, + Character(CharacterId), + Possessor, +} + +impl PresenceKind { + /// Check if the presence represents a control of a character, and thus + /// certain in-game messages from the client such as control inputs + /// should be handled. + pub fn controlling_char(&self) -> bool { matches!(self, Self::Character(_) | Self::Possessor) } +} + +#[derive(PartialEq, Debug, Clone, Copy)] +enum Direction { + Up, + Down, +} + +/// Distance from the [Presence] from which the world is loaded and information +/// is synced to clients. +/// +/// We limit the frequency that changes in the view distance change direction +/// (e.g. shifting from increasing the value to decreasing it). This is useful +/// since we want to avoid rapid cycles of shrinking and expanding of the view +/// distance. +#[derive(Debug)] +pub struct ViewDistance { + direction: Direction, + last_direction_change_time: Instant, + target: Option, + current: u32, +} + +impl ViewDistance { + /// Minimum time allowed between changes in direction of value adjustments. + const TIME_PER_DIR_CHANGE: Duration = Duration::from_millis(300); + + pub fn new(start_value: u32, now: Instant) -> Self { + Self { + direction: Direction::Up, + last_direction_change_time: now.checked_sub(Self::TIME_PER_DIR_CHANGE).unwrap_or(now), + target: None, + current: start_value, + } + } + + /// Returns the current value. + pub fn current(&self) -> u32 { self.current } + + /// Applies deferred change based on the whether the time to apply it has + /// been reached. + pub fn update(&mut self, now: Instant) { + if let Some(target_val) = self.target { + if now.saturating_duration_since(self.last_direction_change_time) + > Self::TIME_PER_DIR_CHANGE + { + self.last_direction_change_time = now; + self.current = target_val; + self.target = None; + } + } + } + + /// Sets the target value. + /// + /// If this hasn't been changed recently or it is in the same direction as + /// the previous change it will be applied immediately. Otherwise, it + /// will be deferred to a later time (limiting the frequency of changes + /// in the change direction). + pub fn set_target(&mut self, new_target: u32, now: Instant) { + use core::cmp::Ordering; + let new_direction = match new_target.cmp(&self.current) { + Ordering::Equal => return, // No change needed. + Ordering::Less => Direction::Down, + Ordering::Greater => Direction::Up, + }; + + // Change is in the same direction as before so we can just apply it. + if new_direction == self.direction { + self.current = new_target; + self.target = None; + // If it has already been a while since the last direction change we can + // directly apply the request and switch the direction. + } else if now.saturating_duration_since(self.last_direction_change_time) + > Self::TIME_PER_DIR_CHANGE + { + self.direction = new_direction; + self.last_direction_change_time = now; + self.current = new_target; + self.target = None; + // Otherwise, we need to defer the request. + } else { + self.target = Some(new_target); + } + } +} diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 9fef179a51..6b04ff8e14 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -3,14 +3,14 @@ // `Agent`). When possible, this should be moved to the `rtsim` // module in `server`. +use crate::character::CharacterId; use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; +use std::collections::VecDeque; use strum::{EnumIter, IntoEnumIterator}; use vek::*; -use crate::comp::dialogue::MoodState; - slotmap::new_key_type! { pub struct NpcId; } slotmap::new_key_type! { pub struct VehicleId; } @@ -26,6 +26,21 @@ impl Component for RtSimEntity { type Storage = specs::VecStorage; } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum Actor { + Npc(NpcId), + Character(CharacterId), +} + +impl Actor { + pub fn npc(&self) -> Option { + match self { + Actor::Npc(id) => Some(*id), + Actor::Character(_) => None, + } + } +} + #[derive(Copy, Clone, Debug)] pub struct RtSimVehicle(pub VehicleId); @@ -33,29 +48,6 @@ impl Component for RtSimVehicle { type Storage = specs::VecStorage; } -#[derive(Clone, Debug)] -pub enum RtSimEvent { - AddMemory(Memory), - SetMood(Memory), - ForgetEnemy(String), - PrintMemories, -} - -#[derive(Clone, Debug)] -pub struct Memory { - pub item: MemoryItem, - pub time_to_forget: f64, -} - -#[derive(Clone, Debug)] -pub enum MemoryItem { - // These are structs to allow more data beyond name to be stored - // such as clothing worn, weapon used, etc. - CharacterInteraction { name: String }, - CharacterFight { name: String }, - Mood { state: MoodState }, -} - #[derive(EnumIter, Clone, Copy)] pub enum PersonalityTrait { Open, @@ -210,8 +202,7 @@ pub struct RtSimController { pub heading_to: Option, /// Proportion of full speed to move pub speed_factor: f32, - /// Events - pub events: Vec, + pub actions: VecDeque, } impl Default for RtSimController { @@ -221,7 +212,7 @@ impl Default for RtSimController { personality: Personality::default(), heading_to: None, speed_factor: 1.0, - events: Vec::new(), + actions: VecDeque::new(), } } } @@ -233,11 +224,16 @@ impl RtSimController { personality: Personality::default(), heading_to: None, speed_factor: 0.5, - events: Vec::new(), + actions: VecDeque::new(), } } } +#[derive(Clone, Copy, Debug)] +pub enum NpcAction { + Greet(Actor), +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] pub enum ChunkResource { #[serde(rename = "0")] diff --git a/rtsim/src/data/faction.rs b/rtsim/src/data/faction.rs index 65322e5097..4a4504825f 100644 --- a/rtsim/src/data/faction.rs +++ b/rtsim/src/data/faction.rs @@ -1,5 +1,4 @@ -use super::Actor; -pub use common::rtsim::FactionId; +pub use common::rtsim::{Actor, FactionId}; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::ops::{Deref, DerefMut}; diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index 5b537c1ac6..45a212cd26 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -20,21 +20,6 @@ use std::{ marker::PhantomData, }; -#[derive(Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum Actor { - Npc(NpcId), - Character(common::character::CharacterId), -} - -impl Actor { - pub fn npc(&self) -> Option { - match self { - Actor::Npc(id) => Some(*id), - Actor::Character(_) => None, - } - } -} - #[derive(Clone, Serialize, Deserialize)] pub struct Data { pub nature: Nature, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 6c0c26e6f1..99f4c5de96 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,7 +3,7 @@ pub use common::rtsim::{NpcId, Profession}; use common::{ comp, grid::Grid, - rtsim::{FactionId, Personality, SiteId, VehicleId}, + rtsim::{Actor, FactionId, NpcAction, Personality, SiteId, VehicleId}, store::Id, vol::RectVolSize, }; @@ -21,8 +21,6 @@ use world::{ util::{RandomPerm, LOCALITY}, }; -use super::Actor; - #[derive(Copy, Clone, Debug, Default)] pub enum SimulationMode { /// The NPC is unloaded and is being simulated via rtsim. @@ -45,24 +43,21 @@ pub struct PathingMemory { pub intersite_path: Option<(PathData<(Id, bool), SiteId>, usize)>, } -#[derive(Clone, Copy)] -pub enum NpcAction { - /// (wpos, speed_factor) - Goto(Vec3, f32), -} - +#[derive(Default)] pub struct Controller { - pub action: Option, + pub actions: Vec, + /// (wpos, speed_factor) + pub goto: Option<(Vec3, f32)>, } impl Controller { - pub fn idle() -> Self { Self { action: None } } + pub fn do_idle(&mut self) { self.goto = None; } - pub fn goto(wpos: Vec3, speed_factor: f32) -> Self { - Self { - action: Some(NpcAction::Goto(wpos, speed_factor)), - } + pub fn do_goto(&mut self, wpos: Vec3, speed_factor: f32) { + self.goto = Some((wpos, speed_factor)); } + + pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } } pub struct Brain { @@ -91,7 +86,7 @@ pub struct Npc { pub current_site: Option, #[serde(skip_serializing, skip_deserializing)] - pub action: Option, + pub controller: Controller, /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// server, loaded corresponds to being within a loaded chunk). When in @@ -118,7 +113,7 @@ impl Clone for Npc { // Not persisted chunk_pos: None, current_site: Default::default(), - action: Default::default(), + controller: Default::default(), mode: Default::default(), brain: Default::default(), } @@ -138,7 +133,7 @@ impl Npc { riding: None, chunk_pos: None, current_site: None, - action: None, + controller: Controller::default(), mode: SimulationMode::Simulated, brain: None, } @@ -248,6 +243,7 @@ impl Vehicle { #[derive(Default, Clone, Serialize, Deserialize)] pub struct GridCell { pub npcs: Vec, + pub characters: Vec, pub vehicles: Vec, } @@ -269,23 +265,33 @@ impl Npcs { } /// Queries nearby npcs, not garantueed to work if radius > 32.0 - pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { + pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { let chunk_pos = wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); let r_sqr = radius * radius; LOCALITY .into_iter() - .filter_map(move |neighbor| { - self.npc_grid.get(chunk_pos + neighbor).map(|cell| { + .flat_map(move |neighbor| { + self.npc_grid.get(chunk_pos + neighbor).map(move |cell| { cell.npcs .iter() .copied() - .filter(|npc| { + .filter(move |npc| { self.npcs .get(*npc) .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) }) - .collect::>() + .map(Actor::Npc) + .chain(cell.characters + .iter() + .copied() + // TODO: Filter characters by distance too + // .filter(move |npc| { + // self.npcs + // .get(*npc) + // .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) + // }) + .map(Actor::Character)) }) }) .flatten() diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 428e193071..718c04535a 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -3,7 +3,7 @@ use std::hash::BuildHasherDefault; use crate::{ ai::{casual, choose, finish, important, just, now, seq, until, urgent, Action, NpcCtx}, data::{ - npc::{Brain, Controller, PathData}, + npc::{Brain, PathData}, Sites, }, event::OnTick, @@ -217,7 +217,7 @@ impl Rule for NpcAi { data.npcs .iter_mut() .map(|(npc_id, npc)| { - let controller = Controller { action: npc.action }; + let controller = std::mem::take(&mut npc.controller); let brain = npc.brain.take().unwrap_or_else(|| Brain { action: Box::new(think().repeat()), }); @@ -251,7 +251,7 @@ impl Rule for NpcAi { // Reinsert NPC brains let mut data = ctx.state.data_mut(); for (npc_id, controller, brain) in npc_data { - data.npcs[npc_id].action = controller.action; + data.npcs[npc_id].controller = controller; data.npcs[npc_id].brain = Some(brain); } }); @@ -260,7 +260,7 @@ impl Rule for NpcAi { } } -fn idle() -> impl Action { just(|ctx| *ctx.controller = Controller::idle()).debug(|| "idle") } +fn idle() -> impl Action { just(|ctx| ctx.controller.do_idle()).debug(|| "idle") } /// Try to walk toward a 3D position without caring for obstacles. fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { @@ -292,7 +292,7 @@ fn goto(wpos: Vec3, speed_factor: f32, goal_dist: f32) -> impl Action { ) }); - *ctx.controller = Controller::goto(*waypoint, speed_factor); + ctx.controller.do_goto(*waypoint, speed_factor); }) .repeat() .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)) @@ -452,6 +452,24 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { move |ctx| ctx.time.0 > *timeout.get_or_insert(ctx.time.0 + time) } +fn socialize() -> impl Action { + just(|ctx| { + let mut rng = thread_rng(); + // TODO: Bit odd, should wait for a while after greeting + if thread_rng().gen_bool(0.0002) { + if let Some(other) = ctx + .state + .data() + .npcs + .nearby(ctx.npc.wpos.xy(), 8.0) + .choose(&mut rng) + { + ctx.controller.do_greet(other); + } + } + }) +} + fn adventure() -> impl Action { choose(|ctx| { // Choose a random site that's fairly close by @@ -540,7 +558,7 @@ fn villager(visiting_site: SiteId) -> impl Action { { travel_to_point(house_wpos) .debug(|| "walk to house") - .then(idle().repeat().debug(|| "wait in house")) + .then(socialize().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) .map(|_| ()) .boxed() @@ -570,7 +588,7 @@ fn villager(visiting_site: SiteId) -> impl Action { // ...then wait for some time before moving on .then({ let wait_time = thread_rng().gen_range(10.0..30.0); - idle().repeat().stop_if(timeout(wait_time)) + socialize().repeat().stop_if(timeout(wait_time)) .debug(|| "wait at plaza") }) .map(|_| ()) @@ -735,7 +753,7 @@ fn humanoid() -> impl Action { casual(finish()) } } else { - important(idle()) + important(socialize()) } } else if matches!( ctx.npc.profession, @@ -806,6 +824,6 @@ fn think() -> impl Action { choose(|ctx| match ctx.npc.body { common::comp::Body::Humanoid(_) => casual(humanoid()), common::comp::Body::BirdLarge(_) => casual(bird_large()), - _ => casual(idle()), + _ => casual(socialize()), }) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 1498a7a3cf..8a514857e2 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -6,7 +6,7 @@ use crate::{ use common::{ comp::{self, Body}, grid::Grid, - rtsim::Personality, + rtsim::{Actor, Personality}, terrain::TerrainChunkSize, vol::RectVolSize, }; @@ -25,7 +25,7 @@ impl Rule for SimulateNpcs { for (npc_id, npc) in data.npcs.npcs.iter() { if let Some(ride) = &npc.riding { if let Some(vehicle) = data.npcs.vehicles.get_mut(ride.vehicle) { - let actor = crate::data::Actor::Npc(npc_id); + let actor = Actor::Npc(npc_id); vehicle.riders.push(actor); if ride.steering && vehicle.driver.replace(actor).is_some() { panic!("Replaced driver"); @@ -153,9 +153,12 @@ impl Rule for SimulateNpcs { .world .sim() .get(npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_()) - .and_then(|chunk| chunk.sites - .iter() - .find_map(|site| data.sites.world_site_map.get(site).copied())); + .and_then(|chunk| { + chunk + .sites + .iter() + .find_map(|site| data.sites.world_site_map.get(site).copied()) + }); let chunk_pos = npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); @@ -176,83 +179,98 @@ impl Rule for SimulateNpcs { // Simulate the NPC's movement and interactions if matches!(npc.mode, SimulationMode::Simulated) { - if let Some(riding) = &npc.riding { - if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { - if let Some(action) = npc.action && riding.steering { - match action { - crate::data::npc::NpcAction::Goto(target, speed_factor) => { - let diff = target.xy() - vehicle.wpos.xy(); - let dist2 = diff.magnitude_squared(); + // Move NPCs if they have a target destination + if let Some((target, speed_factor)) = npc.controller.goto { + // Simulate NPC movement when riding + if let Some(riding) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { + // If steering, the NPC controls the vehicle's motion + if riding.steering { + let diff = target.xy() - vehicle.wpos.xy(); + let dist2 = diff.magnitude_squared(); - if dist2 > 0.5f32.powi(2) { - let mut wpos = vehicle.wpos + (diff - * (vehicle.get_speed() * speed_factor * ctx.event.dt + if dist2 > 0.5f32.powi(2) { + let mut wpos = vehicle.wpos + + (diff + * (vehicle.get_speed() + * speed_factor + * ctx.event.dt / dist2.sqrt()) .min(1.0)) .with_z(0.0); - let is_valid = match vehicle.body { - common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => true, - common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { - let chunk_pos = wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); - ctx.world.sim().get(chunk_pos).map_or(true, |f| f.river.river_kind.is_some()) - }, - _ => false, - }; + let is_valid = match vehicle.body { + common::comp::ship::Body::DefaultAirship + | common::comp::ship::Body::AirBalloon => true, + common::comp::ship::Body::SailBoat + | common::comp::ship::Body::Galleon => { + let chunk_pos = wpos.xy().as_::() + / TerrainChunkSize::RECT_SIZE.as_::(); + ctx.world + .sim() + .get(chunk_pos) + .map_or(true, |f| f.river.river_kind.is_some()) + }, + _ => false, + }; - if is_valid { - match vehicle.body { - common::comp::ship::Body::DefaultAirship | common::comp::ship::Body::AirBalloon => { - if let Some(alt) = ctx.world.sim().get_alt_approx(wpos.xy().as_()).filter(|alt| wpos.z < *alt) { - wpos.z = alt; - } - }, - common::comp::ship::Body::SailBoat | common::comp::ship::Body::Galleon => { - wpos.z = ctx - .world - .sim() - .get_interpolated(wpos.xy().map(|e| e as i32), |chunk| chunk.water_alt) - .unwrap_or(0.0); - }, - _ => {}, - } - vehicle.wpos = wpos; + if is_valid { + match vehicle.body { + common::comp::ship::Body::DefaultAirship + | common::comp::ship::Body::AirBalloon => { + if let Some(alt) = ctx + .world + .sim() + .get_alt_approx(wpos.xy().as_()) + .filter(|alt| wpos.z < *alt) + { + wpos.z = alt; + } + }, + common::comp::ship::Body::SailBoat + | common::comp::ship::Body::Galleon => { + wpos.z = ctx + .world + .sim() + .get_interpolated( + wpos.xy().map(|e| e as i32), + |chunk| chunk.water_alt, + ) + .unwrap_or(0.0); + }, + _ => {}, } + vehicle.wpos = wpos; } } } + npc.wpos = vehicle.wpos; + } else { + // Vehicle doens't exist anymore + npc.riding = None; } - npc.wpos = vehicle.wpos; + // If not riding, we assume they're just walking } else { - // Vehicle doens't exist anymore - npc.riding = None; + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); + + if dist2 > 0.5f32.powi(2) { + npc.wpos += (diff + * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } } } - // Move NPCs if they have a target destination - else if let Some(action) = npc.action { - match action { - crate::data::npc::NpcAction::Goto(target, speed_factor) => { - let diff = target.xy() - npc.wpos.xy(); - let dist2 = diff.magnitude_squared(); - - if dist2 > 0.5f32.powi(2) { - npc.wpos += (diff - * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); - } - }, - } - - // 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(); - } + // 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(); } } }); diff --git a/server/agent/Cargo.toml b/server/agent/Cargo.toml index 9009caae63..1c617fc1e6 100644 --- a/server/agent/Cargo.toml +++ b/server/agent/Cargo.toml @@ -11,6 +11,7 @@ be-dyn-lib = [] [dependencies] common = { package = "veloren-common", path = "../../common"} common-base = { package = "veloren-common-base", path = "../../common/base" } +common-net = { package = "veloren-common-net", path = "../../common/net" } common-ecs = { package = "veloren-common-ecs", path = "../../common/ecs" } common-dynlib = { package = "veloren-common-dynlib", path = "../../common/dynlib", optional = true} rtsim = { package = "veloren-rtsim", path = "../../rtsim" } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 115cc5fb11..d1ab7689ed 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -229,10 +229,11 @@ impl<'a> AgentData<'a> { controller.push_cancel_input(InputKind::Fly) } - let chase_tgt = *travel_to/*read_data.terrain + let chase_tgt = read_data + .terrain .try_find_space(travel_to.as_()) .map(|pos| pos.as_()) - .unwrap_or(*travel_to)*/; + .unwrap_or(*travel_to); if let Some((bearing, speed)) = agent.chaser.chase( &*read_data.terrain, @@ -1460,11 +1461,12 @@ impl<'a> AgentData<'a> { self.idle(agent, controller, read_data, rng); } else { let target_data = TargetData::new(tgt_pos, target, read_data); - if let Some(tgt_name) = - read_data.stats.get(target).map(|stats| stats.name.clone()) - { - agent.add_fight_to_memory(&tgt_name, read_data.time.0) - } + // TODO: Reimplement this in rtsim + // if let Some(tgt_name) = + // read_data.stats.get(target).map(|stats| stats.name.clone()) + // { + // agent.add_fight_to_memory(&tgt_name, read_data.time.0) + // } self.attack(agent, controller, &target_data, read_data, rng); } } diff --git a/server/agent/src/data.rs b/server/agent/src/data.rs index 10f3739f33..6ca3b9c9ce 100644 --- a/server/agent/src/data.rs +++ b/server/agent/src/data.rs @@ -6,21 +6,21 @@ use common::{ group, item::MaterialStatManifest, ActiveAbilities, Alignment, Body, CharacterState, Combo, Energy, Health, Inventory, - LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, Stance, Stats, - Vel, + LightEmitter, LootOwner, Ori, PhysicsState, Poise, Pos, Presence, PresenceKind, Scale, + SkillSet, Stance, Stats, Vel, }, link::Is, mounting::{Mount, Rider}, path::TraversalConfig, resources::{DeltaTime, Time, TimeOfDay}, - rtsim::RtSimEntity, + rtsim::{Actor, RtSimEntity}, states::utils::{ForcedMovement, StageSection}, terrain::TerrainGrid, uid::{Uid, UidAllocator}, }; use specs::{ - shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, - World, + shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, + SystemData, World, }; // TODO: Move rtsim back into AgentData after rtsim2 when it has a separate @@ -246,6 +246,25 @@ pub struct ReadData<'a> { pub msm: ReadExpect<'a, MaterialStatManifest>, pub poises: ReadStorage<'a, Poise>, pub stances: ReadStorage<'a, Stance>, + pub presences: ReadStorage<'a, Presence>, +} + +impl<'a> ReadData<'a> { + pub fn lookup_actor(&self, actor: Actor) -> Option { + // TODO: We really shouldn't be doing a linear search here. The only saving + // grace is that the set of entities that fit each case should be + // *relatively* small. + match actor { + Actor::Character(character_id) => (&self.entities, &self.presences) + .join() + .find(|(_, p)| p.kind == PresenceKind::Character(character_id)) + .map(|(entity, _)| entity), + Actor::Npc(npc_id) => (&self.entities, &self.rtsim_entities) + .join() + .find(|(_, e)| e.0 == npc_id) + .map(|(entity, _)| entity), + } + } } pub enum Path { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index acc753ce1a..4459993b97 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -5,7 +5,6 @@ use crate::{ client::Client, location::Locations, login_provider::LoginProvider, - presence::Presence, settings::{ Ban, BanAction, BanInfo, EditableSetting, SettingError, WhitelistInfo, WhitelistRecord, }, @@ -31,7 +30,7 @@ use common::{ buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource}, inventory::item::{tool::AbilityMap, MaterialStatManifest, Quality}, invite::InviteKind, - AdminRole, ChatType, Inventory, Item, LightEmitter, WaypointArea, + AdminRole, ChatType, Inventory, Item, LightEmitter, Presence, PresenceKind, WaypointArea, }, depot, effect::Effect, @@ -49,7 +48,7 @@ use common::{ weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; use common_net::{ - msg::{DisconnectReason, Notification, PlayerListUpdate, PresenceKind, ServerGeneral}, + msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; use common_state::{BuildAreaError, BuildAreas}; diff --git a/server/src/events/player.rs b/server/src/events/player.rs index a634686cb3..3a0cdce141 100644 --- a/server/src/events/player.rs +++ b/server/src/events/player.rs @@ -1,16 +1,16 @@ use super::Event; use crate::{ client::Client, metrics::PlayerMetrics, persistence::character_updater::CharacterUpdater, - presence::Presence, state_ext::StateExt, BattleModeBuffer, Server, + state_ext::StateExt, BattleModeBuffer, Server, }; use common::{ character::CharacterId, comp, - comp::{group, pet::is_tameable}, + comp::{group, pet::is_tameable, Presence, PresenceKind}, uid::{Uid, UidAllocator}, }; use common_base::span; -use common_net::msg::{PlayerListUpdate, PresenceKind, ServerGeneral}; +use common_net::msg::{PlayerListUpdate, ServerGeneral}; use common_state::State; use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt}; use tracing::{debug, error, trace, warn, Instrument}; diff --git a/server/src/lib.rs b/server/src/lib.rs index 3ae719da85..c79365524d 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -62,7 +62,7 @@ use crate::{ location::Locations, login_provider::LoginProvider, persistence::PersistedComponents, - presence::{Presence, RegionSubscription, RepositionOnChunkLoad}, + presence::{RegionSubscription, RepositionOnChunkLoad}, state_ext::StateExt, sys::sentinel::DeletedEntities, }; @@ -376,7 +376,7 @@ impl Server { // Server-only components state.ecs_mut().register::(); state.ecs_mut().register::(); - state.ecs_mut().register::(); + state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); @@ -833,7 +833,7 @@ impl Server { ( &self.state.ecs().entities(), &self.state.ecs().read_storage::(), - !&self.state.ecs().read_storage::(), + !&self.state.ecs().read_storage::(), self.state.ecs().read_storage::().maybe(), ) .join() diff --git a/server/src/presence.rs b/server/src/presence.rs index 99c7c10c4c..732225a205 100644 --- a/server/src/presence.rs +++ b/server/src/presence.rs @@ -1,34 +1,8 @@ -use common_net::msg::PresenceKind; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; use specs::{Component, NullStorage}; -use std::time::{Duration, Instant}; use vek::*; -#[derive(Debug)] -pub struct Presence { - pub terrain_view_distance: ViewDistance, - pub entity_view_distance: ViewDistance, - pub kind: PresenceKind, - pub lossy_terrain_compression: bool, -} - -impl Presence { - pub fn new(view_distances: common::ViewDistances, kind: PresenceKind) -> Self { - let now = Instant::now(); - Self { - terrain_view_distance: ViewDistance::new(view_distances.terrain, now), - entity_view_distance: ViewDistance::new(view_distances.entity, now), - kind, - lossy_terrain_compression: false, - } - } -} - -impl Component for Presence { - type Storage = specs::DenseVecStorage; -} - // Distance from fuzzy_chunk before snapping to current chunk pub const CHUNK_FUZZ: u32 = 2; // Distance out of the range of a region before removing it from subscriptions @@ -51,88 +25,3 @@ pub struct RepositionOnChunkLoad; impl Component for RepositionOnChunkLoad { type Storage = NullStorage; } - -#[derive(PartialEq, Debug, Clone, Copy)] -enum Direction { - Up, - Down, -} - -/// Distance from the [Presence] from which the world is loaded and information -/// is synced to clients. -/// -/// We limit the frequency that changes in the view distance change direction -/// (e.g. shifting from increasing the value to decreasing it). This is useful -/// since we want to avoid rapid cycles of shrinking and expanding of the view -/// distance. -#[derive(Debug)] -pub struct ViewDistance { - direction: Direction, - last_direction_change_time: Instant, - target: Option, - current: u32, -} - -impl ViewDistance { - /// Minimum time allowed between changes in direction of value adjustments. - const TIME_PER_DIR_CHANGE: Duration = Duration::from_millis(300); - - pub fn new(start_value: u32, now: Instant) -> Self { - Self { - direction: Direction::Up, - last_direction_change_time: now.checked_sub(Self::TIME_PER_DIR_CHANGE).unwrap_or(now), - target: None, - current: start_value, - } - } - - /// Returns the current value. - pub fn current(&self) -> u32 { self.current } - - /// Applies deferred change based on the whether the time to apply it has - /// been reached. - pub fn update(&mut self, now: Instant) { - if let Some(target_val) = self.target { - if now.saturating_duration_since(self.last_direction_change_time) - > Self::TIME_PER_DIR_CHANGE - { - self.last_direction_change_time = now; - self.current = target_val; - self.target = None; - } - } - } - - /// Sets the target value. - /// - /// If this hasn't been changed recently or it is in the same direction as - /// the previous change it will be applied immediately. Otherwise, it - /// will be deferred to a later time (limiting the frequency of changes - /// in the change direction). - pub fn set_target(&mut self, new_target: u32, now: Instant) { - use core::cmp::Ordering; - let new_direction = match new_target.cmp(&self.current) { - Ordering::Equal => return, // No change needed. - Ordering::Less => Direction::Down, - Ordering::Greater => Direction::Up, - }; - - // Change is in the same direction as before so we can just apply it. - if new_direction == self.direction { - self.current = new_target; - self.target = None; - // If it has already been a while since the last direction change we can - // directly apply the request and switch the direction. - } else if now.saturating_duration_since(self.last_direction_change_time) - > Self::TIME_PER_DIR_CHANGE - { - self.direction = new_direction; - self.last_direction_change_time = now; - self.current = new_target; - self.target = None; - // Otherwise, we need to defer the request. - } else { - self.target = Some(new_target); - } - } -} diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 443957a097..ca0544da03 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -7,7 +7,7 @@ use common::{ event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, - rtsim::{RtSimEntity, RtSimVehicle}, + rtsim::{Actor, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, terrain::CoordinateConversions, trade::{Good, SiteInformation}, @@ -16,7 +16,7 @@ use common::{ use common_ecs::{Job, Origin, Phase, System}; use rtsim::data::{ npc::{Profession, SimulationMode}, - Actor, Npc, Sites, + Npc, Sites, }; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use std::{sync::Arc, time::Duration}; @@ -366,17 +366,17 @@ impl<'a> System<'a> for Sys { // Update entity state if let Some(agent) = agent { agent.rtsim_controller.personality = npc.personality; - if let Some(action) = npc.action { - match action { - rtsim::data::npc::NpcAction::Goto(wpos, sf) => { - agent.rtsim_controller.travel_to = Some(wpos); - agent.rtsim_controller.speed_factor = sf; - }, - } + if let Some((wpos, speed_factor)) = npc.controller.goto { + agent.rtsim_controller.travel_to = Some(wpos); + agent.rtsim_controller.speed_factor = speed_factor; } else { agent.rtsim_controller.travel_to = None; agent.rtsim_controller.speed_factor = 1.0; } + agent + .rtsim_controller + .actions + .extend(std::mem::take(&mut npc.controller.actions)); } }); } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a939bd113a..4cc1f23cc4 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -4,7 +4,7 @@ use crate::{ events::{self, update_map_markers}, persistence::PersistedComponents, pet::restore_pet, - presence::{Presence, RepositionOnChunkLoad}, + presence::RepositionOnChunkLoad, rtsim::RtSim, settings::Settings, sys::sentinel::DeletedEntities, @@ -19,7 +19,7 @@ use common::{ self, item::{ItemKind, MaterialStatManifest}, skills::{GeneralSkill, Skill}, - ChatType, Group, Inventory, Item, Player, Poise, + ChatType, Group, Inventory, Item, Player, Poise, Presence, PresenceKind, }, effect::Effect, link::{Link, LinkHandle}, @@ -30,7 +30,7 @@ use common::{ LoadoutBuilder, ViewDistances, }; use common_net::{ - msg::{CharacterInfo, PlayerListUpdate, PresenceKind, ServerGeneral}, + msg::{CharacterInfo, PlayerListUpdate, ServerGeneral}, sync::WorldSyncExt, }; use common_state::State; diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index ce2fd6891a..6e40207ca8 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -9,7 +9,7 @@ use common::{ }, event::{Emitter, ServerEvent}, path::TraversalConfig, - rtsim::RtSimEntity, + rtsim::{NpcAction, RtSimEntity}, }; use rand::{prelude::ThreadRng, thread_rng, Rng}; use specs::{ @@ -160,7 +160,11 @@ impl BehaviorTree { /// Idle BehaviorTree pub fn idle() -> Self { Self { - tree: vec![set_owner_if_no_target, handle_timed_events], + tree: vec![ + set_owner_if_no_target, + handle_rtsim_actions, + handle_timed_events, + ], } } @@ -464,6 +468,42 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool { false } +/// Handle action requests from rtsim, such as talking to NPCs or attacking +fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { + if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() { + match action { + NpcAction::Greet(actor) => { + if bdata.agent.allowed_to_speak() { + if let Some(target) = bdata.read_data.lookup_actor(actor) { + let target_pos = bdata.read_data.positions.get(target).map(|pos| pos.0); + + bdata.agent.target = Some(Target::new( + target, + false, + bdata.read_data.time.0, + false, + target_pos, + )); + + if bdata.agent_data.look_toward( + &mut bdata.controller, + &bdata.read_data, + target, + ) { + bdata.controller.push_utterance(UtteranceKind::Greeting); + bdata.controller.push_action(ControlAction::Talk); + bdata + .agent_data + .chat_npc("npc-speech-villager", &mut bdata.event_emitter); + } + } + } + }, + } + } + false +} + /// Handle timed events, like looking at the player we are talking to fn handle_timed_events(bdata: &mut BehaviorData) -> bool { let timeout = if bdata.agent.behavior.is(BehaviorState::TRADING) { @@ -746,9 +786,11 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { if aggro_on { let target_data = TargetData::new(tgt_pos, target, read_data); - let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); + // let tgt_name = read_data.stats.get(target).map(|stats| stats.name.clone()); - tgt_name.map(|tgt_name| agent.add_fight_to_memory(&tgt_name, read_data.time.0)); + // TODO: Reimplement in rtsim2 + // tgt_name.map(|tgt_name| agent.add_fight_to_memory(&tgt_name, + // read_data.time.0)); agent_data.attack(agent, controller, &target_data, read_data, rng); } else { agent_data.menacing( @@ -760,7 +802,9 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { rng, remembers_fight_with(agent_data.rtsim_entity, read_data, target), ); - remember_fight(agent_data.rtsim_entity, read_data, agent, target); + // TODO: Reimplement in rtsim2 + // remember_fight(agent_data.rtsim_entity, read_data, agent, + // target); } } } @@ -784,17 +828,17 @@ fn remembers_fight_with( false } -/// Remember target. -fn remember_fight( - rtsim_entity: Option<&RtSimEntity>, - read_data: &ReadData, - agent: &mut Agent, - target: EcsEntity, -) { - rtsim_entity.is_some().then(|| { - read_data - .stats - .get(target) - .map(|stats| agent.add_fight_to_memory(&stats.name, read_data.time.0)) - }); -} +// /// Remember target. +// fn remember_fight( +// rtsim_entity: Option<&RtSimEntity>, +// read_data: &ReadData, +// agent: &mut Agent, +// target: EcsEntity, +// ) { +// rtsim_entity.is_some().then(|| { +// read_data +// .stats +// .get(target) +// .map(|stats| agent.add_fight_to_memory(&stats.name, +// read_data.time.0)) }); +// } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 45b05a843f..567758d4f1 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -9,7 +9,7 @@ use common::{ BehaviorState, ControlAction, Item, TradingBehavior, UnresolvedChatMsg, UtteranceKind, }, event::ServerEvent, - rtsim::{Memory, MemoryItem, PersonalityTrait, RtSimEvent}, + rtsim::PersonalityTrait, trade::{TradeAction, TradePhase, TradeResult}, }; use rand::{thread_rng, Rng}; @@ -106,15 +106,6 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { match subject { Subject::Regular => { if let Some(tgt_stats) = read_data.stats.get(target) { - agent - .rtsim_controller - .events - .push(RtSimEvent::AddMemory(Memory { - item: MemoryItem::CharacterInteraction { - name: tgt_stats.name.clone(), - }, - time_to_forget: read_data.time.0 + 600.0, - })); if let Some(destination_name) = &agent.rtsim_controller.heading_to { let personality = &agent.rtsim_controller.personality; let standard_response_msg = || -> String { @@ -252,6 +243,7 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { } }, Subject::Mood => { + // TODO: Reimplement in rtsim2 /* if let Some(rtsim_entity) = &bdata.rtsim_entity { if !rtsim_entity.brain.remembers_mood() { diff --git a/server/src/sys/agent/data.rs b/server/src/sys/agent/data.rs deleted file mode 100644 index 0b13b2524d..0000000000 --- a/server/src/sys/agent/data.rs +++ /dev/null @@ -1,169 +0,0 @@ -// use crate::rtsim::Entity as RtSimData; -use common::{ - comp::{ - buff::Buffs, group, item::MaterialStatManifest, ActiveAbilities, Alignment, Body, - CharacterState, Combo, Energy, Health, Inventory, LightEmitter, LootOwner, Ori, - PhysicsState, Pos, Scale, SkillSet, Stats, Vel, - }, - link::Is, - mounting::Mount, - path::TraversalConfig, - resources::{DeltaTime, Time, TimeOfDay}, - rtsim::RtSimEntity, - terrain::TerrainGrid, - uid::{Uid, UidAllocator}, -}; -use specs::{ - shred::ResourceId, Entities, Entity as EcsEntity, Read, ReadExpect, ReadStorage, SystemData, - World, -}; -use std::sync::Arc; - -pub struct AgentData<'a> { - pub entity: &'a EcsEntity, - pub rtsim_entity: Option<&'a RtSimEntity>, - //pub rtsim_entity: Option<&'a RtSimData>, - pub uid: &'a Uid, - pub pos: &'a Pos, - pub vel: &'a Vel, - pub ori: &'a Ori, - pub energy: &'a Energy, - pub body: Option<&'a Body>, - pub inventory: &'a Inventory, - pub skill_set: &'a SkillSet, - #[allow(dead_code)] // may be useful for pathing - pub physics_state: &'a PhysicsState, - pub alignment: Option<&'a Alignment>, - pub traversal_config: TraversalConfig, - pub scale: f32, - pub damage: f32, - pub light_emitter: Option<&'a LightEmitter>, - pub glider_equipped: bool, - pub is_gliding: bool, - pub health: Option<&'a Health>, - pub char_state: &'a CharacterState, - pub active_abilities: &'a ActiveAbilities, - pub cached_spatial_grid: &'a common::CachedSpatialGrid, - pub msm: &'a MaterialStatManifest, -} - -pub struct TargetData<'a> { - pub pos: &'a Pos, - pub body: Option<&'a Body>, - pub scale: Option<&'a Scale>, -} - -impl<'a> TargetData<'a> { - pub fn new(pos: &'a Pos, body: Option<&'a Body>, scale: Option<&'a Scale>) -> Self { - Self { pos, body, scale } - } -} - -pub struct AttackData { - pub min_attack_dist: f32, - pub dist_sqrd: f32, - pub angle: f32, - pub angle_xy: f32, -} - -impl AttackData { - pub fn in_min_range(&self) -> bool { self.dist_sqrd < self.min_attack_dist.powi(2) } -} - -#[derive(Eq, PartialEq)] -// When adding a new variant, first decide if it should instead fall under one -// of the pre-existing tactics -pub enum Tactic { - // General tactics - SimpleMelee, - SimpleBackstab, - ElevatedRanged, - Turret, - FixedTurret, - RotatingTurret, - RadialTurret, - - // Tool specific tactics - Axe, - Hammer, - Sword, - Bow, - Staff, - Sceptre, - - // Broad creature tactics - CircleCharge { radius: u32, circle_time: u32 }, - QuadLowRanged, - TailSlap, - QuadLowQuick, - QuadLowBasic, - QuadLowBeam, - QuadMedJump, - QuadMedBasic, - Theropod, - BirdLargeBreathe, - BirdLargeFire, - BirdLargeBasic, - ArthropodMelee, - ArthropodRanged, - ArthropodAmbush, - - // Specific species tactics - Mindflayer, - Minotaur, - ClayGolem, - TidalWarrior, - Yeti, - Harvester, - StoneGolem, - Deadwood, - Mandragora, - WoodGolem, - GnarlingChieftain, - OrganAura, - Dagon, - Cardinal, -} - -#[derive(SystemData)] -pub struct ReadData<'a> { - pub entities: Entities<'a>, - pub uid_allocator: Read<'a, UidAllocator>, - pub dt: Read<'a, DeltaTime>, - pub time: Read<'a, Time>, - pub cached_spatial_grid: Read<'a, common::CachedSpatialGrid>, - pub group_manager: Read<'a, group::GroupManager>, - pub energies: ReadStorage<'a, Energy>, - pub positions: ReadStorage<'a, Pos>, - pub velocities: ReadStorage<'a, Vel>, - pub orientations: ReadStorage<'a, Ori>, - pub scales: ReadStorage<'a, Scale>, - pub healths: ReadStorage<'a, Health>, - pub inventories: ReadStorage<'a, Inventory>, - pub stats: ReadStorage<'a, Stats>, - pub skill_set: ReadStorage<'a, SkillSet>, - pub physics_states: ReadStorage<'a, PhysicsState>, - pub char_states: ReadStorage<'a, CharacterState>, - pub uids: ReadStorage<'a, Uid>, - pub groups: ReadStorage<'a, group::Group>, - pub terrain: ReadExpect<'a, TerrainGrid>, - pub alignments: ReadStorage<'a, Alignment>, - pub bodies: ReadStorage<'a, Body>, - pub is_mounts: ReadStorage<'a, Is>, - pub time_of_day: Read<'a, TimeOfDay>, - pub light_emitter: ReadStorage<'a, LightEmitter>, - #[cfg(feature = "worldgen")] - pub world: ReadExpect<'a, Arc>, - pub rtsim_entity: ReadStorage<'a, RtSimEntity>, - pub buffs: ReadStorage<'a, Buffs>, - pub combos: ReadStorage<'a, Combo>, - pub active_abilities: ReadStorage<'a, ActiveAbilities>, - pub loot_owners: ReadStorage<'a, LootOwner>, - pub msm: ReadExpect<'a, MaterialStatManifest>, -} - -pub enum Path { - Full, - Separate, - Partial, -} diff --git a/server/src/sys/chunk_serialize.rs b/server/src/sys/chunk_serialize.rs index c12006007a..0810aa9951 100644 --- a/server/src/sys/chunk_serialize.rs +++ b/server/src/sys/chunk_serialize.rs @@ -2,10 +2,9 @@ use crate::{ chunk_serialize::{ChunkSendEntry, SerializedChunk}, client::Client, metrics::NetworkRequestMetrics, - presence::Presence, Tick, }; -use common::{event::EventBus, slowjob::SlowJobPool, terrain::TerrainGrid}; +use common::{comp::Presence, event::EventBus, slowjob::SlowJobPool, terrain::TerrainGrid}; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{SerializedTerrainChunk, ServerGeneral}; use hashbrown::{hash_map::Entry, HashMap}; diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index c2c246e61c..c6ac05e2d9 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -1,12 +1,8 @@ use super::sentinel::{DeletedEntities, TrackedStorages, UpdateTrackers}; -use crate::{ - client::Client, - presence::{Presence, RegionSubscription}, - Tick, -}; +use crate::{client::Client, presence::RegionSubscription, Tick}; use common::{ calendar::Calendar, - comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Player, Pos, Vel}, + comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Player, Pos, Presence, Vel}, event::EventBus, outcome::Outcome, region::{Event as RegionEvent, RegionMap}, diff --git a/server/src/sys/msg/character_screen.rs b/server/src/sys/msg/character_screen.rs index 661aaabc9f..1aea142188 100644 --- a/server/src/sys/msg/character_screen.rs +++ b/server/src/sys/msg/character_screen.rs @@ -8,11 +8,10 @@ use crate::{ character_creator, client::Client, persistence::{character_loader::CharacterLoader, character_updater::CharacterUpdater}, - presence::Presence, EditableSettings, }; use common::{ - comp::{Admin, AdminRole, ChatType, Player, UnresolvedChatMsg, Waypoint}, + comp::{Admin, AdminRole, ChatType, Player, Presence, UnresolvedChatMsg, Waypoint}, event::{EventBus, ServerEvent}, resources::Time, terrain::TerrainChunkSize, diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index 6c50879800..4e216483d8 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -1,10 +1,10 @@ #[cfg(feature = "persistent_world")] use crate::TerrainPersistence; -use crate::{client::Client, presence::Presence, Settings}; +use crate::{client::Client, Settings}; use common::{ comp::{ Admin, AdminRole, CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Player, - Pos, SkillSet, Vel, + Pos, Presence, PresenceKind, SkillSet, Vel, }, event::{EventBus, ServerEvent}, link::Is, @@ -15,7 +15,7 @@ use common::{ vol::ReadVol, }; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::{ClientGeneral, PresenceKind, ServerGeneral}; +use common_net::msg::{ClientGeneral, ServerGeneral}; use common_state::{BlockChange, BuildAreas}; use core::mem; use rayon::prelude::*; diff --git a/server/src/sys/msg/terrain.rs b/server/src/sys/msg/terrain.rs index 6eb01033a3..6c050d6a83 100644 --- a/server/src/sys/msg/terrain.rs +++ b/server/src/sys/msg/terrain.rs @@ -1,9 +1,9 @@ use crate::{ chunk_serialize::ChunkSendEntry, client::Client, lod::Lod, metrics::NetworkRequestMetrics, - presence::Presence, ChunkRequest, + ChunkRequest, }; use common::{ - comp::Pos, + comp::{Pos, Presence}, event::{EventBus, ServerEvent}, spiral::Spiral2d, terrain::{CoordinateConversions, TerrainChunkSize, TerrainGrid}, diff --git a/server/src/sys/persistence.rs b/server/src/sys/persistence.rs index de4eb10885..3011664ebe 100644 --- a/server/src/sys/persistence.rs +++ b/server/src/sys/persistence.rs @@ -1,13 +1,13 @@ -use crate::{persistence::character_updater, presence::Presence, sys::SysScheduler}; +use crate::{persistence::character_updater, sys::SysScheduler}; use common::{ comp::{ pet::{is_tameable, Pet}, - ActiveAbilities, Alignment, Body, Inventory, MapMarker, SkillSet, Stats, Waypoint, + ActiveAbilities, Alignment, Body, Inventory, MapMarker, Presence, PresenceKind, SkillSet, + Stats, Waypoint, }, uid::Uid, }; use common_ecs::{Job, Origin, Phase, System}; -use common_net::msg::PresenceKind; use specs::{Join, ReadStorage, Write, WriteExpect}; #[derive(Default)] diff --git a/server/src/sys/subscription.rs b/server/src/sys/subscription.rs index 1be6e66d28..3b20bde524 100644 --- a/server/src/sys/subscription.rs +++ b/server/src/sys/subscription.rs @@ -1,10 +1,10 @@ use super::sentinel::{DeletedEntities, TrackedStorages}; use crate::{ client::Client, - presence::{self, Presence, RegionSubscription}, + presence::{self, RegionSubscription}, }; use common::{ - comp::{Ori, Pos, Vel}, + comp::{Ori, Pos, Presence, Vel}, region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap}, terrain::{CoordinateConversions, TerrainChunkSize}, uid::Uid, diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index d401221aae..c4deb96978 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -6,18 +6,14 @@ use crate::TerrainPersistence; use world::{IndexOwned, World}; use crate::{ - chunk_generator::ChunkGenerator, - chunk_serialize::ChunkSendEntry, - client::Client, - presence::{Presence, RepositionOnChunkLoad}, - rtsim, - settings::Settings, - ChunkRequest, Tick, + chunk_generator::ChunkGenerator, chunk_serialize::ChunkSendEntry, client::Client, + presence::RepositionOnChunkLoad, rtsim, settings::Settings, ChunkRequest, Tick, }; use common::{ calendar::Calendar, comp::{ - self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Waypoint, + self, agent, bird_medium, skillset::skills, BehaviorCapability, ForceUpdate, Pos, Presence, + Waypoint, }, event::{EventBus, NpcBuilder, ServerEvent}, generation::EntityInfo, diff --git a/server/src/sys/terrain_sync.rs b/server/src/sys/terrain_sync.rs index 78accf71f3..cfc32fe04e 100644 --- a/server/src/sys/terrain_sync.rs +++ b/server/src/sys/terrain_sync.rs @@ -1,5 +1,8 @@ -use crate::{chunk_serialize::ChunkSendEntry, client::Client, presence::Presence, Settings}; -use common::{comp::Pos, event::EventBus}; +use crate::{chunk_serialize::ChunkSendEntry, client::Client, Settings}; +use common::{ + comp::{Pos, Presence}, + event::EventBus, +}; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{CompressedData, ServerGeneral}; use common_state::TerrainChanges; diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 342d470dc9..808772a2f2 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -99,7 +99,7 @@ use common::{ loot_owner::LootOwnerKind, pet::is_mountable, skillset::{skills::Skill, SkillGroupKind, SkillsPersistenceError}, - BuffData, BuffKind, Health, Item, MapMarkerChange, + BuffData, BuffKind, Health, Item, MapMarkerChange, PresenceKind, }, consts::MAX_PICKUP_RANGE, link::Is, @@ -115,7 +115,7 @@ use common::{ }; use common_base::{prof_span, span}; use common_net::{ - msg::{world_msg::SiteId, Notification, PresenceKind}, + msg::{world_msg::SiteId, Notification}, sync::WorldSyncExt, }; use conrod_core::{ diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 7a6ff8bbdc..c23375c1d7 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -38,7 +38,6 @@ use common::{ vol::ReadVol, }; use common_base::{prof_span, span}; -use common_net::msg::PresenceKind; use common_state::State; use comp::item::Reagent; use hashbrown::HashMap; @@ -311,7 +310,7 @@ impl Scene { let terrain = Terrain::new(renderer, &data, lod.get_data(), sprite_render_context); let camera_mode = match client.presence() { - Some(PresenceKind::Spectator) => CameraMode::Freefly, + Some(comp::PresenceKind::Spectator) => CameraMode::Freefly, _ => CameraMode::ThirdPerson, }; diff --git a/voxygen/src/session/mod.rs b/voxygen/src/session/mod.rs index 9939c8500b..849dc0c313 100644 --- a/voxygen/src/session/mod.rs +++ b/voxygen/src/session/mod.rs @@ -18,7 +18,8 @@ use common::{ inventory::slot::{EquipSlot, Slot}, invite::InviteKind, item::{tool::ToolKind, ItemDesc}, - ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, Stats, UtteranceKind, Vel, + ChatMsg, ChatType, InputKind, InventoryUpdateEvent, Pos, PresenceKind, Stats, + UtteranceKind, Vel, }, consts::MAX_MOUNT_RANGE, event::UpdateCharacterMetadata, @@ -32,10 +33,7 @@ use common::{ vol::ReadVol, }; use common_base::{prof_span, span}; -use common_net::{ - msg::{server::InviteAnswer, PresenceKind}, - sync::WorldSyncExt, -}; +use common_net::{msg::server::InviteAnswer, sync::WorldSyncExt}; use crate::{ audio::sfx::SfxEvent, From dfb5e32803e4e2f28b6f13765f89b88959888525 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 2 Apr 2023 18:47:54 +0100 Subject: [PATCH 079/139] Don't interact forever --- server/src/sys/agent/behavior_tree.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6e40207ca8..f261ecd937 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -490,11 +490,15 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { &bdata.read_data, target, ) { - bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.controller.push_action(ControlAction::Talk); + bdata.controller.push_utterance(UtteranceKind::Greeting); bdata .agent_data .chat_npc("npc-speech-villager", &mut bdata.event_emitter); + bdata + .agent + .timer + .start(bdata.read_data.time.0, TimerAction::Interact); } } } From 1e70ccfb8d4b2bea4ba34fab04a467e73559b05e Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 2 Apr 2023 19:59:05 +0100 Subject: [PATCH 080/139] Swallow actions for simulated NPCs --- rtsim/src/rule/simulate_npcs.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 8a514857e2..42c0e709af 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -6,7 +6,7 @@ use crate::{ use common::{ comp::{self, Body}, grid::Grid, - rtsim::{Actor, Personality}, + rtsim::{Actor, NpcAction, Personality}, terrain::TerrainChunkSize, vol::RectVolSize, }; @@ -264,6 +264,13 @@ impl Rule for SimulateNpcs { } } + // Consume NPC actions + for action in std::mem::take(&mut npc.controller.actions) { + match action { + NpcAction::Greet(_) => {}, // Currently, just swallow greeting actions + } + } + // Make sure NPCs remain on the surface npc.wpos.z = ctx .world From b402e450cfa88fd7c73b6727ebc7d3bc0d1c8b7a Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 2 Apr 2023 23:02:06 +0100 Subject: [PATCH 081/139] Added rtsim_npc, made herbalists gather ingredients --- common/src/cmd.rs | 13 +- common/src/rtsim.rs | 37 +-- rtsim/src/ai/mod.rs | 13 + rtsim/src/data/npc.rs | 15 +- rtsim/src/rule/npc_ai.rs | 113 ++++--- rtsim/src/rule/simulate_npcs.rs | 60 ++-- server/agent/src/action_nodes.rs | 431 +++++++++++++------------- server/src/cmd.rs | 56 ++++ server/src/rtsim/tick.rs | 8 +- server/src/sys/agent/behavior_tree.rs | 1 + 10 files changed, 438 insertions(+), 309 deletions(-) diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 270b60377a..ef152d7334 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -310,10 +310,11 @@ pub enum ServerChatCommand { Tell, Time, Tp, - RtsimTp, - RtsimInfo, - RtsimPurge, RtsimChunk, + RtsimInfo, + RtsimNpc, + RtsimPurge, + RtsimTp, Unban, Version, Waypoint, @@ -693,6 +694,11 @@ impl ServerChatCommand { "Display information about an rtsim NPC", Some(Moderator), ), + ServerChatCommand::RtsimNpc => cmd( + vec![Any("query", Required)], + "List rtsim NPCs that fit a given query (e.g: simulated,merchant)", + Some(Moderator), + ), ServerChatCommand::RtsimPurge => cmd( vec![Boolean( "whether purging of rtsim data should occur on next startup", @@ -836,6 +842,7 @@ impl ServerChatCommand { ServerChatCommand::Tp => "tp", ServerChatCommand::RtsimTp => "rtsim_tp", ServerChatCommand::RtsimInfo => "rtsim_info", + ServerChatCommand::RtsimNpc => "rtsim_npc", ServerChatCommand::RtsimPurge => "rtsim_purge", ServerChatCommand::RtsimChunk => "rtsim_chunk", ServerChatCommand::Unban => "unban", diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 6b04ff8e14..0d58760f19 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -192,43 +192,30 @@ impl Default for Personality { /// into the game as a physical entity or not). Agent code should attempt to act /// upon its instructions where reasonable although deviations for various /// reasons (obstacle avoidance, counter-attacking, etc.) are expected. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct RtSimController { - /// When this field is `Some(..)`, the agent should attempt to make progress - /// toward the given location, accounting for obstacles and other - /// high-priority situations like being attacked. - pub travel_to: Option>, + pub activity: Option, + pub actions: VecDeque, pub personality: Personality, pub heading_to: Option, - /// Proportion of full speed to move - pub speed_factor: f32, - pub actions: VecDeque, -} - -impl Default for RtSimController { - fn default() -> Self { - Self { - travel_to: None, - personality: Personality::default(), - heading_to: None, - speed_factor: 1.0, - actions: VecDeque::new(), - } - } } impl RtSimController { pub fn with_destination(pos: Vec3) -> Self { Self { - travel_to: Some(pos), - personality: Personality::default(), - heading_to: None, - speed_factor: 0.5, - actions: VecDeque::new(), + activity: Some(NpcActivity::Goto(pos, 0.5)), + ..Default::default() } } } +#[derive(Clone, Copy, Debug)] +pub enum NpcActivity { + /// (travel_to, speed_factor) + Goto(Vec3, f32), + Gather(&'static [ChunkResource]), +} + #[derive(Clone, Copy, Debug)] pub enum NpcAction { Greet(Actor), diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 2d27002298..3de71dc85d 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -86,6 +86,7 @@ pub trait Action: Any + Send + Sync { /// // Walk toward an enemy NPC and, once done, attack the enemy NPC /// goto(enemy_npc).then(attack(enemy_npc)) /// ``` + #[must_use] fn then, R1>(self, other: A1) -> Then where Self: Sized, @@ -106,6 +107,7 @@ pub trait Action: Any + Send + Sync { /// // Endlessly collect flax from the environment /// find_and_collect(ChunkResource::Flax).repeat() /// ``` + #[must_use] fn repeat(self) -> Repeat where Self: Sized, @@ -121,6 +123,7 @@ pub trait Action: Any + Send + Sync { /// // Keep going on adventures until your 111th birthday /// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0) /// ``` + #[must_use] fn stop_if bool>(self, f: F) -> StopIf where Self: Sized, @@ -129,6 +132,7 @@ pub trait Action: Any + Send + Sync { } /// Map the completion value of this action to something else. + #[must_use] fn map R1, R1>(self, f: F) -> Map where Self: Sized, @@ -157,6 +161,7 @@ pub trait Action: Any + Send + Sync { /// go_on_an_adventure().boxed() /// } /// ``` + #[must_use] fn boxed(self) -> Box> where Self: Sized, @@ -172,6 +177,7 @@ pub trait Action: Any + Send + Sync { /// ```ignore /// goto(npc.home).debug(|| "Going home") /// ``` + #[must_use] fn debug(self, mk_info: F) -> Debug where Self: Sized, @@ -412,6 +418,7 @@ impl Action<()> for Finish { /// } /// }) /// ``` +#[must_use] pub fn finish() -> Finish { Finish } // Tree @@ -425,12 +432,15 @@ pub const CASUAL: Priority = 2; pub struct Node(Box>, Priority); /// Perform an action with [`URGENT`] priority (see [`choose`]). +#[must_use] pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } /// Perform an action with [`IMPORTANT`] priority (see [`choose`]). +#[must_use] pub fn important, R>(a: A) -> Node { Node(Box::new(a), IMPORTANT) } /// Perform an action with [`CASUAL`] priority (see [`choose`]). +#[must_use] pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } /// See [`choose`] and [`watch`]. @@ -501,6 +511,7 @@ impl Node + Send + Sync + 'static, R: 'static> Actio /// } /// }) /// ``` +#[must_use] pub fn choose(f: F) -> impl Action where F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, @@ -535,6 +546,7 @@ where /// } /// }) /// ``` +#[must_use] pub fn watch(f: F) -> impl Action where F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, @@ -679,6 +691,7 @@ impl + Clone + Send + Sync + 'st /// .into_iter() /// .map(|enemy| attack(enemy))) /// ``` +#[must_use] pub fn seq(iter: I) -> Sequence where I: Iterator + Clone, diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 99f4c5de96..cbfc8fe258 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -3,7 +3,9 @@ pub use common::rtsim::{NpcId, Profession}; use common::{ comp, grid::Grid, - rtsim::{Actor, FactionId, NpcAction, Personality, SiteId, VehicleId}, + rtsim::{ + Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, + }, store::Id, vol::RectVolSize, }; @@ -46,15 +48,18 @@ pub struct PathingMemory { #[derive(Default)] pub struct Controller { pub actions: Vec, - /// (wpos, speed_factor) - pub goto: Option<(Vec3, f32)>, + pub activity: Option, } impl Controller { - pub fn do_idle(&mut self) { self.goto = None; } + pub fn do_idle(&mut self) { self.activity = None; } pub fn do_goto(&mut self, wpos: Vec3, speed_factor: f32) { - self.goto = Some((wpos, speed_factor)); + self.activity = Some(NpcActivity::Goto(wpos, speed_factor)); + } + + pub fn do_gather(&mut self, resources: &'static [ChunkResource]) { + self.activity = Some(NpcActivity::Gather(resources)); } pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 718c04535a..7ca787d5f8 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -12,7 +12,8 @@ use crate::{ use common::{ astar::{Astar, PathResult}, path::Path, - rtsim::{Profession, SiteId}, + rtsim::{ChunkResource, Profession, SiteId}, + spiral::Spiral2d, store::Id, terrain::{SiteKindMeta, TerrainChunkSize}, time::DayPeriod, @@ -514,8 +515,22 @@ fn adventure() -> impl Action { .debug(move || "adventure") } +fn gather_ingredients() -> impl Action { + just(|ctx| { + ctx.controller.do_gather( + &[ + ChunkResource::Fruit, + ChunkResource::Mushroom, + ChunkResource::Plant, + ][..], + ) + }) + .debug(|| "gather ingredients") +} + fn villager(visiting_site: SiteId) -> impl Action { choose(move |ctx| { + /* if ctx .state .data() @@ -523,23 +538,24 @@ fn villager(visiting_site: SiteId) -> impl Action { .get(visiting_site) .map_or(true, |s| s.world_site.is_none()) { - casual( - idle().debug(|| "idling (visiting site does not exist, perhaps it's stale data?)"), - ) + return casual(idle() + .debug(|| "idling (visiting site does not exist, perhaps it's stale data?)")); } else if ctx.npc.current_site != Some(visiting_site) { let npc_home = ctx.npc.home; // Travel to the site we're supposed to be in - urgent(travel_to_site(visiting_site).debug(move || { + return urgent(travel_to_site(visiting_site).debug(move || { if npc_home == Some(visiting_site) { "travel home".to_string() } else { "travel to visiting site".to_string() } - })) - } else if DayPeriod::from(ctx.time_of_day.0).is_dark() + })); + } else + */ + if DayPeriod::from(ctx.time_of_day.0).is_dark() && !matches!(ctx.npc.profession, Some(Profession::Guard)) { - important( + return important( now(move |ctx| { if let Some(house_wpos) = ctx .state @@ -567,38 +583,63 @@ fn villager(visiting_site: SiteId) -> impl Action { } }) .debug(|| "find somewhere to sleep"), - ) - } else { - casual(now(move |ctx| { - // Choose a plaza in the site we're visiting to walk to - if let Some(plaza_wpos) = ctx - .state - .data() - .sites - .get(visiting_site) - .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) - .and_then(|site2| { - let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; - Some(site2.tile_center_wpos(plaza.root_tile()).as_()) - }) - { - // Walk to the plaza... - travel_to_point(plaza_wpos) - .debug(|| "walk to plaza") - // ...then wait for some time before moving on + ); + // Villagers with roles should perform those roles + } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { + let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); + if let Some(tree_chunk) = Spiral2d::new() + .skip(thread_rng().gen_range(1..=8)) + .take(49) + .map(|rpos| chunk_pos + rpos) + .find(|cpos| { + ctx.world + .sim() + .get(*cpos) + .map_or(false, |c| c.tree_density > 0.75) + }) + { + return important( + travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_()) + .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(10.0..30.0); - socialize().repeat().stop_if(timeout(wait_time)) - .debug(|| "wait at plaza") + gather_ingredients().repeat().stop_if(timeout(wait_time)) }) - .map(|_| ()) - .boxed() - } else { - // No plazas? :( - finish().boxed() - } - })) + .map(|_| ()), + ); + } } + + // If nothing else needs doing, walk between plazas and socialize + casual(now(move |ctx| { + // Choose a plaza in the site we're visiting to walk to + if let Some(plaza_wpos) = ctx + .state + .data() + .sites + .get(visiting_site) + .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) + .and_then(|site2| { + let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; + Some(site2.tile_center_wpos(plaza.root_tile()).as_()) + }) + { + // Walk to the plaza... + travel_to_point(plaza_wpos) + .debug(|| "walk to plaza") + // ...then wait for some time before moving on + .then({ + let wait_time = thread_rng().gen_range(30.0..90.0); + socialize().repeat().stop_if(timeout(wait_time)) + .debug(|| "wait at plaza") + }) + .map(|_| ()) + .boxed() + } else { + // No plazas? :( + finish().boxed() + } + })) }) .debug(move || format!("villager at site {:?}", visiting_site)) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 42c0e709af..68c15c7915 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -6,7 +6,7 @@ use crate::{ use common::{ comp::{self, Body}, grid::Grid, - rtsim::{Actor, NpcAction, Personality}, + rtsim::{Actor, NpcAction, NpcActivity, Personality}, terrain::TerrainChunkSize, vol::RectVolSize, }; @@ -179,13 +179,14 @@ impl Rule for SimulateNpcs { // Simulate the NPC's movement and interactions if matches!(npc.mode, SimulationMode::Simulated) { - // Move NPCs if they have a target destination - if let Some((target, speed_factor)) = npc.controller.goto { - // Simulate NPC movement when riding - if let Some(riding) = &npc.riding { - if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { + // Simulate NPC movement when riding + if let Some(riding) = &npc.riding { + if let Some(vehicle) = data.npcs.vehicles.get_mut(riding.vehicle) { + match npc.controller.activity { // If steering, the NPC controls the vehicle's motion - if riding.steering { + Some(NpcActivity::Goto(target, speed_factor)) + if riding.steering => + { let diff = target.xy() - vehicle.wpos.xy(); let dist2 = diff.magnitude_squared(); @@ -243,24 +244,39 @@ impl Rule for SimulateNpcs { vehicle.wpos = wpos; } } - } - npc.wpos = vehicle.wpos; - } else { - // Vehicle doens't exist anymore - npc.riding = None; + }, + // When riding, other actions are disabled + Some(NpcActivity::Goto(_, _) | NpcActivity::Gather(_)) => {}, + None => {}, } - // If not riding, we assume they're just walking + npc.wpos = vehicle.wpos; } else { - let diff = target.xy() - npc.wpos.xy(); - let dist2 = diff.magnitude_squared(); + // Vehicle doens't exist anymore + npc.riding = None; + } + // If not riding, we assume they're just walking + } else { + match npc.controller.activity { + // Move NPCs if they have a target destination + Some(NpcActivity::Goto(target, speed_factor)) => { + let diff = target.xy() - npc.wpos.xy(); + let dist2 = diff.magnitude_squared(); - if dist2 > 0.5f32.powi(2) { - npc.wpos += (diff - * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt - / dist2.sqrt()) - .min(1.0)) - .with_z(0.0); - } + if dist2 > 0.5f32.powi(2) { + npc.wpos += (diff + * (npc.body.max_speed_approx() + * speed_factor + * ctx.event.dt + / dist2.sqrt()) + .min(1.0)) + .with_z(0.0); + } + }, + Some(NpcActivity::Gather(_)) => { + // TODO: Maybe they should walk around randomly + // when gathering resources? + }, + None => {}, } } diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index d1ab7689ed..8c1169f27d 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -28,6 +28,7 @@ use common::{ effect::{BuffEffect, Effect}, event::{Emitter, ServerEvent}, path::TraversalConfig, + rtsim::NpcActivity, states::basic_beam, terrain::{Block, TerrainGrid}, time::DayPeriod, @@ -212,118 +213,230 @@ impl<'a> AgentData<'a> { } agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; - if let Some(travel_to) = &agent.rtsim_controller.travel_to { - // If it has an rtsim destination and can fly, then it should. - // If it is flying and bumps something above it, then it should move down. - if self.traversal_config.can_fly - && !read_data + match agent.rtsim_controller.activity { + Some(NpcActivity::Goto(travel_to, speed_factor)) => { + // If it has an rtsim destination and can fly, then it should. + // If it is flying and bumps something above it, then it should move down. + if self.traversal_config.can_fly + && !read_data + .terrain + .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0)) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some()) + { + controller.push_basic_input(InputKind::Fly); + } else { + controller.push_cancel_input(InputKind::Fly) + } + + let chase_tgt = read_data .terrain - .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0)) - .until(Block::is_solid) - .cast() - .1 - .map_or(true, |b| b.is_some()) - { - controller.push_basic_input(InputKind::Fly); - } else { - controller.push_cancel_input(InputKind::Fly) - } + .try_find_space(travel_to.as_()) + .map(|pos| pos.as_()) + .unwrap_or(travel_to); - let chase_tgt = read_data - .terrain - .try_find_space(travel_to.as_()) - .map(|pos| pos.as_()) - .unwrap_or(*travel_to); + if let Some((bearing, speed)) = agent.chaser.chase( + &*read_data.terrain, + self.pos.0, + self.vel.0, + chase_tgt, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + controller.inputs.move_dir = + bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) + * speed.min(speed_factor); + self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller); + controller.inputs.climb = Some(comp::Climb::Up); + //.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some()); - if let Some((bearing, speed)) = agent.chaser.chase( - &*read_data.terrain, - self.pos.0, - self.vel.0, - chase_tgt, - TraversalConfig { - min_tgt_dist: 1.25, - ..self.traversal_config - }, - ) { - controller.inputs.move_dir = - bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) - * speed.min(agent.rtsim_controller.speed_factor); - self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller); - controller.inputs.climb = Some(comp::Climb::Up); - //.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some()); + let height_offset = bearing.z + + if self.traversal_config.can_fly { + // NOTE: costs 4 us (imbris) + let obstacle_ahead = read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z(), + self.pos.0 + + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) + * 80.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some()); - let height_offset = bearing.z - + if self.traversal_config.can_fly { - // NOTE: costs 4 us (imbris) - let obstacle_ahead = read_data + let mut ground_too_close = self + .body + .map(|body| { + #[cfg(feature = "worldgen")] + let height_approx = self.pos.0.z + - read_data + .world + .sim() + .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) + .unwrap_or(0.0); + #[cfg(not(feature = "worldgen"))] + let height_approx = self.pos.0.z; + + height_approx < body.flying_height() + }) + .unwrap_or(false); + + const NUM_RAYS: usize = 5; + + // NOTE: costs 15-20 us (imbris) + for i in 0..=NUM_RAYS { + let magnitude = self.body.map_or(20.0, |b| b.flying_height()); + // Lerp between a line straight ahead and straight down to detect a + // wedge of obstacles we might fly into (inclusive so that both + // vectors are sampled) + if let Some(dir) = Lerp::lerp( + -Vec3::unit_z(), + Vec3::new(bearing.x, bearing.y, 0.0), + i as f32 / NUM_RAYS as f32, + ) + .try_normalized() + { + ground_too_close |= read_data + .terrain + .ray(self.pos.0, self.pos.0 + magnitude * dir) + .until(|b: &Block| b.is_solid() || b.is_liquid()) + .cast() + .1 + .map_or(false, |b| b.is_some()) + } + } + + if obstacle_ahead || ground_too_close { + 5.0 //fly up when approaching obstacles + } else { + -2.0 + } //flying things should slowly come down from the stratosphere + } else { + 0.05 //normal land traveller offset + }; + if let Some(pid) = agent.position_pid_controller.as_mut() { + pid.sp = self.pos.0.z + height_offset * Vec3::unit_z(); + controller.inputs.move_z = pid.calc_err(); + } else { + controller.inputs.move_z = height_offset; + } + // Put away weapon + if rng.gen_bool(0.1) + && matches!( + read_data.char_states.get(*self.entity), + Some(CharacterState::Wielding(_)) + ) + { + controller.push_action(ControlAction::Unwield); + } + } + }, + Some(NpcActivity::Gather(resources)) => { + // TODO: Implement + controller.push_action(ControlAction::Dance); + }, + None => { + // Bats should fly + // Use a proportional controller as the bouncing effect mimics bat flight + if self.traversal_config.can_fly + && self + .inventory + .equipped(EquipSlot::ActiveMainhand) + .as_ref() + .map_or(false, |item| { + item.ability_spec().map_or(false, |a_s| match &*a_s { + AbilitySpec::Custom(spec) => { + matches!( + spec.as_str(), + "Simple Flying Melee" + | "Flame Wyvern" + | "Frost Wyvern" + | "Cloud Wyvern" + | "Sea Wyvern" + | "Weald Wyvern" + ) + }, + _ => false, + }) + }) + { + // Bats don't like the ground, so make sure they are always flying + controller.push_basic_input(InputKind::Fly); + // Use a proportional controller with a coefficient of 1.0 to + // maintain altitude + let alt = read_data + .terrain + .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) + .until(Block::is_solid) + .cast() + .0; + let set_point = 5.0; + let error = set_point - alt; + controller.inputs.move_z = error; + // If on the ground, jump + if self.physics_state.on_ground.is_some() { + controller.push_basic_input(InputKind::Jump); + } + } + agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 0.1 + - agent.bearing * 0.003 + - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { + (self.pos.0 - patrol_origin).xy() * 0.0002 + }); + + // Stop if we're too close to a wall + // or about to walk off a cliff + // NOTE: costs 1 us (imbris) <- before cliff raycast added + agent.bearing *= 0.1 + + if read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z(), + self.pos.0 + + Vec3::from(agent.bearing) + .try_normalized() + .unwrap_or_else(Vec3::unit_y) + * 5.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_none()) + && read_data .terrain .ray( - self.pos.0 + Vec3::unit_z(), self.pos.0 - + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) * 80.0 - + Vec3::unit_z(), + + Vec3::from(agent.bearing) + .try_normalized() + .unwrap_or_else(Vec3::unit_y), + self.pos.0 + + Vec3::from(agent.bearing) + .try_normalized() + .unwrap_or_else(Vec3::unit_y) + - Vec3::unit_z() * 4.0, ) .until(Block::is_solid) .cast() - .1 - .map_or(true, |b| b.is_some()); - - let mut ground_too_close = self - .body - .map(|body| { - #[cfg(feature = "worldgen")] - let height_approx = self.pos.0.z - - read_data - .world - .sim() - .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) - .unwrap_or(0.0); - #[cfg(not(feature = "worldgen"))] - let height_approx = self.pos.0.z; - - height_approx < body.flying_height() - }) - .unwrap_or(false); - - const NUM_RAYS: usize = 5; - - // NOTE: costs 15-20 us (imbris) - for i in 0..=NUM_RAYS { - let magnitude = self.body.map_or(20.0, |b| b.flying_height()); - // Lerp between a line straight ahead and straight down to detect a - // wedge of obstacles we might fly into (inclusive so that both vectors - // are sampled) - if let Some(dir) = Lerp::lerp( - -Vec3::unit_z(), - Vec3::new(bearing.x, bearing.y, 0.0), - i as f32 / NUM_RAYS as f32, - ) - .try_normalized() - { - ground_too_close |= read_data - .terrain - .ray(self.pos.0, self.pos.0 + magnitude * dir) - .until(|b: &Block| b.is_solid() || b.is_liquid()) - .cast() - .1 - .map_or(false, |b| b.is_some()) - } - } - - if obstacle_ahead || ground_too_close { - 5.0 //fly up when approaching obstacles - } else { - -2.0 - } //flying things should slowly come down from the stratosphere + .0 + < 3.0 + { + 0.9 } else { - 0.05 //normal land traveller offset + 0.0 }; - if let Some(pid) = agent.position_pid_controller.as_mut() { - pid.sp = self.pos.0.z + height_offset * Vec3::unit_z(); - controller.inputs.move_z = pid.calc_err(); - } else { - controller.inputs.move_z = height_offset; + + if agent.bearing.magnitude_squared() > 0.5f32.powi(2) { + controller.inputs.move_dir = agent.bearing * 0.65; } + // Put away weapon if rng.gen_bool(0.1) && matches!( @@ -333,120 +446,16 @@ impl<'a> AgentData<'a> { { controller.push_action(ControlAction::Unwield); } - } - } else { - // Bats should fly - // Use a proportional controller as the bouncing effect mimics bat flight - if self.traversal_config.can_fly - && self - .inventory - .equipped(EquipSlot::ActiveMainhand) - .as_ref() - .map_or(false, |item| { - item.ability_spec().map_or(false, |a_s| match &*a_s { - AbilitySpec::Custom(spec) => { - matches!( - spec.as_str(), - "Simple Flying Melee" - | "Flame Wyvern" - | "Frost Wyvern" - | "Cloud Wyvern" - | "Sea Wyvern" - | "Weald Wyvern" - ) - }, - _ => false, - }) - }) - { - // Bats don't like the ground, so make sure they are always flying - controller.push_basic_input(InputKind::Fly); - // Use a proportional controller with a coefficient of 1.0 to - // maintain altitude - let alt = read_data - .terrain - .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) - .until(Block::is_solid) - .cast() - .0; - let set_point = 5.0; - let error = set_point - alt; - controller.inputs.move_z = error; - // If on the ground, jump - if self.physics_state.on_ground.is_some() { - controller.push_basic_input(InputKind::Jump); + + if rng.gen::() < 0.0015 { + controller.push_utterance(UtteranceKind::Calm); } - } - agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 0.1 - - agent.bearing * 0.003 - - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { - (self.pos.0 - patrol_origin).xy() * 0.0002 - }); - // Stop if we're too close to a wall - // or about to walk off a cliff - // NOTE: costs 1 us (imbris) <- before cliff raycast added - agent.bearing *= 0.1 - + if read_data - .terrain - .ray( - self.pos.0 + Vec3::unit_z(), - self.pos.0 - + Vec3::from(agent.bearing) - .try_normalized() - .unwrap_or_else(Vec3::unit_y) - * 5.0 - + Vec3::unit_z(), - ) - .until(Block::is_solid) - .cast() - .1 - .map_or(true, |b| b.is_none()) - && read_data - .terrain - .ray( - self.pos.0 - + Vec3::from(agent.bearing) - .try_normalized() - .unwrap_or_else(Vec3::unit_y), - self.pos.0 - + Vec3::from(agent.bearing) - .try_normalized() - .unwrap_or_else(Vec3::unit_y) - - Vec3::unit_z() * 4.0, - ) - .until(Block::is_solid) - .cast() - .0 - < 3.0 - { - 0.9 - } else { - 0.0 - }; - - if agent.bearing.magnitude_squared() > 0.5f32.powi(2) { - controller.inputs.move_dir = agent.bearing * 0.65; - } - - // Put away weapon - if rng.gen_bool(0.1) - && matches!( - read_data.char_states.get(*self.entity), - Some(CharacterState::Wielding(_)) - ) - { - controller.push_action(ControlAction::Unwield); - } - - if rng.gen::() < 0.0015 { - controller.push_utterance(UtteranceKind::Calm); - } - - // Sit - if rng.gen::() < 0.0035 { - controller.push_action(ControlAction::Sit); - } + // Sit + if rng.gen::() < 0.0035 { + controller.push_action(ControlAction::Sit); + } + }, } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 4459993b97..246dd56875 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -185,6 +185,7 @@ fn do_command( ServerChatCommand::Tp => handle_tp, ServerChatCommand::RtsimTp => handle_rtsim_tp, ServerChatCommand::RtsimInfo => handle_rtsim_info, + ServerChatCommand::RtsimNpc => handle_rtsim_npc, ServerChatCommand::RtsimPurge => handle_rtsim_purge, ServerChatCommand::RtsimChunk => handle_rtsim_chunk, ServerChatCommand::Unban => handle_unban, @@ -1263,6 +1264,61 @@ fn handle_rtsim_info( } } +fn handle_rtsim_npc( + server: &mut Server, + client: EcsEntity, + _target: EcsEntity, + args: Vec, + action: &ServerChatCommand, +) -> CmdResult<()> { + use crate::rtsim::RtSim; + if let Some(query) = parse_cmd_args!(args, String) { + let terms = query + .split(',') + .map(|s| s.trim().to_lowercase()) + .collect::>(); + + let rtsim = server.state.ecs().read_resource::(); + let data = rtsim.state().data(); + let npcs = data + .npcs + .values() + .enumerate() + .filter(|(idx, npc)| { + let tags = [ + npc.profession + .as_ref() + .map(|p| format!("{:?}", p)) + .unwrap_or_default(), + format!("{:?}", npc.mode), + format!("{}", idx), + ]; + terms + .iter() + .all(|term| tags.iter().any(|tag| term.eq_ignore_ascii_case(tag.trim()))) + }) + .collect::>(); + + let mut info = String::new(); + + let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", ")); + for (idx, _) in &npcs { + let _ = write!(&mut info, "{}, ", idx); + } + let _ = writeln!(&mut info, ""); + let _ = writeln!(&mut info, "Matched {} NPCs.", npcs.len()); + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, info), + ); + + Ok(()) + } else { + Err(action.help_string()) + } +} + fn handle_rtsim_purge( server: &mut Server, client: EcsEntity, diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index ca0544da03..2cb8fef1f6 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -366,13 +366,7 @@ impl<'a> System<'a> for Sys { // Update entity state if let Some(agent) = agent { agent.rtsim_controller.personality = npc.personality; - if let Some((wpos, speed_factor)) = npc.controller.goto { - agent.rtsim_controller.travel_to = Some(wpos); - agent.rtsim_controller.speed_factor = speed_factor; - } else { - agent.rtsim_controller.travel_to = None; - agent.rtsim_controller.speed_factor = 1.0; - } + agent.rtsim_controller.activity = npc.controller.activity; agent .rtsim_controller .actions diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index f261ecd937..5845ea8d0e 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -495,6 +495,7 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { bdata .agent_data .chat_npc("npc-speech-villager", &mut bdata.event_emitter); + // Start a timer so that they eventually stop interacting bdata .agent .timer From 7175f7f02fe3e83c8690df47f0ea433d1d1d1c5b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sun, 2 Apr 2023 23:48:05 +0100 Subject: [PATCH 082/139] Hunters explore forests to hunt game --- common/src/rtsim.rs | 2 + rtsim/src/data/npc.rs | 2 + rtsim/src/rule/npc_ai.rs | 46 ++- rtsim/src/rule/simulate_npcs.rs | 8 +- server/agent/src/action_nodes.rs | 497 ++++++++++++++------------ server/src/sys/agent/behavior_tree.rs | 17 +- 6 files changed, 318 insertions(+), 254 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 0d58760f19..1c0fbc55e6 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -214,6 +214,8 @@ pub enum NpcActivity { /// (travel_to, speed_factor) Goto(Vec3, f32), Gather(&'static [ChunkResource]), + // TODO: Generalise to other entities? What kinds of animals? + HuntAnimals, } #[derive(Clone, Copy, Debug)] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index cbfc8fe258..3c4fcf5099 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -62,6 +62,8 @@ impl Controller { self.activity = Some(NpcActivity::Gather(resources)); } + pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); } + pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } } diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 7ca787d5f8..3e36b81355 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -528,6 +528,25 @@ fn gather_ingredients() -> impl Action { .debug(|| "gather ingredients") } +fn hunt_animals() -> impl Action { + just(|ctx| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals") +} + +fn find_forest(ctx: &NpcCtx) -> Option> { + let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); + Spiral2d::new() + .skip(thread_rng().gen_range(1..=8)) + .take(49) + .map(|rpos| chunk_pos + rpos) + .find(|cpos| { + ctx.world + .sim() + .get(*cpos) + .map_or(false, |c| c.tree_density > 0.75 && c.surface_veg > 0.5) + }) + .map(|chunk| TerrainChunkSize::center_wpos(chunk).as_()) +} + fn villager(visiting_site: SiteId) -> impl Action { choose(move |ctx| { /* @@ -586,20 +605,9 @@ fn villager(visiting_site: SiteId) -> impl Action { ); // Villagers with roles should perform those roles } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { - let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); - if let Some(tree_chunk) = Spiral2d::new() - .skip(thread_rng().gen_range(1..=8)) - .take(49) - .map(|rpos| chunk_pos + rpos) - .find(|cpos| { - ctx.world - .sim() - .get(*cpos) - .map_or(false, |c| c.tree_density > 0.75) - }) - { + if let Some(forest_wpos) = find_forest(ctx) { return important( - travel_to_point(TerrainChunkSize::center_wpos(tree_chunk).as_()) + travel_to_point(forest_wpos) .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(10.0..30.0); @@ -608,6 +616,18 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } + } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) { + if let Some(forest_wpos) = find_forest(ctx) { + return important( + travel_to_point(forest_wpos) + .debug(|| "walk to forest") + .then({ + let wait_time = thread_rng().gen_range(30.0..60.0); + hunt_animals().repeat().stop_if(timeout(wait_time)) + }) + .map(|_| ()), + ); + } } // If nothing else needs doing, walk between plazas and socialize diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index 68c15c7915..cc78c17142 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -246,7 +246,11 @@ impl Rule for SimulateNpcs { } }, // When riding, other actions are disabled - Some(NpcActivity::Goto(_, _) | NpcActivity::Gather(_)) => {}, + Some( + NpcActivity::Goto(_, _) + | NpcActivity::Gather(_) + | NpcActivity::HuntAnimals, + ) => {}, None => {}, } npc.wpos = vehicle.wpos; @@ -272,7 +276,7 @@ impl Rule for SimulateNpcs { .with_z(0.0); } }, - Some(NpcActivity::Gather(_)) => { + Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals) => { // TODO: Maybe they should walk around randomly // when gathering resources? }, diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 8c1169f27d..f555cc887f 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -161,6 +161,7 @@ impl<'a> AgentData<'a> { agent: &mut Agent, controller: &mut Controller, read_data: &ReadData, + event_emitter: &mut Emitter, rng: &mut impl Rng, ) { enum ActionTimers { @@ -213,249 +214,267 @@ impl<'a> AgentData<'a> { } agent.action_state.timers[ActionTimers::TimerIdle as usize] = 0.0; - match agent.rtsim_controller.activity { - Some(NpcActivity::Goto(travel_to, speed_factor)) => { - // If it has an rtsim destination and can fly, then it should. - // If it is flying and bumps something above it, then it should move down. - if self.traversal_config.can_fly - && !read_data - .terrain - .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0)) - .until(Block::is_solid) - .cast() - .1 - .map_or(true, |b| b.is_some()) - { - controller.push_basic_input(InputKind::Fly); - } else { - controller.push_cancel_input(InputKind::Fly) - } - - let chase_tgt = read_data - .terrain - .try_find_space(travel_to.as_()) - .map(|pos| pos.as_()) - .unwrap_or(travel_to); - - if let Some((bearing, speed)) = agent.chaser.chase( - &*read_data.terrain, - self.pos.0, - self.vel.0, - chase_tgt, - TraversalConfig { - min_tgt_dist: 1.25, - ..self.traversal_config - }, - ) { - controller.inputs.move_dir = - bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) - * speed.min(speed_factor); - self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller); - controller.inputs.climb = Some(comp::Climb::Up); - //.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some()); - - let height_offset = bearing.z - + if self.traversal_config.can_fly { - // NOTE: costs 4 us (imbris) - let obstacle_ahead = read_data - .terrain - .ray( - self.pos.0 + Vec3::unit_z(), - self.pos.0 - + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) - * 80.0 - + Vec3::unit_z(), - ) - .until(Block::is_solid) - .cast() - .1 - .map_or(true, |b| b.is_some()); - - let mut ground_too_close = self - .body - .map(|body| { - #[cfg(feature = "worldgen")] - let height_approx = self.pos.0.z - - read_data - .world - .sim() - .get_alt_approx(self.pos.0.xy().map(|x: f32| x as i32)) - .unwrap_or(0.0); - #[cfg(not(feature = "worldgen"))] - let height_approx = self.pos.0.z; - - height_approx < body.flying_height() - }) - .unwrap_or(false); - - const NUM_RAYS: usize = 5; - - // NOTE: costs 15-20 us (imbris) - for i in 0..=NUM_RAYS { - let magnitude = self.body.map_or(20.0, |b| b.flying_height()); - // Lerp between a line straight ahead and straight down to detect a - // wedge of obstacles we might fly into (inclusive so that both - // vectors are sampled) - if let Some(dir) = Lerp::lerp( - -Vec3::unit_z(), - Vec3::new(bearing.x, bearing.y, 0.0), - i as f32 / NUM_RAYS as f32, - ) - .try_normalized() - { - ground_too_close |= read_data - .terrain - .ray(self.pos.0, self.pos.0 + magnitude * dir) - .until(|b: &Block| b.is_solid() || b.is_liquid()) - .cast() - .1 - .map_or(false, |b| b.is_some()) - } - } - - if obstacle_ahead || ground_too_close { - 5.0 //fly up when approaching obstacles - } else { - -2.0 - } //flying things should slowly come down from the stratosphere - } else { - 0.05 //normal land traveller offset - }; - if let Some(pid) = agent.position_pid_controller.as_mut() { - pid.sp = self.pos.0.z + height_offset * Vec3::unit_z(); - controller.inputs.move_z = pid.calc_err(); - } else { - controller.inputs.move_z = height_offset; - } - // Put away weapon - if rng.gen_bool(0.1) - && matches!( - read_data.char_states.get(*self.entity), - Some(CharacterState::Wielding(_)) - ) + 'activity: { + match agent.rtsim_controller.activity { + Some(NpcActivity::Goto(travel_to, speed_factor)) => { + // If it has an rtsim destination and can fly, then it should. + // If it is flying and bumps something above it, then it should move down. + if self.traversal_config.can_fly + && !read_data + .terrain + .ray(self.pos.0, self.pos.0 + (Vec3::unit_z() * 3.0)) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some()) { - controller.push_action(ControlAction::Unwield); + controller.push_basic_input(InputKind::Fly); + } else { + controller.push_cancel_input(InputKind::Fly) } - } - }, - Some(NpcActivity::Gather(resources)) => { - // TODO: Implement - controller.push_action(ControlAction::Dance); - }, - None => { - // Bats should fly - // Use a proportional controller as the bouncing effect mimics bat flight - if self.traversal_config.can_fly - && self - .inventory - .equipped(EquipSlot::ActiveMainhand) - .as_ref() - .map_or(false, |item| { - item.ability_spec().map_or(false, |a_s| match &*a_s { - AbilitySpec::Custom(spec) => { - matches!( - spec.as_str(), - "Simple Flying Melee" - | "Flame Wyvern" - | "Frost Wyvern" - | "Cloud Wyvern" - | "Sea Wyvern" - | "Weald Wyvern" - ) - }, - _ => false, - }) - }) - { - // Bats don't like the ground, so make sure they are always flying - controller.push_basic_input(InputKind::Fly); - // Use a proportional controller with a coefficient of 1.0 to - // maintain altitude - let alt = read_data - .terrain - .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) - .until(Block::is_solid) - .cast() - .0; - let set_point = 5.0; - let error = set_point - alt; - controller.inputs.move_z = error; - // If on the ground, jump - if self.physics_state.on_ground.is_some() { - controller.push_basic_input(InputKind::Jump); - } - } - agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 0.1 - - agent.bearing * 0.003 - - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { - (self.pos.0 - patrol_origin).xy() * 0.0002 - }); - // Stop if we're too close to a wall - // or about to walk off a cliff - // NOTE: costs 1 us (imbris) <- before cliff raycast added - agent.bearing *= 0.1 - + if read_data + let chase_tgt = read_data + .terrain + .try_find_space(travel_to.as_()) + .map(|pos| pos.as_()) + .unwrap_or(travel_to); + + if let Some((bearing, speed)) = agent.chaser.chase( + &*read_data.terrain, + self.pos.0, + self.vel.0, + chase_tgt, + TraversalConfig { + min_tgt_dist: 1.25, + ..self.traversal_config + }, + ) { + controller.inputs.move_dir = + bearing.xy().try_normalized().unwrap_or_else(Vec2::zero) + * speed.min(speed_factor); + self.jump_if(bearing.z > 1.5 || self.traversal_config.can_fly, controller); + controller.inputs.climb = Some(comp::Climb::Up); + //.filter(|_| bearing.z > 0.1 || self.physics_state.in_liquid().is_some()); + + let height_offset = bearing.z + + if self.traversal_config.can_fly { + // NOTE: costs 4 us (imbris) + let obstacle_ahead = read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z(), + self.pos.0 + + bearing.try_normalized().unwrap_or_else(Vec3::unit_y) + * 80.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_some()); + + let mut ground_too_close = self + .body + .map(|body| { + #[cfg(feature = "worldgen")] + let height_approx = self.pos.0.z + - read_data + .world + .sim() + .get_alt_approx( + self.pos.0.xy().map(|x: f32| x as i32), + ) + .unwrap_or(0.0); + #[cfg(not(feature = "worldgen"))] + let height_approx = self.pos.0.z; + + height_approx < body.flying_height() + }) + .unwrap_or(false); + + const NUM_RAYS: usize = 5; + + // NOTE: costs 15-20 us (imbris) + for i in 0..=NUM_RAYS { + let magnitude = self.body.map_or(20.0, |b| b.flying_height()); + // Lerp between a line straight ahead and straight down to + // detect a + // wedge of obstacles we might fly into (inclusive so that both + // vectors are sampled) + if let Some(dir) = Lerp::lerp( + -Vec3::unit_z(), + Vec3::new(bearing.x, bearing.y, 0.0), + i as f32 / NUM_RAYS as f32, + ) + .try_normalized() + { + ground_too_close |= read_data + .terrain + .ray(self.pos.0, self.pos.0 + magnitude * dir) + .until(|b: &Block| b.is_solid() || b.is_liquid()) + .cast() + .1 + .map_or(false, |b| b.is_some()) + } + } + + if obstacle_ahead || ground_too_close { + 5.0 //fly up when approaching obstacles + } else { + -2.0 + } //flying things should slowly come down from the stratosphere + } else { + 0.05 //normal land traveller offset + }; + if let Some(pid) = agent.position_pid_controller.as_mut() { + pid.sp = self.pos.0.z + height_offset * Vec3::unit_z(); + controller.inputs.move_z = pid.calc_err(); + } else { + controller.inputs.move_z = height_offset; + } + // Put away weapon + if rng.gen_bool(0.1) + && matches!( + read_data.char_states.get(*self.entity), + Some(CharacterState::Wielding(_)) + ) + { + controller.push_action(ControlAction::Unwield); + } + } + break 'activity; // Don't fall through to idle wandering + }, + Some(NpcActivity::Gather(resources)) => { + // TODO: Implement + controller.push_action(ControlAction::Dance); + break 'activity; // Don't fall through to idle wandering + }, + Some(NpcActivity::HuntAnimals) => { + if rng.gen::() < 0.1 { + self.choose_target( + agent, + controller, + read_data, + event_emitter, + AgentData::is_hunting_animal, + ); + } + }, + None => {}, + } + + // Bats should fly + // Use a proportional controller as the bouncing effect mimics bat flight + if self.traversal_config.can_fly + && self + .inventory + .equipped(EquipSlot::ActiveMainhand) + .as_ref() + .map_or(false, |item| { + item.ability_spec().map_or(false, |a_s| match &*a_s { + AbilitySpec::Custom(spec) => { + matches!( + spec.as_str(), + "Simple Flying Melee" + | "Flame Wyvern" + | "Frost Wyvern" + | "Cloud Wyvern" + | "Sea Wyvern" + | "Weald Wyvern" + ) + }, + _ => false, + }) + }) + { + // Bats don't like the ground, so make sure they are always flying + controller.push_basic_input(InputKind::Fly); + // Use a proportional controller with a coefficient of 1.0 to + // maintain altitude + let alt = read_data + .terrain + .ray(self.pos.0, self.pos.0 - (Vec3::unit_z() * 7.0)) + .until(Block::is_solid) + .cast() + .0; + let set_point = 5.0; + let error = set_point - alt; + controller.inputs.move_z = error; + // If on the ground, jump + if self.physics_state.on_ground.is_some() { + controller.push_basic_input(InputKind::Jump); + } + } + agent.bearing += Vec2::new(rng.gen::() - 0.5, rng.gen::() - 0.5) * 0.1 + - agent.bearing * 0.003 + - agent.patrol_origin.map_or(Vec2::zero(), |patrol_origin| { + (self.pos.0 - patrol_origin).xy() * 0.0002 + }); + + // Stop if we're too close to a wall + // or about to walk off a cliff + // NOTE: costs 1 us (imbris) <- before cliff raycast added + agent.bearing *= 0.1 + + if read_data + .terrain + .ray( + self.pos.0 + Vec3::unit_z(), + self.pos.0 + + Vec3::from(agent.bearing) + .try_normalized() + .unwrap_or_else(Vec3::unit_y) + * 5.0 + + Vec3::unit_z(), + ) + .until(Block::is_solid) + .cast() + .1 + .map_or(true, |b| b.is_none()) + && read_data .terrain .ray( - self.pos.0 + Vec3::unit_z(), + self.pos.0 + + Vec3::from(agent.bearing) + .try_normalized() + .unwrap_or_else(Vec3::unit_y), self.pos.0 + Vec3::from(agent.bearing) .try_normalized() .unwrap_or_else(Vec3::unit_y) - * 5.0 - + Vec3::unit_z(), + - Vec3::unit_z() * 4.0, ) .until(Block::is_solid) .cast() - .1 - .map_or(true, |b| b.is_none()) - && read_data - .terrain - .ray( - self.pos.0 - + Vec3::from(agent.bearing) - .try_normalized() - .unwrap_or_else(Vec3::unit_y), - self.pos.0 - + Vec3::from(agent.bearing) - .try_normalized() - .unwrap_or_else(Vec3::unit_y) - - Vec3::unit_z() * 4.0, - ) - .until(Block::is_solid) - .cast() - .0 - < 3.0 - { - 0.9 - } else { - 0.0 - }; - - if agent.bearing.magnitude_squared() > 0.5f32.powi(2) { - controller.inputs.move_dir = agent.bearing * 0.65; - } - - // Put away weapon - if rng.gen_bool(0.1) - && matches!( - read_data.char_states.get(*self.entity), - Some(CharacterState::Wielding(_)) - ) + .0 + < 3.0 { - controller.push_action(ControlAction::Unwield); - } + 0.9 + } else { + 0.0 + }; - if rng.gen::() < 0.0015 { - controller.push_utterance(UtteranceKind::Calm); - } + if agent.bearing.magnitude_squared() > 0.5f32.powi(2) { + controller.inputs.move_dir = agent.bearing * 0.65; + } - // Sit - if rng.gen::() < 0.0035 { - controller.push_action(ControlAction::Sit); - } - }, + // Put away weapon + if rng.gen_bool(0.1) + && matches!( + read_data.char_states.get(*self.entity), + Some(CharacterState::Wielding(_)) + ) + { + controller.push_action(ControlAction::Unwield); + } + + if rng.gen::() < 0.0015 { + controller.push_utterance(UtteranceKind::Calm); + } + + // Sit + if rng.gen::() < 0.0035 { + controller.push_action(ControlAction::Sit); + } } } @@ -660,6 +679,7 @@ impl<'a> AgentData<'a> { controller: &mut Controller, read_data: &ReadData, event_emitter: &mut Emitter, + is_enemy: fn(&Self, EcsEntity, &ReadData) -> bool, ) { enum ActionStateTimers { TimerChooseTarget = 0, @@ -700,7 +720,7 @@ impl<'a> AgentData<'a> { let get_pos = |entity| read_data.positions.get(entity); let get_enemy = |(entity, attack_target): (EcsEntity, bool)| { if attack_target { - if self.is_enemy(entity, read_data) { + if is_enemy(self, entity, read_data) { Some((entity, true)) } else if can_ambush(entity, read_data) { controller.clone().push_utterance(UtteranceKind::Ambush); @@ -1385,12 +1405,13 @@ impl<'a> AgentData<'a> { agent: &mut Agent, controller: &mut Controller, read_data: &ReadData, + event_emitter: &mut Emitter, rng: &mut impl Rng, ) { agent.forget_old_sounds(read_data.time.0); if is_invulnerable(*self.entity, read_data) { - self.idle(agent, controller, read_data, rng); + self.idle(agent, controller, read_data, event_emitter, rng); return; } @@ -1423,13 +1444,13 @@ impl<'a> AgentData<'a> { } else if self.below_flee_health(agent) || !follows_threatening_sounds { self.flee(agent, controller, &sound_pos, &read_data.terrain); } else { - self.idle(agent, controller, read_data, rng); + self.idle(agent, controller, read_data, event_emitter, rng); } } else { - self.idle(agent, controller, read_data, rng); + self.idle(agent, controller, read_data, event_emitter, rng); } } else { - self.idle(agent, controller, read_data, rng); + self.idle(agent, controller, read_data, event_emitter, rng); } } @@ -1438,6 +1459,7 @@ impl<'a> AgentData<'a> { agent: &mut Agent, read_data: &ReadData, controller: &mut Controller, + event_emitter: &mut Emitter, rng: &mut impl Rng, ) { if let Some(Target { target, .. }) = agent.target { @@ -1467,7 +1489,7 @@ impl<'a> AgentData<'a> { Some(tgt_pos.0), )); - self.idle(agent, controller, read_data, rng); + self.idle(agent, controller, read_data, event_emitter, rng); } else { let target_data = TargetData::new(tgt_pos, target, read_data); // TODO: Reimplement this in rtsim @@ -1595,7 +1617,7 @@ impl<'a> AgentData<'a> { }) } - fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + pub fn is_enemy(&self, entity: EcsEntity, read_data: &ReadData) -> bool { let other_alignment = read_data.alignments.get(entity); (entity != *self.entity) @@ -1604,6 +1626,11 @@ impl<'a> AgentData<'a> { || (is_villager(self.alignment) && is_dressed_as_cultist(entity, read_data))) } + pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + (entity != *self.entity) + && matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_))) + } + fn should_defend(&self, entity: EcsEntity, read_data: &ReadData) -> bool { let entity_alignment = read_data.alignments.get(entity); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 5845ea8d0e..9f3924be17 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -437,6 +437,7 @@ fn attack_if_owner_hurt(bdata: &mut BehaviorData) -> bool { bdata.agent, bdata.read_data, bdata.controller, + bdata.event_emitter, bdata.rng, ); return true; @@ -543,12 +544,14 @@ fn handle_timed_events(bdata: &mut BehaviorData) -> bool { bdata.controller, bdata.read_data, bdata.event_emitter, + AgentData::is_enemy, ); } else { bdata.agent_data.handle_sounds_heard( bdata.agent, bdata.controller, bdata.read_data, + bdata.event_emitter, bdata.rng, ); } @@ -763,12 +766,12 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { [ActionStateBehaviorTreeTimers::TimerBehaviorTree as usize] = 0.0; agent.target = None; agent.flee_from_pos = None; - agent_data.idle(agent, controller, read_data, rng); + agent_data.idle(agent, controller, read_data, event_emitter, rng); } } else if is_dead(target, read_data) { agent_data.exclaim_relief_about_enemy_dead(agent, event_emitter); agent.target = None; - agent_data.idle(agent, controller, read_data, rng); + agent_data.idle(agent, controller, read_data, event_emitter, rng); } else if is_invulnerable(target, read_data) || stop_pursuing( dist_sqrd, @@ -780,13 +783,19 @@ fn do_combat(bdata: &mut BehaviorData) -> bool { ) { agent.target = None; - agent_data.idle(agent, controller, read_data, rng); + agent_data.idle(agent, controller, read_data, event_emitter, rng); } else { let is_time_to_retarget = read_data.time.0 - selected_at > RETARGETING_THRESHOLD_SECONDS; if !in_aggro_range && is_time_to_retarget { - agent_data.choose_target(agent, controller, read_data, event_emitter); + agent_data.choose_target( + agent, + controller, + read_data, + event_emitter, + AgentData::is_enemy, + ); } if aggro_on { From 2d7d172f49efc6abcb8fb9befd90fde2ce6f3038 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 15:16:17 +0100 Subject: [PATCH 083/139] Made rtsim aware of character locations --- rtsim/src/data/npc.rs | 56 ++++++++++++++++----------- rtsim/src/gen/mod.rs | 1 + rtsim/src/rule/npc_ai.rs | 2 +- server/src/rtsim/tick.rs | 47 ++++++++++++++++++++-- server/src/sys/agent/behavior_tree.rs | 18 ++++----- 5 files changed, 89 insertions(+), 35 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 3c4fcf5099..fcfe4dbd50 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -7,8 +7,10 @@ use common::{ Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, }, store::Id, + terrain::TerrainChunkSize, vol::RectVolSize, }; +use hashbrown::HashMap; use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; @@ -87,22 +89,22 @@ pub struct Npc { pub personality: Personality, // Unpersisted state - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub chunk_pos: Option>, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub current_site: Option, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub controller: Controller, /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the /// server, loaded corresponds to being within a loaded chunk). When in /// loaded mode, the interactions of the NPC should not be simulated but /// should instead be derived from the game. - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub mode: SimulationMode, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub brain: Option, } @@ -203,13 +205,13 @@ pub struct Vehicle { pub body: comp::ship::Body, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub chunk_pos: Option>, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub driver: Option, - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] // TODO: Find a way to detect riders when the vehicle is loaded pub riders: Vec, @@ -217,7 +219,7 @@ pub struct Vehicle { /// the server, loaded corresponds to being within a loaded chunk). When /// in loaded mode, the interactions of the Vehicle should not be /// simulated but should instead be derived from the game. - #[serde(skip_serializing, skip_deserializing)] + #[serde(skip)] pub mode: SimulationMode, } @@ -250,7 +252,6 @@ impl Vehicle { #[derive(Default, Clone, Serialize, Deserialize)] pub struct GridCell { pub npcs: Vec, - pub characters: Vec, pub vehicles: Vec, } @@ -258,8 +259,11 @@ pub struct GridCell { pub struct Npcs { pub npcs: HopSlotMap, pub vehicles: HopSlotMap, + // TODO: This feels like it should be its own rtsim resource #[serde(skip, default = "construct_npc_grid")] pub npc_grid: Grid, + #[serde(skip)] + pub character_map: HashMap, Vec<(common::character::CharacterId, Vec3)>>, } fn construct_npc_grid() -> Grid { Grid::new(Vec2::zero(), Default::default()) } @@ -273,8 +277,11 @@ impl Npcs { /// Queries nearby npcs, not garantueed to work if radius > 32.0 pub fn nearby(&self, wpos: Vec2, radius: f32) -> impl Iterator + '_ { - let chunk_pos = - wpos.as_::() / common::terrain::TerrainChunkSize::RECT_SIZE.as_::(); + let chunk_pos = wpos + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }); let r_sqr = radius * radius; LOCALITY .into_iter() @@ -289,19 +296,24 @@ impl Npcs { .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) }) .map(Actor::Npc) - .chain(cell.characters - .iter() - .copied() - // TODO: Filter characters by distance too - // .filter(move |npc| { - // self.npcs - // .get(*npc) - // .map_or(false, |npc| npc.wpos.xy().distance_squared(wpos) < r_sqr) - // }) - .map(Actor::Character)) }) }) .flatten() + .chain( + self.character_map + .get(&chunk_pos) + .map(|characters| { + characters.iter().filter_map(move |(character, c_wpos)| { + if c_wpos.xy().distance_squared(wpos) < r_sqr { + Some(Actor::Character(*character)) + } else { + None + } + }) + }) + .into_iter() + .flatten(), + ) } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 19ba5e692b..4f64d34aea 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -34,6 +34,7 @@ impl Data { npcs: Default::default(), vehicles: Default::default(), npc_grid: Grid::new(Vec2::zero(), Default::default()), + character_map: Default::default(), }, sites: Sites { sites: Default::default(), diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 3e36b81355..7bc95ade9c 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -457,7 +457,7 @@ fn socialize() -> impl Action { just(|ctx| { let mut rng = thread_rng(); // TODO: Bit odd, should wait for a while after greeting - if thread_rng().gen_bool(0.0002) { + if thread_rng().gen_bool(0.0003) { if let Some(other) = ctx .state .data() diff --git a/server/src/rtsim/tick.rs b/server/src/rtsim/tick.rs index 2cb8fef1f6..e39cbed114 100644 --- a/server/src/rtsim/tick.rs +++ b/server/src/rtsim/tick.rs @@ -3,14 +3,15 @@ use super::*; use crate::sys::terrain::NpcData; use common::{ - comp::{self, Body}, + comp::{self, Body, Presence, PresenceKind}, event::{EventBus, NpcBuilder, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time, TimeOfDay}, rtsim::{Actor, RtSimEntity, RtSimVehicle}, slowjob::SlowJobPool, - terrain::CoordinateConversions, + terrain::{CoordinateConversions, TerrainChunkSize}, trade::{Good, SiteInformation}, + vol::RectVolSize, LoadoutBuilder, }; use common_ecs::{Job, Origin, Phase, System}; @@ -187,6 +188,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, RtSimEntity>, ReadStorage<'a, RtSimVehicle>, WriteStorage<'a, comp::Agent>, + ReadStorage<'a, Presence>, ); const NAME: &'static str = "rtsim::tick"; @@ -208,16 +210,53 @@ impl<'a> System<'a> for Sys { rtsim_entities, rtsim_vehicles, mut agents, + presences, ): Self::SystemData, ) { let mut emitter = server_event_bus.emitter(); let rtsim = &mut *rtsim; - rtsim.state.data_mut().time_of_day = *time_of_day; + // Set up rtsim inputs + { + let mut data = rtsim.state.data_mut(); + + // Update time of day + data.time_of_day = *time_of_day; + + // Update character map (i.e: so that rtsim knows where players are) + // TODO: Other entities too? Or do we now care about that? + data.npcs.character_map.clear(); + for (character, wpos) in + (&presences, &positions) + .join() + .filter_map(|(presence, pos)| { + if let PresenceKind::Character(character) = &presence.kind { + Some((character, pos.0)) + } else { + None + } + }) + { + let chunk_pos = wpos + .xy() + .as_::() + .map2(TerrainChunkSize::RECT_SIZE.as_::(), |e, sz| { + e.div_euclid(sz) + }); + data.npcs + .character_map + .entry(chunk_pos) + .or_default() + .push((*character, wpos)); + } + } + + // Tick rtsim rtsim .state .tick(&world, index.as_index_ref(), *time_of_day, *time, dt.0); + // Perform a save if required if rtsim .last_saved .map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) @@ -230,6 +269,7 @@ impl<'a> System<'a> for Sys { let chunk_states = rtsim.state.resource::(); let data = &mut *rtsim.state.data_mut(); + // Load in vehicles for (vehicle_id, vehicle) in data.npcs.vehicles.iter_mut() { let chunk = vehicle.wpos.xy().as_::().wpos_to_cpos(); @@ -296,6 +336,7 @@ impl<'a> System<'a> for Sys { } } + // Load in NPCs for (npc_id, npc) in data.npcs.npcs.iter_mut() { let chunk = npc.wpos.xy().as_::().wpos_to_cpos(); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 9f3924be17..b56ea35fd5 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -478,24 +478,24 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { if let Some(target) = bdata.read_data.lookup_actor(actor) { let target_pos = bdata.read_data.positions.get(target).map(|pos| pos.0); - bdata.agent.target = Some(Target::new( - target, - false, - bdata.read_data.time.0, - false, - target_pos, - )); - if bdata.agent_data.look_toward( &mut bdata.controller, &bdata.read_data, target, ) { + bdata.agent.target = Some(Target::new( + target, + false, + bdata.read_data.time.0, + false, + target_pos, + )); + bdata.controller.push_action(ControlAction::Talk); bdata.controller.push_utterance(UtteranceKind::Greeting); bdata .agent_data - .chat_npc("npc-speech-villager", &mut bdata.event_emitter); + .chat_npc("npc-speech-villager_open", &mut bdata.event_emitter); // Start a timer so that they eventually stop interacting bdata .agent From 5aaee96cb14577649313302bc7675000bc17cdee Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 15:55:44 +0100 Subject: [PATCH 084/139] Removed special-casing of merchants --- assets/common/loadout/village/merchant.ron | 3 +-- server/src/sys/agent/behavior_tree.rs | 8 ++++---- voxygen/src/hud/mod.rs | 12 +----------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/assets/common/loadout/village/merchant.ron b/assets/common/loadout/village/merchant.ron index be52cd5da1..35c25e586a 100644 --- a/assets/common/loadout/village/merchant.ron +++ b/assets/common/loadout/village/merchant.ron @@ -11,5 +11,4 @@ legs: Item("common.items.armor.merchant.pants"), feet: Item("common.items.armor.merchant.foot"), lantern: Item("common.items.lantern.black_0"), - tabard: Item("common.items.debug.admin"), -) \ No newline at end of file +) diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index b56ea35fd5..d76f24156a 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -475,9 +475,9 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { match action { NpcAction::Greet(actor) => { if bdata.agent.allowed_to_speak() { - if let Some(target) = bdata.read_data.lookup_actor(actor) { - let target_pos = bdata.read_data.positions.get(target).map(|pos| pos.0); - + if let Some(target) = bdata.read_data.lookup_actor(actor) + && let Some(target_pos) = bdata.read_data.positions.get(target) + { if bdata.agent_data.look_toward( &mut bdata.controller, &bdata.read_data, @@ -488,7 +488,7 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { false, bdata.read_data.time.0, false, - target_pos, + Some(target_pos.0), )); bdata.controller.push_action(ControlAction::Talk); diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 808772a2f2..66a459f807 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -212,8 +212,6 @@ const MENU_BG: Color = Color::Rgba(0.1, 0.12, 0.12, 1.0); /// Distance at which nametags are visible for group members const NAMETAG_GROUP_RANGE: f32 = 1000.0; -/// Distance at which nametags are visible for merchants -const NAMETAG_MERCHANT_RANGE: f32 = 50.0; /// Distance at which nametags are visible const NAMETAG_RANGE: f32 = 40.0; /// Time nametags stay visible after doing damage even if they are out of range @@ -2268,11 +2266,6 @@ impl Hud { let pos = interpolated.map_or(pos.0, |i| i.pos); let in_group = client.group_members().contains_key(uid); let is_me = entity == me; - // TODO: once the site2 rework lands and merchants have dedicated stalls or - // buildings, they no longer need to be emphasized via the higher overhead - // text radius relative to other NPCs - let is_merchant = - stats.name == "Merchant" && client.player_list().get(uid).is_none(); let dist_sqr = pos.distance_squared(player_pos); // Determine whether to display nametag and healthbar based on whether the // entity has been damaged, is targeted/selected, or is in your group @@ -2282,13 +2275,10 @@ impl Hud { && (info.target_entity.map_or(false, |e| e == entity) || info.selected_entity.map_or(false, |s| s.0 == entity) || health.map_or(true, overhead::should_show_healthbar) - || in_group - || is_merchant) + || in_group) && dist_sqr < (if in_group { NAMETAG_GROUP_RANGE - } else if is_merchant { - NAMETAG_MERCHANT_RANGE } else if hpfl .time_since_last_dmg_by_me .map_or(false, |t| t < NAMETAG_DMG_TIME) From 7dfbc2bdab9a07d101a6d28e91a4e95f24b2e38f Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 17:16:29 +0100 Subject: [PATCH 085/139] Made socialising NPCs dance --- common/src/rtsim.rs | 1 + rtsim/src/data/npc.rs | 4 +++- rtsim/src/rule/npc_ai.rs | 28 +++++++++++++++++----------- rtsim/src/rule/simulate_npcs.rs | 9 +++++++-- server/agent/src/action_nodes.rs | 4 ++++ 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 1c0fbc55e6..eb5913c1aa 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -216,6 +216,7 @@ pub enum NpcActivity { Gather(&'static [ChunkResource]), // TODO: Generalise to other entities? What kinds of animals? HuntAnimals, + Dance, } #[derive(Clone, Copy, Debug)] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index fcfe4dbd50..880e989e46 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -66,7 +66,9 @@ impl Controller { pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); } - pub fn do_greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } + pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } + + pub fn greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } } pub struct Brain { diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 7bc95ade9c..e59d4aeb97 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -454,19 +454,25 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { } fn socialize() -> impl Action { - just(|ctx| { + now(|ctx| { let mut rng = thread_rng(); // TODO: Bit odd, should wait for a while after greeting - if thread_rng().gen_bool(0.0003) { - if let Some(other) = ctx - .state - .data() - .npcs - .nearby(ctx.npc.wpos.xy(), 8.0) - .choose(&mut rng) - { - ctx.controller.do_greet(other); - } + if thread_rng().gen_bool(0.0003) && let Some(other) = ctx + .state + .data() + .npcs + .nearby(ctx.npc.wpos.xy(), 8.0) + .choose(&mut rng) + { + just(move |ctx| ctx.controller.greet(other)).boxed() + } else if thread_rng().gen_bool(0.0003) { + just(|ctx| ctx.controller.do_dance()) + .repeat() + .stop_if(timeout(6.0)) + .map(|_| ()) + .boxed() + } else { + idle().boxed() } }) } diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index cc78c17142..f59f1cb037 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -249,7 +249,8 @@ impl Rule for SimulateNpcs { Some( NpcActivity::Goto(_, _) | NpcActivity::Gather(_) - | NpcActivity::HuntAnimals, + | NpcActivity::HuntAnimals + | NpcActivity::Dance, ) => {}, None => {}, } @@ -276,7 +277,11 @@ impl Rule for SimulateNpcs { .with_z(0.0); } }, - Some(NpcActivity::Gather(_) | NpcActivity::HuntAnimals) => { + Some( + NpcActivity::Gather(_) + | NpcActivity::HuntAnimals + | NpcActivity::Dance, + ) => { // TODO: Maybe they should walk around randomly // when gathering resources? }, diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index f555cc887f..a3498cd2e4 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -349,6 +349,10 @@ impl<'a> AgentData<'a> { controller.push_action(ControlAction::Dance); break 'activity; // Don't fall through to idle wandering }, + Some(NpcActivity::Dance) => { + controller.push_action(ControlAction::Dance); + break 'activity; // Don't fall through to idle wandering + }, Some(NpcActivity::HuntAnimals) => { if rng.gen::() < 0.1 { self.choose_target( From b72d8f3192be387252ff7dc920f65508b0b0f159 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 18:03:07 +0100 Subject: [PATCH 086/139] Added the ability for rtsim to tell NPCs to speak --- common/src/rtsim.rs | 6 ++++-- rtsim/src/data/npc.rs | 5 +++++ rtsim/src/rule/npc_ai.rs | 28 ++++++++++++++++++--------- rtsim/src/rule/simulate_npcs.rs | 2 +- server/src/sys/agent/behavior_tree.rs | 3 +++ 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index eb5913c1aa..887cfadab9 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -7,7 +7,7 @@ use crate::character::CharacterId; use rand::{seq::IteratorRandom, Rng}; use serde::{Deserialize, Serialize}; use specs::Component; -use std::collections::VecDeque; +use std::{borrow::Cow, collections::VecDeque}; use strum::{EnumIter, IntoEnumIterator}; use vek::*; @@ -219,9 +219,11 @@ pub enum NpcActivity { Dance, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum NpcAction { Greet(Actor), + // TODO: Use some sort of structured, language-independent value that frontends can translate instead + Say(Cow<'static, str>), } #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 880e989e46..4ff70046f8 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -15,6 +15,7 @@ use rand::prelude::*; use serde::{Deserialize, Serialize}; use slotmap::HopSlotMap; use std::{ + borrow::Cow, collections::VecDeque, ops::{Deref, DerefMut}, }; @@ -69,6 +70,10 @@ impl Controller { pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } pub fn greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } + + pub fn say(&mut self, msg: impl Into>) { + self.actions.push(NpcAction::Say(msg.into())); + } } pub struct Brain { diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index e59d4aeb97..86bc1c6218 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -348,7 +348,7 @@ where /// Try to travel to a site. Where practical, paths will be taken. fn travel_to_point(wpos: Vec2) -> impl Action { now(move |ctx| { - const WAYPOINT: f32 = 24.0; + const WAYPOINT: f32 = 48.0; let start = ctx.npc.wpos.xy(); let diff = wpos - start; let n = (diff.magnitude() / WAYPOINT).max(1.0); @@ -506,9 +506,12 @@ fn adventure() -> impl Action { } else { 60.0 * 3.0 }; + let site_name = ctx.state.data().sites[tgt_site].world_site + .map(|ws| ctx.index.sites.get(ws).name().to_string()) + .unwrap_or_default(); // Travel to the site - important( - travel_to_site(tgt_site) + important(just(move |ctx| ctx.controller.say(format!("I've spent enough time here, onward to {}!", site_name))) + .then(travel_to_site(tgt_site)) // Stop for a few minutes .then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .map(|_| ()) @@ -597,10 +600,12 @@ fn villager(visiting_site: SiteId) -> impl Action { Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { - travel_to_point(house_wpos) + just(|ctx| ctx.controller.say("It's dark, time to go home")) + .then(travel_to_point(house_wpos)) .debug(|| "walk to house") .then(socialize().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) + .then(just(|ctx| ctx.controller.say("A new day begins!"))) .map(|_| ()) .boxed() } else { @@ -610,9 +615,11 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "find somewhere to sleep"), ); // Villagers with roles should perform those roles - } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) { + } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) + && thread_rng().gen_bool(0.8) + { if let Some(forest_wpos) = find_forest(ctx) { - return important( + return casual( travel_to_point(forest_wpos) .debug(|| "walk to forest") .then({ @@ -622,10 +629,13 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) { + } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) + && thread_rng().gen_bool(0.8) + { if let Some(forest_wpos) = find_forest(ctx) { - return important( - travel_to_point(forest_wpos) + return casual( + just(|ctx| ctx.controller.say("Time to go hunting!")) + .then(travel_to_point(forest_wpos)) .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(30.0..60.0); diff --git a/rtsim/src/rule/simulate_npcs.rs b/rtsim/src/rule/simulate_npcs.rs index f59f1cb037..81140b6a8a 100644 --- a/rtsim/src/rule/simulate_npcs.rs +++ b/rtsim/src/rule/simulate_npcs.rs @@ -292,7 +292,7 @@ impl Rule for SimulateNpcs { // Consume NPC actions for action in std::mem::take(&mut npc.controller.actions) { match action { - NpcAction::Greet(_) => {}, // Currently, just swallow greeting actions + NpcAction::Greet(_) | NpcAction::Say(_) => {}, // Currently, just swallow interactions } } diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index d76f24156a..e1a51b40f0 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -505,6 +505,9 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { } } }, + NpcAction::Say(msg) => { + bdata.agent_data.chat_npc(msg, &mut bdata.event_emitter); + }, } } false From 1fcb46ae0c054d14088278a99e4d4429938f84b2 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 18:36:33 +0100 Subject: [PATCH 087/139] Made merchants advertise wares --- common/src/rtsim.rs | 3 ++- rtsim/src/ai/mod.rs | 15 +++++++++------ rtsim/src/rule/npc_ai.rs | 24 ++++++++++++++++++++++++ server/src/sys/agent/behavior_tree.rs | 1 + 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index 887cfadab9..e2dc656221 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -222,7 +222,8 @@ pub enum NpcActivity { #[derive(Clone, Debug)] pub enum NpcAction { Greet(Actor), - // TODO: Use some sort of structured, language-independent value that frontends can translate instead + // TODO: Use some sort of structured, language-independent value that frontends can translate + // instead Say(Cow<'static, str>), } diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 3de71dc85d..9bf321728e 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -124,11 +124,11 @@ pub trait Action: Any + Send + Sync { /// go_on_an_adventure().repeat().stop_if(|ctx| ctx.npc.age > 111.0) /// ``` #[must_use] - fn stop_if bool>(self, f: F) -> StopIf + fn stop_if bool + Clone>(self, f: F) -> StopIf where Self: Sized, { - StopIf(self, f) + StopIf(self, f.clone(), f) } /// Map the completion value of this action to something else. @@ -704,10 +704,10 @@ where /// See [`Action::stop_if`]. #[derive(Copy, Clone)] -pub struct StopIf(A, F); +pub struct StopIf(A, F, F); -impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action> - for StopIf +impl, F: FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync + 'static, R> + Action> for StopIf { fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } @@ -715,7 +715,10 @@ impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Act fn backtrace(&self, bt: &mut Vec) { self.0.backtrace(bt); } - fn reset(&mut self) { self.0.reset(); } + fn reset(&mut self) { + self.0.reset(); + self.1 = self.2.clone(); + } fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { if (self.1)(ctx) { diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 86bc1c6218..056ea5aee2 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -469,6 +469,7 @@ fn socialize() -> impl Action { just(|ctx| ctx.controller.do_dance()) .repeat() .stop_if(timeout(6.0)) + .debug(|| "dancing") .map(|_| ()) .boxed() } else { @@ -644,6 +645,29 @@ fn villager(visiting_site: SiteId) -> impl Action { .map(|_| ()), ); } + } else if matches!(ctx.npc.profession, Some(Profession::Merchant)) + && thread_rng().gen_bool(0.8) + { + return casual( + just(|ctx| { + ctx.controller.say( + *[ + "All my goods are of the highest quality!", + "Does anybody want to buy my wares?", + "I've got the best offers in town.", + "Looking for supplies? I've got you covered.", + ] + .iter() + .choose(&mut thread_rng()) + .unwrap(), + ) // Can't fail + }) + .then(idle().repeat().stop_if(timeout(8.0))) + .repeat() + .stop_if(timeout(60.0)) + .debug(|| "sell wares") + .map(|_| ()), + ); } // If nothing else needs doing, walk between plazas and socialize diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index e1a51b40f0..e258bb2e00 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -506,6 +506,7 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { } }, NpcAction::Say(msg) => { + bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.agent_data.chat_npc(msg, &mut bdata.event_emitter); }, } From 9f025de27d01c78846573092787856f89e86fed6 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 19:29:37 +0100 Subject: [PATCH 088/139] Addressed feedback --- common/src/rtsim.rs | 4 ++ rtsim/src/ai/mod.rs | 10 ++-- rtsim/src/data/nature.rs | 6 ++- rtsim/src/data/npc.rs | 2 +- rtsim/src/rule/npc_ai.rs | 39 +++++++++------ server/src/lib.rs | 18 ------- server/src/rtsim/mod.rs | 6 +-- server/src/sys/agent.rs | 31 +----------- .../sys/agent/behavior_tree/interaction.rs | 49 ------------------- server/src/sys/terrain.rs | 2 - 10 files changed, 41 insertions(+), 126 deletions(-) diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index e2dc656221..638667435c 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -227,6 +227,8 @@ pub enum NpcAction { Say(Cow<'static, str>), } +// Note: the `serde(name = "...")` is to minimise the length of field +// identifiers for the sake of rtsim persistence #[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] pub enum ChunkResource { #[serde(rename = "0")] @@ -253,6 +255,8 @@ pub enum ChunkResource { Ore, // Iron, copper, etc. } +// Note: the `serde(name = "...")` is to minimise the length of field +// identifiers for the sake of rtsim persistence #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Profession { #[serde(rename = "0")] diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 9bf321728e..9995e2b546 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -528,15 +528,15 @@ where /// /// The inner function will be run every tick to decide on an action. When an /// action is chosen, it will be performed until completed unless a different -/// action is chosen in a subsequent tick. [`watch`] is very unfocussed and will -/// happily switch between actions rapidly between ticks if conditions change. -/// If you want something that tends to commit to actions until they are -/// completed, see [`choose`]. +/// action of the same or higher priority is chosen in a subsequent tick. +/// [`watch`] is very unfocussed and will happily switch between actions +/// rapidly between ticks if conditions change. If you want something that +/// tends to commit to actions until they are completed, see [`choose`]. /// /// # Example /// /// ```ignore -/// choose(|ctx| { +/// watch(|ctx| { /// if ctx.npc.is_being_attacked() { /// urgent(combat()) // If we're in danger, do something! /// } else if ctx.npc.is_hungry() { diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index c94ca5fae9..0e4b39f4f7 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -42,8 +42,10 @@ pub struct Chunk { /// this chunk. /// /// 0.0 => None of the resources generated by terrain generation should be - /// present 1.0 => All of the resources generated by terrain generation - /// should be present + /// present + /// + /// 1.0 => All of the resources generated by terrain generation should be + /// present /// /// It's important to understand this this number does not represent the /// total amount of a resource present in a chunk, nor is it even diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 4ff70046f8..23e4fcd902 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -83,8 +83,8 @@ pub struct Brain { #[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state - /// Represents the location of the NPC. pub seed: u32, + /// Represents the location of the NPC. pub wpos: Vec3, pub body: comp::Body, diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 056ea5aee2..8cab657361 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -310,7 +310,7 @@ fn goto_2d(wpos2d: Vec2, speed_factor: f32, goal_dist: f32) -> impl Action }) } -fn traverse_points(mut next_point: F) -> impl Action +fn traverse_points(mut next_point: F, speed_factor: f32) -> impl Action where F: FnMut(&mut NpcCtx) -> Option> + Send + Sync + 'static, { @@ -333,33 +333,40 @@ where if let Some(path) = path_site(wpos, site_exit, site, ctx.index) { Some(itertools::Either::Left( - seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))) - .then(goto_2d(site_exit, 1.0, 8.0)), + seq(path.into_iter().map(|wpos| goto_2d(wpos, 1.0, 8.0))).then(goto_2d( + site_exit, + speed_factor, + 8.0, + )), )) } else { - Some(itertools::Either::Right(goto_2d(site_exit, 1.0, 8.0))) + Some(itertools::Either::Right(goto_2d( + site_exit, + speed_factor, + 8.0, + ))) } } else { - Some(itertools::Either::Right(goto_2d(wpos, 1.0, 8.0))) + Some(itertools::Either::Right(goto_2d(wpos, speed_factor, 8.0))) } }) } /// Try to travel to a site. Where practical, paths will be taken. -fn travel_to_point(wpos: Vec2) -> impl Action { +fn travel_to_point(wpos: Vec2, speed_factor: f32) -> impl Action { now(move |ctx| { const WAYPOINT: f32 = 48.0; let start = ctx.npc.wpos.xy(); let diff = wpos - start; let n = (diff.magnitude() / WAYPOINT).max(1.0); let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n)); - traverse_points(move |_| points.next()) + traverse_points(move |_| points.next(), speed_factor) }) .debug(|| "travel to point") } /// Try to travel to a site. Where practical, paths will be taken. -fn travel_to_site(tgt_site: SiteId) -> impl Action { +fn travel_to_site(tgt_site: SiteId, speed_factor: f32) -> impl Action { now(move |ctx| { let sites = &ctx.state.data().sites; @@ -397,7 +404,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { } else { None } - }) + }, speed_factor) .boxed() // For every track in the path we discovered between the sites... @@ -438,7 +445,7 @@ fn travel_to_site(tgt_site: SiteId) -> impl Action { // .boxed() } else if let Some(site) = sites.get(tgt_site) { // If all else fails, just walk toward the target site in a straight line - travel_to_point(site.wpos.map(|e| e as f32 + 0.5)).boxed() + travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).boxed() } else { // If we can't find a way to get to the site at all, there's nothing more to be done finish().boxed() @@ -512,7 +519,7 @@ fn adventure() -> impl Action { .unwrap_or_default(); // Travel to the site important(just(move |ctx| ctx.controller.say(format!("I've spent enough time here, onward to {}!", site_name))) - .then(travel_to_site(tgt_site)) + .then(travel_to_site(tgt_site, 0.6)) // Stop for a few minutes .then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .map(|_| ()) @@ -572,7 +579,7 @@ fn villager(visiting_site: SiteId) -> impl Action { } else if ctx.npc.current_site != Some(visiting_site) { let npc_home = ctx.npc.home; // Travel to the site we're supposed to be in - return urgent(travel_to_site(visiting_site).debug(move || { + return urgent(travel_to_site(visiting_site, 1.0).debug(move || { if npc_home == Some(visiting_site) { "travel home".to_string() } else { @@ -602,7 +609,7 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { just(|ctx| ctx.controller.say("It's dark, time to go home")) - .then(travel_to_point(house_wpos)) + .then(travel_to_point(house_wpos, 0.65)) .debug(|| "walk to house") .then(socialize().repeat().debug(|| "wait in house")) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) @@ -621,7 +628,7 @@ fn villager(visiting_site: SiteId) -> impl Action { { if let Some(forest_wpos) = find_forest(ctx) { return casual( - travel_to_point(forest_wpos) + travel_to_point(forest_wpos, 0.5) .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(10.0..30.0); @@ -636,7 +643,7 @@ fn villager(visiting_site: SiteId) -> impl Action { if let Some(forest_wpos) = find_forest(ctx) { return casual( just(|ctx| ctx.controller.say("Time to go hunting!")) - .then(travel_to_point(forest_wpos)) + .then(travel_to_point(forest_wpos, 0.75)) .debug(|| "walk to forest") .then({ let wait_time = thread_rng().gen_range(30.0..60.0); @@ -685,7 +692,7 @@ fn villager(visiting_site: SiteId) -> impl Action { }) { // Walk to the plaza... - travel_to_point(plaza_wpos) + travel_to_point(plaza_wpos, 0.5) .debug(|| "walk to plaza") // ...then wait for some time before moving on .then({ diff --git a/server/src/lib.rs b/server/src/lib.rs index c79365524d..96b8baee2f 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -549,17 +549,6 @@ impl Server { let connection_handler = ConnectionHandler::new(network, &runtime); - // Initiate real-time world simulation - /* - #[cfg(feature = "worldgen")] - { - rtsim::init(&mut state, &world, index.as_index_ref()); - weather::init(&mut state, &world); - } - #[cfg(not(feature = "worldgen"))] - rtsim::init(&mut state); - */ - // Init rtsim, loading it from disk if possible #[cfg(feature = "worldgen")] { @@ -738,13 +727,6 @@ impl Server { add_local_systems(dispatcher_builder); sys::msg::add_server_systems(dispatcher_builder); sys::add_server_systems(dispatcher_builder); - /* - #[cfg(feature = "worldgen")] - { - rtsim::add_server_systems(dispatcher_builder); - weather::add_server_systems(dispatcher_builder); - } - */ #[cfg(feature = "worldgen")] { rtsim::add_server_systems(dispatcher_builder); diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index d9f5d8db63..5202769c42 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -22,7 +22,7 @@ use std::{ path::PathBuf, time::Instant, }; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info, trace, warn}; use vek::*; use world::{IndexRef, World}; @@ -175,10 +175,10 @@ impl RtSim { } pub fn save(&mut self, /* slowjob_pool: &SlowJobPool, */ wait_until_finished: bool) { - info!("Saving rtsim data..."); + debug!("Saving rtsim data..."); let file_path = self.file_path.clone(); let data = self.state.data().clone(); - debug!("Starting rtsim data save job..."); + trace!("Starting rtsim data save job..."); // TODO: Use slow job // slowjob_pool.spawn("RTSIM_SAVE", move || { let handle = std::thread::spawn(move || { diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index ec85a61727..5151fbea23 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -29,7 +29,6 @@ impl<'a> System<'a> for Sys { Read<'a, EventBus>, WriteStorage<'a, Agent>, WriteStorage<'a, Controller>, - //WriteExpect<'a, RtSim>, ); const NAME: &'static str = "agent"; @@ -38,9 +37,8 @@ impl<'a> System<'a> for Sys { fn run( job: &mut Job, - (read_data, event_bus, mut agents, mut controllers /* mut rtsim */): Self::SystemData, + (read_data, event_bus, mut agents, mut controllers): Self::SystemData, ) { - //let rtsim = &mut *rtsim; job.cpu_stats.measure(ParMode::Rayon); ( @@ -187,12 +185,6 @@ impl<'a> System<'a> for Sys { can_fly: moving_body.map_or(false, |b| b.fly_thrust().is_some()), }; let health_fraction = health.map_or(1.0, Health::fraction); - /* - let rtsim_entity = read_data - .rtsim_entities - .get(entity) - .and_then(|rtsim_ent| rtsim.get_entity(rtsim_ent.0)); - */ if traversal_config.can_fly && matches!(moving_body, Some(Body::Ship(_))) { // hack (kinda): Never turn off flight airships @@ -255,7 +247,6 @@ impl<'a> System<'a> for Sys { // inputs. let mut behavior_data = BehaviorData { agent, - // rtsim_entity, agent_data: data, read_data: &read_data, event_emitter: &mut event_emitter, @@ -269,25 +260,5 @@ impl<'a> System<'a> for Sys { debug_assert!(controller.inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); }, ); - /* - for (agent, rtsim_entity) in (&mut agents, &read_data.rtsim_entities).join() { - // Entity must be loaded in as it has an agent component :) - // React to all events in the controller - for event in core::mem::take(&mut agent.rtsim_controller.events) { - match event { - RtSimEvent::AddMemory(memory) => { - rtsim.insert_entity_memory(rtsim_entity.0, memory.clone()); - }, - RtSimEvent::ForgetEnemy(name) => { - rtsim.forget_entity_enemy(rtsim_entity.0, &name); - }, - RtSimEvent::SetMood(memory) => { - rtsim.set_entity_mood(rtsim_entity.0, memory.clone()); - }, - RtSimEvent::PrintMemories => {}, - } - } - } - */ } } diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 567758d4f1..0aaf20d73f 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -244,55 +244,6 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { }, Subject::Mood => { // TODO: Reimplement in rtsim2 - /* - if let Some(rtsim_entity) = &bdata.rtsim_entity { - if !rtsim_entity.brain.remembers_mood() { - // TODO: the following code will need a rework to - // implement more mood contexts - // This require that town NPCs becomes rtsim_entities to - // work fully. - match rand::random::() % 3 { - 0 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Good( - MoodContext::GoodWeather, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 1 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Neutral( - MoodContext::EverydayLife, - ), - }, - time_to_forget: read_data.time.0 + 21200.0, - }), - ), - 2 => agent.rtsim_controller.events.push( - RtSimEvent::SetMood(Memory { - item: MemoryItem::Mood { - state: MoodState::Bad( - MoodContext::GoodWeather, - ), - }, - time_to_forget: read_data.time.0 + 86400.0, - }), - ), - _ => {}, // will never happen - } - } - if let Some(memory) = rtsim_entity.brain.get_mood() { - let msg = match &memory.item { - MemoryItem::Mood { state } => state.describe(), - _ => "".to_string(), - }; - agent_data.chat_npc(msg, event_emitter); - } - }*/ }, Subject::Location(location) => { if let Some(tgt_pos) = read_data.positions.get(target) { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index c4deb96978..bfe2c5fb9b 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -74,7 +74,6 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, TerrainGrid>, Write<'a, TerrainChanges>, Write<'a, Vec>, - //WriteExpect<'a, RtSim>, RtSimData<'a>, TerrainPersistenceData<'a>, WriteStorage<'a, Pos>, @@ -107,7 +106,6 @@ impl<'a> System<'a> for Sys { mut terrain, mut terrain_changes, mut chunk_requests, - //mut rtsim, mut rtsim, mut _terrain_persistence, mut positions, From 2eaf3c7e925df1c4a37ea884971dff9ab1e77f5b Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 19:58:00 +0100 Subject: [PATCH 089/139] Spawn dogs and cats in towns --- assets/common/entity/wild/peaceful/dog.ron | 11 +++++++++++ world/src/site2/mod.rs | 21 +++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 assets/common/entity/wild/peaceful/dog.ron diff --git a/assets/common/entity/wild/peaceful/dog.ron b/assets/common/entity/wild/peaceful/dog.ron new file mode 100644 index 0000000000..df7bad54a9 --- /dev/null +++ b/assets/common/entity/wild/peaceful/dog.ron @@ -0,0 +1,11 @@ +#![enable(implicit_some)] +( + name: Automatic, + body: RandomWith("dog"), + alignment: Alignment(Wild), + loot: LootTable("common.loot_tables.creature.quad_small.generic"), + inventory: ( + loadout: FromBody, + ), + meta: [], +) diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 9da82b9d44..2c0b9f3d2a 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -18,6 +18,8 @@ use crate::{ }; use common::{ astar::Astar, + comp::Alignment, + generation::EntityInfo, lottery::Lottery, spiral::Spiral2d, store::{Id, Store}, @@ -1076,9 +1078,10 @@ impl Site { self.origin + tile * TILE_SIZE as i32 + TILE_SIZE as i32 / 2 } - pub fn render_tile(&self, canvas: &mut Canvas, _dynamic_rng: &mut impl Rng, tpos: Vec2) { + pub fn render_tile(&self, canvas: &mut Canvas, dynamic_rng: &mut impl Rng, tpos: Vec2) { let tile = self.tiles.get(tpos); let twpos = self.tile_wpos(tpos); + let twpos_center = self.tile_center_wpos(tpos); let border = TILE_SIZE as i32; let cols = (-border..TILE_SIZE as i32 + border).flat_map(|y| { (-border..TILE_SIZE as i32 + border) @@ -1111,11 +1114,9 @@ impl Site { let sub_surface_color = canvas .col(wpos2d) .map_or(Rgb::zero(), |col| col.sub_surface_color * 0.5); - let mut underground = true; for z in -8..6 { canvas.map(Vec3::new(wpos2d.x, wpos2d.y, alt + z), |b| { if b.kind() == BlockKind::Snow { - underground = false; b.into_vacant() } else if b.is_filled() { if b.is_terrain() { @@ -1127,11 +1128,23 @@ impl Site { b } } else { - underground = false; b.into_vacant() } }) } + if wpos2d == twpos_center && dynamic_rng.gen_bool(0.01) { + let spec = [ + "common.entity.wild.peaceful.cat", + "common.entity.wild.peaceful.dog", + ] + .choose(dynamic_rng) + .unwrap(); + canvas.spawn( + EntityInfo::at(Vec3::new(wpos2d.x, wpos2d.y, alt).as_()) + .with_asset_expect(&spec, dynamic_rng) + .with_alignment(Alignment::Tame), + ); + } } }); }, From c8d0443111101d774763187627a22b506845be04 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 20:12:57 +0100 Subject: [PATCH 090/139] Clippy fixes --- rtsim/src/rule/npc_ai.rs | 2 +- server/agent/src/action_nodes.rs | 2 +- server/src/cmd.rs | 2 +- server/src/sys/agent/behavior_tree.rs | 52 ++++++++++++--------------- world/src/site2/mod.rs | 2 +- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 8cab657361..7424fd847a 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -1,7 +1,7 @@ use std::hash::BuildHasherDefault; use crate::{ - ai::{casual, choose, finish, important, just, now, seq, until, urgent, Action, NpcCtx}, + ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx}, data::{ npc::{Brain, PathData}, Sites, diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index a3498cd2e4..4a5e1ee192 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -344,7 +344,7 @@ impl<'a> AgentData<'a> { } break 'activity; // Don't fall through to idle wandering }, - Some(NpcActivity::Gather(resources)) => { + Some(NpcActivity::Gather(_resources)) => { // TODO: Implement controller.push_action(ControlAction::Dance); break 'activity; // Don't fall through to idle wandering diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 246dd56875..1ea826763d 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1305,7 +1305,7 @@ fn handle_rtsim_npc( for (idx, _) in &npcs { let _ = write!(&mut info, "{}, ", idx); } - let _ = writeln!(&mut info, ""); + let _ = writeln!(&mut info); let _ = writeln!(&mut info, "Matched {} NPCs.", npcs.len()); server.notify_client( diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index e258bb2e00..6f9a914fd5 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -474,40 +474,34 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() { match action { NpcAction::Greet(actor) => { - if bdata.agent.allowed_to_speak() { - if let Some(target) = bdata.read_data.lookup_actor(actor) - && let Some(target_pos) = bdata.read_data.positions.get(target) - { - if bdata.agent_data.look_toward( - &mut bdata.controller, - &bdata.read_data, - target, - ) { - bdata.agent.target = Some(Target::new( - target, - false, - bdata.read_data.time.0, - false, - Some(target_pos.0), - )); + if bdata.agent.allowed_to_speak() + && let Some(target) = bdata.read_data.lookup_actor(actor) + && let Some(target_pos) = bdata.read_data.positions.get(target) + && bdata.agent_data.look_toward(bdata.controller, bdata.read_data, target) + { + bdata.agent.target = Some(Target::new( + target, + false, + bdata.read_data.time.0, + false, + Some(target_pos.0), + )); - bdata.controller.push_action(ControlAction::Talk); - bdata.controller.push_utterance(UtteranceKind::Greeting); - bdata - .agent_data - .chat_npc("npc-speech-villager_open", &mut bdata.event_emitter); - // Start a timer so that they eventually stop interacting - bdata - .agent - .timer - .start(bdata.read_data.time.0, TimerAction::Interact); - } - } + bdata.controller.push_action(ControlAction::Talk); + bdata.controller.push_utterance(UtteranceKind::Greeting); + bdata + .agent_data + .chat_npc("npc-speech-villager_open", bdata.event_emitter); + // Start a timer so that they eventually stop interacting + bdata + .agent + .timer + .start(bdata.read_data.time.0, TimerAction::Interact); } }, NpcAction::Say(msg) => { bdata.controller.push_utterance(UtteranceKind::Greeting); - bdata.agent_data.chat_npc(msg, &mut bdata.event_emitter); + bdata.agent_data.chat_npc(msg, bdata.event_emitter); }, } } diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 2c0b9f3d2a..223566da22 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -1141,7 +1141,7 @@ impl Site { .unwrap(); canvas.spawn( EntityInfo::at(Vec3::new(wpos2d.x, wpos2d.y, alt).as_()) - .with_asset_expect(&spec, dynamic_rng) + .with_asset_expect(spec, dynamic_rng) .with_alignment(Alignment::Tame), ); } From 082bcdb7550358383457497c82a0a6d959cc49b4 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 20:33:06 +0100 Subject: [PATCH 091/139] Don't hunt friendly animals --- common/src/comp/agent.rs | 16 +++++++++++++++- server/agent/src/action_nodes.rs | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 71d4113585..6d9785f924 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -83,7 +83,7 @@ impl Alignment { } } - // Never attacks + // Usually never attacks pub fn passive_towards(self, other: Alignment) -> bool { match (self, other) { (Alignment::Enemy, Alignment::Enemy) => true, @@ -98,6 +98,20 @@ impl Alignment { _ => false, } } + + // Never attacks + pub fn friendly_towards(self, other: Alignment) -> bool { + match (self, other) { + (Alignment::Enemy, Alignment::Enemy) => true, + (Alignment::Owned(a), Alignment::Owned(b)) if a == b => true, + (Alignment::Npc, Alignment::Npc) => true, + (Alignment::Npc, Alignment::Tame) => true, + (Alignment::Tame, Alignment::Npc) => true, + (Alignment::Tame, Alignment::Tame) => true, + (_, Alignment::Passive) => true, + _ => false, + } + } } impl Component for Alignment { diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 4a5e1ee192..031d6bb369 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -1632,6 +1632,7 @@ impl<'a> AgentData<'a> { pub fn is_hunting_animal(&self, entity: EcsEntity, read_data: &ReadData) -> bool { (entity != *self.entity) + && !self.friendly_towards(entity, read_data) && matches!(read_data.bodies.get(entity), Some(Body::QuadrupedSmall(_))) } @@ -1667,6 +1668,16 @@ impl<'a> AgentData<'a> { } } + fn friendly_towards(&self, entity: EcsEntity, read_data: &ReadData) -> bool { + if let (Some(self_alignment), Some(other_alignment)) = + (self.alignment, read_data.alignments.get(entity)) + { + self_alignment.friendly_towards(*other_alignment) + } else { + false + } + } + pub fn can_see_entity( &self, agent: &Agent, From 2e047f67237883d565f72de2cea9938d577cbe76 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 21:27:16 +0100 Subject: [PATCH 092/139] Use atomic file for rtsim data --- server/src/rtsim/mod.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/server/src/rtsim/mod.rs b/server/src/rtsim/mod.rs index 5202769c42..e74a5e0eaa 100644 --- a/server/src/rtsim/mod.rs +++ b/server/src/rtsim/mod.rs @@ -2,6 +2,7 @@ pub mod event; pub mod rule; pub mod tick; +use atomicwrites::{AtomicFile, OverwriteBehavior}; use common::{ grid::Grid, rtsim::{ChunkResource, RtSimEntity, RtSimVehicle, WorldSettings}, @@ -18,7 +19,7 @@ use specs::DispatcherBuilder; use std::{ error::Error, fs::{self, File}, - io::{self, Write}, + io, path::PathBuf, time::Instant, }; @@ -182,23 +183,24 @@ impl RtSim { // TODO: Use slow job // slowjob_pool.spawn("RTSIM_SAVE", move || { let handle = std::thread::spawn(move || { - let tmp_file_name = "data_tmp.dat"; if let Err(e) = file_path .parent() .map(|dir| { fs::create_dir_all(dir)?; // We write to a temporary file and then rename to avoid corruption. - Ok(dir.join(tmp_file_name)) + Ok(dir.join(&file_path)) }) - .unwrap_or_else(|| Ok(tmp_file_name.into())) - .and_then(|tmp_file_path| Ok((File::create(&tmp_file_path)?, tmp_file_path))) + .unwrap_or(Ok(file_path)) + .map(|file_path| AtomicFile::new(file_path, OverwriteBehavior::AllowOverwrite)) .map_err(|e: io::Error| Box::new(e) as Box) - .and_then(|(mut file, tmp_file_path)| { + .and_then(|file| { debug!("Writing rtsim data to file..."); - data.write_to(io::BufWriter::new(&mut file))?; - file.flush()?; + file.write(move |file| -> Result<(), rtsim::data::WriteError> { + data.write_to(io::BufWriter::new(file))?; + // file.flush()?; + Ok(()) + })?; drop(file); - fs::rename(tmp_file_path, file_path)?; debug!("Rtsim data saved."); Ok(()) }) From b2627e2690c35d945a323477340f127a6447874f Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Mon, 3 Apr 2023 21:41:22 +0100 Subject: [PATCH 093/139] Use cheap RNG in NPC AI code --- Cargo.lock | 1 + rtsim/Cargo.toml | 1 + rtsim/src/ai/mod.rs | 3 +++ rtsim/src/rule/npc_ai.rs | 43 +++++++++++++++++++--------------------- 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17a61eee1e..05d8a07480 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6952,6 +6952,7 @@ dependencies = [ "hashbrown 0.12.3", "itertools", "rand 0.8.5", + "rand_chacha 0.3.1", "rayon", "rmp-serde", "ron 0.8.0", diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index fa2fd17800..639680d709 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -17,6 +17,7 @@ tracing = "0.1" atomic_refcell = "0.1" slotmap = { version = "1.0.6", features = ["serde"] } rand = { version = "0.8", features = ["small_rng"] } +rand_chacha = "0.3" fxhash = "0.2.1" itertools = "0.10.3" rayon = "1.5" diff --git a/rtsim/src/ai/mod.rs b/rtsim/src/ai/mod.rs index 9995e2b546..2122442ef3 100644 --- a/rtsim/src/ai/mod.rs +++ b/rtsim/src/ai/mod.rs @@ -3,6 +3,7 @@ use crate::{ RtState, }; use common::resources::{Time, TimeOfDay}; +use rand_chacha::ChaChaRng; use std::{any::Any, marker::PhantomData, ops::ControlFlow}; use world::{IndexRef, World}; @@ -20,6 +21,8 @@ pub struct NpcCtx<'a> { pub npc_id: NpcId, pub npc: &'a Npc, pub controller: &'a mut Controller, + + pub rng: ChaChaRng, } /// A trait that describes 'actions': long-running tasks performed by rtsim diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 7424fd847a..27bed72b37 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -22,6 +22,7 @@ use common::{ use fxhash::FxHasher64; use itertools::Itertools; use rand::prelude::*; +use rand_chacha::ChaChaRng; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use vek::*; use world::{ @@ -245,6 +246,7 @@ impl Rule for NpcAi { npc, npc_id: *npc_id, controller, + rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()), }); }); } @@ -462,17 +464,16 @@ fn timeout(time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { fn socialize() -> impl Action { now(|ctx| { - let mut rng = thread_rng(); // TODO: Bit odd, should wait for a while after greeting - if thread_rng().gen_bool(0.0003) && let Some(other) = ctx + if ctx.rng.gen_bool(0.0003) && let Some(other) = ctx .state .data() .npcs .nearby(ctx.npc.wpos.xy(), 8.0) - .choose(&mut rng) + .choose(&mut ctx.rng) { just(move |ctx| ctx.controller.greet(other)).boxed() - } else if thread_rng().gen_bool(0.0003) { + } else if ctx.rng.gen_bool(0.0003) { just(|ctx| ctx.controller.do_dance()) .repeat() .stop_if(timeout(6.0)) @@ -504,7 +505,7 @@ fn adventure() -> impl Action { | SiteKind::DesertCity(_) ), ) && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) - && thread_rng().gen_bool(0.25) + && ctx.rng.gen_bool(0.25) }) .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) .map(|(site_id, _)| site_id) @@ -549,10 +550,10 @@ fn hunt_animals() -> impl Action { just(|ctx| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals") } -fn find_forest(ctx: &NpcCtx) -> Option> { +fn find_forest(ctx: &mut NpcCtx) -> Option> { let chunk_pos = ctx.npc.wpos.xy().as_() / TerrainChunkSize::RECT_SIZE.as_(); Spiral2d::new() - .skip(thread_rng().gen_range(1..=8)) + .skip(ctx.rng.gen_range(1..=8)) .take(49) .map(|rpos| chunk_pos + rpos) .find(|cpos| { @@ -604,7 +605,7 @@ fn villager(visiting_site: SiteId) -> impl Action { let house = site2 .plots() .filter(|p| matches!(p.kind(), PlotKind::House(_))) - .choose(&mut thread_rng())?; + .choose(&mut ctx.rng)?; Some(site2.tile_center_wpos(house.root_tile()).as_()) }) { @@ -623,37 +624,33 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "find somewhere to sleep"), ); // Villagers with roles should perform those roles - } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) - && thread_rng().gen_bool(0.8) + } else if matches!(ctx.npc.profession, Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8) { if let Some(forest_wpos) = find_forest(ctx) { return casual( travel_to_point(forest_wpos, 0.5) .debug(|| "walk to forest") .then({ - let wait_time = thread_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)) }) .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) - && thread_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) { return casual( just(|ctx| ctx.controller.say("Time to go hunting!")) .then(travel_to_point(forest_wpos, 0.75)) .debug(|| "walk to forest") .then({ - let wait_time = thread_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)) }) .map(|_| ()), ); } - } else if matches!(ctx.npc.profession, Some(Profession::Merchant)) - && thread_rng().gen_bool(0.8) + } else if matches!(ctx.npc.profession, Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8) { return casual( just(|ctx| { @@ -665,7 +662,7 @@ fn villager(visiting_site: SiteId) -> impl Action { "Looking for supplies? I've got you covered.", ] .iter() - .choose(&mut thread_rng()) + .choose(&mut ctx.rng) .unwrap(), ) // Can't fail }) @@ -687,7 +684,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .get(visiting_site) .and_then(|site| ctx.index.sites.get(site.world_site?).site2()) .and_then(|site2| { - let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?]; + let plaza = &site2.plots[site2.plazas().choose(&mut ctx.rng)?]; Some(site2.tile_center_wpos(plaza.root_tile()).as_()) }) { @@ -696,7 +693,7 @@ fn villager(visiting_site: SiteId) -> impl Action { .debug(|| "walk to plaza") // ...then wait for some time before moving on .then({ - let wait_time = thread_rng().gen_range(30.0..90.0); + let wait_time = ctx.rng.gen_range(30.0..90.0); socialize().repeat().stop_if(timeout(wait_time)) .debug(|| "wait at plaza") }) @@ -794,7 +791,7 @@ fn pilot() -> impl Action { .and_then(|site| ctx.index.sites.get(site).kind.convert_to_meta()) .map_or(false, |meta| matches!(meta, SiteKindMeta::Settlement(_))) }) - .choose(&mut thread_rng()); + .choose(&mut ctx.rng); if let Some((_id, site)) = site { let start_chunk = ctx.npc.wpos.xy().as_::() / TerrainChunkSize::RECT_SIZE.as_::(); @@ -826,7 +823,7 @@ fn captain() -> impl Action { .get(*neighbor) .map_or(false, |c| c.river.river_kind.is_some()) }) - .choose(&mut thread_rng()) + .choose(&mut ctx.rng) { let wpos = TerrainChunkSize::center_wpos(chunk); let wpos = wpos.as_().with_z( @@ -891,7 +888,7 @@ fn bird_large() -> impl Action { matches!(ctx.index.sites.get(site).kind, SiteKind::Dungeon(_)) }) }) - .choose(&mut thread_rng()) + .choose(&mut ctx.rng) { casual(goto( site.wpos.as_::().with_z( From 06820dbf16b1e97a8cbf2806a5e45c99084d1db7 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 4 Apr 2023 10:36:29 +0100 Subject: [PATCH 094/139] Better path distance check for site2 --- world/src/site2/mod.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 223566da22..3d4f557047 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -340,8 +340,17 @@ impl Site { .map(|tile| tile.kind = TileKind::Hazard(kind)); } } - if let Some((dist, _, Path { width }, _)) = land.get_nearest_path(wpos) { - if dist < 2.0 * width { + if let Some((_, path_wpos, Path { width }, _)) = land.get_nearest_path(wpos) { + let tile_aabb = Aabr { + min: self.tile_wpos(tile), + max: self.tile_wpos(tile + 1) - 1, + }; + + if (tile_aabb + .projected_point(path_wpos.as_()) + .distance_squared(path_wpos.as_()) as f32) + < width.powi(2) + { self.tiles .get_mut(tile) .map(|tile| tile.kind = TileKind::Path); From 85c572f6e2433c651fdab3bcaeae25dd7bbff110 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 4 Apr 2023 11:06:51 +0100 Subject: [PATCH 095/139] Better town layout --- common/src/astar.rs | 8 ++++---- common/src/path.rs | 4 ++-- common/src/states/utils.rs | 6 +++--- rtsim/src/rule/npc_ai.rs | 29 ++++++++--------------------- world/src/civ/mod.rs | 12 +++--------- world/src/site/settlement/mod.rs | 13 ++++--------- world/src/site2/mod.rs | 21 +++++++++++---------- world/src/site2/plot/dungeon.rs | 10 +++------- 8 files changed, 38 insertions(+), 65 deletions(-) diff --git a/common/src/astar.rs b/common/src/astar.rs index 37b9b09f95..83a263fa76 100644 --- a/common/src/astar.rs +++ b/common/src/astar.rs @@ -87,7 +87,7 @@ impl fmt::Debug for Astar Astar { - pub fn new(max_iters: usize, start: S, heuristic: impl FnOnce(&S) -> f32, hasher: H) -> Self { + pub fn new(max_iters: usize, start: S, hasher: H) -> Self { Self { max_iters, iter: 0, @@ -104,7 +104,7 @@ impl Astar { }, final_scores: { let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone()); - h.extend(core::iter::once((start.clone(), heuristic(&start)))); + h.extend(core::iter::once((start.clone(), 0.0))); h }, visited: { @@ -120,7 +120,7 @@ impl Astar { pub fn poll( &mut self, iters: usize, - mut heuristic: impl FnMut(&S) -> f32, + mut heuristic: impl FnMut(&S, &S) -> f32, mut neighbors: impl FnMut(&S) -> I, mut transition: impl FnMut(&S, &S) -> f32, mut satisfied: impl FnMut(&S) -> bool, @@ -143,7 +143,7 @@ impl Astar { if cost < *neighbor_cheapest { self.came_from.insert(neighbor.clone(), node.clone()); self.cheapest_scores.insert(neighbor.clone(), cost); - let h = heuristic(&neighbor); + let h = heuristic(&neighbor, &node); let neighbor_cost = cost + h; self.final_scores.insert(neighbor.clone(), neighbor_cost); diff --git a/common/src/path.rs b/common/src/path.rs index 682a9ff16c..a6dc55a5c4 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -534,7 +534,7 @@ where _ => return (None, false), }; - let heuristic = |pos: &Vec3| (pos.distance_squared(end) as f32).sqrt(); + let heuristic = |pos: &Vec3, _: &Vec3| (pos.distance_squared(end) as f32).sqrt(); let neighbors = |pos: &Vec3| { let pos = *pos; const DIRS: [Vec3; 17] = [ @@ -639,7 +639,7 @@ where let satisfied = |pos: &Vec3| pos == &end; let mut new_astar = match astar.take() { - None => Astar::new(25_000, start, heuristic, DefaultHashBuilder::default()), + None => Astar::new(25_000, start, DefaultHashBuilder::default()), Some(astar) => astar, }; diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 085832b890..3aaf1177b8 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -929,13 +929,13 @@ pub fn handle_manipulate_loadout( let iters = (3.0 * (sprite_pos_f32 - data.pos.0).map(|x| x.abs()).sum()) as usize; // Heuristic compares manhattan distance of start and end pos - let heuristic = - move |pos: &Vec3| (sprite_pos - pos).map(|x| x.abs()).sum() as f32; + let heuristic = move |pos: &Vec3, _: &Vec3| { + (sprite_pos - pos).map(|x| x.abs()).sum() as f32 + }; let mut astar = Astar::new( iters, data.pos.0.map(|x| x.floor() as i32), - heuristic, BuildHasherDefault::::default(), ); diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 27bed72b37..380423c5ff 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -43,13 +43,8 @@ const CARDINALS: &[Vec2] = &[ ]; fn path_in_site(start: Vec2, end: Vec2, site: &site2::Site) -> PathResult> { - let heuristic = |tile: &Vec2| tile.as_::().distance(end.as_()); - let mut astar = Astar::new( - 1000, - start, - heuristic, - BuildHasherDefault::::default(), - ); + let heuristic = |tile: &Vec2, _: &Vec2| tile.as_::().distance(end.as_()); + let mut astar = Astar::new(1000, start, BuildHasherDefault::::default()); let transition = |a: &Vec2, b: &Vec2| { let distance = a.as_::().distance(b.as_()); @@ -128,14 +123,10 @@ fn path_between_sites( let get_site = |site: &Id| world.civs().sites.get(*site); let end_pos = get_site(&end).center.as_::(); - let heuristic = |site: &Id| get_site(site).center.as_().distance(end_pos); + let heuristic = + |site: &Id, _: &Id| get_site(site).center.as_().distance(end_pos); - let mut astar = Astar::new( - 250, - start, - heuristic, - BuildHasherDefault::::default(), - ); + let mut astar = Astar::new(250, start, BuildHasherDefault::::default()); let neighbors = |site: &Id| world.civs().neighbors(*site); @@ -734,14 +725,10 @@ fn chunk_path( to: Vec2, chunk_height: impl Fn(Vec2) -> Option, ) -> Box { - let heuristics = |(p, _): &(Vec2, i32)| p.distance_squared(to) as f32; + let heuristics = + |(p, _): &(Vec2, i32), _: &(Vec2, i32)| p.distance_squared(to) as f32; let start = (from, chunk_height(from).unwrap()); - let mut astar = Astar::new( - 1000, - start, - heuristics, - BuildHasherDefault::::default(), - ); + let mut astar = Astar::new(1000, start, BuildHasherDefault::::default()); let path = astar.poll( 1000, diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index c456da2f17..e482d844c6 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -659,7 +659,7 @@ impl Civs { /// Find the cheapest route between two places fn route_between(&self, a: Id, b: Id) -> Option<(Path>, f32)> { - let heuristic = move |p: &Id| { + let heuristic = move |p: &Id, _: &Id| { (self .sites .get(*p) @@ -676,12 +676,7 @@ impl Civs { // (1) we don't care about DDOS attacks (ruling out SipHash); // (2) we care about determinism across computers (ruling out AAHash); // (3) we have 8-byte keys (for which FxHash is fastest). - let mut astar = Astar::new( - 100, - a, - heuristic, - BuildHasherDefault::::default(), - ); + let mut astar = Astar::new(100, a, BuildHasherDefault::::default()); astar .poll(100, heuristic, neighbors, transition, satisfied) .into_path() @@ -1306,7 +1301,7 @@ fn find_path( ) -> Option<(Path>, f32)> { const MAX_PATH_ITERS: usize = 100_000; let sim = &ctx.sim; - let heuristic = move |l: &Vec2| (l.distance_squared(b) as f32).sqrt(); + let heuristic = move |l: &Vec2, _: &Vec2| (l.distance_squared(b) as f32).sqrt(); let get_bridge = &get_bridge; let neighbors = |l: &Vec2| { let l = *l; @@ -1327,7 +1322,6 @@ fn find_path( let mut astar = Astar::new( MAX_PATH_ITERS, a, - heuristic, BuildHasherDefault::::default(), ); astar diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 1c2b16178f..c7d394f4d8 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -1349,7 +1349,7 @@ impl Land { dest: Vec2, mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32, ) -> Option>> { - let heuristic = |pos: &Vec2| (pos - dest).map(|e| e as f32).magnitude(); + let heuristic = |pos: &Vec2, _: &Vec2| (pos - dest).map(|e| e as f32).magnitude(); let neighbors = |pos: &Vec2| { let pos = *pos; CARDINALS.iter().map(move |dir| pos + *dir) @@ -1362,14 +1362,9 @@ impl Land { // (1) we don't care about DDOS attacks (ruling out SipHash); // (2) we don't care about determinism across computers (we could use AAHash); // (3) we have 8-byte keys (for which FxHash is fastest). - Astar::new( - 250, - origin, - heuristic, - BuildHasherDefault::::default(), - ) - .poll(250, heuristic, neighbors, transition, satisfied) - .into_path() + Astar::new(250, origin, BuildHasherDefault::::default()) + .poll(250, heuristic, neighbors, transition, satisfied) + .into_path() } /// We use this hasher (FxHasher64) because diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs index 3d4f557047..8d8964ab5f 100644 --- a/world/src/site2/mod.rs +++ b/world/src/site2/mod.rs @@ -146,7 +146,8 @@ impl Site { ) -> Option> { const MAX_ITERS: usize = 4096; let range = -(w as i32) / 2..w as i32 - (w as i32 + 1) / 2; - let heuristic = |tile: &Vec2| { + let heuristic = |(tile, dir): &(Vec2, Vec2), + (_, old_dir): &(Vec2, Vec2)| { let mut max_cost = (tile.distance_squared(b) as f32).sqrt(); for y in range.clone() { for x in range.clone() { @@ -157,35 +158,35 @@ impl Site { } } } - max_cost + max_cost + (dir != old_dir) as i32 as f32 * 35.0 }; - let path = Astar::new(MAX_ITERS, a, heuristic, DefaultHashBuilder::default()) + let path = Astar::new(MAX_ITERS, (a, Vec2::zero()), DefaultHashBuilder::default()) .poll( MAX_ITERS, &heuristic, - |tile| { + |(tile, _)| { let tile = *tile; - CARDINALS.iter().map(move |dir| tile + *dir) + CARDINALS.iter().map(move |dir| (tile + *dir, *dir)) }, - |a, b| { + |(a, _), (b, _)| { let alt_a = land.get_alt_approx(self.tile_center_wpos(*a)); let alt_b = land.get_alt_approx(self.tile_center_wpos(*b)); (alt_a - alt_b).abs() / TILE_SIZE as f32 }, - |tile| *tile == b, + |(tile, _)| *tile == b, ) .into_path()?; let plot = self.create_plot(Plot { - kind: PlotKind::Road(path.clone()), + kind: PlotKind::Road(path.iter().map(|(tile, _)| *tile).collect()), root_tile: a, - tiles: path.clone().into_iter().collect(), + tiles: path.iter().map(|(tile, _)| *tile).collect(), seed: rng.gen(), }); self.roads.push(plot); - for (i, &tile) in path.iter().enumerate() { + for (i, (tile, _)) in path.iter().enumerate() { for y in range.clone() { for x in range.clone() { let tile = tile + Vec2::new(x, y); diff --git a/world/src/site2/plot/dungeon.rs b/world/src/site2/plot/dungeon.rs index 8b6d2d0a99..0adb946c78 100644 --- a/world/src/site2/plot/dungeon.rs +++ b/world/src/site2/plot/dungeon.rs @@ -543,7 +543,8 @@ impl Floor { } fn create_route(&mut self, _ctx: &mut GenCtx, a: Vec2, b: Vec2) { - let heuristic = move |l: &Vec2| (l - b).map(|e| e.abs()).reduce_max() as f32; + let heuristic = + move |l: &Vec2, _: &Vec2| (l - b).map(|e| e.abs()).reduce_max() as f32; let neighbors = |l: &Vec2| { let l = *l; CARDINALS @@ -562,12 +563,7 @@ impl Floor { // (1) we don't care about DDOS attacks (ruling out SipHash); // (2) we don't care about determinism across computers (we could use AAHash); // (3) we have 8-byte keys (for which FxHash is fastest). - let mut astar = Astar::new( - 20000, - a, - heuristic, - BuildHasherDefault::::default(), - ); + let mut astar = Astar::new(20000, a, BuildHasherDefault::::default()); let path = astar .poll( FLOOR_SIZE.product() as usize + 1, From 3e0f5295c036ce1e39554612fc40da86653965d1 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Tue, 4 Apr 2023 15:55:57 +0100 Subject: [PATCH 096/139] Added CharacterActivity, made NPCs look at the player when speaking to them --- common/net/src/synced_components.rs | 5 + common/src/comp/agent.rs | 5 + common/src/comp/body.rs | 2 +- common/src/comp/character_state.rs | 20 ++ common/src/comp/compass.rs | 1 + common/src/comp/mod.rs | 2 +- common/src/states/basic_ranged.rs | 5 +- common/src/states/basic_summon.rs | 2 +- common/src/states/behavior.rs | 7 +- common/src/states/charged_ranged.rs | 4 +- common/src/states/repeater_ranged.rs | 4 +- common/src/states/utils.rs | 7 +- common/state/src/state.rs | 1 + common/systems/src/character_behavior.rs | 19 +- common/systems/src/controller.rs | 9 +- common/systems/src/melee.rs | 5 +- server/agent/src/action_nodes.rs | 40 ++- server/agent/src/attack.rs | 137 ++++++- server/agent/src/util.rs | 12 +- server/src/state_ext.rs | 3 + server/src/sys/agent/behavior_tree.rs | 25 +- .../sys/agent/behavior_tree/interaction.rs | 339 +++++++++--------- voxygen/anim/src/character/talk.rs | 3 +- voxygen/egui/src/lib.rs | 56 ++- voxygen/src/scene/figure/mod.rs | 21 +- voxygen/src/scene/mod.rs | 2 +- voxygen/src/scene/particle.rs | 12 +- 27 files changed, 491 insertions(+), 257 deletions(-) diff --git a/common/net/src/synced_components.rs b/common/net/src/synced_components.rs index 299a496ee6..71bcf20610 100644 --- a/common/net/src/synced_components.rs +++ b/common/net/src/synced_components.rs @@ -40,6 +40,7 @@ macro_rules! synced_components { sticky: Sticky, immovable: Immovable, character_state: CharacterState, + character_activity: CharacterActivity, shockwave: Shockwave, beam_segment: BeamSegment, alignment: Alignment, @@ -201,6 +202,10 @@ impl NetSync for CharacterState { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } +impl NetSync for CharacterActivity { + const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; +} + impl NetSync for Shockwave { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 6d9785f924..24a01fc26c 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -625,6 +625,11 @@ impl Awareness { self.reached = false; } } + + pub fn set_maximally_aware(&mut self) { + self.reached = true; + self.level = Self::ALERT; + } } #[derive(Clone, Debug, PartialOrd, PartialEq, Eq)] diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index b259123d0d..de07e6e590 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -983,7 +983,7 @@ impl Body { } /// Returns the eye height for this creature. - pub fn eye_height(&self) -> f32 { self.height() * 0.9 } + pub fn eye_height(&self, scale: f32) -> f32 { self.height() * 0.9 * scale } pub fn default_light_offset(&self) -> Vec3 { // TODO: Make this a manifest diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 1c3bf62e55..e914d4b32b 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -12,6 +12,7 @@ use crate::{ utils::{AbilityInfo, StageSection}, *, }, + util::Dir, }; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; @@ -30,6 +31,7 @@ pub struct StateUpdate { pub should_strafe: bool, pub queued_inputs: BTreeMap, pub removed_inputs: Vec, + pub character_activity: CharacterActivity, } pub struct OutputEvents<'a> { @@ -60,6 +62,7 @@ impl From<&JoinData<'_>> for StateUpdate { character: data.character.clone(), queued_inputs: BTreeMap::new(), removed_inputs: Vec::new(), + character_activity: *data.character_activity, } } } @@ -979,3 +982,20 @@ impl Default for CharacterState { impl Component for CharacterState { type Storage = DerefFlaggedStorage>; } + +/// Contains information about the visual activity of a character. +/// +/// For now this only includes the direction they're looking in, but later it +/// might include markers indicating that they're available for +/// trade/interaction, more details about their stance or appearance, facial +/// expression, etc. +#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +pub struct CharacterActivity { + /// `None` means that the look direction should be derived from the + /// orientation + pub look_dir: Option

, +} + +impl Component for CharacterActivity { + type Storage = DerefFlaggedStorage>; +} diff --git a/common/src/comp/compass.rs b/common/src/comp/compass.rs index 389c4fb67f..4937e87d1c 100644 --- a/common/src/comp/compass.rs +++ b/common/src/comp/compass.rs @@ -1,4 +1,5 @@ use vek::Vec2; +// TODO: Move this to common/src/, it's not a component /// Cardinal directions pub enum Direction { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index a66a22f130..9b47025300 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -73,7 +73,7 @@ pub use self::{ Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs, ModifierKind, }, - character_state::{CharacterState, StateUpdate}, + character_state::{CharacterActivity, CharacterState, StateUpdate}, chat::{ ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg, }, diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs index 84fbd82165..f03e7810f2 100644 --- a/common/src/states/basic_ranged.rs +++ b/common/src/states/basic_ranged.rs @@ -91,7 +91,10 @@ impl CharacterBehavior for Data { // Shoots all projectiles simultaneously for i in 0..self.static_data.num_projectiles { // Gets offsets - let body_offsets = data.body.projectile_offsets(update.ori.look_vec()); + let body_offsets = data.body.projectile_offsets( + update.ori.look_vec(), + data.scale.map_or(1.0, |s| s.0), + ); let pos = Pos(data.pos.0 + body_offsets); // Adds a slight spread to the projectiles. First projectile has no spread, // and spread increases linearly with number of projectiles created. diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 316985b89f..fdda7ed54d 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -149,7 +149,7 @@ impl CharacterBehavior for Data { let collision_vector = Vec3::new( data.pos.0.x + (summon_frac * 2.0 * PI).sin() * obstacle_xy, data.pos.0.y + (summon_frac * 2.0 * PI).cos() * obstacle_xy, - data.pos.0.z + data.body.eye_height(), + data.pos.0.z + data.body.eye_height(data.scale.map_or(1.0, |s| s.0)), ); // Check for collision in z up to 50 blocks diff --git a/common/src/states/behavior.rs b/common/src/states/behavior.rs index dd52911ca8..0746a94b84 100644 --- a/common/src/states/behavior.rs +++ b/common/src/states/behavior.rs @@ -3,8 +3,8 @@ use crate::{ self, character_state::OutputEvents, item::{tool::AbilityMap, MaterialStatManifest}, - ActiveAbilities, Beam, Body, CharacterState, Combo, ControlAction, Controller, - ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, + ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, ControlAction, + Controller, ControllerInputs, Density, Energy, Health, InputAttr, InputKind, Inventory, InventoryAction, Mass, Melee, Ori, PhysicsState, Pos, Scale, SkillSet, Stance, StateUpdate, Stats, Vel, }, @@ -120,6 +120,7 @@ pub struct JoinData<'a> { pub entity: Entity, pub uid: &'a Uid, pub character: &'a CharacterState, + pub character_activity: &'a CharacterActivity, pub pos: &'a Pos, pub vel: &'a Vel, pub ori: &'a Ori, @@ -153,6 +154,7 @@ pub struct JoinStruct<'a> { pub entity: Entity, pub uid: &'a Uid, pub char_state: FlaggedAccessMut<'a, &'a mut CharacterState, CharacterState>, + pub character_activity: FlaggedAccessMut<'a, &'a mut CharacterActivity, CharacterActivity>, pub pos: &'a mut Pos, pub vel: &'a mut Vel, pub ori: &'a mut Ori, @@ -190,6 +192,7 @@ impl<'a> JoinData<'a> { entity: j.entity, uid: j.uid, character: &j.char_state, + character_activity: &j.character_activity, pos: j.pos, vel: j.vel, ori: j.ori, diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs index 0277c42312..fcc03f9b2b 100644 --- a/common/src/states/charged_ranged.rs +++ b/common/src/states/charged_ranged.rs @@ -114,7 +114,9 @@ impl CharacterBehavior for Data { get_crit_data(data, self.static_data.ability_info); let tool_stats = get_tool_stats(data, self.static_data.ability_info); // Gets offsets - let body_offsets = data.body.projectile_offsets(update.ori.look_vec()); + let body_offsets = data + .body + .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0)); let pos = Pos(data.pos.0 + body_offsets); let projectile = arrow.create_projectile( Some(*data.uid), diff --git a/common/src/states/repeater_ranged.rs b/common/src/states/repeater_ranged.rs index 639c47f57b..f1bcce0972 100644 --- a/common/src/states/repeater_ranged.rs +++ b/common/src/states/repeater_ranged.rs @@ -95,7 +95,9 @@ impl CharacterBehavior for Data { get_crit_data(data, self.static_data.ability_info); let tool_stats = get_tool_stats(data, self.static_data.ability_info); // Gets offsets - let body_offsets = data.body.projectile_offsets(update.ori.look_vec()); + let body_offsets = data + .body + .projectile_offsets(update.ori.look_vec(), data.scale.map_or(1.0, |s| s.0)); let pos = Pos(data.pos.0 + body_offsets); let projectile = self.static_data.projectile.create_projectile( Some(*data.uid), diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 3aaf1177b8..ade2c10ed0 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -298,10 +298,10 @@ impl Body { /// Returns the position where a projectile should be fired relative to this /// body - pub fn projectile_offsets(&self, ori: Vec3) -> Vec3 { + pub fn projectile_offsets(&self, ori: Vec3, scale: f32) -> Vec3 { let body_offsets_z = match self { Body::Golem(_) => self.height() * 0.4, - _ => self.eye_height(), + _ => self.eye_height(scale), }; let dim = self.dimensions(); @@ -611,6 +611,9 @@ pub fn handle_orientation( .ori .slerped_towards(target_ori, target_fraction.min(1.0)) }; + + // Look at things + update.character_activity.look_dir = Some(data.controller.inputs.look_dir); } /// Updates components to move player as if theyre swimming diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 20ac10d6d4..db0d1f834c 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -199,6 +199,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index aaad927eaa..39ffd3e13a 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -8,9 +8,9 @@ use common::{ self, character_state::OutputEvents, inventory::item::{tool::AbilityMap, MaterialStatManifest}, - ActiveAbilities, Beam, Body, CharacterState, Combo, Controller, Density, Energy, Health, - Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, Scale, SkillSet, - Stance, StateUpdate, Stats, Vel, + ActiveAbilities, Beam, Body, CharacterActivity, CharacterState, Combo, Controller, Density, + Energy, Health, Inventory, InventoryManip, Mass, Melee, Ori, PhysicsState, Poise, Pos, + Scale, SkillSet, Stance, StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, link::Is, @@ -65,6 +65,7 @@ impl<'a> System<'a> for Sys { type SystemData = ( ReadData<'a>, WriteStorage<'a, CharacterState>, + WriteStorage<'a, CharacterActivity>, WriteStorage<'a, Pos>, WriteStorage<'a, Vel>, WriteStorage<'a, Ori>, @@ -84,6 +85,7 @@ impl<'a> System<'a> for Sys { ( read_data, mut character_states, + mut character_activities, mut positions, mut velocities, mut orientations, @@ -106,6 +108,7 @@ impl<'a> System<'a> for Sys { entity, uid, mut char_state, + character_activity, pos, vel, ori, @@ -116,13 +119,13 @@ impl<'a> System<'a> for Sys { controller, health, body, - physics, - (scale, stat, skill_set, active_abilities, is_rider), + (physics, scale, stat, skill_set, active_abilities, is_rider), combo, ) in ( &read_data.entities, &read_data.uids, &mut character_states, + &mut character_activities, &mut positions, &mut velocities, &mut orientations, @@ -133,8 +136,8 @@ impl<'a> System<'a> for Sys { &mut controllers, read_data.healths.maybe(), &read_data.bodies, - &read_data.physics_states, ( + &read_data.physics_states, read_data.scales.maybe(), &read_data.stats, &read_data.skill_sets, @@ -182,6 +185,7 @@ impl<'a> System<'a> for Sys { entity, uid, char_state, + character_activity, pos, vel, ori, @@ -261,6 +265,9 @@ impl Sys { if *join.char_state != state_update.character { *join.char_state = state_update.character } + if *join.character_activity != state_update.character_activity { + *join.character_activity = state_update.character_activity + } if *join.density != state_update.density { *join.density = state_update.density } diff --git a/common/systems/src/controller.rs b/common/systems/src/controller.rs index 2386709f06..0102b5ee11 100644 --- a/common/systems/src/controller.rs +++ b/common/systems/src/controller.rs @@ -2,7 +2,7 @@ use common::{ comp::{ ability::Stance, agent::{Sound, SoundKind}, - Body, BuffChange, ControlEvent, Controller, Pos, + Body, BuffChange, ControlEvent, Controller, Pos, Scale, }, event::{EventBus, ServerEvent}, uid::UidAllocator, @@ -22,6 +22,7 @@ pub struct ReadData<'a> { server_bus: Read<'a, EventBus>, positions: ReadStorage<'a, Pos>, bodies: ReadStorage<'a, Body>, + scales: ReadStorage<'a, Scale>, } #[derive(Default)] @@ -91,13 +92,15 @@ impl<'a> System<'a> for Sys { }, ControlEvent::Respawn => server_emitter.emit(ServerEvent::Respawn(entity)), ControlEvent::Utterance(kind) => { - if let (Some(pos), Some(body)) = ( + if let (Some(pos), Some(body), scale) = ( read_data.positions.get(entity), read_data.bodies.get(entity), + read_data.scales.get(entity), ) { let sound = Sound::new( SoundKind::Utterance(kind, *body), - pos.0 + Vec3::unit_z() * body.eye_height(), + pos.0 + + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0)), 8.0, // TODO: Come up with a better way of determining this 1.0, ); diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index b08576e145..f344db95c7 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -68,13 +68,14 @@ impl<'a> System<'a> for Sys { let mut outcomes_emitter = outcomes.emitter(); // Attacks - for (attacker, uid, pos, ori, melee_attack, body) in ( + for (attacker, uid, pos, ori, melee_attack, body, scale) in ( &read_data.entities, &read_data.uids, &read_data.positions, &read_data.orientations, &mut melee_attacks, &read_data.bodies, + read_data.scales.maybe(), ) .join() { @@ -87,7 +88,7 @@ impl<'a> System<'a> for Sys { melee_attack.applied = true; // Scales - let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height(); + let eye_pos = pos.0 + Vec3::unit_z() * body.eye_height(scale.map_or(1.0, |s| s.0)); let scale = read_data.scales.get(attacker).map_or(1.0, |s| s.0); let height = body.height() * scale; // TODO: use Capsule Prisms instead of Cylinders diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 031d6bb369..a05bdb3711 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -23,7 +23,7 @@ use common::{ item_drop, projectile::ProjectileConstructor, Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, - HealthChange, InputKind, InventoryAction, Pos, UnresolvedChatMsg, UtteranceKind, + HealthChange, InputKind, InventoryAction, Pos, Scale, UnresolvedChatMsg, UtteranceKind, }, effect::{BuffEffect, Effect}, event::{Emitter, ServerEvent}, @@ -514,8 +514,10 @@ impl<'a> AgentData<'a> { target: EcsEntity, ) -> bool { if let Some(tgt_pos) = read_data.positions.get(target) { - let eye_offset = self.body.map_or(0.0, |b| b.eye_height()); - let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| b.eye_height()); + let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale)); + let tgt_eye_offset = read_data.bodies.get(target).map_or(0.0, |b| { + b.eye_height(read_data.scales.get(target).map_or(1.0, |s| s.0)) + }); if let Some(dir) = Dir::from_unnormalized( Vec3::new(tgt_pos.0.x, tgt_pos.0.y, tgt_pos.0.z + tgt_eye_offset) - Vec3::new(self.pos.0.x, self.pos.0.y, self.pos.0.z + eye_offset), @@ -794,8 +796,8 @@ impl<'a> AgentData<'a> { }, }; - let is_detected = |entity: &EcsEntity, e_pos: &Pos| { - self.detects_other(agent, controller, entity, e_pos, read_data) + let is_detected = |entity: &EcsEntity, e_pos: &Pos, e_scale: Option<&Scale>| { + self.detects_other(agent, controller, entity, e_pos, e_scale, read_data) }; let target = entities_nearby @@ -805,7 +807,7 @@ impl<'a> AgentData<'a> { .filter_map(|(entity, attack_target)| { get_pos(entity).map(|pos| (entity, pos, attack_target)) }) - .filter(|(entity, e_pos, _)| is_detected(entity, e_pos)) + .filter(|(entity, e_pos, _)| is_detected(entity, e_pos, read_data.scales.get(*entity))) .min_by_key(|(_, e_pos, attack_target)| { ( *attack_target, @@ -997,9 +999,11 @@ impl<'a> AgentData<'a> { .angle_between((tgt_data.pos.0 - self.pos.0).xy()) .to_degrees(); - let eye_offset = self.body.map_or(0.0, |b| b.eye_height()); + let eye_offset = self.body.map_or(0.0, |b| b.eye_height(self.scale)); - let tgt_eye_height = tgt_data.body.map_or(0.0, |b| b.eye_height()); + let tgt_eye_height = tgt_data + .body + .map_or(0.0, |b| b.eye_height(tgt_data.scale.map_or(1.0, |s| s.0))); let tgt_eye_offset = tgt_eye_height + // Special case for jumping attacks to jump at the body // of the target and not the ground around the target @@ -1037,7 +1041,7 @@ impl<'a> AgentData<'a> { projectile_speed, self.pos.0 + self.body.map_or(Vec3::zero(), |body| { - body.projectile_offsets(self.ori.look_vec()) + body.projectile_offsets(self.ori.look_vec(), self.scale) }), Vec3::new( tgt_data.pos.0.x, @@ -1062,7 +1066,7 @@ impl<'a> AgentData<'a> { projectile_speed, self.pos.0 + self.body.map_or(Vec3::zero(), |body| { - body.projectile_offsets(self.ori.look_vec()) + body.projectile_offsets(self.ori.look_vec(), self.scale) }), Vec3::new( tgt_data.pos.0.x, @@ -1077,7 +1081,7 @@ impl<'a> AgentData<'a> { projectile_speed, self.pos.0 + self.body.map_or(Vec3::zero(), |body| { - body.projectile_offsets(self.ori.look_vec()) + body.projectile_offsets(self.ori.look_vec(), self.scale) }), Vec3::new( tgt_data.pos.0.x, @@ -1684,6 +1688,7 @@ impl<'a> AgentData<'a> { controller: &Controller, other: EcsEntity, other_pos: &Pos, + other_scale: Option<&Scale>, read_data: &ReadData, ) -> bool { let other_stealth_multiplier = { @@ -1708,7 +1713,15 @@ impl<'a> AgentData<'a> { (within_sight_dist) && within_fov - && entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data) + && entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + other_pos, + other_body, + other_scale, + read_data, + ) } pub fn detects_other( @@ -1717,10 +1730,11 @@ impl<'a> AgentData<'a> { controller: &Controller, other: &EcsEntity, other_pos: &Pos, + other_scale: Option<&Scale>, read_data: &ReadData, ) -> bool { self.can_sense_directly_near(other_pos) - || self.can_see_entity(agent, controller, *other, other_pos, read_data) + || self.can_see_entity(agent, controller, *other, other_pos, other_scale, read_data) } pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool { diff --git a/server/agent/src/attack.rs b/server/agent/src/attack.rs index 031e134609..85e4a16010 100644 --- a/server/agent/src/attack.rs +++ b/server/agent/src/attack.rs @@ -247,7 +247,15 @@ impl<'a> AgentData<'a> { read_data: &ReadData, ) { let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; let elevation = self.pos.0.z - tgt_data.pos.0.z; @@ -374,8 +382,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -451,8 +461,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -1269,7 +1281,15 @@ impl<'a> AgentData<'a> { const DESIRED_ENERGY_LEVEL: f32 = 50.0; let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; // Logic to use abilities @@ -1521,8 +1541,10 @@ impl<'a> AgentData<'a> { if entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && attack_data.angle < 45.0 { @@ -1574,7 +1596,15 @@ impl<'a> AgentData<'a> { const DESIRED_COMBO_LEVEL: u32 = 8; let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; // Logic to use abilities @@ -1724,8 +1754,10 @@ impl<'a> AgentData<'a> { ) && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && attack_data.angle < 90.0 { @@ -1907,8 +1939,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -2130,8 +2164,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -2365,8 +2401,15 @@ impl<'a> AgentData<'a> { tgt_data: &TargetData, read_data: &ReadData, ) { - if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) - && attack_data.angle < 15.0 + if entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { @@ -2383,8 +2426,15 @@ impl<'a> AgentData<'a> { read_data: &ReadData, ) { controller.inputs.look_dir = self.ori.look_dir(); - if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) - && attack_data.angle < 15.0 + if entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) && attack_data.angle < 15.0 { controller.push_basic_input(InputKind::Primary); } else { @@ -2406,8 +2456,15 @@ impl<'a> AgentData<'a> { .try_normalized() .unwrap_or_default(), ); - if entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) - { + if entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) { controller.push_basic_input(InputKind::Primary); } else { agent.target = None; @@ -2467,8 +2524,10 @@ impl<'a> AgentData<'a> { if entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { // If close to target, use either primary or secondary ability @@ -2564,8 +2623,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && attack_data.angle < 15.0 @@ -2695,8 +2756,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && attack_data.angle < 15.0 @@ -2931,8 +2994,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -3186,7 +3251,15 @@ impl<'a> AgentData<'a> { .and_then(|e| read_data.velocities.get(e)) .map_or(0.0, |v| v.0.cross(self.ori.look_vec()).magnitude_squared()); let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; if attack_data.dist_sqrd < golem_melee_range.powi(2) { @@ -3263,7 +3336,15 @@ impl<'a> AgentData<'a> { let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; // Sets counter at start of combat, using `condition` to keep track of whether @@ -3461,7 +3542,15 @@ impl<'a> AgentData<'a> { let health_fraction = self.health.map_or(0.5, |h| h.fraction()); let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; if health_fraction < VINE_CREATION_THRESHOLD @@ -3530,7 +3619,15 @@ impl<'a> AgentData<'a> { } let line_of_sight_with_target = || { - entities_have_line_of_sight(self.pos, self.body, tgt_data.pos, tgt_data.body, read_data) + entities_have_line_of_sight( + self.pos, + self.body, + self.scale, + tgt_data.pos, + tgt_data.body, + tgt_data.scale, + read_data, + ) }; let health_fraction = self.health.map_or(0.5, |h| h.fraction()); // Sets counter at start of combat, using `condition` to keep track of whether @@ -3671,8 +3768,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -3738,8 +3837,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -3822,8 +3923,10 @@ impl<'a> AgentData<'a> { if entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && attack_data.angle < 45.0 { @@ -3901,8 +4004,10 @@ impl<'a> AgentData<'a> { } else if entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { // if enemy in mid range shoot dagon bombs and steamwave @@ -4016,8 +4121,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { @@ -4177,8 +4284,10 @@ impl<'a> AgentData<'a> { } else if entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) { // Else if in sight, barrage @@ -4241,8 +4350,10 @@ impl<'a> AgentData<'a> { && entities_have_line_of_sight( self.pos, self.body, + self.scale, tgt_data.pos, tgt_data.body, + tgt_data.scale, read_data, ) && agent.action_state.timers[DASH_TIMER] > 4.0 diff --git a/server/agent/src/util.rs b/server/agent/src/util.rs index 22ba3b8ba4..7dfe62b4e2 100644 --- a/server/agent/src/util.rs +++ b/server/agent/src/util.rs @@ -2,7 +2,7 @@ use crate::data::{ActionMode, AgentData, AttackData, Path, ReadData, TargetData} use common::{ comp::{ agent::Psyche, buff::BuffKind, inventory::item::ItemTag, item::ItemDesc, Agent, Alignment, - Body, Controller, InputKind, Pos, + Body, Controller, InputKind, Pos, Scale, }, consts::GRAVITY, terrain::Block, @@ -146,17 +146,19 @@ pub fn are_our_owners_hostile( pub fn entities_have_line_of_sight( pos: &Pos, body: Option<&Body>, + scale: f32, other_pos: &Pos, other_body: Option<&Body>, + other_scale: Option<&Scale>, read_data: &ReadData, ) -> bool { - let get_eye_pos = |pos: &Pos, body: Option<&Body>| { - let eye_offset = body.map_or(0.0, |b| b.eye_height()); + let get_eye_pos = |pos: &Pos, body: Option<&Body>, scale: f32| { + let eye_offset = body.map_or(0.0, |b| b.eye_height(scale)); Pos(pos.0.with_z(pos.0.z + eye_offset)) }; - let eye_pos = get_eye_pos(pos, body); - let other_eye_pos = get_eye_pos(other_pos, other_body); + let eye_pos = get_eye_pos(pos, body, scale); + let other_eye_pos = get_eye_pos(other_pos, other_body, other_scale.map_or(1.0, |s| s.0)); positions_have_line_of_sight(&eye_pos, &other_eye_pos, read_data) } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 4cc1f23cc4..872dd1a56c 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -286,6 +286,7 @@ impl StateExt for State { .with(poise) .with(comp::Alignment::Npc) .with(comp::CharacterState::default()) + .with(comp::CharacterActivity::default()) .with(inventory) .with(comp::Buffs::default()) .with(comp::Combo::default()) @@ -352,6 +353,7 @@ impl StateExt for State { .with(comp::Controller::default()) .with(Inventory::with_empty()) .with(comp::CharacterState::default()) + .with(comp::CharacterActivity::default()) // TODO: some of these are required in order for the character_behavior system to // recognize a possesed airship; that system should be refactored to use `.maybe()` .with(comp::Energy::new(ship.into(), 0)) @@ -559,6 +561,7 @@ impl StateExt for State { z_max: 1.75, }); self.write_component_ignore_entity_dead(entity, comp::CharacterState::default()); + self.write_component_ignore_entity_dead(entity, comp::CharacterActivity::default()); self.write_component_ignore_entity_dead(entity, comp::Alignment::Owned(player_uid)); self.write_component_ignore_entity_dead(entity, comp::Buffs::default()); self.write_component_ignore_entity_dead(entity, comp::Auras::default()); diff --git a/server/src/sys/agent/behavior_tree.rs b/server/src/sys/agent/behavior_tree.rs index 6f9a914fd5..96b2faaa19 100644 --- a/server/src/sys/agent/behavior_tree.rs +++ b/server/src/sys/agent/behavior_tree.rs @@ -139,11 +139,12 @@ impl BehaviorTree { handle_inbox_trade_accepted, handle_inbox_finished_trade, handle_inbox_update_pending_trade, + handle_timed_events, ]); Self { tree } } else { Self { - tree: vec![handle_inbox_cancel_interactions], + tree: vec![handle_inbox_cancel_interactions, handle_timed_events], } } } @@ -477,7 +478,6 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { if bdata.agent.allowed_to_speak() && let Some(target) = bdata.read_data.lookup_actor(actor) && let Some(target_pos) = bdata.read_data.positions.get(target) - && bdata.agent_data.look_toward(bdata.controller, bdata.read_data, target) { bdata.agent.target = Some(Target::new( target, @@ -486,6 +486,8 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { false, Some(target_pos.0), )); + // We're always aware of someone we're talking to + bdata.agent.awareness.set_maximally_aware(); bdata.controller.push_action(ControlAction::Talk); bdata.controller.push_utterance(UtteranceKind::Greeting); @@ -497,15 +499,20 @@ fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { .agent .timer .start(bdata.read_data.time.0, TimerAction::Interact); + true + } else { + false } }, NpcAction::Say(msg) => { bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.agent_data.chat_npc(msg, bdata.event_emitter); + false }, } + } else { + false } - false } /// Handle timed events, like looking at the player we are talking to @@ -571,7 +578,14 @@ fn update_last_known_pos(bdata: &mut BehaviorData) -> bool { let target = target_info.target; if let Some(target_pos) = read_data.positions.get(target) { - if agent_data.detects_other(agent, controller, &target, target_pos, read_data) { + if agent_data.detects_other( + agent, + controller, + &target, + target_pos, + read_data.scales.get(target), + read_data, + ) { let updated_pos = Some(target_pos.0); let Target { @@ -631,9 +645,10 @@ fn update_target_awareness(bdata: &mut BehaviorData) -> bool { let target = agent.target.map(|t| t.target); let tgt_pos = target.and_then(|t| read_data.positions.get(t)); + let tgt_scale = target.and_then(|t| read_data.scales.get(t)); if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) { - if agent_data.can_see_entity(agent, controller, target, tgt_pos, read_data) { + if agent_data.can_see_entity(agent, controller, target, tgt_pos, tgt_scale, read_data) { agent.awareness.change_by(1.75 * read_data.dt.0); } else if agent_data.can_sense_directly_near(tgt_pos) { agent.awareness.change_by(0.25); diff --git a/server/src/sys/agent/behavior_tree/interaction.rs b/server/src/sys/agent/behavior_tree/interaction.rs index 0aaf20d73f..92e0c15deb 100644 --- a/server/src/sys/agent/behavior_tree/interaction.rs +++ b/server/src/sys/agent/behavior_tree/interaction.rs @@ -97,203 +97,190 @@ pub fn handle_inbox_talk(bdata: &mut BehaviorData) -> bool { false, target_pos, )); + // We're always aware of someone we're talking to + agent.awareness.set_maximally_aware(); - if agent_data.look_toward(controller, read_data, target) { - controller.push_action(ControlAction::Stand); - controller.push_action(ControlAction::Talk); - controller.push_utterance(UtteranceKind::Greeting); + controller.push_action(ControlAction::Stand); + controller.push_action(ControlAction::Talk); + controller.push_utterance(UtteranceKind::Greeting); - match subject { - Subject::Regular => { - if let Some(tgt_stats) = read_data.stats.get(target) { - if let Some(destination_name) = &agent.rtsim_controller.heading_to { - let personality = &agent.rtsim_controller.personality; - let standard_response_msg = || -> String { - if personality.will_ambush() { + match subject { + Subject::Regular => { + if let Some(tgt_stats) = read_data.stats.get(target) { + if let Some(destination_name) = &agent.rtsim_controller.heading_to { + let personality = &agent.rtsim_controller.personality; + let standard_response_msg = || -> String { + if personality.will_ambush() { + format!( + "I'm heading to {}! Want to come along? We'll make \ + great travel buddies, hehe.", + destination_name + ) + } else if personality.is(PersonalityTrait::Extroverted) { + format!( + "I'm heading to {}! Want to come along?", + destination_name + ) + } else if personality.is(PersonalityTrait::Disagreeable) { + "Hrm.".to_string() + } else { + "Hello!".to_string() + } + }; + let msg = if false + /* TODO: Remembers character */ + { + if personality.will_ambush() { + "Just follow me a bit more, hehe.".to_string() + } else if personality.is(PersonalityTrait::Extroverted) { + if personality.is(PersonalityTrait::Extroverted) { format!( - "I'm heading to {}! Want to come along? We'll \ - make great travel buddies, hehe.", - destination_name - ) - } else if personality.is(PersonalityTrait::Extroverted) { - format!( - "I'm heading to {}! Want to come along?", - destination_name + "Greetings fair {}! It has been far too long \ + since last I saw you. I'm going to {} right now.", + &tgt_stats.name, destination_name ) } else if personality.is(PersonalityTrait::Disagreeable) { - "Hrm.".to_string() + "Oh. It's you again.".to_string() } else { - "Hello!".to_string() - } - }; - let msg = if false - /* TODO: Remembers character */ - { - if personality.will_ambush() { - "Just follow me a bit more, hehe.".to_string() - } else if personality.is(PersonalityTrait::Extroverted) { - if personality.is(PersonalityTrait::Extroverted) { - format!( - "Greetings fair {}! It has been far too long \ - since last I saw you. I'm going to {} right \ - now.", - &tgt_stats.name, destination_name - ) - } else if personality.is(PersonalityTrait::Disagreeable) - { - "Oh. It's you again.".to_string() - } else { - format!( - "Hi again {}! Unfortunately I'm in a hurry \ - right now. See you!", - &tgt_stats.name - ) - } - } else { - standard_response_msg() + format!( + "Hi again {}! Unfortunately I'm in a hurry right \ + now. See you!", + &tgt_stats.name + ) } } else { standard_response_msg() + } + } else { + standard_response_msg() + }; + agent_data.chat_npc(msg, event_emitter); + } else { + let mut rng = thread_rng(); + if let Some(extreme_trait) = + agent.rtsim_controller.personality.chat_trait(&mut rng) + { + let msg = match extreme_trait { + PersonalityTrait::Open => "npc-speech-villager_open", + PersonalityTrait::Adventurous => { + "npc-speech-villager_adventurous" + }, + PersonalityTrait::Closed => "npc-speech-villager_closed", + PersonalityTrait::Conscientious => { + "npc-speech-villager_conscientious" + }, + PersonalityTrait::Busybody => { + "npc-speech-villager_busybody" + }, + PersonalityTrait::Unconscientious => { + "npc-speech-villager_unconscientious" + }, + PersonalityTrait::Extroverted => { + "npc-speech-villager_extroverted" + }, + PersonalityTrait::Introverted => { + "npc-speech-villager_introverted" + }, + PersonalityTrait::Agreeable => { + "npc-speech-villager_agreeable" + }, + PersonalityTrait::Sociable => { + "npc-speech-villager_sociable" + }, + PersonalityTrait::Disagreeable => { + "npc-speech-villager_disagreeable" + }, + PersonalityTrait::Neurotic => { + "npc-speech-villager_neurotic" + }, + PersonalityTrait::Seeker => "npc-speech-villager_seeker", + PersonalityTrait::SadLoner => { + "npc-speech-villager_sad_loner" + }, + PersonalityTrait::Worried => "npc-speech-villager_worried", + PersonalityTrait::Stable => "npc-speech-villager_stable", }; agent_data.chat_npc(msg, event_emitter); } else { - let mut rng = thread_rng(); - if let Some(extreme_trait) = - agent.rtsim_controller.personality.chat_trait(&mut rng) - { - let msg = match extreme_trait { - PersonalityTrait::Open => "npc-speech-villager_open", - PersonalityTrait::Adventurous => { - "npc-speech-villager_adventurous" - }, - PersonalityTrait::Closed => { - "npc-speech-villager_closed" - }, - PersonalityTrait::Conscientious => { - "npc-speech-villager_conscientious" - }, - PersonalityTrait::Busybody => { - "npc-speech-villager_busybody" - }, - PersonalityTrait::Unconscientious => { - "npc-speech-villager_unconscientious" - }, - PersonalityTrait::Extroverted => { - "npc-speech-villager_extroverted" - }, - PersonalityTrait::Introverted => { - "npc-speech-villager_introverted" - }, - PersonalityTrait::Agreeable => { - "npc-speech-villager_agreeable" - }, - PersonalityTrait::Sociable => { - "npc-speech-villager_sociable" - }, - PersonalityTrait::Disagreeable => { - "npc-speech-villager_disagreeable" - }, - PersonalityTrait::Neurotic => { - "npc-speech-villager_neurotic" - }, - PersonalityTrait::Seeker => { - "npc-speech-villager_seeker" - }, - PersonalityTrait::SadLoner => { - "npc-speech-villager_sad_loner" - }, - PersonalityTrait::Worried => { - "npc-speech-villager_worried" - }, - PersonalityTrait::Stable => { - "npc-speech-villager_stable" - }, - }; - agent_data.chat_npc(msg, event_emitter); - } else { - agent_data.chat_npc("npc-speech-villager", event_emitter); - } + agent_data.chat_npc("npc-speech-villager", event_emitter); } } - }, - Subject::Trade => { - if agent.behavior.can_trade(agent_data.alignment.copied(), by) { - if !agent.behavior.is(BehaviorState::TRADING) { - controller.push_initiate_invite(by, InviteKind::Trade); - agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_advertisement", - agent, - event_emitter, - ); - } else { - agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-merchant_busy", - agent, - event_emitter, - ); - } - } else { - // TODO: maybe make some travellers willing to trade with - // simpler goods like potions + } + }, + Subject::Trade => { + if agent.behavior.can_trade(agent_data.alignment.copied(), by) { + if !agent.behavior.is(BehaviorState::TRADING) { + controller.push_initiate_invite(by, InviteKind::Trade); agent_data.chat_npc_if_allowed_to_speak( - "npc-speech-villager_decline_trade", + "npc-speech-merchant_advertisement", + agent, + event_emitter, + ); + } else { + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-merchant_busy", agent, event_emitter, ); } - }, - Subject::Mood => { - // TODO: Reimplement in rtsim2 - }, - Subject::Location(location) => { - if let Some(tgt_pos) = read_data.positions.get(target) { - let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); - let dist = Distance::from_dir(raw_dir).name(); - let dir = Direction::from_dir(raw_dir).name(); + } else { + // TODO: maybe make some travellers willing to trade with + // simpler goods like potions + agent_data.chat_npc_if_allowed_to_speak( + "npc-speech-villager_decline_trade", + agent, + event_emitter, + ); + } + }, + Subject::Mood => { + // TODO: Reimplement in rtsim2 + }, + Subject::Location(location) => { + if let Some(tgt_pos) = read_data.positions.get(target) { + let raw_dir = location.origin.as_::() - tgt_pos.0.xy(); + let dist = Distance::from_dir(raw_dir).name(); + let dir = Direction::from_dir(raw_dir).name(); - let msg = format!( - "{} ? I think it's {} {} from here!", - location.name, dist, dir - ); - agent_data.chat_npc(msg, event_emitter); - } - }, - Subject::Person(person) => { - if let Some(src_pos) = read_data.positions.get(target) { - let msg = if let Some(person_pos) = person.origin { - let distance = - Distance::from_dir(person_pos.xy() - src_pos.0.xy()); - match distance { - Distance::NextTo | Distance::Near => { - format!( - "{} ? I think he's {} {} from here!", - person.name(), - distance.name(), - Direction::from_dir( - person_pos.xy() - src_pos.0.xy(), - ) + let msg = format!( + "{} ? I think it's {} {} from here!", + location.name, dist, dir + ); + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Person(person) => { + if let Some(src_pos) = read_data.positions.get(target) { + let msg = if let Some(person_pos) = person.origin { + let distance = Distance::from_dir(person_pos.xy() - src_pos.0.xy()); + match distance { + Distance::NextTo | Distance::Near => { + format!( + "{} ? I think he's {} {} from here!", + person.name(), + distance.name(), + Direction::from_dir(person_pos.xy() - src_pos.0.xy(),) .name() - ) - }, - _ => { - format!( - "{} ? I think he's gone visiting another town. \ - Come back later!", - person.name() - ) - }, - } - } else { - format!( - "{} ? Sorry, I don't know where you can find him.", - person.name() - ) - }; - agent_data.chat_npc(msg, event_emitter); - } - }, - Subject::Work => {}, - } + ) + }, + _ => { + format!( + "{} ? I think he's gone visiting another town. Come \ + back later!", + person.name() + ) + }, + } + } else { + format!( + "{} ? Sorry, I don't know where you can find him.", + person.name() + ) + }; + agent_data.chat_npc(msg, event_emitter); + } + }, + Subject::Work => {}, } } } diff --git a/voxygen/anim/src/character/talk.rs b/voxygen/anim/src/character/talk.rs index f37de18162..74027cc41c 100644 --- a/voxygen/anim/src/character/talk.rs +++ b/voxygen/anim/src/character/talk.rs @@ -31,7 +31,8 @@ impl Animation for TalkAnimation { let slowb = (anim_time * 4.0 + PI / 2.0).sin(); let slowc = (anim_time * 12.0 + PI / 2.0).sin(); - next.head.orientation = Quaternion::rotation_x(slowc * 0.035 + look_dir.z * 0.7); + next.head.orientation = + Quaternion::rotation_x(slowc * 0.035 + look_dir.z.atan2(look_dir.xy().magnitude())); next.hand_l.position = Vec3::new( -s_a.hand.0 + 0.5 + slowb * 0.5, s_a.hand.1 + 5.0 + slowc * 1.0, diff --git a/voxygen/egui/src/lib.rs b/voxygen/egui/src/lib.rs index f83b0f59dd..25c2c699bf 100644 --- a/voxygen/egui/src/lib.rs +++ b/voxygen/egui/src/lib.rs @@ -346,8 +346,19 @@ pub fn maintain_egui_inner( ui.label("Body"); ui.label("Poise"); ui.label("Character State"); + ui.label("Character Activity"); ui.end_row(); - for (entity, body, stats, pos, _ori, vel, poise, character_state) in ( + for ( + entity, + body, + stats, + pos, + _ori, + vel, + poise, + character_state, + character_activity, + ) in ( &ecs.entities(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), @@ -356,14 +367,18 @@ pub fn maintain_egui_inner( ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ) .join() - .filter(|(_, _, _, pos, _, _, _, _)| { - client_pos.map_or(true, |client_pos| { - pos.map_or(0.0, |pos| pos.0.distance_squared(client_pos.0)) - < max_entity_distance - }) - }) + .filter( + |(_, _, _, pos, _, _, _, _, _)| { + client_pos.map_or(true, |client_pos| { + pos.map_or(0.0, |pos| { + pos.0.distance_squared(client_pos.0) + }) < max_entity_distance + }) + }, + ) { if ui.button("View").clicked() { previous_selected_entity = @@ -420,6 +435,12 @@ pub fn maintain_egui_inner( ui.label("-"); } + if let Some(character_activity) = character_activity { + ui.label(format!("{:?}", character_activity)); + } else { + ui.label("-"); + } + ui.end_row(); } }); @@ -507,11 +528,11 @@ fn selected_entity_window( buffs, auras, character_state, + character_activity, physics_state, alignment, scale, - mass, - (density, health, energy), + (mass, density, health, energy), ) in ( &ecs.entities(), ecs.read_storage::().maybe(), @@ -523,18 +544,19 @@ fn selected_entity_window( ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), - ecs.read_storage::().maybe(), ( + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ), ) .join() - .filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _))| e.id() == entity_id) + .filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _, _))| e.id() == entity_id) { let time = ecs.read_resource::