diff --git a/game/src/abtest/setup.rs b/game/src/abtest/setup.rs index ae642eee66..d75a917b15 100644 --- a/game/src/abtest/setup.rs +++ b/game/src/abtest/setup.rs @@ -6,7 +6,7 @@ use crate::render::DrawMap; use ezgui::{Choice, EventCtx, GfxCtx, Wizard, WrappedWizard}; use geom::Duration; use map_model::MapEdits; -use sim::{ABTest, Scenario, SimFlags, SimOptions}; +use sim::{ABTest, AlertHandler, Scenario, SimFlags, SimOptions}; pub struct PickABTest; impl PickABTest { @@ -167,6 +167,7 @@ fn launch_test(test: &ABTest, app: &mut App, ctx: &mut EventCtx) -> ABTestMode { .opts .break_turn_conflict_cycles, enable_pandemic_model: None, + alerts: AlertHandler::Print, }, }, ..current_flags.clone() diff --git a/game/src/sandbox/speed.rs b/game/src/sandbox/speed.rs index b572198dc0..772d2a8e3e 100644 --- a/game/src/sandbox/speed.rs +++ b/game/src/sandbox/speed.rs @@ -274,27 +274,22 @@ impl SpeedControls { if !alerts.is_empty() { self.pause(ctx, app); - // Just go to the first one, but print all messages - let id = ID::Intersection(alerts[0].1); - return Some(Transition::PushTwice( - msg( - "Alerts", - alerts - .into_iter() - .map(|(_, _, msg)| { - println!("Alert: {}", msg); - msg - }) - .collect(), - ), - Warping::new( - ctx, - id.canonical_point(&app.primary).unwrap(), - Some(10.0), - None, - &mut app.primary, - ), - )); + let popup = msg("Alerts", alerts.iter().map(|(_, _, msg)| msg).collect()); + if let Some(i) = alerts[0].1 { + // Just go to the first one, but print all messages + return Some(Transition::PushTwice( + popup, + Warping::new( + ctx, + ID::Intersection(i).canonical_point(&app.primary).unwrap(), + Some(10.0), + None, + &mut app.primary, + ), + )); + } else { + return Some(Transition::Push(popup)); + } } None @@ -517,9 +512,13 @@ impl State for TimeWarpScreen { )); } // TODO secondary for a/b test mode - // For now, don't stop for this - for (t, i, msg) in app.primary.sim.clear_alerts() { - println!("- Alert: At {}, near {}, {}", t, i, msg); + + for (t, maybe_i, alert) in app.primary.sim.clear_alerts() { + // TODO Just the first :( + return Transition::Replace(msg( + "Alert", + vec![format!("At {}, near {:?}, {}", t, maybe_i, alert)], + )); } // I'm covered in shame for not doing this from the start. diff --git a/sim/src/analytics.rs b/sim/src/analytics.rs index 37b65c7e26..2f9aac3014 100644 --- a/sim/src/analytics.rs +++ b/sim/src/analytics.rs @@ -26,7 +26,7 @@ pub struct Analytics { pub intersection_delays: BTreeMap>, // Per parking lane, when does a spot become filled (true) or free (false) parking_spot_changes: BTreeMap>, - pub(crate) alerts: Vec<(Time, IntersectionID, String)>, + pub(crate) alerts: Vec<(Time, Option, String)>, // After we restore from a savestate, don't record anything. This is only going to make sense // if savestates are only used for quickly previewing against prebaked results, where we have diff --git a/sim/src/events.rs b/sim/src/events.rs index 8e4b87f770..8a6c5e5df9 100644 --- a/sim/src/events.rs +++ b/sim/src/events.rs @@ -47,7 +47,8 @@ pub enum Event { // to plumb info into Analytics is Event. PathAmended(Path), - Alert(IntersectionID, String), + // TODO Also buildings + Alert(Option, String), } #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] diff --git a/sim/src/lib.rs b/sim/src/lib.rs index f06673c168..da790e6559 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -23,7 +23,7 @@ pub(crate) use self::mechanics::{ pub(crate) use self::pandemic::PandemicModel; pub(crate) use self::router::{ActionAtEnd, Router}; pub(crate) use self::scheduler::{Command, Scheduler}; -pub use self::sim::{AgentProperties, Sim, SimOptions}; +pub use self::sim::{AgentProperties, AlertHandler, Sim, SimOptions}; pub(crate) use self::transit::TransitSimState; pub use self::trips::{Person, PersonState, TripCount, TripResult}; pub use self::trips::{TripEndpoint, TripMode}; diff --git a/sim/src/make/load.rs b/sim/src/make/load.rs index 35741f0029..2a584ff85c 100644 --- a/sim/src/make/load.rs +++ b/sim/src/make/load.rs @@ -1,4 +1,4 @@ -use crate::{Scenario, Sim, SimOptions}; +use crate::{AlertHandler, Scenario, Sim, SimOptions}; use abstutil::CmdArgs; use geom::Duration; use map_model::{Map, MapEdits}; @@ -41,6 +41,15 @@ impl SimFlags { } else { None }, + alerts: args + .optional("--alerts") + .map(|x| match x.as_ref() { + "print" => AlertHandler::Print, + "block" => AlertHandler::Block, + "silence" => AlertHandler::Silence, + _ => panic!("Bad --alerts={}. Must be print|block|silence", x), + }) + .unwrap_or(AlertHandler::Print), }, } } diff --git a/sim/src/mechanics/intersection.rs b/sim/src/mechanics/intersection.rs index 623dfd5c2b..cddeb91154 100644 --- a/sim/src/mechanics/intersection.rs +++ b/sim/src/mechanics/intersection.rs @@ -395,12 +395,10 @@ impl State { while !queue.is_empty() { let current = queue.pop().unwrap(); if !seen.is_empty() && current == req.agent { - if false { - events.push(Event::Alert( - req.turn.parent, - format!("Turn conflict cycle involving {:?}", seen), - )); - } + events.push(Event::Alert( + Some(req.turn.parent), + format!("Turn conflict cycle involving {:?}", seen), + )); return true; } // Because the blocked-by relation is many-to-many, this might happen. @@ -539,11 +537,13 @@ impl State { if time_to_cross > remaining_phase_time { // Actually, we might have bigger problems... if time_to_cross > phase.duration { - println!( - "OYYY! {:?} is impossible to fit into phase duration of {}. Allowing, but fix \ - the policy!", - req, phase.duration - ); + events.push(Event::Alert( + Some(req.turn.parent), + format!( + "{:?} is impossible to fit into phase duration of {}", + req, phase.duration + ), + )); } else { return false; } diff --git a/sim/src/sim.rs b/sim/src/sim.rs index fd3918a328..0e39b01cf4 100644 --- a/sim/src/sim.rs +++ b/sim/src/sim.rs @@ -61,6 +61,10 @@ pub struct Sim { #[derivative(PartialEq = "ignore")] #[serde(skip_serializing, skip_deserializing)] check_for_gridlock: Option<(Time, Duration)>, + + #[derivative(PartialEq = "ignore")] + #[serde(skip_serializing, skip_deserializing)] + alerts: AlertHandler, } #[derive(Clone)] @@ -72,6 +76,23 @@ pub struct SimOptions { pub recalc_lanechanging: bool, pub break_turn_conflict_cycles: bool, pub enable_pandemic_model: Option, + pub alerts: AlertHandler, +} + +#[derive(Clone)] +pub enum AlertHandler { + // Just print the alert to STDOUT + Print, + // Print the alert to STDOUT and don't proceed until the UI calls clear_alerts() + Block, + // Don't do anything + Silence, +} + +impl std::default::Default for AlertHandler { + fn default() -> AlertHandler { + AlertHandler::Print + } } impl SimOptions { @@ -84,6 +105,7 @@ impl SimOptions { recalc_lanechanging: true, break_turn_conflict_cycles: false, enable_pandemic_model: None, + alerts: AlertHandler::Print, } } } @@ -123,6 +145,7 @@ impl Sim { step_count: 0, trip_positions: None, check_for_gridlock: None, + alerts: opts.alerts, analytics: Analytics::new(), } @@ -586,10 +609,25 @@ impl Sim { timer.start(format!("Advance sim to {}", end_time)); while self.time < end_time { - if !self.analytics.alerts.is_empty() { - break; - } self.minimal_step(map, end_time - self.time); + if !self.analytics.alerts.is_empty() { + match self.alerts { + AlertHandler::Print => { + for (t, _, msg) in self.analytics.alerts.drain(..) { + println!("Alert at {}: {}", t, msg); + } + } + AlertHandler::Block => { + for (t, _, msg) in &self.analytics.alerts { + println!("Alert at {}: {}", t, msg); + } + break; + } + AlertHandler::Silence => { + self.analytics.alerts.clear(); + } + } + } if Duration::realtime_elapsed(last_update) >= Duration::seconds(1.0) { // TODO Not timer? println!( @@ -627,10 +665,25 @@ impl Sim { let end_time = self.time + dt; while self.time < end_time && Duration::realtime_elapsed(started_at) < real_time_limit { - if !self.analytics.alerts.is_empty() { - break; - } self.minimal_step(map, end_time - self.time); + if !self.analytics.alerts.is_empty() { + match self.alerts { + AlertHandler::Print => { + for (t, _, msg) in self.analytics.alerts.drain(..) { + println!("Alert at {}: {}", t, msg); + } + } + AlertHandler::Block => { + for (t, _, msg) in &self.analytics.alerts { + println!("Alert at {}: {}", t, msg); + } + break; + } + AlertHandler::Silence => { + self.analytics.alerts.clear(); + } + } + } if let Some((ref mut t, dt)) = self.check_for_gridlock { if self.time >= *t { *t += dt; @@ -1134,7 +1187,7 @@ impl Sim { } } - pub fn clear_alerts(&mut self) -> Vec<(Time, IntersectionID, String)> { + pub fn clear_alerts(&mut self) -> Vec<(Time, Option, String)> { std::mem::replace(&mut self.analytics.alerts, Vec::new()) } } diff --git a/sim/src/trips.rs b/sim/src/trips.rs index c5ae18e567..a2d3ed4251 100644 --- a/sim/src/trips.rs +++ b/sim/src/trips.rs @@ -195,6 +195,7 @@ impl TripManager { &self.people[trip.person.0], map, scheduler, + &mut self.events, ) { self.unfinished_trips -= 1; } @@ -242,10 +243,13 @@ impl TripManager { let path = if let Some(p) = map.pathfind(req.clone()) { p } else { - println!( - "Aborting {} at {} because no path for the car portion! {} to {}", - trip.id, now, start, end - ); + self.events.push(Event::Alert( + None, + format!( + "Aborting {} because no path for the car portion! {} to {}", + trip.id, start, end + ), + )); // Move the car to the destination... parking.remove_parked_car(parked_car.clone()); let trip = trip.id; @@ -324,11 +328,14 @@ impl TripManager { ), ); } else { - println!( - "Aborting {} at {} because no path for the bike portion (or sidewalk connection \ - at the end)! {} to {}", - trip.id, now, driving_pos, end - ); + self.events.push(Event::Alert( + None, + format!( + "Aborting {} because no path for the bike portion (or sidewalk connection at \ + the end)! {} to {}", + trip.id, driving_pos, end + ), + )); let trip = trip.id; self.abort_trip(now, trip, None, parking, scheduler, map); } @@ -357,7 +364,14 @@ impl TripManager { _ => unreachable!(), }; - if !trip.spawn_ped(now, bike_rack, &self.people[trip.person.0], map, scheduler) { + if !trip.spawn_ped( + now, + bike_rack, + &self.people[trip.person.0], + map, + scheduler, + &mut self.events, + ) { self.unfinished_trips -= 1; } } @@ -478,7 +492,14 @@ impl TripManager { _ => unreachable!(), }; - if !trip.spawn_ped(now, start, &self.people[trip.person.0], map, scheduler) { + if !trip.spawn_ped( + now, + start, + &self.people[trip.person.0], + map, + scheduler, + &mut self.events, + ) { self.unfinished_trips -= 1; } } @@ -599,17 +620,23 @@ impl TripManager { .map(|(_, spot, _)| spot) }) { - println!( - "{} had a trip aborted, and their car was warped to {:?}", - person, spot - ); + self.events.push(Event::Alert( + None, + format!( + "{} had a trip aborted, and their car was warped to {:?}", + person, spot + ), + )); parking.reserve_spot(spot); parking.add_parked_car(ParkedCar { vehicle, spot }); } else { - println!( - "{} had a trip aborted, but nowhere to warp their car! Sucks.", - person - ); + self.events.push(Event::Alert( + None, + format!( + "{} had a trip aborted, but nowhere to warp their car! Sucks.", + person + ), + )); } } } @@ -798,10 +825,13 @@ impl TripManager { return; } let (trip, spec, maybe_req, maybe_path) = person.delayed_trips.remove(0); - println!( - "At {}, {} just freed up, so starting delayed trip {}", - now, person.id, trip - ); + self.events.push(Event::Alert( + None, + format!( + "{} just freed up, so starting delayed trip {}", + person.id, trip + ), + )); self.start_trip( now, trip, spec, maybe_req, maybe_path, parking, scheduler, map, ); @@ -821,10 +851,13 @@ impl TripManager { let person = &mut self.people[self.trips[trip.0].person.0]; if let PersonState::Trip(_) = person.state { // Previous trip isn't done. Defer this one! - println!( - "At {}, {} is still doing a trip, so not starting {}", - now, person.id, trip - ); + self.events.push(Event::Alert( + None, + format!( + "{} is still doing a trip, so not starting {}", + person.id, trip + ), + )); person .delayed_trips .push((trip, spec, maybe_req, maybe_path)); @@ -864,19 +897,25 @@ impl TripManager { ), ); } else { - println!( - "VehicleAppearing trip couldn't find the first path (or no bike->sidewalk \ - connection at the end): {}", - req - ); + self.events.push(Event::Alert( + None, + format!( + "VehicleAppearing trip couldn't find the first path (or no \ + bike->sidewalk connection at the end): {}", + req + ), + )); self.abort_trip(now, trip, Some(vehicle), parking, scheduler, map); } } TripSpec::NoRoomToSpawn { i, use_vehicle, .. } => { - println!( - "{} couldn't spawn at border {}, just aborting", - person.id, i - ); + self.events.push(Event::Alert( + Some(i), + format!( + "{} couldn't spawn at border {}, just aborting", + person.id, i + ), + )); let vehicle = person.get_vehicle(use_vehicle); self.abort_trip(now, trip, Some(vehicle), parking, scheduler, map); } @@ -912,7 +951,10 @@ impl TripManager { }), ); } else { - println!("UsingParkedCar trip couldn't find the walking path {}", req); + self.events.push(Event::Alert( + None, + format!("UsingParkedCar trip couldn't find the walking path {}", req), + )); // Move the car to the destination parking.remove_parked_car(parked_car.clone()); self.abort_trip( @@ -927,11 +969,14 @@ impl TripManager { } else { // This should only happen when a driving trip has been aborted and there was // absolutely no room to warp the car. - println!( - "At {}, {} should have {} parked somewhere, but it's unavailable, so \ - aborting {}", - now, person.id, car, trip - ); + self.events.push(Event::Alert( + None, + format!( + "{} should have {} parked somewhere, but it's unavailable, so \ + aborting {}", + person.id, car, trip + ), + )); self.abort_trip(now, trip, None, parking, scheduler, map); } } @@ -963,7 +1008,10 @@ impl TripManager { }), ); } else { - println!("JustWalking trip couldn't find the first path {}", req); + self.events.push(Event::Alert( + None, + format!("JustWalking trip couldn't find the first path {}", req), + )); self.abort_trip(now, trip, None, parking, scheduler, map); } } @@ -997,7 +1045,10 @@ impl TripManager { }), ); } else { - println!("UsingBike trip couldn't find the first path {}", req); + self.events.push(Event::Alert( + None, + format!("UsingBike trip couldn't find the first path {}", req), + )); self.abort_trip(now, trip, None, parking, scheduler, map); } } @@ -1030,7 +1081,10 @@ impl TripManager { }), ); } else { - println!("UsingTransit trip couldn't find the first path {}", req); + self.events.push(Event::Alert( + None, + format!("UsingTransit trip couldn't find the first path {}", req), + )); self.abort_trip(now, trip, None, parking, scheduler, map); } } @@ -1061,6 +1115,7 @@ impl Trip { person: &Person, map: &Map, scheduler: &mut Scheduler, + events: &mut Vec, ) -> bool { let walk_to = match self.legs[0] { TripLeg::Walk(ref to) => to.clone(), @@ -1075,10 +1130,13 @@ impl Trip { let path = if let Some(p) = map.pathfind(req.clone()) { p } else { - println!( - "Aborting {} at {} because no path for the walking portion! {:?} to {:?}", - self.id, now, start, walk_to - ); + events.push(Event::Alert( + None, + format!( + "Aborting {} because no path for the walking portion! {:?} to {:?}", + self.id, start, walk_to + ), + )); return false; };