ck3_history_extractor/display/
renderer.rs1use 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
28fn 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
105pub struct Renderer<'a> {
109 roots: Vec<EntryPoint<'a>>,
110 depth_map: HashMap<EntityRef, usize>,
111 path: &'a Path,
114 data: &'a GameData,
116 grapher: Option<&'a Grapher>,
119 state: &'a GameState,
122 initial_depth: usize,
123}
124
125impl<'a> Renderer<'a> {
126 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 fn render<T: Renderable, D: Deref<Target = T>>(&self, obj: D, env: &Environment<'_>) {
167 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 fs::write(path, contents).unwrap();
175 });
176 }
177
178 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 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 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 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 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
286pub trait Renderable: Serialize + GetPath {
290 fn get_template() -> &'static str;
293
294 #[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}