tiger_lib/ck3/
events.rs

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        // Suppress missing-localization messages
48        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    // TODO: Should be an `Item::WidgetName` but widget name processing currently doesn't catch
62    // subwidgets with names.
63    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    // "dlc or mod this event comes from"
72    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    // TODO: check that artifacts are not in the same position as a character
120    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    // TODO: warn if more than one of each is defined with no trigger
127    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    // Note: override_environment seems to be unused, and themes defined in
138    // common/event_themes don't have environments. So I left it out even though
139    // it's in the docs.
140
141    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"); // TODO
162    vd.field_bool("orphan");
163    // TODO: validate widget
164    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"); // arbitrary string passed to the UI
195
196    // "this option is available because you have the ... trait"
197    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    // TODO: check what this does.
204    vd.field_bool("exclusive");
205
206    // TODO: check what this does.
207    vd.field_bool("is_cancel_option");
208
209    // If fallback = yes, the option is shown despite its trigger,
210    // if there would otherwise be no other option
211    vd.field_bool("fallback");
212
213    vd.field_target("highlight_portrait", sc, Scopes::Character);
214    vd.field_bool("show_unlock_reason");
215
216    // undocumented
217    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    // trigger is apparently optional
319    vd.field_trigger("trigger", Tooltipped::No, sc);
320    // TODO: check that at least one of these is set?
321    vd.field_list("outfit_tags"); // TODO
322    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"); // TODO
342            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            // TODO: is this only useful when animation is prisondungeon ?
350            vd.field_bool("override_imprisonment_visuals");
351            vd.field_bool("animate_if_dead");
352        }
353    }
354}