abstio/
abst_paths.rs

1//! Generate paths for different A/B Street files
2
3// TODO There's some repeated code here for listing files locally/from the manifest/merged. Maybe
4// have a Source enum and simplify the API. But we would either have to call Manifest::load
5// constantly, or plumb it around with a borrow? Or maybe even owned.
6
7use std::sync::OnceLock;
8
9use anyhow::Result;
10use serde::{Deserialize, Serialize};
11
12use abstutil::basename;
13
14use crate::{file_exists, list_all_objects, Manifest};
15
16static ROOT_DIR: OnceLock<String> = OnceLock::new();
17static ROOT_PLAYER_DIR: OnceLock<String> = OnceLock::new();
18
19pub fn path<I: AsRef<str>>(p: I) -> String {
20    let p = p.as_ref();
21    if p.starts_with("player/") {
22        let dir = ROOT_PLAYER_DIR.get_or_init(|| {
23            // If you're packaging for a release and want the player's local data directory to be
24            // $HOME/.abstreet, set ABST_PLAYER_HOME_DIR=1
25            if option_env!("ABST_PLAYER_HOME_DIR").is_some() {
26                match std::env::var("HOME") {
27                    Ok(dir) => format!("{}/.abstreet", dir.trim_end_matches('/')),
28                    Err(err) => panic!("This build of A/B Street stores player data in $HOME/.abstreet, but $HOME isn't set: {}", err),
29                }
30            } else if cfg!(target_arch = "wasm32") {
31                "../data".to_string()
32            } else if file_exists("data/".to_string()) {
33                "data".to_string()
34            } else if file_exists("../data/".to_string()) {
35                "../data".to_string()
36            } else if file_exists("../../data/".to_string()) {
37                "../../data".to_string()
38            } else if file_exists("../../../data/".to_string()) {
39                "../../../data".to_string()
40            } else {
41                panic!("Can't find the data/ directory");
42            }
43        });
44        format!("{dir}/{p}")
45    } else {
46        let dir = ROOT_DIR.get_or_init(|| {
47            // If you're packaging for a release and need the data directory to be in some fixed
48            // location: ABST_DATA_DIR=/some/path cargo build ...
49            if let Some(dir) = option_env!("ABST_DATA_DIR") {
50                dir.trim_end_matches('/').to_string()
51            } else if cfg!(target_arch = "wasm32") {
52                "../data".to_string()
53            } else if file_exists("data/".to_string()) {
54                "data".to_string()
55            } else if file_exists("../data/".to_string()) {
56                "../data".to_string()
57            } else if file_exists("../../data/".to_string()) {
58                "../../data".to_string()
59            } else if file_exists("../../../data/".to_string()) {
60                "../../../data".to_string()
61            } else {
62                panic!("Can't find the data/ directory");
63            }
64        });
65        format!("{dir}/{p}")
66    }
67}
68
69/// A single city is identified using this.
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
71pub struct CityName {
72    /// A two letter lowercase country code, from https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2.
73    /// To represent imaginary/test cities, use the code `zz`.
74    pub country: String,
75    /// The name of the city, in filename-friendly form -- for example, "tel_aviv".
76    pub city: String,
77}
78
79impl CityName {
80    /// Create a CityName from a country code and city.
81    pub fn new(country: &str, city: &str) -> CityName {
82        if country.len() != 2 {
83            panic!(
84                "CityName::new({}, {}) has a country code that isn't two letters",
85                country, city
86            );
87        }
88        CityName {
89            country: country.to_string(),
90            city: city.to_string(),
91        }
92    }
93
94    /// Convenient constructor for the main city of the game.
95    pub fn seattle() -> CityName {
96        CityName::new("us", "seattle")
97    }
98
99    /// Returns all city names available locally.
100    fn list_all_cities_locally() -> Vec<CityName> {
101        let mut cities = Vec::new();
102        for country in list_all_objects(path("system")) {
103            if country == "assets"
104                || country == "extra_fonts"
105                || country == "ltn_proposals"
106                || country == "proposals"
107                || country == "study_areas"
108            {
109                continue;
110            }
111            for city in list_all_objects(path(format!("system/{}", country))) {
112                cities.push(CityName::new(&country, &city));
113            }
114        }
115        cities
116    }
117
118    /// Returns all city names based on the manifest of available files.
119    fn list_all_cities_from_manifest(manifest: &Manifest) -> Vec<CityName> {
120        let mut cities = Vec::new();
121        for path in manifest.entries.keys() {
122            if let Some(city) = Manifest::path_to_city(path) {
123                cities.push(city);
124            }
125        }
126        // The paths in the manifest are ordered, so the same cities will be adjacent.
127        cities.dedup();
128        cities
129    }
130
131    /// Returns all city names either available locally or based on the manifest of available files.
132    pub fn list_all_cities_merged(manifest: &Manifest) -> Vec<CityName> {
133        let mut all = CityName::list_all_cities_locally();
134        all.extend(CityName::list_all_cities_from_manifest(manifest));
135        all.sort();
136        all.dedup();
137        all
138    }
139
140    /// Returns all city names based on importer config.
141    pub fn list_all_cities_from_importer_config() -> Vec<CityName> {
142        let mut cities = Vec::new();
143        for country in list_all_objects("importer/config".to_string()) {
144            for city in list_all_objects(format!("importer/config/{}", country)) {
145                cities.push(CityName::new(&country, &city));
146            }
147        }
148        cities
149    }
150
151    /// Returns all maps in a city based on importer config.
152    pub fn list_all_maps_in_city_from_importer_config(&self) -> Vec<MapName> {
153        crate::list_dir(format!("importer/config/{}/{}", self.country, self.city))
154            .into_iter()
155            .filter(|path| path.ends_with(".geojson"))
156            .map(|path| MapName::from_city(self, &basename(path)))
157            .collect()
158    }
159
160    /// Parses a CityName from something like "gb/london"; the inverse of `to_path`.
161    pub fn parse(x: &str) -> Result<CityName> {
162        let parts = x.split('/').collect::<Vec<_>>();
163        if parts.len() != 2 || parts[0].len() != 2 {
164            bail!("Bad CityName {}", x);
165        }
166        Ok(CityName::new(parts[0], parts[1]))
167    }
168
169    /// Expresses the city as a path, like "gb/london"; the inverse of `parse`.
170    pub fn to_path(&self) -> String {
171        format!("{}/{}", self.country, self.city)
172    }
173
174    /// Stringify the city name for debug messages. Don't implement `std::fmt::Display`, to force
175    /// callers to explicitly opt into this description, which could change.
176    pub fn describe(&self) -> String {
177        format!("{} ({})", self.city, self.country)
178    }
179
180    /// Constructs the path to some city-scoped data/input.
181    pub fn input_path<I: AsRef<str>>(&self, file: I) -> String {
182        path(format!(
183            "input/{}/{}/{}",
184            self.country,
185            self.city,
186            file.as_ref()
187        ))
188    }
189
190    /// Should metric units be used by default for this map? (Imperial if false)
191    pub fn uses_metric(&self) -> bool {
192        // We don't need a full locale lookup or anything. Myanmar and Liberia apparently use both
193        // but are leaning metric.
194        self.country != "us"
195    }
196}
197
198/// A single map is identified using this.
199#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
200pub struct MapName {
201    pub city: CityName,
202    /// The name of the map within the city, in filename-friendly form -- for example, "downtown"
203    pub map: String,
204}
205
206impl abstutil::CloneableAny for MapName {}
207
208impl MapName {
209    /// Create a MapName from a country code, city, and map name.
210    pub fn new(country: &str, city: &str, map: &str) -> MapName {
211        MapName {
212            city: CityName::new(country, city),
213            map: map.to_string(),
214        }
215    }
216
217    pub fn blank() -> Self {
218        Self::new("zz", "blank city", "blank")
219    }
220
221    /// Create a MapName from a city and map within that city.
222    pub fn from_city(city: &CityName, map: &str) -> MapName {
223        MapName::new(&city.country, &city.city, map)
224    }
225
226    /// Convenient constructor for the main city of the game.
227    pub fn seattle(map: &str) -> MapName {
228        MapName::new("us", "seattle", map)
229    }
230
231    /// Stringify the map name for debug messages. Don't implement `std::fmt::Display`, to force
232    /// callers to explicitly opt into this description, which could change.
233    pub fn describe(&self) -> String {
234        format!(
235            "{} (in {} ({}))",
236            self.map, self.city.city, self.city.country
237        )
238    }
239
240    /// Stringify the map name for filenames.
241    pub fn as_filename(&self) -> String {
242        format!("{}_{}_{}", self.city.country, self.city.city, self.map)
243    }
244
245    /// Transforms a path to a map back to a MapName. Returns `None` if the input is strange.
246    pub fn from_path(path: &str) -> Option<MapName> {
247        let parts = path.split('/').collect::<Vec<_>>();
248        // Expect something ending like system/us/seattle/maps/montlake.bin
249        if parts.len() < 5 || parts[parts.len() - 5] != "system" || parts[parts.len() - 2] != "maps"
250        {
251            return None;
252        }
253        let country = parts[parts.len() - 4];
254        let city = parts[parts.len() - 3];
255        let map = basename(parts[parts.len() - 1]);
256        Some(MapName::new(country, city, &map))
257    }
258
259    /// Returns the filesystem path to this map.
260    pub fn path(&self) -> String {
261        path(format!(
262            "system/{}/{}/maps/{}.bin",
263            self.city.country, self.city.city, self.map
264        ))
265    }
266
267    /// Returns all maps from one city that're available locally.
268    fn list_all_maps_in_city_locally(city: &CityName) -> Vec<MapName> {
269        let mut names = Vec::new();
270        for map in list_all_objects(path(format!("system/{}/{}/maps", city.country, city.city))) {
271            names.push(MapName {
272                city: city.clone(),
273                map,
274            });
275        }
276        names
277    }
278
279    /// Returns all maps from all cities available locally.
280    pub fn list_all_maps_locally() -> Vec<MapName> {
281        let mut names = Vec::new();
282        for city in CityName::list_all_cities_locally() {
283            names.extend(MapName::list_all_maps_in_city_locally(&city));
284        }
285        names
286    }
287
288    /// Returns all maps from all cities based on the manifest of available files.
289    fn list_all_maps_from_manifest(manifest: &Manifest) -> Vec<MapName> {
290        let mut names = Vec::new();
291        for path in manifest.entries.keys() {
292            if let Some(name) = MapName::from_path(path) {
293                names.push(name);
294            }
295        }
296        names
297    }
298
299    /// Returns all maps from all cities either available locally or based on the manifest of available files.
300    pub fn list_all_maps_merged(manifest: &Manifest) -> Vec<MapName> {
301        let mut all = MapName::list_all_maps_locally();
302        all.extend(MapName::list_all_maps_from_manifest(manifest));
303        all.sort();
304        all.dedup();
305        all
306    }
307
308    /// Returns all maps from one city based on the manifest of available files.
309    fn list_all_maps_in_city_from_manifest(city: &CityName, manifest: &Manifest) -> Vec<MapName> {
310        MapName::list_all_maps_from_manifest(manifest)
311            .into_iter()
312            .filter(|name| &name.city == city)
313            .collect()
314    }
315
316    /// Returns all maps from one city that're available either locally or according to the
317    /// manifest.
318    pub fn list_all_maps_in_city_merged(city: &CityName, manifest: &Manifest) -> Vec<MapName> {
319        let mut all = MapName::list_all_maps_in_city_locally(city);
320        all.extend(MapName::list_all_maps_in_city_from_manifest(city, manifest));
321        all.sort();
322        all.dedup();
323        all
324    }
325
326    /// Returns the string to opt into runtime or input files for DataPacks.
327    pub fn to_data_pack_name(&self) -> String {
328        if Manifest::is_file_part_of_huge_seattle(&self.path()) {
329            return "us/huge_seattle".to_string();
330        }
331        self.city.to_path()
332    }
333}
334
335// System data (Players can't edit, needed at runtime)
336
337pub fn path_prebaked_results(name: &MapName, scenario_name: &str) -> String {
338    path(format!(
339        "system/{}/{}/prebaked_results/{}/{}.bin",
340        name.city.country, name.city.city, name.map, scenario_name
341    ))
342}
343
344pub fn path_scenario(name: &MapName, scenario_name: &str) -> String {
345    // TODO Getting complicated. Sometimes we're trying to load, so we should look for .bin, then
346    // .json. But when we're writing a custom scenario, we actually want to write a .bin.
347    let bin = path(format!(
348        "system/{}/{}/scenarios/{}/{}.bin",
349        name.city.country, name.city.city, name.map, scenario_name
350    ));
351    let json = path(format!(
352        "system/{}/{}/scenarios/{}/{}.json",
353        name.city.country, name.city.city, name.map, scenario_name
354    ));
355    if file_exists(&bin) {
356        return bin;
357    }
358    if file_exists(&json) {
359        return json;
360    }
361    bin
362}
363pub fn path_all_scenarios(name: &MapName) -> String {
364    path(format!(
365        "system/{}/{}/scenarios/{}",
366        name.city.country, name.city.city, name.map
367    ))
368}
369
370/// Extract the map and scenario name from a path. Crashes if the input is strange.
371pub fn parse_scenario_path(path: &str) -> (MapName, String) {
372    // TODO regex
373    let parts = path.split('/').collect::<Vec<_>>();
374    let country = parts[parts.len() - 5];
375    let city = parts[parts.len() - 4];
376    let map = parts[parts.len() - 2];
377    let scenario = basename(parts[parts.len() - 1]);
378    let map_name = MapName::new(country, city, map);
379    (map_name, scenario)
380}
381
382// Player data (Players edit this)
383
384pub fn path_player<I: AsRef<str>>(p: I) -> String {
385    path(format!("player/{}", p.as_ref()))
386}
387
388pub fn path_camera_state(name: &MapName) -> String {
389    path(format!(
390        "player/camera_state/{}/{}/{}.json",
391        name.city.country, name.city.city, name.map
392    ))
393}
394
395pub fn path_edits(name: &MapName, edits_name: &str) -> String {
396    path(format!(
397        "player/edits/{}/{}/{}/{}.json",
398        name.city.country, name.city.city, name.map, edits_name
399    ))
400}
401pub fn path_all_edits(name: &MapName) -> String {
402    path(format!(
403        "player/edits/{}/{}/{}",
404        name.city.country, name.city.city, name.map
405    ))
406}
407
408pub fn path_ltn_proposals(name: &MapName, proposal_name: &str) -> String {
409    path(format!(
410        "player/ltn_proposals/{}/{}/{}/{}.json.gz",
411        name.city.country, name.city.city, name.map, proposal_name
412    ))
413}
414pub fn path_all_ltn_proposals(name: &MapName) -> String {
415    path(format!(
416        "player/ltn_proposals/{}/{}/{}",
417        name.city.country, name.city.city, name.map
418    ))
419}
420
421pub fn path_save(name: &MapName, edits_name: &str, run_name: &str, time: String) -> String {
422    path(format!(
423        "player/saves/{}/{}/{}/{}_{}/{}.bin",
424        name.city.country, name.city.city, name.map, edits_name, run_name, time
425    ))
426}
427pub fn path_all_saves(name: &MapName, edits_name: &str, run_name: &str) -> String {
428    path(format!(
429        "player/saves/{}/{}/{}/{}_{}",
430        name.city.country, name.city.city, name.map, edits_name, run_name
431    ))
432}
433
434pub fn path_trips(name: &MapName) -> String {
435    path(format!(
436        "player/routes/{}/{}/{}.json",
437        name.city.country, name.city.city, name.map
438    ))
439}
440
441// Input data (For developers to build maps, not needed at runtime)
442
443pub fn path_popdat() -> String {
444    path("input/us/seattle/popdat.bin")
445}
446
447pub fn path_raw_map(name: &MapName) -> String {
448    path(format!(
449        "input/{}/{}/raw_maps/{}.bin",
450        name.city.country, name.city.city, name.map
451    ))
452}
453
454pub fn path_shared_input<I: AsRef<str>>(i: I) -> String {
455    path(format!("input/shared/{}", i.as_ref()))
456}