Fix job queueing system: add JobId tracking for failed job reassignment

- Add JobId (rolling u16 counter) to track jobs through their lifecycle
- Add job_id field to all Task variants (Idle, GoTo, ChopTree, HaulCargo, DropHauled)
- Add unclaim_job() method to requeue failed jobs for reassignment
- Change task failure paths to use unclaim_job() instead of complete_job_by_id()
  - This allows failed jobs to be picked up by another dorf instead of being stuck
- Add to_job_kind() and get_job_id() helper methods to Task
- Fix pathfinding is_standable_tile debug logging spam
- Fix chunk bounds check in ChunkData::is_standable (was checking wrong z range)
This commit is contained in:
2026-03-27 20:28:58 +00:00
parent bd1b45e4d7
commit 79e0efa409
12 changed files with 119 additions and 42 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ pub struct Container {
pub is_open: bool, pub is_open: bool,
/// Items held. May include entities that themselves have Container components. /// Items held. May include entities that themselves have Container components.
/// TODO: migrate away from Entity when stable ID scheme exists. /// TODO: migrate away from Entity when stable ID scheme exists.
pub contents: SmallVec<[Entity; 4]>, pub contents: SmallVec<[Entity; 8]>,
/// Cached total weight of contents (updated on add/remove, not per-tick). /// Cached total weight of contents (updated on add/remove, not per-tick).
pub current_weight: u32, pub current_weight: u32,
} }
+2
View File
@@ -4,6 +4,7 @@ use crate::constants::*;
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType}; use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
use crate::entities::cargo::{CarryVisualState, HaulSlot}; use crate::entities::cargo::{CarryVisualState, HaulSlot};
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
use crate::entities::tasks::job_queue::JobId;
use crate::entities::tasks::{IdleState, Task, TaskQueue, TaskState}; use crate::entities::tasks::{IdleState, Task, TaskQueue, TaskState};
use crate::game::SpawnDelay; use crate::game::SpawnDelay;
use crate::world::VisibleGameEntity; use crate::world::VisibleGameEntity;
@@ -57,6 +58,7 @@ impl Dorf {
carry_visual: CarryVisualState::new(normal_sprite, carry_sprite), carry_visual: CarryVisualState::new(normal_sprite, carry_sprite),
task_queue: TaskQueue { task_queue: TaskQueue {
tasks: VecDeque::from([Task::Idle { tasks: VecDeque::from([Task::Idle {
job_id: JobId::default(),
origin, origin,
sigma_world: behaviour.idle.sigma_world, sigma_world: behaviour.idle.sigma_world,
state: IdleState::Picking { state: IdleState::Picking {
+1 -19
View File
@@ -902,25 +902,7 @@ fn validate_next_steps(tilemap: &TileMap, path: &[Vec3], start_index: usize, ste
} }
fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool { fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
let result = tilemap.is_standable(pos); tilemap.is_standable(pos)
if !result {
let chunk_pos = crate::world::chunks::world_to_chunk(pos);
let chunk_exists = tilemap.chunks.contains_key(&chunk_pos);
let (local_x, local_y, z) = if let Some(c) = tilemap.chunks.get(&chunk_pos) {
crate::world::tiles::chunk_data::ChunkData::world_to_local(pos)
} else {
(0, 0, 0)
};
let floor_exists = tilemap.floor_tiles.contains_key(&pos);
let fixture_exists = tilemap.fixture_tiles.contains_key(&pos);
info!(
"[PATH] is_standable_tile=false: pos={:?} chunk={:?} chunk_loaded={} local=({},{},{}) floor={} fixture={}",
pos, chunk_pos, chunk_exists, local_x, local_y, z, floor_exists, fixture_exists
);
}
result
} }
/// Sample a random standable tile on an edge of `next_chunk`, picking the one /// Sample a random standable tile on an edge of `next_chunk`, picking the one
+38
View File
@@ -14,6 +14,8 @@
use bevy::prelude::*; use bevy::prelude::*;
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::entities::tasks::{job_queue::JobId, JobKind};
/// Maximum consecutive pick failures before panic. /// Maximum consecutive pick failures before panic.
/// Prevents infinite retry loops when tilemap has no standable tiles. /// Prevents infinite retry loops when tilemap has no standable tiles.
pub const IDLE_MAX_RETRIES: u32 = 50; pub const IDLE_MAX_RETRIES: u32 = 50;
@@ -112,6 +114,7 @@ pub const CHOP_TICKS_DEFAULT: u32 = 120;
#[repr(u8)] #[repr(u8)]
pub enum Task { pub enum Task {
Idle { Idle {
job_id: JobId,
/// Home position in world units — centre of Gaussian distribution. /// Home position in world units — centre of Gaussian distribution.
/// Set at spawn, does not drift. /// Set at spawn, does not drift.
origin: IVec3, origin: IVec3,
@@ -125,6 +128,7 @@ pub enum Task {
/// Completes when entity is within `threshold_tiles` of target. /// Completes when entity is within `threshold_tiles` of target.
/// Uses existing pathfinding infrastructure. /// Uses existing pathfinding infrastructure.
GoTo { GoTo {
job_id: JobId,
target: IVec3, target: IVec3,
/// Completion threshold in tiles (Chebyshev distance). /// Completion threshold in tiles (Chebyshev distance).
threshold_tiles: i32, threshold_tiles: i32,
@@ -132,6 +136,7 @@ pub enum Task {
/// Walk to a tree trunk and fell it. Produces Cargo log entities on completion. /// Walk to a tree trunk and fell it. Produces Cargo log entities on completion.
ChopTree { ChopTree {
job_id: JobId,
/// World position of the lowest trunk fixture tile. /// World position of the lowest trunk fixture tile.
trunk_pos: IVec3, trunk_pos: IVec3,
/// Ticks required to chop. Future: derived from skill + tool. /// Ticks required to chop. Future: derived from skill + tool.
@@ -142,6 +147,7 @@ pub enum Task {
/// Pick up a specific Cargo entity and haul it to dest. /// Pick up a specific Cargo entity and haul it to dest.
/// Renamed from HaulObject for consistency with the Cargo type. /// Renamed from HaulObject for consistency with the Cargo type.
HaulCargo { HaulCargo {
job_id: JobId,
/// The Cargo entity to pick up. /// The Cargo entity to pick up.
cargo_entity: Entity, cargo_entity: Entity,
/// Tile position of the cargo in the world (for pathfinding). /// Tile position of the cargo in the world (for pathfinding).
@@ -153,6 +159,7 @@ pub enum Task {
/// Drop whatever is in HaulSlot at or near pos. /// Drop whatever is in HaulSlot at or near pos.
DropHauled { DropHauled {
job_id: JobId,
/// Preferred drop position. Actual drop may be nearby if occupied. /// Preferred drop position. Actual drop may be nearby if occupied.
pos: IVec3, pos: IVec3,
step: DropStep, step: DropStep,
@@ -178,6 +185,37 @@ impl Task {
Task::DropHauled { .. } => "DropHauled", Task::DropHauled { .. } => "DropHauled",
} }
} }
pub fn to_job_kind(&self) -> Option<JobKind> {
match self {
// 1. Handle the "Global" job types
Task::ChopTree { trunk_pos, .. } => Some(JobKind::FellTree {
trunk_pos: *trunk_pos,
}),
Task::HaulCargo {
cargo_entity,
cargo_pos,
dest,
..
} => Some(JobKind::HaulCargo {
cargo_entity: *cargo_entity,
cargo_pos: *cargo_pos,
dest: *dest,
}),
// 2. Handle everything else (GoTo, DropHauled, Idle, etc.)
// We use the underscore _ to say "for any other variant, do this"
_ => None,
}
}
pub fn get_job_id(&self) -> Option<JobId> {
match self {
Task::ChopTree { job_id, .. } => Some(*job_id),
Task::HaulCargo { job_id, .. } => Some(*job_id),
_ => None,
}
}
} }
/// Queue of tasks for an entity. Front of queue is currently executing task. /// Queue of tasks for an entity. Front of queue is currently executing task.
+1 -1
View File
@@ -45,7 +45,7 @@ pub struct TaskBlocked {
#[derive(Message, Clone)] #[derive(Message, Clone)]
pub struct LogsSpawned { pub struct LogsSpawned {
/// Entities of the spawned logs. /// Entities of the spawned logs.
pub log_entities: SmallVec<[Entity; 8]>, pub log_entities: SmallVec<[Entity; 12]>,
/// Destination for hauling (currently origin, later stockpile). /// Destination for hauling (currently origin, later stockpile).
pub dest: IVec3, pub dest: IVec3,
} }
+20 -5
View File
@@ -16,7 +16,7 @@ use crate::entities::tasks::components::{
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState, ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
}; };
use crate::entities::tasks::events::{LogsSpawned, TaskClaimed, TaskCompleted, TaskFailed}; use crate::entities::tasks::events::{LogsSpawned, TaskClaimed, TaskCompleted, TaskFailed};
use crate::entities::tasks::job_queue::{JobKind, JobQueue}; use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue};
use crate::entities::tasks::tasks::idle::execute_idle; use crate::entities::tasks::tasks::idle::execute_idle;
use crate::world::chunks::ChunkMap; use crate::world::chunks::ChunkMap;
use crate::world::generation::forestry::{fell_tree, TreePart}; use crate::world::generation::forestry::{fell_tree, TreePart};
@@ -79,6 +79,7 @@ pub fn task_executor_system(
if queue.is_empty() { if queue.is_empty() {
queue.push(Task::Idle { queue.push(Task::Idle {
job_id: JobId::default(),
origin, origin,
sigma_world: behaviour.idle.sigma_world, sigma_world: behaviour.idle.sigma_world,
state: IdleState::Picking { state: IdleState::Picking {
@@ -102,8 +103,6 @@ pub fn task_executor_system(
// Execute current task if Active // Execute current task if Active
if *state == TaskState::Active { if *state == TaskState::Active {
if let Some(current_task) = queue.current_mut() { if let Some(current_task) = queue.current_mut() {
let mut failed_reason: Option<&'static str> = None;
match current_task { match current_task {
Task::Idle { .. } => { Task::Idle { .. } => {
execute_idle( execute_idle(
@@ -119,6 +118,7 @@ pub fn task_executor_system(
); );
} }
Task::GoTo { Task::GoTo {
job_id: _,
target, target,
threshold_tiles, threshold_tiles,
} => { } => {
@@ -139,11 +139,13 @@ pub fn task_executor_system(
} }
} }
Task::ChopTree { Task::ChopTree {
job_id,
trunk_pos, trunk_pos,
chop_ticks, chop_ticks,
step, step,
} => match step { } => match step {
ChopStep::MovingToTree { ref mut approach } => { ChopStep::MovingToTree { ref mut approach } => {
let job_id_val = *job_id;
if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) { if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) {
info!( info!(
"TASK FAILED: {:?} for {:?} - {}", "TASK FAILED: {:?} for {:?} - {}",
@@ -154,6 +156,7 @@ pub fn task_executor_system(
task: current_task.clone(), task: current_task.clone(),
reason: "tree already gone", reason: "tree already gone",
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
@@ -207,6 +210,7 @@ pub fn task_executor_system(
task: current_task.clone(), task: current_task.clone(),
reason, reason,
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
@@ -259,7 +263,7 @@ pub fn task_executor_system(
fall_dir = Vec2::new(1.0, 0.0); // default: fall east fall_dir = Vec2::new(1.0, 0.0); // default: fall east
} }
let log_sprite: Handle<Image> = asset_server.load("log_cargo.png"); let log_sprite: Handle<Image> = asset_server.load("log_cargo.png");
let mut log_entities: SmallVec<[(Entity, IVec3); 8]> = let mut log_entities: SmallVec<[(Entity, IVec3); 12]> =
SmallVec::new(); SmallVec::new();
// Loop from 0 up to the number of trunk segments found // Loop from 0 up to the number of trunk segments found
for i in 0..trunk_count { for i in 0..trunk_count {
@@ -321,20 +325,25 @@ pub fn task_executor_system(
} }
ChopStep::Done => { ChopStep::Done => {
*state = TaskState::Completed; *state = TaskState::Completed;
// Remove job from queue
job_queue.complete_job_by_id(current_task.get_job_id().unwrap());
} }
}, },
Task::HaulCargo { Task::HaulCargo {
job_id,
cargo_entity, cargo_entity,
cargo_pos, cargo_pos,
dest, dest,
step, step,
} => { } => {
let job_id_val = *job_id;
let Some(ref mut haul) = haul_slot else { let Some(ref mut haul) = haul_slot else {
failed_writer.write(TaskFailed { failed_writer.write(TaskFailed {
entity, entity,
task: current_task.clone(), task: current_task.clone(),
reason: "entity has no HaulSlot", reason: "entity has no HaulSlot",
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
}; };
@@ -356,6 +365,7 @@ pub fn task_executor_system(
task: current_task.clone(), task: current_task.clone(),
reason: "cargo no longer exists", reason: "cargo no longer exists",
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
@@ -475,6 +485,7 @@ pub fn task_executor_system(
task: current_task.clone(), task: current_task.clone(),
reason: "no standable tile near cargo", reason: "no standable tile near cargo",
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
@@ -508,6 +519,7 @@ pub fn task_executor_system(
task: current_task.clone(), task: current_task.clone(),
reason: "HaulSlot already occupied", reason: "HaulSlot already occupied",
}); });
job_queue.unclaim_job(job_id_val);
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
@@ -596,10 +608,11 @@ pub fn task_executor_system(
HaulStep::Done => { HaulStep::Done => {
info!("[HAUL] {:?} haul task COMPLETE", entity); info!("[HAUL] {:?} haul task COMPLETE", entity);
*state = TaskState::Completed; *state = TaskState::Completed;
job_queue.complete_job_by_id(current_task.get_job_id().unwrap());
} }
} }
} }
Task::DropHauled { pos, step } => { Task::DropHauled { job_id, pos, step } => {
let Some(ref mut haul) = haul_slot else { let Some(ref mut haul) = haul_slot else {
*state = TaskState::Completed; *state = TaskState::Completed;
continue; continue;
@@ -651,6 +664,7 @@ pub fn task_executor_system(
*step = DropStep::Done; *step = DropStep::Done;
} }
DropStep::Done => { DropStep::Done => {
job_queue.complete_job_by_id(current_task.get_job_id().unwrap());
*state = TaskState::Completed; *state = TaskState::Completed;
} }
} }
@@ -677,6 +691,7 @@ pub fn task_executor_system(
if completed_task.is_terminal() && queue.is_empty() { if completed_task.is_terminal() && queue.is_empty() {
queue.push(Task::Idle { queue.push(Task::Idle {
job_id: JobId::default(),
origin, origin,
sigma_world: behaviour.idle.sigma_world, sigma_world: behaviour.idle.sigma_world,
state: IdleState::Picking { state: IdleState::Picking {
+7 -5
View File
@@ -2,7 +2,7 @@ use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
use crate::entities::cargo::HaulSlot; use crate::entities::cargo::HaulSlot;
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
use crate::entities::tasks::components::{Task, TaskQueue, TaskState}; use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
use crate::entities::tasks::job_queue::{JobKind, JobQueue}; use crate::entities::tasks::job_queue::{JobId, JobKind, JobQueue};
use crate::entities::tasks::jobs::{FellTreeJob, HaulCargoJob, IdleJob}; use crate::entities::tasks::jobs::{FellTreeJob, HaulCargoJob, IdleJob};
use crate::world::tiles::TileMap; use crate::world::tiles::TileMap;
use bevy::prelude::*; use bevy::prelude::*;
@@ -35,7 +35,9 @@ pub fn job_assignment_system(
let dorf_pos_ivec = transform.translation.as_ivec3(); let dorf_pos_ivec = transform.translation.as_ivec3();
let dorf_pos_2d = Vec2::new(transform.translation.x, transform.translation.y); let dorf_pos_2d = Vec2::new(transform.translation.x, transform.translation.y);
if let Some(job) = job_queue.pop_best_pathfinding(&tilemap, dorf_pos_ivec, dorf_pos_2d) { if let Some((job_id, job)) =
job_queue.pop_best_pathfinding(&tilemap, dorf_pos_ivec, dorf_pos_2d)
{
info!( info!(
"[ASSIGN] Job assigned to dorf at {:?}: {:?}", "[ASSIGN] Job assigned to dorf at {:?}: {:?}",
dorf_pos_ivec.xy(), dorf_pos_ivec.xy(),
@@ -51,7 +53,7 @@ pub fn job_assignment_system(
JobKind::FellTree { trunk_pos } => { JobKind::FellTree { trunk_pos } => {
info!("[ASSIGN] Dorf assigned to FellTree at {:?}", trunk_pos); info!("[ASSIGN] Dorf assigned to FellTree at {:?}", trunk_pos);
queue.clear(); queue.clear();
queue.push(FellTreeJob::start(trunk_pos)); queue.push(FellTreeJob::start(job_id, trunk_pos));
*state = TaskState::Pending; *state = TaskState::Pending;
} }
JobKind::HaulCargo { JobKind::HaulCargo {
@@ -61,7 +63,7 @@ pub fn job_assignment_system(
} => { } => {
info!("[ASSIGN] Dorf assigned to HaulCargo at {:?}", cargo_pos); info!("[ASSIGN] Dorf assigned to HaulCargo at {:?}", cargo_pos);
queue.clear(); queue.clear();
queue.push(HaulCargoJob::start(cargo_entity, cargo_pos, dest)); queue.push(HaulCargoJob::start(job_id, cargo_entity, cargo_pos, dest));
*state = TaskState::Pending; *state = TaskState::Pending;
} }
} }
@@ -69,7 +71,7 @@ pub fn job_assignment_system(
let behaviour = EntityBehaviourRegistry::global_get("dorf"); let behaviour = EntityBehaviourRegistry::global_get("dorf");
let origin = transform.translation.as_ivec3(); let origin = transform.translation.as_ivec3();
if queue.is_empty() { if queue.is_empty() {
queue.push(IdleJob::start(origin, &behaviour.idle)); queue.push(IdleJob::start(JobId::default(), origin, &behaviour.idle));
*state = TaskState::Pending; *state = TaskState::Pending;
} }
} }
+31 -3
View File
@@ -5,7 +5,7 @@ use smallvec::SmallVec;
use std::cmp::Ordering; use std::cmp::Ordering;
use std::collections::VecDeque; use std::collections::VecDeque;
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub enum JobKind { pub enum JobKind {
FellTree { FellTree {
trunk_pos: IVec3, trunk_pos: IVec3,
@@ -44,14 +44,27 @@ impl JobKind {
} }
} }
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct JobId(u16);
impl JobId {
#[inline]
pub fn next(&mut self) -> JobId {
self.0 = self.0.wrapping_add(1);
*self
}
}
struct Entry { struct Entry {
kind: JobKind, kind: JobKind,
claimed: bool, claimed: bool,
id: JobId,
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct JobQueue { pub struct JobQueue {
jobs: VecDeque<Entry>, jobs: VecDeque<Entry>,
next_id: JobId,
} }
impl JobQueue { impl JobQueue {
@@ -60,15 +73,30 @@ impl JobQueue {
self.jobs.push_back(Entry { self.jobs.push_back(Entry {
kind, kind,
claimed: false, claimed: false,
id: self.next_id.next(),
}); });
} }
pub fn complete_job_by_id(&mut self, id: JobId) {
if let Some(index) = self.jobs.iter().position(|j| j.id == id) {
self.jobs.remove(index);
info!("[QUEUE] Job ID {:?} removed from global queue", id);
}
}
pub fn unclaim_job(&mut self, id: JobId) {
if let Some(entry) = self.jobs.iter_mut().find(|j| j.id == id) {
entry.claimed = false;
info!("[QUEUE] Job ID {:?} marked unclaimed for reassignment", id);
}
}
pub fn pop_best_pathfinding( pub fn pop_best_pathfinding(
&mut self, &mut self,
tilemap: &crate::world::tiles::TileMap, tilemap: &crate::world::tiles::TileMap,
dorf_pos: IVec3, dorf_pos: IVec3,
_dorf_pos_2d: Vec2, _dorf_pos_2d: Vec2,
) -> Option<JobKind> { ) -> Option<(JobId, JobKind)> {
if self.jobs.is_empty() { if self.jobs.is_empty() {
return None; return None;
} }
@@ -140,7 +168,7 @@ impl JobQueue {
self.jobs[idx].claimed = true; self.jobs[idx].claimed = true;
// Note: JobKind needs `#[derive(Clone)]` if it doesn't have it already // Note: JobKind needs `#[derive(Clone)]` if it doesn't have it already
return Some(self.jobs[idx].kind.clone()); return Some((self.jobs[idx].id, self.jobs[idx].kind.clone()));
} }
} }
+7 -3
View File
@@ -1,12 +1,14 @@
use crate::entities::behaviour::IdleBehaviour; use crate::entities::behaviour::IdleBehaviour;
use crate::entities::tasks::components::{ChopStep, HaulStep, IdleState, Task, CHOP_TICKS_DEFAULT}; use crate::entities::tasks::components::{ChopStep, HaulStep, IdleState, Task, CHOP_TICKS_DEFAULT};
use crate::entities::tasks::job_queue::JobId;
use bevy::prelude::{Entity, IVec3}; use bevy::prelude::{Entity, IVec3};
pub struct FellTreeJob; pub struct FellTreeJob;
impl FellTreeJob { impl FellTreeJob {
pub fn start(trunk_pos: IVec3) -> Task { pub fn start(job_id: JobId, trunk_pos: IVec3) -> Task {
Task::ChopTree { Task::ChopTree {
job_id,
trunk_pos, trunk_pos,
chop_ticks: CHOP_TICKS_DEFAULT, chop_ticks: CHOP_TICKS_DEFAULT,
step: ChopStep::MovingToTree { approach: None }, step: ChopStep::MovingToTree { approach: None },
@@ -17,8 +19,9 @@ impl FellTreeJob {
pub struct HaulCargoJob; pub struct HaulCargoJob;
impl HaulCargoJob { impl HaulCargoJob {
pub fn start(cargo_entity: Entity, cargo_pos: IVec3, dest: IVec3) -> Task { pub fn start(job_id: JobId, cargo_entity: Entity, cargo_pos: IVec3, dest: IVec3) -> Task {
Task::HaulCargo { Task::HaulCargo {
job_id,
cargo_entity, cargo_entity,
cargo_pos, cargo_pos,
dest, dest,
@@ -30,8 +33,9 @@ impl HaulCargoJob {
pub struct IdleJob; pub struct IdleJob;
impl IdleJob { impl IdleJob {
pub fn start(origin: IVec3, behaviour: &IdleBehaviour) -> Task { pub fn start(job_id: JobId, origin: IVec3, behaviour: &IdleBehaviour) -> Task {
Task::Idle { Task::Idle {
job_id,
origin, origin,
sigma_world: behaviour.sigma_world, sigma_world: behaviour.sigma_world,
state: IdleState::Picking { state: IdleState::Picking {
+1
View File
@@ -22,6 +22,7 @@ pub fn execute_idle(
current_tick: u32, current_tick: u32,
) -> TaskResult { ) -> TaskResult {
let Task::Idle { let Task::Idle {
job_id,
origin, origin,
sigma_world, sigma_world,
state, state,
+1 -1
View File
@@ -311,7 +311,7 @@ pub fn fell_tree(
let mut dirty_columns: FxHashSet<IVec3> = FxHashSet::default(); let mut dirty_columns: FxHashSet<IVec3> = FxHashSet::default();
let z_total = crate::world::chunks::Z_BELOW as i32 + crate::world::chunks::Z_ABOVE as i32 + 1; let z_total = crate::world::chunks::Z_BELOW as i32 + crate::world::chunks::Z_ABOVE as i32 + 1;
let mut trunk_positions: SmallVec<[IVec3; 8]> = SmallVec::new(); let mut trunk_positions: SmallVec<[IVec3; 12]> = SmallVec::new();
for (entity, tile_pos, is_trunk) in to_remove.iter() { for (entity, tile_pos, is_trunk) in to_remove.iter() {
if *is_trunk { if *is_trunk {
+9 -4
View File
@@ -99,8 +99,12 @@ impl ChunkData {
// chunkdata.rs // chunkdata.rs
pub fn is_standable(&self, local_x: i32, local_y: i32, z: i32) -> bool { pub fn is_standable(&self, local_x: i32, local_y: i32, z: i32) -> bool {
// 1. Bounds check (z is an index here, e.g., 0 to 64) // 1. Bounds check: z is a signed local Z index (e.g. -Z_BELOW to Z_ABOVE)
if z < 0 || z >= (Z_BELOW + Z_ABOVE) as i32 { if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
return false;
}
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
return false; return false;
} }
@@ -114,9 +118,10 @@ impl ChunkData {
let in_fixture = (self.stand_in_fixture[word] & mask) != 0; let in_fixture = (self.stand_in_fixture[word] & mask) != 0;
// 2. Check the tile immediately below (z - 1) // 2. Check the tile immediately below (z - 1)
if z <= 0 { // Can't stand at the absolute bottom of the generated world
if z <= -(Z_BELOW as i32) {
return false; return false;
} // Bottom of the world }
let below_idx = Self::pos_to_index(local_x, local_y, z - 1); let below_idx = Self::pos_to_index(local_x, local_y, z - 1);
let below_word = below_idx / 32; let below_word = below_idx / 32;