lib_efo/epoch/epoch_config.rs
1//! Management of the *current-epoch* configuration.
2//!
3//! The configuration is stored in a file named `.epoch` and contains only
4//! the lightweight information needed for the CLI to switch between epochs
5//! quickly:
6//! * the numeric ID of the current epoch,
7//! * a map of human-friendly tags to epoch IDs,
8//! * a map of known epoch IDs to unloaded metadata (`EpochInfo`).
9//!
10//! The world data and all event logs belong to the individual epoch
11//! directories and are **not** part of this file.
12
13use crate::{
14 epoch::{
15 EpochId, EpochIdentifier, EpochInfo, MAIN_NAME, SUBFOLDER_NAMING, SaveMode, TagList,
16 epoch_traits::ConfigWritable,
17 },
18 stats::{self, Symbols},
19};
20use colored::*;
21use std::{
22 collections::HashMap,
23 fs::{read_to_string, remove_dir_all},
24 path::Path,
25};
26
27use log::{info, warn};
28use serde::{Deserialize, Serialize};
29
30use crate::error::EpochError;
31
32/// The file written to *.epochconfig* that tells the CLI which
33/// epoch folder is the “current” one.
34///
35/// Only the minimal information needed for fast switches is stored:
36/// * the numeric identifier of the selected epoch,
37/// * a list of human-readable tags,
38/// * the absolute path to the epoch's directory.
39///
40/// No world or event data lives here - it is purely a lightweight pointer.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct EpochConfig {
43 /// The epoch ID that the binary should consider “current”.
44 pub epoch_id: EpochId,
45
46 /// The version of the file loaded, and hopefully of all the project
47 pub version: String,
48
49 /// Optional human-friendly tags (e.g. `"winter-B"`, `"post-war"`).
50 pub tags: HashMap<String, EpochId>,
51 // TODO(6): Can become important
52 // #[serde(skip)]
53 // /// Absolute path to `epoch/<epoch_id>/`.
54 // pub path: PathBuf,
55 /// A hashmap containing the name of all existing epochs, only loaded in memory if used
56 #[serde(skip)]
57 pub list_existing_epochs: HashMap<EpochId, Option<EpochInfo>>,
58}
59
60impl Default for EpochConfig {
61 /// Create an empty config, mainly for initialisation
62 fn default() -> Self {
63 EpochConfig {
64 epoch_id: EpochId::new(0),
65 tags: HashMap::new(),
66 list_existing_epochs: HashMap::new(),
67 version: env!("EFO_SHORT_VERSION").to_string(),
68 }
69 }
70}
71
72impl EpochConfig {
73 /// Create a new config from an existig path
74 pub fn new_from_path(_path: Option<&Path>) -> Result<Self, EpochError> {
75 let path = Path::new(MAIN_NAME);
76
77 if path.is_file() {
78 // TODO(6): write a trait for this
79 let config: EpochConfig = serde_json::from_str(&io_err_with_help!(
80 read_to_string(path),
81 "Reading config from path {:?}",
82 path
83 )?)
84 .map_err(|e| EpochError::Serde {
85 source: e,
86 file_name: path.to_string_lossy().to_string(),
87 version: format!("Current version: {}", env!("EFO_SHORT_VERSION")),
88 })?;
89 Ok(config.initialize()?)
90 } else {
91 Err(EpochError::MainFolderInvalid(
92 EpochConfig::default().save_folder(),
93 ))
94 }
95 }
96
97 /// Output the current version vs the filesystem version
98 pub fn version(&self) -> String {
99 format!(
100 "Program version {}.\nFilesystem version: {}.",
101 self.version,
102 env!("EFO_SHORT_VERSION")
103 )
104 }
105
106 /// Initialize the epoch structure in depth.
107 ///
108 /// Scans the default configuration folder for sub-directories that are
109 /// interpreted as epoch identifiers, logs each discovered epoch and
110 /// registers it in the in-memory cache.
111 ///
112 /// This function may return an error if the configuration folder cannot
113 /// be read or if any directory name cannot be parsed as an `EpochId`.
114 pub(crate) fn initialize(mut self) -> Result<Self, EpochError> {
115 let path = EpochConfig::default().save_subfolder();
116 let path = io_err_with_help!(Path::new(&path).read_dir(), "Reading initial folder {path}")?;
117 // let mut valid_folders = HashMap::new();
118 for elt in path {
119 let elt = io_err_with_help!(elt, "Opening file in main config folder")?;
120 if io_err_with_help!(elt.file_type(), "Determining file type of {:?}", elt)?.is_dir() {
121 let id = elt.file_name().to_string_lossy().parse::<EpochId>()?;
122 info!("Preloaded Epoch {:?}", id);
123 self.list_existing_epochs.insert(id, None);
124 }
125 }
126 Ok(self)
127 }
128
129 /// Loads the metadata for an epoch from disk into the in-memory cache.
130 ///
131 /// The epoch identified by `epoch_id` must exist in `list_existing_epochs`.
132 /// The JSON file on disk is read, deserialized into an `EpochInfo`, and
133 /// stored in the map. Any existing value for that epoch is overwritten.
134 ///
135 /// # Returns
136 /// * `Ok(())` when the epoch was successfully read and cached.
137 /// * `Err(EpochError::EpochNotFound(..))` if the epoch is not present in the
138 /// internal map.
139 /// * `Err(EpochError::Serde { .. })` if the JSON file could not be parsed.
140 /// * `Err(EpochError::Io { .. })` if the file could not be read.
141 pub fn try_load(&mut self, epoch_id: &EpochId) -> Result<(), EpochError> {
142 // Preload the epoch
143 let version = self.version();
144 let value = self
145 .list_existing_epochs
146 .get_mut(epoch_id)
147 .ok_or(EpochError::EpochNotFound(format!("{epoch_id}")))?;
148
149 // Create a mock epoch
150 let path = EpochInfo::with_id(epoch_id).save_path();
151 let read_to_string = io_err_with_help!(read_to_string(&path), "Reading {:?}", path);
152
153 // Try to load
154 let maybe_epoch: EpochInfo =
155 serde_json::from_str(&read_to_string?).map_err(|e| EpochError::Serde {
156 source: e,
157 file_name: path.to_string(),
158 version,
159 })?;
160 log::info!("Fully loaded Epoch {:?}", epoch_id);
161
162 // Overwrite the value
163 *value = Some(maybe_epoch);
164 Ok(())
165 }
166
167 /// Load the currently active epoch.
168 ///
169 /// This helper first ensures that the current epoch's metadata is cached
170 /// by delegating to `try_load`, then returns a reference to it.
171 ///
172 /// # Returns
173 /// * `Ok(&EpochInfo)` - Reference to the cached epoch.
174 /// * Propagates any error from `try_load`.
175 pub fn try_load_current(&mut self) -> Result<&EpochInfo, EpochError> {
176 self.try_load(&self.epoch_id.clone())?;
177 unsafe { self.try_load_id(self.epoch_id) }
178 }
179
180 /// Load an epoch by its identifier.
181 ///
182 /// The caller must guarantee that the epoch has already been loaded into
183 /// the cache (e.g. by calling `try_load`). This function is marked `unsafe`
184 /// because it performs a raw reference conversion without performing that
185 /// check.
186 ///
187 /// # Safety
188 /// Calling this function when the requested epoch has not yet been
189 /// loaded results in undefined behaviour.
190 ///
191 /// # Arguments
192 /// * `epoch_id` - Identifier of the epoch to load.
193 ///
194 /// # Returns
195 /// * `Ok(&EpochInfo)` - Reference to the cached epoch.
196 /// * `Err(EpochError::EpochHashingImpossible(..))` - The identifier is
197 /// unknown.
198 /// * `Err(EpochError::LoadingEpochId(..))` - The epoch data is not yet
199 /// present in the cache.
200 pub unsafe fn try_load_id(&self, epoch_id: EpochId) -> Result<&EpochInfo, EpochError> {
201 let current = self
202 .list_existing_epochs
203 .get(&epoch_id)
204 .ok_or(EpochError::EpochHashingImpossible(file!(), line!()))?
205 .as_ref()
206 .ok_or(EpochError::LoadingEpochId(self.epoch_id))?;
207 Ok(current)
208 }
209
210 /// Load the currently active epoch mutably.
211 ///
212 /// This is a convenience wrapper that forwards to `try_load_id_as_mut`
213 /// for the current epoch.
214 ///
215 /// # Returns
216 /// * `Ok(&mut EpochInfo)` - Mutable reference to the cached epoch.
217 /// * Propagates any error from the underlying load routine.
218 pub fn try_load_current_as_mut(&mut self) -> Result<&mut EpochInfo, EpochError> {
219 self.try_load_id_as_mut(self.epoch_id)
220 }
221
222 /// Load an epoch's metadata mutably.
223 ///
224 /// The function guarantees that the epoch data is present in the cache
225 /// by first invoking `try_load` on the current epoch ID. It then
226 /// retrieves a mutable reference to the requested epoch.
227 ///
228 /// # Arguments
229 /// * `epoch_id` - Identifier of the epoch to load.
230 ///
231 /// # Returns
232 /// * `Ok(&mut EpochInfo)` - Mutable reference to the cached epoch.
233 /// * `Err(EpochError::EpochHashingImpossible(..))` - The identifier is
234 /// unknown.
235 /// * `Err(EpochError::LoadingEpochId(..))` - The epoch data is not yet
236 /// present in the cache.
237 ///
238 /// # Notes
239 /// The call to `try_load` may incur a small performance penalty because
240 /// it ensures that the epoch metadata is read from disk even when the
241 /// caller already knows the epoch is cached. This function is *not* `unsafe`;
242 /// it performs all safety checks internally.
243 fn try_load_id_as_mut(&mut self, epoch_id: EpochId) -> Result<&mut EpochInfo, EpochError> {
244 self.try_load(&self.epoch_id.clone())?;
245 let current = self
246 .list_existing_epochs
247 .get_mut(&epoch_id)
248 .ok_or(EpochError::EpochHashingImpossible(file!(), line!()))?
249 .as_mut()
250 .ok_or(EpochError::LoadingEpochId(self.epoch_id))?;
251 Ok(current)
252 }
253
254 /// Retrieves the internal epoch ID for a given identifier.
255 ///
256 /// The identifier can be either a *tag* string (`EpochIdentifier::Tag`) or an
257 /// explicit epoch ID (`EpochIdentifier::Id`).
258 /// * When a tag is supplied the function looks up the corresponding epoch ID in
259 /// the `tags` map.
260 /// * When an ID is supplied it verifies that an epoch with that ID exists in
261 /// `list_existing_epochs` and returns it.
262 ///
263 /// # Returns
264 /// * `Ok(epoch_id)` when the identifier is successfully resolved.
265 /// * `Err(EpochError::AmbiguousTag(tag))` if a supplied tag does not exist.
266 /// * `Err(EpochError::EpochFolderMissing(id, path))` if an ID does not have an
267 /// associated epoch folder.
268 pub fn get_epoch_id(&self, id: &EpochIdentifier) -> Result<EpochId, EpochError> {
269 match id {
270 EpochIdentifier::Tag(s) => self
271 .tags
272 .get(s)
273 .copied()
274 .ok_or(EpochError::AmbiguousTag(s.to_string())),
275 EpochIdentifier::Id(epoch_id) => self
276 .list_existing_epochs
277 .contains_key(epoch_id)
278 .then_some(epoch_id)
279 .ok_or(EpochError::EpochFolderMissing(
280 *epoch_id,
281 EpochConfig::default().save_folder().into(),
282 ))
283 .copied(),
284 }
285 }
286
287 /// Return the name of the subfolder used to save this structure
288 pub(crate) fn save_subfolder(&self) -> String {
289 format!("{}/", SUBFOLDER_NAMING)
290 }
291
292 /// Delete an entire epoch folder by its ID.
293 ///
294 /// The function removes the directory, deletes all references to the
295 /// epoch from `list_existing_epochs` and `tags`, and then writes the
296 /// updated configuration to disk.
297 // TODO(9): Create a delete_ids taking an iterator for added performance on huge deletions
298 pub fn delete_id(&mut self, id: EpochId) -> Result<(), EpochError> {
299 if !self.list_existing_epochs.contains_key(&id) {
300 warn!(
301 "The given epoch {} doesn't exists and it's deletion was skipped",
302 id
303 )
304 } else {
305 io_err_with_help!(
306 remove_dir_all(EpochInfo::with_id(&id).save_folder()),
307 "Removing the epoch {}",
308 id
309 )?;
310 self.list_existing_epochs.remove(&id);
311 self.tags.retain(|_k, v| *v != id);
312 self.save_with_context("Saving after delete_id", &SaveMode::Erase)?;
313 }
314 Ok(())
315 }
316
317 /// Compute the next epoch.
318 ///
319 /// Currently this function is a placeholder that creates a new epoch
320 /// with the ID immediately following the current one. The actual logic
321 /// is deferred to the `EpochInfo -> World` subsystem.
322 pub fn compute_next(&mut self) -> Result<EpochInfo, EpochError> {
323 // unimplemented!();
324 // Iter on events, increment time
325 // Iter on character, increment time
326 //
327 // create a new_towns_values vector, update it with base stats, then all events modifying underlying value
328 // create_new_character_base_v vector, do the same
329 // calculate the temps values (via function)
330 // TODO(1): Implement this
331 warn!("DEBUG LINE: COMPUTING NEXT EPOCH");
332 // Set base for new epoch
333 let mut new_epoch = self.try_load_current()?.clone();
334 let _ = self.increment_time(&mut new_epoch);
335 new_epoch.epoch_id = self.epoch_id.next();
336 new_epoch.summary = String::new();
337 new_epoch.tags = TagList::new();
338
339 // Increment time on all events
340 Ok(new_epoch)
341 }
342
343 /// Increment time, can only be called from inside
344 /// Must be called on initialized data
345 fn increment_time(&mut self, new_epoch: &mut EpochInfo) -> Result<(), EpochError> {
346 new_epoch
347 .iter_all_entities_mut()
348 .for_each(|f| f.increment_time());
349
350 Ok(())
351 }
352
353 /// Update **all** temporary values of every entity in the current epoch.
354 ///
355 /// # Safety
356 /// * The caller must guarantee that the current epoch is **loaded** and
357 /// its storage is still valid for the duration of the call.
358 /// * The function performs raw pointer arithmetic / `unsafe` reads/writes
359 /// on the internal data structures. No other thread may be mutably
360 /// borrowing the same data at the same time.
361 pub unsafe fn update_all_temporary_values(&mut self) {}
362
363 /// Reset **all** temporary values to the current live values.
364 ///
365 /// # Safety
366 /// * The caller must ensure that the current epoch is loaded and that
367 /// no other mutable references to the same data exist.
368 /// * This method writes directly into the entity structs; any concurrent
369 /// access will lead to undefined behaviour.
370 pub unsafe fn reset_temporary_stats(&mut self) -> Result<(), EpochError> {
371 let current_epoch = self.try_load_current_as_mut()?;
372 current_epoch
373 .vec_entities_mut(&stats::EntityType::Town)
374 .iter_mut()
375 .for_each(|entity| entity.reset_temporary_stats_to(Some(entity.get_stats().clone())));
376
377 Ok(())
378 }
379
380 /// Update temporary values for every entity assuming the current epoch
381 /// is loaded.
382 ///
383 /// # Safety
384 /// * Same as `update_all_temporary_values`: the current epoch must be
385 /// loaded and no other mutable references may exist.
386 pub unsafe fn update_temps(&mut self) {}
387
388 /// Insert a new epoch into the configuration and persist it.
389 ///
390 /// The function creates the epoch's directory, writes its metadata,
391 /// updates the internal maps, and finally writes the updated
392 /// configuration to disk.
393 ///
394 /// # Returns
395 /// * `Ok(())` - the epoch was added and the configuration was saved.
396 /// * `Err(EpochError::EpochAlreadyExists(..))` if an epoch with the same ID already exists.
397 pub fn insert_and_save(
398 &mut self,
399 new_epoch: EpochInfo,
400 save_mode: &SaveMode,
401 ) -> Result<(), EpochError> {
402 if self.list_existing_epochs.contains_key(&new_epoch.epoch_id) {
403 return Err(EpochError::EpochAlreadyExists(new_epoch.epoch_id));
404 }
405
406 new_epoch.create_subfolders()?;
407 new_epoch.save_with_context("Saving new epoch", save_mode)?;
408
409 // Add the epoch to the list of epochs, not doing so will still have it added
410 // at the next iteration since the command always parse the files to determine it's state rater that the list
411 // not doing so would likely create errors if insert is used before another command
412 let new_id = new_epoch.epoch_id;
413 self.list_existing_epochs.insert(new_id, Some(new_epoch));
414 self.epoch_id = new_id;
415 self.save_with_context("Saving after insert_and_save", &SaveMode::Erase)?;
416 Ok(())
417 }
418
419 /// Load and display the metadata for a list of epoch IDs.
420 ///
421 /// The function first ensures that each requested epoch's metadata is
422 /// loaded into memory, then displays a formatted log for each one.
423 pub fn log_elements(&mut self, list_ids: &[EpochId]) -> Result<(), EpochError> {
424 let mut items_to_log = Vec::with_capacity(list_ids.len());
425 // Tries to load all ids into memory
426 for id in list_ids {
427 self.try_load(id)?;
428 }
429 // Makes a vector with all elements from the list of ids
430 for id in list_ids {
431 if let Some(info) = self
432 .list_existing_epochs
433 .get(id)
434 .ok_or(EpochError::EpochNotFound(id.to_string()))?
435 {
436 items_to_log.push(info);
437 }
438 }
439
440 // TODO: Have this in a message and return a string
441 println!(
442 "{}",
443 "List of epoch, most recent shown first".bright_green()
444 );
445 // TODO: Make a method for this
446 for elt in items_to_log.iter().rev() {
447 let star = if elt.epoch_id == self.epoch_id {
448 " "
449 } else {
450 " "
451 };
452 println!(
453 "{} {:>4} - {}{}{:10} {:50}",
454 star.green(),
455 elt.epoch_id.to_symbol().blue(),
456 " ".yellow(),
457 (elt.events.len()
458 + elt.characters.len()
459 + elt.towns.len()
460 + elt.calamities.len()
461 + elt.blockers.len())
462 .to_string()
463 .yellow(),
464 elt.tags.join(","),
465 elt.summary
466 )
467 }
468 // println!("{}", EpochInfo::as_table(items_to_log)?);
469 Ok(())
470 }
471}
472
473impl ConfigWritable for EpochConfig {
474 fn save_path(&self) -> String {
475 MAIN_NAME.to_string()
476 }
477
478 fn save_folder(&self) -> String {
479 ".".to_string()
480 }
481}