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 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 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 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 vd.field_effect("redirect", Tooltipped::No, &mut sc);
97
98 vd.field_bool("ai_instant_response");
99 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"); 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 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 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 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 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 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 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"); 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 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 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"); 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 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}