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: ColorBitArray,
69
70 provinces: TigerHashMap<ProvId, Province>,
74
75 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 }
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 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 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)] #[derive(Copy, Clone, Debug, Default)]
378pub struct Coords {
379 x: i32,
380 y: i32,
381}
382
383#[allow(dead_code)] #[derive(Clone, Debug)]
385pub struct Adjacency {
386 line: Loc,
387 from: ProvId,
388 to: ProvId,
389 kind: Token,
392 through: ProvId,
393 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 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}