1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
//! Phase 2: assign idle/stopped elevators to stops via the dispatch strategy.
use crate::components::{ElevatorPhase, RiderPhase, Route, TransportMode};
use crate::dispatch::{
DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup, RiderInfo,
};
use crate::entity::EntityId;
use crate::events::{Event, EventBus};
use crate::ids::GroupId;
use crate::rider_index::RiderIndex;
use crate::world::World;
use std::collections::BTreeMap;
use super::PhaseContext;
/// Assign idle/stopped elevators to stops via the dispatch strategy.
#[allow(clippy::too_many_lines)]
pub fn run(
world: &mut World,
events: &mut EventBus,
ctx: &PhaseContext,
groups: &[ElevatorGroup],
dispatchers: &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
rider_index: &RiderIndex,
) {
for group in groups {
let manifest = build_manifest(world, group, ctx.tick, rider_index);
// Give strategies a chance to mutate world state (e.g. write rider
// assignments to extension storage) before per-elevator decisions.
if let Some(dispatch) = dispatchers.get_mut(&group.id()) {
dispatch.pre_dispatch(group, &manifest, world);
}
// Collect idle elevators in this group.
let idle_elevators: Vec<(EntityId, f64)> = group
.elevator_entities()
.iter()
.filter_map(|eid| {
if world.is_disabled(*eid) {
return None;
}
// Skip elevators that opt out of automatic dispatch.
if world
.service_mode(*eid)
.is_some_and(|m| m.is_dispatch_excluded())
{
return None;
}
let car = world.elevator(*eid)?;
if matches!(car.phase, ElevatorPhase::Idle | ElevatorPhase::Stopped) {
let pos = world.position(*eid)?.value;
Some((*eid, pos))
} else {
None
}
})
.collect();
if idle_elevators.is_empty() {
continue;
}
let Some(dispatch) = dispatchers.get_mut(&group.id()) else {
continue;
};
let decisions = dispatch.decide_all(&idle_elevators, group, &manifest, world);
for (eid, decision) in decisions {
match decision {
DispatchDecision::GoToStop(stop_eid) => {
let pos = world.position(eid).map_or(0.0, |p| p.value);
let current_stop = world.find_stop_at_position(pos);
events.emit(Event::ElevatorAssigned {
elevator: eid,
stop: stop_eid,
tick: ctx.tick,
});
// Compute direction indicators from target vs current position.
let target_pos = world.stop_position(stop_eid).unwrap_or(pos);
let (new_up, new_down) = if target_pos > pos {
(true, false)
} else if target_pos < pos {
(false, true)
} else {
// At the target already — treat as idle (both lamps lit).
(true, true)
};
update_indicators(world, events, eid, new_up, new_down, ctx.tick);
// Already at this stop — open doors directly, don't push.
if current_stop == Some(stop_eid) {
// Pop the queue front if it equals this stop, mirroring
// the arrive-in-place branch of advance_queue.
if let Some(q) = world.destination_queue_mut(eid)
&& q.front() == Some(stop_eid)
{
q.pop_front();
}
events.emit(Event::ElevatorArrived {
elevator: eid,
at_stop: stop_eid,
tick: ctx.tick,
});
if let Some(car) = world.elevator_mut(eid) {
car.phase = ElevatorPhase::DoorOpening;
car.target_stop = Some(stop_eid);
car.door = crate::door::DoorState::request_open(
car.door_transition_ticks,
car.door_open_ticks,
);
}
continue;
}
// Push onto queue with adjacent dedup; emit event iff appended.
// Strategies with `pre_dispatch` (e.g. DestinationDispatch)
// may have already committed `stop_eid` to the queue —
// short-circuit to avoid a duplicate entry and a phantom
// `DestinationQueued` event.
if let Some(q) = world.destination_queue_mut(eid)
&& !q.contains(&stop_eid)
&& q.push_back(stop_eid)
{
events.emit(Event::DestinationQueued {
elevator: eid,
stop: stop_eid,
tick: ctx.tick,
});
}
if let Some(car) = world.elevator_mut(eid) {
car.phase = ElevatorPhase::MovingToStop(stop_eid);
car.target_stop = Some(stop_eid);
car.repositioning = false;
}
if let Some(from) = current_stop {
events.emit(Event::ElevatorDeparted {
elevator: eid,
from_stop: from,
tick: ctx.tick,
});
}
}
DispatchDecision::Idle => {
// Check if elevator was already idle before setting phase.
let was_idle = world
.elevator(eid)
.is_some_and(|car| car.phase == ElevatorPhase::Idle);
if let Some(car) = world.elevator_mut(eid) {
car.phase = ElevatorPhase::Idle;
}
// Reset indicators to both-lit when returning to idle.
update_indicators(world, events, eid, true, true, ctx.tick);
if !was_idle {
let at_stop = world
.position(eid)
.and_then(|p| world.find_stop_at_position(p.value));
events.emit(Event::ElevatorIdle {
elevator: eid,
at_stop,
tick: ctx.tick,
});
}
}
}
}
}
}
/// Update the direction indicator lamps on an elevator and emit a
/// [`Event::DirectionIndicatorChanged`] iff the pair actually changed.
///
/// Shared with `systems::advance_queue` so both dispatch- and
/// imperative-driven movement keep the indicators in sync.
pub fn update_indicators(
world: &mut World,
events: &mut EventBus,
eid: EntityId,
new_up: bool,
new_down: bool,
tick: u64,
) {
let Some(car) = world.elevator_mut(eid) else {
return;
};
if car.going_up == new_up && car.going_down == new_down {
return;
}
car.going_up = new_up;
car.going_down = new_down;
events.emit(Event::DirectionIndicatorChanged {
elevator: eid,
going_up: new_up,
going_down: new_down,
tick,
});
}
/// Build a dispatch manifest with per-rider metadata for a group.
fn build_manifest(
world: &World,
group: &ElevatorGroup,
tick: u64,
rider_index: &RiderIndex,
) -> DispatchManifest {
let mut manifest = DispatchManifest::default();
// Waiting riders at this group's stops.
for (rid, rider) in world.iter_riders() {
if world.is_disabled(rid) {
continue;
}
if rider.phase != RiderPhase::Waiting {
continue;
}
if let Some(stop) = rider.current_stop
&& group.stop_entities().contains(&stop)
{
// Group/line match: only include riders whose current route leg targets
// this group (or one of its lines). Mirrors the filter in systems/loading.rs
// so dispatch and loading agree about which riders this group can serve.
if let Some(route) = world.route(rid)
&& let Some(leg) = route.current()
{
match leg.via {
TransportMode::Group(g) => {
if g != group.id() {
continue;
}
}
TransportMode::Line(l) => {
if !group.lines().iter().any(|line| line.entity() == l) {
continue;
}
}
TransportMode::Walk => continue,
}
}
let destination = world.route(rid).and_then(Route::current_destination);
let wait_ticks = tick.saturating_sub(rider.spawn_tick);
manifest
.waiting_at_stop
.entry(stop)
.or_default()
.push(RiderInfo {
id: rid,
destination,
weight: rider.weight,
wait_ticks,
});
}
}
// Riders currently aboard this group's elevators, grouped by destination.
for &elev_eid in group.elevator_entities() {
if let Some(car) = world.elevator(elev_eid) {
for &rider_eid in car.riders() {
let destination = world.route(rider_eid).and_then(Route::current_destination);
if let Some(dest) = destination {
let rider = world.rider(rider_eid);
let weight = rider.map_or(0.0, |r| r.weight);
manifest
.riding_to_stop
.entry(dest)
.or_default()
.push(RiderInfo {
id: rider_eid,
destination: Some(dest),
weight,
wait_ticks: 0,
});
}
}
}
}
// Populate resident counts as read-only hints for dispatch strategies.
for &stop in group.stop_entities() {
let count = rider_index.resident_count_at(stop);
if count > 0 {
manifest.resident_count_at_stop.insert(stop, count);
}
}
manifest
}