tiger_lib/ck3/data/
traits.rs

1use std::path::PathBuf;
2
3use crate::block::{BV, Block};
4use crate::ck3::modif::ModifKinds;
5use crate::context::ScopeContext;
6use crate::desc::{validate_desc, validate_desc_map};
7use crate::everything::Everything;
8use crate::fileset::{FileEntry, FileHandler};
9use crate::helpers::{TigerHashMap, TigerHashSet, dup_error};
10use crate::item::Item;
11use crate::lowercase::Lowercase;
12use crate::modif::validate_modifs;
13use crate::parse::ParserMemory;
14use crate::pdxfile::PdxFile;
15use crate::report::{ErrorKey, err};
16use crate::scopes::Scopes;
17use crate::script_value::validate_script_value;
18use crate::token::Token;
19use crate::tooltipped::Tooltipped;
20use crate::validator::Validator;
21use crate::variables::Variables;
22
23#[derive(Clone, Debug, Default)]
24#[allow(clippy::struct_field_names)]
25pub struct Traits {
26    traits: TigerHashMap<&'static str, Trait>,
27    groups: TigerHashSet<Token>,
28    tracks: TigerHashSet<Token>,
29    constraints: TigerHashSet<Token>,
30    flags: TigerHashSet<Token>,
31
32    // Lowercased registries of the above collections, for case-insensitive lookups
33    traits_lc: TigerHashMap<Lowercase<'static>, &'static str>,
34    tracks_lc: TigerHashSet<Lowercase<'static>>,
35    groups_lc: TigerHashSet<Lowercase<'static>>,
36}
37
38impl Traits {
39    fn load_item(&mut self, key: Token, block: Block) {
40        if let Some(other) = self.traits.get(key.as_str()) {
41            if other.key.loc.kind >= key.loc.kind {
42                dup_error(&key, &other.key, "trait");
43            }
44        }
45        self.traits_lc.insert(Lowercase::new(key.as_str()), key.as_str());
46        self.traits.insert(key.as_str(), Trait::new(key, block));
47    }
48
49    pub fn scan_variables(&self, registry: &mut Variables) {
50        for item in self.traits.values() {
51            registry.scan(&item.block);
52        }
53    }
54
55    pub fn exists(&self, key: &str) -> bool {
56        self.traits.contains_key(key) || self.groups.contains(key)
57    }
58
59    pub fn exists_lc(&self, key: &Lowercase) -> bool {
60        self.traits_lc.contains_key(key) || self.groups_lc.contains(key)
61    }
62
63    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
64        self.traits.values().map(|item| &item.key).chain(self.groups.iter())
65    }
66
67    pub fn constraint_exists(&self, key: &str) -> bool {
68        self.constraints.contains(key)
69    }
70
71    pub fn iter_constraint_keys(&self) -> impl Iterator<Item = &Token> {
72        self.constraints.iter()
73    }
74
75    pub fn flag_exists(&self, key: &str) -> bool {
76        self.flags.contains(key)
77    }
78
79    pub fn iter_flag_keys(&self) -> impl Iterator<Item = &Token> {
80        self.flags.iter()
81    }
82
83    pub fn track_exists(&self, key: &str) -> bool {
84        self.tracks.contains(key)
85    }
86
87    pub fn track_exists_lc(&self, key: &Lowercase) -> bool {
88        self.tracks_lc.contains(key)
89    }
90
91    pub fn iter_track_keys(&self) -> impl Iterator<Item = &Token> {
92        self.tracks.iter()
93    }
94
95    // Is the trait itself a track? Different than a trait having multiple tracks
96    pub fn has_track_lc(&self, key: &Lowercase) -> bool {
97        // SAFETY: traits[t] will always succeed due to invariant of traits_lc.
98        self.traits_lc.get(key).is_some_and(|t| self.traits[t].has_track)
99    }
100
101    pub fn validate(&self, data: &Everything) {
102        for item in self.traits.values() {
103            item.validate(data);
104        }
105    }
106}
107
108impl FileHandler<Block> for Traits {
109    fn subpath(&self) -> PathBuf {
110        PathBuf::from("common/traits")
111    }
112
113    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
114        if !entry.filename().to_string_lossy().ends_with(".txt") {
115            return None;
116        }
117
118        PdxFile::read(entry, parser)
119    }
120
121    fn handle_file(&mut self, _entry: &FileEntry, mut block: Block) {
122        for (key, block) in block.drain_definitions_warn() {
123            self.load_item(key, block);
124        }
125    }
126
127    fn finalize(&mut self) {
128        for traititem in self.traits.values() {
129            if let Some(token) = traititem.block.get_field_value("group") {
130                self.groups.insert(token.clone());
131                self.groups_lc.insert(Lowercase::new(token.as_str()));
132            }
133            for token in traititem.block.get_field_values("flag") {
134                self.flags.insert(token.clone());
135            }
136            for field in
137                &["genetic_constraint_all", "genetic_constraint_men", "genetic_constraint_women"]
138            {
139                if let Some(token) = traititem.block.get_field_value(field) {
140                    self.constraints.insert(token.clone());
141                }
142            }
143            if let Some(token) = traititem.block.get_field_value("group_equivalence") {
144                self.groups.insert(token.clone());
145                self.groups_lc.insert(Lowercase::new(token.as_str()));
146            }
147            if traititem.block.has_key("track") {
148                self.tracks.insert(traititem.key.clone());
149                self.tracks_lc.insert(Lowercase::new(traititem.key.as_str()));
150            }
151            if let Some(block) = traititem.block.get_field_block("tracks") {
152                for (key, _) in block.iter_definitions() {
153                    self.tracks.insert(key.clone());
154                    self.tracks_lc.insert(Lowercase::new(key.as_str()));
155                }
156            }
157        }
158    }
159}
160
161#[derive(Clone, Debug)]
162pub struct Trait {
163    key: Token,
164    block: Block,
165    has_track: bool,
166}
167
168impl Trait {
169    pub fn new(key: Token, block: Block) -> Self {
170        let has_track = block.has_key("track") || block.has_key("tracks");
171        Self { key, block, has_track }
172    }
173
174    pub fn validate(&self, data: &Everything) {
175        let mut vd = Validator::new(&self.block, data);
176        let mut sc = ScopeContext::new(Scopes::Character, &self.key);
177
178        let genetic = self.block.field_value_is("genetic", "yes");
179
180        if !vd.field_validated_sc("name", &mut sc, validate_desc) {
181            let loca = format!("trait_{}", self.key);
182            data.verify_exists_implied(Item::Localization, &loca, &self.key);
183        }
184
185        if !vd.field_validated_sc("desc", &mut sc, validate_desc) {
186            let loca = format!("trait_{}_desc", self.key);
187            data.verify_exists_implied(Item::Localization, &loca, &self.key);
188        }
189
190        if !vd.field_validated("icon", |bv, data| {
191            validate_desc_map(bv, data, &mut sc, |name, data, _| {
192                data.verify_icon("NGameIcons|TRAIT_ICON_PATH", name, "");
193            });
194        }) {
195            data.verify_icon("NGameIcons|TRAIT_ICON_PATH", &self.key, ".dds");
196        }
197
198        vd.field_item("category", Item::TraitCategory);
199        vd.multi_field_validated_block("culture_modifier", validate_culture_modifier);
200        vd.multi_field_validated_block("faith_modifier", validate_faith_modifier);
201        vd.field_item("culture_succession_prio", Item::CultureParameter);
202        vd.multi_field_validated_block("triggered_opinion", validate_triggered_opinion);
203
204        vd.field_validated_block("tracks", |block, data| {
205            let mut vd = Validator::new(block, data);
206            vd.unknown_block_fields(|key, block| {
207                validate_trait_track(key, block, data, key);
208            });
209        });
210        vd.field_validated_key_block("track", |key, block, data| {
211            validate_trait_track(&self.key, block, data, key);
212        });
213
214        vd.field_list_items("opposites", Item::Trait);
215        if let Some(tokens) = self.block.get_field_list("opposites") {
216            for token in tokens {
217                data.verify_exists(Item::Trait, &token);
218            }
219        }
220
221        vd.field_validated_block("compatibility", |block, data| {
222            let mut vd = Validator::new(block, data);
223            vd.unknown_value_fields(|key, value| {
224                data.verify_exists(Item::Trait, key);
225                value.expect_number();
226            });
227        });
228
229        vd.field_trigger("potential", Tooltipped::No, &mut sc);
230
231        vd.field_validated_block("monthly_track_xp_degradation", |block, data| {
232            let mut vd = Validator::new(block, data);
233            vd.field_numeric("min");
234            vd.field_numeric("change");
235        });
236
237        vd.field_integer("minimum_age");
238        vd.field_integer("maximum_age");
239        vd.field_choice("valid_sex", &["all", "male", "female"]);
240        vd.replaced_field("education", "`category = education`");
241        vd.replaced_field("childhood", "`category = childhood`");
242        vd.field_integer("ruler_designer_cost");
243        vd.field_bool("shown_in_ruler_designer");
244        vd.field_bool("add_commander_trait");
245        vd.replaced_field("fame", "`category = fame`");
246        vd.replaced_field("lifestyle", "`category = lifestyle`");
247        vd.replaced_field("personality", "`category = personality`");
248        vd.replaced_field("health_trait", "`category = health`");
249        vd.replaced_field("commander", "`category = commander`");
250        vd.replaced_field("court_type_trait", "`category = court_type`");
251        vd.field_bool("genetic");
252        vd.field_bool("physical");
253        vd.field_bool("good");
254        vd.field_bool("immortal");
255        vd.field_bool("can_have_children");
256        vd.field_bool("enables_inbred");
257        vd.field_value("group");
258        vd.field_value("group_equivalence");
259        vd.field_numeric("same_opinion");
260        vd.field_numeric("same_opinion_if_same_faith");
261        vd.field_numeric("opposite_opinion");
262        vd.field_numeric("same_faith_opinion");
263        vd.field_integer("level");
264        vd.field_integer_range("inherit_chance", 0..=100);
265        vd.field_integer_range("both_parent_has_trait_inherit_chance", 0..=100);
266        vd.advice_field("can_inherit", "no longer used");
267        vd.field_bool("inherit_from_real_mother");
268        vd.field_bool("inherit_from_real_father");
269        vd.field_choice("parent_inheritance_sex", &["male", "female", "all"]);
270        vd.field_choice("child_inheritance_sex", &["male", "female", "all"]);
271        if genetic {
272            vd.field_numeric_range("birth", 0.0..=1.0);
273            vd.field_numeric_range("random_creation", 0.0..=1.0);
274            vd.ban_field("random_creation_weight", || "genetic = no");
275        } else {
276            vd.ban_field("birth", || "genetic = yes");
277            vd.ban_field("random_creation", || "genetic = yes");
278            vd.field_numeric("random_creation_weight");
279        }
280        vd.replaced_field("blocks_from_claim_inheritance", "`claim_inheritance_blocker = all`");
281        vd.replaced_field(
282            "blocks_from_claim_inheritance_from_dynasty",
283            "`claim_inheritance_blocker = dynasty`",
284        );
285        vd.field_bool("incapacitating");
286        vd.field_bool("disables_combat_leadership");
287        for token in vd.multi_field_value("flag") {
288            // These are optional
289            let loca = format!("TRAIT_FLAG_DESC_{token}");
290            data.localization.suggest(&loca, token);
291        }
292        vd.field_bool("shown_in_encyclopedia");
293
294        vd.field_choice("inheritance_blocker", &["none", "dynasty", "all"]);
295        vd.field_choice("claim_inheritance_blocker", &["none", "dynasty", "all"]);
296        vd.field_choice("bastard", &["none", "illegitimate", "legitimate"]);
297
298        // The ethnicity files refer to these
299        vd.field_value("genetic_constraint_all");
300        vd.field_value("genetic_constraint_men");
301        vd.field_value("genetic_constraint_women");
302
303        vd.field_numeric("portrait_extremity_shift");
304        vd.field_numeric("ugliness_portrait_extremity_shift");
305        vd.advice_field("portrait_pose", "Removed in 1.13");
306
307        vd.field_list_items("trait_exclusive_if_realm_contains", Item::Terrain);
308        vd.replaced_field("trait_winter_exclusive", "`category = winter_commander`");
309
310        validate_modifs(&self.block, data, ModifKinds::Character, vd);
311    }
312}
313
314fn validate_culture_modifier(block: &Block, data: &Everything) {
315    let mut vd = Validator::new(block, data);
316
317    vd.field_item("parameter", Item::CultureParameter);
318    validate_modifs(block, data, ModifKinds::Character, vd);
319}
320
321fn validate_faith_modifier(block: &Block, data: &Everything) {
322    let mut vd = Validator::new(block, data);
323
324    vd.field_item("parameter", Item::DoctrineParameter);
325    validate_modifs(block, data, ModifKinds::Character, vd);
326}
327
328fn validate_triggered_opinion(block: &Block, data: &Everything) {
329    let mut vd = Validator::new(block, data);
330
331    vd.field_item("opinion_modifier", Item::OpinionModifier);
332    vd.field_item("parameter", Item::DoctrineParameter);
333    vd.field_bool("check_missing");
334    vd.field_bool("same_faith");
335    vd.field_bool("same_dynasty");
336    vd.field_bool("ignore_opinion_value_if_same_trait");
337    vd.field_bool("male_only");
338    vd.field_bool("female_only");
339}
340
341fn validate_trait_track(key: &Token, block: &Block, data: &Everything, warn_key: &Token) {
342    let mut vd = Validator::new(block, data);
343    vd.unknown_block_fields(|key, block| {
344        let mut sc = ScopeContext::new(Scopes::None, warn_key);
345        validate_script_value(&BV::Value(key.clone()), data, &mut sc);
346        if let Some(xp) = key.get_integer() {
347            // LAST UPDATED CK3 VERSION 1.11.3
348            if xp > 100 {
349                let msg = "trait xp only goes up to 100";
350                err(ErrorKey::Range).strong().msg(msg).loc(key).push();
351            }
352        }
353
354        let mut vd = Validator::new(block, data);
355        vd.multi_field_validated_block("culture_modifier", validate_culture_modifier);
356        vd.multi_field_validated_block("faith_modifier", validate_faith_modifier);
357        validate_modifs(block, data, ModifKinds::Character, vd);
358    });
359    // let modif = format!("{key}_xp_degradation_mult");
360    // data.verify_exists_implied(Item::ModifierFormat, &modif, warn_key);
361    // let modif = format!("{key}_xp_gain_mult");
362    // data.verify_exists_implied(Item::ModifierFormat, &modif, warn_key);
363    // let modif = format!("{key}_xp_loss_mult");
364    // data.verify_exists_implied(Item::ModifierFormat, &modif, warn_key);
365
366    let loca = format!("trait_track_{key}");
367    data.verify_exists_implied(Item::Localization, &loca, warn_key);
368    let loca = format!("trait_track_{key}_desc");
369    data.verify_exists_implied(Item::Localization, &loca, warn_key);
370}