1#![allow(clippy::too_many_arguments, clippy::type_complexity)]
3
4#[macro_use]
5extern crate anyhow;
6#[macro_use]
7extern crate log;
8
9use structopt::StructOpt;
10
11use abstio::MapName;
12use abstutil::Timer;
13use geom::Duration;
14use map_gui::colors::ColorSchemeChoice;
15use map_gui::options::Options;
16use map_model::{Map, MapEdits};
17use sim::Sim;
18use synthpop::Scenario;
19use widgetry::tools::{FutureLoader, PopupMsg, URLManager};
20use widgetry::{EventCtx, GfxCtx, Settings, State};
21
22use crate::app::{App, Flags, PerMap, Transition};
23use crate::common::jump_to_time_upon_startup;
24use crate::id::ID;
25use crate::pregame::TitleScreen;
26use crate::sandbox::{GameplayMode, SandboxMode};
27
28mod app;
29mod challenges;
30mod common;
31mod debug;
32mod devtools;
33mod edit;
34mod id;
35mod info;
36mod layer;
37mod pregame;
38mod render;
39mod sandbox;
40mod ungap;
41
42pub fn main() {
43 let settings = Settings::new("A/B Street");
44 run(settings);
45}
46
47#[derive(StructOpt)]
48#[structopt(name = "abstreet", about = "The A/B Street traffic simulator")]
49struct Args {
50 #[structopt(flatten)]
51 flags: Flags,
52 #[structopt(long = "edits")]
54 start_with_edits: Option<String>,
55 #[structopt(long)]
58 cam: Option<String>,
59 #[structopt(long = "time", parse(try_from_str = Duration::parse))]
61 start_time: Option<Duration>,
62 #[structopt(long = "diff")]
64 diff_map: Option<String>,
65 #[structopt(long)]
67 dump_raw_events: bool,
68 #[structopt(long)]
70 scale_factor: Option<f64>,
71
72 #[structopt(long)]
75 dev: bool,
76 #[structopt(long, parse(try_from_str = ColorSchemeChoice::parse))]
78 color_scheme: Option<ColorSchemeChoice>,
79 #[structopt(long)]
81 minimal_controls: bool,
82
83 #[structopt(long)]
85 prebake: bool,
86
87 #[structopt(long)]
89 tutorial_intro: bool,
90 #[structopt(long)]
92 challenges: bool,
93 #[structopt(long)]
95 sandbox: bool,
96 #[structopt(long)]
98 proposals: bool,
99 #[structopt(long)]
101 ungap: bool,
102 #[structopt(long)]
104 devtools: bool,
105 #[structopt(long = "kml")]
107 load_kml: Option<String>,
108 #[structopt(long)]
110 challenge: Option<String>,
111 #[structopt(long)]
113 tutorial: Option<usize>,
114 #[structopt(long)]
116 layer: Option<String>,
117 #[structopt(long)]
119 info: Option<String>,
120 #[structopt(long)]
122 actdev: Option<String>,
123 #[structopt(long)]
125 actdev_scenario: Option<String>,
126 #[structopt(long)]
128 compare_counts: Option<Vec<String>>,
129}
130
131struct Setup {
132 flags: Flags,
133 opts: Options,
134 start_with_edits: Option<String>,
135 initialize_tutorial: bool,
136 center_camera: Option<String>,
137 start_time: Option<Duration>,
138 diff_map: Option<String>,
139 mode: Mode,
140 start_with_layer: Option<String>,
141 start_with_info_panel: Option<String>,
142}
143
144#[derive(PartialEq)]
147enum Mode {
148 SomethingElse,
149 TutorialIntro,
150 Challenges,
151 Sandbox,
152 Proposals,
153 Ungap,
154 Devtools,
155 LoadKML(String),
156 CompareCounts(String, String),
157 Gameplay(GameplayMode),
158}
159
160fn run(mut settings: Settings) {
161 abstutil::logger::setup();
162
163 settings = settings
164 .read_svg(Box::new(abstio::slurp_bytes))
165 .window_icon(abstio::path("system/assets/pregame/icon.png"))
166 .loading_tips(map_gui::tools::loading_tips())
167 .require_minimum_width(1500.0);
169
170 let mut args = Args::from_iter(abstutil::cli_args());
171 args.flags.sim_flags.initialize();
172
173 if args.prebake {
174 challenges::prebake::prebake_all();
175 return;
176 }
177
178 let mut setup = Setup {
179 flags: args.flags,
180 opts: Options::load_or_default(),
181 start_with_edits: args.start_with_edits,
182 initialize_tutorial: false,
183 center_camera: args.cam,
184 start_time: args.start_time,
185 diff_map: args.diff_map,
186 start_with_layer: args.layer,
187 start_with_info_panel: args.info,
188 mode: if args.tutorial_intro {
189 Mode::TutorialIntro
190 } else if args.challenges {
191 Mode::Challenges
192 } else if args.sandbox {
193 Mode::Sandbox
194 } else if args.proposals {
195 Mode::Proposals
196 } else if args.ungap {
197 Mode::Ungap
198 } else if args.devtools {
199 Mode::Devtools
200 } else if let Some(kml) = args.load_kml {
201 Mode::LoadKML(kml)
202 } else if let Some(mut paths) = args.compare_counts {
203 if paths.len() != 2 {
204 panic!("--compare-counts takes exactly two paths");
205 }
206 Mode::CompareCounts(paths.remove(0), paths.remove(0))
207 } else {
208 Mode::SomethingElse
209 },
210 };
211
212 setup.opts.toggle_day_night_colors = true;
213 setup.opts.dev = args.dev;
215 setup.opts.minimal_controls = args.minimal_controls;
216 if let Some(cs) = args.color_scheme {
217 setup.opts.color_scheme = cs;
218 setup.opts.toggle_day_night_colors = false;
219 }
220
221 settings = settings.canvas_settings(setup.opts.canvas_settings.clone());
222
223 if args.dump_raw_events {
224 settings = settings.dump_raw_events();
225 }
226 if let Some(s) = args.scale_factor {
227 settings = settings.scale_factor(s);
228 }
229
230 if let Some(x) = args.challenge {
231 assert!(setup.mode == Mode::SomethingElse);
233 let mut aliases = Vec::new();
234 'OUTER: for (_, stages) in challenges::Challenge::all() {
235 for challenge in stages {
236 if challenge.alias == x {
237 setup.flags.sim_flags.load = challenge.gameplay.map_name().path();
238 setup.mode = Mode::Gameplay(challenge.gameplay);
239 break 'OUTER;
240 } else {
241 aliases.push(challenge.alias);
242 }
243 }
244 }
245 if setup.mode == Mode::SomethingElse {
246 panic!("Invalid --challenge={}. Choices: {}", x, aliases.join(", "));
247 }
248 }
249 if let Some(n) = args.tutorial {
250 setup.initialize_tutorial = true;
251 setup.mode = Mode::Gameplay(sandbox::GameplayMode::Tutorial(
252 sandbox::TutorialPointer::new(n - 1, 0),
253 ));
254 }
255
256 let modifiers = setup.flags.sim_flags.scenario_modifiers.drain(..).collect();
259
260 if setup.mode == Mode::SomethingElse && setup.flags.sim_flags.load.contains("scenarios/") {
261 let (map_name, scenario) = abstio::parse_scenario_path(&setup.flags.sim_flags.load);
262 setup.flags.sim_flags.load = map_name.path();
263 setup.mode = Mode::Gameplay(sandbox::GameplayMode::PlayScenario(
264 map_name, scenario, modifiers,
265 ));
266 }
267
268 if let Some(site) = args.actdev {
269 let site = site.replace("_", "-");
272 let city = site.replace("-", "_");
273 let name = MapName::new("gb", &city, "center");
274 setup.flags.sim_flags.load = name.path();
275 setup.flags.study_area = Some(site);
276 setup.flags.sim_flags.opts.infinite_parking = true;
279 let scenario = if args.actdev_scenario == Some("go_active".to_string()) {
280 "go_active".to_string()
281 } else {
282 "base".to_string()
283 };
284 setup.mode = Mode::Gameplay(sandbox::GameplayMode::Actdev(name, scenario, false));
285 }
286
287 widgetry::run(settings, |ctx| setup_app(ctx, setup))
288}
289
290fn setup_app(ctx: &mut EventCtx, mut setup: Setup) -> (App, Vec<Box<dyn State<App>>>) {
291 let title = !setup.opts.dev
292 && !setup.flags.sim_flags.load.contains("player/save")
293 && !setup.flags.sim_flags.load.contains("/scenarios/")
294 && setup.mode == Mode::SomethingElse;
295
296 if title && setup.flags.sim_flags.load == MapName::seattle("montlake").path() {
298 if let Ok(default) = abstio::maybe_read_json::<map_gui::tools::DefaultMap>(
299 abstio::path_player("maps.json"),
300 &mut Timer::throwaway(),
301 ) {
302 setup.flags.sim_flags.load = default.last_map.path();
303 }
304 }
305
306 if let Mode::Gameplay(
311 GameplayMode::PlayScenario(_, _, _)
312 | GameplayMode::FixTrafficSignals
313 | GameplayMode::OptimizeCommute(_, _)
314 | GameplayMode::Tutorial(_),
315 ) = setup.mode
316 {
317 setup.opts.color_scheme = map_gui::colors::ColorSchemeChoice::NightMode;
318 }
319 if setup.mode != Mode::SomethingElse {
320 setup.opts.color_scheme = map_gui::colors::ColorSchemeChoice::DayMode;
321 }
322 let cs = map_gui::colors::ColorScheme::new(ctx, setup.opts.color_scheme);
323
324 let secondary = setup.diff_map.as_ref().map(|path| {
326 ctx.loading_screen("load secondary map", |ctx, timer| {
327 let mut map: Map = abstio::read_binary(path.clone(), timer);
330 map.map_loaded_directly(timer);
331 let sim = Sim::new(&map, setup.flags.sim_flags.opts.clone());
332 let mut per_map =
333 PerMap::map_loaded(map, sim, setup.flags.clone(), &setup.opts, &cs, ctx, timer);
334 per_map.is_secondary = true;
335 per_map
336 })
337 });
338
339 if setup.flags.sim_flags.load.contains("/maps/") {
345 let map = Map::blank();
347 let sim = Sim::new(&map, setup.flags.sim_flags.opts.clone());
348 let primary = PerMap::map_loaded(
349 map,
350 sim,
351 setup.flags.clone(),
352 &setup.opts,
353 &cs,
354 ctx,
355 &mut Timer::throwaway(),
356 );
357 let app = App {
358 primary,
359 secondary: None,
360 store_unedited_map_in_secondary: false,
361 cs,
362 opts: setup.opts.clone(),
363 per_obj: crate::app::PerObjectActions::new(),
364 session: crate::app::SessionState::empty(),
365 };
366 let map_name = MapName::from_path(&app.primary.current_flags.sim_flags.load).unwrap();
367 let states = vec![map_gui::load::MapLoader::new_state(
368 ctx,
369 &app,
370 map_name,
371 Box::new(move |ctx, app| {
372 Transition::Clear(continue_app_setup(ctx, app, title, setup, secondary))
373 }),
374 )];
375 (app, states)
376 } else {
377 let primary = ctx.loading_screen("load map", |ctx, timer| {
380 assert!(setup.flags.sim_flags.scenario_modifiers.is_empty());
381 let (map, sim, _) = setup.flags.sim_flags.load_synchronously(timer);
382 PerMap::map_loaded(map, sim, setup.flags.clone(), &setup.opts, &cs, ctx, timer)
383 });
384 assert!(secondary.is_none());
385 let mut app = App {
386 primary,
387 secondary: None,
388 store_unedited_map_in_secondary: false,
389 cs,
390 opts: setup.opts.clone(),
391 per_obj: crate::app::PerObjectActions::new(),
392 session: crate::app::SessionState::empty(),
393 };
394
395 let states = continue_app_setup(ctx, &mut app, title, setup, None);
396 (app, states)
397 }
398}
399
400fn continue_app_setup(
401 ctx: &mut EventCtx,
402 app: &mut App,
403 title: bool,
404 setup: Setup,
405 secondary: Option<PerMap>,
406) -> Vec<Box<dyn State<App>>> {
407 app.secondary = secondary;
409
410 if !URLManager::change_camera(
411 ctx,
412 setup.center_camera.as_ref(),
413 app.primary.map.get_gps_bounds(),
414 ) {
415 app.primary.init_camera_for_loaded_map(ctx);
416 }
417
418 let savestate = if app
420 .primary
421 .current_flags
422 .sim_flags
423 .load
424 .contains("player/saves/")
425 {
426 assert!(setup.mode == Mode::SomethingElse);
427 Some(app.primary.clear_sim())
428 } else {
429 None
430 };
431
432 if let Some(ref edits_name) = setup.start_with_edits {
435 if let Some(id) = edits_name.strip_prefix("remote/") {
437 let (_, outer_progress_rx) = futures_channel::mpsc::channel(1);
438 let (_, inner_progress_rx) = futures_channel::mpsc::channel(1);
439 let url = format!("{}/get?id={}", crate::common::share::PROPOSAL_HOST_URL, id);
440 return vec![FutureLoader::<App, Vec<u8>>::new_state(
441 ctx,
442 Box::pin(async move {
443 let bytes = abstio::http_get(url).await?;
444 let wrapper: Box<dyn Send + FnOnce(&App) -> Vec<u8>> = Box::new(move |_| bytes);
445 Ok(wrapper)
446 }),
447 outer_progress_rx,
448 inner_progress_rx,
449 "Downloading proposal",
450 Box::new(move |ctx, app, result| {
451 match result
452 .and_then(|bytes| MapEdits::load_from_bytes(&app.primary.map, bytes))
453 {
454 Ok(edits) => Transition::Clear(finish_app_setup(
455 ctx,
456 app,
457 title,
458 savestate,
459 Some(edits),
460 setup,
461 )),
462 Err(err) => {
463 error!("Couldn't load remote proposal: {}", err);
466 Transition::Replace(PopupMsg::new_state(
467 ctx,
468 "Couldn't load remote proposal",
469 vec![err.to_string()],
470 ))
471 }
472 }
473 }),
474 )];
475 }
476
477 for path in [
478 abstio::path_edits(app.primary.map.get_name(), edits_name),
479 abstio::path(format!("system/proposals/{}.json", edits_name)),
480 ] {
481 if abstio::file_exists(&path) {
482 let edits = map_model::MapEdits::load_from_file(
483 &app.primary.map,
484 path,
485 &mut Timer::throwaway(),
486 )
487 .unwrap();
488 return finish_app_setup(ctx, app, title, savestate, Some(edits), setup);
489 }
490 }
491
492 panic!("Can't start with nonexistent edits {}", edits_name);
494 }
495
496 finish_app_setup(ctx, app, title, savestate, None, setup)
497}
498
499fn finish_app_setup(
500 ctx: &mut EventCtx,
501 app: &mut App,
502 title: bool,
503 savestate: Option<Sim>,
504 edits: Option<MapEdits>,
505 setup: Setup,
506) -> Vec<Box<dyn State<App>>> {
507 if setup.mode == Mode::Ungap {
508 app.store_unedited_map_in_secondary = true;
509 }
510 if let Some(edits) = edits {
511 ctx.loading_screen("apply initial edits", |ctx, timer| {
512 crate::edit::apply_map_edits(ctx, app, edits);
513 app.primary.map.recalculate_pathfinding_after_edits(timer);
514 app.primary.clear_sim();
515 });
516 }
517
518 if setup.initialize_tutorial {
519 crate::sandbox::gameplay::Tutorial::initialize(ctx, app);
520 }
521
522 if title {
523 return vec![TitleScreen::new_state(ctx, app)];
524 }
525
526 if let Some(layer_name) = setup.start_with_layer {
528 match layer_name.as_str() {
529 "steep_streets" => {
530 app.primary.layer = Some(Box::new(layer::elevation::SteepStreets::new(ctx, app)));
531 }
532 "elevation" => {
533 app.primary.layer = Some(Box::new(layer::elevation::ElevationContours::new(ctx, app)));
534 }
535 "map_edits" => {
536 app.primary.layer = Some(Box::new(layer::map::Static::edits(ctx, app)));
537 }
538 "no_sidewalks" => {
539 app.primary.layer = Some(Box::new(layer::map::Static::no_sidewalks(ctx, app)));
540 }
541 "parking_occupancy" => {
542 warn!("parking_occupancy layer not implemented in URL parameters yet");
544 }
545 "transit_network" => {
546 app.primary.layer = Some(Box::new(layer::transit::TransitNetwork::new(ctx, app, true, true, true)));
548 }
549 _ => {
550 warn!("Unknown layer: {}", layer_name);
551 }
552 }
553 }
554
555 let state = if let Some(ss) = savestate {
556 app.primary.sim = ss;
557 SandboxMode::start_from_savestate(app)
558 } else {
559 match setup.mode {
560 Mode::Gameplay(gameplay) => {
561 if let GameplayMode::Actdev(_, _, _) = gameplay {
562 SandboxMode::async_new(
563 app,
564 gameplay,
565 jump_to_time_upon_startup(Duration::hours(8)),
566 )
567 } else if let Some(t) = setup.start_time {
568 SandboxMode::async_new(app, gameplay, jump_to_time_upon_startup(t))
569 } else {
570 SandboxMode::simple_new(app, gameplay)
571 }
572 }
573 Mode::SomethingElse => {
574 let start_time = setup.start_time.unwrap_or(Duration::hours(6));
575
576 if let Some(ref mut secondary) = app.secondary {
580 secondary.sim.timed_step(
581 &secondary.map,
582 start_time,
583 &mut None,
584 &mut Timer::throwaway(),
585 );
586 }
587
588 SandboxMode::async_new(
591 app,
592 GameplayMode::Freeform(app.primary.map.get_name().clone()),
593 jump_to_time_upon_startup(start_time),
594 )
595 }
596 Mode::TutorialIntro => sandbox::gameplay::Tutorial::start(ctx, app),
597 Mode::Challenges => challenges::ChallengesPicker::new_state(ctx, app),
598 Mode::Sandbox => SandboxMode::simple_new(
599 app,
600 GameplayMode::PlayScenario(
601 app.primary.map.get_name().clone(),
602 Scenario::default_scenario_for_map(app.primary.map.get_name()),
603 Vec::new(),
604 ),
605 ),
606 Mode::Proposals => pregame::proposals::Proposals::new_state(ctx, None),
607 Mode::Ungap => {
608 let layers = ungap::Layers::new(ctx, app);
609 ungap::ExploreMap::new_state(ctx, app, layers)
610 }
611 Mode::Devtools => devtools::DevToolsMode::new_state(ctx, app),
612 Mode::LoadKML(path) => {
614 crate::devtools::kml::ViewKML::new_state(ctx, app, Some((path, Vec::new())))
615 }
616 Mode::CompareCounts(path1, path2) => {
617 crate::devtools::compare_counts::GenericCompareCounts::new_state(
618 ctx, app, path1, path2,
619 )
620 }
621 }
622 };
623
624 let mut states = vec![TitleScreen::new_state(ctx, app), state];
625
626 if let Some(info_id) = setup.start_with_info_panel {
628 states.push(Box::new(InitialInfoPanel { info_id }));
629 }
630
631 states
632}
633
634struct InitialInfoPanel {
635 info_id: String,
636}
637
638impl State<App> for InitialInfoPanel {
639 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
640 match crate::common::inner_warp_to_id(ctx, app, &self.info_id) {
641 Some(t) => t,
642 None => {
643 warn!("Invalid info ID: {}", self.info_id);
644 Transition::Pop
645 }
646 }
647 }
648
649 fn draw(&self, _: &mut GfxCtx, _: &App) {}
650}
651
652#[cfg(target_arch = "wasm32")]
653use wasm_bindgen::prelude::*;
654
655#[cfg(target_arch = "wasm32")]
656#[wasm_bindgen(js_name = "run")]
657pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
658 let settings = Settings::new("A/B Street")
659 .root_dom_element_id(root_dom_id)
660 .assets_base_url(assets_base_url)
661 .assets_are_gzipped(assets_are_gzipped);
662
663 run(settings);
664}