tiger_lib/ck3/data/
interactions.rs

1use crate::block::{BV, Block};
2use crate::ck3::validate::{
3    validate_ai_targets, validate_cost, validate_quick_trigger, validate_theme_background,
4};
5use crate::context::ScopeContext;
6use crate::db::{Db, DbKind};
7use crate::desc::validate_desc;
8use crate::effect::validate_effect;
9use crate::everything::Everything;
10use crate::game::GameFlags;
11use crate::item::{Item, ItemLoader};
12use crate::report::{ErrorKey, warn};
13use crate::scopes::Scopes;
14use crate::token::Token;
15use crate::tooltipped::Tooltipped;
16use crate::trigger::{validate_target, validate_trigger};
17use crate::validate::{validate_ai_chance, validate_duration, validate_modifiers_with_base};
18use crate::validator::Validator;
19
20#[derive(Clone, Debug)]
21pub struct CharacterInteraction {}
22
23inventory::submit! {
24    ItemLoader::Normal(GameFlags::Ck3, Item::CharacterInteraction, CharacterInteraction::add)
25}
26
27impl CharacterInteraction {
28    pub fn add(db: &mut Db, key: Token, block: Block) {
29        db.add(Item::CharacterInteraction, key, block, Box::new(Self {}));
30    }
31}
32
33impl DbKind for CharacterInteraction {
34    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
35        let mut vd = Validator::new(block, data);
36
37        // You're expected to use scope:actor and scope:recipient instead of root
38        let mut sc = ScopeContext::new(Scopes::None, key);
39        sc.define_name("actor", Scopes::Character, key);
40        sc.define_name("recipient", Scopes::Character, key);
41        sc.define_name("hook", Scopes::Bool, key);
42        // TODO: figure out when these are available
43        sc.define_name("secondary_actor", Scopes::Character, key);
44        sc.define_name("secondary_recipient", Scopes::Character, key);
45        sc.define_name("intermediary", Scopes::Character, key);
46        // TODO: figure out if there's a better way than exhaustively matching on "interface" and "special_interaction"
47        if let Some(target_type) = block.get_field_value("target_type") {
48            if target_type.is("artifact") {
49                sc.define_name("target", Scopes::Artifact, target_type);
50            } else if target_type.is("title") {
51                sc.define_name("target", Scopes::LandedTitle, target_type);
52                sc.define_name("landed_title", Scopes::LandedTitle, target_type);
53            }
54        } else if let Some(interface) = block.get_field_value("interface") {
55            if interface.is("interfere_in_war") || interface.is("call_ally") {
56                sc.define_name("target", Scopes::War, interface);
57            } else if interface.is("blackmail") {
58                sc.define_name("target", Scopes::Secret, interface);
59            } else if interface.is("council_task_interaction") {
60                sc.define_name("target", Scopes::CouncilTask, interface);
61            } else if interface.is("create_claimant_faction_against") {
62                sc.define_name("landed_title", Scopes::LandedTitle, interface);
63            } else if interface.is("modify_vassal_contract") {
64                sc.define_list("changed_obligations", Scopes::VassalObligationLevel, interface);
65            }
66        } else if let Some(special) = block.get_field_value("special_interaction") {
67            if special.is("invite_to_council_interaction") {
68                sc.define_name("target", Scopes::CouncilTask, special);
69            } else if special.is("end_war_attacker_victory_interaction")
70                || special.is("end_war_attacker_defeat_interaction")
71                || special.is("end_war_white_peace_interaction")
72            {
73                sc.define_name("war", Scopes::War, special);
74            } else if special.is("remove_scheme_interaction")
75                || special.is("invite_to_scheme_interaction")
76            {
77                sc.define_name("scheme", Scopes::Scheme, special);
78            }
79        }
80        for block in block.get_field_blocks("send_option") {
81            if let Some(token) = block.get_field_value("flag") {
82                sc.define_name(token.as_str(), Scopes::Bool, token);
83            }
84        }
85
86        vd.field_validated_block("localization_values", |block, data| {
87            let mut vd = Validator::new(block, data);
88            vd.unknown_value_fields(|key, value| {
89                let scopes = validate_target(value, data, &mut sc, Scopes::all());
90                sc.define_name(key.as_str(), scopes, value);
91            });
92        });
93
94        // Validate this early, to update the saved scopes in `sc`
95        // TODO: figure out when exactly `redirect` is run
96        vd.field_effect("redirect", Tooltipped::No, &mut sc);
97
98        vd.field_bool("ai_instant_response");
99        // Let ai_set_target set scope:target if it wants
100        vd.field_effect("ai_set_target", Tooltipped::No, &mut sc);
101        vd.multi_field_validated_block("ai_targets", validate_ai_targets);
102        vd.field_validated_block("ai_target_quick_trigger", validate_quick_trigger);
103
104        vd.field_numeric("interface_priority");
105        vd.field_bool("common_interaction");
106        if !block.get_field_bool("hidden").unwrap_or(false) {
107            vd.req_field("category");
108        }
109        vd.field_item("category", Item::CharacterInteractionCategory);
110
111        if !vd.multi_field_validated_sc("icon", &mut sc, validate_icon) {
112            data.mark_used_icon("NGameIcons|CHARACTER_INTERACTION_ICON_PATH", key, ".dds");
113        }
114        vd.field_icon("alert_icon", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");
115        vd.field_icon("icon_small", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");
116
117        vd.field_validated_key("override_background", |key, bv, data| {
118            let mut sc = ScopeContext::new(Scopes::Character, key);
119            validate_theme_background(bv, data, &mut sc);
120        });
121
122        vd.field_trigger("is_highlighted", Tooltipped::No, &mut sc.clone());
123        vd.field_validated_sc("highlighted_reason", &mut sc, validate_desc);
124
125        vd.field_value("special_interaction");
126        vd.field_value("special_ai_interaction");
127
128        vd.field_bool("ai_intermediary_maybe");
129        vd.field_bool("ai_maybe");
130        vd.field_integer("ai_min_reply_days");
131        vd.field_integer("ai_max_reply_days");
132
133        vd.field_value("interface"); // TODO
134        vd.field_list_choice(
135            "custom_character_sort",
136            &["candidate_score", "governor_efficiency", "obedience", "merit"],
137        );
138        vd.field_item("scheme", Item::Scheme);
139        vd.field_bool("popup_on_receive");
140        vd.field_bool("pause_on_receive");
141        vd.field_bool("force_notification");
142        vd.field_bool("ai_accept_negotiation");
143        vd.field_bool("secondary_scopes_optional");
144
145        vd.field_bool("hidden");
146
147        vd.field_validated_sc("use_diplomatic_range", &mut sc.clone(), validate_bool_or_trigger);
148        vd.field_bool("can_send_despite_rejection");
149        vd.field_bool("ignores_pending_interaction_block");
150
151        // The cooldowns seem to be in actor scope
152        vd.field_validated_block_rerooted("cooldown", &sc, Scopes::Character, validate_duration);
153        vd.field_validated_block_rerooted(
154            "cooldown_against_recipient",
155            &sc,
156            Scopes::Character,
157            validate_duration,
158        );
159        // undocumented, but used in marriage interaction
160        vd.field_validated_block_rerooted(
161            "recipient_recieve_cooldown",
162            &sc,
163            Scopes::Character,
164            validate_duration,
165        );
166        vd.field_validated_block_rerooted(
167            "category_cooldown",
168            &sc,
169            Scopes::Character,
170            validate_duration,
171        );
172        vd.field_validated_block_rerooted(
173            "category_cooldown_against_recipient",
174            &sc,
175            Scopes::Character,
176            validate_duration,
177        );
178
179        vd.field_validated_block_rerooted(
180            "ignore_recipient_recieve_cooldown",
181            &sc,
182            Scopes::Character,
183            |block, data, sc| {
184                validate_trigger(block, data, sc, Tooltipped::No);
185            },
186        );
187
188        // TODO: The ai_ name check is a heuristic. It would be better to check if the
189        // is_shown trigger requires scope:actor to be is_ai = yes. But that's a long way off.
190        if !key.as_str().starts_with("ai_") && !block.get_field_bool("hidden").unwrap_or(false) {
191            if !block.has_key("name") {
192                data.verify_exists(Item::Localization, key);
193            }
194            if !block.has_key("desc") {
195                data.localization.suggest(&format!("{key}_desc"), key);
196            }
197        }
198        vd.field_validated_value("extra_icon", |k, mut vd| {
199            vd.item(Item::File);
200            let loca = format!("{key}_extra_icon");
201            data.verify_exists_implied(Item::Localization, &loca, k);
202        });
203        vd.field_trigger("should_use_extra_icon", Tooltipped::No, &mut sc.clone());
204        vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
205        vd.field_trigger("is_valid", Tooltipped::Yes, &mut sc.clone());
206        vd.field_trigger(
207            "is_valid_showing_failures_only",
208            Tooltipped::FailuresOnly,
209            &mut sc.clone(),
210        );
211        vd.field_trigger(
212            "has_valid_target_showing_failures_only",
213            Tooltipped::FailuresOnly,
214            &mut sc.clone(),
215        );
216        vd.field_trigger("has_valid_target", Tooltipped::Yes, &mut sc.clone());
217
218        vd.field_trigger("can_send", Tooltipped::Yes, &mut sc.clone());
219        vd.field_trigger("can_be_blocked", Tooltipped::Yes, &mut sc.clone());
220
221        vd.field_validated_key_block("populate_actor_list", |k, block, data| {
222            // TODO: this loca check and the one for recipient_secondary have a lot of false positives in vanilla.
223            // Not sure why.
224            let loca = format!("actor_secondary_{key}");
225            data.verify_exists_implied(Item::Localization, &loca, k);
226            validate_effect(block, data, &mut sc.clone(), Tooltipped::No);
227        });
228        vd.field_validated_key_block("populate_recipient_list", |k, block, data| {
229            let loca = format!("recipient_secondary_{key}");
230            data.verify_exists_implied(Item::Localization, &loca, k);
231            validate_effect(block, data, &mut sc.clone(), Tooltipped::No);
232        });
233
234        vd.multi_field_validated_block("send_option", |b, data| {
235            let mut vd = Validator::new(b, data);
236            vd.req_field("flag");
237            // If localization field is not set, then flag is used as the localization
238            if vd.field_localization("localization", &mut sc) {
239                vd.field_value("flag");
240            } else {
241                vd.field_localization("flag", &mut sc);
242            }
243            vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
244            vd.field_trigger("is_valid", Tooltipped::FailuresOnly, &mut sc.clone());
245            vd.field_trigger("starts_enabled", Tooltipped::No, &mut sc.clone());
246            vd.field_trigger("can_be_changed", Tooltipped::No, &mut sc.clone());
247            vd.field_validated_sc("current_description", &mut sc.clone(), validate_desc);
248            vd.field_bool("can_invalidate_interaction");
249
250            // undocumented
251
252            vd.field_script_value("scheme_preview_success_chance", &mut sc);
253            vd.field_script_value("scheme_preview_success_chance_max", &mut sc);
254            vd.field_script_value("scheme_preview_speed", &mut sc);
255        });
256
257        vd.field_bool("send_options_exclusive");
258        vd.field_effect("on_send", Tooltipped::Yes, &mut sc);
259        vd.field_effect("on_accept", Tooltipped::Yes, &mut sc);
260        vd.field_effect("on_decline", Tooltipped::Yes, &mut sc);
261        vd.field_effect("on_blocked_effect", Tooltipped::No, &mut sc);
262        vd.field_effect("pre_auto_accept", Tooltipped::No, &mut sc);
263        vd.field_effect("on_auto_accept", Tooltipped::Yes, &mut sc);
264        vd.field_effect("on_intermediary_accept", Tooltipped::Yes, &mut sc);
265        vd.field_effect("on_intermediary_decline", Tooltipped::Yes, &mut sc);
266
267        vd.field_integer("ai_frequency"); // months
268        vd.field_validated_key_block("ai_frequency_by_tier", |key, b, data| {
269            let mut vd = Validator::new(b, data);
270            for tier in &["barony", "county", "duchy", "kingdom", "empire", "hegemony"] {
271                vd.req_field(tier);
272                vd.field_integer(tier);
273            }
274            if block.has_key("ai_frequency") {
275                let msg = "must not have both `ai_frequency` and `ai_frequency_by_tier`";
276                warn(ErrorKey::Validation).msg(msg).loc(key).push();
277            }
278        });
279
280        // This is in character scope with no other named scopes builtin
281        vd.field_trigger_rooted("ai_potential", Tooltipped::Yes, Scopes::Character);
282        if let Some(token) = block.get_key("ai_potential") {
283            if block.get_field_integer("ai_frequency").unwrap_or(0) == 0
284                && !key.is("revoke_title_interaction")
285                && !block.has_key("ai_frequency_by_tier")
286            {
287                let msg = "`ai_potential` will not be used if `ai_frequency` is 0";
288                warn(ErrorKey::Unneeded).msg(msg).loc(token).push();
289            }
290            let msg = "should use `is_available` instead of `ai_potential`";
291            warn(ErrorKey::Deprecated).msg(msg).loc(token).push();
292        }
293        vd.field_trigger_rooted("is_available", Tooltipped::Yes, Scopes::Character);
294        vd.field_validated_sc("ai_intermediary_accept", &mut sc.clone(), validate_ai_chance);
295
296        // These seem to be in character scope
297        vd.field_validated_block_rerooted(
298            "ai_accept",
299            &sc,
300            Scopes::Character,
301            validate_modifiers_with_base,
302        );
303        vd.field_validated_block_rerooted(
304            "ai_will_do",
305            &sc,
306            Scopes::Character,
307            validate_modifiers_with_base,
308        );
309
310        vd.field_validated_sc("name", &mut sc.clone(), validate_desc);
311        vd.field_validated_sc("desc", &mut sc.clone(), validate_desc);
312        vd.field_choice("greeting", &["negative", "positive"]);
313        vd.field_validated_sc("prompt", &mut sc.clone(), validate_desc);
314        vd.field_validated_sc("intermediary_notification_text", &mut sc.clone(), validate_desc);
315        vd.field_validated_sc("notification_text", &mut sc.clone(), validate_desc);
316        vd.field_validated_sc("on_decline_summary", &mut sc.clone(), validate_desc);
317        vd.field_localization("answer_block_key", &mut sc);
318        vd.field_localization("answer_accept_key", &mut sc);
319        vd.field_localization("answer_reject_key", &mut sc);
320        vd.field_localization("answer_acknowledge_key", &mut sc);
321        vd.field_localization("options_heading", &mut sc);
322        vd.field_localization("pre_answer_maybe_breakdown_key", &mut sc);
323        vd.field_localization("pre_answer_no_breakdown_key", &mut sc);
324        vd.field_localization("pre_answer_yes_breakdown_key", &mut sc);
325        vd.field_localization("pre_answer_maybe_key", &mut sc);
326        vd.field_localization("pre_answer_no_key", &mut sc);
327        vd.field_localization("pre_answer_yes_key", &mut sc);
328        vd.field_localization("intermediary_breakdown_maybe", &mut sc);
329        vd.field_localization("intermediary_breakdown_no", &mut sc);
330        vd.field_localization("intermediary_breakdown_yes", &mut sc);
331        vd.field_localization("intermediary_answer_accept_key", &mut sc);
332        vd.field_localization("intermediary_answer_reject_key", &mut sc);
333        vd.field_localization("reply_item_key", &mut sc);
334        vd.field_localization("send_name", &mut sc);
335
336        vd.field_bool("needs_recipient_to_open");
337        vd.field_bool("show_effects_in_notification");
338        vd.field_bool("diarch_interaction");
339        vd.field_validated_sc("auto_accept", &mut sc.clone(), validate_bool_or_trigger);
340
341        vd.field_choice(
342            "target_type",
343            &["artifact", "title", "men_at_arms", "court_position_type", "count"],
344        );
345        vd.field_value("target_filter"); // TODO
346
347        // root is the character being picked
348        vd.field_validated_block_rerooted(
349            "can_be_picked",
350            &sc,
351            Scopes::Character,
352            |block, data, sc| {
353                validate_trigger(block, data, sc, Tooltipped::Yes);
354            },
355        );
356        vd.field_trigger("can_be_picked_title", Tooltipped::Yes, &mut sc.clone());
357        vd.field_trigger("can_be_picked_artifact", Tooltipped::Yes, &mut sc.clone());
358        vd.field_trigger("can_be_picked_regiment", Tooltipped::Yes, &mut sc.clone());
359
360        vd.field_trigger("needs_confirmation", Tooltipped::No, &mut sc.clone());
361
362        // Experimentation showed that even the cost block has scope none
363        vd.field_validated_block_rerooted("cost", &sc, Scopes::None, validate_cost);
364
365        vd.field_list("filter_tags");
366    }
367}
368
369fn validate_bool_or_trigger(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
370    match bv {
371        BV::Value(t) => {
372            if !t.is("yes") && !t.is("no") {
373                warn(ErrorKey::Validation).msg("expected yes or no").loc(t).push();
374            }
375        }
376        BV::Block(b) => {
377            validate_trigger(b, data, sc, Tooltipped::No);
378        }
379    }
380}
381
382fn validate_icon(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
383    match bv {
384        BV::Value(token) => {
385            data.verify_icon("NGameIcons|CHARACTER_INTERACTION_ICON_PATH", token, ".dds");
386        }
387        BV::Block(block) => {
388            let mut vd = Validator::new(block, data);
389            vd.req_field("reference");
390
391            vd.field_trigger("trigger", Tooltipped::No, sc);
392            vd.field_icon("reference", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");
393        }
394    }
395}