tiger_lib/ck3/data/
activities.rs

1use crate::block::{BV, Block};
2use crate::ck3::data::scripted_animations::validate_scripted_animation;
3use crate::ck3::tables::misc::PROVINCE_FILTERS;
4use crate::ck3::validate::{validate_ai_targets, validate_cost, validate_quick_trigger};
5use crate::context::ScopeContext;
6use crate::db::{Db, DbKind};
7use crate::desc::validate_desc;
8use crate::everything::Everything;
9use crate::game::GameFlags;
10use crate::item::{Item, ItemLoader};
11use crate::report::{ErrorKey, err, warn};
12use crate::scopes::Scopes;
13use crate::token::Token;
14use crate::tooltipped::Tooltipped;
15use crate::trigger::validate_trigger;
16use crate::validate::{validate_duration, validate_modifiers_with_base};
17use crate::validator::Validator;
18
19#[derive(Clone, Debug)]
20pub struct ActivityType {}
21
22inventory::submit! {
23    ItemLoader::Normal(GameFlags::Ck3, Item::ActivityType, ActivityType::add)
24}
25
26impl ActivityType {
27    pub fn add(db: &mut Db, key: Token, block: Block) {
28        db.add(Item::ActivityType, key, block, Box::new(Self {}));
29    }
30}
31
32impl DbKind for ActivityType {
33    fn add_subitems(&self, _key: &Token, block: &Block, db: &mut Db) {
34        if let Some(block) = block.get_field_block("options") {
35            for (key, block) in block.iter_definitions() {
36                db.add_flag(Item::ActivityOptionCategory, key.clone());
37                for (key, _) in block.iter_definitions() {
38                    db.add_flag(Item::ActivityOption, key.clone());
39                }
40            }
41        }
42        if let Some(block) = block.get_field_block("phases") {
43            for (key, _) in block.iter_definitions() {
44                db.add_flag(Item::ActivityPhase, key.clone());
45            }
46        }
47        if let Some(block) = block.get_field_block("special_guests") {
48            for (key, _) in block.iter_definitions() {
49                db.add_flag(Item::SpecialGuest, key.clone());
50            }
51        }
52        // window_characters seem to count as special guests too
53        if let Some(block) = block.get_field_block("window_characters") {
54            for (key, _) in block.iter_definitions() {
55                db.add_flag(Item::SpecialGuest, key.clone());
56            }
57        }
58        if let Some(vec) = block.get_field_list("guest_subsets") {
59            for token in vec {
60                db.add_flag(Item::GuestSubset, token.clone());
61            }
62        }
63    }
64
65    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
66        let define = "NGameIcons|ACTIVITY_TYPE_ICON_PATH";
67        data.verify_icon(define, key, ".dds");
68        data.verify_icon(define, key, "_header.dds");
69
70        let define = "NGameIcons|ACTIVITY_HEADER_BACKGROUND_PATH";
71        data.verify_icon(define, key, ".dds");
72
73        let mut vd = Validator::new(block, data);
74        let has_special_option = block.has_key("special_option_category");
75
76        data.verify_exists(Item::Localization, key);
77        let loca = format!("{key}_name");
78        data.verify_exists_implied(Item::Localization, &loca, key);
79        let loca = format!("{key}_desc");
80        data.verify_exists_implied(Item::Localization, &loca, key);
81        let loca = format!("{key}_owner");
82        data.verify_exists_implied(Item::Localization, &loca, key);
83        let loca = format!("{key}_destination_selection");
84        data.verify_exists_implied(Item::Localization, &loca, key);
85        let loca = format!("{key}_selection_tooltip");
86        data.verify_exists_implied(Item::Localization, &loca, key);
87        let loca = format!("{key}_guest_help_text");
88        data.verify_exists_implied(Item::Localization, &loca, key);
89        let loca = format!("{key}_predicted_cost");
90        data.verify_exists_implied(Item::Localization, &loca, key);
91        let loca = format!("TRAVEL_NAME_FOR_{key}");
92        data.verify_exists_implied(Item::Localization, &loca, key);
93
94        if block.has_key("province_description") {
95            let mut sc = ScopeContext::new(Scopes::Province, key);
96            sc.define_name("host", Scopes::Character, key);
97            if has_special_option {
98                sc.define_name("special_option", Scopes::Flag, key);
99            }
100            vd.field_validated_sc("province_description", &mut sc, validate_desc);
101        } else {
102            let loca = format!("{key}_province_desc");
103            data.verify_exists_implied(Item::Localization, &loca, key);
104        }
105
106        if block.has_key("host_description") {
107            let mut sc = ScopeContext::new(Scopes::Character, key);
108            vd.field_validated_sc("host_description", &mut sc, validate_desc);
109        } else {
110            let loca = format!("{key}_host_desc");
111            data.verify_exists_implied(Item::Localization, &loca, key);
112        }
113
114        vd.field_integer("sort_order");
115        vd.field_bool("notify_player_can_join_activity");
116        vd.field_item("activity_group_type", Item::ActivityGroupType);
117
118        // undocumented
119        if block.has_key("guest_description") {
120            let mut sc = ScopeContext::new(Scopes::Character, key);
121            vd.field_validated_sc("guest_description", &mut sc, validate_desc);
122        } else {
123            let loca = format!("{key}_guest_desc");
124            data.verify_exists_implied(Item::Localization, &loca, key);
125        }
126
127        let ch_host_activity_sc = |key: &Token| {
128            let mut sc = ScopeContext::new(Scopes::Character, key);
129            sc.define_name("host", Scopes::Character, key);
130            sc.define_name("activity", Scopes::Activity, key);
131            sc
132        };
133        let ac_host_activity_sc = |key: &Token| {
134            let mut sc = ScopeContext::new(Scopes::Activity, key);
135            sc.define_name("host", Scopes::Character, key);
136            sc.define_name("activity", Scopes::Activity, key);
137            sc
138        };
139
140        if block.has_key("conclusion_description") {
141            let mut sc = ch_host_activity_sc(key);
142            vd.field_validated_sc("conclusion_description", &mut sc, validate_desc);
143        } else {
144            let loca = format!("{key}_conclusion_desc");
145            data.verify_exists_implied(Item::Localization, &loca, key);
146        }
147
148        let mut join_chance_sc = ScopeContext::new(Scopes::Character, key);
149        join_chance_sc.define_name("host", Scopes::Character, key);
150        join_chance_sc.define_name("minimal_travel_time", Scopes::Value, key);
151        // docs say activity_start_diff_days
152        join_chance_sc.define_name("estimated_arrival_diff_days", Scopes::Value, key);
153        join_chance_sc.define_list("special_guests", Scopes::Character, key);
154
155        let mut special_guests_sc = ScopeContext::new(Scopes::Character, key);
156        if has_special_option {
157            special_guests_sc.define_name("special_option", Scopes::Flag, key);
158        }
159        special_guests_sc.define_list("special_guests", Scopes::Character, key);
160
161        // TODO: figure out root type here
162        let mut max_guests_sc = ScopeContext::new(Scopes::all(), key);
163
164        if let Some(block) = block.get_field_block("special_guests") {
165            for (key, _) in block.iter_definitions() {
166                join_chance_sc.define_name(key.as_str(), Scopes::Character, key);
167                special_guests_sc.define_name(key.as_str(), Scopes::Character, key);
168            }
169        }
170        if let Some(block) = block.get_field_block("options") {
171            for (_, block) in block.iter_definitions() {
172                for (key, _) in block.iter_definitions() {
173                    join_chance_sc.define_name(key.as_str(), Scopes::Flag, key);
174                    max_guests_sc.define_name(key.as_str(), Scopes::Flag, key);
175                }
176            }
177        }
178
179        let mut sc = ScopeContext::new(Scopes::Character, key);
180
181        vd.field_trigger("is_shown", Tooltipped::No, &mut sc);
182        vd.field_trigger("can_plan", Tooltipped::Yes, &mut sc);
183        // TODO: cryptic comment in docs
184        // "any saved scope targets that are added to the creation data will also be available here
185        // when validating if an activity can start"
186        vd.field_trigger("can_start", Tooltipped::Yes, &mut sc);
187        vd.field_trigger("can_start_showing_failures_only", Tooltipped::FailuresOnly, &mut sc);
188
189        vd.field_bool("can_always_plan");
190        vd.field_script_value("num_pickable_phases", &mut sc);
191        vd.field_script_value("max_pickable_phases_per_province", &mut sc);
192        vd.field_validated_block_sc("wait_time_before_start", &mut sc, validate_duration);
193        vd.field_validated_block_sc("max_guest_arrival_delay_time", &mut sc, validate_duration);
194        vd.field_numeric("max_route_deviation_mult");
195
196        vd.field_validated_block_sc("cooldown", &mut sc, validate_duration);
197
198        vd.replaced_field("is_grand_activity", "activity_group_type");
199        vd.field_bool("is_single_location");
200        vd.field_choice("planner_type", &["province", "holder"]);
201
202        vd.field_script_value_no_breakdown("ai_will_do", &mut sc);
203        vd.field_integer("ai_check_interval");
204        vd.field_validated_key_block("ai_check_interval_by_tier", |key, b, data| {
205            let mut vd = Validator::new(b, data);
206            for tier in &["barony", "county", "duchy", "kingdom", "empire", "hegemony"] {
207                vd.req_field(tier);
208                vd.field_integer(tier);
209            }
210            if block.has_key("ai_check_interval") {
211                let msg = "must not have both `ai_check_interval` and `ai_check_interval_by_tier`";
212                warn(ErrorKey::Validation).msg(msg).loc(key).push();
213            }
214        });
215        vd.field_script_value_no_breakdown("ai_select_num_provinces", &mut sc);
216
217        vd.field_trigger_builder("is_valid", Tooltipped::No, ac_host_activity_sc);
218        vd.field_effect_builder("on_invalidated", Tooltipped::No, ac_host_activity_sc);
219        vd.field_effect_builder("on_host_death", Tooltipped::No, ac_host_activity_sc);
220
221        vd.field_choice("province_filter", PROVINCE_FILTERS);
222        vd.field_choice("ai_province_filter", PROVINCE_FILTERS);
223        let province_filter = block.get_field_value("province_filter");
224        let ai_province_filter = block.get_field_value("ai_province_filter");
225        if province_filter.is_some_and(|t| t.is("landed_title"))
226            || ai_province_filter.is_some_and(|t| t.is("landed_title"))
227        {
228            if let Some(province_filter) = province_filter {
229                if let Some(ai_province_filter) = ai_province_filter {
230                    if province_filter != ai_province_filter {
231                        let msg = "for `landed_title`, `province_filter` and `ai_province_filter` must be the same";
232                        err(ErrorKey::Validation).msg(msg).loc(ai_province_filter).push();
233                    }
234                }
235            }
236            vd.field_item("province_filter_target", Item::Title);
237        } else if province_filter.is_some_and(|t| t.is("geographical_region"))
238            || ai_province_filter.is_some_and(|t| t.is("geographical_region"))
239        {
240            if let Some(province_filter) = province_filter {
241                if let Some(ai_province_filter) = ai_province_filter {
242                    if province_filter != ai_province_filter {
243                        let msg = "for `geographical_region`, `province_filter` and `ai_province_filter` must be the same";
244                        err(ErrorKey::Validation).msg(msg).loc(ai_province_filter).push();
245                    }
246                }
247            }
248            vd.field_item("province_filter_target", Item::Region);
249        } else {
250            vd.ban_field(
251                "province_filter_target",
252                || "`landed_title` or `geographical_region` filters",
253            );
254        }
255
256        let mut sc = ScopeContext::new(Scopes::Province, key);
257        sc.define_name("host", Scopes::Character, key);
258        vd.field_script_value_no_breakdown("province_score", &mut sc);
259
260        if has_special_option {
261            sc.define_name("special_option", Scopes::Flag, key);
262        }
263        vd.field_trigger("is_location_valid", Tooltipped::No, &mut sc);
264
265        sc.define_name("score", Scopes::Value, key);
266        vd.field_script_value_no_breakdown("ai_will_select_province", &mut sc);
267
268        vd.field_integer("max_province_icons");
269
270        vd.field_item("special_option_category", Item::ActivityOptionCategory);
271        let special_option_category = block.get_field_value("special_option_category");
272
273        vd.field_validated_block("options", |block, data| {
274            let mut vd = Validator::new(block, data);
275            vd.unknown_block_fields(|key, block| {
276                // option categories
277                data.verify_exists(Item::Localization, key);
278                let mut vd = Validator::new(block, data);
279                let is_special_option =
280                    special_option_category.is_some_and(|special| key.is(special.as_str()));
281                if !is_special_option {
282                    let define = "NGameIcons|ACTIVITY_OPTION_CATEGORY_TEXTURE_PATH";
283                    data.verify_icon(define, key, ".dds");
284                }
285                vd.unknown_block_fields(|key, block| {
286                    validate_option(key, block, data, has_special_option, is_special_option);
287                });
288            });
289        });
290
291        vd.field_validated_block("phases", |block, data| {
292            let mut vd = Validator::new(block, data);
293            vd.unknown_block_fields(|key, block| {
294                validate_phase(key, block, data, has_special_option);
295            });
296        });
297
298        vd.field_validated_key_block("cost", |key, block, data| {
299            let mut sc = ScopeContext::new(Scopes::Character, key);
300            if has_special_option {
301                sc.define_name("special_option", Scopes::Flag, key);
302            }
303            sc.define_name("province", Scopes::Province, key);
304            sc.define_list("provinces", Scopes::Province, key);
305            validate_cost(block, data, &mut sc);
306        });
307
308        let mut sc = ScopeContext::new(Scopes::Character, key);
309        vd.field_validated_block_sc("ui_predicted_cost", &mut sc, validate_cost);
310
311        vd.field_script_value("max_guests", &mut max_guests_sc);
312        // TODO: check if this must be an integer
313        vd.field_script_value("reserved_guest_slots", &mut max_guests_sc);
314        vd.field_bool("allow_zero_guest_invites");
315
316        vd.field_validated_block("guest_invite_rules", |block, data| {
317            let mut vd = Validator::new(block, data);
318            vd.field_validated_block("rules", |block, data| {
319                validate_rules(block, data);
320            });
321            vd.field_validated_block("defaults", |block, data| {
322                // TODO: the rules items in defaults should not be in the rules block above
323                validate_rules(block, data);
324            });
325        });
326
327        let mut sc = ScopeContext::new(Scopes::Character, key);
328        sc.define_name("host", Scopes::Character, key);
329        if has_special_option {
330            sc.define_name("special_option", Scopes::Flag, key);
331        }
332        vd.field_trigger("can_be_activity_guest", Tooltipped::No, &mut sc);
333
334        vd.field_list("guest_subsets");
335
336        vd.field_validated_block("special_guests", |block, data| {
337            let mut vd = Validator::new(block, data);
338            vd.unknown_block_fields(|key, block| {
339                validate_special_guest(
340                    key,
341                    block,
342                    data,
343                    has_special_option,
344                    &mut special_guests_sc,
345                );
346            });
347        });
348
349        vd.field_validated_block("locales", |block, data| {
350            let mut vd = Validator::new(block, data);
351            // TODO: can we validate the key against anything?
352            vd.unknown_block_fields(|_, block| {
353                let mut vd = Validator::new(block, data);
354                vd.field_trigger_builder("is_available", Tooltipped::No, ac_host_activity_sc);
355                vd.field_list_items("locales", Item::ActivityLocale);
356            });
357        });
358
359        let mut sc = ch_host_activity_sc(key);
360        vd.field_validated_block_sc("locale_cooldown", &mut sc, validate_duration);
361        vd.field_validated_block_sc("auto_select_locale_cooldown", &mut sc, validate_duration);
362        for field in &[
363            "on_enter_travel_state",
364            "on_enter_passive_state",
365            "on_enter_active_state",
366            "on_leave_travel_state",
367            "on_leave_passive_state",
368            "on_leave_active_state",
369            "on_travel_state_pulse",
370            "on_passive_state_pulse",
371            "on_active_state_pulse",
372            "on_complete",
373        ] {
374            vd.field_effect_builder(field, Tooltipped::No, ch_host_activity_sc);
375        }
376        vd.field_effect_builder("on_start", Tooltipped::No, |key| {
377            let mut sc = ch_host_activity_sc(key);
378            sc.change_root(Scopes::Activity, key);
379            sc
380        });
381
382        sc.change_root(Scopes::None, key);
383        vd.field_validated_block_sc("early_locale_opening_duration", &mut sc, validate_duration);
384
385        vd.field_bool("open_invite");
386
387        vd.field_validated_block("host_intents", |block, data| {
388            let mut vd = Validator::new(block, data);
389            vd.field_list_items("intents", Item::ActivityIntent);
390            vd.field_item("default", Item::ActivityIntent);
391            vd.field_list_items("player_defaults", Item::ActivityIntent);
392        });
393        vd.field_validated_block("guest_intents", |block, data| {
394            let mut vd = Validator::new(block, data);
395            vd.field_list_items("intents", Item::ActivityIntent);
396            vd.field_item("default", Item::ActivityIntent);
397            vd.field_list_items("player_defaults", Item::ActivityIntent);
398        });
399
400        vd.field_validated_block_sc(
401            "guest_join_chance",
402            &mut join_chance_sc,
403            validate_modifiers_with_base,
404        );
405
406        vd.field_validated_block("activity_window_widgets", |block, data| {
407            let mut vd = Validator::new(block, data);
408            vd.unknown_value_fields(|key, _| {
409                let pathname = format!("gui/activity_window_widgets/{key}.gui");
410                data.verify_exists_implied(Item::File, &pathname, key);
411                // TODO: what is value?
412            });
413        });
414
415        vd.field_validated_block("activity_planner_widgets", |block, data| {
416            let mut vd = Validator::new(block, data);
417            vd.unknown_value_fields(|key, _| {
418                let pathname = format!("gui/activity_window_widgets/{key}.gui");
419                data.verify_exists_implied(Item::File, &pathname, key);
420                // TODO: what is value?
421            });
422        });
423
424        let mut sc = ScopeContext::new(Scopes::Activity, key);
425        sc.define_name("host", Scopes::Character, key);
426        sc.define_name("activity", Scopes::Activity, key);
427        vd.multi_field_validated("map_entity", |bv, data| match bv {
428            BV::Value(token) => data.verify_exists(Item::Entity, token),
429            BV::Block(block) => {
430                let mut vd = Validator::new(block, data);
431                vd.field_trigger("trigger", Tooltipped::No, &mut sc);
432                vd.field_item("reference", Item::Entity);
433            }
434        });
435        vd.multi_field_validated_block("background", |block, data| {
436            let mut vd = Validator::new(block, data);
437            vd.field_trigger("trigger", Tooltipped::No, &mut sc);
438            vd.field_item("texture", Item::File);
439            vd.field_item("environment", Item::PortraitEnvironment);
440            vd.field_item("ambience", Item::Sound);
441            vd.field_item("music", Item::Music);
442        });
443        vd.multi_field_validated_block("locale_background", |block, data| {
444            let mut vd = Validator::new(block, data);
445            vd.field_trigger("trigger", Tooltipped::No, &mut sc);
446            vd.field_item("texture", Item::File);
447            vd.field_item("environment", Item::PortraitEnvironment);
448            vd.field_item("ambience", Item::Sound);
449            vd.field_item("music", Item::Music);
450        });
451        vd.field_validated_block("window_characters", |block, data| {
452            let mut vd = Validator::new(block, data);
453            vd.unknown_block_fields(|key, block| {
454                validate_window_characters(key, block, data);
455            });
456        });
457        vd.field_validated_key_block("travel_entourage_selection", |key, block, data| {
458            validate_tes(key, block, data, has_special_option);
459        });
460
461        // undocumented
462        vd.field_validated_block("pulse_actions", |block, data| {
463            let mut vd = Validator::new(block, data);
464            vd.field_list_items("entries", Item::ActivityPulseAction);
465            vd.field_numeric_range("chance_of_no_event", 0.0..=100.0);
466        });
467    }
468}
469
470fn validate_rules(block: &Block, data: &Everything) {
471    let mut vd = Validator::new(block, data);
472    vd.integer_keys(|_, bv| {
473        if let Some(token) = bv.expect_value() {
474            data.verify_exists(Item::GuestInviteRule, token);
475        }
476    });
477}
478
479pub fn validate_tes(key: &Token, block: &Block, data: &Everything, has_special_option: bool) {
480    let mut sc = ScopeContext::new(Scopes::Character, key);
481    sc.define_name("host", Scopes::Character, key);
482    sc.define_name("owner", Scopes::Character, key);
483    // TODO: is this available when called from the travel folder?
484    sc.define_list("special_guests", Scopes::Character, key);
485    if has_special_option {
486        sc.define_name("special_option", Scopes::Flag, key);
487    }
488    let mut vd = Validator::new(block, data);
489    vd.field_script_value("weight", &mut sc);
490    let mut sc = ScopeContext::new(Scopes::None, key);
491    vd.field_script_value("max", &mut sc);
492    vd.field_script_value("ai_max", &mut sc);
493    vd.field_integer("invite_rule_order");
494}
495
496fn validate_window_characters(key: &Token, block: &Block, data: &Everything) {
497    let mut vd = Validator::new(block, data);
498    let loca = format!("activity_window_character_{key}");
499    data.verify_exists_implied(Item::Localization, &loca, key);
500    vd.field_item("camera", Item::PortraitCamera);
501    vd.field_effect_builder("effect", Tooltipped::No, |key| {
502        let mut sc = ScopeContext::new(Scopes::Activity, key);
503        sc.define_name("activity", Scopes::Activity, key);
504        sc.define_name("host", Scopes::Character, key);
505        sc.define_name("player", Scopes::Character, key);
506        sc.define_list("characters", Scopes::Character, key);
507        sc
508    });
509
510    let mut sc = ScopeContext::new(Scopes::Activity, key);
511    sc.define_name("activity", Scopes::Activity, key);
512    sc.define_name("host", Scopes::Character, key);
513    sc.define_name("player", Scopes::Character, key);
514    sc.define_name("character", Scopes::Character, key);
515    vd.field_validated_sc("scripted_animation", &mut sc, validate_scripted_animation);
516    vd.field_item("animation", Item::PortraitAnimation);
517}
518
519fn validate_phase(key: &Token, block: &Block, data: &Everything, has_special_option: bool) {
520    data.verify_exists(Item::Localization, key);
521    let loca = format!("{key}_desc");
522    data.verify_exists_implied(Item::Localization, &loca, key);
523
524    let define = "NGameIcons|ACTIVITY_PHASE_ICON_PATH";
525    data.verify_icon(define, key, ".dds");
526
527    let mut vd = Validator::new(block, data);
528    vd.field_bool("is_predefined");
529    let mut sc = ScopeContext::new(Scopes::None, key);
530    vd.field_script_value("number_of_picks", &mut sc);
531    // TODO: "you should have unique order nr for your phases, if you have more than one phase"
532    vd.field_integer("order");
533    vd.field_choice(
534        "location_source",
535        &["pickable", "first_picked_phase", "last_picked_phase", "scripted"],
536    );
537    if block.field_value_is("location_source", "scripted") {
538        vd.field_effect_builder("select_scripted_location", Tooltipped::No, |key| {
539            let mut sc = ScopeContext::new(Scopes::Character, key);
540            if has_special_option {
541                sc.define_name("special_option", Scopes::Flag, key);
542            }
543            sc
544        });
545    } else {
546        vd.ban_field("select_scripted_location", || "location_source = scripted");
547    }
548    vd.field_script_value_no_breakdown_builder("ai_will_do", |key| {
549        let mut sc = ScopeContext::new(Scopes::Character, key);
550        sc.define_name("province", Scopes::Province, key);
551        if has_special_option {
552            sc.define_name("special_option", Scopes::Flag, key);
553        }
554        sc
555    });
556
557    let mut sc = ScopeContext::new(Scopes::Character, key);
558    sc.define_name("province", Scopes::Province, key);
559    if has_special_option {
560        sc.define_name("special_option", Scopes::Flag, key);
561    }
562    vd.field_trigger("is_shown", Tooltipped::No, &mut sc);
563    vd.field_trigger("can_pick", Tooltipped::Yes, &mut sc);
564    vd.field_trigger_builder("is_valid", Tooltipped::No, |key| {
565        let mut sc = ScopeContext::new(Scopes::Activity, key);
566        sc.define_name("host", Scopes::Character, key);
567        sc.define_name("province", Scopes::Province, key);
568        sc
569    });
570
571    let mut sc = ScopeContext::new(Scopes::Character, key);
572    sc.define_name("activity", Scopes::Activity, key);
573    sc.define_name("host", Scopes::Character, key);
574    vd.field_effect("on_enter_phase", Tooltipped::No, &mut sc);
575    vd.field_effect("on_phase_active", Tooltipped::No, &mut sc);
576    vd.field_effect("on_end", Tooltipped::No, &mut sc);
577    vd.field_effect("on_monthly_pulse", Tooltipped::No, &mut sc);
578    vd.field_effect("on_weekly_pulse", Tooltipped::No, &mut sc);
579    sc.define_name("province", Scopes::Province, key);
580    vd.field_effect("on_invalidated", Tooltipped::No, &mut sc);
581
582    let mut sc = ScopeContext::new(Scopes::Character, key);
583    sc.define_name("province", Scopes::Province, key);
584    sc.define_name("previous_province", Scopes::Province, key);
585    if has_special_option {
586        sc.define_name("special_option", Scopes::Flag, key);
587    }
588    vd.field_validated_block_sc("cost", &mut sc, validate_cost);
589}
590
591fn validate_special_guest(
592    key: &Token,
593    block: &Block,
594    data: &Everything,
595    has_special_option: bool,
596    special_guests_sc: &mut ScopeContext,
597) {
598    let mut vd = Validator::new(block, data);
599    data.verify_exists(Item::Localization, key);
600    let loca = format!("{key}_desc");
601    data.verify_exists_implied(Item::Localization, &loca, key);
602    let loca = format!("{key}_for_host");
603    data.verify_exists_implied(Item::Localization, &loca, key);
604    let loca = format!("{key}_desc_for_host");
605    data.verify_exists_implied(Item::Localization, &loca, key);
606    let mut sc = ScopeContext::new(Scopes::Character, key);
607    if has_special_option {
608        sc.define_name("special_option", Scopes::Flag, key);
609    }
610    vd.field_trigger("is_shown", Tooltipped::No, &mut sc);
611    vd.field_bool("is_required");
612
613    vd.field_effect("select_character", Tooltipped::No, special_guests_sc);
614
615    special_guests_sc.define_name("host", Scopes::Character, key);
616    vd.field_trigger("can_pick", Tooltipped::No, special_guests_sc);
617    vd.field_effect("on_invite", Tooltipped::No, special_guests_sc);
618
619    vd.field_script_value_no_breakdown_rooted("ai_will_do", Scopes::Character);
620}
621
622#[derive(Clone, Debug)]
623pub struct ActivityLocale {}
624
625inventory::submit! {
626    ItemLoader::Normal(GameFlags::Ck3, Item::ActivityLocale, ActivityLocale::add)
627}
628
629impl ActivityLocale {
630    pub fn add(db: &mut Db, key: Token, block: Block) {
631        db.add(Item::ActivityLocale, key, block, Box::new(Self {}));
632    }
633}
634
635impl DbKind for ActivityLocale {
636    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
637        let mut vd = Validator::new(block, data);
638
639        data.verify_exists(Item::Localization, key);
640        let loca = format!("{key}_desc");
641        data.verify_exists_implied(Item::Localization, &loca, key);
642
643        let define = "NGameIcons|ACTIVITY_LOCALE_ICON_PATH";
644        data.verify_icon(define, key, ".dds");
645
646        let mut sc = ScopeContext::new(Scopes::Character, key);
647        sc.define_name("host", Scopes::Character, key);
648        sc.define_name("activity", Scopes::Activity, key);
649        vd.field_trigger("is_available", Tooltipped::No, &mut sc);
650        vd.field_script_value("chance", &mut sc);
651        vd.field_effect("on_enter_locale", Tooltipped::No, &mut sc);
652        vd.field_script_value_no_breakdown("ai_will_do", &mut sc);
653        vd.field_validated_block_sc("cooldown", &mut sc, validate_duration);
654
655        vd.multi_field_validated_key("visuals", validate_visuals);
656    }
657}
658
659fn validate_visuals(key: &Token, bv: &BV, data: &Everything) {
660    fn validate_visual(visual: &Token, data: &Everything) {
661        let pathname = format!("/gui/activity_locale_widgets/{visual}.gui");
662        data.verify_exists_implied(Item::File, &pathname, visual);
663    }
664
665    match bv {
666        BV::Value(token) => validate_visual(token, data),
667        BV::Block(block) => {
668            let mut vd = Validator::new(block, data);
669            let mut sc = ScopeContext::new(Scopes::Activity, key);
670            sc.define_name("activity", Scopes::Activity, key);
671            sc.define_name("host", Scopes::Character, key);
672            vd.field_trigger("trigger", Tooltipped::No, &mut sc);
673            vd.req_field("reference");
674            if let Some(token) = vd.field_value("reference") {
675                validate_visual(token, data);
676            }
677        }
678    }
679}
680
681#[derive(Clone, Debug)]
682pub struct GuestInviteRule {}
683
684inventory::submit! {
685    ItemLoader::Normal(GameFlags::Ck3, Item::GuestInviteRule, GuestInviteRule::add)
686}
687
688impl GuestInviteRule {
689    pub fn add(db: &mut Db, key: Token, block: Block) {
690        db.add(Item::GuestInviteRule, key, block, Box::new(Self {}));
691    }
692}
693
694impl DbKind for GuestInviteRule {
695    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
696        let mut vd = Validator::new(block, data);
697        data.verify_exists(Item::Localization, key);
698
699        vd.field_effect_builder("effect", Tooltipped::No, |key| {
700            let mut sc = ScopeContext::new(Scopes::Character, key);
701            // TODO: scope:special_option
702            // TODO: option category flags
703            sc.define_list("characters", Scopes::Character, key);
704            sc
705        });
706    }
707}
708
709#[derive(Clone, Debug)]
710pub struct ActivityPulseAction {}
711
712inventory::submit! {
713    ItemLoader::Normal(GameFlags::Ck3, Item::ActivityPulseAction, ActivityPulseAction::add)
714}
715
716impl ActivityPulseAction {
717    pub fn add(db: &mut Db, key: Token, block: Block) {
718        db.add(Item::ActivityPulseAction, key, block, Box::new(Self {}));
719    }
720}
721
722impl DbKind for ActivityPulseAction {
723    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
724        let mut vd = Validator::new(block, data);
725
726        let loca = format!("{key}_title");
727        data.verify_exists_implied(Item::Localization, &loca, key);
728
729        let define = "NGameIcons|ACTIVITY_PULSE_ACTION_ICON_PATH";
730        let icon = vd.field_value("icon").unwrap_or(key);
731        data.verify_icon(define, icon, ".dds");
732
733        let mut sc = ScopeContext::new(Scopes::Activity, key);
734        sc.define_name("activity", Scopes::Activity, key);
735        sc.define_name("host", Scopes::Character, key);
736        sc.define_name("province", Scopes::Province, key);
737        vd.field_trigger("is_valid", Tooltipped::No, &mut sc);
738        vd.field_script_value("weight", &mut sc);
739        vd.field_effect("effect", Tooltipped::No, &mut sc);
740    }
741}
742
743#[derive(Clone, Debug)]
744pub struct ActivityIntent {}
745
746inventory::submit! {
747    ItemLoader::Normal(GameFlags::Ck3, Item::ActivityIntent, ActivityIntent::add)
748}
749
750impl ActivityIntent {
751    pub fn add(db: &mut Db, key: Token, block: Block) {
752        db.add(Item::ActivityIntent, key, block, Box::new(Self {}));
753    }
754}
755
756impl DbKind for ActivityIntent {
757    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
758        let mut vd = Validator::new(block, data);
759
760        data.verify_exists(Item::Localization, key);
761        let loca = format!("{key}_desc");
762        data.verify_exists_implied(Item::Localization, &loca, key);
763
764        let define = "NGameIcons|ACTIVITY_INTENTS_ICON_PATH";
765        let icon = vd.field_value("icon").unwrap_or(key);
766        data.verify_icon(define, icon, ".dds");
767
768        vd.field_bool("auto_complete");
769
770        for field in &["is_shown", "is_valid"] {
771            vd.field_trigger_builder(field, Tooltipped::No, |key| {
772                let mut sc = ScopeContext::new(Scopes::Character, key);
773                sc.define_name("magnificence", Scopes::Value, key);
774                sc.define_name("special_option", Scopes::Flag, key);
775                sc
776            });
777        }
778
779        vd.field_trigger_builder("is_target_valid", Tooltipped::No, |key| {
780            let mut sc = ScopeContext::new(Scopes::Character, key);
781            sc.define_name("target", Scopes::Character, key);
782            sc.define_name("special_option", Scopes::Flag, key);
783            sc
784        });
785
786        vd.field_effect_rooted("on_invalidated", Tooltipped::No, Scopes::Character);
787        vd.field_effect_builder("on_target_invalidated", Tooltipped::No, |key| {
788            let mut sc = ScopeContext::new(Scopes::Character, key);
789            sc.define_name("target", Scopes::Character, key);
790            sc
791        });
792
793        vd.field_script_value_no_breakdown_builder("ai_will_do", |key| {
794            let mut sc = ScopeContext::new(Scopes::Character, key);
795            sc.define_name("activity", Scopes::Activity, key);
796            sc
797        });
798
799        vd.multi_field_validated_block("ai_targets", validate_ai_targets);
800        vd.field_validated_block("ai_target_quick_trigger", validate_quick_trigger);
801
802        vd.field_script_value_no_breakdown_builder("ai_target_score", |key| {
803            let mut sc = ScopeContext::new(Scopes::Character, key);
804            sc.define_name("target", Scopes::Character, key);
805            sc.define_name("special_option", Scopes::Flag, key);
806            sc
807        });
808
809        let mut sc = ScopeContext::new(Scopes::Character, key);
810        vd.field_validated_sc("scripted_animation", &mut sc, validate_scripted_animation);
811    }
812}
813
814fn validate_option(
815    key: &Token,
816    block: &Block,
817    data: &Everything,
818    has_special_option: bool,
819    is_special_option: bool,
820) {
821    let mut vd = Validator::new(block, data);
822    data.verify_exists(Item::Localization, key);
823    let loca = format!("{key}_desc");
824    data.verify_exists_implied(Item::Localization, &loca, key);
825    if is_special_option {
826        data.verify_icon("NGameIcons|ACTIVITY_OPTION_TEXTURE_PATH", key, ".dds");
827        data.verify_icon("NGameIcons|ACTIVITY_OPTION_ICON_PATH", key, "_icon.dds");
828    }
829    let mut sc = ScopeContext::new(Scopes::Character, key);
830    if has_special_option {
831        sc.define_name("special_option", Scopes::Flag, key);
832    }
833    vd.field_trigger("is_shown", Tooltipped::No, &mut sc);
834    vd.field_trigger("is_valid", Tooltipped::Yes, &mut sc);
835    vd.field_script_value_no_breakdown("ai_will_do", &mut sc);
836
837    vd.field_effect_builder("on_start", Tooltipped::No, |key| {
838        let mut sc = ScopeContext::new(Scopes::Activity, key);
839        sc.define_name("activity", Scopes::Activity, key);
840        sc.define_name("host", Scopes::Character, key);
841        sc
842    });
843
844    vd.field_validated("default", |bv, data| {
845        match bv {
846            BV::Value(token) => {
847                if !token.is("yes") {
848                    let msg = "expected `default = yes`";
849                    warn(ErrorKey::Validation).msg(msg).loc(token).push();
850                }
851            }
852            BV::Block(block) => {
853                // TODO: what is the scope context?
854                let mut sc = ScopeContext::new(Scopes::Character, key);
855                validate_trigger(block, data, &mut sc, Tooltipped::No);
856            }
857        }
858    });
859
860    // TODO: check that these intents and phases belong to this activity
861    vd.field_list_items("blocked_intents", Item::ActivityIntent);
862    vd.field_list_items("blocked_phases", Item::ActivityPhase);
863
864    let mut sc = ScopeContext::new(Scopes::Character, key);
865    vd.field_validated_block_sc("cost", &mut sc, validate_cost);
866
867    vd.field_validated_key_block("travel_entourage_selection", |key, block, data| {
868        validate_tes(key, block, data, has_special_option);
869    });
870}
871
872#[derive(Clone, Debug)]
873pub struct ActivityGroupType {}
874
875inventory::submit! {
876    ItemLoader::Normal(GameFlags::Ck3, Item::ActivityGroupType, ActivityGroupType::add)
877}
878
879impl ActivityGroupType {
880    pub fn add(db: &mut Db, key: Token, block: Block) {
881        db.add(Item::ActivityGroupType, key, block, Box::new(Self {}));
882    }
883}
884
885impl DbKind for ActivityGroupType {
886    fn validate(&self, _key: &Token, block: &Block, data: &Everything) {
887        let mut vd = Validator::new(block, data);
888
889        vd.field_integer("sort_order");
890        vd.field_list("gui_tags");
891    }
892}