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 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"); });
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 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"); vd.field("chance"); vd.field_value("animation"); 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) => (), BV::Block(block) => {
347 let mut vd = Validator::new(block, data);
348 vd.unknown_value_fields(|_, _| ());
350 }
351 }
352 });
353 });
354 vd.field_value("default_state"); 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"); }
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 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 } 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"); vd.field_value("node"); vd.field_value("particle"); 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"); vd.field_value("remove_attachment"); 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 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 } else {
478 data.verify_exists(Item::Sound, token);
479 }
480 }
481 vd.field_bool("stop_on_state_change");
482 });
483 vd.field_value("light"); }
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"); 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 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"); vd.field_item("shader_file", Item::File);
505 vd.field_value("subpass");
506 vd.field_value("shadow_shader");
507 vd.field_value("rasterizerstate"); 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}