use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader},
path::Path,
};
use image::io::Reader as ImageReader;
use crate::{
config::ModPaths,
metadata::REGION_TITLE_INDICES_KEY,
model::game::{TitleTier, TitleId, MapData, ProvincesMap},
loading::{self},
texture::{self, RegionsMapImageBuffer},
error::Error,
};
pub use self::error::PredefinedRegionsLoadingError;
pub struct RegionsMapData {
pub image_buffer: RegionsMapImageBuffer,
pub region_title_indices: Option<HashMap<TitleId, u32>>,
pub max_region_index: u32,
pub region_initial_pixel_coords: HashMap<u32, (u32, u32)>,
}
pub fn build_regions_map_from_mod_data(region_title_tier: TitleTier, mod_paths: &ModPaths) -> Result<RegionsMapData, Error> {
let map_data = loading::load_map_data_from_mod_path(&mod_paths.mod_root)?;
let mut region_title_indices = HashMap::new();
let mut current_region_title_index = 0u32;
let mut region_initial_pixel_coords = HashMap::new();
let provinces_map = loading::load_provinces_map_from_mod_path(&mod_paths.mod_root, &map_data)?;
let map_dimensions = (provinces_map.0.cols() as u32, provinces_map.0.rows() as u32);
let mut regions_map_image_buffer = RegionsMapImageBuffer::new(map_dimensions.0, map_dimensions.1);
assert_eq!(regions_map_image_buffer.dimensions(), map_dimensions);
for y in 0..regions_map_image_buffer.height() {
for x in 0..regions_map_image_buffer.width() {
let region_title_index = get_region_title_index_from_provinces_map(
&provinces_map,
&map_data,
region_title_tier,
(x, y),
&mut region_title_indices,
&mut current_region_title_index
);
regions_map_image_buffer.get_pixel_mut(x, y).0 = if let Some(region_title_index) = region_title_index {
if !region_initial_pixel_coords.contains_key(®ion_title_index) {
region_initial_pixel_coords.insert(region_title_index, (x, y));
}
texture::encode_index_as_rgba(region_title_index | (0xFF00 << 16))
} else {
[0, 0, 0, 0xFF]
};
}
}
Ok(RegionsMapData{
image_buffer: regions_map_image_buffer,
region_title_indices: Some(region_title_indices),
max_region_index: current_region_title_index,
region_initial_pixel_coords
})
}
pub fn load_predefined_regions_map(input_regions_map_path: &Path) -> Result<RegionsMapData, Error> {
let regions_map_image = ImageReader::open(input_regions_map_path).map_err(PredefinedRegionsLoadingError::from)?.decode()?;
let regions_map_image_buffer = regions_map_image.to_rgba8();
let mut max_region_index = 0u32;
let mut region_initial_pixel_coords = HashMap::new();
for y in 0..regions_map_image_buffer.height() {
for x in 0..regions_map_image_buffer.width() {
let region_index = texture::decode_index_from_rgb(®ions_map_image_buffer.get_pixel(x, y).0[..3]);
max_region_index = region_index.max(max_region_index);
if !region_initial_pixel_coords.contains_key(®ion_index) {
region_initial_pixel_coords.insert(region_index, (x, y));
}
}
}
let missing_region_indices = (0..max_region_index).filter(|region_index| !region_initial_pixel_coords.contains_key(®ion_index));
if !missing_region_indices.clone().all(|region_index| region_index == 0) {
let first_missing_index = missing_region_indices.filter(|®ion_index| region_index != 0)
.min()
.expect("missing region indices range can't be empty at this point");
return Err(PredefinedRegionsLoadingError::DiscontiguousRegionIndices(first_missing_index).into());
}
Ok(RegionsMapData{
image_buffer: regions_map_image_buffer,
region_title_indices: None,
max_region_index,
region_initial_pixel_coords
})
}
pub fn is_regions_data_current(
raw_regions_map_path: &Path,
region_title_indices_yaml_path: &Path,
raw_regions_map_image_buffer: &RegionsMapImageBuffer,
region_title_indices: &HashMap<TitleId, u32>
) -> bool {
let are_region_title_indices_current = try_read_region_title_indices_yaml(region_title_indices_yaml_path)
.map_or(false, |read_region_title_indices| &read_region_title_indices == region_title_indices);
if !are_region_title_indices_current {
println!("region title indices are out of date");
return false;
}
let is_raw_regions_map_current = try_read_regions_map_image(raw_regions_map_path)
.map_or(false, |read_raw_regions_map_image_buffer| &read_raw_regions_map_image_buffer == raw_regions_map_image_buffer);
if !is_raw_regions_map_current {
println!("raw regions map is out of date");
return false;
}
true
}
fn get_region_title_index_from_provinces_map(
provinces_map: &ProvincesMap,
map_data: &MapData,
region_title_tier: TitleTier,
coords: (u32, u32),
region_title_indices_cache: &mut HashMap<TitleId, u32>,
next_available_title_index: &mut u32
) -> Option<u32> {
let province_id = *provinces_map.0.get(coords.1 as usize, coords.0 as usize)
.expect("expected map coordinates to be in bounds");
if let Some(region_title_id) = map_data.get_containing_title_id_by_province_id(province_id, region_title_tier) {
let region_title_index = region_title_indices_cache.get(region_title_id)
.copied()
.unwrap_or_else(|| {
*next_available_title_index += 1;
region_title_indices_cache.insert(region_title_id.clone(), *next_available_title_index);
*next_available_title_index
});
Some(region_title_index)
} else {
None
}
}
fn try_read_region_title_indices_yaml(region_title_indices_yaml_path: &Path) -> Option<HashMap<TitleId, u32>> {
let yaml_file = File::open(region_title_indices_yaml_path).ok()?;
let reader = BufReader::new(yaml_file);
Some(reader.lines()
.skip_while(|line| line.as_ref().map_or(true, |line| !line.starts_with(&format!("{}:", REGION_TITLE_INDICES_KEY))))
.skip(1)
.filter_map(
|line| line.map_or(
None,
|line| {
let substring_before_comment = line.split('#').next();
if let Some(kvp_string) = substring_before_comment {
let tokens = kvp_string.split(':').collect::<Vec<&str>>();
if tokens.len() == 2 {
Some((TitleId::try_from(tokens[0].trim()).ok()?, tokens[1].trim().parse::<u32>().ok()?))
} else {
None
}
} else {
None
}
}
)
)
.collect())
}
fn try_read_regions_map_image(regions_map_path: &Path) -> Option<RegionsMapImageBuffer> {
ImageReader::open(regions_map_path)
.ok()?
.decode()
.map(|dynamic_image| dynamic_image.to_rgba8())
.ok()
}
mod error {
use std::io::Error as IoError;
use thiserror::Error;
use image::ImageError;
#[derive(Error, Debug)]
pub enum PredefinedRegionsLoadingError {
#[error("IO error while loading predefined regions map: {0}")]
IoError(#[from] IoError),
#[error("failed to decode predefined regions image: {0}")]
ImageDecodingFailed(#[from] ImageError),
#[error("predefined region indices are discontiguous; first missing index is {0}")]
DiscontiguousRegionIndices(u32),
}
}