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