1use crate::block::{BV, Block};
2use crate::ck3::validate::{
3 validate_theme_background, validate_theme_effect_2d, validate_theme_header_background,
4 validate_theme_icon, validate_theme_sound, validate_theme_transition,
5};
6use crate::context::ScopeContext;
7use crate::data::events::Event;
8use crate::desc::validate_desc;
9use crate::effect::{validate_effect, validate_effect_internal};
10use crate::everything::Everything;
11use crate::item::Item;
12use crate::lowercase::Lowercase;
13use crate::report::{ErrorKey, Severity, err, warn};
14use crate::scopes::Scopes;
15use crate::special_tokens::SpecialTokens;
16use crate::token::Token;
17use crate::tooltipped::Tooltipped;
18use crate::trigger::validate_target;
19use crate::validate::{
20 ListType, validate_ai_chance, validate_duration, validate_modifiers_with_base,
21};
22use crate::validator::Validator;
23
24const EVENT_TYPES: &[&str] = &[
25 "letter_event",
26 "character_event",
27 "court_event",
28 "duel_event",
29 "fullscreen_event",
30 "activity_event",
31];
32
33pub fn get_event_scope(key: &Token, block: &Block) -> (Scopes, Token) {
34 if let Some(token) = block.get_field_value("scope") {
35 (Scopes::from_snake_case(token.as_str()).unwrap_or(Scopes::non_primitive()), token.clone())
36 } else {
37 (Scopes::Character, key.clone())
38 }
39}
40
41pub fn validate_event(event: &Event, data: &Everything, sc: &mut ScopeContext) {
42 let mut vd = Validator::new(&event.block, data);
43
44 let mut tooltipped_immediate = Tooltipped::Past;
45 let mut tooltipped = Tooltipped::Yes;
46 if event.key.starts_with("debug.") || event.block.field_value_is("hidden", "yes") {
47 tooltipped_immediate = Tooltipped::No;
49 tooltipped = Tooltipped::No;
50 }
51
52 let evtype = event.block.get_field_value("type").map_or("character_event", |t| t.as_str());
53 if evtype == "empty" {
54 let msg = "`type = empty` has been replaced by `scope = none`";
55 let errloc = vd.field_value("type").unwrap();
56 err(ErrorKey::Validation).msg(msg).loc(errloc).push();
57 } else {
58 vd.field_choice("type", EVENT_TYPES);
59 }
60
61 vd.field_value("window");
64
65 if let Some(token) = vd.field_value("scope") {
66 if Scopes::from_snake_case(token.as_str()).is_none() {
67 warn(ErrorKey::Scopes).msg("unknown scope type").loc(token).push();
68 }
69 }
70
71 vd.field_item("content_source", Item::Dlc);
73
74 vd.field_bool("hidden");
75 vd.field_bool("major");
76 vd.field_trigger("major_trigger", Tooltipped::No, sc);
77
78 vd.field_trigger("trigger", Tooltipped::No, sc);
79 vd.field_effect("on_trigger_fail", Tooltipped::No, sc);
80 vd.field_validated_block_sc("weight_multiplier", sc, validate_modifiers_with_base);
81
82 sc.wipe_temporaries();
83 vd.field_effect("immediate", tooltipped_immediate, sc);
84 vd.field_validated_sc("title", sc, validate_desc);
85 vd.field_validated_sc("desc", sc, validate_desc);
86
87 if evtype == "letter_event" {
88 vd.field_validated_sc("opening", sc, validate_desc);
89 vd.req_field("sender");
90 vd.field_validated_sc("sender", sc, validate_portrait);
91 } else {
92 vd.advice_field("opening", "only needed for letter_event");
93 vd.advice_field("sender", "only needed for letter_event");
94 }
95 if evtype == "court_event" {
96 vd.advice_field("left_portrait", "not needed for court_event");
97 vd.advice_field("right_portrait", "not needed for court_event");
98 vd.advice_field("center_portrait", "not needed for court_event");
99 } else {
100 vd.field_validated("left_portrait", |bv, data| {
101 validate_portrait(bv, data, sc);
102 });
103 vd.field_validated("right_portrait", |bv, data| {
104 validate_portrait(bv, data, sc);
105 });
106 vd.field_validated("center_portrait", |bv, data| {
107 validate_portrait(bv, data, sc);
108 });
109 }
110 vd.field_validated("lower_left_portrait", |bv, data| {
111 validate_portrait(bv, data, sc);
112 });
113 vd.field_validated("lower_center_portrait", |bv, data| {
114 validate_portrait(bv, data, sc);
115 });
116 vd.field_validated("lower_right_portrait", |bv, data| {
117 validate_portrait(bv, data, sc);
118 });
119 vd.multi_field_validated_block_sc("artifact", sc, validate_artifact);
121 vd.field_validated_block_sc("court_scene", sc, validate_court_scene);
122 if let Some(token) = vd.field_value("theme") {
123 data.verify_exists(Item::EventTheme, token);
124 data.validate_call(Item::EventTheme, token, &event.block, sc);
125 }
126 if evtype == "court_event" {
128 vd.advice_field("override_background", "not needed for court_event");
129 } else {
130 vd.multi_field_validated_sc("override_background", sc, validate_theme_background);
131 }
132 vd.multi_field_validated_sc("override_icon", sc, validate_theme_icon);
133 vd.multi_field_validated_sc("override_header_background", sc, validate_theme_header_background);
134 vd.multi_field_validated_block_sc("override_sound", sc, validate_theme_sound);
135 vd.multi_field_validated_block_sc("override_transition", sc, validate_theme_transition);
136 vd.multi_field_validated_sc("override_effect_2d", sc, validate_theme_effect_2d);
137 if !event.block.get_field_bool("hidden").unwrap_or(false) {
142 vd.req_field("option");
143 }
144 let mut has_options = false;
145 vd.multi_field_validated_block("option", |block, data| {
146 has_options = true;
147 sc.wipe_temporaries();
148 validate_event_option(block, data, sc, tooltipped);
149 });
150
151 vd.field_validated_key_block("after", |key, block, data| {
152 if !has_options {
153 let msg = "`after` effect will not run if there are no `option` blocks";
154 let info = "you can put it in `immediate` instead";
155 err(ErrorKey::Logic).msg(msg).info(info).loc(key).push();
156 }
157 sc.wipe_temporaries();
158 validate_effect(block, data, sc, tooltipped);
159 });
160 vd.field_validated_block_sc("cooldown", sc, validate_duration);
161 vd.field_value("soundeffect"); vd.field_bool("orphan");
163 vd.field("widget");
165 vd.field_block("widgets");
166}
167
168fn validate_event_option(
169 block: &Block,
170 data: &Everything,
171 sc: &mut ScopeContext,
172 tooltipped: Tooltipped,
173) {
174 let mut vd = Validator::new(block, data);
175 vd.multi_field_validated("name", |bv, data| match bv {
176 BV::Value(t) => {
177 data.verify_exists(Item::Localization, t);
178 }
179 BV::Block(b) => {
180 let mut vd = Validator::new(b, data);
181 vd.req_field("text");
182 vd.field_trigger("trigger", Tooltipped::No, sc);
183 vd.field_validated_sc("text", sc, validate_desc);
184 for field in &["desc", "first_valid", "random_valid", "triggered_desc"] {
185 vd.advice_field(field, "use this inside `name = { text = { ... } }`");
186 }
187 }
188 });
189
190 vd.field_trigger("trigger", Tooltipped::No, sc);
191 vd.field_trigger("show_as_unavailable", Tooltipped::No, sc);
192
193 vd.field_validated_sc("flavor", sc, validate_desc);
194 vd.field_value("reason"); vd.multi_field_item("trait", Item::Trait);
198 vd.multi_field_item("skill", Item::Skill);
199
200 vd.field_validated_sc("ai_chance", sc, validate_ai_chance);
201 vd.field_script_value_no_breakdown("ai_will_select", sc);
202
203 vd.field_bool("exclusive");
205
206 vd.field_bool("is_cancel_option");
208
209 vd.field_bool("fallback");
212
213 vd.field_target("highlight_portrait", sc, Scopes::Character);
214 vd.field_bool("show_unlock_reason");
215
216 vd.field_item("clicksound", Item::Sound);
218
219 validate_effect_internal(
220 &Lowercase::new_unchecked("option"),
221 ListType::None,
222 block,
223 data,
224 sc,
225 &mut vd,
226 tooltipped,
227 &mut SpecialTokens::none(),
228 );
229}
230
231fn validate_court_scene(block: &Block, data: &Everything, sc: &mut ScopeContext) {
232 let mut vd = Validator::new(block, data);
233
234 vd.req_field("button_position_character");
235 vd.field_target("button_position_character", sc, Scopes::Character);
236 vd.field_bool("court_event_force_open");
237 vd.field_bool("show_timeout_info");
238 vd.field_bool("should_pause_time");
239 vd.field_target("court_owner", sc, Scopes::Character);
240 vd.field_item("scripted_animation", Item::ScriptedAnimation);
241 vd.multi_field_validated_block("roles", |b, data| {
242 for (key, bv) in b.iter_assignments_and_definitions_warn() {
243 match bv {
244 BV::Block(block) => {
245 validate_target(key, data, sc, Scopes::Character);
246 let mut vd = Validator::new(block, data);
247 vd.req_field_one_of(&["group", "role"]);
248 vd.field_item("group", Item::CourtSceneGroup);
249 vd.field_item("role", Item::CourtSceneRole);
250 vd.field_item("animation", Item::PortraitAnimation);
251 vd.multi_field_validated_block("triggered_animation", |b, data| {
252 validate_triggered_animation(b, data, sc);
253 });
254 }
255 BV::Value(token) => {
256 data.verify_exists(Item::CourtSceneGroup, token);
257 }
258 }
259 }
260 });
261}
262
263fn validate_artifact(block: &Block, data: &Everything, sc: &mut ScopeContext) {
264 let mut vd = Validator::new(block, data);
265
266 vd.req_field("target");
267 vd.req_field("position");
268 vd.field_target("target", sc, Scopes::Artifact);
269 vd.field_choice(
270 "position",
271 &["lower_left_portrait", "lower_center_portrait", "lower_right_portrait"],
272 );
273 vd.field_trigger("trigger", Tooltipped::No, sc);
274}
275
276fn validate_animations(vd: &mut Validator) {
277 vd.field_validated_value("animation", |_, mut vd| {
278 if !vd.maybe_item(Item::PortraitAnimation) && vd.maybe_item(Item::ScriptedAnimation) {
279 let msg = format!(
280 "portrait animation {vd} not defined in {}",
281 Item::PortraitAnimation.path()
282 );
283 let info = format!("Did you mean `scripted_animation = {vd}`?");
284 warn(ErrorKey::MissingItem).strong().msg(msg).info(info).loc(vd).push();
285 } else {
286 vd.item(Item::PortraitAnimation);
287 }
288 });
289 vd.field_validated_value("scripted_animation", |_, mut vd| {
290 if !vd.maybe_item(Item::ScriptedAnimation) && vd.maybe_item(Item::PortraitAnimation) {
291 let msg = format!(
292 "scripted animation {vd} not defined in {}",
293 Item::ScriptedAnimation.path()
294 );
295 let info = format!("Did you mean `animation = {vd}`?");
296 warn(ErrorKey::MissingItem).strong().msg(msg).info(info).loc(vd).push();
297 } else {
298 vd.item(Item::ScriptedAnimation);
299 }
300 });
301}
302
303fn validate_triggered_animation(block: &Block, data: &Everything, sc: &mut ScopeContext) {
304 let mut vd = Validator::new(block, data);
305 vd.set_max_severity(Severity::Warning);
306
307 vd.req_field("trigger");
308 vd.field_trigger("trigger", Tooltipped::No, sc);
309 vd.field_item("camera", Item::PortraitCamera);
310 vd.req_field_one_of(&["animation", "scripted_animation"]);
311 validate_animations(&mut vd);
312}
313
314fn validate_triggered_outfit(block: &Block, data: &Everything, sc: &mut ScopeContext) {
315 let mut vd = Validator::new(block, data);
316 vd.set_max_severity(Severity::Warning);
317
318 vd.field_trigger("trigger", Tooltipped::No, sc);
320 vd.field_list("outfit_tags"); vd.field_bool("remove_default_outfit");
323 vd.field_bool("hide_info");
324}
325
326fn validate_portrait(v: &BV, data: &Everything, sc: &mut ScopeContext) {
327 match v {
328 BV::Value(t) => {
329 validate_target(t, data, sc, Scopes::Character);
330 }
331 BV::Block(b) => {
332 let mut vd = Validator::new(b, data);
333
334 vd.req_field("character");
335 vd.field_target("character", sc, Scopes::Character);
336 vd.field_trigger("trigger", Tooltipped::No, sc);
337 validate_animations(&mut vd);
338 vd.multi_field_validated_block("triggered_animation", |b, data| {
339 validate_triggered_animation(b, data, sc);
340 });
341 vd.field_list("outfit_tags"); vd.field_bool("remove_default_outfit");
343 vd.field_bool("hide_info");
344 vd.multi_field_validated_block("triggered_outfit", |b, data| {
345 validate_triggered_outfit(b, data, sc);
346 });
347 vd.field_item("camera", Item::PortraitCamera);
348
349 vd.field_bool("override_imprisonment_visuals");
351 vd.field_bool("animate_if_dead");
352 }
353 }
354}