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 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 pub fn has_track_lc(&self, key: &Lowercase) -> bool {
97 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 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 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 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 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}