1 use crate::app::{App, Flags, ShowEverything};
2 use crate::options::Options;
3 use crate::pregame::TitleScreen;
4 use crate::render::DrawOptions;
5 use crate::sandbox::{GameplayMode, SandboxMode};
6 use geom::Polygon;
7 use map_model::PermanentMapEdits;
8 use widgetry::{
9     hotkey, hotkeys, Btn, Canvas, Choice, Drawable, EventCtx, GeomBatch, GfxCtx,
10     HorizontalAlignment, Key, Line, Menu, Outcome, Panel, ScreenRectangle, Text, VerticalAlignment,
11     Widget, GUI,
12 };
13 
14 // This is the top-level of the GUI logic. This module should just manage interactions between the
15 // top-level game states.
16 pub struct Game {
17     // A stack of states
18     states: Vec<Box<dyn State>>,
19     app: App,
20 }
21 
22 impl Game {
new( flags: Flags, opts: Options, start_with_edits: Option<String>, maybe_mode: Option<GameplayMode>, ctx: &mut EventCtx, ) -> Game23     pub fn new(
24         flags: Flags,
25         opts: Options,
26         start_with_edits: Option<String>,
27         maybe_mode: Option<GameplayMode>,
28         ctx: &mut EventCtx,
29     ) -> Game {
30         let title = !opts.dev
31             && !flags.sim_flags.load.contains("player/save")
32             && !flags.sim_flags.load.contains("system/scenarios")
33             && maybe_mode.is_none();
34         let mut app = App::new(flags, opts, ctx, title);
35 
36         // Handle savestates
37         let savestate = if app
38             .primary
39             .current_flags
40             .sim_flags
41             .load
42             .contains("player/saves/")
43         {
44             assert!(maybe_mode.is_none());
45             Some(app.primary.clear_sim())
46         } else {
47             None
48         };
49 
50         // Just apply this here, don't plumb to SimFlags or anything else. We recreate things using
51         // these flags later, but we don't want to keep applying the same edits.
52         if let Some(edits_name) = start_with_edits {
53             // TODO Maybe loading screen
54             let mut timer = abstutil::Timer::new("apply initial edits");
55             let edits = map_model::MapEdits::load(
56                 &app.primary.map,
57                 abstutil::path_edits(app.primary.map.get_name(), &edits_name),
58                 &mut timer,
59             )
60             .unwrap();
61             crate::edit::apply_map_edits(ctx, &mut app, edits);
62             app.primary
63                 .map
64                 .recalculate_pathfinding_after_edits(&mut timer);
65             app.primary.clear_sim();
66         }
67 
68         let states: Vec<Box<dyn State>> = if title {
69             vec![Box::new(TitleScreen::new(ctx, &mut app))]
70         } else {
71             // TODO We're assuming we never wind up starting freeform mode with a synthetic map
72             let mode = maybe_mode.unwrap_or_else(|| {
73                 GameplayMode::Freeform(abstutil::path_map(app.primary.map.get_name()))
74             });
75             vec![SandboxMode::new(ctx, &mut app, mode)]
76         };
77         if let Some(ss) = savestate {
78             // TODO This is weird, we're left in Freeform mode with the wrong UI. Can't instantiate
79             // PlayScenario without clobbering.
80             app.primary.sim = ss;
81         }
82         Game { states, app }
83     }
84 
85     // If true, then the top-most state on the stack needs to be "woken up" with a fake mouseover
86     // event.
execute_transition(&mut self, ctx: &mut EventCtx, transition: Transition) -> bool87     fn execute_transition(&mut self, ctx: &mut EventCtx, transition: Transition) -> bool {
88         match transition {
89             Transition::Keep => false,
90             Transition::KeepWithMouseover => true,
91             Transition::Pop => {
92                 self.states.pop().unwrap().on_destroy(ctx, &mut self.app);
93                 if self.states.is_empty() {
94                     self.before_quit(ctx.canvas);
95                     std::process::exit(0);
96                 }
97                 true
98             }
99             Transition::PopWithData(cb) => {
100                 self.states.pop().unwrap().on_destroy(ctx, &mut self.app);
101                 cb(self.states.last_mut().unwrap(), ctx, &mut self.app);
102                 true
103             }
104             Transition::ReplaceWithData(cb) => {
105                 let mut last = self.states.pop().unwrap();
106                 last.on_destroy(ctx, &mut self.app);
107                 let new_states = cb(last, ctx, &mut self.app);
108                 self.states.extend(new_states);
109                 true
110             }
111             Transition::KeepWithData(cb) => {
112                 cb(self.states.last_mut().unwrap(), ctx, &mut self.app);
113                 true
114             }
115             Transition::Push(state) => {
116                 self.states.push(state);
117                 true
118             }
119             Transition::Replace(state) => {
120                 self.states.pop().unwrap().on_destroy(ctx, &mut self.app);
121                 self.states.push(state);
122                 true
123             }
124             Transition::Clear(states) => {
125                 while !self.states.is_empty() {
126                     self.states.pop().unwrap().on_destroy(ctx, &mut self.app);
127                 }
128                 self.states.extend(states);
129                 true
130             }
131             Transition::Multi(list) => {
132                 // Always wake-up just the last state remaining after the sequence
133                 for t in list {
134                     self.execute_transition(ctx, t);
135                 }
136                 true
137             }
138         }
139     }
140 }
141 
142 impl GUI for Game {
event(&mut self, ctx: &mut EventCtx)143     fn event(&mut self, ctx: &mut EventCtx) {
144         self.app.per_obj.reset();
145 
146         let transition = self.states.last_mut().unwrap().event(ctx, &mut self.app);
147         if self.execute_transition(ctx, transition) {
148             // Let the new state initialize with a fake event. Usually these just return
149             // Transition::Keep, but nothing stops them from doing whatever. (For example, entering
150             // tutorial mode immediately pushes on a Warper.) So just recurse.
151             ctx.no_op_event(true, |ctx| self.event(ctx));
152         }
153     }
154 
draw(&self, g: &mut GfxCtx)155     fn draw(&self, g: &mut GfxCtx) {
156         let state = self.states.last().unwrap();
157 
158         match state.draw_baselayer() {
159             DrawBaselayer::DefaultMap => {
160                 self.app.draw(
161                     g,
162                     DrawOptions::new(),
163                     &self.app.primary.sim,
164                     &ShowEverything::new(),
165                 );
166             }
167             DrawBaselayer::Custom => {}
168             DrawBaselayer::PreviousState => {
169                 match self.states[self.states.len() - 2].draw_baselayer() {
170                     DrawBaselayer::DefaultMap => {
171                         self.app.draw(
172                             g,
173                             DrawOptions::new(),
174                             &self.app.primary.sim,
175                             &ShowEverything::new(),
176                         );
177                     }
178                     DrawBaselayer::Custom => {}
179                     // Nope, don't recurse
180                     DrawBaselayer::PreviousState => {}
181                 }
182 
183                 self.states[self.states.len() - 2].draw(g, &self.app);
184             }
185         }
186         state.draw(g, &self.app);
187     }
188 
dump_before_abort(&self, canvas: &Canvas)189     fn dump_before_abort(&self, canvas: &Canvas) {
190         println!();
191         println!(
192             "********************************************************************************"
193         );
194         canvas.save_camera_state(self.app.primary.map.get_name());
195         println!(
196             "Crash! Please report to https://github.com/dabreegster/abstreet/issues/ and include \
197              all output.txt; at least everything starting from the stack trace above!"
198         );
199 
200         println!();
201         self.app.primary.sim.dump_before_abort();
202 
203         println!();
204         println!("Camera:");
205         println!(
206             r#"{{ "cam_x": {}, "cam_y": {}, "cam_zoom": {} }}"#,
207             canvas.cam_x, canvas.cam_y, canvas.cam_zoom
208         );
209 
210         println!();
211         if self.app.primary.map.get_edits().commands.is_empty() {
212             println!("No edits");
213         } else {
214             println!("Edits:");
215             println!(
216                 "{}",
217                 abstutil::to_json(&PermanentMapEdits::to_permanent(
218                     self.app.primary.map.get_edits(),
219                     &self.app.primary.map
220                 ))
221             );
222         }
223 
224         // Repeat, because it can be hard to see the top of the report if it's long
225         println!();
226         println!(
227             "Crash! Please report to https://github.com/dabreegster/abstreet/issues/ and include \
228              all output.txt; at least everything above here until the start of the report!"
229         );
230         println!(
231             "********************************************************************************"
232         );
233     }
234 
before_quit(&self, canvas: &Canvas)235     fn before_quit(&self, canvas: &Canvas) {
236         canvas.save_camera_state(self.app.primary.map.get_name());
237     }
238 }
239 
240 pub enum DrawBaselayer {
241     DefaultMap,
242     Custom,
243     PreviousState,
244 }
245 
246 pub trait State: downcast_rs::Downcast {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition247     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition;
draw(&self, g: &mut GfxCtx, app: &App)248     fn draw(&self, g: &mut GfxCtx, app: &App);
249 
draw_baselayer(&self) -> DrawBaselayer250     fn draw_baselayer(&self) -> DrawBaselayer {
251         DrawBaselayer::DefaultMap
252     }
253 
254     // Before this state is popped or replaced, call this.
on_destroy(&mut self, _: &mut EventCtx, _: &mut App)255     fn on_destroy(&mut self, _: &mut EventCtx, _: &mut App) {}
256     // We don't need an on_enter -- the constructor for the state can just do it.
257 }
258 
259 impl dyn State {
grey_out_map(g: &mut GfxCtx, app: &App)260     pub fn grey_out_map(g: &mut GfxCtx, app: &App) {
261         // Make it clear the map can't be interacted with right now.
262         g.fork_screenspace();
263         // TODO - OSD height
264         g.draw_polygon(
265             app.cs.fade_map_dark,
266             Polygon::rectangle(g.canvas.window_width, g.canvas.window_height),
267         );
268         g.unfork();
269     }
270 }
271 
272 downcast_rs::impl_downcast!(State);
273 
274 pub enum Transition {
275     Keep,
276     KeepWithMouseover,
277     Pop,
278     // If a state needs to pass data back to the parent, use this. Sadly, runtime type casting.
279     // TODO Collapse some of these cases too
280     PopWithData(Box<dyn FnOnce(&mut Box<dyn State>, &mut EventCtx, &mut App)>),
281     KeepWithData(Box<dyn FnOnce(&mut Box<dyn State>, &mut EventCtx, &mut App)>),
282     ReplaceWithData(
283         Box<dyn FnOnce(Box<dyn State>, &mut EventCtx, &mut App) -> Vec<Box<dyn State>>>,
284     ),
285     Push(Box<dyn State>),
286     Replace(Box<dyn State>),
287     Clear(Vec<Box<dyn State>>),
288     Multi(Vec<Transition>),
289 }
290 
291 pub struct ChooseSomething<T> {
292     panel: Panel,
293     cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
294 }
295 
296 impl<T: 'static> ChooseSomething<T> {
new( ctx: &mut EventCtx, query: &str, choices: Vec<Choice<T>>, cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>, ) -> Box<dyn State>297     pub fn new(
298         ctx: &mut EventCtx,
299         query: &str,
300         choices: Vec<Choice<T>>,
301         cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
302     ) -> Box<dyn State> {
303         Box::new(ChooseSomething {
304             panel: Panel::new(Widget::col(vec![
305                 Widget::row(vec![
306                     Line(query).small_heading().draw(ctx),
307                     Btn::plaintext("X")
308                         .build(ctx, "close", hotkey(Key::Escape))
309                         .align_right(),
310                 ]),
311                 Menu::new(ctx, choices).named("menu"),
312             ]))
313             .build(ctx),
314             cb,
315         })
316     }
317 
new_below( ctx: &mut EventCtx, rect: &ScreenRectangle, choices: Vec<Choice<T>>, cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>, ) -> Box<dyn State>318     pub fn new_below(
319         ctx: &mut EventCtx,
320         rect: &ScreenRectangle,
321         choices: Vec<Choice<T>>,
322         cb: Box<dyn Fn(T, &mut EventCtx, &mut App) -> Transition>,
323     ) -> Box<dyn State> {
324         Box::new(ChooseSomething {
325             panel: Panel::new(Menu::new(ctx, choices).named("menu").container())
326                 .aligned(
327                     HorizontalAlignment::Centered(rect.center().x),
328                     VerticalAlignment::Below(rect.y2 + 15.0),
329                 )
330                 .build(ctx),
331             cb,
332         })
333     }
334 }
335 
336 impl<T: 'static> State for ChooseSomething<T> {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition337     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
338         match self.panel.event(ctx) {
339             Outcome::Clicked(x) => match x.as_ref() {
340                 "close" => Transition::Pop,
341                 _ => {
342                     let data = self.panel.take_menu_choice::<T>("menu");
343                     (self.cb)(data, ctx, app)
344                 }
345             },
346             _ => {
347                 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
348                     return Transition::Pop;
349                 }
350                 // new_below doesn't make an X button
351                 if ctx.input.key_pressed(Key::Escape) {
352                     return Transition::Pop;
353                 }
354                 Transition::Keep
355             }
356         }
357     }
358 
draw_baselayer(&self) -> DrawBaselayer359     fn draw_baselayer(&self) -> DrawBaselayer {
360         DrawBaselayer::PreviousState
361     }
362 
draw(&self, g: &mut GfxCtx, app: &App)363     fn draw(&self, g: &mut GfxCtx, app: &App) {
364         State::grey_out_map(g, app);
365         self.panel.draw(g);
366     }
367 }
368 
369 pub struct PromptInput {
370     panel: Panel,
371     cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
372 }
373 
374 impl PromptInput {
new( ctx: &mut EventCtx, query: &str, cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>, ) -> Box<dyn State>375     pub fn new(
376         ctx: &mut EventCtx,
377         query: &str,
378         cb: Box<dyn Fn(String, &mut EventCtx, &mut App) -> Transition>,
379     ) -> Box<dyn State> {
380         Box::new(PromptInput {
381             panel: Panel::new(Widget::col(vec![
382                 Widget::row(vec![
383                     Line(query).small_heading().draw(ctx),
384                     Btn::plaintext("X")
385                         .build(ctx, "close", hotkey(Key::Escape))
386                         .align_right(),
387                 ]),
388                 Widget::text_entry(ctx, String::new(), true).named("input"),
389                 Btn::text_fg("confirm").build_def(ctx, hotkey(Key::Enter)),
390             ]))
391             .build(ctx),
392             cb,
393         })
394     }
395 }
396 
397 impl State for PromptInput {
event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition398     fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
399         match self.panel.event(ctx) {
400             Outcome::Clicked(x) => match x.as_ref() {
401                 "close" => Transition::Pop,
402                 "confirm" => {
403                     let data = self.panel.text_box("input");
404                     (self.cb)(data, ctx, app)
405                 }
406                 _ => unreachable!(),
407             },
408             _ => {
409                 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
410                     return Transition::Pop;
411                 }
412                 Transition::Keep
413             }
414         }
415     }
416 
draw_baselayer(&self) -> DrawBaselayer417     fn draw_baselayer(&self) -> DrawBaselayer {
418         DrawBaselayer::PreviousState
419     }
420 
draw(&self, g: &mut GfxCtx, app: &App)421     fn draw(&self, g: &mut GfxCtx, app: &App) {
422         State::grey_out_map(g, app);
423         self.panel.draw(g);
424     }
425 }
426 
427 pub struct PopupMsg {
428     panel: Panel,
429     unzoomed: Drawable,
430     zoomed: Drawable,
431 }
432 
433 impl PopupMsg {
new<I: Into<String>>(ctx: &mut EventCtx, title: &str, lines: Vec<I>) -> Box<dyn State>434     pub fn new<I: Into<String>>(ctx: &mut EventCtx, title: &str, lines: Vec<I>) -> Box<dyn State> {
435         PopupMsg::also_draw(
436             ctx,
437             title,
438             lines,
439             ctx.upload(GeomBatch::new()),
440             ctx.upload(GeomBatch::new()),
441         )
442     }
443 
also_draw<I: Into<String>>( ctx: &mut EventCtx, title: &str, lines: Vec<I>, unzoomed: Drawable, zoomed: Drawable, ) -> Box<dyn State>444     pub fn also_draw<I: Into<String>>(
445         ctx: &mut EventCtx,
446         title: &str,
447         lines: Vec<I>,
448         unzoomed: Drawable,
449         zoomed: Drawable,
450     ) -> Box<dyn State> {
451         let mut txt = Text::new();
452         txt.add(Line(title).small_heading());
453         for l in lines {
454             txt.add(Line(l));
455         }
456         Box::new(PopupMsg {
457             panel: Panel::new(Widget::col(vec![
458                 txt.draw(ctx),
459                 Btn::text_bg2("OK").build_def(ctx, hotkeys(vec![Key::Enter, Key::Escape])),
460             ]))
461             .build(ctx),
462             unzoomed,
463             zoomed,
464         })
465     }
466 }
467 
468 impl State for PopupMsg {
event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition469     fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
470         match self.panel.event(ctx) {
471             Outcome::Clicked(x) => match x.as_ref() {
472                 "OK" => Transition::Pop,
473                 _ => unreachable!(),
474             },
475             _ => {
476                 if ctx.normal_left_click() && ctx.canvas.get_cursor_in_screen_space().is_none() {
477                     return Transition::Pop;
478                 }
479                 Transition::Keep
480             }
481         }
482     }
483 
draw_baselayer(&self) -> DrawBaselayer484     fn draw_baselayer(&self) -> DrawBaselayer {
485         DrawBaselayer::PreviousState
486     }
487 
draw(&self, g: &mut GfxCtx, app: &App)488     fn draw(&self, g: &mut GfxCtx, app: &App) {
489         State::grey_out_map(g, app);
490         self.panel.draw(g);
491         if g.canvas.cam_zoom < app.opts.min_zoom_for_detail {
492             g.redraw(&self.unzoomed);
493         } else {
494             g.redraw(&self.zoomed);
495         }
496     }
497 }
498