tiger_lib/ck3/data/
legends.rs

1use crate::Everything;
2use crate::block::{BV, Block};
3use crate::ck3::modif::ModifKinds;
4use crate::ck3::tables::misc::LEGEND_QUALITY;
5use crate::ck3::validate::validate_cost;
6use crate::context::ScopeContext;
7use crate::db::{Db, DbKind};
8use crate::desc::validate_desc;
9use crate::effect::validate_effect;
10use crate::game::GameFlags;
11use crate::helpers::{TigerHashMap, TigerHashSet};
12use crate::item::{Item, ItemLoader};
13use crate::modif::validate_modifs;
14use crate::report::{ErrorKey, err};
15use crate::scopes::Scopes;
16use crate::script_value::{validate_non_dynamic_script_value, validate_script_value_no_breakdown};
17use crate::token::Token;
18use crate::tooltipped::Tooltipped;
19use crate::trigger::validate_target;
20use crate::validate::{validate_duration, validate_possibly_named_color};
21use crate::validator::Validator;
22
23#[derive(Clone, Debug)]
24pub struct LegendType {}
25
26inventory::submit! {
27    ItemLoader::Normal(GameFlags::Ck3, Item::LegendType, LegendType::add)
28}
29
30impl LegendType {
31    pub fn add(db: &mut Db, key: Token, block: Block) {
32        db.add(Item::LegendType, key, block, Box::new(Self {}));
33    }
34}
35
36impl DbKind for LegendType {
37    fn validate(&self, key: &Token, block: &Block, data: &crate::Everything) {
38        let loca = format!("legend_{key}");
39        data.verify_exists_implied(Item::Localization, &loca, key);
40        let loca = format!("legend_{key}_name");
41        data.verify_exists_implied(Item::Localization, &loca, key);
42        let loca = format!("legend_{key}_desc");
43        data.verify_exists_implied(Item::Localization, &loca, key);
44
45        let mut vd = Validator::new(block, data);
46        vd.field_validated("color", validate_possibly_named_color);
47        vd.field_effect_builder("on_province_spread", Tooltipped::No, build_province_legend_sc);
48        vd.field_effect_builder("on_province_recovered", Tooltipped::No, build_province_legend_sc);
49        vd.field_effect_rooted("on_start", Tooltipped::No, Scopes::Legend);
50        vd.field_effect_rooted("on_end", Tooltipped::No, Scopes::Legend);
51        // ScopeContext undocumented
52        vd.field_effect_builder("on_yearly", Tooltipped::No, build_character_legend_sc);
53        // TODO: verify tooltip
54        vd.field_effect_builder(
55            "on_legend_start_promote",
56            Tooltipped::No,
57            build_character_legend_sc,
58        );
59        vd.field_effect_builder(
60            "on_legend_stop_promote",
61            Tooltipped::No,
62            build_character_legend_sc,
63        );
64        // TODO: verify tooltip
65        vd.field_trigger_builder(
66            "is_valid_protagonist",
67            Tooltipped::No,
68            build_character_character_sc,
69        );
70        vd.field_validated_build_sc(
71            "ai_protagonist_weight",
72            build_character_character_sc,
73            validate_script_value_no_breakdown,
74        );
75        vd.field_validated_block("quality", |block, data| {
76            let mut vd = Validator::new(block, data);
77            for &quality in LEGEND_QUALITY {
78                vd.req_field(quality);
79                vd.field_validated_block(quality, validate_legend_quality);
80            }
81        });
82    }
83
84    fn validate_call(
85        &self,
86        _key: &Token,
87        block: &Block,
88        _from: &Token,
89        _from_block: &Block,
90        data: &Everything,
91        sc: &mut ScopeContext,
92    ) {
93        if let Some(block) = block.get_field_block("quality") {
94            for (_, block) in block.iter_definitions() {
95                if let Some(block) = block.get_field_block("impact") {
96                    if let Some(block) = block.get_field_block("on_complete") {
97                        validate_effect(block, data, sc, Tooltipped::Yes); // TODO verify tooltip
98                    }
99                }
100            }
101        }
102    }
103}
104
105fn build_province_legend_sc(key: &Token) -> ScopeContext {
106    let mut sc = ScopeContext::new(Scopes::Province, key);
107    sc.define_name("legend", Scopes::Legend, key);
108    sc
109}
110
111fn build_character_legend_sc(key: &Token) -> ScopeContext {
112    let mut sc = ScopeContext::new(Scopes::Character, key);
113    sc.define_name("legend", Scopes::Legend, key);
114    sc
115}
116
117fn build_character_character_sc(key: &Token) -> ScopeContext {
118    let mut sc = ScopeContext::new(Scopes::Character, key);
119    sc.define_name("creator", Scopes::Character, key);
120    sc
121}
122
123fn validate_legend_quality(block: &Block, data: &Everything) {
124    let mut vd = Validator::new(block, data);
125    vd.field_validated_build_sc(
126        "spread_chance",
127        build_province_legend_sc,
128        validate_script_value_no_breakdown,
129    );
130    vd.field_validated("max_provinces", validate_non_dynamic_script_value);
131    vd.field_validated_block_rooted("owner_cost", Scopes::Character, validate_cost);
132    vd.field_validated_block_build_sc("promoter_cost", build_character_legend_sc, validate_cost);
133    vd.field_validated_block_rooted("creation_cost", Scopes::Character, validate_cost);
134    vd.field_validated_block_rooted("upgrade_cost", Scopes::Character, validate_cost);
135    vd.field_validated_block_rooted("removal_duration", Scopes::None, validate_duration);
136    vd.field_validated_block("impact", |block, data| {
137        let mut vd = Validator::new(block, data);
138        validate_impact_modifiers(&mut vd);
139        // proper validation in `validate_call`
140        vd.field_block("on_complete");
141    });
142    vd.field_validated_block("ai_chance", validate_ai_chance);
143}
144
145fn validate_impact_modifiers(vd: &mut Validator) {
146    vd.field_validated_block("province_modifier", |block, data| {
147        let vd = Validator::new(block, data);
148        validate_modifs(block, data, ModifKinds::Province, vd);
149    });
150    vd.field_validated_block("county_modifier", |block, data| {
151        let vd = Validator::new(block, data);
152        validate_modifs(block, data, ModifKinds::County, vd);
153    });
154    vd.field_validated_block("owner_modifier", |block, data| {
155        let vd = Validator::new(block, data);
156        validate_modifs(block, data, ModifKinds::Character, vd);
157    });
158    vd.field_validated_block("promoter_modifier", |block, data| {
159        let vd = Validator::new(block, data);
160        validate_modifs(block, data, ModifKinds::Character, vd);
161    });
162}
163
164fn validate_ai_chance(block: &Block, data: &Everything) {
165    let mut vd = Validator::new(block, data);
166
167    vd.field_validated_rooted("create", Scopes::Character, validate_script_value_no_breakdown);
168    vd.field_validated_build_sc(
169        "promote",
170        build_character_legend_sc,
171        validate_script_value_no_breakdown,
172    );
173    vd.field_validated_build_sc(
174        "take_unowned",
175        build_character_legend_sc,
176        validate_script_value_no_breakdown,
177    );
178    vd.field_validated_build_sc(
179        "upgrade",
180        build_character_legend_sc,
181        validate_script_value_no_breakdown,
182    );
183    vd.field_validated_build_sc(
184        "complete",
185        |key| {
186            let mut sc = build_character_legend_sc(key);
187            sc.define_name("can_afford_current_level", Scopes::Bool, key);
188            sc
189        },
190        validate_script_value_no_breakdown,
191    );
192}
193
194#[derive(Clone, Debug)]
195pub struct LegendSeed {}
196
197inventory::submit! {
198    ItemLoader::Normal(GameFlags::Ck3, Item::LegendSeed, LegendSeed::add)
199}
200
201impl LegendSeed {
202    pub fn add(db: &mut Db, key: Token, block: Block) {
203        db.add(Item::LegendSeed, key, block, Box::new(Self {}));
204    }
205}
206
207impl DbKind for LegendSeed {
208    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
209        let loca = format!("legend_{key}");
210        data.verify_exists_implied(Item::Localization, &loca, key);
211        let loca = format!("legend_{key}_desc");
212        data.verify_exists_implied(Item::Localization, &loca, key);
213
214        let mut vd = Validator::new(block, data);
215        vd.req_field("quality");
216        vd.req_field("type");
217        vd.req_field("chronicle");
218
219        vd.field_choice("quality", LEGEND_QUALITY);
220        vd.field_item("type", Item::LegendType);
221        vd.field_trigger_rooted("is_shown", Tooltipped::No, Scopes::Character);
222        // TODO verify tooltip
223        vd.field_trigger_rooted("is_valid", Tooltipped::Yes, Scopes::Character);
224
225        if let Some(chronicle_token) = vd.field_value("chronicle").cloned() {
226            data.verify_exists(Item::LegendChronicle, &chronicle_token);
227
228            if let Some((_, _, chronicle)) =
229                data.get_item::<LegendChronicle>(Item::LegendChronicle, chronicle_token.as_str())
230            {
231                vd.field_validated_key_block("chronicle_properties", |key, block, data| {
232                    let mut found_properties = TigerHashSet::default();
233                    let mut sc = ScopeContext::new(Scopes::Character, key);
234                    let mut vd = Validator::new(block, data);
235                    vd.unknown_fields(|key, bv| {
236                        if let Some(scopes) = chronicle.properties.get(key).copied() {
237                            found_properties.insert(key.clone());
238
239                            match bv {
240                                BV::Value(value) => {
241                                    validate_target(value, data, &mut sc, scopes);
242                                }
243                                BV::Block(block) => {
244                                    let mut vd = Validator::new(block, data);
245                                    vd.field_target("target", &mut sc, scopes);
246                                    vd.field_trigger("is_valid", Tooltipped::No, &mut sc);
247                                }
248                            }
249                        } else {
250                            let msg =
251                                format!("property {key} not found in {chronicle_token} chronicle");
252                            err(ErrorKey::Validation).msg(msg).loc(key).push();
253                        }
254                    });
255
256                    for property in chronicle.properties.keys() {
257                        if !found_properties.contains(property) {
258                            let msg = format!("property {property} not found");
259                            err(ErrorKey::Validation)
260                                .msg(msg)
261                                .loc(key)
262                                .loc_msg(property, "from here")
263                                .push();
264                        }
265                    }
266                });
267                vd.field_validated_block("chronicle_chapters", |block, data| {
268                    let mut vd = Validator::new(block, data);
269                    vd.unknown_value_fields(|key, value| {
270                        if !chronicle.chapters.contains(key) {
271                            let msg =
272                                format!("chapter {key} not found in {chronicle_token} chronicle");
273                            err(ErrorKey::Validation).msg(msg).loc(key).push();
274                        }
275                        data.verify_exists(Item::Localization, value);
276                    });
277                });
278
279                // Validate type's `on_complete` block based on the chronicle's properties
280                if let Some(value) = vd.field_value("type") {
281                    data.validate_call(
282                        Item::LegendType,
283                        key,
284                        block,
285                        &mut build_impact_on_complete_sc(chronicle, value),
286                    );
287                }
288            }
289        }
290    }
291}
292
293#[derive(Clone, Debug)]
294pub struct LegendChronicle {
295    pub properties: TigerHashMap<Token, Scopes>,
296    chapters: TigerHashSet<Token>,
297}
298
299inventory::submit! {
300    ItemLoader::Normal(GameFlags::Ck3, Item::LegendChronicle, LegendChronicle::add)
301}
302
303impl LegendChronicle {
304    pub fn add(db: &mut Db, key: Token, block: Block) {
305        let mut properties = TigerHashMap::default();
306        let mut chapters = TigerHashSet::default();
307
308        if let Some(block) = block.get_field_block("properties") {
309            for (key, value) in block.iter_assignments() {
310                if let Some(scopes) = Scopes::from_snake_case(value.as_str()) {
311                    properties.insert(key.clone(), scopes);
312                }
313            }
314        }
315
316        if let Some(block) = block.get_field_block("chapters") {
317            for (key, _) in block.iter_assignments() {
318                chapters.insert(key.clone());
319            }
320        }
321        db.add(Item::LegendChronicle, key, block, Box::new(Self { properties, chapters }));
322    }
323}
324
325impl DbKind for LegendChronicle {
326    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
327        if let Some(block) = block.get_field_block("properties") {
328            for (key, _) in block.iter_assignments() {
329                db.add_flag(Item::LegendProperty, key.clone());
330            }
331        }
332
333        if let Some(block) = block.get_field_block("chapters") {
334            for (key, _) in block.iter_assignments() {
335                db.add_flag(Item::LegendChapter, key.clone());
336            }
337        }
338    }
339
340    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
341        let mut vd = Validator::new(block, data);
342
343        if !vd.field_validated_build_sc(
344            "name",
345            |key| build_impact_on_complete_sc(self, key),
346            validate_desc,
347        ) {
348            let loca = format!("legend_chronicle_{key}");
349            data.verify_exists_implied(Item::Localization, &loca, key);
350        }
351
352        if !vd.field_validated_build_sc(
353            "description",
354            |key| build_impact_on_complete_sc(self, key),
355            validate_desc,
356        ) {
357            let loca = format!("legend_chronicle_{key}_desc");
358            data.verify_exists_implied(Item::Localization, &loca, key);
359        }
360
361        // undocumented
362        vd.field_item("portrait_animation", Item::PortraitAnimation);
363
364        vd.field_validated_block("properties", |block, data| {
365            let mut vd = Validator::new(block, data);
366            vd.unknown_value_fields(|_, value| {
367                if Scopes::from_snake_case(value.as_str()).is_none() {
368                    let msg = "expected a valid scope type";
369                    err(ErrorKey::Validation).msg(msg).loc(value).push();
370                }
371            });
372        });
373        vd.field_validated_block_build_sc(
374            "chapters",
375            |key| build_root_properties_sc(Scopes::Legend, self, key),
376            |block, data, sc| {
377                let mut vd = Validator::new(block, data);
378                vd.unknown_value_fields(|_, value| {
379                    data.validate_localization_sc(value.as_str(), sc);
380                });
381            },
382        );
383        // Assume the same scope context as impact in `LegendType`
384        vd.field_validated_block("impact", |block, data| {
385            let mut vd = Validator::new(block, data);
386            validate_impact_modifiers(&mut vd);
387            // TODO verify tooltip
388            vd.field_effect_builder("on_complete", Tooltipped::Yes, |key| {
389                build_impact_on_complete_sc(self, key)
390            });
391        });
392    }
393}
394
395fn build_impact_on_complete_sc(chronicle: &LegendChronicle, key: &Token) -> ScopeContext {
396    let mut sc = build_root_properties_sc(Scopes::Character, chronicle, key);
397    sc.define_name("protagonist", Scopes::Character, key);
398    sc
399}
400
401fn build_root_properties_sc(
402    root: Scopes,
403    chronicle: &LegendChronicle,
404    key: &Token,
405) -> ScopeContext {
406    let mut sc = ScopeContext::new(root, key);
407    for (property, scopes) in &chronicle.properties {
408        sc.define_name(property.as_str(), *scopes, key);
409    }
410    sc
411}