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>, }