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:
2026-03-29 13:13:53 +01:00
parent 3c74a68d4d
commit 053fe415bf
8 changed files with 506 additions and 131 deletions
+3
View File
@@ -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
+12
View File
@@ -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
+114
View File
@@ -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(&current).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
}
+5 -126
View File
@@ -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();
+232
View File
@@ -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
}
+133 -1
View File
@@ -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) {
+2
View File
@@ -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;
+4 -3
View File
@@ -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,