tiger_lib/
rivers.rs

1//! Special validator for the `rivers.png/bmp` file.
2//!
3//! The `rivers.png/bmp` file has detailed requirements for its image format and the layout of every pixel.
4
5use std::fs;
6use std::ops::{RangeInclusive, RangeToInclusive};
7use std::path::PathBuf;
8
9#[cfg(feature = "jomini")]
10use png::{ColorType, Decoder};
11#[cfg(feature = "hoi4")]
12use tinybmp::{Bpp, CompressionMethod, RawBmp};
13
14use crate::Game;
15use crate::everything::Everything;
16use crate::fileset::{FileEntry, FileHandler};
17use crate::helpers::{TigerHashMap, TigerHashSet};
18use crate::parse::ParserMemory;
19use crate::report::{ErrorKey, err, warn, will_maybe_log};
20
21#[inline]
22fn river_image_path() -> &'static str {
23    // TODO: for Imperator, CK3 and Vic3, instead of hardcoded rivers.png file name, get it from map_data/default.map
24    if Game::is_hoi4() { "map/rivers.bmp" } else { "map_data/rivers.png" }
25}
26
27/// The `rivers.png/bmp` has an indexed palette where the colors don't matter, only the index values
28/// used in the pixels matter. Pixels that are not among the values defined here are ignored when
29/// the game processes the `rivers.png/bmp`.
30struct RiverPixels {}
31impl RiverPixels {
32    /// Normal rivers of various widths (usually blue through greenish).
33    /// They are still all one pixel wide in the `rivers.png/bmp`; this just controls how they are painted on the map.
34    /// River pixels must be adjacent to each other horizontally or vertically; together they form river segments.
35    /// River widths go up to 15 even though the vanilla maps only use up to 11 (confirmed for CK3 and Hoi4).
36    const NORMAL: RangeInclusive<u8> = (RiverPixels::FIRST_NORMAL..=RiverPixels::LAST_NORMAL);
37    const FIRST_NORMAL: u8 = 3;
38    const LAST_NORMAL: u8 = 15;
39    /// "specials" are the starting and ending pixels of river segments
40    const SPECIAL: RangeToInclusive<u8> = (..=RiverPixels::LAST_SPECIAL);
41    const LAST_SPECIAL: u8 = 2;
42    /// A pixel at the start of a river segment (usually green)
43    const SOURCE: u8 = 0;
44    /// A pixel that joins one river segment into another (usually red)
45    const TRIBUTARY: u8 = 1;
46    /// A pixel that is used where a river splits off from another (usually yellow)
47    const SPLIT: u8 = 2;
48    /// Noncoding pixels
49    const FIRST_IGNORE: u8 = 16;
50}
51
52#[derive(Clone, Debug, Default)]
53pub struct Rivers {
54    /// for error reporting
55    entry: Option<FileEntry>,
56    width: u32,
57    height: u32,
58    pixels: Vec<u8>,
59}
60
61impl Rivers {
62    pub fn handle_image(&mut self, loaded: &[u8], entry: &FileEntry) {
63        #[cfg(feature = "jomini")]
64        if Game::is_jomini() {
65            let decoder = Decoder::new(std::io::Cursor::new(loaded));
66            let mut reader = match decoder.read_info() {
67                Ok(r) => r,
68                Err(e) => {
69                    err(ErrorKey::ImageFormat)
70                        .msg(format!("image format error: {e:#}"))
71                        .loc(entry)
72                        .push();
73                    return;
74                }
75            };
76
77            let info = reader.info();
78
79            if info.color_type != ColorType::Indexed {
80                let msg = "image should be in indexed color format (with 8-bit palette)";
81                err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
82                return;
83            }
84
85            if info.palette.as_ref().is_none() {
86                let msg = "image must have an 8-bit palette";
87                err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
88                return;
89            }
90
91            self.width = info.width;
92            self.height = info.height;
93            let color_type = info.color_type;
94
95            self.pixels = vec![0; reader.output_buffer_size().unwrap()];
96            let frame_info = match reader.next_frame(&mut self.pixels) {
97                Ok(i) => i,
98                Err(e) => {
99                    err(ErrorKey::ImageFormat)
100                        .msg(format!("image frame error: {e:#}"))
101                        .loc(entry)
102                        .push();
103                    return;
104                }
105            };
106
107            if frame_info.width != self.width
108                || frame_info.height != self.height
109                || frame_info.color_type != color_type
110            {
111                let msg = "image frame did not match image info";
112                err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
113            }
114        }
115
116        #[cfg(feature = "hoi4")]
117        #[allow(clippy::cast_possible_truncation)]
118        if Game::is_hoi4() {
119            let bmp = match RawBmp::from_slice(loaded) {
120                Ok(b) => b,
121                Err(e) => {
122                    err(ErrorKey::ImageFormat)
123                        .msg(format!("image format error: {e:#?}"))
124                        .loc(entry)
125                        .push();
126                    return;
127                }
128            };
129
130            if loaded[14] != 40 {
131                let msg = "bitmap has wrong DIB header format, should be BITMAPINFOHEADER";
132                let info = "see https://hoi4.paradoxwikis.com/Map_modding#BMP_format";
133                err(ErrorKey::ImageFormat).msg(msg).info(info).loc(entry).push();
134                return;
135            }
136
137            let header = bmp.header();
138
139            if header.bpp != Bpp::Bits8 || header.compression_method != CompressionMethod::Rgb {
140                let msg =
141                    "image should be in indexed, uncompressed color format (with 8-bit palette)";
142                err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
143                return;
144            }
145
146            if bmp.color_table().is_none() {
147                let msg = "image must have an 8-bit palette";
148                err(ErrorKey::ImageFormat).msg(msg).loc(entry).push();
149                return;
150            }
151
152            self.width = header.image_size.width;
153            self.height = header.image_size.height;
154            // SAFETY: Known to be 8bpp
155            self.pixels = bmp.pixels().map(|p| p.color as u8).collect();
156        }
157    }
158
159    fn river_neighbors(&self, x: u32, y: u32, output: &mut Vec<(u32, u32)>) {
160        output.clear();
161        if x > 0 && RiverPixels::NORMAL.contains(&self.pixel(x - 1, y)) {
162            output.push((x - 1, y));
163        }
164        if y > 0 && RiverPixels::NORMAL.contains(&self.pixel(x, y - 1)) {
165            output.push((x, y - 1));
166        }
167        if x + 1 < self.width && RiverPixels::NORMAL.contains(&self.pixel(x + 1, y)) {
168            output.push((x + 1, y));
169        }
170        if y + 1 < self.height && RiverPixels::NORMAL.contains(&self.pixel(x, y + 1)) {
171            output.push((x, y + 1));
172        }
173    }
174
175    fn special_neighbors(&self, c: (u32, u32)) -> Vec<(u32, u32)> {
176        let (x, y) = c;
177        let mut vec = Vec::new();
178        if x > 0 && RiverPixels::SPECIAL.contains(&self.pixel(x - 1, y)) {
179            vec.push((x - 1, y));
180        }
181        if y > 0 && RiverPixels::SPECIAL.contains(&self.pixel(x, y - 1)) {
182            vec.push((x, y - 1));
183        }
184        if x + 1 < self.width && RiverPixels::SPECIAL.contains(&self.pixel(x + 1, y)) {
185            vec.push((x + 1, y));
186        }
187        if y + 1 < self.height && RiverPixels::SPECIAL.contains(&self.pixel(x, y + 1)) {
188            vec.push((x, y + 1));
189        }
190        vec
191    }
192
193    #[inline]
194    fn pixel(&self, x: u32, y: u32) -> u8 {
195        let idx = (x + self.width * y) as usize;
196        self.pixels[idx]
197    }
198
199    fn validate_segments(
200        &self,
201        entry: &FileEntry,
202        river_segments: TigerHashMap<(u32, u32), (u32, u32)>,
203        mut specials: TigerHashMap<(u32, u32), bool>,
204    ) {
205        let mut seen = TigerHashSet::default();
206
207        for (start, end) in river_segments {
208            if seen.contains(&start) {
209                continue;
210            }
211            seen.insert(end);
212
213            if start == end {
214                // Single-pixel segment
215                let special_neighbors = self.special_neighbors(start);
216                if special_neighbors.len() > 1 {
217                    let msg = format!(
218                        "({}, {}) river pixel connects two special pixels",
219                        start.0, start.1
220                    );
221                    warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
222                } else if special_neighbors.is_empty() {
223                    let msg = format!("({}, {}) orphan river pixel", start.0, start.1);
224                    warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
225                } else {
226                    let s = special_neighbors[0];
227                    if specials[&s] {
228                        let msg =
229                            format!("({}, {}) pixel terminates multiple river segments", s.0, s.1);
230                        warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
231                    } else {
232                        specials.insert(s, true);
233                    }
234                }
235            } else {
236                let mut special_neighbors = self.special_neighbors(start);
237                special_neighbors.append(&mut self.special_neighbors(end));
238                if special_neighbors.is_empty() {
239                    let msg = format!(
240                        "({}, {}) - ({}, {}) orphan river segment",
241                        start.0, start.1, end.0, end.1
242                    );
243                    warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
244                } else if special_neighbors.len() > 1 {
245                    let msg = format!(
246                        "({}, {}) - ({}, {}) river segment has two terminators",
247                        start.0, start.1, end.0, end.1
248                    );
249                    warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
250                } else {
251                    let s = special_neighbors[0];
252                    if specials[&s] {
253                        let msg =
254                            format!("({}, {}) pixel terminates multiple river segments", s.0, s.1);
255                        warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
256                    } else {
257                        specials.insert(s, true);
258                    }
259                }
260            }
261        }
262    }
263
264    pub fn validate(&self, _data: &Everything) {
265        // TODO: check image width and height against world defines
266
267        let Some(entry) = self.entry.as_ref() else {
268            // Shouldn't happen, it should come from vanilla if not from the mod
269            eprintln!("{} is missing?!?", river_image_path());
270            return;
271        };
272
273        // Early exit before expensive loop, if errors won't be logged anyway
274        if !will_maybe_log(entry, ErrorKey::Rivers) {
275            return;
276        }
277
278        // Maps each endpoint of a segment to the other endpoint.
279        // Single-pixel segments map that coordinate to itself.
280        // The river pixels that connect the endpoints are not remembered.
281        let mut river_segments: TigerHashMap<(u32, u32), (u32, u32)> = TigerHashMap::default();
282
283        // Maps the coordinates of special pixels (sources, sinks, and splits)
284        // to a boolean that says whether the pixel terminates a segment.
285        let mut specials = TigerHashMap::default();
286
287        // A working vec, holding the list of river-pixel neighbors of the current pixel.
288        // It is declared here to avoid the overhead of creating and destroying the Vec in every
289        // iteration.
290        let mut river_neighbors = Vec::new();
291
292        let mut bad_problem = false;
293        // TODO: multi-thread this
294        for x in 0..self.width {
295            for y in 0..self.height {
296                match self.pixel(x, y) {
297                    RiverPixels::SOURCE => {
298                        self.river_neighbors(x, y, &mut river_neighbors);
299                        if river_neighbors.len() == 1 {
300                            specials.insert((x, y), false);
301                        } else {
302                            let msg =
303                                format!("({x}, {y}) river source (green) not at source of a river");
304                            warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
305                            bad_problem = true;
306                        }
307                    }
308                    RiverPixels::TRIBUTARY => {
309                        self.river_neighbors(x, y, &mut river_neighbors);
310                        if river_neighbors.len() >= 2 {
311                            specials.insert((x, y), false);
312                        } else {
313                            let msg = format!(
314                                "({x}, {y}) river tributary (red) not joining another river",
315                            );
316                            warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
317                            bad_problem = true;
318                        }
319                    }
320                    RiverPixels::SPLIT => {
321                        self.river_neighbors(x, y, &mut river_neighbors);
322                        if river_neighbors.len() >= 2 {
323                            specials.insert((x, y), false);
324                        } else {
325                            let msg = format!(
326                                "({x}, {y}) river split (yellow) not splitting off from a river",
327                            );
328                            warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
329                            bad_problem = true;
330                        }
331                    }
332                    RiverPixels::FIRST_NORMAL..=RiverPixels::LAST_NORMAL => {
333                        self.river_neighbors(x, y, &mut river_neighbors);
334                        if river_neighbors.len() <= 2 {
335                            let mut found = false;
336                            for &coords in &river_neighbors {
337                                if let Some(&other_end) = river_segments.get(&coords) {
338                                    found = true;
339                                    if let Some(&third_end) = river_segments.get(&(x, y)) {
340                                        // This can only happen if we're on the second iteration.
341                                        // It means the pixel borders two segments, and joins them.
342                                        // First make sure it's not a single segment in a loop
343                                        // though.
344                                        if third_end == (x, y) {
345                                            let msg = format!("({x}, {y}) river forms a loop");
346                                            warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
347                                            bad_problem = true;
348                                        } else {
349                                            river_segments.insert(other_end, third_end);
350                                            river_segments.insert(third_end, other_end);
351                                            river_segments.remove(&(x, y));
352                                            river_segments.remove(&coords);
353                                        }
354                                    } else {
355                                        // Extend the neighboring segment to include this pixel.
356                                        river_segments.insert((x, y), other_end);
357                                        river_segments.insert(other_end, (x, y));
358                                        river_segments.remove(&coords);
359                                    }
360                                }
361                            }
362                            if !found {
363                                // Start a new single-pixel segment.
364                                river_segments.insert((x, y), (x, y));
365                            }
366                        } else {
367                            let msg = format!(
368                                "({x}, {y}) river pixel has {} neighbors",
369                                river_neighbors.len()
370                            );
371                            warn(ErrorKey::Rivers).msg(msg).loc(entry).push();
372                            bad_problem = true;
373                        }
374                    }
375                    RiverPixels::FIRST_IGNORE.. => (),
376                }
377            }
378        }
379        if !bad_problem {
380            self.validate_segments(entry, river_segments, specials);
381        }
382    }
383}
384
385impl FileHandler<Vec<u8>> for Rivers {
386    fn subpath(&self) -> PathBuf {
387        PathBuf::from(river_image_path())
388    }
389
390    fn load_file(&self, entry: &FileEntry, _parser: &ParserMemory) -> Option<Vec<u8>> {
391        match fs::read(entry.fullpath()) {
392            Err(e) => {
393                err(ErrorKey::ReadError)
394                    .msg(format!("could not read file: {e:#}"))
395                    .loc(entry)
396                    .push();
397                None
398            }
399            Ok(loaded) => Some(loaded),
400        }
401    }
402
403    fn handle_file(&mut self, entry: &FileEntry, loaded: Vec<u8>) {
404        self.entry = Some(entry.clone());
405        self.handle_image(&loaded, entry);
406    }
407}