mod construction;
mod lifecycle;
mod topology;
use crate::components::{
AccessControl, Orientation, Patience, Preferences, Rider, RiderPhase, Route, SpatialPosition,
Velocity,
};
use crate::dispatch::{BuiltinReposition, DispatchStrategy, ElevatorGroup, RepositionStrategy};
use crate::entity::EntityId;
use crate::error::{EtaError, SimError};
use crate::events::{Event, EventBus};
use crate::hooks::{Phase, PhaseHooks};
use crate::ids::GroupId;
use crate::metrics::Metrics;
use crate::rider_index::RiderIndex;
use crate::stop::{StopId, StopRef};
use crate::systems::PhaseContext;
use crate::time::TimeAdapter;
use crate::topology::TopologyGraph;
use crate::world::World;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt;
use std::sync::Mutex;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ElevatorParams {
pub max_speed: f64,
pub acceleration: f64,
pub deceleration: f64,
pub weight_capacity: f64,
pub door_transition_ticks: u32,
pub door_open_ticks: u32,
pub restricted_stops: HashSet<EntityId>,
pub inspection_speed_factor: f64,
}
impl Default for ElevatorParams {
fn default() -> Self {
Self {
max_speed: 2.0,
acceleration: 1.5,
deceleration: 2.0,
weight_capacity: 800.0,
door_transition_ticks: 5,
door_open_ticks: 10,
restricted_stops: HashSet::new(),
inspection_speed_factor: 0.25,
}
}
}
#[derive(Debug, Clone)]
pub struct LineParams {
pub name: String,
pub group: GroupId,
pub orientation: Orientation,
pub min_position: f64,
pub max_position: f64,
pub position: Option<SpatialPosition>,
pub max_cars: Option<usize>,
}
impl LineParams {
pub fn new(name: impl Into<String>, group: GroupId) -> Self {
Self {
name: name.into(),
group,
orientation: Orientation::default(),
min_position: 0.0,
max_position: 0.0,
position: None,
max_cars: None,
}
}
}
pub struct RiderBuilder<'a> {
sim: &'a mut Simulation,
origin: EntityId,
destination: EntityId,
weight: f64,
group: Option<GroupId>,
route: Option<Route>,
patience: Option<u64>,
preferences: Option<Preferences>,
access_control: Option<AccessControl>,
}
impl RiderBuilder<'_> {
#[must_use]
pub const fn weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
#[must_use]
pub const fn group(mut self, group: GroupId) -> Self {
self.group = Some(group);
self
}
#[must_use]
pub fn route(mut self, route: Route) -> Self {
self.route = Some(route);
self
}
#[must_use]
pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
self.patience = Some(max_wait_ticks);
self
}
#[must_use]
pub const fn preferences(mut self, prefs: Preferences) -> Self {
self.preferences = Some(prefs);
self
}
#[must_use]
pub fn access_control(mut self, ac: AccessControl) -> Self {
self.access_control = Some(ac);
self
}
pub fn spawn(self) -> Result<EntityId, SimError> {
let route = if let Some(route) = self.route {
if let Some(leg) = route.current()
&& leg.from != self.origin
{
return Err(SimError::RouteOriginMismatch {
expected_origin: self.origin,
route_origin: leg.from,
});
}
route
} else if let Some(group) = self.group {
if !self.sim.groups.iter().any(|g| g.id() == group) {
return Err(SimError::GroupNotFound(group));
}
Route::direct(self.origin, self.destination, group)
} else {
let matching: Vec<GroupId> = self
.sim
.groups
.iter()
.filter(|g| {
g.stop_entities().contains(&self.origin)
&& g.stop_entities().contains(&self.destination)
})
.map(ElevatorGroup::id)
.collect();
match matching.len() {
0 => {
let origin_groups: Vec<GroupId> = self
.sim
.groups
.iter()
.filter(|g| g.stop_entities().contains(&self.origin))
.map(ElevatorGroup::id)
.collect();
let destination_groups: Vec<GroupId> = self
.sim
.groups
.iter()
.filter(|g| g.stop_entities().contains(&self.destination))
.map(ElevatorGroup::id)
.collect();
return Err(SimError::NoRoute {
origin: self.origin,
destination: self.destination,
origin_groups,
destination_groups,
});
}
1 => Route::direct(self.origin, self.destination, matching[0]),
_ => {
return Err(SimError::AmbiguousRoute {
origin: self.origin,
destination: self.destination,
groups: matching,
});
}
}
};
let eid = self
.sim
.spawn_rider_inner(self.origin, self.destination, self.weight, route);
if let Some(max_wait) = self.patience {
self.sim.world.set_patience(
eid,
Patience {
max_wait_ticks: max_wait,
waited_ticks: 0,
},
);
}
if let Some(prefs) = self.preferences {
self.sim.world.set_preferences(eid, prefs);
}
if let Some(ac) = self.access_control {
self.sim.world.set_access_control(eid, ac);
}
Ok(eid)
}
}
pub struct Simulation {
world: World,
events: EventBus,
pending_output: Vec<Event>,
tick: u64,
dt: f64,
groups: Vec<ElevatorGroup>,
stop_lookup: HashMap<StopId, EntityId>,
dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
metrics: Metrics,
time: TimeAdapter,
hooks: PhaseHooks,
elevator_ids_buf: Vec<EntityId>,
topo_graph: Mutex<TopologyGraph>,
rider_index: RiderIndex,
}
impl Simulation {
#[must_use]
#[allow(clippy::missing_const_for_fn)]
pub fn world(&self) -> &World {
&self.world
}
#[allow(clippy::missing_const_for_fn)]
pub fn world_mut(&mut self) -> &mut World {
&mut self.world
}
#[must_use]
pub const fn current_tick(&self) -> u64 {
self.tick
}
#[must_use]
pub const fn dt(&self) -> f64 {
self.dt
}
#[must_use]
pub fn position_at(&self, id: EntityId, alpha: f64) -> Option<f64> {
let current = self.world.position(id)?.value;
let alpha = if alpha.is_nan() {
0.0
} else {
alpha.clamp(0.0, 1.0)
};
let prev = self.world.prev_position(id).map_or(current, |p| p.value);
Some((current - prev).mul_add(alpha, prev))
}
#[must_use]
pub fn velocity(&self, id: EntityId) -> Option<f64> {
self.world.velocity(id).map(Velocity::value)
}
#[must_use]
pub const fn metrics(&self) -> &Metrics {
&self.metrics
}
#[must_use]
pub const fn time(&self) -> &TimeAdapter {
&self.time
}
#[must_use]
pub fn groups(&self) -> &[ElevatorGroup] {
&self.groups
}
pub fn groups_mut(&mut self) -> &mut [ElevatorGroup] {
&mut self.groups
}
#[must_use]
pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
self.stop_lookup.get(&id).copied()
}
fn resolve_stop(&self, stop: StopRef) -> Result<EntityId, SimError> {
match stop {
StopRef::ByEntity(id) => Ok(id),
StopRef::ById(sid) => self.stop_entity(sid).ok_or(SimError::StopNotFound(sid)),
}
}
#[must_use]
pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
self.strategy_ids.get(&group)
}
pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
self.stop_lookup.iter()
}
#[must_use]
pub fn pending_events(&self) -> &[Event] {
&self.pending_output
}
#[must_use]
pub fn destination_queue(&self, elev: EntityId) -> Option<&[EntityId]> {
self.world
.destination_queue(elev)
.map(crate::components::DestinationQueue::queue)
}
pub fn push_destination(
&mut self,
elev: EntityId,
stop: impl Into<StopRef>,
) -> Result<(), SimError> {
let stop = self.resolve_stop(stop.into())?;
self.validate_push_targets(elev, stop)?;
let appended = self
.world
.destination_queue_mut(elev)
.is_some_and(|q| q.push_back(stop));
if appended {
self.events.emit(Event::DestinationQueued {
elevator: elev,
stop,
tick: self.tick,
});
}
Ok(())
}
pub fn push_destination_front(
&mut self,
elev: EntityId,
stop: impl Into<StopRef>,
) -> Result<(), SimError> {
let stop = self.resolve_stop(stop.into())?;
self.validate_push_targets(elev, stop)?;
let inserted = self
.world
.destination_queue_mut(elev)
.is_some_and(|q| q.push_front(stop));
if inserted {
self.events.emit(Event::DestinationQueued {
elevator: elev,
stop,
tick: self.tick,
});
}
Ok(())
}
pub fn clear_destinations(&mut self, elev: EntityId) -> Result<(), SimError> {
if self.world.elevator(elev).is_none() {
return Err(SimError::NotAnElevator(elev));
}
if let Some(q) = self.world.destination_queue_mut(elev) {
q.clear();
}
Ok(())
}
fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
if self.world.elevator(elev).is_none() {
return Err(SimError::NotAnElevator(elev));
}
if self.world.stop(stop).is_none() {
return Err(SimError::NotAStop(stop));
}
Ok(())
}
pub fn eta(&self, elev: EntityId, stop: EntityId) -> Result<Duration, EtaError> {
let elevator = self
.world
.elevator(elev)
.ok_or(EtaError::NotAnElevator(elev))?;
self.world.stop(stop).ok_or(EtaError::NotAStop(stop))?;
let svc = self.world.service_mode(elev).copied().unwrap_or_default();
if svc.is_dispatch_excluded() {
return Err(EtaError::ServiceModeExcluded(elev));
}
let mut route: Vec<EntityId> = Vec::new();
if let Some(t) = elevator.phase().moving_target() {
route.push(t);
}
if let Some(q) = self.world.destination_queue(elev) {
for &s in q.queue() {
if route.last() != Some(&s) {
route.push(s);
}
}
}
if !route.contains(&stop) {
return Err(EtaError::StopNotQueued {
elevator: elev,
stop,
});
}
let max_speed = elevator.max_speed();
let accel = elevator.acceleration();
let decel = elevator.deceleration();
let door_cycle_ticks =
u64::from(elevator.door_transition_ticks()) * 2 + u64::from(elevator.door_open_ticks());
let door_cycle_secs = (door_cycle_ticks as f64) * self.dt;
let mut total = match elevator.door() {
crate::door::DoorState::Opening {
ticks_remaining,
open_duration,
close_duration,
} => f64::from(*ticks_remaining + *open_duration + *close_duration) * self.dt,
crate::door::DoorState::Open {
ticks_remaining,
close_duration,
} => f64::from(*ticks_remaining + *close_duration) * self.dt,
crate::door::DoorState::Closing { ticks_remaining } => {
f64::from(*ticks_remaining) * self.dt
}
crate::door::DoorState::Closed => 0.0,
};
let in_door_cycle = !matches!(elevator.door(), crate::door::DoorState::Closed);
let mut pos = self
.world
.position(elev)
.ok_or(EtaError::NotAnElevator(elev))?
.value;
let vel_signed = self.world.velocity(elev).map_or(0.0, Velocity::value);
for (idx, &s) in route.iter().enumerate() {
let s_pos = self
.world
.stop_position(s)
.ok_or(EtaError::StopVanished(s))?;
let dist = (s_pos - pos).abs();
let v0 = if idx == 0 && !in_door_cycle && vel_signed.abs() > f64::EPSILON {
let dir = (s_pos - pos).signum();
if dir * vel_signed > 0.0 {
vel_signed.abs()
} else {
0.0
}
} else {
0.0
};
total += crate::eta::travel_time(dist, v0, max_speed, accel, decel);
if s == stop {
return Ok(Duration::from_secs_f64(total.max(0.0)));
}
total += door_cycle_secs;
pos = s_pos;
}
Err(EtaError::StopNotQueued {
elevator: elev,
stop,
})
}
#[must_use]
pub fn best_eta(
&self,
stop: impl Into<StopRef>,
direction: crate::components::Direction,
) -> Option<(EntityId, Duration)> {
use crate::components::Direction;
let stop = self.resolve_stop(stop.into()).ok()?;
self.world
.iter_elevators()
.filter_map(|(eid, _, elev)| {
let car_dir = elev.direction();
let direction_ok = match direction {
Direction::Either => true,
requested => car_dir == Direction::Either || car_dir == requested,
};
if !direction_ok {
return None;
}
self.eta(eid, stop).ok().map(|d| (eid, d))
})
.min_by_key(|(_, d)| *d)
}
pub fn set_max_speed(&mut self, elevator: EntityId, speed: f64) -> Result<(), SimError> {
Self::validate_positive_finite_f64(speed, "elevators.max_speed")?;
let old = self.require_elevator(elevator)?.max_speed;
if let Some(car) = self.world.elevator_mut(elevator) {
car.max_speed = speed;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::MaxSpeed,
crate::events::UpgradeValue::float(old),
crate::events::UpgradeValue::float(speed),
);
Ok(())
}
pub fn set_acceleration(&mut self, elevator: EntityId, accel: f64) -> Result<(), SimError> {
Self::validate_positive_finite_f64(accel, "elevators.acceleration")?;
let old = self.require_elevator(elevator)?.acceleration;
if let Some(car) = self.world.elevator_mut(elevator) {
car.acceleration = accel;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::Acceleration,
crate::events::UpgradeValue::float(old),
crate::events::UpgradeValue::float(accel),
);
Ok(())
}
pub fn set_deceleration(&mut self, elevator: EntityId, decel: f64) -> Result<(), SimError> {
Self::validate_positive_finite_f64(decel, "elevators.deceleration")?;
let old = self.require_elevator(elevator)?.deceleration;
if let Some(car) = self.world.elevator_mut(elevator) {
car.deceleration = decel;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::Deceleration,
crate::events::UpgradeValue::float(old),
crate::events::UpgradeValue::float(decel),
);
Ok(())
}
pub fn set_weight_capacity(
&mut self,
elevator: EntityId,
capacity: f64,
) -> Result<(), SimError> {
Self::validate_positive_finite_f64(capacity, "elevators.weight_capacity")?;
let old = self.require_elevator(elevator)?.weight_capacity;
if let Some(car) = self.world.elevator_mut(elevator) {
car.weight_capacity = capacity;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::WeightCapacity,
crate::events::UpgradeValue::float(old),
crate::events::UpgradeValue::float(capacity),
);
Ok(())
}
pub fn set_door_transition_ticks(
&mut self,
elevator: EntityId,
ticks: u32,
) -> Result<(), SimError> {
Self::validate_nonzero_u32(ticks, "elevators.door_transition_ticks")?;
let old = self.require_elevator(elevator)?.door_transition_ticks;
if let Some(car) = self.world.elevator_mut(elevator) {
car.door_transition_ticks = ticks;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::DoorTransitionTicks,
crate::events::UpgradeValue::ticks(old),
crate::events::UpgradeValue::ticks(ticks),
);
Ok(())
}
pub fn set_door_open_ticks(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
Self::validate_nonzero_u32(ticks, "elevators.door_open_ticks")?;
let old = self.require_elevator(elevator)?.door_open_ticks;
if let Some(car) = self.world.elevator_mut(elevator) {
car.door_open_ticks = ticks;
}
self.emit_upgrade(
elevator,
crate::events::UpgradeField::DoorOpenTicks,
crate::events::UpgradeValue::ticks(old),
crate::events::UpgradeValue::ticks(ticks),
);
Ok(())
}
pub fn open_door(&mut self, elevator: EntityId) -> Result<(), SimError> {
self.require_enabled_elevator(elevator)?;
self.enqueue_door_command(elevator, crate::door::DoorCommand::Open);
Ok(())
}
pub fn close_door(&mut self, elevator: EntityId) -> Result<(), SimError> {
self.require_enabled_elevator(elevator)?;
self.enqueue_door_command(elevator, crate::door::DoorCommand::Close);
Ok(())
}
pub fn hold_door(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
Self::validate_nonzero_u32(ticks, "hold_door.ticks")?;
self.require_enabled_elevator(elevator)?;
self.enqueue_door_command(elevator, crate::door::DoorCommand::HoldOpen { ticks });
Ok(())
}
pub fn cancel_door_hold(&mut self, elevator: EntityId) -> Result<(), SimError> {
self.require_enabled_elevator(elevator)?;
self.enqueue_door_command(elevator, crate::door::DoorCommand::CancelHold);
Ok(())
}
pub fn set_target_velocity(
&mut self,
elevator: EntityId,
velocity: f64,
) -> Result<(), SimError> {
self.require_enabled_elevator(elevator)?;
self.require_manual_mode(elevator)?;
if !velocity.is_finite() {
return Err(SimError::InvalidConfig {
field: "target_velocity",
reason: format!("must be finite, got {velocity}"),
});
}
let max = self
.world
.elevator(elevator)
.map_or(f64::INFINITY, |c| c.max_speed);
let clamped = velocity.clamp(-max, max);
if let Some(car) = self.world.elevator_mut(elevator) {
car.manual_target_velocity = Some(clamped);
}
self.events.emit(Event::ManualVelocityCommanded {
elevator,
target_velocity: Some(ordered_float::OrderedFloat(clamped)),
tick: self.tick,
});
Ok(())
}
pub fn emergency_stop(&mut self, elevator: EntityId) -> Result<(), SimError> {
self.require_enabled_elevator(elevator)?;
self.require_manual_mode(elevator)?;
if let Some(car) = self.world.elevator_mut(elevator) {
car.manual_target_velocity = Some(0.0);
}
self.events.emit(Event::ManualVelocityCommanded {
elevator,
target_velocity: None,
tick: self.tick,
});
Ok(())
}
fn require_manual_mode(&self, elevator: EntityId) -> Result<(), SimError> {
let actual = self
.world
.service_mode(elevator)
.copied()
.unwrap_or_default();
if actual != crate::components::ServiceMode::Manual {
return Err(SimError::WrongServiceMode {
entity: elevator,
expected: crate::components::ServiceMode::Manual,
actual,
});
}
Ok(())
}
fn enqueue_door_command(&mut self, elevator: EntityId, command: crate::door::DoorCommand) {
if let Some(car) = self.world.elevator_mut(elevator) {
let q = &mut car.door_command_queue;
let collapse = matches!(
command,
crate::door::DoorCommand::Open
| crate::door::DoorCommand::Close
| crate::door::DoorCommand::CancelHold
) && q.last().copied() == Some(command);
if !collapse {
q.push(command);
if q.len() > crate::components::DOOR_COMMAND_QUEUE_CAP {
q.remove(0);
}
}
}
self.events.emit(Event::DoorCommandQueued {
elevator,
command,
tick: self.tick,
});
}
fn require_enabled_elevator(&self, elevator: EntityId) -> Result<(), SimError> {
if self.world.elevator(elevator).is_none() {
return Err(SimError::NotAnElevator(elevator));
}
if self.world.is_disabled(elevator) {
return Err(SimError::ElevatorDisabled(elevator));
}
Ok(())
}
fn require_elevator(
&self,
elevator: EntityId,
) -> Result<&crate::components::Elevator, SimError> {
self.world
.elevator(elevator)
.ok_or(SimError::NotAnElevator(elevator))
}
fn validate_positive_finite_f64(value: f64, field: &'static str) -> Result<(), SimError> {
if !value.is_finite() {
return Err(SimError::InvalidConfig {
field,
reason: format!("must be finite, got {value}"),
});
}
if value <= 0.0 {
return Err(SimError::InvalidConfig {
field,
reason: format!("must be positive, got {value}"),
});
}
Ok(())
}
fn validate_nonzero_u32(value: u32, field: &'static str) -> Result<(), SimError> {
if value == 0 {
return Err(SimError::InvalidConfig {
field,
reason: "must be > 0".into(),
});
}
Ok(())
}
fn emit_upgrade(
&mut self,
elevator: EntityId,
field: crate::events::UpgradeField,
old: crate::events::UpgradeValue,
new: crate::events::UpgradeValue,
) {
self.events.emit(Event::ElevatorUpgraded {
elevator,
field,
old,
new,
tick: self.tick,
});
}
pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) -> Result<(), SimError> {
if !self.world.is_alive(id) {
return Err(SimError::EntityNotFound(id));
}
if let Some(tags) = self
.world
.resource_mut::<crate::tagged_metrics::MetricTags>()
{
tags.tag(id, tag);
}
Ok(())
}
pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
if let Some(tags) = self
.world
.resource_mut::<crate::tagged_metrics::MetricTags>()
{
tags.untag(id, tag);
}
}
#[must_use]
pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
self.world
.resource::<crate::tagged_metrics::MetricTags>()
.and_then(|tags| tags.metric(tag))
}
pub fn all_tags(&self) -> Vec<&str> {
self.world
.resource::<crate::tagged_metrics::MetricTags>()
.map_or_else(Vec::new, |tags| tags.all_tags().collect())
}
pub fn build_rider(
&mut self,
origin: impl Into<StopRef>,
destination: impl Into<StopRef>,
) -> Result<RiderBuilder<'_>, SimError> {
let origin = self.resolve_stop(origin.into())?;
let destination = self.resolve_stop(destination.into())?;
Ok(RiderBuilder {
sim: self,
origin,
destination,
weight: 75.0,
group: None,
route: None,
patience: None,
preferences: None,
access_control: None,
})
}
pub fn spawn_rider(
&mut self,
origin: impl Into<StopRef>,
destination: impl Into<StopRef>,
weight: f64,
) -> Result<EntityId, SimError> {
let origin = self.resolve_stop(origin.into())?;
let destination = self.resolve_stop(destination.into())?;
let matching: Vec<GroupId> = self
.groups
.iter()
.filter(|g| {
g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
})
.map(ElevatorGroup::id)
.collect();
let group = match matching.len() {
0 => {
let origin_groups: Vec<GroupId> = self
.groups
.iter()
.filter(|g| g.stop_entities().contains(&origin))
.map(ElevatorGroup::id)
.collect();
let destination_groups: Vec<GroupId> = self
.groups
.iter()
.filter(|g| g.stop_entities().contains(&destination))
.map(ElevatorGroup::id)
.collect();
return Err(SimError::NoRoute {
origin,
destination,
origin_groups,
destination_groups,
});
}
1 => matching[0],
_ => {
return Err(SimError::AmbiguousRoute {
origin,
destination,
groups: matching,
});
}
};
let route = Route::direct(origin, destination, group);
Ok(self.spawn_rider_inner(origin, destination, weight, route))
}
fn spawn_rider_inner(
&mut self,
origin: EntityId,
destination: EntityId,
weight: f64,
route: Route,
) -> EntityId {
let eid = self.world.spawn();
self.world.set_rider(
eid,
Rider {
weight,
phase: RiderPhase::Waiting,
current_stop: Some(origin),
spawn_tick: self.tick,
board_tick: None,
},
);
self.world.set_route(eid, route);
self.rider_index.insert_waiting(origin, eid);
self.events.emit(Event::RiderSpawned {
rider: eid,
origin,
destination,
tick: self.tick,
});
if let (Some(op), Some(dp)) = (
self.world.stop_position(origin),
self.world.stop_position(destination),
) && let Some(direction) = crate::components::CallDirection::between(op, dp)
{
self.register_hall_call_for_rider(origin, direction, eid, destination);
}
let stop_tag = self
.world
.stop(origin)
.map(|s| format!("stop:{}", s.name()));
if let Some(tags_res) = self
.world
.resource_mut::<crate::tagged_metrics::MetricTags>()
{
let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
for tag in origin_tags {
tags_res.tag(eid, tag);
}
if let Some(tag) = stop_tag {
tags_res.tag(eid, tag);
}
}
eid
}
pub fn drain_events(&mut self) -> Vec<Event> {
self.pending_output.extend(self.events.drain());
std::mem::take(&mut self.pending_output)
}
pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
self.pending_output.extend(self.events.drain());
let mut matched = Vec::new();
let mut remaining = Vec::new();
for event in std::mem::take(&mut self.pending_output) {
if predicate(&event) {
matched.push(event);
} else {
remaining.push(event);
}
}
self.pending_output = remaining;
matched
}
#[must_use]
pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
&self.dispatchers
}
pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
&mut self.dispatchers
}
pub const fn events_mut(&mut self) -> &mut EventBus {
&mut self.events
}
pub const fn metrics_mut(&mut self) -> &mut Metrics {
&mut self.metrics
}
#[must_use]
pub const fn phase_context(&self) -> PhaseContext {
PhaseContext {
tick: self.tick,
dt: self.dt,
}
}
pub fn run_advance_transient(&mut self) {
self.hooks
.run_before(Phase::AdvanceTransient, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
}
let ctx = self.phase_context();
crate::systems::advance_transient::run(
&mut self.world,
&mut self.events,
&ctx,
&mut self.rider_index,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
}
self.hooks
.run_after(Phase::AdvanceTransient, &mut self.world);
}
pub fn run_dispatch(&mut self) {
self.hooks.run_before(Phase::Dispatch, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::Dispatch, group.id(), &mut self.world);
}
let ctx = self.phase_context();
crate::systems::dispatch::run(
&mut self.world,
&mut self.events,
&ctx,
&self.groups,
&mut self.dispatchers,
&self.rider_index,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::Dispatch, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::Dispatch, &mut self.world);
}
pub fn run_movement(&mut self) {
self.hooks.run_before(Phase::Movement, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::Movement, group.id(), &mut self.world);
}
let ctx = self.phase_context();
self.world.elevator_ids_into(&mut self.elevator_ids_buf);
crate::systems::movement::run(
&mut self.world,
&mut self.events,
&ctx,
&self.elevator_ids_buf,
&mut self.metrics,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::Movement, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::Movement, &mut self.world);
}
pub fn run_doors(&mut self) {
self.hooks.run_before(Phase::Doors, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::Doors, group.id(), &mut self.world);
}
let ctx = self.phase_context();
self.world.elevator_ids_into(&mut self.elevator_ids_buf);
crate::systems::doors::run(
&mut self.world,
&mut self.events,
&ctx,
&self.elevator_ids_buf,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::Doors, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::Doors, &mut self.world);
}
pub fn run_loading(&mut self) {
self.hooks.run_before(Phase::Loading, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::Loading, group.id(), &mut self.world);
}
let ctx = self.phase_context();
self.world.elevator_ids_into(&mut self.elevator_ids_buf);
crate::systems::loading::run(
&mut self.world,
&mut self.events,
&ctx,
&self.elevator_ids_buf,
&mut self.rider_index,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::Loading, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::Loading, &mut self.world);
}
pub fn run_advance_queue(&mut self) {
self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
}
let ctx = self.phase_context();
self.world.elevator_ids_into(&mut self.elevator_ids_buf);
crate::systems::advance_queue::run(
&mut self.world,
&mut self.events,
&ctx,
&self.elevator_ids_buf,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
}
pub fn run_reposition(&mut self) {
if self.repositioners.is_empty() {
return;
}
self.hooks.run_before(Phase::Reposition, &mut self.world);
for group in &self.groups {
if self.repositioners.contains_key(&group.id()) {
self.hooks
.run_before_group(Phase::Reposition, group.id(), &mut self.world);
}
}
let ctx = self.phase_context();
crate::systems::reposition::run(
&mut self.world,
&mut self.events,
&ctx,
&self.groups,
&mut self.repositioners,
);
for group in &self.groups {
if self.repositioners.contains_key(&group.id()) {
self.hooks
.run_after_group(Phase::Reposition, group.id(), &mut self.world);
}
}
self.hooks.run_after(Phase::Reposition, &mut self.world);
}
#[cfg(feature = "energy")]
fn run_energy(&mut self) {
let ctx = self.phase_context();
self.world.elevator_ids_into(&mut self.elevator_ids_buf);
crate::systems::energy::run(
&mut self.world,
&mut self.events,
&ctx,
&self.elevator_ids_buf,
);
}
pub fn run_metrics(&mut self) {
self.hooks.run_before(Phase::Metrics, &mut self.world);
for group in &self.groups {
self.hooks
.run_before_group(Phase::Metrics, group.id(), &mut self.world);
}
let ctx = self.phase_context();
crate::systems::metrics::run(
&mut self.world,
&self.events,
&mut self.metrics,
&ctx,
&self.groups,
);
for group in &self.groups {
self.hooks
.run_after_group(Phase::Metrics, group.id(), &mut self.world);
}
self.hooks.run_after(Phase::Metrics, &mut self.world);
}
pub fn advance_tick(&mut self) {
self.pending_output.extend(self.events.drain());
self.tick += 1;
}
pub fn step(&mut self) {
self.world.snapshot_prev_positions();
self.run_advance_transient();
self.run_dispatch();
self.run_reposition();
self.run_advance_queue();
self.run_movement();
self.run_doors();
self.run_loading();
#[cfg(feature = "energy")]
self.run_energy();
self.run_metrics();
self.advance_tick();
}
pub fn press_hall_button(
&mut self,
stop: impl Into<StopRef>,
direction: crate::components::CallDirection,
) -> Result<(), SimError> {
let stop = self.resolve_stop(stop.into())?;
if self.world.stop(stop).is_none() {
return Err(SimError::EntityNotFound(stop));
}
self.ensure_hall_call(stop, direction, None, None);
Ok(())
}
pub fn press_car_button(
&mut self,
car: EntityId,
floor: impl Into<StopRef>,
) -> Result<(), SimError> {
let floor = self.resolve_stop(floor.into())?;
if self.world.elevator(car).is_none() {
return Err(SimError::EntityNotFound(car));
}
if self.world.stop(floor).is_none() {
return Err(SimError::EntityNotFound(floor));
}
self.ensure_car_call(car, floor, None);
Ok(())
}
pub fn pin_assignment(
&mut self,
car: EntityId,
stop: EntityId,
direction: crate::components::CallDirection,
) -> Result<(), SimError> {
let Some(elev) = self.world.elevator(car) else {
return Err(SimError::EntityNotFound(car));
};
let car_line = elev.line;
let line_serves_stop = self
.groups
.iter()
.flat_map(|g| g.lines().iter())
.find(|li| li.entity() == car_line)
.map(|li| li.serves().contains(&stop));
if line_serves_stop == Some(false) {
return Err(SimError::LineDoesNotServeStop {
line_or_car: car,
stop,
});
}
let Some(call) = self.world.hall_call_mut(stop, direction) else {
return Err(SimError::HallCallNotFound { stop, direction });
};
call.assigned_car = Some(car);
call.pinned = true;
Ok(())
}
pub fn unpin_assignment(
&mut self,
stop: EntityId,
direction: crate::components::CallDirection,
) {
if let Some(call) = self.world.hall_call_mut(stop, direction) {
call.pinned = false;
}
}
pub fn hall_calls(&self) -> impl Iterator<Item = &crate::components::HallCall> {
self.world.iter_hall_calls()
}
#[must_use]
pub fn car_calls(&self, car: EntityId) -> &[crate::components::CarCall] {
self.world.car_calls(car)
}
#[must_use]
pub fn assigned_car(
&self,
stop: EntityId,
direction: crate::components::CallDirection,
) -> Option<EntityId> {
self.world
.hall_call(stop, direction)
.and_then(|c| c.assigned_car)
}
pub fn eta_for_call(
&self,
stop: EntityId,
direction: crate::components::CallDirection,
) -> Result<u64, EtaError> {
let call = self
.world
.hall_call(stop, direction)
.ok_or(EtaError::NotAStop(stop))?;
let car = call.assigned_car.ok_or(EtaError::NoCarAssigned(stop))?;
let car_pos = self
.world
.position(car)
.ok_or(EtaError::NotAnElevator(car))?
.value;
let stop_pos = self
.world
.stop_position(stop)
.ok_or(EtaError::StopVanished(stop))?;
let max_speed = self
.world
.elevator(car)
.ok_or(EtaError::NotAnElevator(car))?
.max_speed();
if max_speed <= 0.0 {
return Err(EtaError::NotAnElevator(car));
}
let distance = (car_pos - stop_pos).abs();
Ok((distance / max_speed).ceil() as u64)
}
fn register_hall_call_for_rider(
&mut self,
stop: EntityId,
direction: crate::components::CallDirection,
rider: EntityId,
destination: EntityId,
) {
let mode = self
.groups
.iter()
.find(|g| g.stop_entities().contains(&stop))
.map(crate::dispatch::ElevatorGroup::hall_call_mode);
let dest = match mode {
Some(crate::dispatch::HallCallMode::Destination) => Some(destination),
_ => None,
};
self.ensure_hall_call(stop, direction, Some(rider), dest);
}
fn ensure_hall_call(
&mut self,
stop: EntityId,
direction: crate::components::CallDirection,
rider: Option<EntityId>,
destination: Option<EntityId>,
) {
let mut fresh_press = false;
if self.world.hall_call(stop, direction).is_none() {
let mut call = crate::components::HallCall::new(stop, direction, self.tick);
call.destination = destination;
call.ack_latency_ticks = self.ack_latency_for_stop(stop);
if call.ack_latency_ticks == 0 {
call.acknowledged_at = Some(self.tick);
}
if let Some(rid) = rider {
call.pending_riders.push(rid);
}
self.world.set_hall_call(call);
fresh_press = true;
} else if let Some(existing) = self.world.hall_call_mut(stop, direction) {
if let Some(rid) = rider
&& !existing.pending_riders.contains(&rid)
{
existing.pending_riders.push(rid);
}
if existing.destination.is_none() {
existing.destination = destination;
}
}
if fresh_press {
self.events.emit(Event::HallButtonPressed {
stop,
direction,
tick: self.tick,
});
if let Some(call) = self.world.hall_call(stop, direction)
&& call.acknowledged_at == Some(self.tick)
{
self.events.emit(Event::HallCallAcknowledged {
stop,
direction,
tick: self.tick,
});
}
}
}
fn ack_latency_for(
&self,
entity: EntityId,
members: impl Fn(&crate::dispatch::ElevatorGroup) -> &[EntityId],
) -> u32 {
self.groups
.iter()
.find(|g| members(g).contains(&entity))
.map_or(0, crate::dispatch::ElevatorGroup::ack_latency_ticks)
}
fn ack_latency_for_stop(&self, stop: EntityId) -> u32 {
self.ack_latency_for(stop, crate::dispatch::ElevatorGroup::stop_entities)
}
fn ack_latency_for_car(&self, car: EntityId) -> u32 {
self.ack_latency_for(car, crate::dispatch::ElevatorGroup::elevator_entities)
}
fn ensure_car_call(&mut self, car: EntityId, floor: EntityId, rider: Option<EntityId>) {
let press_tick = self.tick;
let ack_latency = self.ack_latency_for_car(car);
let Some(queue) = self.world.car_calls_mut(car) else {
return;
};
let existing_idx = queue.iter().position(|c| c.floor == floor);
let fresh = existing_idx.is_none();
if let Some(idx) = existing_idx {
if let Some(rid) = rider
&& !queue[idx].pending_riders.contains(&rid)
{
queue[idx].pending_riders.push(rid);
}
} else {
let mut call = crate::components::CarCall::new(car, floor, press_tick);
call.ack_latency_ticks = ack_latency;
if ack_latency == 0 {
call.acknowledged_at = Some(press_tick);
}
if let Some(rid) = rider {
call.pending_riders.push(rid);
}
queue.push(call);
}
if fresh {
self.events.emit(Event::CarButtonPressed {
car,
floor,
rider,
tick: press_tick,
});
}
}
}
impl fmt::Debug for Simulation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Simulation")
.field("tick", &self.tick)
.field("dt", &self.dt)
.field("groups", &self.groups.len())
.field("entities", &self.world.entity_count())
.finish_non_exhaustive()
}
}