use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use crate::components::{DestinationQueue, Direction, ElevatorPhase};
use crate::entity::EntityId;
use crate::world::{ExtKey, World};
use super::{DispatchManifest, DispatchStrategy, ElevatorGroup, RankContext, pair_can_do_work};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssignedCar(pub EntityId);
pub const ASSIGNED_CAR_KEY: ExtKey<AssignedCar> = ExtKey::new("assigned_car");
pub struct DestinationDispatch {
stop_penalty: Option<f64>,
commitment_window_ticks: Option<u64>,
}
impl DestinationDispatch {
#[must_use]
pub const fn new() -> Self {
Self {
stop_penalty: None,
commitment_window_ticks: None,
}
}
#[must_use]
pub const fn with_stop_penalty(mut self, penalty: f64) -> Self {
self.stop_penalty = Some(penalty);
self
}
#[must_use]
pub const fn with_commitment_window_ticks(mut self, window: u64) -> Self {
self.commitment_window_ticks = Some(window);
self
}
}
impl Default for DestinationDispatch {
fn default() -> Self {
Self::new()
}
}
impl DispatchStrategy for DestinationDispatch {
#[allow(clippy::too_many_lines)]
fn pre_dispatch(
&mut self,
group: &ElevatorGroup,
manifest: &DispatchManifest,
world: &mut World,
) {
if group.hall_call_mode() != super::HallCallMode::Destination {
return;
}
let candidate_cars: Vec<EntityId> = group
.elevator_entities()
.iter()
.copied()
.filter(|eid| !world.is_disabled(*eid))
.filter(|eid| {
!world
.service_mode(*eid)
.is_some_and(|m| m.is_dispatch_excluded())
})
.filter(|eid| world.elevator(*eid).is_some())
.collect();
if candidate_cars.is_empty() {
return;
}
let mut stale_assignments: Vec<EntityId> = Vec::new();
let mut pending: Vec<(EntityId, EntityId, EntityId, f64)> = Vec::new();
for (_, riders) in manifest.iter_waiting_stops() {
for info in riders {
if let Some(AssignedCar(c)) = world.ext::<AssignedCar>(info.id) {
let alive = world.elevator(c).is_some() && !world.is_disabled(c);
let latched = self
.commitment_window_ticks
.is_none_or(|w| assigned_car_within_window(world, info.id, c, w));
if alive && latched {
continue; }
stale_assignments.push(info.id);
}
let Some(dest) = info.destination else {
continue;
};
let Some(route) = world.route(info.id) else {
continue;
};
let Some(leg) = route.current() else {
continue;
};
if !group.accepts_leg(leg) {
continue;
}
pending.push((info.id, leg.from, dest, info.weight.value()));
}
}
pending.sort_by_key(|(rid, ..)| *rid);
for rid in stale_assignments {
world.remove_ext::<AssignedCar>(rid);
}
let mut committed_load: std::collections::BTreeMap<EntityId, f64> =
std::collections::BTreeMap::new();
for &eid in &candidate_cars {
if let Some(car) = world.elevator(eid) {
committed_load.insert(eid, car.current_load().value());
}
}
let waiting_assignments: Vec<(EntityId, EntityId)> = world
.ext_map::<AssignedCar>()
.map(|m| m.iter().map(|(rid, AssignedCar(c))| (rid, *c)).collect())
.unwrap_or_default();
for (rid, car) in waiting_assignments {
if let Some(rider) = world.rider(rid)
&& rider.phase() == crate::components::RiderPhase::Waiting
{
*committed_load.entry(car).or_insert(0.0) += rider.weight.value();
}
}
for (rid, origin, dest, weight) in pending {
let best = candidate_cars
.iter()
.filter_map(|&eid| {
let car = world.elevator(eid)?;
if car.restricted_stops().contains(&dest)
|| car.restricted_stops().contains(&origin)
{
return None;
}
if car.weight_capacity().value() > 0.0 && weight > car.weight_capacity().value()
{
return None;
}
let com = committed_load.get(&eid).copied().unwrap_or(0.0);
let cost = self.compute_cost(eid, origin, dest, world, com);
if cost.is_finite() {
Some((eid, cost))
} else {
None
}
})
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(eid, _)| eid);
let Some(car_eid) = best else {
continue;
};
world.insert_ext(rid, AssignedCar(car_eid), ASSIGNED_CAR_KEY);
*committed_load.entry(car_eid).or_insert(0.0) += weight;
}
for &car_eid in &candidate_cars {
rebuild_car_queue(world, car_eid);
}
}
fn rank(&mut self, ctx: &RankContext<'_>) -> Option<f64> {
let front = ctx
.world
.destination_queue(ctx.car)
.and_then(DestinationQueue::front)?;
if front == ctx.stop && pair_can_do_work(ctx) {
Some(0.0)
} else {
None
}
}
}
impl DestinationDispatch {
fn compute_cost(
&self,
eid: EntityId,
origin: EntityId,
dest: EntityId,
world: &World,
committed_load: f64,
) -> f64 {
let Some(car) = world.elevator(eid) else {
return f64::INFINITY;
};
if car.max_speed().value() <= 0.0 {
return f64::INFINITY;
}
let Some(car_pos) = world.position(eid).map(|p| p.value) else {
return f64::INFINITY;
};
let Some(origin_pos) = world.stop_position(origin) else {
return f64::INFINITY;
};
let Some(dest_pos) = world.stop_position(dest) else {
return f64::INFINITY;
};
let door_overhead = f64::from(car.door_transition_ticks() * 2 + car.door_open_ticks());
let penalty = self.stop_penalty.unwrap_or_else(|| door_overhead.max(1.0));
let pickup_dist = (car_pos - origin_pos).abs();
let pickup_travel = pickup_dist / car.max_speed().value();
let intervening_committed = world.destination_queue(eid).map_or(0usize, |q| {
let (lo, hi) = if car_pos < origin_pos {
(car_pos, origin_pos)
} else {
(origin_pos, car_pos)
};
q.queue()
.iter()
.filter_map(|s| world.stop_position(*s))
.filter(|p| *p > lo + 1e-9 && *p < hi - 1e-9)
.count()
});
let pickup_time = (intervening_committed as f64).mul_add(door_overhead, pickup_travel);
let ride_dist = (origin_pos - dest_pos).abs();
let ride_time = ride_dist / car.max_speed().value() + door_overhead;
let queue_contains = |s: EntityId| {
world
.destination_queue(eid)
.is_some_and(|q| q.queue().contains(&s))
};
let mut new_stops = 0f64;
if !queue_contains(origin) {
new_stops += 1.0;
}
if dest != origin && !queue_contains(dest) {
new_stops += 1.0;
}
let idle_bonus = if car.phase() == ElevatorPhase::Idle && car.riders().is_empty() {
-0.1 * pickup_travel
} else {
0.0
};
let load_penalty = if car.weight_capacity().value() > 0.0 {
let effective = car.current_load().value().max(committed_load);
let ratio = (effective / car.weight_capacity().value()).min(2.0);
ratio * door_overhead * 4.0
} else {
0.0
};
pickup_time + ride_time + penalty * new_stops + idle_bonus + load_penalty
}
}
fn assigned_car_within_window(
world: &crate::world::World,
rider: EntityId,
car: EntityId,
window: u64,
) -> bool {
let Some(leg) = world.route(rider).and_then(|r| r.current()) else {
return false;
};
let Some(origin_pos) = world.stop_position(leg.from) else {
return false;
};
let Some(car_pos) = world.position(car).map(|p| p.value) else {
return false;
};
let Some(car_data) = world.elevator(car) else {
return false;
};
let speed = car_data.max_speed().value();
if !speed.is_finite() || speed <= 0.0 {
return false;
}
let tick_rate = world
.resource::<crate::time::TickRate>()
.map_or(60.0, |r| r.0);
let eta_ticks = (car_pos - origin_pos).abs() / speed * tick_rate;
if !eta_ticks.is_finite() {
return false;
}
eta_ticks.round() as u64 <= window
}
pub fn clear_assignments_to(world: &mut crate::world::World, car_eid: EntityId) {
let stale: Vec<EntityId> = world
.ext_map::<AssignedCar>()
.map(|m| {
m.iter()
.filter_map(|(rid, AssignedCar(c))| (*c == car_eid).then_some(rid))
.collect()
})
.unwrap_or_default();
for rid in stale {
world.remove_ext::<AssignedCar>(rid);
}
}
#[allow(clippy::too_many_lines)]
fn rebuild_car_queue(world: &mut crate::world::World, car_eid: EntityId) {
use crate::components::RiderPhase;
struct Trip {
origin: Option<EntityId>,
dest: EntityId,
}
let Some(car) = world.elevator(car_eid) else {
return;
};
let car_pos = world.position(car_eid).map_or(0.0, |p| p.value);
let sweep_up = match car.direction() {
Direction::Up | Direction::Either => true,
Direction::Down => false,
};
let at_stop_loading: Option<EntityId> = {
let stopped_here = !matches!(
car.phase(),
ElevatorPhase::MovingToStop(_) | ElevatorPhase::Repositioning(_)
);
if stopped_here {
world.find_stop_at_position(car_pos)
} else {
None
}
};
let assigned_ids: Vec<EntityId> = world
.ext_map::<AssignedCar>()
.map(|m| {
m.iter()
.filter_map(|(rid, AssignedCar(c))| (*c == car_eid).then_some(rid))
.collect()
})
.unwrap_or_default();
let mut trips: Vec<Trip> = Vec::new();
for rid in assigned_ids {
let Some(rider) = world.rider(rid) else {
continue;
};
let Some(dest) = world
.route(rid)
.and_then(crate::components::Route::current_destination)
else {
continue;
};
match rider.phase() {
RiderPhase::Waiting => {
let origin = world
.route(rid)
.and_then(|r| r.current().map(|leg| leg.from));
let origin = origin.filter(|o| Some(*o) != at_stop_loading);
trips.push(Trip { origin, dest });
}
RiderPhase::Boarding(_) | RiderPhase::Riding(_) => {
trips.push(Trip { origin: None, dest });
}
_ => {}
}
}
if trips.is_empty() {
if let Some(q) = world.destination_queue_mut(car_eid) {
q.clear();
}
return;
}
let mut run1: Vec<(EntityId, f64)> = Vec::new();
let mut run2: Vec<(EntityId, f64)> = Vec::new();
let mut run3: Vec<(EntityId, f64)> = Vec::new();
let in_run1 = |sp: f64| -> bool {
if sweep_up {
sp >= car_pos - 1e-9
} else {
sp <= car_pos + 1e-9
}
};
let push_unique = |v: &mut Vec<(EntityId, f64)>, s: EntityId, p: f64| {
if !v.iter().any(|(e, _)| *e == s) {
v.push((s, p));
}
};
for trip in &trips {
let dp = world.stop_position(trip.dest).unwrap_or(car_pos);
if let Some(o) = trip.origin {
let op = world.stop_position(o).unwrap_or(car_pos);
let o_in_run1 = in_run1(op);
let d_in_run1 = in_run1(dp);
if o_in_run1 {
push_unique(&mut run1, o, op);
if d_in_run1 {
let d_fits = if sweep_up {
dp >= op - 1e-9
} else {
dp <= op + 1e-9
};
if d_fits {
push_unique(&mut run1, trip.dest, dp);
} else {
push_unique(&mut run2, trip.dest, dp);
}
} else {
push_unique(&mut run2, trip.dest, dp);
}
} else {
push_unique(&mut run2, o, op);
if d_in_run1 {
push_unique(&mut run3, trip.dest, dp);
} else {
let d_further = if sweep_up {
dp <= op + 1e-9
} else {
dp >= op - 1e-9
};
if d_further {
push_unique(&mut run2, trip.dest, dp);
} else {
push_unique(&mut run3, trip.dest, dp);
}
}
}
} else {
if in_run1(dp) {
push_unique(&mut run1, trip.dest, dp);
} else {
push_unique(&mut run2, trip.dest, dp);
}
}
}
if sweep_up {
run1.sort_by(|a, b| a.1.total_cmp(&b.1));
run2.sort_by(|a, b| b.1.total_cmp(&a.1));
run3.sort_by(|a, b| a.1.total_cmp(&b.1));
} else {
run1.sort_by(|a, b| b.1.total_cmp(&a.1));
run2.sort_by(|a, b| a.1.total_cmp(&b.1));
run3.sort_by(|a, b| b.1.total_cmp(&a.1));
}
let mut out: Vec<EntityId> = Vec::with_capacity(run1.len() + run2.len() + run3.len());
out.extend(run1.into_iter().map(|(e, _)| e));
out.extend(run2.into_iter().map(|(e, _)| e));
out.extend(run3.into_iter().map(|(e, _)| e));
let mut seen = HashSet::with_capacity(out.len());
out.retain(|e| seen.insert(*e));
if let Some(q) = world.destination_queue_mut(car_eid) {
q.replace(out);
}
}