Pathing between sites.

This commit is contained in:
IsseW 2022-08-15 20:54:01 +02:00 committed by Joshua Barretto
parent feaaaa9a25
commit 9be6c7b527
11 changed files with 365 additions and 53 deletions

1
Cargo.lock generated
View File

@ -6950,6 +6950,7 @@ dependencies = [
"enum-map",
"fxhash",
"hashbrown 0.12.3",
"itertools",
"rand 0.8.5",
"rmp-serde",
"ron 0.8.0",

View File

@ -45,6 +45,15 @@ impl<T> PathResult<T> {
_ => None,
}
}
pub fn map<U>(self, f: impl FnOnce(Path<T>) -> Path<U>) -> PathResult<U> {
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)]

View File

@ -19,7 +19,7 @@ use vek::*;
#[derive(Clone, Debug)]
pub struct Path<T> {
nodes: Vec<T>,
pub nodes: Vec<T>,
}
impl<T> Default for Path<T> {

View File

@ -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"
fxhash = "0.2.1"
itertools = "0.10.3"

View File

@ -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<P, N> {
pub end: N,
pub path: VecDeque<P>,
pub repoll: bool,
}
#[derive(Clone, Default)]
pub struct PathingMemory {
pub intrasite_path: Option<(PathData<Vec2<i32>, Vec2<i32>>, Id<WorldSite>)>,
pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Npc {
// Persisted state
@ -35,6 +49,11 @@ pub struct Npc {
pub faction: Option<FactionId>,
// Unpersisted state
#[serde(skip_serializing, skip_deserializing)]
pub pathing: PathingMemory,
#[serde(skip_serializing, skip_deserializing)]
pub current_site: Option<SiteId>,
/// (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,
}

View File

@ -38,11 +38,19 @@ impl Site {
#[derive(Clone, Serialize, Deserialize)]
pub struct Sites {
pub sites: HopSlotMap<SiteId, Site>,
#[serde(skip_serializing, skip_deserializing)]
pub world_site_map: HashMap<Id<WorldSite>, 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
}
}

View File

@ -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));

View File

@ -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<i32>] = &[
Vec2::new(0, -1),
];
fn path_between(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathResult<Vec2<i32>> {
let heuristic = |tile: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(
100,
@ -51,8 +60,7 @@ fn path_between(start: Vec2<i32>, end: Vec2<i32>, 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<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
| TileKind::Gate
| TileKind::GnarlingFortification => 3.0,
};
let is_door_tile = |plot: Id<site2::Plot>, tile: Vec2<i32>| {
match site.plot(plot).kind() {
site2::PlotKind::House(house) => house.door_tile == tile,
site2::PlotKind::Workshop(_) => true,
_ => false,
}
let is_door_tile = |plot: Id<site2::Plot>, tile: Vec2<i32>| 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<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
)
}
fn path_between_sites(
start: SiteId,
end: SiteId,
sites: &Sites,
world: &World,
) -> PathResult<(Id<Track>, 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<civ::Site>| world.civs().sites.get(*site);
let end_pos = get_site(&end).center.as_::<f32>();
let heuristic = |site: &Id<civ::Site>| get_site(site).center.as_().distance(end_pos);
let mut astar = Astar::new(
100,
start,
heuristic,
BuildHasherDefault::<FxHasher64>::default(),
);
let neighbors = |site: &Id<civ::Site>| world.civs().neighbors(*site);
let track_between = |a: Id<civ::Site>, b: Id<civ::Site>| {
world
.civs()
.tracks
.get(world.civs().track_between(a, b).unwrap().0)
};
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| 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<f32>,
site: Id<WorldSite>,
index: IndexRef,
time: f64,
seed: u32,
world: &World,
) -> Option<(Vec3<f32>, f32)> {
end: impl FnOnce(&site2::Site) -> Option<Vec2<i32>>,
) -> Option<PathData<Vec2<i32>, Vec2<i32>>> {
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<T>(mut queue: VecDeque<T>) -> VecDeque<T> {
queue.pop_front();
queue
}
let wpos = site.tile_center_wpos(next_tile);
let wpos = wpos.as_::<f32>().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<Track>, 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<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|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_::<f32>().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_::<f32>().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

View File

@ -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<Self, RuleError> {
rtstate.bind::<Self, OnTick>(|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_::<i32>() / TerrainChunkSize::RECT_SIZE.as_()).and_then(|chunk| {
data.sites.world_site_map.get(chunk.sites.first()?).copied()
});
}
});

View File

@ -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",

View File

@ -628,13 +628,12 @@ impl Civs {
}
}
/// Return the direct track between two places
pub fn track_between(&self, a: Id<Site>, b: Id<Site>) -> Option<Id<Track>> {
/// Return the direct track between two places, bool if the track should be reversed or not
pub fn track_between(&self, a: Id<Site>, b: Id<Site>) -> Option<(Id<Track>, 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<Site>| self.neighbors(*p);
let transition =
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost;
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap().0).cost;
let satisfied = |p: &Id<Site>| *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<Vec2<i32>>,
}