tiger_lib/ck3/data/
provinces.rs

1use std::path::PathBuf;
2use std::str::FromStr;
3
4use bitvec::bitbox;
5use bitvec::boxed::BitBox;
6use image::{DynamicImage, Rgb};
7use itertools::Itertools;
8
9use crate::block::Block;
10use crate::db::{Db, DbKind};
11use crate::everything::Everything;
12use crate::fileset::{FileEntry, FileHandler};
13use crate::game::GameFlags;
14use crate::helpers::{TigerHashMap, TigerHashSet};
15use crate::item::{Item, ItemLoader, LoadAsFile, Recursive};
16use crate::parse::ParserMemory;
17use crate::parse::csv::{parse_csv, read_csv};
18use crate::pdxfile::{PdxEncoding, PdxFile};
19use crate::report::{ErrorKey, Severity, err, fatal, report, untidy, warn};
20use crate::token::{Loc, Token};
21use crate::validator::Validator;
22
23pub type ProvId = u32;
24
25const COLOUR_COUNT: usize = 256 * 256 * 256;
26
27#[derive(Clone, Debug)]
28struct ColorBitArray(BitBox);
29
30impl Default for ColorBitArray {
31    fn default() -> Self {
32        Self(bitbox![0; COLOUR_COUNT])
33    }
34}
35
36impl ColorBitArray {
37    fn get_index(color: Rgb<u8>) -> usize {
38        let Rgb([r, g, b]) = color;
39        ((r as usize) << 16) | ((g as usize) << 8) | b as usize
40    }
41
42    #[allow(clippy::cast_possible_truncation)]
43    fn get_color(index: usize) -> Rgb<u8> {
44        let r = (index >> 16) as u8;
45        let g = (index >> 8) as u8;
46        let b = index as u8;
47        Rgb([r, g, b])
48    }
49}
50
51impl std::ops::Deref for ColorBitArray {
52    type Target = BitBox;
53
54    fn deref(&self) -> &Self::Target {
55        &self.0
56    }
57}
58
59impl std::ops::DerefMut for ColorBitArray {
60    fn deref_mut(&mut self) -> &mut Self::Target {
61        &mut self.0
62    }
63}
64
65#[derive(Debug, Default)]
66pub struct Ck3Provinces {
67    /// Colors in the provinces.png
68    colors: ColorBitArray,
69
70    /// Provinces defined in definition.csv.
71    /// CK3 requires uninterrupted indices starting at 0, but we want to be able to warn
72    /// and continue if they're not, so it's a hashmap.
73    provinces: TigerHashMap<ProvId, Province>,
74
75    /// Kept and used for error reporting.
76    definition_csv: Option<FileEntry>,
77
78    adjacencies: Vec<Adjacency>,
79
80    impassable: TigerHashSet<ProvId>,
81
82    sea_or_river: TigerHashSet<ProvId>,
83}
84
85impl Ck3Provinces {
86    fn parse_definition(&mut self, csv: &[Token]) {
87        if let Some(province) = Province::parse(csv) {
88            if self.provinces.contains_key(&province.id) {
89                err(ErrorKey::DuplicateItem)
90                    .msg("duplicate entry for this province id")
91                    .loc(&province.comment)
92                    .push();
93            }
94            self.provinces.insert(province.id, province);
95        }
96    }
97
98    pub fn load_impassable(&mut self, block: &Block) {
99        enum Expecting<'a> {
100            Range(&'a Token),
101            List(&'a Token),
102            Nothing,
103        }
104
105        let mut expecting = Expecting::Nothing;
106        for item in block.iter_items() {
107            match expecting {
108                Expecting::Nothing => {
109                    if let Some((key, token)) = item.expect_assignment() {
110                        if key.is("sea_zones")
111                            || key.is("river_provinces")
112                            || key.is("impassable_mountains")
113                            || key.is("impassable_seas")
114                            || key.is("lakes")
115                        {
116                            if token.is("LIST") {
117                                expecting = Expecting::List(key);
118                            } else if token.is("RANGE") {
119                                expecting = Expecting::Range(key);
120                            } else {
121                                expecting = Expecting::Nothing;
122                            }
123                        } else {
124                            // TODO: this has to wait until full validation
125                            // let msg = format!("unexpected key `{key}`");
126                            // warn(ErrorKey::UnknownField).weak().msg(msg).loc(key).push();
127                        }
128                    }
129                }
130                Expecting::Range(key) => {
131                    if let Some(block) = item.expect_block() {
132                        let vec: Vec<&Token> = block.iter_values().collect();
133                        if vec.len() != 2 {
134                            err(ErrorKey::Validation).msg("invalid RANGE").loc(block).push();
135                            expecting = Expecting::Nothing;
136                            continue;
137                        }
138                        let from = vec[0].as_str().parse::<ProvId>();
139                        let to = vec[1].as_str().parse::<ProvId>();
140                        if from.is_err() || to.is_err() {
141                            err(ErrorKey::Validation).msg("invalid RANGE").loc(block).push();
142                            expecting = Expecting::Nothing;
143                            continue;
144                        }
145                        for provid in from.unwrap()..=to.unwrap() {
146                            self.impassable.insert(provid);
147                            if key.is("sea_zones") || key.is("river_provinces") {
148                                self.sea_or_river.insert(provid);
149                            }
150                        }
151                    }
152                    expecting = Expecting::Nothing;
153                }
154                Expecting::List(key) => {
155                    if let Some(block) = item.expect_block() {
156                        for token in block.iter_values() {
157                            let provid = token.as_str().parse::<ProvId>();
158                            if let Ok(provid) = provid {
159                                self.impassable.insert(provid);
160                                if key.is("sea_zones") || key.is("river_provinces") {
161                                    self.sea_or_river.insert(provid);
162                                }
163                            } else {
164                                err(ErrorKey::Validation)
165                                    .msg("invalid LIST item")
166                                    .loc(token)
167                                    .push();
168                                break;
169                            }
170                        }
171                    }
172                    expecting = Expecting::Nothing;
173                }
174            }
175        }
176    }
177
178    pub(crate) fn verify_exists_provid(&self, provid: ProvId, item: &Token, max_sev: Severity) {
179        if !self.provinces.contains_key(&provid) {
180            let msg = format!("province {provid} not defined in map_data/definition.csv");
181            report(ErrorKey::MissingItem, Item::Province.severity().at_most(max_sev))
182                .msg(msg)
183                .loc(item)
184                .push();
185        }
186    }
187
188    pub fn verify_exists_implied(&self, key: &str, item: &Token, max_sev: Severity) {
189        if let Ok(provid) = key.parse::<ProvId>() {
190            self.verify_exists_provid(provid, item, max_sev);
191        } else {
192            let msg = "province id should be numeric";
193            let sev = Item::Province.severity().at_most(max_sev);
194            report(ErrorKey::Validation, sev).msg(msg).loc(item).push();
195        }
196    }
197
198    pub fn exists(&self, key: &str) -> bool {
199        if let Ok(provid) = key.parse::<ProvId>() {
200            self.provinces.contains_key(&provid)
201        } else {
202            false
203        }
204    }
205
206    pub(crate) fn is_sea_or_river(&self, provid: ProvId) -> bool {
207        self.sea_or_river.contains(&provid)
208    }
209
210    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
211        self.provinces.values().map(|item| &item.key)
212    }
213
214    pub fn validate(&self, data: &Everything) {
215        for item in &self.adjacencies {
216            item.validate(self);
217        }
218        for item in self.provinces.values() {
219            item.validate(self, data);
220        }
221    }
222}
223
224#[derive(Debug)]
225pub enum FileContent {
226    Adjacencies(String),
227    Definitions(String),
228    Provinces(DynamicImage),
229    DefaultMap(Block),
230}
231
232impl FileHandler<FileContent> for Ck3Provinces {
233    fn subpath(&self) -> PathBuf {
234        PathBuf::from("map_data")
235    }
236
237    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<FileContent> {
238        if entry.path().components().count() == 2 {
239            match &*entry.filename().to_string_lossy() {
240                "adjacencies.csv" => {
241                    let content = match read_csv(entry.fullpath()) {
242                        Ok(content) => content,
243                        Err(e) => {
244                            err(ErrorKey::ReadError)
245                                .msg(format!("could not read file: {e:#}"))
246                                .loc(entry)
247                                .push();
248                            return None;
249                        }
250                    };
251                    return Some(FileContent::Adjacencies(content));
252                }
253
254                "definition.csv" => {
255                    let content = match read_csv(entry.fullpath()) {
256                        Ok(content) => content,
257                        Err(e) => {
258                            let msg =
259                                format!("could not read `{}`: {:#}", entry.path().display(), e);
260                            err(ErrorKey::ReadError).msg(msg).loc(entry).push();
261                            return None;
262                        }
263                    };
264                    return Some(FileContent::Definitions(content));
265                }
266
267                "provinces.png" => {
268                    let img = match image::open(entry.fullpath()) {
269                        Ok(img) => img,
270                        Err(e) => {
271                            let msg = format!("could not read `{}`: {e:#}", entry.path().display());
272                            err(ErrorKey::ReadError).msg(msg).loc(entry).push();
273                            return None;
274                        }
275                    };
276                    if let DynamicImage::ImageRgb8(_) = img {
277                        return Some(FileContent::Provinces(img));
278                    }
279                    let msg = format!(
280                        "`{}` has wrong color format `{:?}`, should be Rgb8",
281                        entry.path().display(),
282                        img.color()
283                    );
284                    err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
285                }
286
287                "default.map" => {
288                    return PdxFile::read_optional_bom(entry, parser).map(FileContent::DefaultMap);
289                }
290                _ => (),
291            }
292        }
293        None
294    }
295
296    fn handle_file(&mut self, entry: &FileEntry, content: FileContent) {
297        match content {
298            FileContent::Adjacencies(content) => {
299                let mut seen_terminator = false;
300                for csv in parse_csv(entry, 1, &content) {
301                    if csv[0].is("-1") {
302                        seen_terminator = true;
303                    } else if seen_terminator {
304                        let msg = "the line with all `-1;` should be the last line in the file";
305                        warn(ErrorKey::ParseError).msg(msg).loc(&csv[0]).push();
306                        break;
307                    } else {
308                        self.adjacencies.extend(Adjacency::parse(&csv));
309                    }
310                }
311                if !seen_terminator {
312                    let msg = "CK3 needs a line with all `-1;` at the end of this file";
313                    err(ErrorKey::ParseError).msg(msg).loc(entry).push();
314                }
315            }
316            FileContent::Definitions(content) => {
317                self.definition_csv = Some(entry.clone());
318                for csv in parse_csv(entry, 0, &content) {
319                    self.parse_definition(&csv);
320                }
321            }
322            FileContent::Provinces(img) => {
323                if let DynamicImage::ImageRgb8(img) = img {
324                    for pixel in img.pixels().dedup().copied() {
325                        unsafe {
326                            // SAFETY: `ColorBitArray::index` is guaranteed to return a valid index
327                            self.colors
328                                .get_unchecked_mut(ColorBitArray::get_index(pixel))
329                                .commit(true);
330                        }
331                    }
332                }
333            }
334            FileContent::DefaultMap(block) => self.load_impassable(&block),
335        }
336    }
337
338    fn finalize(&mut self) {
339        if self.definition_csv.is_none() {
340            // Shouldn't happen, it should come from vanilla if not from the mod
341            eprintln!("map_data/definition.csv is missing?!?");
342            return;
343        }
344        let definition_csv = self.definition_csv.as_ref().unwrap();
345
346        let mut seen_colors = TigerHashMap::default();
347        #[allow(clippy::cast_possible_truncation)]
348        for i in 1..self.provinces.len() as u32 {
349            if let Some(province) = self.provinces.get(&i) {
350                if let Some(k) = seen_colors.get(&province.color) {
351                    let msg = format!("color was already used for id {k}");
352                    warn(ErrorKey::Colors).msg(msg).loc(&province.comment).push();
353                } else {
354                    seen_colors.insert(province.color, i);
355                }
356            } else {
357                let msg = format!("province ids must be sequential, but {i} is missing");
358                err(ErrorKey::Validation).msg(msg).loc(definition_csv).push();
359                return;
360            }
361        }
362        for color_index in self.colors.iter_ones() {
363            let color = ColorBitArray::get_color(color_index);
364            if !seen_colors.contains_key(&color) {
365                let Rgb(rgb) = color;
366                let msg = format!(
367                    "definitions.csv lacks entry for color ({}, {}, {})",
368                    rgb[0], rgb[1], rgb[2]
369                );
370                untidy(ErrorKey::Colors).msg(msg).loc(definition_csv).push();
371            }
372        }
373    }
374}
375
376#[allow(dead_code)] // TODO
377#[derive(Copy, Clone, Debug, Default)]
378pub struct Coords {
379    x: i32,
380    y: i32,
381}
382
383#[allow(dead_code)] // TODO
384#[derive(Clone, Debug)]
385pub struct Adjacency {
386    line: Loc,
387    from: ProvId,
388    to: ProvId,
389    /// TODO: check type is sea or `river_large`
390    /// sea or `river_large`
391    kind: Token,
392    through: ProvId,
393    /// TODO: check start and stop are map coordinates and have the right color on province.png
394    /// They can be -1 -1 though.
395    start: Coords,
396    stop: Coords,
397    comment: Token,
398}
399
400fn verify_field<T: FromStr>(v: &Token, msg: &str) -> Option<T> {
401    let r = v.as_str().parse().ok();
402    if r.is_none() {
403        err(ErrorKey::ParseError).msg(msg).loc(v).push();
404    }
405    r
406}
407
408impl Adjacency {
409    pub fn parse(csv: &[Token]) -> Option<Self> {
410        if csv.is_empty() {
411            return None;
412        }
413
414        let line = csv[0].loc;
415
416        if csv.len() != 9 {
417            let msg = "wrong number of fields for this line, expected 9";
418            err(ErrorKey::ParseError).msg(msg).loc(&csv[0]).push();
419            return None;
420        }
421
422        let from = verify_field(&csv[0], "expected province id");
423        let to = verify_field(&csv[1], "expected province id");
424        let through = verify_field(&csv[3], "expected province id");
425        let start_x = verify_field(&csv[4], "expected x coordinate");
426        let start_y = verify_field(&csv[5], "expected y coordinate");
427        let stop_x = verify_field(&csv[6], "expected x coordinate");
428        let stop_y = verify_field(&csv[7], "expected y coordinate");
429
430        Some(Adjacency {
431            line,
432            from: from?,
433            to: to?,
434            kind: csv[2].clone(),
435            through: through?,
436            start: Coords { x: start_x?, y: start_y? },
437            stop: Coords { x: stop_x?, y: stop_y? },
438            comment: csv[8].clone(),
439        })
440    }
441
442    fn validate(&self, provinces: &Ck3Provinces) {
443        for prov in &[self.from, self.to, self.through] {
444            if !provinces.provinces.contains_key(prov) {
445                let msg = format!("province id {prov} not defined in definitions.csv");
446                fatal(ErrorKey::Crash).msg(msg).loc(self.line).push();
447            }
448        }
449    }
450}
451
452#[derive(Clone, Debug)]
453pub struct Province {
454    key: Token,
455    id: ProvId,
456    color: Rgb<u8>,
457    comment: Token,
458}
459
460impl Province {
461    fn parse(csv: &[Token]) -> Option<Self> {
462        if csv.is_empty() {
463            return None;
464        }
465
466        if csv.len() < 5 {
467            let msg = "too few fields for this line, expected 5";
468            err(ErrorKey::ParseError).msg(msg).loc(&csv[0]).push();
469            return None;
470        }
471
472        let id = verify_field(&csv[0], "expected province id")?;
473        let r = verify_field(&csv[1], "expected red value")?;
474        let g = verify_field(&csv[2], "expected green value")?;
475        let b = verify_field(&csv[3], "expected blue value")?;
476        let color = Rgb::from([r, g, b]);
477        Some(Province { key: csv[0].clone(), id, color, comment: csv[4].clone() })
478    }
479
480    fn validate(&self, provinces: &Ck3Provinces, data: &Everything) {
481        if provinces.sea_or_river.contains(&self.id) {
482            // TODO: this really needs an explanation, like "missing .... for sea zone"
483            data.verify_exists(Item::Localization, &self.comment);
484        }
485    }
486}
487
488#[derive(Clone, Debug)]
489pub struct ProvinceMapping {}
490
491inventory::submit! {
492    ItemLoader::Full(GameFlags::Ck3, Item::ProvinceMapping, PdxEncoding::Utf8Bom, ".txt", LoadAsFile::Yes, Recursive::No, ProvinceMapping::add)
493}
494
495impl ProvinceMapping {
496    pub fn add(db: &mut Db, key: Token, block: Block) {
497        db.add(Item::ProvinceMapping, key, block, Box::new(Self {}));
498    }
499}
500
501impl DbKind for ProvinceMapping {
502    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
503        let mut vd = Validator::new(block, data);
504
505        vd.unknown_value_fields(|key, value| {
506            data.verify_exists(Item::Province, key);
507            data.verify_exists(Item::Province, value);
508        });
509    }
510}