diff --git a/Cargo.lock b/Cargo.lock index ddfee3d3b5..b04a58257d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2852,6 +2852,7 @@ version = "0.1.0" dependencies = [ "abstutil 0.1.0", "derivative 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "downcast-rs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "geom 0.1.0", "instant 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "libm 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/game/src/app.rs b/game/src/app.rs index a4f6528d13..6a5a8cceb6 100644 --- a/game/src/app.rs +++ b/game/src/app.rs @@ -7,10 +7,10 @@ use crate::render::{AgentCache, AgentColorScheme, DrawMap, DrawOptions, Renderab use crate::sandbox::{GameplayMode, TutorialState}; use abstutil::{MeasureMemory, Timer}; use ezgui::{EventCtx, GfxCtx, Prerender}; -use geom::{Bounds, Circle, Distance, Pt2D}; -use map_model::{Map, Traversable}; +use geom::{Bounds, Circle, Distance, Duration, Pt2D, Time}; +use map_model::{IntersectionID, Map, Traversable}; use rand::seq::SliceRandom; -use sim::{Analytics, GetDrawAgents, Sim, SimFlags}; +use sim::{Analytics, GetDrawAgents, Sim, SimCallback, SimFlags}; use std::collections::BTreeMap; pub struct App { @@ -508,6 +508,7 @@ pub struct PerMap { pub current_selection: Option, pub current_flags: Flags, pub last_warped_from: Option<(Pt2D, f64)>, + pub sim_cb: Option>, } impl PerMap { @@ -528,6 +529,7 @@ impl PerMap { current_selection: None, current_flags: flags.clone(), last_warped_from: None, + sim_cb: None, } } @@ -579,3 +581,21 @@ impl PerObjectActions { ctx.normal_left_click() } } + +pub struct FindDelayedIntersections { + pub halt_limit: Duration, + pub report_limit: Duration, + + pub currently_delayed: Vec<(IntersectionID, Time)>, +} + +impl SimCallback for FindDelayedIntersections { + fn run(&mut self, sim: &Sim, _: &Map) -> bool { + self.currently_delayed = sim.delayed_intersections(self.report_limit); + if let Some((_, t)) = self.currently_delayed.get(0) { + sim.time() - *t >= self.halt_limit + } else { + false + } + } +} diff --git a/game/src/challenges.rs b/game/src/challenges.rs index bcd19b2185..59549b098c 100644 --- a/game/src/challenges.rs +++ b/game/src/challenges.rs @@ -362,9 +362,14 @@ fn prebake(map: &Map, scenario: Scenario, time_limit: Option, timer: & let mut rng = SimFlags::for_test("prebaked").make_rng(); scenario.instantiate(&mut sim, &map, &mut rng, timer); if let Some(dt) = time_limit { - sim.timed_step(&map, dt, timer); + sim.timed_step(&map, dt, &mut None, timer); } else { - sim.timed_step(&map, sim.get_end_of_day() - Time::START_OF_DAY, timer); + sim.timed_step( + &map, + sim.get_end_of_day() - Time::START_OF_DAY, + &mut None, + timer, + ); } abstutil::write_binary( diff --git a/game/src/debug/mod.rs b/game/src/debug/mod.rs index b167c37f49..868f8f9721 100644 --- a/game/src/debug/mod.rs +++ b/game/src/debug/mod.rs @@ -479,7 +479,9 @@ impl ContextualActions for Actions { } (ID::Car(c), "forcibly kill this car") => { app.primary.sim.kill_stuck_car(c, &app.primary.map); - app.primary.sim.tiny_step(&app.primary.map); + app.primary + .sim + .tiny_step(&app.primary.map, &mut app.primary.sim_cb); app.primary.current_selection = None; Transition::Keep } diff --git a/game/src/edit/traffic_signals.rs b/game/src/edit/traffic_signals.rs index c42251ba02..d58a30d1d3 100644 --- a/game/src/edit/traffic_signals.rs +++ b/game/src/edit/traffic_signals.rs @@ -621,9 +621,12 @@ fn make_previewer(i: IntersectionID, phase: usize) -> Box { for idx in 0..phase { step += signal.phases[idx].duration; } - app.primary - .sim - .timed_step(&app.primary.map, step, &mut Timer::throwaway()); + app.primary.sim.timed_step( + &app.primary.map, + step, + &mut app.primary.sim_cb, + &mut Timer::throwaway(), + ); spawn_agents_around(i, app); } diff --git a/game/src/sandbox/gameplay/freeform.rs b/game/src/sandbox/gameplay/freeform.rs index f6aedfd3fd..0908c23238 100644 --- a/game/src/sandbox/gameplay/freeform.rs +++ b/game/src/sandbox/gameplay/freeform.rs @@ -290,7 +290,7 @@ impl State for AgentSpawner { &mut rng, &mut Timer::new("spawn trip"), ); - app.primary.sim.tiny_step(map); + app.primary.sim.tiny_step(map, &mut app.primary.sim_cb); app.recalculate_current_selection(ctx); return Transition::Pop; } @@ -573,7 +573,7 @@ pub fn spawn_agents_around(i: IntersectionID, app: &mut App) { } sim.flush_spawner(spawner, map, &mut timer); - sim.tiny_step(map); + sim.tiny_step(map, &mut app.primary.sim_cb); } pub fn actions(_: &App, id: ID) -> Vec<(Key, String)> { diff --git a/game/src/sandbox/gameplay/mod.rs b/game/src/sandbox/gameplay/mod.rs index 4b94373a94..a20ab3cc34 100644 --- a/game/src/sandbox/gameplay/mod.rs +++ b/game/src/sandbox/gameplay/mod.rs @@ -185,7 +185,9 @@ impl GameplayMode { &mut app.primary.current_flags.sim_flags.make_rng(), timer, ); - app.primary.sim.tiny_step(&app.primary.map); + app.primary + .sim + .tiny_step(&app.primary.map, &mut app.primary.sim_cb); // Maybe we've already got prebaked data for this map+scenario. if !app diff --git a/game/src/sandbox/gameplay/tutorial.rs b/game/src/sandbox/gameplay/tutorial.rs index 25a5fa4a58..75e4d7cd08 100644 --- a/game/src/sandbox/gameplay/tutorial.rs +++ b/game/src/sandbox/gameplay/tutorial.rs @@ -752,7 +752,9 @@ impl TutorialState { if let Some(ref cb) = self.stage().spawn { (cb)(app); - app.primary.sim.tiny_step(&app.primary.map); + app.primary + .sim + .tiny_step(&app.primary.map, &mut app.primary.sim_cb); } let last_finished_task = if self.current.stage == 0 { @@ -1088,7 +1090,7 @@ impl TutorialState { &mut rng, &mut Timer::new("spawn trip"), ); - app.primary.sim.tiny_step(map); + app.primary.sim.tiny_step(map, &mut app.primary.sim_cb); // And add some noise spawn_agents_around(app.primary.map.find_i_by_osm_id(1709145066).unwrap(), app); diff --git a/game/src/sandbox/speed.rs b/game/src/sandbox/speed.rs index bfcb87ad06..edd1c4b9ef 100644 --- a/game/src/sandbox/speed.rs +++ b/game/src/sandbox/speed.rs @@ -1,4 +1,4 @@ -use crate::app::App; +use crate::app::{App, FindDelayedIntersections}; use crate::common::Warping; use crate::game::{msg, State, Transition}; use crate::helpers::ID; @@ -187,7 +187,9 @@ impl SpeedControls { "step forwards" => { let dt = self.composite.persistent_split_value("step forwards"); if dt == Duration::seconds(0.1) { - app.primary.sim.tiny_step(&app.primary.map); + app.primary + .sim + .tiny_step(&app.primary.map, &mut app.primary.sim_cb); app.recalculate_current_selection(ctx); return Some(Transition::KeepWithMouseover); } @@ -259,9 +261,12 @@ impl SpeedControls { let dt = multiplier * real_dt; // TODO This should match the update frequency in ezgui. Plumb along the deadline // or frequency to here. - app.primary - .sim - .time_limited_step(&app.primary.map, dt, Duration::seconds(0.033)); + app.primary.sim.time_limited_step( + &app.primary.map, + dt, + Duration::seconds(0.033), + &mut app.primary.sim_cb, + ); app.recalculate_current_selection(ctx); } } @@ -476,12 +481,20 @@ impl TimeWarpScreen { ctx: &mut EventCtx, app: &mut App, target: Time, - traffic_jams: bool, + mut traffic_jams: bool, ) -> Box { if traffic_jams { - app.primary - .sim - .set_gridlock_checker(Some(Duration::minutes(5))); + if app.primary.sim_cb.is_none() { + app.primary.sim_cb = Some(Box::new(FindDelayedIntersections { + halt_limit: Duration::minutes(5), + report_limit: Duration::minutes(5), + currently_delayed: Vec::new(), + })); + // TODO Can we get away with less frequently? Not sure about all the edge cases + app.primary.sim.set_periodic_callback(Duration::minutes(1)); + } else { + traffic_jams = false; + } } Box::new(TimeWarpScreen { @@ -507,24 +520,12 @@ impl State for TimeWarpScreen { fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { if ctx.input.nonblocking_is_update_event().is_some() { ctx.input.use_update_event(); - if let Some(problems) = app.primary.sim.time_limited_step( + app.primary.sim.time_limited_step( &app.primary.map, self.target - app.primary.sim.time(), Duration::seconds(0.033), - ) { - let id = ID::Intersection(problems[0].0); - app.layer = Some(Box::new(crate::layer::traffic::Dynamic::traffic_jams( - ctx, app, - ))); - return Transition::Replace(Warping::new( - ctx, - id.canonical_point(&app.primary).unwrap(), - Some(10.0), - Some(id), - &mut app.primary, - )); - } - + &mut app.primary.sim_cb, + ); for (t, maybe_i, alert) in app.primary.sim.clear_alerts() { // TODO Just the first :( return Transition::Replace(msg( @@ -532,6 +533,22 @@ impl State for TimeWarpScreen { vec![format!("At {}, near {:?}, {}", t, maybe_i, alert)], )); } + if let Some(ref mut cb) = app.primary.sim_cb { + let di = cb.downcast_mut::().unwrap(); + if let Some((i, _)) = di.currently_delayed.get(0) { + let id = ID::Intersection(*i); + app.layer = Some(Box::new(crate::layer::traffic::Dynamic::traffic_jams( + ctx, app, + ))); + return Transition::Replace(Warping::new( + ctx, + id.canonical_point(&app.primary).unwrap(), + Some(10.0), + Some(id), + &mut app.primary, + )); + } + } // I'm covered in shame for not doing this from the start. let mut txt = Text::from(Line("Let's do the time warp again!").small_heading()); @@ -578,7 +595,7 @@ impl State for TimeWarpScreen { fn on_destroy(&mut self, _: &mut EventCtx, app: &mut App) { if self.traffic_jams { - app.primary.sim.set_gridlock_checker(None); + app.primary.sim.unset_periodic_callback(); } } } diff --git a/sim/Cargo.toml b/sim/Cargo.toml index 3be3761323..2ea84c1174 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" [dependencies] abstutil = { path = "../abstutil" } derivative = "2.1.1" +downcast-rs = "1.1.1" geom = { path = "../geom" } instant = "0.1.2" libm = "0.2.1" diff --git a/sim/src/lib.rs b/sim/src/lib.rs index b673f9e9de..f3822c825e 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, AlertHandler, Sim, SimOptions}; +pub use self::sim::{AgentProperties, AlertHandler, Sim, SimCallback, SimOptions}; pub(crate) use self::transit::TransitSimState; pub use self::trips::{Person, PersonState, TripResult}; pub use self::trips::{TripEndpoint, TripMode}; diff --git a/sim/src/make/load.rs b/sim/src/make/load.rs index e2a6a9d455..21b367dabf 100644 --- a/sim/src/make/load.rs +++ b/sim/src/make/load.rs @@ -1,6 +1,5 @@ use crate::{AlertHandler, Scenario, Sim, SimOptions}; use abstutil::CmdArgs; -use geom::Duration; use map_model::{Map, MapEdits}; use rand::SeedableRng; use rand_xorshift::XorShiftRng; @@ -29,7 +28,6 @@ impl SimFlags { run_name: args .optional("--run_name") .unwrap_or_else(|| "unnamed".to_string()), - savestate_every: args.optional_parse("--savestate_every", Duration::parse), use_freeform_policy_everywhere: args.enabled("--freeform_policy"), dont_block_the_box: !args.enabled("--disable_block_the_box"), recalc_lanechanging: !args.enabled("--disable_recalc_lc"), diff --git a/sim/src/scheduler.rs b/sim/src/scheduler.rs index b5d87b8a97..6616f7a1f2 100644 --- a/sim/src/scheduler.rs +++ b/sim/src/scheduler.rs @@ -20,7 +20,7 @@ pub enum Command { UpdateLaggyHead(CarID), UpdatePed(PedestrianID), UpdateIntersection(IntersectionID), - Savestate(Duration), + Callback(Duration), Pandemic(pandemic::Cmd), FinishRemoteTrip(TripID), } @@ -42,7 +42,7 @@ impl Command { Command::UpdateLaggyHead(id) => CommandType::CarLaggyHead(*id), Command::UpdatePed(id) => CommandType::Ped(*id), Command::UpdateIntersection(id) => CommandType::Intersection(*id), - Command::Savestate(_) => CommandType::Savestate, + Command::Callback(_) => CommandType::Callback, Command::Pandemic(ref p) => CommandType::Pandemic(p.clone()), Command::FinishRemoteTrip(t) => CommandType::FinishRemoteTrip(*t), } @@ -58,7 +58,7 @@ pub enum CommandType { CarLaggyHead(CarID), Ped(PedestrianID), Intersection(IntersectionID), - Savestate, + Callback, Pandemic(pandemic::Cmd), FinishRemoteTrip(TripID), } diff --git a/sim/src/sim.rs b/sim/src/sim.rs index 36bad06226..396a63f2c4 100644 --- a/sim/src/sim.rs +++ b/sim/src/sim.rs @@ -57,10 +57,6 @@ pub struct Sim { #[serde(skip_serializing, skip_deserializing)] analytics: Analytics, - #[derivative(PartialEq = "ignore")] - #[serde(skip_serializing, skip_deserializing)] - check_for_gridlock: Option<(Time, Duration)>, - #[derivative(PartialEq = "ignore")] #[serde(skip_serializing, skip_deserializing)] alerts: AlertHandler, @@ -69,7 +65,6 @@ pub struct Sim { #[derive(Clone)] pub struct SimOptions { pub run_name: String, - pub savestate_every: Option, pub use_freeform_policy_everywhere: bool, pub dont_block_the_box: bool, pub recalc_lanechanging: bool, @@ -98,7 +93,6 @@ impl SimOptions { pub fn new(run_name: &str) -> SimOptions { SimOptions { run_name: run_name.to_string(), - savestate_every: None, use_freeform_policy_everywhere: false, dont_block_the_box: true, recalc_lanechanging: true, @@ -113,9 +107,6 @@ impl SimOptions { impl Sim { pub fn new(map: &Map, opts: SimOptions, timer: &mut Timer) -> Sim { let mut scheduler = Scheduler::new(); - if let Some(d) = opts.savestate_every { - scheduler.push(Time::START_OF_DAY + d, Command::Savestate(d)); - } Sim { driving: DrivingSimState::new(map, opts.recalc_lanechanging), parking: ParkingSimState::new(map, timer), @@ -143,7 +134,6 @@ impl Sim { run_name: opts.run_name, step_count: 0, trip_positions: None, - check_for_gridlock: None, alerts: opts.alerts, analytics: Analytics::new(), @@ -385,47 +375,57 @@ impl GetDrawAgents for Sim { // Running impl Sim { - // Advances time as minimally as possible, also limited by max_dt. - fn minimal_step(&mut self, map: &Map, max_dt: Duration) { + // Advances time as minimally as possible, also limited by max_dt. Returns true if the callback + // said to halt the sim. + fn minimal_step( + &mut self, + map: &Map, + max_dt: Duration, + maybe_cb: &mut Option>, + ) -> bool { self.step_count += 1; let max_time = if let Some(t) = self.scheduler.peek_next_time() { if t > self.time + max_dt { // Next event is after when we want to stop. self.time += max_dt; - return; + return false; } t } else { // No events left at all self.time += max_dt; - return; + return false; }; - let mut savestate = false; + let mut halt = false; while let Some(time) = self.scheduler.peek_next_time() { if time > max_time { - return; + return false; } if let Some(cmd) = self.scheduler.get_next() { - if self.do_step(map, time, cmd) { - savestate = true; + if self.do_step(map, time, cmd, maybe_cb) { + halt = true; + break; } } } self.trip_positions = None; - - if savestate { - self.save(); - } + halt } - // If true, savestate was requested. - fn do_step(&mut self, map: &Map, time: Time, cmd: Command) -> bool { + // If true, halt simulation because the callback said so. + fn do_step( + &mut self, + map: &Map, + time: Time, + cmd: Command, + maybe_cb: &mut Option>, + ) -> bool { self.time = time; let mut events = Vec::new(); - let mut savestate = false; + let mut halt = false; match cmd { Command::StartTrip(id, trip_spec, maybe_req, maybe_path) => { self.trips.start_trip( @@ -576,10 +576,12 @@ impl Sim { self.intersections .update_intersection(self.time, i, map, &mut self.scheduler); } - Command::Savestate(frequency) => { + Command::Callback(frequency) => { self.scheduler - .push(self.time + frequency, Command::Savestate(frequency)); - savestate = true; + .push(self.time + frequency, Command::Callback(frequency)); + if maybe_cb.as_mut().unwrap().run(self, map) { + halt = true; + } } Command::Pandemic(cmd) => { self.pandemic @@ -601,7 +603,7 @@ impl Sim { // Record events at precisely the time they occur. self.dispatch_events(events, map); - savestate + halt } fn dispatch_events(&mut self, mut events: Vec, map: &Map) { @@ -620,14 +622,22 @@ impl Sim { } } - pub fn timed_step(&mut self, map: &Map, dt: Duration, timer: &mut Timer) { + pub fn timed_step( + &mut self, + map: &Map, + dt: Duration, + maybe_cb: &mut Option>, + timer: &mut Timer, + ) { let end_time = self.time + dt; let start = Instant::now(); let mut last_update = Instant::now(); timer.start(format!("Advance sim to {}", end_time)); while self.time < end_time { - self.minimal_step(map, end_time - self.time); + if self.minimal_step(map, end_time - self.time, maybe_cb) { + break; + } if !self.analytics.alerts.is_empty() { match self.alerts { AlertHandler::Print => { @@ -658,32 +668,29 @@ impl Sim { } timer.stop(format!("Advance sim to {}", end_time)); } - pub fn tiny_step(&mut self, map: &Map) { - self.timed_step(map, Duration::seconds(0.1), &mut Timer::throwaway()); + pub fn tiny_step(&mut self, map: &Map, maybe_cb: &mut Option>) { + self.timed_step( + map, + Duration::seconds(0.1), + maybe_cb, + &mut Timer::throwaway(), + ); } - // TODO Do this like periodic savestating instead? - pub fn set_gridlock_checker(&mut self, freq: Option) { - if let Some(dt) = freq { - assert!(self.check_for_gridlock.is_none()); - self.check_for_gridlock = Some((self.time + dt, dt)); - } else { - assert!(self.check_for_gridlock.is_some()); - self.check_for_gridlock = None; - } - } - // This will return delayed intersections if that's why it stops early. pub fn time_limited_step( &mut self, map: &Map, dt: Duration, real_time_limit: Duration, - ) -> Option> { + maybe_cb: &mut Option>, + ) { let started_at = Instant::now(); let end_time = self.time + dt; while self.time < end_time && Duration::realtime_elapsed(started_at) < real_time_limit { - self.minimal_step(map, end_time - self.time); + if self.minimal_step(map, end_time - self.time, maybe_cb) { + break; + } if !self.analytics.alerts.is_empty() { match self.alerts { AlertHandler::Print => { @@ -702,18 +709,7 @@ impl Sim { } } } - if let Some((ref mut t, dt)) = self.check_for_gridlock { - if self.time >= *t { - *t += dt; - let gridlock = self.delayed_intersections(dt); - if !gridlock.is_empty() { - return Some(gridlock); - } - } - } } - - None } pub fn dump_before_abort(&self) { @@ -745,7 +741,7 @@ impl Sim { let dt = time_limit.unwrap_or_else(|| Duration::seconds(30.0)); match panic::catch_unwind(panic::AssertUnwindSafe(|| { - self.timed_step(map, dt, &mut Timer::throwaway()); + self.timed_step(map, dt, &mut None, &mut Timer::throwaway()); })) { Ok(()) => {} Err(err) => { @@ -1197,6 +1193,27 @@ impl Sim { } } +// Callbacks +pub trait SimCallback: downcast_rs::Downcast { + // Run at some scheduled time. If this returns true, halt simulation. + fn run(&mut self, sim: &Sim, map: &Map) -> bool; +} +downcast_rs::impl_downcast!(SimCallback); + +impl Sim { + // Only one at a time supported. + pub fn set_periodic_callback(&mut self, frequency: Duration) { + // TODO Round up time nicely? + self.scheduler + .push(self.time + frequency, Command::Callback(frequency)); + } + pub fn unset_periodic_callback(&mut self) { + // Frequency doesn't matter + self.scheduler + .cancel(Command::Callback(Duration::seconds(1.0))); + } +} + pub struct AgentProperties { // TODO Of this leg of the trip only! pub total_time: Duration,