1use serde::{Deserialize, Serialize};
28
29use crate::components::{DestinationQueue, Direction, ElevatorPhase, TransportMode};
30use crate::entity::EntityId;
31use crate::world::World;
32
33use super::{DispatchDecision, DispatchManifest, DispatchStrategy, ElevatorGroup};
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub struct AssignedCar(pub EntityId);
43
44pub const ASSIGNED_CAR_EXT_NAME: &str = "assigned_car";
47
48pub struct DestinationDispatch {
59 stop_penalty: Option<f64>,
66}
67
68impl DestinationDispatch {
69 #[must_use]
71 pub const fn new() -> Self {
72 Self { stop_penalty: None }
73 }
74
75 #[must_use]
78 pub const fn with_stop_penalty(mut self, penalty: f64) -> Self {
79 self.stop_penalty = Some(penalty);
80 self
81 }
82}
83
84impl Default for DestinationDispatch {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90impl DispatchStrategy for DestinationDispatch {
91 fn pre_dispatch(
92 &mut self,
93 group: &ElevatorGroup,
94 manifest: &DispatchManifest,
95 world: &mut World,
96 ) {
97 let candidate_cars: Vec<EntityId> = group
99 .elevator_entities()
100 .iter()
101 .copied()
102 .filter(|eid| !world.is_disabled(*eid))
103 .filter(|eid| {
104 !world
105 .service_mode(*eid)
106 .is_some_and(|m| m.is_dispatch_excluded())
107 })
108 .filter(|eid| world.elevator(*eid).is_some())
109 .collect();
110
111 if candidate_cars.is_empty() {
112 return;
113 }
114
115 let mut pending: Vec<(EntityId, EntityId, EntityId, f64)> = Vec::new();
117 for riders in manifest.waiting_at_stop.values() {
118 for info in riders {
119 if world.get_ext::<AssignedCar>(info.id).is_some() {
120 continue; }
122 let Some(dest) = info.destination else {
123 continue;
124 };
125 let Some(route) = world.route(info.id) else {
126 continue;
127 };
128 let Some(leg) = route.current() else {
129 continue;
130 };
131 let group_ok = match leg.via {
132 TransportMode::Group(g) => g == group.id(),
133 TransportMode::Line(l) => group.lines().iter().any(|li| li.entity() == l),
134 TransportMode::Walk => false,
135 };
136 if !group_ok {
137 continue;
138 }
139 pending.push((info.id, leg.from, dest, info.weight));
140 }
141 }
142 pending.sort_by_key(|(rid, ..)| *rid);
143
144 let mut committed_load: std::collections::BTreeMap<EntityId, f64> =
148 std::collections::BTreeMap::new();
149 for (rid, rider) in world.iter_riders() {
150 use crate::components::RiderPhase;
151 let car = match rider.phase() {
158 RiderPhase::Riding(c) | RiderPhase::Boarding(c) => Some(c),
159 RiderPhase::Waiting => world.get_ext::<AssignedCar>(rid).map(|AssignedCar(c)| c),
160 _ => None,
161 };
162 if let Some(c) = car {
163 *committed_load.entry(c).or_insert(0.0) += rider.weight;
164 }
165 }
166
167 for (rid, origin, dest, weight) in pending {
168 let best = candidate_cars
169 .iter()
170 .filter_map(|&eid| {
171 let car = world.elevator(eid)?;
172 if car.restricted_stops().contains(&dest)
173 || car.restricted_stops().contains(&origin)
174 {
175 return None;
176 }
177 if car.weight_capacity() > 0.0 && weight > car.weight_capacity() {
178 return None;
179 }
180 let com = committed_load.get(&eid).copied().unwrap_or(0.0);
181 let cost = self.compute_cost(eid, origin, dest, world, com);
182 if cost.is_finite() {
183 Some((eid, cost))
184 } else {
185 None
186 }
187 })
188 .min_by(|a, b| a.1.total_cmp(&b.1))
189 .map(|(eid, _)| eid);
190
191 let Some(car_eid) = best else {
192 continue;
193 };
194 world.insert_ext(rid, AssignedCar(car_eid), ASSIGNED_CAR_EXT_NAME);
195 *committed_load.entry(car_eid).or_insert(0.0) += weight;
196 }
197
198 for &car_eid in &candidate_cars {
203 rebuild_car_queue(world, car_eid);
204 }
205 }
206
207 fn decide(
208 &mut self,
209 elevator: EntityId,
210 _elevator_position: f64,
211 _group: &ElevatorGroup,
212 _manifest: &DispatchManifest,
213 world: &World,
214 ) -> DispatchDecision {
215 world
217 .destination_queue(elevator)
218 .and_then(DestinationQueue::front)
219 .map_or(DispatchDecision::Idle, DispatchDecision::GoToStop)
220 }
221}
222
223impl DestinationDispatch {
224 fn compute_cost(
227 &self,
228 eid: EntityId,
229 origin: EntityId,
230 dest: EntityId,
231 world: &World,
232 committed_load: f64,
233 ) -> f64 {
234 let Some(car) = world.elevator(eid) else {
235 return f64::INFINITY;
236 };
237 if car.max_speed() <= 0.0 {
238 return f64::INFINITY;
239 }
240
241 let Some(car_pos) = world.position(eid).map(|p| p.value) else {
242 return f64::INFINITY;
243 };
244 let Some(origin_pos) = world.stop_position(origin) else {
245 return f64::INFINITY;
246 };
247 let Some(dest_pos) = world.stop_position(dest) else {
248 return f64::INFINITY;
249 };
250
251 let door_overhead = f64::from(car.door_transition_ticks() * 2 + car.door_open_ticks());
252 let penalty = self.stop_penalty.unwrap_or_else(|| door_overhead.max(1.0));
253
254 let pickup_dist = (car_pos - origin_pos).abs();
257 let pickup_travel = pickup_dist / car.max_speed();
258 let intervening_committed = world.destination_queue(eid).map_or(0usize, |q| {
259 let (lo, hi) = if car_pos < origin_pos {
260 (car_pos, origin_pos)
261 } else {
262 (origin_pos, car_pos)
263 };
264 q.queue()
265 .iter()
266 .filter_map(|s| world.stop_position(*s))
267 .filter(|p| *p > lo + 1e-9 && *p < hi - 1e-9)
268 .count()
269 });
270 let pickup_time = (intervening_committed as f64).mul_add(door_overhead, pickup_travel);
271
272 let ride_dist = (origin_pos - dest_pos).abs();
274 let ride_time = ride_dist / car.max_speed() + door_overhead;
275
276 let existing: Vec<EntityId> = world
279 .destination_queue(eid)
280 .map_or_else(Vec::new, |q| q.queue().to_vec());
281 let mut new_stops = 0f64;
282 if !existing.contains(&origin) {
283 new_stops += 1.0;
284 }
285 if !existing.contains(&dest) && dest != origin {
286 new_stops += 1.0;
287 }
288
289 let idle_bonus = if car.phase() == ElevatorPhase::Idle && car.riders().is_empty() {
291 -0.1 * pickup_travel
292 } else {
293 0.0
294 };
295
296 let load_penalty = if car.weight_capacity() > 0.0 {
299 let effective = car.current_load().max(committed_load);
300 let ratio = (effective / car.weight_capacity()).min(2.0);
301 ratio * door_overhead * 4.0
302 } else {
303 0.0
304 };
305
306 pickup_time + ride_time + penalty * new_stops + idle_bonus + load_penalty
307 }
308}
309
310#[allow(clippy::too_many_lines)]
323fn rebuild_car_queue(world: &mut crate::world::World, car_eid: EntityId) {
324 use crate::components::RiderPhase;
325
326 struct Trip {
328 origin: Option<EntityId>,
329 dest: EntityId,
330 }
331
332 let Some(car) = world.elevator(car_eid) else {
333 return;
334 };
335 let car_pos = world.position(car_eid).map_or(0.0, |p| p.value);
336 let sweep_up = match car.direction() {
337 Direction::Up | Direction::Either => true,
338 Direction::Down => false,
339 };
340
341 let at_stop_loading: Option<EntityId> = {
343 let stopped_here = !matches!(
344 car.phase(),
345 ElevatorPhase::MovingToStop(_) | ElevatorPhase::Repositioning(_)
346 );
347 if stopped_here {
348 world.find_stop_at_position(car_pos)
349 } else {
350 None
351 }
352 };
353
354 let mut trips: Vec<Trip> = Vec::new();
356 for (rid, rider) in world.iter_riders() {
357 let Some(AssignedCar(assigned)) = world.get_ext::<AssignedCar>(rid) else {
358 continue;
359 };
360 if assigned != car_eid {
361 continue;
362 }
363 let Some(dest) = world
364 .route(rid)
365 .and_then(crate::components::Route::current_destination)
366 else {
367 continue;
368 };
369 match rider.phase() {
370 RiderPhase::Waiting => {
371 let origin = world
372 .route(rid)
373 .and_then(|r| r.current().map(|leg| leg.from));
374 let origin = origin.filter(|o| Some(*o) != at_stop_loading);
376 trips.push(Trip { origin, dest });
377 }
378 RiderPhase::Boarding(_) | RiderPhase::Riding(_) => {
379 trips.push(Trip { origin: None, dest });
380 }
381 _ => {}
382 }
383 }
384
385 if trips.is_empty() {
386 if let Some(q) = world.destination_queue_mut(car_eid) {
387 q.clear();
388 }
389 return;
390 }
391
392 let mut run1: Vec<(EntityId, f64)> = Vec::new();
398 let mut run2: Vec<(EntityId, f64)> = Vec::new();
399 let mut run3: Vec<(EntityId, f64)> = Vec::new();
400
401 let in_run1 = |sp: f64| -> bool {
402 if sweep_up {
403 sp >= car_pos - 1e-9
404 } else {
405 sp <= car_pos + 1e-9
406 }
407 };
408
409 let push_unique = |v: &mut Vec<(EntityId, f64)>, s: EntityId, p: f64| {
410 if !v.iter().any(|(e, _)| *e == s) {
411 v.push((s, p));
412 }
413 };
414
415 for trip in &trips {
416 let dp = world.stop_position(trip.dest).unwrap_or(car_pos);
417 if let Some(o) = trip.origin {
418 let op = world.stop_position(o).unwrap_or(car_pos);
419 let o_in_run1 = in_run1(op);
420 let d_in_run1 = in_run1(dp);
421 if o_in_run1 {
422 push_unique(&mut run1, o, op);
423 if d_in_run1 {
424 let d_fits = if sweep_up {
426 dp >= op - 1e-9
427 } else {
428 dp <= op + 1e-9
429 };
430 if d_fits {
431 push_unique(&mut run1, trip.dest, dp);
432 } else {
433 push_unique(&mut run2, trip.dest, dp);
435 }
436 } else {
437 push_unique(&mut run2, trip.dest, dp);
438 }
439 } else {
440 push_unique(&mut run2, o, op);
442 if d_in_run1 {
443 push_unique(&mut run3, trip.dest, dp);
445 } else {
446 let d_further = if sweep_up {
449 dp <= op + 1e-9
450 } else {
451 dp >= op - 1e-9
452 };
453 if d_further {
454 push_unique(&mut run2, trip.dest, dp);
455 } else {
456 push_unique(&mut run3, trip.dest, dp);
457 }
458 }
459 }
460 } else {
461 if in_run1(dp) {
463 push_unique(&mut run1, trip.dest, dp);
464 } else {
465 push_unique(&mut run2, trip.dest, dp);
466 }
467 }
468 }
469
470 if sweep_up {
472 run1.sort_by(|a, b| a.1.total_cmp(&b.1));
473 run2.sort_by(|a, b| b.1.total_cmp(&a.1));
474 run3.sort_by(|a, b| a.1.total_cmp(&b.1));
475 } else {
476 run1.sort_by(|a, b| b.1.total_cmp(&a.1));
477 run2.sort_by(|a, b| a.1.total_cmp(&b.1));
478 run3.sort_by(|a, b| b.1.total_cmp(&a.1));
479 }
480
481 let mut out: Vec<EntityId> = Vec::with_capacity(run1.len() + run2.len() + run3.len());
482 out.extend(run1.into_iter().map(|(e, _)| e));
483 out.extend(run2.into_iter().map(|(e, _)| e));
484 out.extend(run3.into_iter().map(|(e, _)| e));
485 out.dedup();
486
487 if let Some(q) = world.destination_queue_mut(car_eid) {
488 q.replace(out);
489 }
490}