Stub out a native-only UI to download extra cities. Move the config

about what cities somebody has opted into downloading to JSON and a more
common place. #326

Note this is a breaking change for this config file, but I don't think
many people have started using this yet.
This commit is contained in:
Dustin Carlino 2020-11-07 16:20:27 -08:00
parent 104c987edd
commit 54ced5b5b4
7 changed files with 139 additions and 78 deletions

1
.gitignore vendored
View File

@ -3,7 +3,6 @@
__pycache__
.idea/
data/config
data/player
importer.json

View File

@ -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<String>,
/// A list of cities to download for running the map importer.
pub input: BTreeSet<String>,
}
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::<DataPacks>(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
}
}
}
}

View File

@ -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

View File

@ -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<App> 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,

View File

@ -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...

View File

@ -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<dyn State<App>> {
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<App> 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<String, usize> {
let mut per_city = BTreeMap::new();
for (path, entry) in &manifest.entries {
let parts = path.split("/").collect::<Vec<_>>();
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)
}

View File

@ -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::<Vec<_>>();
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<String>,
input: Vec<String>,
}
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::<Vec<_>>();
assert_eq!(parts.len(), 2);
let list = parts[1]
.split(",")
.map(|x| x.to_string())
.filter(|x| !x.is_empty())
.collect::<Vec<_>>();
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() {