game/
lib.rs

1// Disable some noisy lints
2#![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    /// Start with these map edits loaded. This should be the name of edits, not a full path.
53    #[structopt(long = "edits")]
54    start_with_edits: Option<String>,
55    /// Initially position the camera here. The format is an OSM-style `zoom/lat/lon` string
56    /// (https://wiki.openstreetmap.org/wiki/Browsing#Other_URL_tricks).
57    #[structopt(long)]
58    cam: Option<String>,
59    /// Start the simulation at this time
60    #[structopt(long = "time", parse(try_from_str = Duration::parse))]
61    start_time: Option<Duration>,
62    /// Load the map at this path as a secondary debug map to compare to the main one
63    #[structopt(long = "diff")]
64    diff_map: Option<String>,
65    /// Print raw widgetry events to the console for debugging
66    #[structopt(long)]
67    dump_raw_events: bool,
68    /// Override the monitor's auto-detected scale factor
69    #[structopt(long)]
70    scale_factor: Option<f64>,
71
72    /// Dev mode exposes experimental tools useful for debugging, but that'd likely confuse most
73    /// players.
74    #[structopt(long)]
75    dev: bool,
76    /// The color scheme for map elements, agents, and the UI.
77    #[structopt(long, parse(try_from_str = ColorSchemeChoice::parse))]
78    color_scheme: Option<ColorSchemeChoice>,
79    /// When making a screen recording, enable this option to hide some UI elements
80    #[structopt(long)]
81    minimal_controls: bool,
82
83    /// Run a configured set of simulations and record prebaked data.
84    #[structopt(long)]
85    prebake: bool,
86
87    /// Start at the tutorial intro screen
88    #[structopt(long)]
89    tutorial_intro: bool,
90    /// Start by listing gameplay challenges
91    #[structopt(long)]
92    challenges: bool,
93    /// Start in the simulation sandbox mode
94    #[structopt(long)]
95    sandbox: bool,
96    /// Start by showing community proposals
97    #[structopt(long)]
98    proposals: bool,
99    /// Launch Ungap the Map, a bike network planning tool
100    #[structopt(long)]
101    ungap: bool,
102    /// Start by listing internal developer tools
103    #[structopt(long)]
104    devtools: bool,
105    /// Start by showing this KMl file in a debug viewer
106    #[structopt(long = "kml")]
107    load_kml: Option<String>,
108    /// Start playing a particular challenge
109    #[structopt(long)]
110    challenge: Option<String>,
111    /// Start on a particular tutorial stage
112    #[structopt(long)]
113    tutorial: Option<usize>,
114    /// Start with a specific layer enabled. Example: steep_streets
115    #[structopt(long)]
116    layer: Option<String>,
117    /// Start with a specific info panel open. Example: b42 for building 42
118    #[structopt(long)]
119    info: Option<String>,
120    /// Start in ActDev mode for a particular site name.
121    #[structopt(long)]
122    actdev: Option<String>,
123    /// Start by showing an ActDev scenario. Either "base" or "go_active".
124    #[structopt(long)]
125    actdev_scenario: Option<String>,
126    /// Start in a tool for comparing traffic counts
127    #[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// TODO Switch to explicit enum subcommands, each of which includes precisely the set of common
145// flags that're valid for that mode
146#[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        // This is approximately how much the 3 top panels in sandbox mode require.
168        .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    // Update options from CLI flags
214    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        // TODO This is a weak form of mutual exclusion; just use subcommands
232        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    // Don't keep the scenario modifiers in the original sim_flags; they shouldn't apply to
257    // other scenarios loaed in the UI later.
258    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        // Handle if the site was accidentally passed in with underscores. Otherwise, some study
270        // areas won't be found!
271        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        // Parking data in the actdev maps is nonexistent, so many people have convoluted walking
277        // routes just to fetch their car. Just disable parking entirely.
278        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    // Load the map used previously if we're starting on the title screen without any overrides.
297    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 we're starting directly in a challenge mode, the tutorial, or by playing a scenario,
307    // usually time is midnight, so save some effort and start with the correct color scheme. If
308    // we're loading a savestate and it's actually daytime, we'll pay a small penalty to switch
309    // colors.
310    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    // No web support; this uses blocking IO
325    let secondary = setup.diff_map.as_ref().map(|path| {
326        ctx.loading_screen("load secondary map", |ctx, timer| {
327            // Use this low-level API, since the secondary map file probably isn't in the usual
328            // directory structure
329            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    // SimFlags::load doesn't know how to do async IO, which we need on the web. But in the common
340    // case, all we're creating there is a map. If so, use the proper async interface.
341    //
342    // Note if we started with a scenario, main() rewrote it to be the appropriate map, along with
343    // mode.
344    if setup.flags.sim_flags.load.contains("/maps/") {
345        // Get App created with a dummy blank map
346        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        // We're loading a savestate or a RawMap. Do it with blocking IO. This won't
378        // work on the web.
379        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    // Run this after loading the primary map. That process wipes out app.secondary.
408    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    // Handle savestates
419    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    // Just apply this here, don't plumb to SimFlags or anything else. We recreate things using
433    // these flags later, but we don't want to keep applying the same edits.
434    if let Some(ref edits_name) = setup.start_with_edits {
435        // Remote edits require another intermediate state to load
436        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                            // TODO Fail more gracefully -- add a popup with the error, but continue
464                            // app setup?
465                            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        // TODO Fail more gracefully -- add a popup with the error, but continue app setup?
493        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    // Handle layer parameter before creating the state
527    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                // Parking occupancy layer - skipping for now as it needs more parameters
543                warn!("parking_occupancy layer not implemented in URL parameters yet");
544            }
545            "transit_network" => {
546                // Transit network layer - showing all routes, buses, and trains by default
547                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                // Not attempting to keep the primary and secondary simulations synchronized at the
577                // same time yet. Just handle this one startup case, so we can switch maps without
578                // constantly flopping day/night mode.
579                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                // We got here by just passing --dev and a map as flags; we're just looking at an
589                // empty map. Start in the daytime.
590                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            // TODO Load bytes here
613            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    // Handle info panel parameter - needs to be done after the state is created
627    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}