Implement async job pathfinding system
- Add calculate_traversal_distance() - full A* that returns actual distance - Add batch_calculate_traversals() for efficient batch processing - Add Calculating state to JobState enum for dorf locking - Add approach_target and retry_count to job Entry - Add helper methods: is_dorf_locked, get_locked_dorfs, get_scope_for_job - Add job_assignment config section with max_dorfs_per_job - Create job_pathfinding.rs with main pathfinding system: - Throttled to MAX_JOBS_PER_TICK (5) per frame - Expanding scope: 5 -> 20 -> 200 dorfs based on retry count - Uses full pathfinding, not provisional - Pre-computes approach_target for assigned dorfs - Simplify job_assignment.rs to only handle idle fallback - Update has_fell_tree to check state (Unclaimed, Calculating, Claimed)
This commit is contained in:
@@ -7,3 +7,6 @@ vsync = "mailbox"
|
|||||||
dorfs = 5
|
dorfs = 5
|
||||||
pigs = 0
|
pigs = 0
|
||||||
rabbits = 0
|
rabbits = 0
|
||||||
|
|
||||||
|
[job_assignment]
|
||||||
|
max_dorfs_per_job = 5
|
||||||
@@ -10,6 +10,8 @@ pub struct GameConfig {
|
|||||||
pub spawn_counts: SpawnCounts,
|
pub spawn_counts: SpawnCounts,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub display: DisplaySettings,
|
pub display: DisplaySettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub job_assignment: JobAssignmentSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone, Default)]
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
@@ -26,6 +28,16 @@ pub enum VsyncMode {
|
|||||||
Uncapped,
|
Uncapped,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone, Default)]
|
||||||
|
pub struct JobAssignmentSettings {
|
||||||
|
#[serde(default = "default_max_dorfs")]
|
||||||
|
pub max_dorfs_per_job: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_max_dorfs() -> u32 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for VsyncMode {
|
impl<'de> Deserialize<'de> for VsyncMode {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ use crate::constants::{
|
|||||||
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
||||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||||
};
|
};
|
||||||
|
use crate::entities::tasks::job_queue::JobId;
|
||||||
|
|
||||||
use crate::entities::item::inventory::{update_encumbrance, InventoryChangedEvent};
|
use crate::entities::item::inventory::{update_encumbrance, InventoryChangedEvent};
|
||||||
use crate::entities::shared_systems::constants::{
|
use crate::entities::shared_systems::constants::{
|
||||||
@@ -1620,3 +1621,116 @@ fn write_benchmark_csv(bench: &PathfindingBenchmark, filename: &str) -> std::io:
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calculate traversal distance between two points using full A*.
|
||||||
|
/// Returns Ok(distance_in_tiles) or Err(()) if unreachable.
|
||||||
|
/// This is different from provisional pathfinding - it actually searches until it finds the goal.
|
||||||
|
/// Uses PATHFINDER_MAX_NODES (15,000) as the limit.
|
||||||
|
pub fn calculate_traversal_distance(
|
||||||
|
tilemap: &TileMap,
|
||||||
|
start: IVec3,
|
||||||
|
goal: IVec3,
|
||||||
|
) -> Result<i32, ()> {
|
||||||
|
use std::collections::BinaryHeap;
|
||||||
|
|
||||||
|
// Validate start position
|
||||||
|
if !is_standable_tile(tilemap, start) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If goal is also the start, distance is 0
|
||||||
|
if start == goal {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let node_limit = PATHFINDER_MAX_NODES;
|
||||||
|
let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||||||
|
|
||||||
|
SCRATCHPAD.with(|s| {
|
||||||
|
let mut scratch = s.borrow_mut();
|
||||||
|
let capacity = ((estimated_tiles as usize).max(64)).min(16384);
|
||||||
|
scratch.clear_and_reserve(capacity);
|
||||||
|
|
||||||
|
let initial_h = octile_distance_3d(start, goal);
|
||||||
|
scratch.open_set.push(PathNode {
|
||||||
|
position: start,
|
||||||
|
f_score: initial_h,
|
||||||
|
g_score: 0,
|
||||||
|
});
|
||||||
|
scratch.g_scores.insert(start, 0);
|
||||||
|
|
||||||
|
let mut nodes_expanded: usize = 0;
|
||||||
|
|
||||||
|
while let Some(current_node) = scratch.open_set.pop() {
|
||||||
|
let current = current_node.position;
|
||||||
|
nodes_expanded += 1;
|
||||||
|
|
||||||
|
// Found the goal!
|
||||||
|
if current == goal {
|
||||||
|
let distance = *scratch.g_scores.get(&goal).unwrap_or(&0);
|
||||||
|
return Ok(distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes_expanded >= node_limit {
|
||||||
|
// Hit node limit - path may exist but we couldn't find it
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch.closed_set.insert(current);
|
||||||
|
|
||||||
|
for &move_dir in &ALLOWED_MOVES {
|
||||||
|
let neighbor_pos = current + move_dir;
|
||||||
|
|
||||||
|
if !is_standable_tile(tilemap, neighbor_pos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if scratch.closed_set.contains(&neighbor_pos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tile_weight = get_tile_weight(tilemap, neighbor_pos);
|
||||||
|
let movement_cost = calculate_movement_cost(move_dir, tile_weight);
|
||||||
|
if movement_cost == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX) + movement_cost;
|
||||||
|
|
||||||
|
if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||||||
|
scratch.came_from.insert(neighbor_pos, current);
|
||||||
|
scratch.g_scores.insert(neighbor_pos, new_g);
|
||||||
|
let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||||||
|
scratch.open_set.push(PathNode {
|
||||||
|
position: neighbor_pos,
|
||||||
|
f_score: f,
|
||||||
|
g_score: new_g,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No path found
|
||||||
|
Err(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate traversal distances for multiple dorf-to-target pairs.
|
||||||
|
/// Takes: Vec of (job_id, dorf_entity, dorf_pos, target_pos)
|
||||||
|
/// Returns: Vec of (job_id, dorf_entity, distance) for reachable pairs
|
||||||
|
///
|
||||||
|
/// Optimization: When batch calculating, if current g_score exceeds the shortest
|
||||||
|
/// distance found so far, terminate early (passed best_distance must be Some).
|
||||||
|
pub fn batch_calculate_traversals(
|
||||||
|
tilemap: &TileMap,
|
||||||
|
requests: Vec<(JobId, Entity, IVec3, IVec3)>,
|
||||||
|
) -> Vec<(JobId, Entity, i32)> {
|
||||||
|
let mut results = Vec::with_capacity(requests.len());
|
||||||
|
|
||||||
|
for (job_id, dorf_entity, dorf_pos, target_pos) in requests {
|
||||||
|
if let Ok(distance) = calculate_traversal_distance(tilemap, dorf_pos, target_pos) {
|
||||||
|
results.push((job_id, dorf_entity, distance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,134 +1,13 @@
|
|||||||
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||||
use crate::entities::cargo::HaulSlot;
|
use crate::entities::tasks::components::{TaskQueue, TaskState};
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::tasks::job_queue::JobId;
|
||||||
use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
|
use crate::entities::tasks::jobs::IdleJob;
|
||||||
use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue};
|
|
||||||
use crate::entities::tasks::jobs::{FellTreeJob, HaulCargoJob, IdleJob};
|
|
||||||
use crate::world::tiles::TileMap;
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
pub fn job_assignment_system(
|
pub fn job_assignment_system(
|
||||||
mut job_queue: ResMut<JobQueue>,
|
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform), With<EntityType>>,
|
||||||
tilemap: Res<TileMap>,
|
|
||||||
mut dorf_query: Query<
|
|
||||||
(
|
|
||||||
Entity,
|
|
||||||
&mut TaskQueue,
|
|
||||||
&mut TaskState,
|
|
||||||
&Transform,
|
|
||||||
&HaulSlot,
|
|
||||||
&mut Ambulatory,
|
|
||||||
),
|
|
||||||
With<EntityType>,
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
let idle_dorfs: Vec<(Entity, IVec3)> = dorf_query
|
for (dorf_entity, mut queue, mut state, transform) in dorf_query.iter_mut() {
|
||||||
.iter_mut()
|
|
||||||
.filter(|(entity, queue, _state, transform, haul_slot, _)| {
|
|
||||||
let pos = transform.translation.as_ivec3();
|
|
||||||
let pos_standable = tilemap.is_standable(pos);
|
|
||||||
let current_task = queue.current().map(|t| t.name()).unwrap_or("EMPTY");
|
|
||||||
let is_idle = queue.is_empty() || matches!(queue.current(), Some(Task::Idle { .. }));
|
|
||||||
let no_cargo = haul_slot.is_empty();
|
|
||||||
let should_assign = is_idle && no_cargo;
|
|
||||||
if !should_assign {
|
|
||||||
info!("[ASSIGN] Filter out dorf {:?}: pos_standable={} current={} is_idle={} no_cargo={}",
|
|
||||||
entity, pos_standable, current_task, is_idle, no_cargo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !pos_standable {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
is_idle && no_cargo
|
|
||||||
})
|
|
||||||
.map(|(entity, _, _, transform, _, _)| (entity, transform.translation.as_ivec3()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if idle_dorfs.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sorted_jobs: Vec<(usize, JobKind)> = job_queue
|
|
||||||
.iter_unclaimed()
|
|
||||||
.map(|(idx, kind)| (idx, kind.clone()))
|
|
||||||
.collect();
|
|
||||||
sorted_jobs.sort_by(|a, b| b.1.priority().cmp(&a.1.priority()));
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"[ASSIGN] idle_dorfs={} unclaimed_jobs={}",
|
|
||||||
idle_dorfs.len(),
|
|
||||||
sorted_jobs.len()
|
|
||||||
);
|
|
||||||
|
|
||||||
if sorted_jobs.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut assigned_dorfs: HashSet<Entity> = HashSet::new();
|
|
||||||
|
|
||||||
for (job_idx, job_kind) in sorted_jobs {
|
|
||||||
let target_pos = job_kind.target();
|
|
||||||
|
|
||||||
let mut remaining: Vec<_> = idle_dorfs
|
|
||||||
.iter()
|
|
||||||
.filter(|(e, _)| !assigned_dorfs.contains(e))
|
|
||||||
.collect();
|
|
||||||
remaining.sort_by_key(|(_, pos)| {
|
|
||||||
(pos.x - target_pos.x).abs()
|
|
||||||
+ (pos.y - target_pos.y).abs()
|
|
||||||
+ (pos.z - target_pos.z).abs()
|
|
||||||
});
|
|
||||||
|
|
||||||
for (dorf_entity, dorf_pos) in remaining {
|
|
||||||
if let Some((job_id, _)) =
|
|
||||||
job_queue.claim_job_at(job_idx, *dorf_entity, &tilemap, *dorf_pos)
|
|
||||||
{
|
|
||||||
if let Ok((_, mut queue, mut state, _, _, mut ambulatory)) =
|
|
||||||
dorf_query.get_mut(*dorf_entity)
|
|
||||||
{
|
|
||||||
let task = match &job_kind {
|
|
||||||
JobKind::FellTree { trunk_pos } => Task::ChopTree {
|
|
||||||
job_id,
|
|
||||||
trunk_pos: *trunk_pos,
|
|
||||||
chop_ticks: 120,
|
|
||||||
step: crate::entities::tasks::components::ChopStep::MovingToTree {
|
|
||||||
approach: None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
JobKind::HaulCargo {
|
|
||||||
cargo_entity,
|
|
||||||
cargo_pos,
|
|
||||||
dest,
|
|
||||||
} => Task::HaulCargo {
|
|
||||||
job_id,
|
|
||||||
cargo_entity: *cargo_entity,
|
|
||||||
cargo_pos: *cargo_pos,
|
|
||||||
dest: *dest,
|
|
||||||
step: crate::entities::tasks::components::HaulStep::MovingToCargo {
|
|
||||||
approach: None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let task_name = task.name();
|
|
||||||
queue.clear();
|
|
||||||
queue.push(task);
|
|
||||||
*state = TaskState::Pending;
|
|
||||||
info!(
|
|
||||||
"[ASSIGN] Assigned {:?} to dorf {:?}: {} -> Pending",
|
|
||||||
*dorf_entity, dorf_pos, task_name
|
|
||||||
);
|
|
||||||
ambulatory.current_path = None;
|
|
||||||
ambulatory.target = None;
|
|
||||||
ambulatory.path_index = 0;
|
|
||||||
assigned_dorfs.insert(*dorf_entity);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (dorf_entity, mut queue, mut state, transform, _, _) in dorf_query.iter_mut() {
|
|
||||||
if queue.is_empty() && !matches!(*state, TaskState::Active) {
|
if queue.is_empty() && !matches!(*state, TaskState::Active) {
|
||||||
let behaviour = EntityBehaviourRegistry::global_get("dorf");
|
let behaviour = EntityBehaviourRegistry::global_get("dorf");
|
||||||
let origin = transform.translation.as_ivec3();
|
let origin = transform.translation.as_ivec3();
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
use bevy::prelude::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::config::GameConfig;
|
||||||
|
use crate::entities::behaviour::EntityType;
|
||||||
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
use crate::entities::shared_systems::pathfinding::calculate_traversal_distance;
|
||||||
|
use crate::entities::tasks::components::{ChopStep, HaulStep, Task, TaskQueue, TaskState};
|
||||||
|
use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue, JobState};
|
||||||
|
use crate::world::tiles::TileMap;
|
||||||
|
|
||||||
|
const MAX_JOBS_PER_TICK: usize = 5;
|
||||||
|
|
||||||
|
pub fn job_pathfinding_system(
|
||||||
|
mut job_queue: ResMut<JobQueue>,
|
||||||
|
config: Res<GameConfig>,
|
||||||
|
tilemap: Res<TileMap>,
|
||||||
|
mut dorf_query: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&mut TaskQueue,
|
||||||
|
&mut TaskState,
|
||||||
|
&Transform,
|
||||||
|
&mut Ambulatory,
|
||||||
|
),
|
||||||
|
With<EntityType>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
let mut unclaimed: Vec<usize> = job_queue.iter_unclaimed().map(|(idx, _)| idx).collect();
|
||||||
|
|
||||||
|
if unclaimed.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unclaimed.sort_by(|&a, &b| {
|
||||||
|
let pri_a = job_queue.get_job_priority(a).unwrap_or(0);
|
||||||
|
let pri_b = job_queue.get_job_priority(b).unwrap_or(0);
|
||||||
|
pri_b.cmp(&pri_a)
|
||||||
|
});
|
||||||
|
|
||||||
|
let locked_dorfs = job_queue.get_locked_dorfs();
|
||||||
|
|
||||||
|
for job_idx in unclaimed.into_iter().take(MAX_JOBS_PER_TICK) {
|
||||||
|
let (kind, state, _) = match job_queue.get_job_at(job_idx) {
|
||||||
|
Some(k) => k,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !matches!(state, JobState::Unclaimed) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = kind.clone();
|
||||||
|
let target_pos = kind.target();
|
||||||
|
let scope = job_queue.get_scope_for_job(job_idx);
|
||||||
|
|
||||||
|
let mut candidate_dorfs: Vec<(Entity, IVec3)> = dorf_query
|
||||||
|
.iter_mut()
|
||||||
|
.filter(|(entity, queue, state, transform, _)| {
|
||||||
|
if locked_dorfs.contains(entity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let pos = transform.translation.as_ivec3();
|
||||||
|
if !tilemap.is_standable(pos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if !queue.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let state_val: &TaskState = &*state;
|
||||||
|
if matches!(state_val, TaskState::Active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.map(|(entity, _, _, transform, _)| (entity, transform.translation.as_ivec3()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
candidate_dorfs.sort_by_key(|(_, pos)| {
|
||||||
|
(pos.x - target_pos.x).abs()
|
||||||
|
+ (pos.y - target_pos.y).abs()
|
||||||
|
+ (pos.z - target_pos.z).abs()
|
||||||
|
});
|
||||||
|
|
||||||
|
let dorfs_to_try: Vec<_> = candidate_dorfs.into_iter().take(scope as usize).collect();
|
||||||
|
|
||||||
|
if dorfs_to_try.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dorf_entities: Vec<Entity> = dorfs_to_try.iter().map(|(e, _)| *e).collect();
|
||||||
|
if !job_queue.set_job_calculating(job_idx, dorf_entities.clone()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut best_result: Option<(Entity, IVec3, i32)> = None;
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
JobKind::FellTree { trunk_pos } => {
|
||||||
|
let approach_tiles = find_all_standable_adjacent(&trunk_pos, &tilemap);
|
||||||
|
if approach_tiles.is_empty() {
|
||||||
|
job_queue.suspend_job(job_idx);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dorf_entity, dorf_pos) in &dorfs_to_try {
|
||||||
|
let mut best_for_dorf: Option<(IVec3, i32)> = None;
|
||||||
|
|
||||||
|
for approach_tile in &approach_tiles {
|
||||||
|
if let Ok(distance) =
|
||||||
|
calculate_traversal_distance(&tilemap, *dorf_pos, *approach_tile)
|
||||||
|
{
|
||||||
|
match best_for_dorf {
|
||||||
|
None => best_for_dorf = Some((*approach_tile, distance)),
|
||||||
|
Some((_, best_dist)) if distance < best_dist => {
|
||||||
|
best_for_dorf = Some((*approach_tile, distance));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((approach, dist)) = best_for_dorf {
|
||||||
|
match best_result {
|
||||||
|
None => best_result = Some((*dorf_entity, approach, dist)),
|
||||||
|
Some((_, _, best_dist)) if dist < best_dist => {
|
||||||
|
best_result = Some((*dorf_entity, approach, dist));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JobKind::HaulCargo { cargo_pos, .. } => {
|
||||||
|
for (dorf_entity, dorf_pos) in &dorfs_to_try {
|
||||||
|
if let Ok(distance) =
|
||||||
|
calculate_traversal_distance(&tilemap, *dorf_pos, cargo_pos)
|
||||||
|
{
|
||||||
|
match best_result {
|
||||||
|
None => best_result = Some((*dorf_entity, cargo_pos, distance)),
|
||||||
|
Some((_, _, best_dist)) if distance < best_dist => {
|
||||||
|
best_result = Some((*dorf_entity, cargo_pos, distance));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((dorf_entity, approach_target, _)) = best_result {
|
||||||
|
if let Some(job_id) = job_queue.assign_job(job_idx, dorf_entity, approach_target) {
|
||||||
|
if let Ok((_, mut queue, mut state, _, mut ambulatory)) =
|
||||||
|
dorf_query.get_mut(dorf_entity)
|
||||||
|
{
|
||||||
|
let job_kind = match job_queue.get_job_kind_at(job_idx) {
|
||||||
|
Some(k) => k,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
let task = match job_kind {
|
||||||
|
JobKind::FellTree { trunk_pos } => Task::ChopTree {
|
||||||
|
job_id,
|
||||||
|
trunk_pos,
|
||||||
|
chop_ticks: 120,
|
||||||
|
step: ChopStep::MovingToTree {
|
||||||
|
approach: Some(approach_target),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JobKind::HaulCargo {
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
} => Task::HaulCargo {
|
||||||
|
job_id,
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
step: HaulStep::MovingToCargo {
|
||||||
|
approach: Some(approach_target),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
queue.clear();
|
||||||
|
queue.push(task);
|
||||||
|
*state = TaskState::Pending;
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[PATHFIND] Assigned job {:?} to dorf {:?} with approach {:?}",
|
||||||
|
job_id, dorf_entity, approach_target
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job_queue.increment_retry(job_idx);
|
||||||
|
if !dorf_entities.is_empty() {
|
||||||
|
job_queue.unclaim_jobs_for_entity(dorf_entities[0]);
|
||||||
|
}
|
||||||
|
let retry_count = job_queue.get_job_retry_count(job_idx).unwrap_or(0);
|
||||||
|
if retry_count >= 10 {
|
||||||
|
job_queue.suspend_job(job_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_all_standable_adjacent(trunk_pos: &IVec3, tilemap: &TileMap) -> SmallVec<[IVec3; 8]> {
|
||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
|
let mut tiles = SmallVec::new();
|
||||||
|
let standing_z = trunk_pos.z;
|
||||||
|
|
||||||
|
for dx in -1i32..=1 {
|
||||||
|
for dy in -1i32..=1 {
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let candidate = IVec3::new(
|
||||||
|
trunk_pos.x + dx * ITILE_SIZE,
|
||||||
|
trunk_pos.y + dy * ITILE_SIZE,
|
||||||
|
standing_z,
|
||||||
|
);
|
||||||
|
|
||||||
|
if tilemap.is_standable(candidate) {
|
||||||
|
tiles.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tiles
|
||||||
|
}
|
||||||
@@ -10,9 +10,13 @@ use std::collections::VecDeque;
|
|||||||
pub enum JobState {
|
pub enum JobState {
|
||||||
/// No dorf has taken this job.
|
/// No dorf has taken this job.
|
||||||
Unclaimed,
|
Unclaimed,
|
||||||
|
/// Paths are being calculated - dorfs in this vec are locked
|
||||||
|
Calculating(Vec<Entity>),
|
||||||
/// A dorf has claimed this job and is executing it.
|
/// A dorf has claimed this job and is executing it.
|
||||||
/// Stores the entity so we can unclaim if the dorf dies/fails.
|
/// Stores the entity so we can unclaim if the dorf dies/fails.
|
||||||
Claimed(Entity),
|
Claimed(Entity),
|
||||||
|
/// Pathfinding failed too many times - requires manual review or world change
|
||||||
|
Suspended,
|
||||||
/// All tasks for this job are done. Pending removal.
|
/// All tasks for this job are done. Pending removal.
|
||||||
Complete,
|
Complete,
|
||||||
}
|
}
|
||||||
@@ -71,6 +75,8 @@ struct Entry {
|
|||||||
kind: JobKind,
|
kind: JobKind,
|
||||||
state: JobState,
|
state: JobState,
|
||||||
id: JobId,
|
id: JobId,
|
||||||
|
approach_target: Option<IVec3>,
|
||||||
|
retry_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
@@ -94,6 +100,8 @@ impl JobQueue {
|
|||||||
kind,
|
kind,
|
||||||
state: JobState::Unclaimed,
|
state: JobState::Unclaimed,
|
||||||
id: new_id,
|
id: new_id,
|
||||||
|
approach_target: None,
|
||||||
|
retry_count: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +126,51 @@ impl JobQueue {
|
|||||||
entry.state = JobState::Unclaimed;
|
entry.state = JobState::Unclaimed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let JobState::Calculating(dorfs) = &mut entry.state {
|
||||||
|
dorfs.retain(|e| *e != entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dorf_locked(&self, dorf: Entity) -> bool {
|
||||||
|
self.jobs.iter().any(|entry| match &entry.state {
|
||||||
|
JobState::Calculating(dorfs) => dorfs.contains(&dorf),
|
||||||
|
JobState::Claimed(e) => *e == dorf,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_locked_dorfs(&self) -> Vec<Entity> {
|
||||||
|
let mut locked = Vec::new();
|
||||||
|
for entry in &self.jobs {
|
||||||
|
match &entry.state {
|
||||||
|
JobState::Calculating(dorfs) => locked.extend(dorfs.iter().cloned()),
|
||||||
|
JobState::Claimed(dorf) => locked.push(*dorf),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
locked
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_retry(&mut self, idx: usize) {
|
||||||
|
if let Some(entry) = self.jobs.get_mut(idx) {
|
||||||
|
entry.retry_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scope_for_job(&self, idx: usize) -> u32 {
|
||||||
|
const DEFAULT_DORFS: u32 = 5;
|
||||||
|
const EXPANDED_DORFS: u32 = 20;
|
||||||
|
const MAX_DORFS: u32 = 200;
|
||||||
|
|
||||||
|
if let Some(entry) = self.jobs.get(idx) {
|
||||||
|
match entry.retry_count {
|
||||||
|
0 => DEFAULT_DORFS,
|
||||||
|
1..=3 => EXPANDED_DORFS,
|
||||||
|
_ => MAX_DORFS,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DEFAULT_DORFS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +288,77 @@ impl JobQueue {
|
|||||||
.map(|(idx, entry)| (idx, &entry.kind))
|
.map(|(idx, entry)| (idx, &entry.kind))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn iter_unclaimed_with_state(&self) -> impl Iterator<Item = (usize, &JobKind, &JobState)> {
|
||||||
|
self.jobs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, entry)| matches!(entry.state, JobState::Unclaimed))
|
||||||
|
.map(|(idx, entry)| (idx, &entry.kind, &entry.state))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_job_at(&self, idx: usize) -> Option<(&JobKind, &JobState, &Option<IVec3>)> {
|
||||||
|
let entry = self.jobs.get(idx)?;
|
||||||
|
Some((&entry.kind, &entry.state, &entry.approach_target))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_job_kind_at(&self, idx: usize) -> Option<JobKind> {
|
||||||
|
self.jobs.get(idx).map(|e| e.kind.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_job_priority(&self, idx: usize) -> Option<u8> {
|
||||||
|
self.jobs.get(idx).map(|e| e.kind.priority())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_job_retry_count(&self, idx: usize) -> Option<u32> {
|
||||||
|
self.jobs.get(idx).map(|e| e.retry_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_job_calculating(&mut self, idx: usize, dorfs: Vec<Entity>) -> bool {
|
||||||
|
if idx >= self.jobs.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let entry = &mut self.jobs[idx];
|
||||||
|
if !matches!(entry.state, JobState::Unclaimed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
entry.state = JobState::Calculating(dorfs);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assign_job(
|
||||||
|
&mut self,
|
||||||
|
idx: usize,
|
||||||
|
dorf: Entity,
|
||||||
|
approach_target: IVec3,
|
||||||
|
) -> Option<JobId> {
|
||||||
|
if idx >= self.jobs.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let entry = &mut self.jobs[idx];
|
||||||
|
if !matches!(entry.state, JobState::Calculating(_)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
entry.state = JobState::Claimed(dorf);
|
||||||
|
entry.approach_target = Some(approach_target);
|
||||||
|
Some(entry.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn suspend_job(&mut self, idx: usize) {
|
||||||
|
if let Some(entry) = self.jobs.get_mut(idx) {
|
||||||
|
entry.state = JobState::Suspended;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_approach_target(&mut self, idx: usize) {
|
||||||
|
if let Some(entry) = self.jobs.get_mut(idx) {
|
||||||
|
entry.approach_target = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_job_state(&self, idx: usize) -> Option<&JobState> {
|
||||||
|
self.jobs.get(idx).map(|e| &e.state)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn claim_job_at(
|
pub fn claim_job_at(
|
||||||
&mut self,
|
&mut self,
|
||||||
idx: usize,
|
idx: usize,
|
||||||
@@ -316,7 +440,15 @@ impl JobQueue {
|
|||||||
}
|
}
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_fell_tree(&self) -> bool {
|
pub fn has_fell_tree(&self) -> bool {
|
||||||
self.jobs.iter().any(|e| e.kind.is_fell_tree())
|
self.jobs.iter().any(|e| {
|
||||||
|
if !e.kind.is_fell_tree() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matches!(
|
||||||
|
e.state,
|
||||||
|
JobState::Unclaimed | JobState::Calculating(_) | JobState::Claimed(_)
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_counts(&self) -> (usize, usize) {
|
pub fn debug_counts(&self) -> (usize, usize) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub mod demo;
|
|||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod job_assignment;
|
pub mod job_assignment;
|
||||||
|
pub mod job_pathfinding;
|
||||||
pub mod job_queue;
|
pub mod job_queue;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
pub mod queue_debug;
|
pub mod queue_debug;
|
||||||
@@ -13,6 +14,7 @@ pub use demo::demo_system;
|
|||||||
pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
||||||
pub use executor::task_executor_system;
|
pub use executor::task_executor_system;
|
||||||
pub use job_assignment::job_assignment_system;
|
pub use job_assignment::job_assignment_system;
|
||||||
|
pub use job_pathfinding::job_pathfinding_system;
|
||||||
pub use job_queue::{JobKind, JobQueue};
|
pub use job_queue::{JobKind, JobQueue};
|
||||||
pub use queue_debug::queue_debug_system;
|
pub use queue_debug::queue_debug_system;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::entities::tasks::{
|
use crate::entities::tasks::{
|
||||||
demo_system, job_assignment_system, job_queue::JobQueue, queue_debug_system,
|
demo_system, job_assignment_system, job_pathfinding_system, job_queue::JobQueue,
|
||||||
task_executor_system, LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped,
|
queue_debug_system, task_executor_system, LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted,
|
||||||
TaskFailed,
|
TaskDropped, TaskFailed,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ impl Plugin for TasksPlugin {
|
|||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(
|
(
|
||||||
demo_system,
|
demo_system,
|
||||||
|
job_pathfinding_system,
|
||||||
job_assignment_system,
|
job_assignment_system,
|
||||||
task_executor_system,
|
task_executor_system,
|
||||||
queue_debug_system,
|
queue_debug_system,
|
||||||
|
|||||||
Reference in New Issue
Block a user