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:
+4
-1
@@ -6,4 +6,7 @@ vsync = "mailbox"
|
||||
[spawn_counts]
|
||||
dorfs = 5
|
||||
pigs = 0
|
||||
rabbits = 0
|
||||
rabbits = 0
|
||||
|
||||
[job_assignment]
|
||||
max_dorfs_per_job = 5
|
||||
@@ -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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -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<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::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<JobQueue>,
|
||||
tilemap: Res<TileMap>,
|
||||
mut dorf_query: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut TaskQueue,
|
||||
&mut TaskState,
|
||||
&Transform,
|
||||
&HaulSlot,
|
||||
&mut Ambulatory,
|
||||
),
|
||||
With<EntityType>,
|
||||
>,
|
||||
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform), With<EntityType>>,
|
||||
) {
|
||||
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<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() {
|
||||
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();
|
||||
|
||||
@@ -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 {
|
||||
/// No dorf has taken this job.
|
||||
Unclaimed,
|
||||
/// Paths are being calculated - dorfs in this vec are locked
|
||||
Calculating(Vec<Entity>),
|
||||
/// 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<IVec3>,
|
||||
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<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))
|
||||
}
|
||||
|
||||
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(
|
||||
&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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user