From 053fe415bfb693de35d76f4027edcf901f92d28c Mon Sep 17 00:00:00 2001 From: popertots Date: Sun, 29 Mar 2026 13:13:53 +0100 Subject: [PATCH] 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) --- config.toml | 5 +- src/config.rs | 12 ++ src/entities/shared_systems/pathfinding.rs | 114 ++++++++++ src/entities/tasks/job_assignment.rs | 131 +----------- src/entities/tasks/job_pathfinding.rs | 232 +++++++++++++++++++++ src/entities/tasks/job_queue.rs | 134 +++++++++++- src/entities/tasks/mod.rs | 2 + src/plugins/tasks.rs | 7 +- 8 files changed, 506 insertions(+), 131 deletions(-) create mode 100644 src/entities/tasks/job_pathfinding.rs diff --git a/config.toml b/config.toml index 6f36a60..4c88fbe 100644 --- a/config.toml +++ b/config.toml @@ -6,4 +6,7 @@ vsync = "mailbox" [spawn_counts] dorfs = 5 pigs = 0 -rabbits = 0 \ No newline at end of file +rabbits = 0 + +[job_assignment] +max_dorfs_per_job = 5 \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 64fdec9..674a8da 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,8 @@ pub struct GameConfig { pub spawn_counts: SpawnCounts, #[serde(default)] pub display: DisplaySettings, + #[serde(default)] + pub job_assignment: JobAssignmentSettings, } #[derive(Debug, Deserialize, Clone, Default)] @@ -26,6 +28,16 @@ pub enum VsyncMode { 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 { fn deserialize(deserializer: D) -> Result where diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs index 9b5dcb6..7c6809c 100644 --- a/src/entities/shared_systems/pathfinding.rs +++ b/src/entities/shared_systems/pathfinding.rs @@ -63,6 +63,7 @@ use crate::constants::{ ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES, 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::shared_systems::constants::{ @@ -1620,3 +1621,116 @@ fn write_benchmark_csv(bench: &PathfindingBenchmark, filename: &str) -> std::io: 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 { + 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 +} diff --git a/src/entities/tasks/job_assignment.rs b/src/entities/tasks/job_assignment.rs index 9a9643e..ee66846 100644 --- a/src/entities/tasks/job_assignment.rs +++ b/src/entities/tasks/job_assignment.rs @@ -1,134 +1,13 @@ use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType}; -use crate::entities::cargo::HaulSlot; -use crate::entities::shared_components::Ambulatory; -use crate::entities::tasks::components::{Task, TaskQueue, TaskState}; -use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue}; -use crate::entities::tasks::jobs::{FellTreeJob, HaulCargoJob, IdleJob}; -use crate::world::tiles::TileMap; +use crate::entities::tasks::components::{TaskQueue, TaskState}; +use crate::entities::tasks::job_queue::JobId; +use crate::entities::tasks::jobs::IdleJob; use bevy::prelude::*; -use std::collections::HashSet; pub fn job_assignment_system( - mut job_queue: ResMut, - tilemap: Res, - mut dorf_query: Query< - ( - Entity, - &mut TaskQueue, - &mut TaskState, - &Transform, - &HaulSlot, - &mut Ambulatory, - ), - With, - >, + mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform), With>, ) { - let idle_dorfs: Vec<(Entity, IVec3)> = dorf_query - .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 = 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() { + for (dorf_entity, mut queue, mut state, transform) in dorf_query.iter_mut() { if queue.is_empty() && !matches!(*state, TaskState::Active) { let behaviour = EntityBehaviourRegistry::global_get("dorf"); let origin = transform.translation.as_ivec3(); diff --git a/src/entities/tasks/job_pathfinding.rs b/src/entities/tasks/job_pathfinding.rs new file mode 100644 index 0000000..2faa0c9 --- /dev/null +++ b/src/entities/tasks/job_pathfinding.rs @@ -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, + config: Res, + tilemap: Res, + mut dorf_query: Query< + ( + Entity, + &mut TaskQueue, + &mut TaskState, + &Transform, + &mut Ambulatory, + ), + With, + >, +) { + let mut unclaimed: Vec = 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 = 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 +} diff --git a/src/entities/tasks/job_queue.rs b/src/entities/tasks/job_queue.rs index c16f5bd..485cbaf 100644 --- a/src/entities/tasks/job_queue.rs +++ b/src/entities/tasks/job_queue.rs @@ -10,9 +10,13 @@ use std::collections::VecDeque; pub enum JobState { /// No dorf has taken this job. Unclaimed, + /// Paths are being calculated - dorfs in this vec are locked + Calculating(Vec), /// A dorf has claimed this job and is executing it. /// Stores the entity so we can unclaim if the dorf dies/fails. Claimed(Entity), + /// Pathfinding failed too many times - requires manual review or world change + Suspended, /// All tasks for this job are done. Pending removal. Complete, } @@ -71,6 +75,8 @@ struct Entry { kind: JobKind, state: JobState, id: JobId, + approach_target: Option, + retry_count: u32, } #[derive(Resource, Default)] @@ -94,6 +100,8 @@ impl JobQueue { kind, state: JobState::Unclaimed, id: new_id, + approach_target: None, + retry_count: 0, }); } @@ -118,6 +126,51 @@ impl JobQueue { 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 { + 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)) } + pub fn iter_unclaimed_with_state(&self) -> impl Iterator { + 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)> { + let entry = self.jobs.get(idx)?; + Some((&entry.kind, &entry.state, &entry.approach_target)) + } + + pub fn get_job_kind_at(&self, idx: usize) -> Option { + self.jobs.get(idx).map(|e| e.kind.clone()) + } + + pub fn get_job_priority(&self, idx: usize) -> Option { + self.jobs.get(idx).map(|e| e.kind.priority()) + } + + pub fn get_job_retry_count(&self, idx: usize) -> Option { + self.jobs.get(idx).map(|e| e.retry_count) + } + + pub fn set_job_calculating(&mut self, idx: usize, dorfs: Vec) -> 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 { + 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( &mut self, idx: usize, @@ -316,7 +440,15 @@ impl JobQueue { } #[inline] 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) { diff --git a/src/entities/tasks/mod.rs b/src/entities/tasks/mod.rs index 1ec29ba..a7472c8 100644 --- a/src/entities/tasks/mod.rs +++ b/src/entities/tasks/mod.rs @@ -3,6 +3,7 @@ pub mod demo; pub mod events; pub mod executor; pub mod job_assignment; +pub mod job_pathfinding; pub mod job_queue; pub mod jobs; pub mod queue_debug; @@ -13,6 +14,7 @@ pub use demo::demo_system; pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed}; pub use executor::task_executor_system; pub use job_assignment::job_assignment_system; +pub use job_pathfinding::job_pathfinding_system; pub use job_queue::{JobKind, JobQueue}; pub use queue_debug::queue_debug_system; diff --git a/src/plugins/tasks.rs b/src/plugins/tasks.rs index 883fdc3..2d3ca55 100644 --- a/src/plugins/tasks.rs +++ b/src/plugins/tasks.rs @@ -1,7 +1,7 @@ use crate::entities::tasks::{ - demo_system, job_assignment_system, job_queue::JobQueue, queue_debug_system, - task_executor_system, LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, - TaskFailed, + demo_system, job_assignment_system, job_pathfinding_system, job_queue::JobQueue, + queue_debug_system, task_executor_system, LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, + TaskDropped, TaskFailed, }; use bevy::prelude::*; @@ -20,6 +20,7 @@ impl Plugin for TasksPlugin { FixedUpdate, ( demo_system, + job_pathfinding_system, job_assignment_system, task_executor_system, queue_debug_system,