use std::collections::HashMap;
use crate::components::ElevatorPhase;
use crate::door::DoorCommand;
use crate::entity::EntityId;
use crate::world::World;
use super::{BuiltinStrategy, DispatchManifest, DispatchStrategy, ElevatorGroup, RankContext};
pub const DEFAULT_HOLD_CAP_TICKS: u32 = 120;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LoopScheduleDispatch {
dwell_ticks: u32,
target_headway_ticks: u32,
hold_cap_ticks: u32,
#[serde(skip)]
last_arrival_tick: HashMap<EntityId, u64>,
#[serde(skip)]
seen: HashMap<EntityId, (u64, EntityId)>,
}
impl LoopScheduleDispatch {
#[must_use]
pub fn new(dwell_ticks: u32, target_headway_ticks: u32, hold_cap_ticks: u32) -> Self {
Self {
dwell_ticks: dwell_ticks.max(1),
target_headway_ticks: target_headway_ticks.max(1),
hold_cap_ticks: hold_cap_ticks.max(1),
last_arrival_tick: HashMap::new(),
seen: HashMap::new(),
}
}
#[must_use]
pub const fn dwell_ticks(&self) -> u32 {
self.dwell_ticks
}
#[must_use]
pub const fn target_headway_ticks(&self) -> u32 {
self.target_headway_ticks
}
#[must_use]
pub const fn hold_cap_ticks(&self) -> u32 {
self.hold_cap_ticks
}
}
impl Default for LoopScheduleDispatch {
fn default() -> Self {
Self::new(30, 300, DEFAULT_HOLD_CAP_TICKS)
}
}
impl DispatchStrategy for LoopScheduleDispatch {
fn pre_dispatch(
&mut self,
group: &ElevatorGroup,
_manifest: &DispatchManifest,
world: &mut World,
) {
let now = world
.resource::<crate::arrival_log::CurrentTick>()
.map(|ct| ct.0);
for line in group.lines() {
let line_eid = line.entity();
if !world
.line(line_eid)
.is_some_and(crate::components::Line::is_loop)
{
continue;
}
let elevators: Vec<EntityId> = line.elevators().to_vec();
for eid in elevators {
let Some(car) = world.elevator(eid) else {
continue;
};
let phase = car.phase;
let at_stop = car.target_stop;
{
if let Some(c) = world.elevator_mut(eid) {
c.door_open_ticks = self.dwell_ticks;
}
}
if !matches!(phase, ElevatorPhase::Loading) {
continue;
}
let Some(now) = now else { continue };
let Some(stop) = at_stop else { continue };
let prev_seen = self.seen.insert(eid, (now, stop));
let is_fresh_arrival = match prev_seen {
None => true,
Some((prev_tick, prev_stop)) => prev_tick + 1 != now || prev_stop != stop,
};
if !is_fresh_arrival {
continue;
}
let Some(&prev) = self.last_arrival_tick.get(&stop) else {
self.last_arrival_tick.insert(stop, now);
continue;
};
self.last_arrival_tick.insert(stop, now);
let gap = now.saturating_sub(prev);
let target = u64::from(self.target_headway_ticks);
if gap >= target {
continue;
}
#[allow(
clippy::cast_possible_truncation,
reason = "deficit is bounded by target_headway_ticks (u32); truncation to u32 is exact"
)]
let deficit = (target - gap) as u32;
let extra = deficit.min(self.hold_cap_ticks);
if extra == 0 {
continue;
}
if let Some(c) = world.elevator_mut(eid) {
if c.door_command_queue.len() < crate::components::DOOR_COMMAND_QUEUE_CAP {
c.door_command_queue
.push(DoorCommand::HoldOpen { ticks: extra });
}
}
}
}
}
fn rank(&self, _ctx: &RankContext<'_>) -> Option<f64> {
None
}
fn builtin_id(&self) -> Option<BuiltinStrategy> {
Some(BuiltinStrategy::LoopSchedule)
}
fn snapshot_config(&self) -> Option<String> {
ron::to_string(self).ok()
}
fn restore_config(&mut self, config: &str) -> Result<(), String> {
let restored: Self = ron::from_str(config).map_err(|e| e.to_string())?;
*self = restored;
Ok(())
}
fn notify_removed(&mut self, elevator: EntityId) {
self.seen.remove(&elevator);
}
}