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