mod titles;
use std::{
path::{Path, PathBuf},
io::{BufReader, BufRead},
fs::File,
str::FromStr,
num::ParseIntError,
sync::{Arc, Mutex},
};
use csv::{
ReaderBuilder as CsvReaderBuilder,
StringRecord
};
use glob::{glob, GlobError};
use image::{
io::Reader as ImageReader,
ImageError, GenericImageView, Pixel,
};
use grid::Grid;
use crate::model::game::{
MapData,
MapDataBuilder,
ProvinceId,
Rgb,
ProvincesMap,
};
pub use self::error::{
MapDataLoadingError,
ProvincesMapLoadingError
};
const PROVINCE_DEFINITION_PATH_STR: &str = "map_data/definition.csv";
const LANDED_TITLES_DIR_PATH_STR: &str = "common/landed_titles";
const PROVINCES_MAP_IMAGE_PATH_STR: &str = "map_data/provinces.png";
const CSV_FIELD_DELIMITER: u8 = b';';
const CSV_LINE_COMMENT_STR: u8 = b'#';
const MIN_PROVINCE_DEFINITION_COLUMNS: usize = 4;
pub fn load_map_data_from_mod_path(mod_root_path: &Path) -> Result<MapData, MapDataLoadingError> {
let mod_root_path = mod_root_path.to_path_buf();
let definition_file_path = mod_root_path.join(PROVINCE_DEFINITION_PATH_STR);
let landed_titles_dir_path = mod_root_path.join(LANDED_TITLES_DIR_PATH_STR);
let map_data_builder = Arc::new(Mutex::new(MapDataBuilder::new()));
let map_data_builder_arc = Arc::clone(&map_data_builder);
let provinces_thread = std::thread::spawn(move || {
add_provinces_from_definition_file(&map_data_builder_arc, &definition_file_path)
});
let map_data_builder_arc = Arc::clone(&map_data_builder);
let titles_thread = std::thread::spawn(move || {
add_titles_from_landed_titles_dir(&map_data_builder_arc, &landed_titles_dir_path)
});
provinces_thread.join().expect("province loading thread must not panic")?;
titles_thread.join().expect("titles loading thread must not panic")?;
let builder = Arc::try_unwrap(map_data_builder).unwrap().into_inner().unwrap();
builder.build().map_err(MapDataLoadingError::from)
}
pub fn load_provinces_map_from_mod_path(mod_root_path: &Path, map_data: &MapData) -> Result<ProvincesMap, ProvincesMapLoadingError> {
let provinces_map_image = ImageReader::open(mod_root_path.join(PROVINCES_MAP_IMAGE_PATH_STR))?.decode()?;
let provinces_map_dimensions = provinces_map_image.dimensions();
let mut provinces_map_grid = Grid::init(provinces_map_dimensions.1 as usize, provinces_map_dimensions.0 as usize, ProvinceId::NULL_ID);
for y in 0..provinces_map_dimensions.1 {
for x in 0..provinces_map_dimensions.0 {
let province_rgb = provinces_map_image.get_pixel(x, y).to_rgb();
let province_id = map_data.get_province_id_by_rgb(province_rgb)
.ok_or_else(|| ProvincesMapLoadingError::UnrecognizedProvinceRgb(province_rgb, (x, y)))?;
*provinces_map_grid.get_mut(y as usize, x as usize).unwrap() = province_id;
}
}
Ok(ProvincesMap(provinces_map_grid))
}
fn add_provinces_from_definition_file(
map_data_builder: &Arc<Mutex<MapDataBuilder>>,
definition_file_path: &Path
) -> Result<(), MapDataLoadingError> {
let definition_file = File::open(definition_file_path)?;
let mut csv_reader = CsvReaderBuilder::new()
.has_headers(false)
.delimiter(CSV_FIELD_DELIMITER)
.comment(Some(CSV_LINE_COMMENT_STR))
.from_reader(definition_file);
for result in csv_reader.records() {
let record = result
.map_err(|csv_err| MapDataLoadingError::CsvFormatError(definition_file_path.to_path_buf(), csv_err))?;
if record.len() < MIN_PROVINCE_DEFINITION_COLUMNS {
return Err(MapDataLoadingError::TooFewProvinceDefinitionColumns(record.len()));
}
let province_id = ProvinceId(parse_from_csv_cell(&record, 0)?);
let province_rgb = Rgb::from([
parse_from_csv_cell(&record, 1)?,
parse_from_csv_cell(&record, 2)?,
parse_from_csv_cell(&record, 3)?
]);
map_data_builder.lock().unwrap().add_province(province_id, province_rgb);
}
Ok(())
}
fn add_titles_from_landed_titles_dir(
map_data_builder: &Arc<Mutex<MapDataBuilder>>,
landed_titles_dir_path: &Path
) -> Result<(), MapDataLoadingError> {
const LANDED_TITLES_FILENAME_PATTERN: &str = "*.txt";
let landed_titles_file_path_pattern = landed_titles_dir_path
.join(LANDED_TITLES_FILENAME_PATTERN)
.to_str()
.expect("assuming unicode paths")
.to_string();
let landed_titles_file_paths = glob(&landed_titles_file_path_pattern)
.expect("landed titles glob pattern must be valid");
for landed_titles_file_path in landed_titles_file_paths {
add_titles_from_landed_titles_file(
map_data_builder,
&landed_titles_file_path.map_err(GlobError::into_error)?
)?;
}
Ok(())
}
fn add_titles_from_landed_titles_file(
map_data_builder: &Arc<Mutex<MapDataBuilder>>,
landed_titles_file_path: &Path
) -> Result<(), MapDataLoadingError> {
let landed_titles_file = File::open(landed_titles_file_path)?;
let lines = BufReader::new(landed_titles_file).lines();
let mut map_data_builder = map_data_builder.lock().unwrap();
titles::read_landed_titles_from_file_lines(&mut map_data_builder, lines)
.map_err(|err| MapDataLoadingError::from_landed_titles_reading_err(landed_titles_file_path, err))
}
fn parse_from_csv_cell<T>(record: &StringRecord, column_index: usize) -> Result<T, MapDataLoadingError>
where T: FromStr<Err = ParseIntError> {
record[column_index].parse::<T>()
.map_err(MapDataLoadingError::from)
}
mod error {
use std::io::Error as IoError;
use thiserror::Error;
use csv::Error as CsvError;
use crate::model::game::{TitleIdError, MapDataError};
use super::{
titles::{LandedTitlesReadingError, LandedTitlesParsingError},
*
};
#[derive(Error, Debug)]
pub enum MapDataLoadingError {
#[error("IO error while loading map data: {0}")]
IoError(#[from] IoError),
#[error("failed to parse {0} as CSV: {1}")]
CsvFormatError(PathBuf, #[source] CsvError),
#[error(
"province definition has {0} columns, expected at least {}; make sure you use {:?} as CSV delimeter",
MIN_PROVINCE_DEFINITION_COLUMNS,
char::from(CSV_FIELD_DELIMITER)
)]
TooFewProvinceDefinitionColumns(usize),
#[error("failed to parse a value from province definition: {0}")]
ProvinceDefinitionValueError(#[from] ParseIntError),
#[error("{0}")]
InvalidTitleId(#[from] TitleIdError),
#[error("encountered {} map data error{}", .0.len(), if .0.len() != 1 { "s" } else { "" } )]
InvalidMapData(Vec<MapDataError>),
#[error("failed to parse landed titles from {0}: {1}")]
LandedTitlesParsingFailed(PathBuf, LandedTitlesParsingError)
}
impl From<Vec<MapDataError>> for MapDataLoadingError {
fn from(value: Vec<MapDataError>) -> Self {
Self::InvalidMapData(value)
}
}
impl MapDataLoadingError {
pub fn from_landed_titles_reading_err(landed_titles_file_path: &Path, err: LandedTitlesReadingError) -> Self {
match err {
LandedTitlesReadingError::IoError(io_err) => Self::from(io_err),
LandedTitlesReadingError::MapDataError(data_err) => Self::from(vec![data_err]),
LandedTitlesReadingError::ParsingError(parsing_err) => {
Self::LandedTitlesParsingFailed(landed_titles_file_path.to_path_buf(), parsing_err)
}
}
}
}
#[derive(Error, Debug)]
pub enum ProvincesMapLoadingError {
#[error("IO error while loading provinces map: {0}")]
IoError(#[from] IoError),
#[error("failed to decode provinces map: {0}")]
ImageDecodingFailed(#[from] ImageError),
#[error("unrecognized RGB {0:?} at {1:?} in provinces map image")]
UnrecognizedProvinceRgb(Rgb, (u32, u32))
}
}