From 642dcf08b804b2c398a6a1c58fd452c63cbceaf5 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Sat, 27 Mar 2021 07:38:55 -0600 Subject: [PATCH 01/11] First pass at rtsim following paths --- server/src/rtsim/entity.rs | 120 ++++++++++++++++++++++++++++--------- world/src/civ/mod.rs | 4 +- 2 files changed, 94 insertions(+), 30 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index f8ecbeb977..e853918294 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -153,41 +153,103 @@ impl Entity { }); self.brain.tgt = tgt_site; - tgt_site.map(|tgt_site| { + if self.get_body().is_humanoid() && self.brain.track.is_none() && !self.brain.track_computed + { + let nearest_site = world + .civs() + .sites + .iter() + .min_by_key(|(_, site)| { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 + }) + .map(|(id, _)| id); + let track = nearest_site + .zip(tgt_site) + .and_then(|(near, tgt)| world.civs().track_between(near, tgt)); + // .map(|track_id| world.civs().tracks.get(track_id)); + self.brain.track = track; + self.brain.track_progress = 0; + self.brain.track_computed = true; + } + + if let Some(tgt_site) = tgt_site { let site = &world.civs().sites[tgt_site]; let destination_name = site .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); + if let Some(track_id) = self.brain.track { + let track = &world.civs().tracks.get(track_id); + if let Some(sim_pos) = track.path.iter().nth(self.brain.track_progress) { + let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let mut wpos = chunkpos; + if let Some(pathdata) = world.sim().get_nearest_path(chunkpos) { + wpos = pathdata.1.map(|e| e as i32); + } + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + if dist < 32 { + self.brain.track_progress += 1; + if self.brain.track_progress > track.path.len() { + self.brain.track = None; + } + } - if dist < 64 { - self.brain.tgt = None; + 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; + } else { + self.brain.track = None; + } + } else { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + + if dist < 64 { + self.brain.tgt = None; + self.brain.track_computed = false; + } + + 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; } - - 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; - }); + } // Forget old memories self.brain @@ -199,7 +261,9 @@ impl Entity { #[derive(Default)] pub struct Brain { tgt: Option>, - track: Option<(Track, usize)>, + track: Option>, + track_progress: usize, + track_computed: bool, memories: Vec, } diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index b2eb91b549..4605a7160f 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -370,7 +370,7 @@ impl Civs { } /// Return the direct track between two places - fn track_between(&self, a: Id, b: Id) -> Option> { + pub fn track_between(&self, a: Id, b: Id) -> Option> { self.track_map .get(&a) .and_then(|dests| dests.get(&b)) @@ -686,7 +686,7 @@ pub struct Track { /// arbitrary unit and doesn't make sense unless compared to other track /// costs. cost: f32, - path: Path>, + pub path: Path>, } #[derive(Debug)] From 91df480e4b48cca728c8dbd10294663c478ddefd Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Mon, 29 Mar 2021 06:55:19 -0600 Subject: [PATCH 02/11] Make rtsim entities ensure sanity of track --- server/src/rtsim/entity.rs | 182 ++++++++++++++++++++++++------------- server/src/sys/agent.rs | 10 +- 2 files changed, 122 insertions(+), 70 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index e853918294..825b6de2ca 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -35,19 +35,19 @@ impl Entity { match self.rng(PERM_GENUS).gen::() { // we want 5% airships, 45% birds, 50% humans // HUMANS TEMPORARILY DISABLED UNTIL PATHFINDING FIXED - x if x < 0.10 => comp::Body::Ship(comp::ship::Body::DefaultAirship), - _ => { + x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship), + x if x < 0.50 => { let species = *(&comp::bird_medium::ALL_SPECIES) .choose(&mut self.rng(PERM_SPECIES)) .unwrap(); comp::bird_medium::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() - // }, + _ => { + 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() + }, } } @@ -133,6 +133,18 @@ impl Entity { } pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { + // TODO: Make travellers travel smarter + // This is mainly for humanoids + // 1. If they have a track, follow that + // - if the next point is too far away: + // - if progress is 0, attempt to reverse it + // - otherwise, clear the track + // 2. If they have a target site and no track, attempt to go there directly + // 3. If there is no target site or track, check if they are currently at a + // site + // - If they are at site, calculate a new target site + // - If they aren't go to the nearest site + let mut next_pos_calculated = false; let tgt_site = self.brain.tgt.or_else(|| { world .civs() @@ -153,34 +165,50 @@ impl Entity { }); self.brain.tgt = tgt_site; - if self.get_body().is_humanoid() && self.brain.track.is_none() && !self.brain.track_computed - { - let nearest_site = world - .civs() - .sites - .iter() - .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 - }) - .map(|(id, _)| id); - let track = nearest_site - .zip(tgt_site) - .and_then(|(near, tgt)| world.civs().track_between(near, tgt)); - // .map(|track_id| world.civs().tracks.get(track_id)); - self.brain.track = track; - self.brain.track_progress = 0; - self.brain.track_computed = true; - } + if self.get_body().is_humanoid() { + let begin_site_id = self.brain.begin.or_else(|| { + world + .civs() + .sites + .iter() + .min_by_key(|(_, site)| { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 + }) + .map(|(id, _)| id) + }); + self.brain.begin = begin_site_id; - if let Some(tgt_site) = tgt_site { - let site = &world.civs().sites[tgt_site]; + if self.brain.track_computed == false { + begin_site_id + .zip(tgt_site) + .map(|(begin_site_id, tgt_site)| { + let begin_site = &world.civs().sites[begin_site_id]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - if let Some(track_id) = self.brain.track { + let begin_pos = + begin_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let begin_dist = begin_pos.map(|e| e as f32).distance(self.pos.xy()) as u32; + + if begin_dist < 64 { + let track = self + .brain + .track + .or_else(|| world.civs().track_between(begin_site_id, tgt_site)); + self.brain.track = track; + self.brain.track_progress = 0; + self.brain.track_computed = true; + } + }); + } + + if self.brain.track_computed && self.brain.track.is_some() && tgt_site.is_some() { + let track_id = self.brain.track.unwrap(); // track checked above let track = &world.civs().tracks.get(track_id); + + let site = &world.civs().sites[tgt_site.unwrap()]; // tgt_site checked above + let destination_name = site + .site_tmp + .map_or("".to_string(), |id| index.sites[id].name().to_string()); if let Some(sim_pos) = track.path.iter().nth(self.brain.track_progress) { let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); let mut wpos = chunkpos; @@ -189,11 +217,25 @@ impl Entity { } let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - if dist < 32 { + if dist < 32 && !self.brain.track_reversed { self.brain.track_progress += 1; if self.brain.track_progress > track.path.len() { self.brain.track = None; } + } else if dist < 32 && self.brain.track_reversed { + if self.brain.track_progress == 0 { + self.brain.track = None; + } else { + self.brain.track_progress -= 1; + } + } + + if self.brain.track_progress == 0 && dist > 128 { + if !self.brain.track_reversed { + self.brain.track_reversed = true; + } else { + self.brain.track = None; + } } let travel_to = self.pos.xy() @@ -216,41 +258,49 @@ impl Entity { + Vec3::new(0.5, 0.5, 0.0); self.controller.travel_to = Some((travel_to, destination_name)); self.controller.speed_factor = 0.70; - } else { - self.brain.track = None; + next_pos_calculated = true; } - } else { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - - if dist < 64 { - self.brain.tgt = None; - self.brain.track_computed = false; - } - - 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; } } + if !next_pos_calculated && tgt_site.is_some() { + let site = &world.civs().sites[tgt_site.unwrap()]; + let destination_name = site + .site_tmp + .map_or("".to_string(), |id| index.sites[id].name().to_string()); + + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + + if dist < 64 { + self.brain.tgt = None; + self.brain.begin = None; + self.brain.track_computed = false; + } + + 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; + // next_pos_calculated = true; + } + // Forget old memories self.brain .memories @@ -260,10 +310,12 @@ impl Entity { #[derive(Default)] pub struct Brain { + begin: Option>, tgt: Option>, track: Option>, track_progress: usize, track_computed: bool, + track_reversed: bool, memories: Vec, } diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index 966e7cb8fa..e1902d7728 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -912,11 +912,11 @@ impl<'a> AgentData<'a> { .brain .remembers_character(&tgt_stats.name) { - format!( - "Greetings fair {}! It has been far too \ - long since last I saw 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 { format!( "I'm heading to {}! Want to come along?", From dfad4f9a4930759702bcd83ea15803bd474f69e1 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Fri, 2 Apr 2021 20:27:30 -0600 Subject: [PATCH 03/11] Expect instead of unwrap --- server/src/rtsim/entity.rs | 13 +++++++++---- server/src/sys/agent.rs | 11 ++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 825b6de2ca..bb6f14bf7f 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -179,7 +179,7 @@ impl Entity { }); self.brain.begin = begin_site_id; - if self.brain.track_computed == false { + if !self.brain.track_computed { begin_site_id .zip(tgt_site) .map(|(begin_site_id, tgt_site)| { @@ -202,10 +202,14 @@ impl Entity { } if self.brain.track_computed && self.brain.track.is_some() && tgt_site.is_some() { - let track_id = self.brain.track.unwrap(); // track checked above + let track_id = self + .brain + .track + .expect("Track id is none after is_some check"); // track checked above let track = &world.civs().tracks.get(track_id); - let site = &world.civs().sites[tgt_site.unwrap()]; // tgt_site checked above + let site = + &world.civs().sites[tgt_site.expect("Target site is none after is_some check")]; // tgt_site checked above let destination_name = site .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); @@ -264,7 +268,8 @@ impl Entity { } if !next_pos_calculated && tgt_site.is_some() { - let site = &world.civs().sites[tgt_site.unwrap()]; + let site = + &world.civs().sites[tgt_site.expect("Target site is None after is_some check")]; let destination_name = site .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); diff --git a/server/src/sys/agent.rs b/server/src/sys/agent.rs index e1902d7728..50fa9e5089 100644 --- a/server/src/sys/agent.rs +++ b/server/src/sys/agent.rs @@ -912,11 +912,12 @@ impl<'a> AgentData<'a> { .brain .remembers_character(&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 - ) + 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 { format!( "I'm heading to {}! Want to come along?", From 3d000833a64167f674a7823168b0332723abb9ea Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Fri, 2 Apr 2021 22:43:26 -0600 Subject: [PATCH 04/11] Resolve comments --- server/src/rtsim/entity.rs | 39 ++++++++++++++++---------------------- world/src/civ/mod.rs | 6 +++++- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index bb6f14bf7f..73295ceb93 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -34,7 +34,6 @@ impl Entity { pub fn get_body(&self) -> comp::Body { match self.rng(PERM_GENUS).gen::() { // we want 5% airships, 45% birds, 50% humans - // HUMANS TEMPORARILY DISABLED UNTIL PATHFINDING FIXED x if x < 0.05 => comp::Body::Ship(comp::ship::Body::DefaultAirship), x if x < 0.50 => { let species = *(&comp::bird_medium::ALL_SPECIES) @@ -133,17 +132,6 @@ impl Entity { } pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { - // TODO: Make travellers travel smarter - // This is mainly for humanoids - // 1. If they have a track, follow that - // - if the next point is too far away: - // - if progress is 0, attempt to reverse it - // - otherwise, clear the track - // 2. If they have a target site and no track, attempt to go there directly - // 3. If there is no target site or track, check if they are currently at a - // site - // - If they are at site, calculate a new target site - // - If they aren't go to the nearest site let mut next_pos_calculated = false; let tgt_site = self.brain.tgt.or_else(|| { world @@ -213,22 +201,26 @@ impl Entity { let destination_name = site .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); - if let Some(sim_pos) = track.path.iter().nth(self.brain.track_progress) { + if let Some(sim_pos) = track.path().iter().nth(self.brain.track_progress) { let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let mut wpos = chunkpos; - if let Some(pathdata) = world.sim().get_nearest_path(chunkpos) { - wpos = pathdata.1.map(|e| e as i32); - } + 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(self.pos.xy()) as u32; - if dist < 32 && !self.brain.track_reversed { - self.brain.track_progress += 1; - if self.brain.track_progress > track.path.len() { - self.brain.track = None; + if dist < 32 { + if !self.brain.track_reversed { + self.brain.track_progress += 1; + if self.brain.track_progress > track.path().len() { + self.brain.track = None; + self.brain.track_reversed = false; + } } - } else if dist < 32 && self.brain.track_reversed { if self.brain.track_progress == 0 { self.brain.track = None; + self.brain.track_reversed = false; } else { self.brain.track_progress -= 1; } @@ -239,6 +231,7 @@ impl Entity { self.brain.track_reversed = true; } else { self.brain.track = None; + self.brain.track_reversed = false; } } @@ -301,9 +294,9 @@ impl Entity { )) .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; - // next_pos_calculated = true; } // Forget old memories diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 4605a7160f..9592360dee 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -686,7 +686,11 @@ pub struct Track { /// arbitrary unit and doesn't make sense unless compared to other track /// costs. cost: f32, - pub path: Path>, + path: Path>, +} + +impl Track { + pub fn path(&self) -> &Path> { &self.path } } #[derive(Debug)] From 3f488db523b6441939620a00e435bf9591ca7973 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Sat, 3 Apr 2021 21:54:01 -0600 Subject: [PATCH 05/11] Convert rtsim pathfinding logic to a FSM --- server/src/rtsim/entity.rs | 322 ++++++++++++++++++++++--------------- 1 file changed, 189 insertions(+), 133 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 73295ceb93..42ad0fafcf 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -132,30 +132,30 @@ impl Entity { } pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { - let mut next_pos_calculated = false; - let tgt_site = self.brain.tgt.or_else(|| { - world - .civs() - .sites - .iter() - .filter(|s| match self.get_body() { - comp::Body::Humanoid(_) => s.1.is_settlement() | s.1.is_castle(), - 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist + if dist < 96 { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - }); - self.brain.tgt = tgt_site; - - if self.get_body().is_humanoid() { - let begin_site_id = self.brain.begin.or_else(|| { - world + self.brain.route = match self.brain.route { + Travel::Lost => { + if !self.get_body().is_humanoid() { + 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist + if dist < 96 { 100_000 } else { 0 } + }) + .map(|(id, _)| id) + { + Travel::Direct { target_id } + } else { + Travel::Lost + } + } else if let Some(nearest_site_id) = world .civs() .sites .iter() @@ -164,77 +164,69 @@ impl Entity { wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 }) .map(|(id, _)| id) - }); - self.brain.begin = begin_site_id; - - if !self.brain.track_computed { - begin_site_id - .zip(tgt_site) - .map(|(begin_site_id, tgt_site)| { - let begin_site = &world.civs().sites[begin_site_id]; - - let begin_pos = - begin_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let begin_dist = begin_pos.map(|e| e as f32).distance(self.pos.xy()) as u32; - - if begin_dist < 64 { - let track = self - .brain - .track - .or_else(|| world.civs().track_between(begin_site_id, tgt_site)); - self.brain.track = track; - self.brain.track_progress = 0; - self.brain.track_computed = true; + { + let nearest_site = &world.civs().sites[nearest_site_id]; + let site_wpos = nearest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = site_wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + if dist < 32 { + Travel::InSite { + site_id: nearest_site_id, } - }); - } - - if self.brain.track_computed && self.brain.track.is_some() && tgt_site.is_some() { - let track_id = self - .brain - .track - .expect("Track id is none after is_some check"); // track checked above - let track = &world.civs().tracks.get(track_id); - - let site = - &world.civs().sites[tgt_site.expect("Target site is none after is_some check")]; // tgt_site checked above + } 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 + 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() + .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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist + if dist < 96 { 100_000 } else { 0 } + }) + .map(|(id, _)| id) + { + if let Some(track_id) = world.civs().track_between(site_id, target_id) { + Travel::Path { + target_id, + track_id, + progress: 0, + reversed: false, + } + } else { + Travel::Direct { target_id } + } + } else { + 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()); - if let Some(sim_pos) = track.path().iter().nth(self.brain.track_progress) { - let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); - 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(self.pos.xy()) as u32; - if dist < 32 { - if !self.brain.track_reversed { - self.brain.track_progress += 1; - if self.brain.track_progress > track.path().len() { - self.brain.track = None; - self.brain.track_reversed = false; - } - } - if self.brain.track_progress == 0 { - self.brain.track = None; - self.brain.track_reversed = false; - } else { - self.brain.track_progress -= 1; - } - } - - if self.brain.track_progress == 0 && dist > 128 { - if !self.brain.track_reversed { - self.brain.track_reversed = true; - } else { - self.brain.track = None; - self.brain.track_reversed = false; - } - } + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + if dist < 64 { + 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()) @@ -253,51 +245,98 @@ impl Entity { )) .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; - next_pos_calculated = true; + 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 + } else { + progress + }; - if !next_pos_calculated && tgt_site.is_some() { - let site = - &world.civs().sites[tgt_site.expect("Target site is None after is_some check")]; - let destination_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); + if let Some(sim_pos) = track.path().iter().nth(nth) { + let chunkpos = sim_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32); + 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(self.pos.xy()) as u32; - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - - if dist < 64 { - self.brain.tgt = None; - self.brain.begin = None; - self.brain.track_computed = false; - } - - 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; - } + match dist { + d if d < 32 => { + if progress + 1 > track.path().len() { + Travel::InSite { site_id: target_id } + } else { + Travel::Path { + target_id, + track_id, + progress: progress + 1, + reversed, + } + } + }, + d if d > 256 => { + if !reversed && progress == 0 { + Travel::Path { + target_id, + track_id, + progress: 0, + reversed: true, + } + } else { + Travel::Direct { target_id } + } + }, + _ => { + 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 { + Travel::Lost + } + }, + }; // Forget old memories self.brain @@ -306,14 +345,31 @@ impl Entity { } } +enum Travel { + Lost, + InSite { + site_id: Id, + }, + Direct { + target_id: Id, + }, + Path { + target_id: Id, + track_id: Id, + progress: usize, + reversed: bool, + }, +} + +impl Default for Travel { + fn default() -> Self { Self::Lost } +} + #[derive(Default)] pub struct Brain { begin: Option>, tgt: Option>, - track: Option>, - track_progress: usize, - track_computed: bool, - track_reversed: bool, + route: Travel, memories: Vec, } From 275296b15e822fddef47a81a855fe190bbaf7c54 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Sun, 4 Apr 2021 04:55:15 -0600 Subject: [PATCH 06/11] Add CustomPath to Travel enum Defines a custom path to follow, instead of using one from the World. Airships use this to slightly adjust their course to reduce collisions. --- Cargo.lock | 20 +++- server/Cargo.toml | 1 + server/src/rtsim/entity.rs | 207 ++++++++++++++++++++++++++++--------- 3 files changed, 180 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aea01c2083..1c98e40f9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2650,6 +2650,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + [[package]] name = "libsqlite3-sys" version = "0.18.0" @@ -3345,6 +3351,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", + "libm 0.2.1", ] [[package]] @@ -3587,7 +3594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3278e0492f961fd4ae70909f56b2723a7e8d01a228427294e19cdfdebda89a17" dependencies = [ "cfg-if 0.1.10", - "libm", + "libm 0.1.4", ] [[package]] @@ -3965,6 +3972,16 @@ dependencies = [ "getrandom 0.2.2", ] +[[package]] +name = "rand_distr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9e8f32ad24fb80d07d2323a9a2ce8b30d68a62b8cb4df88119ff49a698f038" +dependencies = [ + "num-traits", + "rand 0.8.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -5574,6 +5591,7 @@ dependencies = [ "prometheus", "prometheus-hyper", "rand 0.8.3", + "rand_distr", "rayon", "ron", "scan_fmt", diff --git a/server/Cargo.toml b/server/Cargo.toml index 3866cc5b55..d3e05894b8 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -49,6 +49,7 @@ diesel = { version = "1.4.3", features = ["sqlite"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" slab = "0.4" +rand_distr = "0.4.0" # Plugins plugin-api = { package = "veloren-plugin-api", path = "../plugin/api"} diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 42ad0fafcf..2c7e3bb202 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -6,6 +6,8 @@ use common::{ store::Id, terrain::TerrainGrid, }; +use rand_distr::{Distribution, Normal}; +use std::f32::consts::PI; use world::{ civ::{Site, Track}, util::RandomPerm, @@ -132,56 +134,107 @@ impl Entity { } pub fn tick(&mut self, time: &Time, terrain: &TerrainGrid, world: &World, index: &IndexRef) { - self.brain.route = match self.brain.route { + self.brain.route = match self.brain.route.clone() { Travel::Lost => { - if !self.get_body().is_humanoid() { - 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist + if dist < 96 { 100_000 } else { 0 } - }) - .map(|(id, _)| id) - { - Travel::Direct { target_id } - } else { - Travel::Lost - } - } else if let Some(nearest_site_id) = world - .civs() - .sites - .iter() - .min_by_key(|(_, site)| { - let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 - }) - .map(|(id, _)| id) - { - let nearest_site = &world.civs().sites[nearest_site_id]; - let site_wpos = nearest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = site_wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - if dist < 32 { - Travel::InSite { - site_id: nearest_site_id, + match self.get_body() { + comp::Body::Humanoid(_) => { + if let Some(nearest_site_id) = world + .civs() + .sites + .iter() + .min_by_key(|(_, site)| { + let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 + }) + .map(|(id, _)| id) + { + let nearest_site = &world.civs().sites[nearest_site_id]; + let site_wpos = + nearest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = site_wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + if dist < 32 { + 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 + Travel::Lost } - } else { - Travel::Direct { - target_id: nearest_site_id, + }, + 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist + if dist < 96 { 100_000 } else { 0 } + }) + { + let mut rng = thread_rng(); + if let (Ok(normalpos), Ok(normalrad)) = + (Normal::new(0.0, 64.0), (Normal::new(0.0, PI / 8.0))) + { + let mut path = Vec::>::default(); + let target_site_pos = site.center.map(|e| e as f32) + * TerrainChunk::RECT_SIZE.map(|e| e as f32); + let offset_site_pos = + target_site_pos.map(|v| v + normalpos.sample(&mut rng)); + let dir_vec = (offset_site_pos - self.pos.xy()).normalized(); + let dist = (offset_site_pos - self.pos.xy()).magnitude(); + let inbetween_dir = dir_vec.rotated_z(normalrad.sample(&mut rng)); + let inbetween_pos = self.pos.xy() + (inbetween_dir * (dist / 2.0)); + + 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 } - } - } else { - // Somehow no nearest site could be found - // Logically this should never happen, but if it does the rtsim entity will just - // sit tight - 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist + if dist < 96 { 100_000 } else { 0 } + }) + .map(|(id, _)| id) + { + Travel::Direct { target_id } + } else { + Travel::Lost + } + }, } }, Travel::InSite { site_id } => { @@ -251,6 +304,60 @@ impl Entity { 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(self.pos.xy()) as u32; + if dist < 64 { + 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, @@ -345,6 +452,7 @@ impl Entity { } } +#[derive(Clone)] enum Travel { Lost, InSite { @@ -353,6 +461,11 @@ enum Travel { Direct { target_id: Id, }, + CustomPath { + target_id: Id, + path: Vec>, + progress: usize, + }, Path { target_id: Id, track_id: Id, From 1b3c52aeccd854e95a3ecccc59a49895da947ff5 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Sun, 4 Apr 2021 07:40:09 -0600 Subject: [PATCH 07/11] Refine airship pathing --- server/src/rtsim/entity.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 2c7e3bb202..cfdafd362b 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -185,18 +185,18 @@ impl Entity { }) { let mut rng = thread_rng(); - if let (Ok(normalpos), Ok(normalrad)) = - (Normal::new(0.0, 64.0), (Normal::new(0.0, PI / 8.0))) - { + if let Ok(normalpos) = Normal::new(0.0, 64.0) { let mut path = Vec::>::default(); let target_site_pos = site.center.map(|e| e as f32) * TerrainChunk::RECT_SIZE.map(|e| e as f32); let offset_site_pos = target_site_pos.map(|v| v + normalpos.sample(&mut rng)); - let dir_vec = (offset_site_pos - self.pos.xy()).normalized(); + let offset_dir = (offset_site_pos - self.pos.xy()).normalized(); let dist = (offset_site_pos - self.pos.xy()).magnitude(); - let inbetween_dir = dir_vec.rotated_z(normalrad.sample(&mut rng)); - let inbetween_pos = self.pos.xy() + (inbetween_dir * (dist / 2.0)); + let midpoint = self.pos.xy() + offset_dir * (dist / 2.0); + let perp_dir = offset_dir.rotated_z(PI / 2.0); + let offset = normalpos.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)); From ee302607f0129ae3050defb571e3894fa5fdc588 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Tue, 6 Apr 2021 07:39:10 -0600 Subject: [PATCH 08/11] Tries to implement npcs only travelling by paths Unfortunately, there's a degenerate case where npcs can get stuck in a town. Not sure why --- server/src/rtsim/entity.rs | 66 ++++++++++++++++++++++++++++++-------- world/src/civ/mod.rs | 2 +- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index cfdafd362b..420798f0c6 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -185,7 +185,9 @@ impl Entity { }) { let mut rng = thread_rng(); - if let Ok(normalpos) = Normal::new(0.0, 64.0) { + 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.map(|e| e as f32) * TerrainChunk::RECT_SIZE.map(|e| e as f32); @@ -195,7 +197,7 @@ impl Entity { 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 = normalpos.sample(&mut rng); + let offset = normaloff.sample(&mut rng); let inbetween_pos = midpoint + (perp_dir * offset); path.push(inbetween_pos.map(|e| e as i32)); @@ -238,9 +240,50 @@ impl Entity { } }, Travel::InSite { site_id } => { + let site = &world.civs().sites[site_id]; + let site_name = site + .site_tmp + .map_or("".to_string(), |id| index.sites[id].name().to_string()); + let last_name = self + .brain + .last_visited + .and_then(|last| site.site_tmp.map(|id| index.sites[id].name().to_string())) + .or_else(|| Some("None".to_string())); + println!("In: {:?}, prev: {:?}", site_name, last_name); 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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); + let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + dist > 96 + }) + .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 { + self.brain.last_visited = Some(site_id); + // This should never trigger + Travel::Direct { target_id } + } } else if let Some(target_id) = world .civs() .sites @@ -254,17 +297,11 @@ impl Entity { }) .map(|(id, _)| id) { - if let Some(track_id) = world.civs().track_between(site_id, target_id) { - Travel::Path { - target_id, - track_id, - progress: 0, - reversed: false, - } - } else { - Travel::Direct { target_id } - } + self.brain.last_visited = Some(site_id); + // Directly travel if no paths exist + Travel::Direct { target_id } } else { + self.brain.last_visited = Some(site_id); Travel::InSite { site_id } } }, @@ -387,7 +424,7 @@ impl Entity { match dist { d if d < 32 => { if progress + 1 > track.path().len() { - Travel::InSite { site_id: target_id } + Travel::Direct { target_id } } else { Travel::Path { target_id, @@ -406,7 +443,7 @@ impl Entity { reversed: true, } } else { - Travel::Direct { target_id } + Travel::Lost } }, _ => { @@ -483,6 +520,7 @@ pub struct Brain { begin: Option>, tgt: Option>, route: Travel, + last_visited: Option>, memories: Vec, } diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 9592360dee..399f1ee630 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -379,7 +379,7 @@ impl Civs { } /// Return an iterator over a site's neighbors - fn neighbors(&self, site: Id) -> impl Iterator> + '_ { + pub fn neighbors(&self, site: Id) -> impl Iterator> + '_ { let to = self .track_map .get(&site) From 3bcdd06c876fcf9280df497133296736a9fffed9 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Thu, 8 Apr 2021 04:07:25 -0600 Subject: [PATCH 09/11] Fix travellers stuck in town There was an off by one error in the code that negates the progress along paths that are reversed. --- CHANGELOG.md | 1 + server/src/rtsim/entity.rs | 42 ++++++++++++++++++++++---------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ca66034c..a66c301b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Auto camera setting, making the game easier to play with one hand - Topographic map option - Search bars for social and crafting window +- RTsim travellers now follow paths between towns ### Changed diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 420798f0c6..b892683b46 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -8,6 +8,7 @@ use common::{ }; use rand_distr::{Distribution, Normal}; use std::f32::consts::PI; +use tracing::warn; use world::{ civ::{Site, Track}, util::RandomPerm, @@ -142,17 +143,21 @@ impl Entity { .civs() .sites .iter() + .filter(|s| s.1.is_settlement() || s.1.is_castle()) .min_by_key(|(_, site)| { let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); wpos.map(|e| e as f32).distance(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 * TerrainChunk::RECT_SIZE.map(|e| e as i32); let dist = site_wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - if dist < 32 { + if dist < 64 { Travel::InSite { site_id: nearest_site_id, } @@ -165,6 +170,7 @@ impl Entity { // 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 } }, @@ -240,16 +246,6 @@ impl Entity { } }, Travel::InSite { site_id } => { - let site = &world.civs().sites[site_id]; - let site_name = site - .site_tmp - .map_or("".to_string(), |id| index.sites[id].name().to_string()); - let last_name = self - .brain - .last_visited - .and_then(|last| site.site_tmp.map(|id| index.sites[id].name().to_string())) - .or_else(|| Some("None".to_string())); - println!("In: {:?}, prev: {:?}", site_name, last_name); if !self.get_body().is_humanoid() { // Non humanoids don't care if they start at a site Travel::Lost @@ -280,8 +276,10 @@ impl Entity { 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); - // This should never trigger Travel::Direct { target_id } } } else if let Some(target_id) = world @@ -297,10 +295,13 @@ impl Entity { }) .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); - // Directly travel if no paths exist 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 } } @@ -353,7 +354,7 @@ impl Entity { if let Some(wpos) = &path.get(progress) { let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - if dist < 64 { + if dist < 16 { if progress + 1 < path.len() { Travel::CustomPath { target_id, @@ -407,7 +408,7 @@ impl Entity { .site_tmp .map_or("".to_string(), |id| index.sites[id].name().to_string()); let nth = if reversed { - track.path().len() - progress + track.path().len() - progress - 1 } else { progress }; @@ -422,8 +423,8 @@ impl Entity { let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; match dist { - d if d < 32 => { - if progress + 1 > track.path().len() { + d if d < 16 => { + if progress + 1 >= track.path().len() { Travel::Direct { target_id } } else { Travel::Path { @@ -477,6 +478,11 @@ impl Entity { }, } } 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 } }, @@ -489,7 +495,7 @@ impl Entity { } } -#[derive(Clone)] +#[derive(Clone, Debug)] enum Travel { Lost, InSite { From a7c1a97ddaf900d896a03d93c7251072890b70f6 Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Thu, 8 Apr 2021 05:10:22 -0600 Subject: [PATCH 10/11] Use distance_squared to speed up calculations --- server/src/rtsim/entity.rs | 39 ++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index b892683b46..836a503691 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -146,7 +146,7 @@ impl Entity { .filter(|s| s.1.is_settlement() || s.1.is_castle()) .min_by_key(|(_, site)| { let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - wpos.map(|e| e as f32).distance(self.pos.xy()) as u32 + wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32 }) .map(|(id, _)| id) { @@ -156,8 +156,9 @@ impl Entity { let nearest_site = &world.civs().sites[nearest_site_id]; let site_wpos = nearest_site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = site_wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - if dist < 64 { + 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, } @@ -186,8 +187,9 @@ impl Entity { .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist + if dist < 96 { 100_000 } else { 0 } + 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(); @@ -233,8 +235,9 @@ impl Entity { .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist + if dist < 96 { 100_000 } else { 0 } + 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) { @@ -255,8 +258,8 @@ impl Entity { .filter(|sid| { let site = world.civs().sites.get(*sid); let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist > 96 + 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 { @@ -290,8 +293,8 @@ impl Entity { .filter(|_| thread_rng().gen_range(0i32..4) == 0) .min_by_key(|(_, site)| { let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; - dist + if dist < 96 { 100_000 } else { 0 } + 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) { @@ -313,9 +316,9 @@ impl Entity { .map_or("".to_string(), |id| index.sites[id].name().to_string()); let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; - if dist < 64 { + if dist < 64_u32.pow(2) { Travel::InSite { site_id: target_id } } else { let travel_to = self.pos.xy() @@ -353,8 +356,8 @@ impl Entity { .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(self.pos.xy()) as u32; - if dist < 16 { + 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, @@ -420,10 +423,10 @@ impl Entity { } else { chunkpos }; - let dist = wpos.map(|e| e as f32).distance(self.pos.xy()) as u32; + let dist = wpos.map(|e| e as f32).distance_squared(self.pos.xy()) as u32; match dist { - d if d < 16 => { + d if d < 16_u32.pow(2) => { if progress + 1 >= track.path().len() { Travel::Direct { target_id } } else { @@ -435,7 +438,7 @@ impl Entity { } } }, - d if d > 256 => { + d if d > 256_u32.pow(2) => { if !reversed && progress == 0 { Travel::Path { target_id, From b80bc61eb8e9019adfc9c18d9f2f4646b1dfcf7c Mon Sep 17 00:00:00 2001 From: Louis Pearson Date: Thu, 8 Apr 2021 05:40:56 -0600 Subject: [PATCH 11/11] Document Travel enum --- server/src/rtsim/entity.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/rtsim/entity.rs b/server/src/rtsim/entity.rs index 836a503691..8110498fd7 100644 --- a/server/src/rtsim/entity.rs +++ b/server/src/rtsim/entity.rs @@ -500,18 +500,30 @@ impl Entity { #[derive(Clone, Debug)] 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,