tiger_lib/ck3/data/
doctrines.rs

1use std::path::PathBuf;
2
3use crate::block::Block;
4use crate::ck3::modif::ModifKinds;
5use crate::ck3::validate::validate_traits;
6use crate::context::ScopeContext;
7use crate::desc::validate_desc;
8use crate::everything::Everything;
9use crate::fileset::{FileEntry, FileHandler};
10use crate::helpers::{TigerHashMap, TigerHashSet, dup_error};
11use crate::item::Item;
12use crate::modif::validate_modifs;
13use crate::parse::ParserMemory;
14use crate::pdxfile::PdxFile;
15use crate::scopes::Scopes;
16use crate::token::Token;
17use crate::tooltipped::Tooltipped;
18use crate::validator::Validator;
19use crate::variables::Variables;
20
21#[derive(Clone, Debug, Default)]
22#[allow(clippy::struct_field_names)]
23pub struct Doctrines {
24    categories: TigerHashMap<&'static str, DoctrineCategory>,
25    doctrines: TigerHashMap<&'static str, Doctrine>,
26    parameters: TigerHashSet<Token>, // all parameters, including boolean
27    boolean_parameters: TigerHashSet<Token>, // only the boolean parameters
28}
29
30impl Doctrines {
31    fn load_item(&mut self, key: Token, block: Block) {
32        if let Some(other) = self.categories.get(key.as_str()) {
33            if other.key.loc.kind >= key.loc.kind {
34                dup_error(&key, &other.key, "doctrine category");
35            }
36        }
37        self.categories.insert(key.as_str(), DoctrineCategory::new(key, block));
38    }
39
40    pub fn scan_variables(&self, registry: &mut Variables) {
41        for item in self.categories.values() {
42            registry.scan(&item.block);
43        }
44        for item in self.doctrines.values() {
45            registry.scan(&item.block);
46        }
47    }
48
49    pub fn validate(&self, data: &Everything) {
50        for category in self.categories.values() {
51            category.validate(data);
52        }
53        for doctrine in self.doctrines.values() {
54            doctrine.validate(data);
55        }
56    }
57
58    pub fn exists(&self, key: &str) -> bool {
59        self.doctrines.contains_key(key)
60    }
61
62    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
63        self.doctrines.values().map(|item| &item.key)
64    }
65
66    pub fn category_exists(&self, key: &str) -> bool {
67        self.categories.contains_key(key)
68    }
69
70    pub fn category(&self, key: &str) -> Option<&Token> {
71        self.doctrines.get(key).map(|d| &d.category)
72    }
73
74    pub fn number_of_picks(&self, category: &str) -> Option<&Token> {
75        self.categories.get(category).and_then(|c| c.picks.as_ref())
76    }
77
78    pub fn iter_category_keys(&self) -> impl Iterator<Item = &Token> {
79        self.categories.values().map(|item| &item.key)
80    }
81
82    pub fn parameter_exists(&self, key: &str) -> bool {
83        self.parameters.contains(key)
84    }
85
86    pub fn boolean_parameter_exists(&self, key: &str) -> bool {
87        self.boolean_parameters.contains(key)
88    }
89
90    pub fn iter_parameter_keys(&self) -> impl Iterator<Item = &Token> {
91        self.parameters.iter()
92    }
93
94    pub fn iter_boolean_parameter_keys(&self) -> impl Iterator<Item = &Token> {
95        self.boolean_parameters.iter()
96    }
97
98    pub fn unreformed(&self, key: &str) -> bool {
99        self.doctrines.get(key).is_some_and(Doctrine::unreformed)
100    }
101}
102
103impl FileHandler<Block> for Doctrines {
104    fn subpath(&self) -> PathBuf {
105        PathBuf::from("common/religion/doctrines")
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(entry, parser)
114    }
115
116    fn handle_file(&mut self, _entry: &FileEntry, mut block: Block) {
117        for (key, block) in block.drain_definitions_warn() {
118            self.load_item(key, block);
119        }
120    }
121
122    fn finalize(&mut self) {
123        for category in self.categories.values() {
124            for (doctrine, block) in category.block.iter_definitions() {
125                // skip definitions that are not doctrines
126                if doctrine.is("is_available_on_create") || doctrine.is("name") {
127                    continue;
128                }
129
130                if let Some(other) = self.doctrines.get(doctrine.as_str()) {
131                    if other.key.loc.kind >= doctrine.loc.kind {
132                        dup_error(doctrine, &other.key, "doctrine");
133                    }
134                }
135
136                if let Some(b) = block.get_field_block("parameters") {
137                    for (k, v) in b.iter_assignments() {
138                        self.parameters.insert(k.clone());
139                        if v.is("yes") || v.is("no") {
140                            self.boolean_parameters.insert(k.clone());
141                        }
142                    }
143                }
144                self.doctrines.insert(
145                    doctrine.as_str(),
146                    Doctrine::new(doctrine.clone(), block.clone(), category.key.clone()),
147                );
148            }
149        }
150    }
151}
152
153#[derive(Clone, Debug)]
154pub struct DoctrineCategory {
155    key: Token,
156    block: Block,
157    picks: Option<Token>,
158}
159
160impl DoctrineCategory {
161    pub fn new(key: Token, block: Block) -> Self {
162        let picks = block.get_field_value("number_of_picks").cloned();
163        Self { key, block, picks }
164    }
165
166    pub fn needs_icon(&self, data: &Everything) -> bool {
167        if let Some(group) = self.block.get_field_value("group") {
168            if group.is("special") || group.is("not_creatable") {
169                return false;
170            }
171        }
172
173        if let Some(icon_path) =
174            data.get_defined_string_warn(&self.key, "NGameIcons|FAITH_DOCTRINE_GROUP_ICON_PATH")
175        {
176            let path = format!("{icon_path}/{}.dds", &self.key);
177            data.mark_used(Item::File, &path);
178            return !data.fileset.exists(&path);
179        }
180        true
181    }
182
183    pub fn validate(&self, data: &Everything) {
184        let mut vd = Validator::new(&self.block, data);
185        let mut sc = ScopeContext::new(Scopes::Faith, &self.key);
186
187        if !vd.field_validated_sc("name", &mut sc, validate_desc) {
188            let loca = format!("{}_name", self.key);
189            data.verify_exists_implied(Item::Localization, &loca, &self.key);
190        }
191
192        // doc says "grouping" but that's wrong
193        vd.field_value("group");
194
195        vd.field_integer("number_of_picks");
196        vd.field_trigger("is_available_on_create", Tooltipped::No, &mut sc);
197
198        // Any remaining definitions are doctrines, so accept them all.
199        // They are validated separately.
200        vd.unknown_block_fields(|_, _| ());
201    }
202}
203
204#[derive(Clone, Debug)]
205pub struct Doctrine {
206    key: Token,
207    block: Block,
208    category: Token,
209}
210
211impl Doctrine {
212    pub fn new(key: Token, block: Block, category: Token) -> Self {
213        Self { key, block, category }
214    }
215
216    pub fn validate(&self, data: &Everything) {
217        let mut vd = Validator::new(&self.block, data);
218        let mut sc = ScopeContext::new(Scopes::Faith, &self.key);
219        sc.define_list("selected_doctrines", Scopes::Doctrine, &self.key);
220
221        if let Some(icon) = vd.field_value("icon") {
222            data.verify_icon("NGameIcons|FAITH_DOCTRINE_ICON_PATH", icon, ".dds");
223        } else if data.doctrines.categories[self.category.as_str()].needs_icon(data) {
224            data.verify_icon("NGameIcons|FAITH_DOCTRINE_ICON_PATH", &self.key, ".dds");
225        }
226
227        if !vd.field_validated_sc("name", &mut sc, validate_desc) {
228            let loca = format!("{}_name", self.key);
229            data.verify_exists_implied(Item::Localization, &loca, &self.key);
230        }
231
232        if !vd.field_validated_sc("desc", &mut sc, validate_desc) {
233            let loca = format!("{}_desc", self.key);
234            data.verify_exists_implied(Item::Localization, &loca, &self.key);
235        }
236
237        vd.field_bool("visible");
238        vd.field_validated_block("parameters", validate_parameters);
239        vd.field_script_value("piety_cost", &mut sc);
240        vd.field_trigger("is_shown", Tooltipped::No, &mut sc);
241        vd.field_trigger("can_pick", Tooltipped::Yes, &mut sc);
242
243        vd.field_validated_block("character_modifier", |block, data| {
244            let mut vd = Validator::new(block, data);
245            vd.field_item("name", Item::Localization);
246            validate_modifs(block, data, ModifKinds::Character, vd);
247        });
248
249        // Not documented, but used in vanilla
250        vd.field_validated_block("clergy_modifier", |block, data| {
251            let vd = Validator::new(block, data);
252            validate_modifs(block, data, ModifKinds::Character, vd);
253        });
254
255        // In the docs but not used in vanilla
256        vd.field_validated_block("doctrine_character_modifier", |block, data| {
257            let mut vd = Validator::new(block, data);
258            vd.field_item("doctrine", Item::Doctrine);
259            validate_modifs(block, data, ModifKinds::Character, vd);
260        });
261
262        vd.field_validated_block("traits", validate_traits);
263    }
264
265    fn unreformed(&self) -> bool {
266        if let Some(block) = self.block.get_field_block("parameters") {
267            return block.field_value_is("unreformed", "yes");
268        }
269        false
270    }
271}
272
273fn validate_parameters(block: &Block, data: &Everything) {
274    let mut vd = Validator::new(block, data);
275
276    vd.unknown_value_fields(|_, value| {
277        if value.is("yes") || value.is("no") {
278            return;
279        }
280        value.expect_number();
281    });
282}