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 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 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 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 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 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 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 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 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
303pub 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#[allow(clippy::large_enum_variant)]
439enum LoadStage {
440 LoadingMap,
441 LoadingScenario,
442 GotScenario(Scenario),
443 LoadingPrebaked(String),
445 GotPrebaked(String, Result<Analytics>),
447 Finalizing,
448}
449
450struct SandboxLoader {
451 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 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 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 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 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 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 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 secondary.scenario = Some(scenario.clone());
577
578 secondary.sim.instantiate(
579 &scenario,
580 &secondary.map,
581 &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 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}