ck3_history_extractor/display/
renderer.rs

1use std::{
2    collections::VecDeque,
3    fs,
4    ops::Deref,
5    path::{Path, PathBuf},
6    thread,
7};
8
9use derive_more::From;
10use minijinja::{Environment, Value};
11
12use serde::Serialize;
13
14use super::{
15    super::{
16        game_data::GameData,
17        parser::{GameRef, GameState},
18        structures::{
19            Character, Culture, Dynasty, EntityRef, Faith, FromGameObject, GameObjectDerived,
20            GameObjectEntity, House, Player, Title,
21        },
22        types::{GameId, HashMap, Wrapper},
23    },
24    graph::Grapher,
25    timeline::Timeline,
26};
27
28/// A convenience function to create a directory if it doesn't exist, and do nothing if it does.
29/// Also prints an error message if the directory creation fails.
30fn create_dir_maybe<P: AsRef<Path>>(name: P) {
31    if let Err(err) = fs::create_dir_all(name) {
32        if err.kind() != std::io::ErrorKind::AlreadyExists {
33            println!("Failed to create folder: {}", err);
34        }
35    }
36}
37
38#[derive(From)]
39enum RenderableType {
40    Character(GameRef<Character>),
41    Dynasty(GameRef<Dynasty>),
42    House(GameRef<House>),
43    Title(GameRef<Title>),
44    Faith(GameRef<Faith>),
45    Culture(GameRef<Culture>),
46}
47
48impl TryFrom<&EntityRef> for RenderableType {
49    type Error = ();
50
51    fn try_from(value: &EntityRef) -> Result<Self, Self::Error> {
52        match value {
53            EntityRef::Character(c) => Ok(c.clone().into()),
54            EntityRef::Dynasty(d) => Ok(d.clone().into()),
55            EntityRef::House(h) => Ok(h.clone().into()),
56            EntityRef::Title(t) => Ok(t.clone().into()),
57            EntityRef::Faith(f) => Ok(f.clone().into()),
58            EntityRef::Culture(c) => Ok(c.clone().into()),
59            _ => Err(()),
60        }
61    }
62}
63
64impl RenderableType {
65    fn get_id(&self) -> GameId {
66        match self {
67            RenderableType::Character(c) => c.get_internal().get_id(),
68            RenderableType::Dynasty(d) => d.get_internal().get_id(),
69            RenderableType::House(h) => h.get_internal().get_id(),
70            RenderableType::Title(t) => t.get_internal().get_id(),
71            RenderableType::Faith(f) => f.get_internal().get_id(),
72            RenderableType::Culture(c) => c.get_internal().get_id(),
73        }
74    }
75
76    fn get_subdir(&self) -> &'static str {
77        match self {
78            RenderableType::Character(_) => Character::get_subdir(),
79            RenderableType::Dynasty(_) => Dynasty::get_subdir(),
80            RenderableType::House(_) => House::get_subdir(),
81            RenderableType::Title(_) => Title::get_subdir(),
82            RenderableType::Faith(_) => Faith::get_subdir(),
83            RenderableType::Culture(_) => Culture::get_subdir(),
84        }
85    }
86
87    fn is_initialized(&self) -> bool {
88        match self {
89            RenderableType::Character(c) => c.get_internal().inner().is_some(),
90            RenderableType::Dynasty(d) => d.get_internal().inner().is_some(),
91            RenderableType::House(h) => h.get_internal().inner().is_some(),
92            RenderableType::Title(t) => t.get_internal().inner().is_some(),
93            RenderableType::Faith(f) => f.get_internal().inner().is_some(),
94            RenderableType::Culture(c) => c.get_internal().inner().is_some(),
95        }
96    }
97}
98
99#[derive(From)]
100pub enum EntryPoint<'a> {
101    Player(&'a Player),
102    Timeline(&'a Timeline),
103}
104
105/// A struct that renders objects into html pages.
106/// It is meant to be used as a worker object that collects objects and renders them all at once.
107/// The objects are rendered in a BFS order, with the depth of the objects being determined by the BFS algorithm.
108pub struct Renderer<'a> {
109    roots: Vec<EntryPoint<'a>>,
110    depth_map: HashMap<EntityRef, usize>,
111    /// The path where the objects will be rendered to.
112    /// This usually takes the form of './{username}'s history/'.
113    path: &'a Path,
114    /// The loaded game data object.
115    data: &'a GameData,
116    /// The grapher object, if it exists.
117    /// It may be utilized during the rendering process to render a variety of graphs.
118    grapher: Option<&'a Grapher>,
119    /// The game state object.
120    /// It is used to access the game state during rendering, especially for gathering of data for rendering of optional graphs.
121    state: &'a GameState,
122    initial_depth: usize,
123}
124
125impl<'a> Renderer<'a> {
126    /// Create a new Renderer.
127    /// [create_dir_maybe] is called on the path to ensure that the directory exists, and the subdirectories are created.
128    ///
129    /// # Arguments
130    ///
131    /// * `path` - The root path where the objects will be rendered to. Usually takes the form of './{username}'s history/'.
132    /// * `state` - The game state object.
133    /// * `game_map` - The game map object, if it exists.
134    /// * `grapher` - The grapher object, if it exists.
135    /// * `initial_depth` - The initial depth of the objects that are added to the renderer.
136    ///
137    /// # Returns
138    ///
139    /// A new Renderer object.
140    pub fn new(
141        path: &'a Path,
142        state: &'a GameState,
143        data: &'a GameData,
144        grapher: Option<&'a Grapher>,
145        initial_depth: usize,
146    ) -> Self {
147        create_dir_maybe(path);
148        create_dir_maybe(path.join(Character::get_subdir()));
149        create_dir_maybe(path.join(Dynasty::get_subdir()));
150        create_dir_maybe(path.join(Title::get_subdir()));
151        create_dir_maybe(path.join(Faith::get_subdir()));
152        create_dir_maybe(path.join(Culture::get_subdir()));
153        create_dir_maybe(path.join(House::get_subdir()));
154        Renderer {
155            roots: Vec::new(),
156            depth_map: HashMap::default(),
157            path,
158            data,
159            grapher,
160            state,
161            initial_depth,
162        }
163    }
164
165    /// Renders the [Renderable] object.
166    fn render<T: Renderable, D: Deref<Target = T>>(&self, obj: D, env: &Environment<'_>) {
167        //render the object
168        let template = env.get_template(T::get_template()).unwrap();
169        let path = obj.get_path(self.path);
170        obj.render(&self.path, &self.state, self.grapher, self.data);
171        let contents = template.render(obj.deref()).unwrap();
172        thread::spawn(move || {
173            //IO heavy, so spawn a thread
174            fs::write(path, contents).unwrap();
175        });
176    }
177
178    /// Renders the [RenderableType] object.
179    fn render_enum(&self, obj: &RenderableType, env: &Environment<'_>) {
180        if !obj.is_initialized() {
181            return;
182        }
183        match obj {
184            RenderableType::Character(obj) => self.render(obj.get_internal(), env),
185            RenderableType::Dynasty(obj) => self.render(obj.get_internal(), env),
186            RenderableType::House(obj) => self.render(obj.get_internal(), env),
187            RenderableType::Title(obj) => self.render(obj.get_internal(), env),
188            RenderableType::Faith(obj) => self.render(obj.get_internal(), env),
189            RenderableType::Culture(obj) => self.render(obj.get_internal(), env),
190        }
191    }
192
193    /// Adds an object to the renderer, and returns the number of objects that were added.
194    /// This method uses a BFS algorithm to determine the depth of the object.
195    pub fn add_object<G: GameObjectDerived + Renderable>(&mut self, obj: &'a G) -> usize
196    where
197        EntryPoint<'a>: From<&'a G>,
198    {
199        self.roots.push(EntryPoint::from(obj));
200        // BFS with depth https://stackoverflow.com/a/31248992/12520385
201        let mut queue: VecDeque<Option<EntityRef>> = VecDeque::new();
202        obj.get_references(&mut queue);
203        let mut res = queue.len();
204        queue.push_back(None);
205        // algorithm determined depth
206        let mut alg_depth = self.initial_depth;
207        while let Some(obj) = queue.pop_front() {
208            res += 1;
209            if let Some(obj) = obj {
210                if let Some(stored_depth) = self.depth_map.get_mut(&obj) {
211                    if alg_depth > *stored_depth {
212                        *stored_depth = alg_depth;
213                        obj.get_references(&mut queue);
214                    }
215                } else {
216                    obj.get_references(&mut queue);
217                    self.depth_map.insert(obj, alg_depth);
218                }
219            } else {
220                alg_depth -= 1;
221                if alg_depth == 0 {
222                    break;
223                }
224                queue.push_back(None);
225                if queue.front().unwrap().is_none() {
226                    break;
227                }
228            }
229        }
230        return res;
231    }
232
233    /// Renders all the objects that have been added to the renderer.
234    /// This method consumes the renderer object.
235    ///
236    /// # Arguments
237    ///
238    /// * `env` - The [Environment] object that is used to render the templates.
239    ///
240    /// # Returns
241    ///
242    /// The number of objects that were rendered.
243    pub fn render_all(self, env: &mut Environment<'_>) -> usize {
244        let mut global_depth_map = HashMap::default();
245        for (obj, value) in self.depth_map.iter() {
246            if let Ok(obj) = RenderableType::try_from(obj) {
247                global_depth_map
248                    .entry(obj.get_subdir())
249                    .or_insert(HashMap::default())
250                    .insert(obj.get_id(), *value);
251            }
252        }
253        env.add_global("depth_map", Value::from_serialize(global_depth_map));
254        for root in &self.roots {
255            match root {
256                EntryPoint::Player(p) => self.render(*p, env),
257                EntryPoint::Timeline(t) => self.render(*t, env),
258            }
259        }
260        for obj in self.depth_map.keys() {
261            if let Ok(obj) = obj.try_into() {
262                self.render_enum(&obj, env);
263            }
264        }
265        env.remove_global("depth_map");
266        return self.depth_map.len();
267    }
268}
269
270pub trait ProceduralPath {
271    fn get_subdir() -> &'static str;
272}
273
274pub trait GetPath {
275    fn get_path(&self, path: &Path) -> PathBuf;
276}
277
278impl<T: GameObjectDerived + ProceduralPath + FromGameObject> GetPath for GameObjectEntity<T> {
279    fn get_path(&self, path: &Path) -> PathBuf {
280        let mut buf = path.join(T::get_subdir());
281        buf.push(self.get_id().to_string() + ".html");
282        buf
283    }
284}
285
286/// Trait for objects that can be rendered into a html page.
287/// Since this uses [minijinja] the [serde::Serialize] trait is also needed.
288/// Each object that implements this trait should have a corresponding template file in the templates folder.
289pub trait Renderable: Serialize + GetPath {
290    /// Returns the template file name.
291    /// This method is used to retrieve the template from the [Environment] object in the [Renderer] object.
292    fn get_template() -> &'static str;
293
294    /// Renders all the objects that are related to this object.
295    /// For example: graphs, maps, etc.
296    /// This is where your custom rendering logic should go.
297    ///
298    /// # Arguments
299    ///
300    /// * `path` - The root output path of the renderer.
301    /// * `game_state` - The game state object.
302    /// * `grapher` - The grapher object, if it exists.
303    /// * `data` - The game data object.
304    ///
305    /// # Default Implementation
306    ///
307    /// The default implementation does nothing. It is up to the implementing object to override this method.
308    #[allow(unused_variables)]
309    fn render(
310        &self,
311        path: &Path,
312        game_state: &GameState,
313        grapher: Option<&Grapher>,
314        data: &GameData,
315    ) {
316    }
317}