diff --git a/game/src/pregame.rs b/game/src/pregame/mod.rs similarity index 64% rename from game/src/pregame.rs rename to game/src/pregame/mod.rs index 379db64ce3..ab8c16e24b 100644 --- a/game/src/pregame.rs +++ b/game/src/pregame/mod.rs @@ -1,14 +1,10 @@ -use std::collections::HashMap; - use instant::Instant; use rand::Rng; use rand_xorshift::XorShiftRng; use abstutil::Timer; -use geom::{Duration, Line, Percent, Pt2D, Speed}; -use map_gui::load::MapLoader; -use map_gui::tools::{open_browser, PopupMsg}; -use map_model::PermanentMapEdits; +use geom::{Duration, Line, Pt2D, Speed}; +use map_gui::tools::open_browser; use sim::{AlertHandler, ScenarioGenerator, Sim, SimOptions}; use widgetry::{ hotkeys, Color, ContentMode, DrawBaselayer, EdgeInsets, EventCtx, Font, GfxCtx, Image, Key, @@ -18,10 +14,11 @@ use widgetry::{ use crate::app::{App, Transition}; use crate::challenges::ChallengesPicker; use crate::devtools::DevToolsMode; -use crate::edit::apply_map_edits; use crate::sandbox::gameplay::Tutorial; use crate::sandbox::{GameplayMode, SandboxMode}; +mod proposals; + pub struct TitleScreen { panel: Panel, screensaver: Screensaver, @@ -225,7 +222,7 @@ impl State for MainMenu { open_browser("https://forms.gle/ocvbek1bTaZUr3k49"); } "Community Proposals" => { - return Transition::Push(Proposals::new(ctx, app, None)); + return Transition::Push(proposals::Proposals::new(ctx, app, None)); } "Internal Dev Tools" => { return Transition::Push(DevToolsMode::new(ctx, app)); @@ -332,191 +329,6 @@ impl State for About { } } -struct Proposals { - panel: Panel, - proposals: HashMap, - current: Option, -} - -impl Proposals { - fn new(ctx: &mut EventCtx, app: &App, current: Option) -> Box> { - let mut proposals = HashMap::new(); - let mut buttons = Vec::new(); - let mut current_tab = Vec::new(); - // If a proposal has fallen out of date, it'll be skipped with an error logged. Since these - // are under version control, much more likely to notice when they break (or we could add a - // step to data/regen.sh). - for (name, edits) in - abstio::load_all_objects::(abstio::path("system/proposals")) - { - if current == Some(name.clone()) { - let mut txt = Text::new(); - txt.add(Line(&edits.proposal_description[0]).small_heading()); - for l in edits.proposal_description.iter().skip(1) { - txt.add(Line(l)); - } - current_tab.push( - txt.wrap_to_pct(ctx, 70) - .into_widget(ctx) - .margin_below(15) - .margin_above(15), - ); - - if edits.proposal_link.is_some() { - current_tab.push( - ctx.style() - .btn_plain - .btn() - .label_underlined_text("Read detailed write-up") - .build_def(ctx) - .margin_below(10), - ); - } - current_tab.push( - ctx.style() - .btn_solid_primary - .text("Try out this proposal") - .build_def(ctx), - ); - - buttons.push( - ctx.style() - .btn_tab - .text(&edits.proposal_description[0]) - .disabled(true) - .build_def(ctx) - .margin_below(10), - ); - } else { - buttons.push( - ctx.style() - .btn_tab - .text(&edits.proposal_description[0]) - .no_tooltip() - .build_widget(ctx, &name) - .margin_below(10), - ); - } - - proposals.insert(name, edits); - } - - let mut col = vec![ - { - let mut txt = Text::from(Line("A/B STREET").display_title()); - txt.add(Line("PROPOSALS").big_heading_styled()); - txt.add(Line("")); - txt.add(Line( - "These are proposed changes to Seattle made by community members.", - )); - txt.add(Line("Contact dabreegster@gmail.com to add your idea here!")); - txt.into_widget(ctx).centered_horiz().margin_below(20) - }, - Widget::custom_row(buttons).flex_wrap(ctx, Percent::int(80)), - ]; - col.extend(current_tab); - - Box::new(Proposals { - proposals, - panel: Panel::new(Widget::custom_col(vec![ - ctx.style() - .btn_back("Home") - .hotkey(Key::Escape) - .build_widget(ctx, "back") - .align_left() - .margin_below(20), - Widget::col(col).bg(app.cs.panel_bg).padding(16), - ])) - .exact_size_percent(90, 85) - .build_custom(ctx), - current, - }) - } -} - -impl State for Proposals { - fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { - match self.panel.event(ctx) { - Outcome::Clicked(x) => match x.as_ref() { - "back" => { - return Transition::Pop; - } - "Try out this proposal" => { - let edits = self.proposals[self.current.as_ref().unwrap()].clone(); - - return Transition::Push(MapLoader::new( - ctx, - app, - edits.map_name.clone(), - Box::new(move |ctx, app| { - // Apply edits before setting up the sandbox, for simplicity - let maybe_err = ctx.loading_screen("apply edits", |ctx, mut timer| { - match edits.to_edits(&app.primary.map) { - Ok(edits) => { - apply_map_edits(ctx, app, edits); - app.primary - .map - .recalculate_pathfinding_after_edits(&mut timer); - None - } - Err(err) => Some(err), - } - }); - if let Some(err) = maybe_err { - Transition::Replace(PopupMsg::new( - ctx, - "Can't load proposal", - vec![err.to_string()], - )) - } else { - app.primary.layer = - Some(Box::new(crate::layer::map::Static::edits(ctx, app))); - - let mode = if abstio::file_exists(abstio::path_scenario( - app.primary.map.get_name(), - "weekday", - )) { - GameplayMode::PlayScenario( - app.primary.map.get_name().clone(), - "weekday".to_string(), - Vec::new(), - ) - } else { - GameplayMode::Freeform(app.primary.map.get_name().clone()) - }; - Transition::Replace(SandboxMode::simple_new(app, mode)) - } - }), - )); - } - "Read detailed write-up" => { - open_browser( - self.proposals[self.current.as_ref().unwrap()] - .proposal_link - .clone() - .unwrap(), - ); - } - x => { - return Transition::Replace(Proposals::new(ctx, app, Some(x.to_string()))); - } - }, - _ => {} - } - - Transition::Keep - } - - fn draw_baselayer(&self) -> DrawBaselayer { - DrawBaselayer::Custom - } - - fn draw(&self, g: &mut GfxCtx, app: &App) { - g.clear(app.cs.dialog_bg); - self.panel.draw(g); - } -} - struct Screensaver { line: Line, started: Instant, diff --git a/game/src/pregame/proposals.rs b/game/src/pregame/proposals.rs new file mode 100644 index 0000000000..2488ed60f7 --- /dev/null +++ b/game/src/pregame/proposals.rs @@ -0,0 +1,232 @@ +use std::collections::HashMap; + +use geom::Percent; +use map_gui::load::MapLoader; +use map_gui::tools::{open_browser, sync_missing_files, ChooseSomething, PopupMsg}; +use map_model::PermanentMapEdits; +use widgetry::{DrawBaselayer, EventCtx, GfxCtx, Key, Line, Outcome, Panel, State, Text, Widget}; + +use crate::app::{App, Transition}; +use crate::edit::apply_map_edits; +use crate::sandbox::{GameplayMode, SandboxMode}; + +pub struct Proposals { + panel: Panel, + proposals: HashMap, + current: Option, +} + +impl Proposals { + pub fn new(ctx: &mut EventCtx, app: &App, current: Option) -> Box> { + let mut proposals = HashMap::new(); + let mut buttons = Vec::new(); + let mut current_tab = Vec::new(); + // If a proposal has fallen out of date, it'll be skipped with an error logged. Since these + // are under version control, much more likely to notice when they break (or we could add a + // step to data/regen.sh). + for (name, edits) in + abstio::load_all_objects::(abstio::path("system/proposals")) + { + if current == Some(name.clone()) { + let mut txt = Text::new(); + txt.add(Line(&edits.proposal_description[0]).small_heading()); + for l in edits.proposal_description.iter().skip(1) { + txt.add(Line(l)); + } + current_tab.push( + txt.wrap_to_pct(ctx, 70) + .into_widget(ctx) + .margin_below(15) + .margin_above(15), + ); + + if edits.proposal_link.is_some() { + current_tab.push( + ctx.style() + .btn_plain + .btn() + .label_underlined_text("Read detailed write-up") + .build_def(ctx) + .margin_below(10), + ); + } + current_tab.push( + ctx.style() + .btn_solid_primary + .text("Try out this proposal") + .build_def(ctx), + ); + + buttons.push( + ctx.style() + .btn_tab + .text(&edits.proposal_description[0]) + .disabled(true) + .build_def(ctx) + .margin_below(10), + ); + } else { + buttons.push( + ctx.style() + .btn_tab + .text(&edits.proposal_description[0]) + .no_tooltip() + .build_widget(ctx, &name) + .margin_below(10), + ); + } + + proposals.insert(name, edits); + } + + let mut col = vec![ + { + let mut txt = Text::from(Line("A/B STREET").display_title()); + txt.add(Line("PROPOSALS").big_heading_styled()); + txt.add(Line("")); + txt.add(Line( + "These are proposed changes to Seattle made by community members.", + )); + txt.add(Line("Contact dabreegster@gmail.com to add your idea here!")); + txt.into_widget(ctx).centered_horiz().margin_below(20) + }, + Widget::custom_row(buttons).flex_wrap(ctx, Percent::int(80)), + ]; + col.extend(current_tab); + + Box::new(Proposals { + proposals, + panel: Panel::new(Widget::custom_col(vec![ + ctx.style() + .btn_back("Home") + .hotkey(Key::Escape) + .build_widget(ctx, "back") + .align_left() + .margin_below(20), + Widget::col(col).bg(app.cs.panel_bg).padding(16), + ])) + .exact_size_percent(90, 85) + .build_custom(ctx), + current, + }) + } +} + +impl State for Proposals { + fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { + match self.panel.event(ctx) { + Outcome::Clicked(x) => match x.as_ref() { + "back" => { + return Transition::Pop; + } + "Try out this proposal" => { + return launch( + ctx, + app, + self.proposals[self.current.as_ref().unwrap()].clone(), + ); + } + "Read detailed write-up" => { + open_browser( + self.proposals[self.current.as_ref().unwrap()] + .proposal_link + .clone() + .unwrap(), + ); + } + x => { + return Transition::Replace(Proposals::new(ctx, app, Some(x.to_string()))); + } + }, + _ => {} + } + + Transition::Keep + } + + fn draw_baselayer(&self) -> DrawBaselayer { + DrawBaselayer::Custom + } + + fn draw(&self, g: &mut GfxCtx, app: &App) { + g.clear(app.cs.dialog_bg); + self.panel.draw(g); + } +} + +fn launch(ctx: &mut EventCtx, app: &App, edits: PermanentMapEdits) -> Transition { + #[cfg(not(target_arch = "wasm32"))] + { + if !abstio::file_exists(edits.map_name.path()) { + return Transition::Push(ChooseSomething::new( + ctx, + &format!("Missing data. Download {}?", edits.map_name.city.describe()), + vec![ + widgetry::Choice::string("Yes, download"), + widgetry::Choice::string("Never mind").key(Key::Escape), + ], + Box::new(move |resp, ctx, _| { + if resp == "Never mind" { + return Transition::Pop; + } + + let mut data_packs = abstio::DataPacks::load_or_create(); + data_packs.runtime.insert(edits.map_name.city.to_path()); + abstio::write_json(abstio::path_player("data.json"), &data_packs); + + let messages = + ctx.loading_screen("sync files", |_, timer| sync_missing_files(timer)); + Transition::Replace(PopupMsg::new( + ctx, + "Download complete. Please try again", + messages, + )) + }), + )); + } + } + + Transition::Push(MapLoader::new( + ctx, + app, + edits.map_name.clone(), + Box::new(move |ctx, app| { + // Apply edits before setting up the sandbox, for simplicity + let maybe_err = ctx.loading_screen("apply edits", |ctx, mut timer| { + match edits.to_edits(&app.primary.map) { + Ok(edits) => { + apply_map_edits(ctx, app, edits); + app.primary + .map + .recalculate_pathfinding_after_edits(&mut timer); + None + } + Err(err) => Some(err), + } + }); + if let Some(err) = maybe_err { + Transition::Replace(PopupMsg::new( + ctx, + "Can't load proposal", + vec![err.to_string()], + )) + } else { + app.primary.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app))); + + let mode = if abstio::file_exists(abstio::path_scenario( + app.primary.map.get_name(), + "weekday", + )) { + GameplayMode::PlayScenario( + app.primary.map.get_name().clone(), + "weekday".to_string(), + Vec::new(), + ) + } else { + GameplayMode::Freeform(app.primary.map.get_name().clone()) + }; + Transition::Replace(SandboxMode::simple_new(app, mode)) + } + }), + )) +} diff --git a/map_gui/src/tools/city_picker.rs b/map_gui/src/tools/city_picker.rs index 2497bae5fe..8f7a7f5877 100644 --- a/map_gui/src/tools/city_picker.rs +++ b/map_gui/src/tools/city_picker.rs @@ -213,7 +213,7 @@ impl CityPicker { abstio::write_json(abstio::path_player("data.json"), &data_packs); let messages = ctx.loading_screen("sync files", |_, timer| { - crate::tools::updater::sync(timer) + crate::tools::updater::sync_missing_files(timer) }); Transition::Replace(crate::tools::PopupMsg::new( ctx, diff --git a/map_gui/src/tools/mod.rs b/map_gui/src/tools/mod.rs index 59f6d2c577..b56db9041e 100644 --- a/map_gui/src/tools/mod.rs +++ b/map_gui/src/tools/mod.rs @@ -15,6 +15,9 @@ pub use self::ui::{ChooseSomething, PopupMsg, PromptInput}; pub use self::url::URLManager; use crate::AppLike; +#[cfg(not(target_arch = "wasm32"))] +pub use self::updater::sync_missing_files; + mod camera; mod city_picker; mod colors; diff --git a/map_gui/src/tools/updater.rs b/map_gui/src/tools/updater.rs index 103e74310e..ef1c4f705b 100644 --- a/map_gui/src/tools/updater.rs +++ b/map_gui/src/tools/updater.rs @@ -82,7 +82,8 @@ impl State for Picker { } abstio::write_json(abstio::path_player("data.json"), &data_packs); - let messages = ctx.loading_screen("sync files", |_, timer| sync(timer)); + let messages = + ctx.loading_screen("sync files", |_, timer| sync_missing_files(timer)); return Transition::Multi(vec![ Transition::Replace(crate::tools::CityPicker::new( ctx, @@ -137,7 +138,7 @@ fn prettyprint_bytes(bytes: usize) -> String { // TODO This only downloads files that don't exist but should. It doesn't remove or update // anything. Not sure if everything the updater does should also be done here. -pub fn sync(timer: &mut Timer) -> Vec { +pub fn sync_missing_files(timer: &mut Timer) -> Vec { let truth = Manifest::load().filter(DataPacks::load_or_create()); let version = if cfg!(feature = "release_s3") { NEXT_RELEASE