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 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"); });
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"); 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"); } 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"); });
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"); vd.field("chance"); vd.field_value("animation"); 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) => (), BV::Block(block) => {
340 let mut vd = Validator::new(block, data);
341 vd.unknown_value_fields(|_, _| ());
343 }
344 }
345 });
346 });
347 vd.field_value("default_state"); 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"); }
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 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 } 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"); vd.field_value("node"); vd.field_value("particle"); 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"); vd.field_value("remove_attachment"); 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 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 } 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"); }
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"); 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 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"); vd.field_item("shader_file", Item::File);
498 vd.field_value("subpass");
499 vd.field_value("shadow_shader");
500 vd.field_value("rasterizerstate"); 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}