1use 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 if Game::is_hoi4() { "map/rivers.bmp" } else { "map_data/rivers.png" }
25}
26
27struct RiverPixels {}
31impl RiverPixels {
32 const NORMAL: RangeInclusive<u8> = (RiverPixels::FIRST_NORMAL..=RiverPixels::LAST_NORMAL);
37 const FIRST_NORMAL: u8 = 3;
38 const LAST_NORMAL: u8 = 15;
39 const SPECIAL: RangeToInclusive<u8> = (..=RiverPixels::LAST_SPECIAL);
41 const LAST_SPECIAL: u8 = 2;
42 const SOURCE: u8 = 0;
44 const TRIBUTARY: u8 = 1;
46 const SPLIT: u8 = 2;
48 const FIRST_IGNORE: u8 = 16;
50}
51
52#[derive(Clone, Debug, Default)]
53pub struct Rivers {
54 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 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 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 let Some(entry) = self.entry.as_ref() else {
268 eprintln!("{} is missing?!?", river_image_path());
270 return;
271 };
272
273 if !will_maybe_log(entry, ErrorKey::Rivers) {
275 return;
276 }
277
278 let mut river_segments: TigerHashMap<(u32, u32), (u32, u32)> = TigerHashMap::default();
282
283 let mut specials = TigerHashMap::default();
286
287 let mut river_neighbors = Vec::new();
291
292 let mut bad_problem = false;
293 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 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 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 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}