Skip to main content

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        && Scopes::from_snake_case(token.as_str()).is_none()
67    {
68        warn(ErrorKey::Scopes).msg("unknown scope type").loc(token).push();
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    vd.field_bool("exclusive");
204
205    vd.field_bool("is_cancel_option");
206
207    // If fallback = yes, the option is shown despite its trigger,
208    // if there would otherwise be no other option
209    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    // trigger is apparently optional
316    vd.field_trigger("trigger", Tooltipped::No, sc);
317    // TODO: check that at least one of these is set?
318    vd.field_list("outfit_tags"); // TODO
319    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"); // TODO
339            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            // TODO: is this only useful when animation is prisondungeon ?
347            vd.field_bool("override_imprisonment_visuals");
348            vd.field_bool("animate_if_dead");
349        }
350    }
351}