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}