game/sandbox/
mod.rs

1use anyhow::Result;
2use maplit::btreeset;
3
4use geom::{Circle, Distance, Time};
5use map_gui::colors::ColorSchemeChoice;
6use map_gui::load::MapLoader;
7use map_gui::options::OptionsPanel;
8use map_gui::tools::Minimap;
9use map_gui::AppLike;
10use sim::Analytics;
11use synthpop::Scenario;
12use widgetry::tools::{ChooseSomething, FileLoader, FutureLoader, URLManager};
13use widgetry::{lctrl, Choice, EventCtx, GfxCtx, Key, Outcome, Panel, State, UpdateType};
14
15pub use self::gameplay::{spawn_agents_around, GameplayMode, TutorialPointer, TutorialState};
16pub use self::minimap::MinimapController;
17use self::misc_tools::{RoutePreview, TrafficRecorder};
18pub use self::speed::{SpeedSetting, TimePanel};
19pub use self::time_warp::TimeWarpScreen;
20use crate::app::{App, Transition};
21use crate::common::{tool_panel, CommonState};
22use crate::debug::DebugMode;
23use crate::edit::{
24    can_edit_lane, EditMode, RoadEditor, SaveEdits, StopSignEditor, TrafficSignalEditor,
25};
26use crate::info::ContextualActions;
27use crate::layer::favorites::{Favorites, ShowFavorites};
28use crate::layer::PickLayer;
29use crate::pregame::TitleScreen;
30use crate::render::{unzoomed_agent_radius, UnzoomedAgents};
31use crate::ID;
32
33pub mod dashboards;
34pub mod gameplay;
35mod minimap;
36mod misc_tools;
37mod speed;
38mod time_warp;
39mod turn_explorer;
40
41pub struct SandboxMode {
42    gameplay: Box<dyn gameplay::GameplayState>,
43    pub gameplay_mode: GameplayMode,
44
45    pub controls: SandboxControls,
46
47    recalc_unzoomed_agent: Option<Time>,
48    last_cs: ColorSchemeChoice,
49}
50
51pub struct SandboxControls {
52    pub common: Option<CommonState>,
53    route_preview: Option<RoutePreview>,
54    tool_panel: Option<Panel>,
55    pub time_panel: Option<TimePanel>,
56    minimap: Option<Minimap<App, MinimapController>>,
57}
58
59impl SandboxMode {
60    /// If you don't need to chain any transitions after the SandboxMode that rely on its resources
61    /// being loaded, use this. Otherwise, see `async_new`.
62    pub fn simple_new(app: &mut App, mode: GameplayMode) -> Box<dyn State<App>> {
63        SandboxMode::async_new(app, mode, Box::new(|_, _| Vec::new()))
64    }
65
66    /// This does not immediately initialize anything (like loading the correct map, instantiating
67    /// the scenario, etc). That means if you're chaining this call with other transitions, you
68    /// probably need to defer running them using `finalize`.
69    pub fn async_new(
70        app: &mut App,
71        mode: GameplayMode,
72        finalize: Box<dyn FnOnce(&mut EventCtx, &mut App) -> Vec<Transition>>,
73    ) -> Box<dyn State<App>> {
74        app.primary.clear_sim();
75        if let Some(ref mut secondary) = app.secondary {
76            secondary.clear_sim();
77        }
78        Box::new(SandboxLoader {
79            stage: Some(LoadStage::LoadingMap),
80            mode,
81            finalize: Some(finalize),
82        })
83    }
84
85    /// Assumes that the map and simulation have already been set up, and starts by loading
86    /// prebaked data.
87    pub fn start_from_savestate(app: &App) -> Box<dyn State<App>> {
88        let scenario_name = app.primary.sim.get_run_name().to_string();
89        Box::new(SandboxLoader {
90            stage: Some(LoadStage::LoadingPrebaked(scenario_name.clone())),
91            mode: GameplayMode::PlayScenario(
92                app.primary.map.get_name().clone(),
93                scenario_name,
94                Vec::new(),
95            ),
96            finalize: Some(Box::new(|_, _| Vec::new())),
97        })
98    }
99
100    // Just for Warping
101    pub fn contextual_actions(&self) -> Actions {
102        Actions {
103            is_paused: self
104                .controls
105                .time_panel
106                .as_ref()
107                .map(|s| s.is_paused())
108                .unwrap_or(true),
109            can_interact: self.gameplay.can_examine_objects(),
110            gameplay: self.gameplay_mode.clone(),
111        }
112    }
113}
114
115impl State<App> for SandboxMode {
116    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
117        if app.opts.toggle_day_night_colors {
118            if is_daytime(app) {
119                app.change_color_scheme(ctx, ColorSchemeChoice::DayMode)
120            } else {
121                app.change_color_scheme(ctx, ColorSchemeChoice::NightMode)
122            };
123        }
124
125        if app.opts.color_scheme != self.last_cs {
126            self.last_cs = app.opts.color_scheme;
127            self.controls.recreate_panels(ctx, app);
128            self.gameplay.recreate_panels(ctx, app);
129        }
130
131        // Do this before gameplay
132        if self.gameplay.can_move_canvas() && ctx.canvas_movement() {
133            URLManager::update_url_cam(ctx, app.primary.map.get_gps_bounds());
134        }
135
136        let mut actions = self.contextual_actions();
137        if let Some(t) = self
138            .gameplay
139            .event(ctx, app, &mut self.controls, &mut actions)
140        {
141            return t;
142        }
143
144        if ctx.redo_mouseover() {
145            app.recalculate_current_selection(ctx);
146        }
147
148        // Order here is pretty arbitrary
149        if app.opts.dev && ctx.input.pressed(lctrl(Key::D)) {
150            return Transition::Push(DebugMode::new_state(ctx, app));
151        }
152
153        if let Some(ref mut m) = self.controls.minimap {
154            if let Some(t) = m.event(ctx, app) {
155                return t;
156            }
157            if let Some(t) = PickLayer::update(ctx, app) {
158                return t;
159            }
160        }
161
162        if let Some(ref mut tp) = self.controls.time_panel {
163            if let Some(t) = tp.event(ctx, app, Some(&self.gameplay_mode)) {
164                return t;
165            }
166        }
167
168        // We need to recalculate unzoomed agent mouseover when the mouse is still and time passes
169        // (since something could move beneath the cursor), or when the mouse moves.
170        if app.primary.current_selection.is_none()
171            && ctx.canvas.is_unzoomed()
172            && (ctx.redo_mouseover()
173                || self
174                    .recalc_unzoomed_agent
175                    .map(|t| t != app.primary.sim.time())
176                    .unwrap_or(true))
177        {
178            mouseover_unzoomed_agent_circle(ctx, app);
179        }
180
181        if let Some(ref mut r) = self.controls.route_preview {
182            if let Some(t) = r.event(ctx, app) {
183                return t;
184            }
185        }
186
187        // Fragile ordering. Let this work before tool_panel, so Key::Escape from the info panel
188        // beats the one to quit. And let speed update the sim before we update the info panel.
189        if let Some(ref mut c) = self.controls.common {
190            if let Some(t) = c.event(ctx, app, &mut actions) {
191                return t;
192            }
193        }
194
195        if let Some(ref mut tp) = self.controls.tool_panel {
196            if let Outcome::Clicked(x) = tp.event(ctx) {
197                match x.as_ref() {
198                    "back" => {
199                        return maybe_exit_sandbox(ctx);
200                    }
201                    "settings" => {
202                        return Transition::Push(OptionsPanel::new_state(ctx, app));
203                    }
204                    _ => unreachable!(),
205                }
206            }
207        }
208
209        if self
210            .controls
211            .time_panel
212            .as_ref()
213            .map(|s| s.is_paused())
214            .unwrap_or(true)
215        {
216            Transition::Keep
217        } else {
218            ctx.request_update(UpdateType::Game);
219            Transition::Keep
220        }
221    }
222
223    fn draw(&self, g: &mut GfxCtx, app: &App) {
224        if let Some(ref l) = app.primary.layer {
225            l.draw(g, app);
226        }
227
228        if !app.opts.minimal_controls {
229            if let Some(ref c) = self.controls.common {
230                c.draw(g, app);
231            } else {
232                CommonState::draw_osd(g, app);
233            }
234            if let Some(ref tp) = self.controls.tool_panel {
235                tp.draw(g);
236            }
237        }
238        if let Some(ref tp) = self.controls.time_panel {
239            tp.draw(g);
240        }
241        if let Some(ref m) = self.controls.minimap {
242            m.draw(g, app);
243        }
244        if let Some(ref r) = self.controls.route_preview {
245            r.draw(g);
246        }
247
248        if !app.opts.minimal_controls {
249            self.gameplay.draw(g, app);
250        }
251    }
252
253    fn on_destroy(&mut self, _: &mut EventCtx, app: &mut App) {
254        app.primary.layer = None;
255        app.primary.agents.borrow_mut().unzoomed_agents = UnzoomedAgents::new();
256        self.gameplay.on_destroy(app);
257    }
258}
259
260pub fn maybe_exit_sandbox(ctx: &mut EventCtx) -> Transition {
261    Transition::Push(ChooseSomething::new_state(
262        ctx,
263        "Are you ready to leave this mode?",
264        vec![
265            Choice::string("keep playing"),
266            Choice::string("quit to main screen").key(Key::Q),
267        ],
268        Box::new(|resp, ctx, app| {
269            if resp == "keep playing" {
270                return Transition::Pop;
271            }
272
273            if app.primary.map.unsaved_edits() {
274                return Transition::Multi(vec![
275                    Transition::Push(Box::new(BackToTitleScreen)),
276                    Transition::Push(SaveEdits::new_state(
277                        ctx,
278                        app,
279                        "Do you want to save your proposal first?",
280                        true,
281                        None,
282                        Box::new(|_, _| {}),
283                    )),
284                ]);
285            }
286            Transition::Replace(Box::new(BackToTitleScreen))
287        }),
288    ))
289}
290
291struct BackToTitleScreen;
292
293impl State<App> for BackToTitleScreen {
294    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
295        app.change_color_scheme(ctx, ColorSchemeChoice::DayMode);
296        app.clear_everything(ctx);
297        Transition::Clear(vec![TitleScreen::new_state(ctx, app)])
298    }
299
300    fn draw(&self, _: &mut GfxCtx, _: &App) {}
301}
302
303// pub for Warping
304pub struct Actions {
305    is_paused: bool,
306    can_interact: bool,
307    gameplay: GameplayMode,
308}
309impl ContextualActions for Actions {
310    fn actions(&self, app: &App, id: ID) -> Vec<(Key, String)> {
311        let mut actions = Vec::new();
312        if self.can_interact {
313            match id {
314                ID::Intersection(i) => {
315                    if app.primary.map.get_i(i).is_traffic_signal() {
316                        actions.push((Key::E, "edit traffic signal".to_string()));
317                    }
318                    if app.primary.map.get_i(i).is_stop_sign()
319                        && self.gameplay.can_edit_stop_signs()
320                    {
321                        actions.push((Key::E, "edit stop sign".to_string()));
322                    }
323                    if app.opts.dev && app.primary.sim.num_recorded_trips().is_none() {
324                        actions.push((Key::R, "record traffic here".to_string()));
325                    }
326                }
327                ID::Lane(l) => {
328                    if !app.primary.map.get_turns_from_lane(l).is_empty() {
329                        actions.push((Key::Z, "explore turns from this lane".to_string()));
330                    }
331                    if self.gameplay.can_edit_roads() && can_edit_lane(app, l) {
332                        actions.push((Key::E, "edit lane".to_string()));
333                    }
334                }
335                ID::Building(b) => {
336                    if Favorites::contains(app, b) {
337                        actions.push((Key::F, "remove this building from favorites".to_string()));
338                    } else {
339                        actions.push((Key::F, "add this building to favorites".to_string()));
340                    }
341                }
342                _ => {}
343            }
344        }
345        actions.extend(match self.gameplay {
346            GameplayMode::Freeform(_) => gameplay::freeform::actions(app, id),
347            GameplayMode::Tutorial(_) => gameplay::tutorial::actions(app, id),
348            _ => Vec::new(),
349        });
350        actions
351    }
352    fn execute(
353        &mut self,
354        ctx: &mut EventCtx,
355        app: &mut App,
356        id: ID,
357        action: String,
358        close_panel: &mut bool,
359    ) -> Transition {
360        match (id, action.as_ref()) {
361            (ID::Intersection(i), "edit traffic signal") => Transition::Multi(vec![
362                Transition::Push(EditMode::new_state(ctx, app, self.gameplay.clone())),
363                Transition::Push(TrafficSignalEditor::new_state(
364                    ctx,
365                    app,
366                    btreeset! {i},
367                    self.gameplay.clone(),
368                )),
369            ]),
370            (ID::Intersection(i), "edit stop sign") => Transition::Multi(vec![
371                Transition::Push(EditMode::new_state(ctx, app, self.gameplay.clone())),
372                Transition::Push(StopSignEditor::new_state(
373                    ctx,
374                    app,
375                    i,
376                    self.gameplay.clone(),
377                )),
378            ]),
379            (ID::Intersection(i), "record traffic here") => {
380                Transition::Push(TrafficRecorder::new_state(ctx, btreeset! {i}))
381            }
382            (ID::Lane(l), "explore turns from this lane") => {
383                Transition::Push(turn_explorer::TurnExplorer::new_state(ctx, app, l))
384            }
385            (ID::Lane(l), "edit lane") => Transition::Multi(vec![
386                Transition::Push(EditMode::new_state(ctx, app, self.gameplay.clone())),
387                Transition::Push(RoadEditor::new_state(ctx, app, l)),
388            ]),
389            (ID::Building(b), "add this building to favorites") => {
390                Favorites::add(app, b);
391                app.primary.layer = Some(Box::new(ShowFavorites::new(ctx, app)));
392                Transition::Keep
393            }
394            (ID::Building(b), "remove this building from favorites") => {
395                Favorites::remove(app, b);
396                app.primary.layer = Some(Box::new(ShowFavorites::new(ctx, app)));
397                Transition::Keep
398            }
399            (_, "follow (run the simulation)") => {
400                *close_panel = false;
401                Transition::ModifyState(Box::new(|state, ctx, app| {
402                    let mode = state.downcast_mut::<SandboxMode>().unwrap();
403                    let time_panel = mode.controls.time_panel.as_mut().unwrap();
404                    assert!(time_panel.is_paused());
405                    time_panel.resume(ctx, app, SpeedSetting::Realtime);
406                }))
407            }
408            (_, "unfollow (pause the simulation)") => {
409                *close_panel = false;
410                Transition::ModifyState(Box::new(|state, ctx, app| {
411                    let mode = state.downcast_mut::<SandboxMode>().unwrap();
412                    let time_panel = mode.controls.time_panel.as_mut().unwrap();
413                    assert!(!time_panel.is_paused());
414                    time_panel.pause(ctx, app);
415                }))
416            }
417            (id, action) => match self.gameplay {
418                GameplayMode::Freeform(_) => gameplay::freeform::execute(ctx, app, id, action),
419                GameplayMode::Tutorial(_) => gameplay::tutorial::execute(ctx, app, id, action),
420                _ => unreachable!(),
421            },
422        }
423    }
424    fn is_paused(&self) -> bool {
425        self.is_paused
426    }
427    fn gameplay_mode(&self) -> GameplayMode {
428        self.gameplay.clone()
429    }
430}
431
432// TODO Setting SandboxMode up is quite convoluted, all in order to support asynchronously loading
433// files on the web. Each LoadStage is followed in order, with some optional short-circuiting.
434//
435// Ideally there'd be a much simpler way to express this using Rust's async, to let the compiler
436// express this state machine for us.
437
438#[allow(clippy::large_enum_variant)]
439enum LoadStage {
440    LoadingMap,
441    LoadingScenario,
442    GotScenario(Scenario),
443    // Scenario name
444    LoadingPrebaked(String),
445    // Scenario name, maybe prebaked data
446    GotPrebaked(String, Result<Analytics>),
447    Finalizing,
448}
449
450struct SandboxLoader {
451    // Always exists, just a way to avoid clones
452    stage: Option<LoadStage>,
453    mode: GameplayMode,
454    finalize: Option<Box<dyn FnOnce(&mut EventCtx, &mut App) -> Vec<Transition>>>,
455}
456
457impl State<App> for SandboxLoader {
458    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
459        loop {
460            match self.stage.take().unwrap() {
461                LoadStage::LoadingMap => {
462                    return Transition::Push(MapLoader::new_state(
463                        ctx,
464                        app,
465                        self.mode.map_name(),
466                        Box::new(|_, _| {
467                            Transition::Multi(vec![
468                                Transition::Pop,
469                                Transition::ModifyState(Box::new(|state, _, _| {
470                                    let loader = state.downcast_mut::<SandboxLoader>().unwrap();
471                                    loader.stage = Some(LoadStage::LoadingScenario);
472                                })),
473                            ])
474                        }),
475                    ));
476                }
477                LoadStage::LoadingScenario => {
478                    // TODO Can we cache the dynamically generated scenarios, like home_to_work, and
479                    // avoid regenerating with this call?
480                    match ctx.loading_screen("load scenario", |_, timer| {
481                        self.mode.scenario(
482                            app,
483                            app.primary.current_flags.sim_flags.make_rng(),
484                            timer,
485                        )
486                    }) {
487                        gameplay::LoadScenario::Nothing => {
488                            app.set_prebaked(None);
489                            self.stage = Some(LoadStage::Finalizing);
490                            continue;
491                        }
492                        gameplay::LoadScenario::Scenario(scenario) => {
493                            // TODO Consider using the cached app.primary.scenario, if possible.
494                            self.stage = Some(LoadStage::GotScenario(scenario));
495                            continue;
496                        }
497                        gameplay::LoadScenario::Future(future) => {
498                            let (_, outer_progress_rx) = futures_channel::mpsc::channel(1);
499                            let (_, inner_progress_rx) = futures_channel::mpsc::channel(1);
500                            return Transition::Push(FutureLoader::<App, Scenario>::new_state(
501                                ctx,
502                                Box::pin(future),
503                                outer_progress_rx,
504                                inner_progress_rx,
505                                "Loading Scenario",
506                                Box::new(|_, _, scenario| {
507                                    // TODO show error/retry alert?
508                                    let scenario =
509                                        scenario.expect("failed to load scenario from future");
510                                    Transition::Multi(vec![
511                                        Transition::Pop,
512                                        Transition::ModifyState(Box::new(|state, _, _| {
513                                            let loader =
514                                                state.downcast_mut::<SandboxLoader>().unwrap();
515                                            loader.stage = Some(LoadStage::GotScenario(scenario));
516                                        })),
517                                    ])
518                                }),
519                            ));
520                        }
521                        gameplay::LoadScenario::Path(path) => {
522                            // Reuse the cached scenario, if possible.
523                            if let Some(ref scenario) = app.primary.scenario {
524                                if scenario.scenario_name == abstutil::basename(&path) {
525                                    self.stage = Some(LoadStage::GotScenario(scenario.clone()));
526                                    continue;
527                                }
528                            }
529
530                            return Transition::Push(FileLoader::<App, Scenario>::new_state(
531                                ctx,
532                                path,
533                                Box::new(|_, _, _, scenario| {
534                                    // TODO Handle corrupt files
535                                    let scenario = scenario.unwrap();
536                                    Transition::Multi(vec![
537                                        Transition::Pop,
538                                        Transition::ModifyState(Box::new(|state, _, _| {
539                                            let loader =
540                                                state.downcast_mut::<SandboxLoader>().unwrap();
541                                            loader.stage = Some(LoadStage::GotScenario(scenario));
542                                        })),
543                                    ])
544                                }),
545                            ));
546                        }
547                    }
548                }
549                LoadStage::GotScenario(mut scenario) => {
550                    let scenario_name = scenario.scenario_name.clone();
551                    ctx.loading_screen("instantiate scenario", |_, timer| {
552                        app.primary.scenario = Some(scenario.clone());
553
554                        // Use the same RNG as we apply scenario modifiers and instantiate the
555                        // scenario. One unexpected effect will be that parked car seeding (during
556                        // scenario instantiation) may spuriously change if a scenario modifier
557                        // uses the RNG. This is at least consistent with the tests, headless mode,
558                        // and instantiating a scenario from CLI flags.
559                        let mut rng = app.primary.current_flags.sim_flags.make_rng();
560
561                        if let GameplayMode::PlayScenario(_, _, ref modifiers) = self.mode {
562                            for m in modifiers {
563                                scenario = m.apply(&app.primary.map, scenario, &mut rng);
564                            }
565                        }
566
567                        app.primary
568                            .sim
569                            .instantiate(&scenario, &app.primary.map, &mut rng, timer);
570                        app.primary
571                            .sim
572                            .tiny_step(&app.primary.map, &mut app.primary.sim_cb);
573
574                        if let Some(ref mut secondary) = app.secondary {
575                            // TODO Modifiers already applied
576                            secondary.scenario = Some(scenario.clone());
577
578                            secondary.sim.instantiate(
579                                &scenario,
580                                &secondary.map,
581                                // Start fresh here. This will match up with the primary sim,
582                                // unless modifiers used the RNG
583                                &mut secondary.current_flags.sim_flags.make_rng(),
584                                timer,
585                            );
586                            secondary
587                                .sim
588                                .tiny_step(&secondary.map, &mut secondary.sim_cb);
589                        }
590                    });
591
592                    self.stage = Some(LoadStage::LoadingPrebaked(scenario_name));
593                    continue;
594                }
595                LoadStage::LoadingPrebaked(scenario_name) => {
596                    // Maybe we've already got prebaked data for this map+scenario.
597                    if app
598                        .has_prebaked()
599                        .map(|(m, s)| m == app.primary.map.get_name() && s == &scenario_name)
600                        .unwrap_or(false)
601                    {
602                        self.stage = Some(LoadStage::Finalizing);
603                        continue;
604                    }
605
606                    return Transition::Push(FileLoader::<App, Analytics>::new_state(
607                        ctx,
608                        abstio::path_prebaked_results(app.primary.map.get_name(), &scenario_name),
609                        Box::new(move |_, _, _, prebaked| {
610                            Transition::Multi(vec![
611                                Transition::Pop,
612                                Transition::ModifyState(Box::new(move |state, _, _| {
613                                    let loader = state.downcast_mut::<SandboxLoader>().unwrap();
614                                    loader.stage =
615                                        Some(LoadStage::GotPrebaked(scenario_name, prebaked));
616                                })),
617                            ])
618                        }),
619                    ));
620                }
621                LoadStage::GotPrebaked(scenario_name, prebaked) => {
622                    match prebaked {
623                        Ok(prebaked) => {
624                            app.set_prebaked(Some((
625                                app.primary.map.get_name().clone(),
626                                scenario_name,
627                                prebaked,
628                            )));
629                        }
630                        Err(err) => {
631                            warn!(
632                                "No prebaked simulation results for \"{}\" scenario on {} map. \
633                                 This means trip dashboards can't compare current times to any \
634                                 kind of baseline: {}",
635                                scenario_name,
636                                app.primary.map.get_name().describe(),
637                                err
638                            );
639                            app.set_prebaked(None);
640                        }
641                    }
642                    self.stage = Some(LoadStage::Finalizing);
643                    continue;
644                }
645                LoadStage::Finalizing => {
646                    let mut gameplay = self.mode.initialize(ctx, app);
647                    gameplay.recreate_panels(ctx, app);
648                    let sandbox = Box::new(SandboxMode {
649                        controls: SandboxControls::new(ctx, app, gameplay.as_ref()),
650                        gameplay,
651                        gameplay_mode: self.mode.clone(),
652                        recalc_unzoomed_agent: None,
653                        last_cs: app.opts.color_scheme,
654                    });
655
656                    let mut transitions = vec![Transition::Replace(sandbox)];
657                    transitions.extend((self.finalize.take().unwrap())(ctx, app));
658                    return Transition::Multi(transitions);
659                }
660            }
661        }
662    }
663
664    fn draw(&self, _: &mut GfxCtx, _: &App) {}
665}
666
667fn mouseover_unzoomed_agent_circle(ctx: &mut EventCtx, app: &mut App) {
668    let cursor = if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
669        pt
670    } else {
671        return;
672    };
673
674    for id in app
675        .primary
676        .agents
677        .borrow_mut()
678        .calculate_unzoomed_agents(ctx, &app.primary.map, &app.primary.sim, &app.cs)
679        .query_bbox(Circle::new(cursor, Distance::meters(3.0)).get_bounds())
680    {
681        if let Some(pt) = app.primary.sim.canonical_pt_for_agent(id, &app.primary.map) {
682            if Circle::new(pt, unzoomed_agent_radius(id.to_vehicle_type())).contains_pt(cursor) {
683                app.primary.current_selection = Some(ID::from_agent(id));
684            }
685        }
686    }
687}
688
689fn is_daytime(app: &App) -> bool {
690    let hours = app.primary.sim.time().get_hours() % 24;
691    (6..18).contains(&hours)
692}
693
694impl SandboxControls {
695    pub fn new(
696        ctx: &mut EventCtx,
697        app: &App,
698        gameplay: &dyn gameplay::GameplayState,
699    ) -> SandboxControls {
700        SandboxControls {
701            common: if gameplay.has_common() {
702                Some(CommonState::new())
703            } else {
704                None
705            },
706            route_preview: if gameplay.can_examine_objects() {
707                Some(RoutePreview::new())
708            } else {
709                None
710            },
711            tool_panel: if gameplay.has_tool_panel() {
712                Some(tool_panel(ctx))
713            } else {
714                None
715            },
716            time_panel: if gameplay.has_time_panel() {
717                Some(TimePanel::new(ctx, app))
718            } else {
719                None
720            },
721            minimap: if gameplay.has_minimap() {
722                Some(Minimap::new(ctx, app, MinimapController))
723            } else {
724                None
725            },
726        }
727    }
728
729    fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
730        if self.tool_panel.is_some() {
731            self.tool_panel = Some(tool_panel(ctx));
732        }
733        if let Some(ref mut speed) = self.time_panel {
734            speed.recreate_panel(ctx, app);
735        }
736        if let Some(ref mut minimap) = self.minimap {
737            minimap.recreate_panel(ctx, app);
738        }
739    }
740}