tiger_lib/data/
assets.rs

1use std::path::PathBuf;
2
3use crate::block::{BV, Block};
4use crate::everything::Everything;
5use crate::fileset::{FileEntry, FileHandler};
6use crate::game::Game;
7use crate::helpers::{TigerHashMap, TigerHashSet, dup_error};
8use crate::item::Item;
9use crate::parse::ParserMemory;
10use crate::pdxfile::PdxFile;
11#[cfg(feature = "jomini")]
12use crate::report::{Confidence, Severity};
13use crate::report::{ErrorKey, warn};
14use crate::token::Token;
15use crate::util::SmartJoin;
16#[cfg(feature = "jomini")]
17use crate::validate::validate_numeric_range;
18use crate::validator::Validator;
19
20#[derive(Clone, Debug, Default)]
21#[allow(clippy::struct_field_names)]
22pub struct Assets {
23    assets: TigerHashMap<&'static str, Asset>,
24    attributes: TigerHashSet<Token>,
25    blend_shapes: TigerHashSet<Token>,
26    musics: TigerHashSet<Token>,
27    textures: TigerHashMap<String, (FileEntry, Token)>,
28}
29
30impl Assets {
31    pub fn load_item(&mut self, key: &Token, block: &Block) {
32        if let Some(name) = block.get_field_value("name") {
33            if let Some(other) = self.assets.get(name.as_str()) {
34                if other.key.loc.kind >= name.loc.kind {
35                    dup_error(name, &other.key, "asset");
36                }
37            }
38            self.assets.insert(name.as_str(), Asset::new(key.clone(), name.clone(), block.clone()));
39        }
40    }
41
42    pub fn asset_exists(&self, key: &str) -> bool {
43        self.assets.contains_key(key)
44    }
45
46    pub fn iter_asset_keys(&self) -> impl Iterator<Item = &Token> {
47        self.assets.values().map(|item| &item.name)
48    }
49
50    #[cfg(feature = "jomini")]
51    pub fn mesh_exists(&self, key: &str) -> bool {
52        if let Some(asset) = self.assets.get(key) { asset.key.is("pdxmesh") } else { false }
53    }
54
55    #[cfg(feature = "jomini")]
56    pub fn iter_mesh_keys(&self) -> impl Iterator<Item = &Token> {
57        self.assets.values().filter(|item| item.key.is("pdxmesh")).map(|item| &item.name)
58    }
59
60    pub fn entity_exists(&self, key: &str) -> bool {
61        if let Some(asset) = self.assets.get(key) { asset.key.is("entity") } else { false }
62    }
63
64    pub fn iter_entity_keys(&self) -> impl Iterator<Item = &Token> {
65        self.assets.values().filter(|item| item.key.is("entity")).map(|item| &item.name)
66    }
67
68    pub fn blend_shape_exists(&self, key: &str) -> bool {
69        self.blend_shapes.contains(key)
70    }
71
72    pub fn iter_blend_shape_keys(&self) -> impl Iterator<Item = &Token> {
73        self.blend_shapes.iter()
74    }
75
76    #[cfg(feature = "jomini")]
77    pub fn attribute_exists(&self, key: &str) -> bool {
78        self.attributes.contains(key)
79    }
80
81    #[cfg(feature = "jomini")]
82    pub fn iter_attribute_keys(&self) -> impl Iterator<Item = &Token> {
83        self.attributes.iter()
84    }
85
86    #[cfg(feature = "hoi4")]
87    pub fn music_exists(&self, key: &str) -> bool {
88        self.musics.contains(key)
89    }
90
91    #[cfg(feature = "hoi4")]
92    pub fn iter_music_keys(&self) -> impl Iterator<Item = &Token> {
93        self.musics.iter()
94    }
95
96    pub fn texture_exists(&self, key: &str) -> bool {
97        self.textures.contains_key(key)
98    }
99
100    pub fn iter_texture_keys(&self) -> impl Iterator<Item = &Token> {
101        self.textures.values().map(|(_, token)| token)
102    }
103
104    pub fn get_texture(&self, key: &str) -> Option<&FileEntry> {
105        self.textures.get(key).map(|(entry, _)| entry)
106    }
107
108    pub fn validate(&self, data: &Everything) {
109        for item in self.assets.values() {
110            item.validate(data);
111        }
112    }
113}
114
115impl FileHandler<Option<Block>> for Assets {
116    fn subpath(&self) -> PathBuf {
117        PathBuf::from("gfx/models")
118    }
119
120    /// TODO: should probably simplify this `FileHandler` by keeping the textures in a separate `FileHandler`.
121    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Option<Block>> {
122        let name = entry.filename().to_string_lossy();
123
124        if name.ends_with(".dds") {
125            Some(None)
126        } else if name.ends_with(".asset") {
127            PdxFile::read_optional_bom(entry, parser).map(Some)
128        } else {
129            None
130        }
131    }
132
133    fn handle_file(&mut self, entry: &FileEntry, loaded: Option<Block>) {
134        let name = entry.filename().to_string_lossy();
135        if name.ends_with(".dds") {
136            if let Some((other, _)) = self.textures.get(&*name) {
137                if other.kind() >= entry.kind() {
138                    warn(ErrorKey::DuplicateItem)
139                        .msg("texture file is redefined by another file")
140                        .loc(other)
141                        .loc_msg(entry, "the other file is here")
142                        .push();
143                }
144            }
145            let entry_token = Token::new(&entry.filename().to_string_lossy(), entry.into());
146            self.textures.insert(name.to_string(), (entry.clone(), entry_token));
147            return;
148        }
149
150        let block = loaded.expect("internal error");
151        for (key, block) in block.iter_definitions_warn() {
152            self.load_item(key, block);
153        }
154    }
155
156    fn finalize(&mut self) {
157        for asset in self.assets.values() {
158            if asset.key.is("pdxmesh") {
159                for (key, block) in asset.block.iter_definitions() {
160                    if key.is("blend_shape") {
161                        if let Some(id) = block.get_field_value("id") {
162                            self.blend_shapes.insert(id.clone());
163                        }
164                    }
165                }
166            } else if asset.key.is("entity") {
167                for (key, block) in asset.block.iter_definitions() {
168                    if key.is("attribute") {
169                        if let Some(name) = block.get_field_value("name") {
170                            self.attributes.insert(name.clone());
171                        }
172                    }
173                }
174            } else if asset.key.is("music") {
175                self.musics.insert(asset.name.clone());
176            }
177        }
178    }
179}
180
181#[derive(Clone, Debug)]
182pub struct Asset {
183    key: Token,
184    name: Token,
185    block: Block,
186}
187
188impl Asset {
189    pub fn new(key: Token, name: Token, block: Block) -> Self {
190        Self { key, name, block }
191    }
192
193    pub fn validate_mesh(&self, data: &Everything) {
194        let mut vd = Validator::new(&self.block, data);
195        vd.field_value("name");
196        vd.req_field("file");
197        if let Some(token) = vd.field_value("file") {
198            let path = self.key.loc.pathname().smart_join_parent(token.as_str());
199            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
200        }
201        vd.field_numeric("scale");
202        vd.field_numeric("cull_distance");
203
204        vd.multi_field_validated_block("lod_percentages", |block, data| {
205            let mut vd = Validator::new(block, data);
206            vd.multi_field_validated_block("lod", |block, data| {
207                let mut vd = Validator::new(block, data);
208                vd.req_field("index");
209                vd.req_field("percent");
210                vd.field_integer("index");
211                vd.field_precise_numeric("percent");
212            });
213        });
214
215        vd.multi_field_validated_block("meshsettings", validate_meshsettings);
216        vd.multi_field_validated_block("blend_shape", |block, data| {
217            let mut vd = Validator::new(block, data);
218            vd.req_field("id");
219            vd.req_field("type");
220            vd.field_value("id");
221            if let Some(token) = vd.field_value("type") {
222                let path = self.key.loc.pathname().smart_join_parent(token.as_str());
223                data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
224            }
225            vd.field_value("data"); // TODO
226        });
227
228        vd.multi_field_validated_block("animation", |block, data| {
229            let mut vd = Validator::new(block, data);
230            vd.req_field("id");
231            vd.req_field("type");
232            vd.field_value("id");
233            if let Some(token) = vd.field_value("type") {
234                let path = self.key.loc.pathname().smart_join_parent(token.as_str());
235                data.fileset.verify_exists_implied_crashes(&path.to_string_lossy(), token);
236            }
237        });
238        vd.multi_field_validated_block("additive_animation", |block, data| {
239            let mut vd = Validator::new(block, data);
240            vd.req_field("id");
241            vd.req_field("type");
242            vd.field_value("id");
243            if let Some(token) = vd.field_value("type") {
244                let path = self.key.loc.pathname().smart_join_parent(token.as_str());
245                data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
246            }
247        });
248
249        vd.multi_field_validated_block("import", |block, data| {
250            let mut vd = Validator::new(block, data);
251            vd.field_value("type"); // TODO
252            vd.field_item("name", Item::Asset);
253        });
254    }
255
256    pub fn validate_entity(&self, data: &Everything) {
257        let mut vd = Validator::new(&self.block, data);
258        vd.set_case_sensitive(false);
259
260        vd.field_value("name");
261        vd.field_item("pdxmesh", Item::Pdxmesh);
262        vd.field_item("clone", Item::Entity);
263        vd.field_bool("get_state_from_parent");
264        vd.field_numeric("scale");
265        vd.field_numeric("cull_radius");
266        #[cfg(feature = "jomini")]
267        if Game::is_jomini() {
268            vd.multi_field_validated_block("attribute", |block, data| {
269                let mut vd = Validator::new(block, data);
270                vd.req_field("name");
271                vd.req_field_one_of(&["blend_shape", "additive_animation"]);
272                vd.field_item("name", Item::GeneAttribute);
273                if Game::is_eu5() {
274                    vd.field("additive_animation"); // TODO: eu5 link to defined "additive_animation"
275                } else {
276                    vd.field_item("additive_animation", Item::GeneAttribute);
277                }
278                vd.field_item("blend_shape", Item::BlendShape);
279                vd.field_numeric("default");
280            });
281        }
282        vd.multi_field_validated_block("meshsettings", validate_meshsettings);
283        #[cfg(feature = "jomini")]
284        vd.multi_field_validated_block("game_data", |block, data| {
285            let mut vd = Validator::new(block, data);
286            vd.multi_field_validated_block("portrait_entity_user_data", |block, data| {
287                let mut vd = Validator::new(block, data);
288                vd.multi_field_validated_block("portrait_accessory", |block, data| {
289                    let mut vd = Validator::new(block, data);
290                    vd.field_item("pattern_mask", Item::File);
291                    vd.field_item("variation", Item::AccessoryVariation);
292                });
293                vd.multi_field_validated_block("color_mask_remap_interval", |block, data| {
294                    let mut vd = Validator::new(block, data);
295                    vd.multi_field_validated_block("interval", |block, data| {
296                        validate_numeric_range(
297                            block,
298                            data,
299                            0.0,
300                            1.0,
301                            Severity::Warning,
302                            Confidence::Weak,
303                        );
304                    });
305                });
306                vd.multi_field_validated_block("portrait_decal", |block, data| {
307                    let mut vd = Validator::new(block, data);
308                    vd.field_value("body_part"); // TODO
309                });
310                vd.field_item("coa_mask", Item::File);
311            });
312            vd.multi_field_validated_block("throne_entity_user_data", |block, data| {
313                let mut vd = Validator::new(block, data);
314                vd.field_item("animation", Item::PortraitAnimation);
315                vd.field_bool("use_throne_transform");
316            });
317            vd.multi_field_validated_block("court_entity_user_data", |block, data| {
318                let mut vd = Validator::new(block, data);
319                vd.field_bool("coat_of_arms");
320            });
321        });
322        vd.multi_field_validated_block("state", |block, data| {
323            let mut vd = Validator::new(block, data);
324            vd.req_field("name");
325            vd.field_value("name");
326            vd.field_numeric("state_time");
327            vd.field_bool("looping");
328            vd.field_numeric("animation_speed");
329            vd.field_value("next_state"); // TODO
330            vd.field("chance"); // TODO: can be integer or block
331            vd.field_value("animation"); // TODO
332            vd.field_numeric("animation_blend_time");
333            vd.field_validated("time_offset", validate_time_offset);
334            vd.multi_field_validated_block("start_event", validate_event);
335            vd.multi_field_validated_block("event", validate_event);
336            vd.multi_field_validated("propagate_state", |bv, data| {
337                match bv {
338                    BV::Value(_token) => (), // TODO
339                    BV::Block(block) => {
340                        let mut vd = Validator::new(block, data);
341                        // TODO
342                        vd.unknown_value_fields(|_, _| ());
343                    }
344                }
345            });
346        });
347        vd.field_value("default_state"); // TODO: must be a state name
348        vd.multi_field_validated_block("locator", |block, data| {
349            let mut vd = Validator::new(block, data);
350            vd.req_field("name");
351            vd.field_value("name");
352            vd.multi_field_validated_block("position", |block, data| {
353                let mut vd = Validator::new(block, data);
354                vd.req_tokens_numbers_exactly(3);
355            });
356            vd.multi_field_validated_block("rotation", |block, data| {
357                let mut vd = Validator::new(block, data);
358                vd.req_tokens_numbers_exactly(3);
359            });
360            if Game::is_vic3() || Game::is_eu5() {
361                vd.field("parent_joint"); // TODO: eu5 & vic3
362            }
363            vd.field_numeric("scale");
364        });
365        vd.multi_field_validated_block("attach", |block, data| {
366            let mut vd = Validator::new(block, data);
367            vd.unknown_value_fields(|_, token| {
368                // TODO: what are the keys here?
369                data.verify_exists(Item::Asset, token);
370            });
371        });
372    }
373
374    pub fn validate_animation(&self, data: &Everything) {
375        let mut vd = Validator::new(&self.block, data);
376        vd.field_value("name");
377        if let Some(token) = vd.field_value("file") {
378            let path = self.key.loc.pathname().smart_join_parent(token.as_str());
379            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
380        }
381    }
382
383    pub fn validate_animation_set(&self, data: &Everything) {
384        let mut vd = Validator::new(&self.block, data);
385        vd.field_value("name");
386        vd.req_field("reference_skeleton");
387        vd.multi_field_item("reference_skeleton", Item::Pdxmesh);
388        vd.multi_field_validated_block("animation", |block, data| {
389            let mut vd = Validator::new(block, data);
390            vd.req_field("id");
391            vd.req_field("type");
392            vd.field_value("id");
393            if let Some(token) = vd.field_value("type") {
394                let path = self.key.loc.pathname().smart_join_parent(token.as_str());
395                data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
396            }
397        });
398    }
399
400    pub fn validate_music(&self, data: &Everything) {
401        if !Game::is_hoi4() {
402            let msg = "`music` assets are only used in Hoi4";
403            warn(ErrorKey::WrongGame).msg(msg).loc(&self.key).push();
404        }
405        let mut vd = Validator::new(&self.block, data);
406        vd.field_item("name", Item::Localization);
407        if let Some(token) = vd.field_value("file") {
408            let path = self.key.loc.pathname().smart_join_parent(token.as_str());
409            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
410        }
411        vd.field_numeric("volume");
412    }
413
414    pub fn validate(&self, data: &Everything) {
415        if self.key.is("pdxmesh") {
416            self.validate_mesh(data);
417        } else if self.key.is("entity") {
418            self.validate_entity(data);
419        } else if Game::is_hoi4() && self.key.is("animation") {
420            self.validate_animation(data);
421        } else if self.key.is("skeletal_animation_set") {
422            self.validate_animation_set(data);
423        } else if self.key.is("arrowType") {
424            // TODO: arrowType
425        } else if self.key.is("music") {
426            self.validate_music(data);
427        } else {
428            warn(ErrorKey::UnknownField).msg("unknown asset type").loc(&self.key).push();
429        }
430    }
431}
432
433fn validate_event(block: &Block, data: &Everything) {
434    let mut vd = Validator::new(block, data);
435    vd.field_numeric("time");
436    vd.field_numeric("life");
437    vd.field_numeric("entity_fade_speed");
438    if Game::is_eu5() {
439        vd.field_numeric("entity_editor_id");
440    }
441    vd.field_value("state"); // TODO
442    vd.field_value("node"); // TODO
443    vd.field_value("particle"); // TODO
444    vd.field_bool("keep_particle");
445    vd.field_bool("keep_sound");
446    vd.field_bool("keep_entity");
447    vd.field_bool("trigger_once");
448    vd.field_bool("use_parent_nodes");
449    vd.field_integer("skip_forward");
450    vd.field_value("attachment_id"); // TODO
451    vd.field_value("remove_attachment"); // TODO
452    vd.field_item("entity", Item::Entity);
453    vd.multi_field_validated_block("soundparameter", |block, data| {
454        let mut vd = Validator::new(block, data);
455        vd.unknown_value_fields(|_, token| {
456            // TODO: what are the keys here?
457            token.expect_number();
458        });
459    });
460    vd.multi_field_validated_block("sound", |block, data| {
461        let mut vd = Validator::new(block, data);
462        if let Some(token) = vd.field_value("soundeffect") {
463            if !token.is("") {
464                if Game::is_hoi4() {
465                    #[cfg(feature = "hoi4")]
466                    data.verify_exists(Item::SoundEffect, token);
467                } else if Game::is_eu5() {
468                    // TODO: EU5 sound system is wwise and not documented
469                } else {
470                    data.verify_exists(Item::Sound, token);
471                }
472            }
473        }
474        vd.field_bool("stop_on_state_change");
475    });
476    vd.field_value("light"); // TODO
477}
478
479fn validate_meshsettings(block: &Block, data: &Everything) {
480    let mut vd = Validator::new(block, data);
481    vd.field_value("name");
482    vd.field_integer("index"); // TODO: do these need to be consecutive?
483    vd.field_bool("shadow_only");
484    vd.field_item_or_empty("texture_diffuse", Item::TextureFile);
485    vd.field_item_or_empty("texture_normal", Item::TextureFile);
486    vd.field_item_or_empty("texture_specular", Item::TextureFile);
487    // TODO: verify if indexes have to be unique
488    vd.multi_field_validated_block("texture", |block, data| {
489        let mut vd = Validator::new(block, data);
490        vd.req_field("file");
491        vd.req_field("index");
492        vd.field_item("file", Item::TextureFile);
493        vd.field_integer("index");
494        vd.field_bool("srgb");
495    });
496    vd.field_value("shader"); // TODO
497    vd.field_item("shader_file", Item::File);
498    vd.field_value("subpass");
499    vd.field_value("shadow_shader");
500    vd.field_value("rasterizerstate"); // TODO, choices?
501    if Game::is_vic3() || Game::is_ck3() || Game::is_eu5() {
502        vd.field_list("additional_shader_defines");
503    }
504}
505
506fn validate_time_offset(bv: &BV, data: &Everything) {
507    match bv {
508        BV::Value(token) => {
509            _ = token.expect_number();
510        }
511        BV::Block(block) => {
512            let mut vd = Validator::new(block, data);
513            vd.req_tokens_numbers_exactly(2);
514        }
515    }
516}