lib_efo/epoch/
epoch_info.rs

1//! Metadata management for individual epochs.
2//!
3//! The `EpochInfo` struct holds all information that the engine needs to
4//! reconstruct a particular epoch.
5//! It is persisted to `epoch/<epoch_id>/.epochinfo`. The file contains the epoch ID, the RNG
6//! seed that produced the world, any optional tags, and a human-readable
7//! summary.  World data itself is stored separately (e.g. in `world.json`).
8
9use cli_table::Table;
10use derive_more::Display;
11use serde::{Deserialize, Serialize};
12use std::fs::create_dir_all;
13use uuid::Uuid;
14
15use crate::{
16    entity::{Entity, EntityIdentifier},
17    epoch::{
18        EpochId, SUB_NAME, SUBFOLDER_NAMING, TagList,
19        epoch_traits::{ConfigWritable, Prettiable},
20    },
21    error::EpochError,
22    stats::{EntityType, Position, PositionIdentifier, Symbols, TypedLink},
23};
24
25/// The metadata file stored inside each epoch directory
26/// (`epoch/<epoch_id>/.epochinfo`).
27///
28/// It contains only the information that is needed for the engine to
29/// reconstruct the state *of that particular epoch* (seed, selected
30/// events, parent link, etc.).  The full world data will be kept in
31/// separate storage (e.g. `world.json`) but is for stored here.
32#[derive(Debug, Clone, Serialize, Deserialize, Display, Table)]
33#[display(
34    "Epoch ({epoch_id:>5}) | seed ({rng_seed:<10}) | tags ({tags:<10}) | summary ({summary:<20})"
35)]
36pub struct EpochInfo {
37    /// The ID of this epoch - should match the directory name.
38    #[table(title = "ID")]
39    pub epoch_id: EpochId,
40
41    /// RNG seed that was used to generate randomised data for
42    /// this epoch.  Keeping it in the file guarantees repeatable
43    /// behaviour when replaying the same epoch.
44    #[table(title = "Seed")]
45    pub rng_seed: u64,
46
47    // /// List of event IDs that were accepted for this epoch.
48    // /// The engine uses this to replay the world from the
49    // /// parent epoch to the current one.
50    // pub selected_events: Vec<u64>,
51    /// Optional tags that help identify the epoch (e.g. `"Winter-B"`).
52    #[table(title = "Tags")]
53    pub tags: TagList,
54
55    /// Human-readable description or summary, useful for the `log` command.
56    #[table(title = "Summary")]
57    pub summary: String,
58    // TODO(9): Store world separately
59    #[table(skip)]
60    /// The list of events currently in progress
61    pub events: Vec<Entity>,
62    #[table(skip)]
63    /// The list of characters currently present
64    pub characters: Vec<Entity>,
65    #[table(skip)]
66    /// The list of towns currently present
67    pub towns: Vec<Entity>,
68    #[table(skip)]
69    /// The list of calamities currently present
70    pub calamities: Vec<Entity>,
71    #[table(skip)]
72    /// The list of blockers currently present
73    pub blockers: Vec<Entity>,
74}
75
76impl Prettiable for EpochInfo {}
77
78impl Default for EpochInfo {
79    fn default() -> Self {
80        Self {
81            epoch_id: EpochId::new(0),
82            rng_seed: 0,
83            tags: TagList::new(),
84            summary: String::new(),
85            events: Vec::new(),
86            characters: Vec::new(),
87            towns: Vec::new(),
88            calamities: Vec::new(),
89            blockers: Vec::new(),
90        }
91    }
92}
93
94impl EpochInfo {
95    /// Create all subfolders as needed
96    ///
97    /// This command can be safely used even if the folder already exists
98    pub fn create_subfolders(&self) -> Result<(), EpochError> {
99        io_err_with_help!(create_dir_all(self.save_folder()), "Creating subfolders")
100    }
101
102    /// Create an epoch with only an Id, please note that this function doesn't load the epoch at all
103    pub(crate) fn with_id(epoch_id: &EpochId) -> Self {
104        Self {
105            epoch_id: *epoch_id,
106            ..Default::default()
107        }
108    }
109}
110
111impl ConfigWritable for EpochInfo {
112    fn save_path(&self) -> String {
113        format!("{}/{}/{}", SUBFOLDER_NAMING, self.epoch_id, SUB_NAME)
114    }
115
116    fn save_folder(&self) -> String {
117        format!("{}/{}/", SUBFOLDER_NAMING, self.epoch_id)
118    }
119}
120
121impl EpochInfo {
122    /// Looks up an entity inside the given `epoch_info` according to the
123    /// supplied arguments.
124    ///
125    /// The function returns a reference to the matching entity or an
126    /// `EpochError::EntityNotFound` if no entity matches.
127    ///
128    /// # Arguments
129    /// * `entity_args` - The arguments that identify the entity.
130    /// * `e_type` - The type of entity to look for.
131    /// * `epoch_info` - The epoch that contains all entities.
132    ///
133    /// # Returns
134    /// * `Ok(&Entity)` - The entity that matches the arguments.
135    /// * `Err(EpochError::EntityNotFound)` - No matching entity was found.
136    pub fn find_entity_of_type(
137        &self,
138        entity_to_find: &Option<EntityIdentifier>,
139        e_type: &EntityType,
140    ) -> Result<&Entity, EpochError> {
141        self.vec_entities(e_type)
142            .iter()
143            .find(|&entity| entity.match_identifier(entity_to_find))
144            .ok_or(EpochError::EntityNotFound(*e_type, entity_to_find.clone()))
145    }
146
147    /// Adds a link between two entities inside the epoch.
148    ///
149    /// The link is added to the entity identified by `id_to_add_to`.  If the
150    /// target entity is not found, an `EntityNotFoundById` error is returned.
151    pub fn add_to_links(
152        &mut self,
153        target_id: Uuid,
154        target_t: &EntityType,
155        link_back: TypedLink,
156    ) -> Result<(), EpochError> {
157        log::info!("Adding {} to {}[{:?}]", link_back, target_id, target_t);
158        let e = self
159            .vec_entities_mut(target_t)
160            .iter_mut()
161            .find(|entity| entity.id == target_id)
162            .ok_or(EpochError::EntityNotFoundById(
163                EntityIdentifier::Id(target_id.to_string()),
164                Some(*target_t),
165            ))?;
166        e.add_to_links(link_back);
167        Ok(())
168    }
169
170    /// Removes a link from an entity inside the epoch.
171    ///
172    /// The removal is performed on the *target* entity (`id_to_add_to`).  If the
173    /// target entity cannot be found, an `EpochError::EntityNotFoundById` error
174    /// is returned.  The removal itself is delegated to the entity's
175    /// `del_from_links` method, which silently ignores missing links.
176    pub fn del_from_links(
177        &mut self,
178        target_id: Uuid,
179        target_t: &EntityType,
180        link_back: TypedLink,
181    ) -> Result<(), EpochError> {
182        let e = self
183            .vec_entities_mut(target_t)
184            .iter_mut()
185            .find(|entity| entity.id == target_id)
186            .ok_or(EpochError::EntityNotFoundById(
187                EntityIdentifier::Id(target_id.to_string()),
188                Some(*target_t),
189            ))?;
190
191        e.del_from_links(link_back);
192        Ok(())
193    }
194
195    /// Find an entity by its UUID.
196    ///
197    /// This function searches the currently loaded epoch for an entity whose
198    /// `id` matches the supplied `Uuid`. It looks in both the `events`, `towns`
199    /// and `characters` collections of the epoch.
200    ///
201    /// # Arguments
202    /// * `id` - The `Uuid` of the entity to locate.
203    ///
204    /// # Returns
205    /// * `Ok(&Entity)` - Reference to the entity if it is present in the
206    ///   current epoch.
207    /// * `Err(EpochError::EntityNotFoundById(id))` - No entity with the
208    ///   supplied UUID exists in the epoch.
209    pub fn find_any_entity(&self, id: EntityIdentifier) -> Result<&Entity, EpochError> {
210        let predicate = |entity: &&Entity| match &id {
211            EntityIdentifier::Name(name) => {
212                entity.name.to_lowercase().starts_with(&name.to_lowercase())
213            }
214            EntityIdentifier::Id(id) => entity.id.to_string().starts_with(id),
215        };
216        self.events
217            .iter()
218            .find(predicate)
219            .or(self.characters.iter().find(predicate))
220            .or(self.towns.iter().find(predicate))
221            .or(self.blockers.iter().find(predicate))
222            .or(self.calamities.iter().find(predicate))
223            .ok_or(EpochError::EntityNotFoundById(id, None))
224    }
225    /// Find an entity from an Uuid
226    pub fn find_entity_strict(
227        &self,
228        id: Uuid,
229        strict: Option<EntityType>,
230    ) -> Result<&Entity, EpochError> {
231        let predicate = |entity: &&Entity| entity.id == id;
232        if let Some(e_type) = strict {
233            self.vec_entities(&e_type)
234                .iter()
235                .find(|entity| entity.id == id)
236                .ok_or(EpochError::EntityNotFound(
237                    e_type,
238                    Some(EntityIdentifier::Id(id.to_string())),
239                ))
240        } else {
241            self.events
242                .iter()
243                .find(predicate)
244                .or(self.characters.iter().find(predicate))
245                .or(self.towns.iter().find(predicate))
246                .or(self.blockers.iter().find(predicate))
247                .or(self.calamities.iter().find(predicate))
248                .ok_or(EpochError::EntityNotFound(
249                    EntityType::Player,
250                    Some(EntityIdentifier::Id(id.to_string())),
251                ))
252        }
253    }
254    /// Find an entity from an Uuid
255    // TODO: Siplify this with the better iterator
256    pub fn find_entity_strict_as_mut(
257        &mut self,
258        id: Uuid,
259        strict: Option<EntityType>,
260    ) -> Result<&mut Entity, EpochError> {
261        let predicate = |entity: &&mut Entity| entity.id == id;
262        if let Some(e_type) = strict {
263            self.vec_entities_mut(&e_type)
264                .iter_mut()
265                .find(|entity| entity.id == id)
266                .ok_or(EpochError::EntityNotFound(
267                    e_type,
268                    Some(EntityIdentifier::Id(id.to_string())),
269                ))
270        } else {
271            self.events
272                .iter_mut()
273                .find(predicate)
274                .or(self.characters.iter_mut().find(predicate))
275                .or(self.towns.iter_mut().find(predicate))
276                .or(self.blockers.iter_mut().find(predicate))
277                .or(self.calamities.iter_mut().find(predicate))
278                .ok_or(EpochError::EntityNotFound(
279                    EntityType::Player,
280                    Some(EntityIdentifier::Id(id.to_string())),
281                ))
282        }
283    }
284    /// Returns a slice over the entities of the requested type from the given `EpochInfo`.
285    ///
286    /// The function matches the supplied `e_type` to the appropriate collection
287    /// inside `epoch_info`. For unimplemented entity types the call will panic
288    /// because of the `todo!()` placeholder.
289    pub fn vec_entities(&self, e_type: &EntityType) -> &Vec<Entity> {
290        match e_type {
291            EntityType::Character => &self.characters,
292            EntityType::Player => unimplemented!("No players for now"),
293            EntityType::Town => &self.towns,
294            EntityType::Monster => &self.calamities,
295            EntityType::Blocker => &self.blockers,
296            EntityType::Event => &self.events,
297        }
298    }
299
300    /// Removes an entity from the epoch's collection and returns it.
301    ///
302    /// The function looks up the entity that matches the supplied `entity_identifier`
303    /// in the collection corresponding to `e_type`.  If the entity is found, it
304    /// is removed using `swap_remove` and returned.  When the entity cannot be
305    /// located, an `EpochError::EntityNotFound` is returned.
306    pub fn remove_entity(
307        &mut self,
308        entity_identifier: &Option<EntityIdentifier>,
309        e_type: &EntityType,
310    ) -> Result<Entity, EpochError> {
311        let vec_entities: &mut Vec<Entity> = self.vec_entities_mut(e_type);
312        let index = vec_entities
313            .iter()
314            .position(|entity| entity.match_identifier(entity_identifier))
315            .ok_or(EpochError::EntityNotFound(
316                *e_type,
317                entity_identifier.clone(),
318            ))?;
319
320        let entity = vec_entities.swap_remove(index);
321        Ok(entity)
322    }
323    /// Returns a mutable slice over the entities of the requested type from the given
324    /// `EpochInfo`.
325    ///
326    /// Like `vec_entities`, this function selects the appropriate collection
327    /// based on `e_type`. Unimplemented variants will panic due to the `todo!()`.
328    pub fn vec_entities_mut(&mut self, e_type: &EntityType) -> &mut Vec<Entity> {
329        match e_type {
330            EntityType::Character => &mut self.characters,
331            EntityType::Player => unimplemented!("No players for now"),
332            EntityType::Town => &mut self.towns,
333            EntityType::Monster => &mut self.calamities,
334            EntityType::Blocker => &mut self.blockers,
335            EntityType::Event => &mut self.events,
336        }
337    }
338
339    /// Returns an iterator over mutable references to **all** entities that are currently
340    /// stored in the `EpochInfo` container.
341    pub fn iter_all_entities_mut(&mut self) -> impl Iterator<Item = &mut Entity> {
342        self.events
343            .iter_mut()
344            .chain(self.characters.iter_mut())
345            .chain(self.towns.iter_mut())
346            .chain(self.calamities.iter_mut())
347            .chain(self.blockers.iter_mut())
348    }
349
350    /// Get the position of an entity either directly or cloning one from an epoch
351    pub fn resolve_position(&self, position: &PositionIdentifier) -> Result<Position, EpochError> {
352        Ok(match position {
353            PositionIdentifier::Exact(pos) => pos.clone(),
354            PositionIdentifier::Grid(pos) => pos.to_position(),
355            PositionIdentifier::Id(ident) => {
356                self.find_any_entity(ident.clone())?.position().clone()
357            }
358        })
359    }
360
361    /// Swap an entity with an updated one
362    pub fn swap_entity(&mut self, entity: Entity) -> Result<Entity, EpochError> {
363        // Get a mutable reference to the vector that holds the entities of `e_type`
364        let entities = self.vec_entities_mut(&entity.entity_type);
365
366        // Locate the index of the entity that we want to replace
367        let idx_opt = entities.iter().position(|e| e.id == entity.id);
368
369        match idx_opt {
370            Some(idx) => {
371                // Replace the old entity with the new one, returning the old entity
372                Ok(std::mem::replace(&mut entities[idx], entity))
373            }
374            None => Err(EpochError::EntityNotFound(
375                entity.entity_type,
376                Some(EntityIdentifier::Id(entity.id.to_string())),
377            )),
378        }
379    }
380}
381
382impl Symbols for EpochId {
383    fn to_symbol(&self) -> String {
384        format!("[󰹻 {:<2}]", self)
385    }
386}