diff --git a/.gitignore b/.gitignore index 3d1d91a064..fe29aaef52 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ __pycache__ .idea/ -data/config data/player importer.json diff --git a/abstutil/src/abst_data.rs b/abstutil/src/abst_data.rs index 065e66744c..b85509786e 100644 --- a/abstutil/src/abst_data.rs +++ b/abstutil/src/abst_data.rs @@ -1,7 +1,9 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use serde::{Deserialize, Serialize}; +use crate::Timer; + /// A list of all canonical data files for A/B Street that're uploaded somewhere. The file formats /// are tied to the latest version of the git repo. Players use the updater crate to sync these /// files with local copies. @@ -23,8 +25,7 @@ pub struct Entry { impl Manifest { #[cfg(not(target_arch = "wasm32"))] pub fn load() -> Manifest { - crate::maybe_read_json(crate::path("MANIFEST.json"), &mut crate::Timer::throwaway()) - .unwrap() + crate::maybe_read_json(crate::path("MANIFEST.json"), &mut Timer::throwaway()).unwrap() } #[cfg(target_arch = "wasm32")] @@ -32,3 +33,40 @@ impl Manifest { crate::from_json(&include_bytes!("../../data/MANIFEST.json").to_vec()).unwrap() } } + +/// Player-chosen groups of files to opt into downloading +#[derive(Serialize, Deserialize)] +pub struct DataPacks { + /// A list of cities to download for using in A/B Street. + pub runtime: BTreeSet, + /// A list of cities to download for running the map importer. + pub input: BTreeSet, +} + +impl DataPacks { + /// Load the player's config for what files to download, or create the config. + pub fn load_or_create() -> DataPacks { + if cfg!(target_arch = "wasm32") { + panic!("DataPacks::load_or_create shouldn't be called on wasm"); + } + + let path = crate::path("player/data.json"); + match crate::maybe_read_json::(path.clone(), &mut Timer::throwaway()) { + Ok(mut cfg) => { + // The game breaks without this required data pack. + cfg.runtime.insert("seattle".to_string()); + cfg + } + Err(err) => { + warn!("player/data.json invalid, assuming defaults: {}", err); + let mut cfg = DataPacks { + runtime: BTreeSet::new(), + input: BTreeSet::new(), + }; + cfg.runtime.insert("seattle".to_string()); + crate::write_json(path, &cfg); + cfg + } + } + } +} diff --git a/book/src/dev/README.md b/book/src/dev/README.md index b58aa13b12..8391c4b437 100644 --- a/book/src/dev/README.md +++ b/book/src/dev/README.md @@ -56,15 +56,8 @@ version control will get out of date. At any time, you can run files that have changed. You can also opt into downloading updates for more cities by editing -`data/config`. Opting into everything looks like this: - -``` -runtime: seattle,huge_seattle,krakow,berlin,xian,tel_aviv,london -input: seattle,huge_seattle,krakow,berlin,xian,tel_aviv,london -``` - -`runtime` downloads new maps and scenarios in `data/system/`. `input` is used -for building those maps -- see below. +`data/player/data.json`. In the main UI, there's a button to download more +cities that will help you manage this config file. ## Building map data diff --git a/game/src/common/city_picker.rs b/game/src/common/city_picker.rs index 274ff4d841..4df9bd42f6 100644 --- a/game/src/common/city_picker.rs +++ b/game/src/common/city_picker.rs @@ -130,6 +130,11 @@ impl CityPicker { ) .build_def(ctx, None), ]), + if cfg!(not(target_arch = "wasm32")) { + Btn::text_fg("Download more cities").build_def(ctx, None) + } else { + Widget::nothing() + }, ])) .build(ctx), }) @@ -154,6 +159,9 @@ impl State for CityPicker { "https://dabreegster.github.io/abstreet/howto/new_city.html".to_string(), ); } + "Download more cities" => { + return Transition::Replace(crate::common::updater::Picker::new(ctx)); + } path => { return Transition::Replace(MapLoader::new( ctx, diff --git a/game/src/common/mod.rs b/game/src/common/mod.rs index 52cc97d943..eecbd4aa2d 100644 --- a/game/src/common/mod.rs +++ b/game/src/common/mod.rs @@ -25,6 +25,7 @@ mod heatmap; mod isochrone; mod minimap; mod navigate; +mod updater; mod warp; // TODO This is now just used in two modes... diff --git a/game/src/common/updater.rs b/game/src/common/updater.rs new file mode 100644 index 0000000000..b5261a3275 --- /dev/null +++ b/game/src/common/updater.rs @@ -0,0 +1,77 @@ +use std::collections::BTreeMap; + +use abstutil::{DataPacks, Manifest}; +use widgetry::{Btn, Checkbox, EventCtx, GfxCtx, Line, Outcome, Panel, State, TextExt, Widget}; + +use crate::app::App; +use crate::game::Transition; + +pub struct Picker { + panel: Panel, +} + +impl Picker { + pub fn new(ctx: &mut EventCtx) -> Box> { + let manifest = Manifest::load(); + let data_packs = DataPacks::load_or_create(); + + let mut col = vec![Widget::row(vec![ + Line("Download more cities").small_heading().draw(ctx), + Btn::close(ctx), + ])]; + for (city, bytes) in size_per_city(&manifest) { + col.push(Widget::row(vec![ + Checkbox::checkbox(ctx, &city, None, data_packs.runtime.contains(&city)), + prettyprint_bytes(bytes).draw_text(ctx), + ])); + } + + Box::new(Picker { + panel: Panel::new(Widget::col(col)).build(ctx), + }) + } +} + +impl State for Picker { + fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition { + match self.panel.event(ctx) { + Outcome::Clicked(x) => match x.as_ref() { + "close" => { + return Transition::Pop; + } + _ => unreachable!(), + }, + _ => {} + } + + Transition::Keep + } + + fn draw(&self, g: &mut GfxCtx, _: &App) { + self.panel.draw(g); + } +} + +// For each city, how many total bytes do the runtime files cost? +fn size_per_city(manifest: &Manifest) -> BTreeMap { + let mut per_city = BTreeMap::new(); + for (path, entry) in &manifest.entries { + let parts = path.split("/").collect::>(); + if parts[1] == "system" { + *per_city.entry(parts[2].to_string()).or_insert(0) += entry.size_bytes; + } + } + per_city +} + +fn prettyprint_bytes(bytes: usize) -> String { + if bytes < 1024 { + return format!("{} bytes", bytes); + } + let kb = (bytes as f64) / 1024.0; + if kb < 1024.0 { + return format!("{} kb", kb as usize); + } + let mb = kb / 1024.0; + format!("{} mb", mb as usize) +} diff --git a/updater/src/main.rs b/updater/src/main.rs index f06610eae7..16961979ff 100644 --- a/updater/src/main.rs +++ b/updater/src/main.rs @@ -1,12 +1,12 @@ use std::collections::BTreeMap; use std::error::Error; use std::fs::File; -use std::io::{BufRead, BufReader, Read, Write}; +use std::io::{BufReader, Read, Write}; use std::process::Command; use walkdir::WalkDir; -use abstutil::{Entry, Manifest, Timer}; +use abstutil::{DataPacks, Entry, Manifest, Timer}; const MD5_BUF_READ_SIZE: usize = 4096; const VERSION: &str = "dev"; @@ -33,9 +33,9 @@ async fn main() { } async fn download() { - let cities = Cities::load_or_create(); + let data_packs = DataPacks::load_or_create(); let local = generate_manifest(); - let truth = filter_manifest(Manifest::load(), cities); + let truth = filter_manifest(Manifest::load(), data_packs); // Anything local need deleting? for path in local.entries.keys() { @@ -77,9 +77,9 @@ async fn download() { } fn just_compare() { - let cities = Cities::load_or_create(); + let data_packs = DataPacks::load_or_create(); let local = generate_manifest(); - let truth = filter_manifest(Manifest::load(), cities); + let truth = filter_manifest(Manifest::load(), data_packs); // Anything local need deleting? for path in local.entries.keys() { @@ -199,11 +199,11 @@ fn generate_manifest() -> Manifest { Manifest { entries: kv } } -fn filter_manifest(mut manifest: Manifest, cities: Cities) -> Manifest { +fn filter_manifest(mut manifest: Manifest, data_packs: DataPacks) -> Manifest { let mut remove = Vec::new(); for path in manifest.entries.keys() { // TODO Some hardcoded weird exceptions - if !cities.runtime.contains(&"huge_seattle".to_string()) + if !data_packs.runtime.contains("huge_seattle") && path == "data/system/seattle/scenarios/montlake/everyone_weekday.bin" { remove.push(path.clone()); @@ -212,11 +212,11 @@ fn filter_manifest(mut manifest: Manifest, cities: Cities) -> Manifest { let parts = path.split("/").collect::>(); if parts[1] == "input" { - if cities.input.contains(&parts[2].to_string()) { + if data_packs.input.contains(parts[2]) { continue; } } else if parts[1] == "system" { - if cities.runtime.contains(&parts[2].to_string()) { + if data_packs.runtime.contains(parts[2]) { continue; } } else { @@ -230,61 +230,6 @@ fn filter_manifest(mut manifest: Manifest, cities: Cities) -> Manifest { manifest } -// What data to download? -struct Cities { - runtime: Vec, - input: Vec, -} - -impl Cities { - fn load_or_create() -> Cities { - let path = "data/config"; - if let Ok(f) = File::open(path) { - let mut cities = Cities { - runtime: Vec::new(), - input: Vec::new(), - }; - for line in BufReader::new(f).lines() { - let line = line.unwrap(); - let parts = line.split(": ").collect::>(); - assert_eq!(parts.len(), 2); - let list = parts[1] - .split(",") - .map(|x| x.to_string()) - .filter(|x| !x.is_empty()) - .collect::>(); - if parts[0] == "runtime" { - cities.runtime = list; - } else if parts[0] == "input" { - cities.input = list; - } else { - panic!("{} is corrupted, what's {}", path, parts[0]); - } - } - if !cities.runtime.contains(&"seattle".to_string()) { - panic!( - "{}: runtime must contain seattle; the game breaks without this", - path - ); - } - cities - } else { - let mut f = File::create(&path).unwrap(); - writeln!(f, "runtime: seattle,berlin,krakow").unwrap(); - writeln!(f, "input: ").unwrap(); - println!("- Wrote {}", path); - Cities { - runtime: vec![ - "seattle".to_string(), - "berlin".to_string(), - "krakow".to_string(), - ], - input: vec![], - } - } - } -} - fn must_run_cmd(cmd: &mut Command) { println!("> Running {:?}", cmd); match cmd.status() {