tiger_lib/data/
coa.rs

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    // TODO: warn about duplicate values in the lists?
366
367    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); // TODO: may be unset
377                sc.define_name("faith", Scopes::Faith, key);
378                sc.define_name("culture", Scopes::Culture, key);
379                sc.define_name("title", Scopes::LandedTitle, key); // TODO: may be unset
380            }
381            #[cfg(feature = "vic3")]
382            Game::Vic3 => {
383                // TODO: Exact scope depends on the context of use of this coa list.
384                // Should check again with exact scope at point of use.
385                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                // TODO: what is the correct scope here?
398                sc = ScopeContext::new(Scopes::Country, key);
399            }
400            #[cfg(feature = "eu5")]
401            Game::Eu5 => {
402                // TODO: what is the correct scope here?
403                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        // special_selection can be nested. TODO: how far?
413        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/// Just like [`validate_instance`], but takes offset instead of position
469#[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}