Skip to main content

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                && other.key.loc.kind >= name.loc.kind
35            {
36                dup_error(name, &other.key, "asset");
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                && other.kind() >= entry.kind()
138            {
139                warn(ErrorKey::DuplicateItem)
140                    .msg("texture file is redefined by another file")
141                    .loc(other)
142                    .loc_msg(entry, "the other file is here")
143                    .push();
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                        && let Some(id) = block.get_field_value("id")
162                    {
163                        self.blend_shapes.insert(id.clone());
164                    }
165                }
166            } else if asset.key.is("entity") {
167                for (key, block) in asset.block.iter_definitions() {
168                    if key.is("attribute")
169                        && let Some(name) = block.get_field_value("name")
170                    {
171                        self.attributes.insert(name.clone());
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                vd.field_validated_block("coa_pattern", |block, data| {
312                    let mut vd = Validator::new(block, data);
313                    vd.field_precise_numeric("coa_pattern_tile");
314                    vd.field_precise_numeric("coa_pattern_scale");
315                    vd.field_list_numeric_exactly("coa_staggered_offset", 2);
316                    vd.field_choice("coa_shape_mask_channel", &["red", "green", "blue", "alpha"]);
317                });
318            });
319            vd.multi_field_validated_block("throne_entity_user_data", |block, data| {
320                let mut vd = Validator::new(block, data);
321                vd.field_item("animation", Item::PortraitAnimation);
322                vd.field_bool("use_throne_transform");
323            });
324            vd.multi_field_validated_block("court_entity_user_data", |block, data| {
325                let mut vd = Validator::new(block, data);
326                vd.field_bool("coat_of_arms");
327            });
328        });
329        vd.multi_field_validated_block("state", |block, data| {
330            let mut vd = Validator::new(block, data);
331            vd.req_field("name");
332            vd.field_value("name");
333            vd.field_numeric("state_time");
334            vd.field_bool("looping");
335            vd.field_numeric("animation_speed");
336            vd.field_value("next_state"); // TODO
337            vd.field("chance"); // TODO: can be integer or block
338            vd.field_value("animation"); // TODO
339            vd.field_numeric("animation_blend_time");
340            vd.field_validated("time_offset", validate_time_offset);
341            vd.multi_field_validated_block("start_event", validate_event);
342            vd.multi_field_validated_block("event", validate_event);
343            vd.multi_field_validated("propagate_state", |bv, data| {
344                match bv {
345                    BV::Value(_token) => (), // TODO
346                    BV::Block(block) => {
347                        let mut vd = Validator::new(block, data);
348                        // TODO
349                        vd.unknown_value_fields(|_, _| ());
350                    }
351                }
352            });
353        });
354        vd.field_value("default_state"); // TODO: must be a state name
355        vd.multi_field_validated_block("locator", |block, data| {
356            let mut vd = Validator::new(block, data);
357            vd.req_field("name");
358            vd.field_value("name");
359            vd.multi_field_validated_block("position", |block, data| {
360                let mut vd = Validator::new(block, data);
361                vd.req_tokens_numbers_exactly(3);
362            });
363            vd.multi_field_validated_block("rotation", |block, data| {
364                let mut vd = Validator::new(block, data);
365                vd.req_tokens_numbers_exactly(3);
366            });
367            if Game::is_vic3() || Game::is_eu5() {
368                vd.field("parent_joint"); // TODO: eu5 & vic3
369            }
370            vd.field_numeric("scale");
371        });
372        vd.multi_field_validated_block("attach", |block, data| {
373            let mut vd = Validator::new(block, data);
374            vd.unknown_value_fields(|_, token| {
375                // TODO: what are the keys here?
376                data.verify_exists(Item::Asset, token);
377            });
378        });
379    }
380
381    pub fn validate_animation(&self, data: &Everything) {
382        let mut vd = Validator::new(&self.block, data);
383        vd.field_value("name");
384        if let Some(token) = vd.field_value("file") {
385            let path = self.key.loc.pathname().smart_join_parent(token.as_str());
386            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
387        }
388    }
389
390    pub fn validate_animation_set(&self, data: &Everything) {
391        let mut vd = Validator::new(&self.block, data);
392        vd.field_value("name");
393        vd.req_field("reference_skeleton");
394        vd.multi_field_item("reference_skeleton", Item::Pdxmesh);
395        vd.multi_field_validated_block("animation", |block, data| {
396            let mut vd = Validator::new(block, data);
397            vd.req_field("id");
398            vd.req_field("type");
399            vd.field_value("id");
400            if let Some(token) = vd.field_value("type") {
401                let path = self.key.loc.pathname().smart_join_parent(token.as_str());
402                data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
403            }
404        });
405    }
406
407    pub fn validate_music(&self, data: &Everything) {
408        if !Game::is_hoi4() {
409            let msg = "`music` assets are only used in Hoi4";
410            warn(ErrorKey::WrongGame).msg(msg).loc(&self.key).push();
411        }
412        let mut vd = Validator::new(&self.block, data);
413        vd.field_item("name", Item::Localization);
414        if let Some(token) = vd.field_value("file") {
415            let path = self.key.loc.pathname().smart_join_parent(token.as_str());
416            data.verify_exists_implied(Item::File, &path.to_string_lossy(), token);
417        }
418        vd.field_numeric("volume");
419    }
420
421    pub fn validate(&self, data: &Everything) {
422        if self.key.is("pdxmesh") {
423            self.validate_mesh(data);
424        } else if self.key.is("entity") {
425            self.validate_entity(data);
426        } else if Game::is_hoi4() && self.key.is("animation") {
427            self.validate_animation(data);
428        } else if self.key.is("skeletal_animation_set") {
429            self.validate_animation_set(data);
430        } else if self.key.is("arrowType") {
431            // TODO: arrowType
432        } else if self.key.is("music") {
433            self.validate_music(data);
434        } else {
435            warn(ErrorKey::UnknownField).msg("unknown asset type").loc(&self.key).push();
436        }
437    }
438}
439
440fn validate_event(block: &Block, data: &Everything) {
441    let mut vd = Validator::new(block, data);
442    vd.field_numeric("time");
443    vd.field_numeric("life");
444    vd.field_numeric("entity_fade_speed");
445    if Game::is_eu5() {
446        vd.field_numeric("entity_editor_id");
447    }
448    vd.field_value("state"); // TODO
449    vd.field_value("node"); // TODO
450    vd.field_value("particle"); // TODO
451    vd.field_bool("keep_particle");
452    vd.field_bool("keep_sound");
453    vd.field_bool("keep_entity");
454    vd.field_bool("trigger_once");
455    vd.field_bool("use_parent_nodes");
456    vd.field_integer("skip_forward");
457    vd.field_value("attachment_id"); // TODO
458    vd.field_value("remove_attachment"); // TODO
459    vd.field_item("entity", Item::Entity);
460    vd.multi_field_validated_block("soundparameter", |block, data| {
461        let mut vd = Validator::new(block, data);
462        vd.unknown_value_fields(|_, token| {
463            // TODO: what are the keys here?
464            token.expect_number();
465        });
466    });
467    vd.multi_field_validated_block("sound", |block, data| {
468        let mut vd = Validator::new(block, data);
469        if let Some(token) = vd.field_value("soundeffect")
470            && !token.is("")
471        {
472            if Game::is_hoi4() {
473                #[cfg(feature = "hoi4")]
474                data.verify_exists(Item::SoundEffect, token);
475            } else if Game::is_eu5() {
476                // TODO: EU5 sound system is wwise and not documented
477            } else {
478                data.verify_exists(Item::Sound, token);
479            }
480        }
481        vd.field_bool("stop_on_state_change");
482    });
483    vd.field_value("light"); // TODO
484}
485
486fn validate_meshsettings(block: &Block, data: &Everything) {
487    let mut vd = Validator::new(block, data);
488    vd.field_value("name");
489    vd.field_integer("index"); // TODO: do these need to be consecutive?
490    vd.field_bool("shadow_only");
491    vd.field_item_or_empty("texture_diffuse", Item::TextureFile);
492    vd.field_item_or_empty("texture_normal", Item::TextureFile);
493    vd.field_item_or_empty("texture_specular", Item::TextureFile);
494    // TODO: verify if indexes have to be unique
495    vd.multi_field_validated_block("texture", |block, data| {
496        let mut vd = Validator::new(block, data);
497        vd.req_field("file");
498        vd.req_field("index");
499        vd.field_item("file", Item::TextureFile);
500        vd.field_integer("index");
501        vd.field_bool("srgb");
502    });
503    vd.field_value("shader"); // TODO
504    vd.field_item("shader_file", Item::File);
505    vd.field_value("subpass");
506    vd.field_value("shadow_shader");
507    vd.field_value("rasterizerstate"); // TODO, choices?
508    if Game::is_vic3() || Game::is_ck3() || Game::is_eu5() {
509        vd.field_list("additional_shader_defines");
510    }
511}
512
513fn validate_time_offset(bv: &BV, data: &Everything) {
514    match bv {
515        BV::Value(token) => {
516            _ = token.expect_number();
517        }
518        BV::Block(block) => {
519            let mut vd = Validator::new(block, data);
520            vd.req_tokens_numbers_exactly(2);
521        }
522    }
523}