1use std::path::PathBuf;
2
3use crate::block::{BV, Block};
4use crate::context::ScopeContext;
5use crate::db::{Db, DbKind};
6use crate::everything::Everything;
7use crate::fileset::{FileEntry, FileHandler};
8use crate::game::{Game, GameFlags};
9use crate::helpers::{TigerHashMap, dup_error, exact_dup_advice};
10use crate::item::{Item, ItemLoader, LoadAsFile, Recursive};
11use crate::parse::ParserMemory;
12use crate::pdxfile::{PdxEncoding, PdxFile};
13use crate::report::{ErrorKey, Severity, untidy, warn};
14use crate::scopes::Scopes;
15use crate::token::Token;
16use crate::tooltipped::Tooltipped;
17use crate::trigger::validate_trigger_max_sev;
18use crate::validate::{validate_color, validate_possibly_named_color};
19use crate::validator::Validator;
20use crate::variables::Variables;
21
22#[derive(Clone, Debug, Default)]
23pub struct Coas {
24 coas: TigerHashMap<&'static str, Coa>,
25 templates: TigerHashMap<&'static str, Coa>,
26}
27
28impl Coas {
29 pub fn load_item(&mut self, key: &Token, bv: &BV) {
30 if key.is("template") {
31 if let Some(block) = bv.expect_block() {
32 for (key, block) in block.iter_definitions_warn() {
33 if let Some(other) = self.templates.get(key.as_str()) {
34 if other.key.loc.kind >= key.loc.kind {
35 if let BV::Block(otherblock) = &other.bv {
36 if otherblock.equivalent(block) {
37 exact_dup_advice(key, &other.key, "coa template");
38 } else {
39 dup_error(key, &other.key, "coa template");
40 }
41 }
42 }
43 }
44 self.templates.insert(
45 key.as_str(),
46 Coa::new(key.clone(), BV::Block(block.clone().condense_tag("list"))),
47 );
48 }
49 }
50 } else {
51 if let Some(other) = self.coas.get(key.as_str()) {
52 if other.key.loc.kind >= key.loc.kind {
53 if other.bv.equivalent(bv) {
54 exact_dup_advice(key, &other.key, "coat of arms");
55 } else {
56 dup_error(key, &other.key, "coat of arms");
57 }
58 }
59 }
60 self.coas.insert(key.as_str(), Coa::new(key.clone(), bv.clone()));
61 }
62 }
63
64 pub fn scan_variables(&self, registry: &mut Variables) {
65 for item in self.coas.values() {
66 if let Some(block) = &item.bv.get_block() {
67 registry.scan(block);
68 }
69 }
70 for item in self.templates.values() {
71 if let Some(block) = &item.bv.get_block() {
72 registry.scan(block);
73 }
74 }
75 }
76
77 pub fn exists(&self, key: &str) -> bool {
78 self.coas.contains_key(key)
79 }
80
81 pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
82 self.coas.values().map(|item| &item.key)
83 }
84
85 pub fn template_exists(&self, key: &str) -> bool {
86 self.templates.contains_key(key)
87 }
88
89 pub fn iter_template_keys(&self) -> impl Iterator<Item = &Token> {
90 self.templates.values().map(|item| &item.key)
91 }
92
93 pub fn validate(&self, data: &Everything) {
94 for item in self.coas.values() {
95 item.validate(data);
96 }
97 for item in self.templates.values() {
98 item.validate(data);
99 }
100 }
101}
102
103impl FileHandler<Block> for Coas {
104 fn subpath(&self) -> PathBuf {
105 PathBuf::from("common/coat_of_arms/coat_of_arms/")
106 }
107
108 fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
109 if !entry.filename().to_string_lossy().ends_with(".txt") {
110 return None;
111 }
112
113 PdxFile::read_optional_bom(entry, parser)
114 }
115
116 fn handle_file(&mut self, _entry: &FileEntry, block: Block) {
117 for (key, bv) in block.iter_assignments_and_definitions_warn() {
118 self.load_item(key, bv);
119 }
120 }
121}
122
123#[derive(Clone, Debug)]
124pub struct Coa {
125 key: Token,
126 bv: BV,
127}
128
129impl Coa {
130 pub fn new(key: Token, bv: BV) -> Self {
131 Self { key, bv }
132 }
133
134 pub fn validate(&self, data: &Everything) {
135 match &self.bv {
136 BV::Value(token) => data.verify_exists(Item::Coa, token),
137 BV::Block(block) => validate_coa_layout(block, data),
138 }
139 }
140}
141
142pub fn validate_coa_layout(block: &Block, data: &Everything) {
143 let mut vd = Validator::new(block, data);
144 vd.set_max_severity(Severity::Warning);
145
146 if let Some(token) = vd.field_value("pattern") {
147 if let Some((_, token)) = token.split_once('"') {
148 data.verify_exists(Item::CoaPatternList, &token);
149 } else {
150 let pathname = format!("gfx/coat_of_arms/patterns/{token}");
151 data.verify_exists_implied(Item::File, &pathname, token);
152 }
153 }
154
155 vd.field_validated("color1", |bv, data| {
156 validate_coa_color(bv, None, data);
157 });
158 vd.field_validated("color2", |bv, data| {
159 validate_coa_color(bv, None, data);
160 });
161 vd.field_validated("color3", |bv, data| {
162 validate_coa_color(bv, None, data);
163 });
164 vd.field_validated("color4", |bv, data| {
165 validate_coa_color(bv, None, data);
166 });
167 vd.field_validated("color5", |bv, data| {
168 validate_coa_color(bv, None, data);
169 });
170
171 vd.multi_field_validated_block("colored_emblem", |subblock, data| {
172 let mut vd = Validator::new(subblock, data);
173 vd.set_max_severity(Severity::Warning);
174 vd.req_field("texture");
175 if let Some(token) = vd.field_value("texture") {
176 if let Some((_, token)) = token.split_once('"') {
177 data.verify_exists(Item::CoaColoredEmblemList, &token);
178 } else {
179 let pathname = format!("gfx/coat_of_arms/colored_emblems/{token}");
180 data.verify_exists_implied(Item::File, &pathname, token);
181 }
182 }
183
184 for field in &["color1", "color2", "color3", "color4", "color5"] {
185 vd.field_validated(field, |bv, data| {
186 validate_coa_color(bv, Some(block), data);
187 });
188 }
189 vd.multi_field_validated_block("instance", validate_instance);
190 vd.field_validated_block("mask", |block, data| {
191 let mut vd = Validator::new(block, data);
192 vd.set_max_severity(Severity::Warning);
193 for token in vd.values() {
194 if let Some(mask) = token.expect_integer() {
195 if !(1..=3).contains(&mask) {
196 warn(ErrorKey::Range).msg("mask should be from 1 to 3").loc(token).push();
197 }
198 }
199 }
200 });
201 });
202 vd.multi_field_validated_block("textured_emblem", |block, data| {
203 let mut vd = Validator::new(block, data);
204 vd.set_max_severity(Severity::Warning);
205 vd.req_field("texture");
206 if let Some(token) = vd.field_value("texture") {
207 if let Some((_, token)) = token.split_once('"') {
208 data.verify_exists(Item::CoaTexturedEmblemList, &token);
209 } else {
210 let pathname = format!("gfx/coat_of_arms/textured_emblems/{token}");
211 data.verify_exists_implied(Item::File, &pathname, token);
212 }
213 }
214 vd.multi_field_validated_block("instance", validate_instance);
215 });
216
217 #[cfg(any(feature = "vic3", feature = "eu5"))]
218 if Game::is_vic3() || Game::is_eu5() {
219 vd.multi_field_validated_block("sub", |subblock, data| {
220 let mut vd = Validator::new(subblock, data);
221 vd.set_max_severity(Severity::Warning);
222 vd.field_item("parent", Item::Coa);
223 vd.multi_field_validated_block("instance", validate_instance_offset);
224 for field in &["color1", "color2", "color3", "color4", "color5"] {
225 vd.field_validated(field, |bv, data| {
226 validate_coa_color(bv, Some(block), data);
227 });
228 }
229 });
230 }
231}
232
233fn validate_coa_color(bv: &BV, block: Option<&Block>, data: &Everything) {
234 match bv {
235 BV::Value(color) => {
236 if let Some((_, token)) = color.split_once('"') {
237 data.verify_exists(Item::CoaColorList, &token);
238 } else if color.is("color1")
239 || color.is("color2")
240 || color.is("color3")
241 || color.is("color4")
242 || color.is("color5")
243 {
244 if let Some(block) = block {
245 if !block.has_key(color.as_str()) {
246 let msg = format!("setting to {color} but {color} is not defined");
247 warn(ErrorKey::Colors).msg(msg).loc(color).push();
248 }
249 } else {
250 let msg = format!("setting to {color} only works in an emblem");
251 warn(ErrorKey::Colors).msg(msg).loc(color).push();
252 }
253 } else {
254 data.verify_exists(Item::NamedColor, color);
255 }
256 }
257 BV::Block(block) => validate_color(block, data),
258 }
259}
260
261#[derive(Clone, Debug)]
262pub struct CoaTemplateList {}
263
264inventory::submit! {
265 ItemLoader::Full(GameFlags::all(), Item::CoaTemplateList, PdxEncoding::Utf8OptionalBom, ".txt", LoadAsFile::No, Recursive::Maybe, CoaTemplateList::add)
266}
267
268impl CoaTemplateList {
269 pub fn add(db: &mut Db, key: Token, mut block: Block) {
270 if key.is("coat_of_arms_template_lists") {
271 for (key, block) in block.drain_definitions_warn() {
272 db.add(Item::CoaTemplateList, key, block, Box::new(Self {}));
273 }
274 } else if key.is("colored_emblem_texture_lists") {
275 for (key, block) in block.drain_definitions_warn() {
276 db.add(Item::CoaColoredEmblemList, key, block, Box::new(CoaColoredEmblemList {}));
277 }
278 } else if key.is("color_lists") {
279 for (key, block) in block.drain_definitions_warn() {
280 db.add(Item::CoaColorList, key, block, Box::new(CoaColorList {}));
281 }
282 } else if key.is("pattern_texture_lists") {
283 for (key, block) in block.drain_definitions_warn() {
284 db.add(Item::CoaPatternList, key, block, Box::new(CoaPatternList {}));
285 }
286 } else if key.is("textured_emblem_texture_lists") {
287 for (key, block) in block.drain_definitions_warn() {
288 db.add(Item::CoaTexturedEmblemList, key, block, Box::new(CoaTexturedEmblemList {}));
289 }
290 } else {
291 let msg = format!("unknown list type {key}");
292 warn(ErrorKey::UnknownField).msg(msg).loc(key).push();
293 }
294 }
295}
296
297impl DbKind for CoaTemplateList {
298 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
299 validate_coa_list(key, block, data, |bv, data| {
300 if let Some(value) = bv.expect_value() {
301 data.verify_exists(Item::CoaTemplate, value);
302 }
303 });
304 }
305}
306
307#[derive(Clone, Debug)]
308pub struct CoaColoredEmblemList {}
309
310impl DbKind for CoaColoredEmblemList {
311 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
312 validate_coa_list(key, block, data, |bv, data| {
313 if let Some(value) = bv.expect_value() {
314 let pathname = format!("gfx/coat_of_arms/colored_emblems/{value}");
315 data.verify_exists_implied(Item::File, &pathname, value);
316 }
317 });
318 }
319}
320
321#[derive(Clone, Debug)]
322pub struct CoaTexturedEmblemList {}
323
324impl DbKind for CoaTexturedEmblemList {
325 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
326 validate_coa_list(key, block, data, |bv, data| {
327 if let Some(value) = bv.expect_value() {
328 let pathname = format!("gfx/coat_of_arms/textured_emblems/{value}");
329 data.verify_exists_implied(Item::File, &pathname, value);
330 }
331 });
332 }
333}
334
335#[derive(Clone, Debug)]
336pub struct CoaColorList {}
337
338impl DbKind for CoaColorList {
339 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
340 validate_coa_list(key, block, data, validate_possibly_named_color);
341 }
342}
343
344#[derive(Clone, Debug)]
345pub struct CoaPatternList {}
346
347impl DbKind for CoaPatternList {
348 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
349 validate_coa_list(key, block, data, |bv, data| {
350 if let Some(value) = bv.expect_value() {
351 let pathname = format!("gfx/coat_of_arms/patterns/{value}");
352 data.verify_exists_implied(Item::File, &pathname, value);
353 }
354 });
355 }
356}
357
358fn validate_coa_list<F>(_key: &Token, block: &Block, data: &Everything, f: F)
359where
360 F: Fn(&BV, &Everything),
361{
362 let mut vd = Validator::new(block, data);
363 vd.set_max_severity(Severity::Warning);
364
365 vd.integer_keys(|_, bv| f(bv, data));
368
369 vd.multi_field_validated_key_block("special_selection", |key, block, data| {
370 let mut vd = Validator::new(block, data);
371 vd.set_max_severity(Severity::Warning);
372 let mut sc;
373 match Game::game() {
374 #[cfg(feature = "ck3")]
375 Game::Ck3 => {
376 sc = ScopeContext::new(Scopes::Character, key); sc.define_name("faith", Scopes::Faith, key);
378 sc.define_name("culture", Scopes::Culture, key);
379 sc.define_name("title", Scopes::LandedTitle, key); }
381 #[cfg(feature = "vic3")]
382 Game::Vic3 => {
383 sc = ScopeContext::new(
386 Scopes::Country | Scopes::CountryDefinition | Scopes::PowerBloc,
387 key,
388 );
389 sc.define_name(
390 "target",
391 Scopes::Country | Scopes::CountryDefinition | Scopes::PowerBloc,
392 key,
393 );
394 }
395 #[cfg(feature = "imperator")]
396 Game::Imperator => {
397 sc = ScopeContext::new(Scopes::Country, key);
399 }
400 #[cfg(feature = "eu5")]
401 Game::Eu5 => {
402 sc = ScopeContext::new(Scopes::Country, key);
404 }
405 #[cfg(feature = "hoi4")]
406 Game::Hoi4 => unimplemented!(),
407 }
408 vd.multi_field_validated_block("trigger", |block, data| {
409 validate_trigger_max_sev(block, data, &mut sc, Tooltipped::No, Severity::Warning);
410 });
411 vd.integer_keys(|_, bv| f(bv, data));
412 vd.multi_field_validated_block("special_selection", |block, data| {
414 let mut vd = Validator::new(block, data);
415 vd.set_max_severity(Severity::Warning);
416 vd.multi_field_validated_block("trigger", |block, data| {
417 validate_trigger_max_sev(block, data, &mut sc, Tooltipped::No, Severity::Warning);
418 });
419 vd.integer_keys(|_, bv| f(bv, data));
420 });
421 });
422}
423
424#[cfg(feature = "ck3")]
425#[derive(Clone, Debug)]
426pub struct CoaDynamicDefinition {}
427
428#[cfg(feature = "ck3")]
429inventory::submit! {
430 ItemLoader::Normal(GameFlags::Ck3, Item::CoaDynamicDefinition, CoaDynamicDefinition::add)
431}
432
433#[cfg(feature = "ck3")]
434impl CoaDynamicDefinition {
435 pub fn add(db: &mut Db, key: Token, block: Block) {
436 db.add(Item::CoaDynamicDefinition, key, block, Box::new(Self {}));
437 }
438}
439
440#[cfg(feature = "ck3")]
441impl DbKind for CoaDynamicDefinition {
442 fn validate(&self, key: &Token, block: &Block, data: &Everything) {
443 let mut vd = Validator::new(block, data);
444 vd.set_max_severity(Severity::Warning);
445 let mut sc = ScopeContext::new(Scopes::LandedTitle, key);
446
447 vd.multi_field_validated_block("item", |block, data| {
448 let mut vd = Validator::new(block, data);
449 vd.set_max_severity(Severity::Warning);
450 vd.field_validated_block("trigger", |block, data| {
451 validate_trigger_max_sev(block, data, &mut sc, Tooltipped::No, Severity::Warning);
452 });
453 vd.field_item("coat_of_arms", Item::Coa);
454 });
455 }
456}
457
458fn validate_instance(block: &Block, data: &Everything) {
459 let mut vd = Validator::new(block, data);
460 vd.set_max_severity(Severity::Warning);
461 vd.field_list_precise_numeric_exactly("position", 2);
462 vd.field_validated_block("scale", validate_scale);
463 vd.field_precise_numeric("rotation");
464 vd.field_precise_numeric("depth");
465 vd.ban_field("offset", || "sub blocks");
466}
467
468#[cfg(any(feature = "vic3", feature = "eu5"))]
470fn validate_instance_offset(block: &Block, data: &Everything) {
471 let mut vd = Validator::new(block, data);
472 vd.set_max_severity(Severity::Warning);
473 vd.field_list_precise_numeric_exactly("offset", 2);
474 vd.field_validated_block("scale", validate_scale);
475 vd.field_precise_numeric("rotation");
476 vd.field_precise_numeric("depth");
477 vd.ban_field("position", || "colored and textured emblems");
478}
479
480fn validate_scale(block: &Block, data: &Everything) {
481 let mut vd = Validator::new(block, data);
482 vd.set_max_severity(Severity::Warning);
483 let mut count = 0;
484 for token in vd.values() {
485 count += 1;
486 token.expect_precise_number();
487 }
488 if count == 0 || count > 2 {
489 let msg = "expected 2 numbers";
490 warn(ErrorKey::Validation).msg(msg).loc(block).push();
491 } else if count == 1 {
492 let msg = "found only x scale";
493 let info = "adding the y scale is clearer";
494 untidy(ErrorKey::Validation).msg(msg).info(info).loc(block).push();
495 }
496}