Skip to main content

ck3_regions/
lib.rs

1mod cli;
2mod config;
3mod error;
4mod model;
5mod loading;
6mod map;
7mod texture;
8mod wave;
9mod metadata;
10
11use std::{
12    collections::HashMap,
13    path::Path,
14    sync::{Arc, Mutex},
15    io::Write
16};
17
18use rayon::prelude::*;
19
20use crate::{
21    config::ModeSettings,
22    map::RegionsMapData,
23    model::game::TitleId,
24    texture::RegionsMapImageBuffer,
25    wave::strategies::{ExpandInitialRegionStrategy, FindNearestNonZeroRegionStrategy}
26};
27
28//
29// Exports
30//
31
32pub use crate::{
33    cli::Cli,
34    config::Config,
35    error::Error,
36};
37
38//
39// Constants
40//
41
42const EXPECT_NO_OTHER_ARC_REFERENCES: &str = "no other Arc references";
43
44//
45// Interface
46//
47
48pub fn run(config: Config) -> Result<(), Error> {
49    let RegionsMapData{
50        image_buffer: mut regions_map_image_buffer,
51        region_title_indices,
52        max_region_index,
53        region_initial_pixel_coords,
54    } = match &config.mode_settings {
55        ModeSettings::TitleBasedRegions { region_title_tier } => {
56            map::build_regions_map_from_mod_data(*region_title_tier, &config.mod_paths)?
57        },
58        ModeSettings::PredefinedRegions { input_regions_map_path } => {
59            map::load_predefined_regions_map(&input_regions_map_path)?
60        },
61    };
62
63    println!("map data loaded successfully");
64
65    if config.must_attempt_early_exit || config.is_dry_run {
66        let is_raw_data_current = is_raw_data_current(
67            &config.mod_paths.raw_regions_map,
68            config.output_metadata_yaml_path.as_ref().map(|p| p.as_path()),
69            &regions_map_image_buffer,
70            region_title_indices.as_ref()
71        );
72        println!(
73            "existing raw regions map {} title indices are {}",
74            if is_raw_data_current { "and" } else { "and/or" },
75            if is_raw_data_current { "up to date" } else { "stale" }
76        );
77        if is_raw_data_current {
78            return Ok(());
79        } else if config.is_dry_run {
80            println!("will not regenerate due to --dry-run");
81
82            return Ok(());
83        }
84    }
85
86    regions_map_image_buffer.save(config.mod_paths.raw_regions_map.clone())?;
87    println!("succesfully wrote {}", config.mod_paths.raw_regions_map.display());
88
89    let map_dimensions = regions_map_image_buffer.dimensions();
90
91    if config.must_expand_regions {
92        // Expand regions to fill neutral territory using wave algorithm
93
94        println!("expanding non-zero regions");
95
96        let wave_grid = Arc::new(wave::new_wave_grid(map_dimensions.1 as usize, map_dimensions.0 as usize));
97        let expanded_count = Arc::new(Mutex::new(0));
98        (1..=max_region_index).into_par_iter().for_each(|initial_region_index| {
99            let initial_coords = *region_initial_pixel_coords.get(&initial_region_index)
100                .expect("initial pixel coords must be present for every region");
101
102            wave::expand_wave(&wave_grid, initial_coords, &ExpandInitialRegionStrategy::new(initial_region_index, &regions_map_image_buffer));
103
104            {
105                let mut count = expanded_count.lock().unwrap();
106                *count += 1;
107                print!("\rexpanded {}/{} regions", *count, max_region_index);
108                std::io::stdout().flush().unwrap();
109            }
110        });
111        let wave_grid = Arc::try_unwrap(wave_grid).expect(EXPECT_NO_OTHER_ARC_REFERENCES);
112
113        println!("\nfilling remaining neutral zones");
114
115        for y in 0..wave_grid.rows() as u32 {
116            for x in 0..wave_grid.cols() as u32 {
117                let coords = (x, y);
118
119                let initial_cell_region_index = wave_grid.get(y as usize, x as usize).unwrap().read().unwrap().region_index;
120                if initial_cell_region_index != 0 {
121                    continue;
122                }
123                //println!("filling neutral pixel at ({}, {})", x, y);
124
125                let nearest_region_index = wave::expand_wave(&wave_grid, coords, &FindNearestNonZeroRegionStrategy::new())
126                    .expect("nearest non-zero region must exist");
127
128                wave::set_cell_region_index(&wave_grid, &coords, nearest_region_index);
129            }
130        }
131
132        for y in 0..wave_grid.rows() as u32 {
133            for x in 0..wave_grid.cols() as u32 {
134                let region_index = wave_grid.get(y as usize, x as usize).unwrap().read().unwrap().region_index;
135                if region_index != 0 {
136                    let existing_region_index = texture::decode_index_from_rgb(&regions_map_image_buffer.get_pixel(x, y).0[..3]);
137                    //assert!(existing_region_index == 0 || existing_region_index == region_index);
138                    if existing_region_index == 0 {
139                        regions_map_image_buffer.get_pixel_mut(x, y).0 = texture::encode_index_as_rgba(region_index | (0xFF00 << 16));
140                    }
141                }
142            }
143        }
144    } else {
145        println!("skipped non-zero regions expansion");
146    }
147
148    regions_map_image_buffer.save(config.mod_paths.regions_map.clone())?;
149    println!("succesfully wrote {}", config.mod_paths.regions_map.display());
150
151    println!("registering adjacencies");
152
153    let mut regions_graph = HashMap::new();
154
155    for y in 0..regions_map_image_buffer.height() {
156        for x in 0..regions_map_image_buffer.width() {
157            let region_title_index = texture::decode_index_from_rgb(&regions_map_image_buffer.get_pixel(x, y).0[..3]);
158
159            // Register neighbor adjacencies for our top and left neighbours (the ones we already processed in this loop)
160            for (neighbour_x, neighbour_y) in [(x.checked_sub(1), Some(y)), (Some(x), y.checked_sub(1))] {
161                if neighbour_x.is_none() || neighbour_y.is_none() {
162                    continue;
163                }
164
165                let (neighbour_x, neighbour_y) = (neighbour_x.unwrap(), neighbour_y.unwrap());
166
167                regions_map_image_buffer.get_pixel_checked(neighbour_x, neighbour_y)
168                    .map(|rgba| {
169                        register_adjacency(
170                            &mut regions_graph,
171                            region_title_index,
172                            texture::decode_index_from_rgb(&rgba.0[..3]),
173                            (neighbour_x, neighbour_y)
174                        );
175                    });
176            }
177        }
178    }
179
180    println!("building adjacency and proximity maps");
181
182    let adjacency_map_0_image_buffer = RegionsMapImageBuffer::new(map_dimensions.0, map_dimensions.1);
183    let proximity_map_image_buffer = RegionsMapImageBuffer::new(map_dimensions.0, map_dimensions.1);
184    assert_eq!(adjacency_map_0_image_buffer.dimensions(), map_dimensions);
185    assert_eq!(proximity_map_image_buffer.dimensions(), map_dimensions);
186
187    let adjacency_map_0_image_buffer = Arc::new(Mutex::new(adjacency_map_0_image_buffer));
188    let proximity_map_image_buffer = Arc::new(Mutex::new(proximity_map_image_buffer));
189    let regions_map_image_buffer = Arc::new(regions_map_image_buffer);
190    let regions_graph = Arc::new(regions_graph);
191    let config = Arc::new(config);
192    let processed_rows_count = Arc::new(Mutex::new(0));
193
194    (0..map_dimensions.1).into_par_iter().for_each(|y| {
195        for x in 0..map_dimensions.0 {
196            let current_region_index = texture::decode_index_from_rgb(
197                &regions_map_image_buffer.get_pixel(x, y).0[..3]
198            );
199            if config.must_expand_regions && current_region_index == 0 {
200                continue;
201            }
202
203            let adjacent_regions: Vec<_> = regions_graph.get(&current_region_index)
204                .expect("every region must be present in the graph")
205                .0.iter()
206                .map(|(adjacent_region_index, border_pixels_coords)| {
207                    let min_sq_distance = border_pixels_coords.iter()
208                        .map(|(border_x, border_y)| {
209                            (x as f32 - *border_x as f32).powi(2) + (y as f32 - *border_y as f32).powi(2)
210                        })
211                        .reduce(f32::min)
212                        .expect("border pixel set if present cannot be empty");
213
214                    (adjacent_region_index, min_sq_distance)
215                })
216                .collect();
217
218            let mut adjacent_regions = adjacent_regions;
219            adjacent_regions.sort_by(|(_, sq_distance_0), (_, sq_distance_1)| {
220                sq_distance_0.partial_cmp(sq_distance_1).expect("all distances must be comparable")
221            });
222
223            const DEFAULT_ADJACENCY_PAIR: &(&u32, f32) = &(&0, f32::INFINITY);
224
225            {
226                let mut adj_buf = adjacency_map_0_image_buffer.lock().unwrap();
227                adj_buf.get_pixel_mut(x, y).0 = texture::encode_index_as_rgba(
228                    (adjacent_regions.get(0).unwrap_or(DEFAULT_ADJACENCY_PAIR).0 << 0)
229                        | (0xFF00 << 16)
230                );
231            }
232            {
233                let mut prox_buf = proximity_map_image_buffer.lock().unwrap();
234                prox_buf.get_pixel_mut(x, y).0 = sq_distance_to_proximity_rgba(
235                    adjacent_regions.get(0).unwrap_or(DEFAULT_ADJACENCY_PAIR).1,
236                    config.proximity_distance_limit
237                );
238            }
239        }
240
241        {
242            let mut processed_rows_count = processed_rows_count.lock().unwrap();
243            *processed_rows_count += 1;
244            print!("\rprocessed {}/{} rows", *processed_rows_count, map_dimensions.1);
245            if *processed_rows_count % 128 == 0 {
246                std::io::stdout().flush().unwrap();
247            }
248        }
249    });
250
251    println!(); // newline after progress indicator
252
253    let adjacency_map_0_image_buffer = Arc::try_unwrap(adjacency_map_0_image_buffer)
254        .expect(EXPECT_NO_OTHER_ARC_REFERENCES)
255        .into_inner()
256        .unwrap();
257    let proximity_map_image_buffer = Arc::try_unwrap(proximity_map_image_buffer)
258        .expect(EXPECT_NO_OTHER_ARC_REFERENCES)
259        .into_inner()
260        .unwrap();
261
262    adjacency_map_0_image_buffer.save(config.mod_paths.adjacency_map_0.clone())?;
263    println!("succesfully wrote {}", config.mod_paths.adjacency_map_0.display());
264    //adjacency_map_1_image_buffer.save(config.mod_paths.adjacency_map_1.clone())?;
265    //println!("succesfully wrote {}", config.mod_paths.adjacency_map_1.display());
266
267    proximity_map_image_buffer.save(config.mod_paths.proximity_map.clone())?;
268    println!("succesfully wrote {}", config.mod_paths.proximity_map.display());
269
270    if config.output_metadata_yaml_path.is_some() {
271        metadata::write_metadata(
272            &config.output_metadata_yaml_path.as_ref().unwrap(),
273            &config,
274            region_title_indices.as_ref(),
275            max_region_index
276        ).map_err(Error::from_write_metadata_error)?;
277    }
278
279    Ok(())
280}
281
282//
283// Service types
284//
285
286type BorderPixelsSet = Vec<(u32, u32)>;
287
288struct RegionAdjacencies(HashMap<u32, BorderPixelsSet>);
289
290//
291// Service
292//
293
294fn is_raw_data_current(
295    raw_regions_map_path:           &Path,
296    region_title_indices_yaml_path: Option<&Path>,
297    raw_regions_map_image_buffer:   &RegionsMapImageBuffer,
298    region_title_indices:           Option<&HashMap<TitleId, u32>>
299) -> bool {
300    if !raw_regions_map_path.is_file() {
301        println!("existing raw regions map not found");
302
303        return false;
304    }
305
306    if !region_title_indices_yaml_path.map_or(false, |path| path.is_file()) {
307        println!("no region title indices YAML file available to check");
308
309        return false;
310    }
311
312    map::is_regions_data_current(raw_regions_map_path, region_title_indices_yaml_path.unwrap(), raw_regions_map_image_buffer, region_title_indices.unwrap())
313}
314
315fn register_adjacency(
316    regions_graph:    &mut HashMap<u32, RegionAdjacencies>,
317    current_region_index:  u32,
318    adjacent_region_index: u32,
319    border_pixel_coords:   (u32, u32)
320) {
321    const BORDER_PIXELS_SET_INITIAL_CAPACITY: usize = 256;
322
323    if current_region_index == adjacent_region_index {
324        return;
325    }
326
327    // TODO: Optimize so that we don't store the same adjacency info twice?
328
329    regions_graph.entry(current_region_index)
330        .or_insert_with(|| RegionAdjacencies(HashMap::new()))
331        .0.entry(adjacent_region_index)
332        .or_insert_with(|| BorderPixelsSet::with_capacity(BORDER_PIXELS_SET_INITIAL_CAPACITY))
333        .push(border_pixel_coords);
334
335    regions_graph.entry(adjacent_region_index)
336        .or_insert_with(|| RegionAdjacencies(HashMap::new()))
337        .0.entry(current_region_index)
338        .or_insert_with(|| BorderPixelsSet::with_capacity(BORDER_PIXELS_SET_INITIAL_CAPACITY))
339        .push(border_pixel_coords);
340}
341
342fn sq_distance_to_proximity_rgba(sq_distance: f32, proximity_distance_limit: f32) -> [u8; 4] {
343    let alpha = (proximity_distance_limit - sq_distance.sqrt()).max(0.0)/proximity_distance_limit;
344    let alpha = (alpha*255.0) as u8;
345
346    [0, 0, 0, alpha]
347}