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 && Scopes::from_snake_case(token.as_str()).is_none()
67 {
68 warn(ErrorKey::Scopes).msg("unknown scope type").loc(token).push();
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");
204
205 vd.field_bool("is_cancel_option");
206
207 vd.field_bool("fallback");
210
211 vd.field_target("highlight_portrait", sc, Scopes::Character);
212 vd.field_bool("show_unlock_reason");
213
214 vd.field_item("clicksound", Item::Sound);
215
216 validate_effect_internal(
217 &Lowercase::new_unchecked("option"),
218 ListType::None,
219 block,
220 data,
221 sc,
222 &mut vd,
223 tooltipped,
224 &mut SpecialTokens::none(),
225 );
226}
227
228fn validate_court_scene(block: &Block, data: &Everything, sc: &mut ScopeContext) {
229 let mut vd = Validator::new(block, data);
230
231 vd.req_field("button_position_character");
232 vd.field_target("button_position_character", sc, Scopes::Character);
233 vd.field_bool("court_event_force_open");
234 vd.field_bool("show_timeout_info");
235 vd.field_bool("should_pause_time");
236 vd.field_target("court_owner", sc, Scopes::Character);
237 vd.field_item("scripted_animation", Item::ScriptedAnimation);
238 vd.multi_field_validated_block("roles", |b, data| {
239 for (key, bv) in b.iter_assignments_and_definitions_warn() {
240 match bv {
241 BV::Block(block) => {
242 validate_target(key, data, sc, Scopes::Character);
243 let mut vd = Validator::new(block, data);
244 vd.req_field_one_of(&["group", "role"]);
245 vd.field_item("group", Item::CourtSceneGroup);
246 vd.field_item("role", Item::CourtSceneRole);
247 vd.field_item("animation", Item::PortraitAnimation);
248 vd.multi_field_validated_block("triggered_animation", |b, data| {
249 validate_triggered_animation(b, data, sc);
250 });
251 }
252 BV::Value(token) => {
253 data.verify_exists(Item::CourtSceneGroup, token);
254 }
255 }
256 }
257 });
258}
259
260fn validate_artifact(block: &Block, data: &Everything, sc: &mut ScopeContext) {
261 let mut vd = Validator::new(block, data);
262
263 vd.req_field("target");
264 vd.req_field("position");
265 vd.field_target("target", sc, Scopes::Artifact);
266 vd.field_choice(
267 "position",
268 &["lower_left_portrait", "lower_center_portrait", "lower_right_portrait"],
269 );
270 vd.field_trigger("trigger", Tooltipped::No, sc);
271}
272
273fn validate_animations(vd: &mut Validator) {
274 vd.field_validated_value("animation", |_, mut vd| {
275 if !vd.maybe_item(Item::PortraitAnimation) && vd.maybe_item(Item::ScriptedAnimation) {
276 let msg = format!(
277 "portrait animation {vd} not defined in {}",
278 Item::PortraitAnimation.path()
279 );
280 let info = format!("Did you mean `scripted_animation = {vd}`?");
281 warn(ErrorKey::MissingItem).strong().msg(msg).info(info).loc(vd).push();
282 } else {
283 vd.item(Item::PortraitAnimation);
284 }
285 });
286 vd.field_validated_value("scripted_animation", |_, mut vd| {
287 if !vd.maybe_item(Item::ScriptedAnimation) && vd.maybe_item(Item::PortraitAnimation) {
288 let msg = format!(
289 "scripted animation {vd} not defined in {}",
290 Item::ScriptedAnimation.path()
291 );
292 let info = format!("Did you mean `animation = {vd}`?");
293 warn(ErrorKey::MissingItem).strong().msg(msg).info(info).loc(vd).push();
294 } else {
295 vd.item(Item::ScriptedAnimation);
296 }
297 });
298}
299
300fn validate_triggered_animation(block: &Block, data: &Everything, sc: &mut ScopeContext) {
301 let mut vd = Validator::new(block, data);
302 vd.set_max_severity(Severity::Warning);
303
304 vd.req_field("trigger");
305 vd.field_trigger("trigger", Tooltipped::No, sc);
306 vd.field_item("camera", Item::PortraitCamera);
307 vd.req_field_one_of(&["animation", "scripted_animation"]);
308 validate_animations(&mut vd);
309}
310
311fn validate_triggered_outfit(block: &Block, data: &Everything, sc: &mut ScopeContext) {
312 let mut vd = Validator::new(block, data);
313 vd.set_max_severity(Severity::Warning);
314
315 vd.field_trigger("trigger", Tooltipped::No, sc);
317 vd.field_list("outfit_tags"); vd.field_bool("remove_default_outfit");
320 vd.field_bool("hide_info");
321}
322
323fn validate_portrait(v: &BV, data: &Everything, sc: &mut ScopeContext) {
324 match v {
325 BV::Value(t) => {
326 validate_target(t, data, sc, Scopes::Character);
327 }
328 BV::Block(b) => {
329 let mut vd = Validator::new(b, data);
330
331 vd.req_field("character");
332 vd.field_target("character", sc, Scopes::Character);
333 vd.field_trigger("trigger", Tooltipped::No, sc);
334 validate_animations(&mut vd);
335 vd.multi_field_validated_block("triggered_animation", |b, data| {
336 validate_triggered_animation(b, data, sc);
337 });
338 vd.field_list("outfit_tags"); vd.field_bool("remove_default_outfit");
340 vd.field_bool("hide_info");
341 vd.multi_field_validated_block("triggered_outfit", |b, data| {
342 validate_triggered_outfit(b, data, sc);
343 });
344 vd.field_item("camera", Item::PortraitCamera);
345
346 vd.field_bool("override_imprisonment_visuals");
348 vd.field_bool("animate_if_dead");
349 }
350 }
351}