Skip to main content

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