tiger_lib/
trigger.rs

1//! Validate triggers, which are parts of the script that specify yes or no conditions.
2
3use bitflags::bitflags;
4
5use std::str::FromStr;
6
7use crate::block::{BV, Block, Comparator, Eq::*, Field};
8use crate::context::{Reason, ScopeContext, Temporary};
9#[cfg(feature = "jomini")]
10use crate::data::genes::Gene;
11#[cfg(feature = "jomini")]
12use crate::data::trigger_localization::validate_trigger_localization;
13use crate::date::Date;
14use crate::desc::validate_desc;
15use crate::effect::scope_effect;
16use crate::everything::Everything;
17use crate::game::Game;
18use crate::helpers::is_country_tag;
19use crate::helpers::stringify_choices;
20#[cfg(feature = "hoi4")]
21use crate::hoi4::effect_validation::validate_flag_name;
22#[cfg(feature = "hoi4")]
23use crate::hoi4::variables::validate_variable;
24use crate::item::Item;
25use crate::lowercase::Lowercase;
26#[cfg(any(feature = "vic3", feature = "imperator"))]
27use crate::modif::verify_modif_exists;
28use crate::report::{ErrorKey, Severity, err, fatal, tips, warn};
29use crate::scopes::{
30    ArgumentValue, Scopes, needs_prefix, scope_iterator, scope_prefix, scope_to_scope,
31};
32#[cfg(feature = "jomini")]
33use crate::script_value::validate_script_value;
34use crate::token::{Loc, Token};
35use crate::tooltipped::Tooltipped;
36use crate::validate::{
37    ListType, precheck_iterator_fields, validate_identifier, validate_ifelse_sequence,
38    validate_inside_iterator, validate_iterator_fields,
39};
40use crate::validator::Validator;
41
42/// Look up a trigger token that evaluates to a trigger value.
43///
44/// `name` is the token. `data` is used in special cases to verify the name dynamically,
45/// for example, `<lifestyle>_xp` is only a valid trigger if `<lifestyle>` is present in
46/// the database.
47///
48/// Returns the inscopes valid for the trigger and the output trigger value type.
49pub fn scope_trigger(name: &Token, data: &Everything) -> Option<(Scopes, Trigger)> {
50    let scope_trigger = match Game::game() {
51        #[cfg(feature = "ck3")]
52        Game::Ck3 => crate::ck3::tables::triggers::scope_trigger,
53        #[cfg(feature = "vic3")]
54        Game::Vic3 => crate::vic3::tables::triggers::scope_trigger,
55        #[cfg(feature = "imperator")]
56        Game::Imperator => crate::imperator::tables::triggers::scope_trigger,
57        #[cfg(feature = "eu5")]
58        Game::Eu5 => crate::eu5::tables::triggers::scope_trigger,
59        #[cfg(feature = "hoi4")]
60        Game::Hoi4 => crate::hoi4::tables::triggers::scope_trigger,
61    };
62    scope_trigger(name, data)
63}
64
65/// The standard interface to trigger validation. Validates a trigger in the given [`ScopeContext`].
66///
67/// `tooltipped` determines what warnings are emitted related to tooltippability of the triggers
68/// inside the block.
69///
70/// Returns true iff the trigger had side effects (such as saving scopes).
71pub fn validate_trigger(
72    block: &Block,
73    data: &Everything,
74    sc: &mut ScopeContext,
75    tooltipped: Tooltipped,
76) -> bool {
77    let vd = Validator::new(block, data);
78    validate_trigger_internal(
79        Lowercase::empty(),
80        ListType::None,
81        block,
82        data,
83        sc,
84        vd,
85        tooltipped,
86        false,
87    )
88}
89
90/// Like [`validate_trigger`] but specifies a maximum [`Severity`] for the reports emitted by this
91/// validation. Used to validate triggers in item definitions that don't warrant the `Error` level.
92///
93/// Returns true iff the trigger had side effects (such as saving scopes).
94#[allow(dead_code)]
95pub fn validate_trigger_max_sev(
96    block: &Block,
97    data: &Everything,
98    sc: &mut ScopeContext,
99    tooltipped: Tooltipped,
100    max_sev: Severity,
101) -> bool {
102    let mut vd = Validator::new(block, data);
103    vd.set_max_severity(max_sev);
104    validate_trigger_internal(
105        Lowercase::empty(),
106        ListType::None,
107        block,
108        data,
109        sc,
110        vd,
111        tooltipped,
112        false,
113    )
114}
115
116/// The interface to trigger validation when [`validate_trigger`] is too limited.
117///
118/// `caller` is the key that opened this trigger. It is used to determine which special cases apply.
119/// For example, if `caller` is `trigger_if` then a `limit` block is expected.
120///
121/// `in_list` specifies whether this trigger is directly in an `any_` iterator. It is also used to
122/// determine which special cases apply.
123///
124/// `negated` is true iff this trigger is tested in a negative sense, for example if it is
125/// somewhere inside a `NOT = { ... }` block. `negated` is propagated to all sub-blocks and is
126/// flipped when another `NOT` or similar is encountered inside this one.
127///
128/// Returns true iff the trigger had side effects (such as saving scopes).
129// TODO: `in_list` could be removed if the code checks directly for the `any_` prefix instead.
130#[allow(clippy::too_many_arguments)]
131pub fn validate_trigger_internal(
132    caller: &Lowercase,
133    ltype: ListType,
134    block: &Block,
135    data: &Everything,
136    sc: &mut ScopeContext,
137    mut vd: Validator,
138    mut tooltipped: Tooltipped,
139    negated: bool,
140) -> bool {
141    let mut side_effects = false;
142    let max_sev = vd.max_severity();
143    vd.set_case_sensitive(false);
144
145    #[cfg(feature = "hoi4")]
146    let caller = if Game::is_hoi4() && tooltipped == Tooltipped::Inner {
147        &Lowercase::new_unchecked("custom_override_tooltip")
148    } else {
149        caller
150    };
151
152    // If this condition looks weird, it's because the negation from for example NOR has already
153    // been applied to the `negated` value.
154    if tooltipped == Tooltipped::FailuresOnly
155        && ((negated && (caller == "and" || caller == "nand"))
156            || (!negated && (caller == "or" || caller == "nor" || caller == "all_false")))
157    {
158        let true_negated = if caller == "nor" || caller == "all_false" || caller == "and" {
159            "negated "
160        } else {
161            ""
162        };
163        let msg = format!(
164            "{true_negated}{} is a too complex trigger to be tooltipped in a trigger that shows failures only.",
165            caller.to_uppercase()
166        );
167        let info = "Try adding a custom_description or custom_tooltip, or simplifying the trigger";
168        warn(ErrorKey::Tooltip).msg(msg).info(info).loc(block).push();
169    }
170
171    if block.num_items() == 1 {
172        if caller == "and" {
173            let msg = "AND with only one item inside is useless";
174            let info = "you can remove the AND and just have the item there";
175            tips(ErrorKey::Performance).msg(msg).info(info).loc(block).push();
176        } else if caller == "or" {
177            let msg = "OR with only one item inside is probably not what you intended";
178            warn(ErrorKey::Logic).weak().msg(msg).loc(block).push();
179        }
180    }
181
182    if caller == "trigger_if"
183        || caller == "trigger_else_if"
184        || caller == "trigger_else"
185        || (Game::is_hoi4() && (caller == "if" || caller == "else_if" || caller == "else"))
186    {
187        if caller != "trigger_else" && caller != "else" {
188            vd.req_field_warn("limit");
189        }
190        vd.field_validated_key_block("limit", |key, block, data| {
191            if caller == "trigger_else" {
192                let msg = "`trigger_else` with a `limit` does work, but may indicate a mistake";
193                let info = "normally you would use `trigger_else_if` instead.";
194                tips(ErrorKey::IfElse).msg(msg).info(info).loc(key).push();
195            }
196            side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
197        });
198    } else {
199        vd.ban_field("limit", || "`trigger_if`, `trigger_else_if` or `trigger_else`");
200    }
201
202    #[cfg(feature = "jomini")]
203    if ltype == ListType::None {
204        vd.ban_field("filter", || "lists");
205    } else {
206        vd.field_validated_block("filter", |block, data| {
207            side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
208        });
209    }
210
211    validate_iterator_fields(caller, ltype, data, sc, &mut vd, &mut tooltipped, false);
212
213    if ltype != ListType::None {
214        validate_inside_iterator(caller, ltype, block, data, sc, &mut vd, tooltipped);
215    }
216
217    // TODO: the custom_description and custom_tooltip logic is duplicated for effects
218    #[cfg(feature = "jomini")]
219    if Game::is_jomini() {
220        if caller == "custom_description" || caller == "custom_tooltip" {
221            vd.req_field("text");
222            if caller == "custom_tooltip" {
223                vd.field_item("text", Item::Localization);
224            } else if let Some(token) = vd.field_value("text") {
225                validate_trigger_localization(token, data, tooltipped, negated);
226            }
227            vd.field_target_ok_this("subject", sc, Scopes::non_primitive());
228        } else {
229            vd.ban_field("text", || "`custom_description` or `custom_tooltip`");
230            vd.ban_field("subject", || "`custom_description` or `custom_tooltip`");
231        }
232    }
233
234    if caller == "custom_description" {
235        // object and value are handled in the loop
236    } else {
237        vd.ban_field("object", || "`custom_description`");
238    }
239
240    if caller == "modifier" {
241        // desc is handled in the loop
242        // mark add and factor as known
243        vd.multi_field("add");
244        vd.multi_field("factor");
245        vd.field_validated_block("trigger", |block, data| {
246            side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
247        });
248    } else {
249        vd.ban_field("add", || "`modifier` or script values");
250        vd.ban_field("factor", || "`modifier` blocks");
251        vd.ban_field("desc", || "`modifier` or script values");
252        vd.ban_field("trigger", || "`modifier` blocks");
253    }
254
255    if caller == "calc_true_if" {
256        vd.req_field("amount");
257        // TODO: verify these are integers
258        vd.multi_field_any_cmp("amount");
259    } else if ltype == ListType::None {
260        vd.ban_field("amount", || "`calc_true_if`");
261    }
262
263    validate_ifelse_sequence(block, "trigger_if", "trigger_else_if", "trigger_else");
264
265    vd.unknown_fields_any_cmp(|key, cmp, bv| {
266        #[cfg(feature = "jomini")]
267        if Game::is_jomini() && key.is("value") {
268            validate_script_value(bv, data, sc);
269            side_effects = true;
270            return;
271        }
272
273        if key.is("desc") || key.is("DESC") {
274            validate_desc(bv, data, sc);
275            return;
276        }
277
278        if key.is("object") {
279            if let Some(token) = bv.expect_value() {
280                validate_target_ok_this(token, data, sc, Scopes::non_primitive());
281            }
282            return;
283        }
284
285        if let Some((it_type, it_name)) = key.split_once('_') {
286            if let Ok(ltype) = ListType::try_from(it_type.as_str()) {
287                if let Some((inscopes, outscope)) = scope_iterator(&it_name, data, sc) {
288                    if !ltype.is_for_triggers() {
289                        let msg = format!("cannot use `{it_type}_` list in a trigger");
290                        err(ErrorKey::Validation).msg(msg).loc(key).push();
291                        return;
292                    }
293                    sc.expect(inscopes, &Reason::Token(key.clone()), data);
294                    if let Some(block) = bv.expect_block() {
295                        precheck_iterator_fields(ltype, it_name.as_str(), block, data, sc);
296                        sc.open_scope(outscope, key.clone());
297                        let mut vd = Validator::new(block, data);
298                        vd.set_max_severity(max_sev);
299                        side_effects |= validate_trigger_internal(
300                            &Lowercase::new(it_name.as_str()),
301                            ltype,
302                            block,
303                            data,
304                            sc,
305                            vd,
306                            tooltipped,
307                            negated,
308                        );
309                        sc.close();
310                    }
311                    return;
312                }
313            }
314        }
315
316        if caller == "weighted_calc_true_if" && data.get_trigger(key).is_some() {
317            let msg =
318                "scripted triggers don't work in `weighted_calc_true_if` (confirmed for 1.16)";
319            err(ErrorKey::Bugs).msg(msg).loc(key).push();
320        }
321
322        side_effects |=
323            validate_trigger_key_bv(key, cmp, bv, data, sc, tooltipped, negated, max_sev);
324    });
325
326    if caller == "modifier" {
327        // check add and factor at the end, accounting for any temporary scope saved
328        // elsewhere in the block.
329        vd.multi_field_validated("add", |bv, data| {
330            if Game::is_jomini() {
331                #[cfg(feature = "jomini")]
332                validate_script_value(bv, data, sc);
333                side_effects = true;
334            } else {
335                // TODO HOI4
336                let _ = &bv;
337                let _ = &data;
338            }
339        });
340
341        vd.multi_field_validated("factor", |bv, data| {
342            if Game::is_jomini() {
343                #[cfg(feature = "jomini")]
344                validate_script_value(bv, data, sc);
345                side_effects = true;
346            } else {
347                // TODO HOI4
348                let _ = &bv;
349                let _ = &data;
350            }
351        });
352    }
353
354    side_effects
355}
356
357/// Validate a trigger given its key and argument. It is like [`validate_trigger_internal`] except
358/// that all special cases are assumed to have been handled. This is the interface used for the
359/// `switch` effect, where the key and argument are not together in the script.
360///
361/// Returns true iff the trigger had side effects (such as saving scopes).
362#[allow(clippy::too_many_arguments)] // nothing can be cut
363pub fn validate_trigger_key_bv(
364    key: &Token,
365    cmp: Comparator,
366    bv: &BV,
367    data: &Everything,
368    sc: &mut ScopeContext,
369    tooltipped: Tooltipped,
370    negated: bool,
371    max_sev: Severity,
372) -> bool {
373    let mut side_effects = false;
374
375    // Scripted trigger?
376    if let Some(trigger) = data.get_trigger(key) {
377        match bv {
378            BV::Value(token) => {
379                if !(token.is("yes") || token.is("no") || token.is("YES") || token.is("NO")) {
380                    warn(ErrorKey::Validation).msg("expected yes or no").loc(token).push();
381                }
382                if !trigger.macro_parms().is_empty() {
383                    fatal(ErrorKey::Macro).msg("expected macro arguments").loc(token).push();
384                    return side_effects;
385                }
386                let negated = if token.is("no") { !negated } else { negated };
387                // TODO: check side_effects
388                trigger.validate_call(key, data, sc, tooltipped, negated);
389            }
390            BV::Block(block) => {
391                let parms = trigger.macro_parms();
392                if parms.is_empty() {
393                    let msg = "this scripted trigger does not need macro arguments";
394                    fatal(ErrorKey::Macro).msg(msg).loc(block).push();
395                } else {
396                    let mut vec = Vec::new();
397                    let mut vd = Validator::new(block, data);
398                    vd.set_max_severity(max_sev);
399                    for parm in &parms {
400                        if let Some(token) = vd.field_value(parm) {
401                            vec.push(token.clone());
402                        } else {
403                            let msg = format!("this scripted trigger needs parameter {parm}");
404                            err(ErrorKey::Macro).msg(msg).loc(block).push();
405                            return side_effects;
406                        }
407                    }
408                    vd.unknown_value_fields(|key, _value| {
409                        let msg = format!("this scripted trigger does not need parameter {key}");
410                        let info = "supplying an unneeded parameter often causes a crash";
411                        fatal(ErrorKey::Macro).msg(msg).info(info).loc(key).push();
412                    });
413
414                    let args: Vec<_> = parms.into_iter().zip(vec).collect();
415                    // TODO: check side_effects
416                    trigger.validate_macro_expansion(key, &args, data, sc, tooltipped, negated);
417                }
418            }
419        }
420        return side_effects;
421    }
422
423    // `10 < script value` is a valid trigger
424    if key.is_number() {
425        if Game::is_jomini() {
426            #[cfg(feature = "jomini")]
427            validate_script_value(bv, data, sc);
428        } else {
429            // TODO HOI4
430        }
431        return side_effects;
432    }
433
434    #[cfg(feature = "hoi4")]
435    if Game::is_hoi4() && key.starts_with("var:") {
436        validate_variable(key, data, sc, Severity::Error);
437        sc.open_builder();
438        sc.replace(Scopes::all_but_none(), key.clone());
439        side_effects |= validate_trigger_rhs(key, cmp, bv, data, sc, tooltipped, negated, max_sev);
440        return side_effects;
441    }
442
443    let part_vec = partition(key);
444    sc.open_builder();
445    for i in 0..part_vec.len() {
446        let mut part_flags = PartFlags::empty();
447        if i == 0 {
448            part_flags |= PartFlags::First;
449        }
450        if i + 1 == part_vec.len() {
451            part_flags |= PartFlags::Last;
452        }
453        if matches!(cmp, Comparator::Equals(Question)) {
454            part_flags |= PartFlags::Question;
455        }
456        let part = &part_vec[i];
457
458        match part {
459            Part::TokenArgument(part, func, arg) => {
460                validate_argument(part_flags, part, func, arg, data, sc);
461            }
462            Part::Token(part) => {
463                let part_lc = Lowercase::new(part.as_str());
464                // prefixed scope transition, e.g. cp:councillor_steward
465                if let Some((prefix, mut arg)) = part.split_once(':') {
466                    // event_id have multiple parts separated by `.`
467                    let is_event_id = prefix.lowercase_is("event_id");
468                    if is_event_id {
469                        arg = key.split_once(':').unwrap().1;
470                    }
471                    if !validate_prefix(part_flags, part, &prefix, &arg, data, sc) {
472                        sc.close();
473                        return side_effects;
474                    }
475                    if is_event_id {
476                        break; // force last part
477                    }
478                } else if part_lc == "root" {
479                    sc.replace_root();
480                } else if part_lc == "prev" {
481                    if !part_flags.contains(PartFlags::First) && !Game::is_imperator() {
482                        warn_not_first(part);
483                    }
484                    sc.replace_prev();
485                } else if part_lc == "this" {
486                    sc.replace_this();
487                } else if Game::is_hoi4() && part_lc == "from" {
488                    #[cfg(feature = "hoi4")]
489                    sc.replace_from();
490                } else if data.script_value_exists(part.as_str()) {
491                    // TODO: check side_effects
492                    #[cfg(feature = "jomini")]
493                    data.script_values.validate_call(part, data, sc);
494                    sc.replace(Scopes::Value, part.clone());
495                } else if let Some((inscopes, outscope)) = scope_to_scope(part, sc.scopes(data)) {
496                    #[cfg(feature = "imperator")]
497                    if let Some((inscopes, trigger)) = scope_trigger(part, data) {
498                        // If a trigger of the same name exists, and it's compatible with this
499                        // location and scope context, then that trigger takes precedence.
500                        if part_flags.contains(PartFlags::Last)
501                            && (inscopes.contains(Scopes::None)
502                                || sc.scopes(data).intersects(inscopes))
503                        {
504                            validate_inscopes(part_flags, part, inscopes, sc, data);
505                            sc.close();
506                            side_effects |= match_trigger_bv(
507                                &trigger,
508                                &part.clone(),
509                                cmp,
510                                bv,
511                                data,
512                                sc,
513                                tooltipped,
514                                negated,
515                                max_sev,
516                            );
517                            return side_effects;
518                        }
519                    }
520                    validate_inscopes(part_flags, part, inscopes, sc, data);
521                    sc.replace(outscope, part.clone());
522                } else if let Some((inscopes, trigger)) = scope_trigger(part, data) {
523                    if !part_flags.contains(PartFlags::Last) {
524                        let msg = format!("`{part}` should be the last part");
525                        warn(ErrorKey::Validation).msg(msg).loc(part).push();
526                        sc.close();
527                        return side_effects;
528                    }
529                    if !part_flags.contains(PartFlags::First)
530                        && trigger_comparevalue(part, data).is_none()
531                    {
532                        let msg = format!("`{part}` should be the only part");
533                        warn(ErrorKey::Validation).msg(msg).loc(part).push();
534                        sc.close();
535                        return side_effects;
536                    }
537                    validate_inscopes(part_flags, part, inscopes, sc, data);
538                    sc.close();
539                    side_effects |= match_trigger_bv(
540                        &trigger,
541                        &part.clone(),
542                        cmp,
543                        bv,
544                        data,
545                        sc,
546                        tooltipped,
547                        negated,
548                        max_sev,
549                    );
550                    return side_effects;
551                } else if Game::is_hoi4() && is_country_tag(part.as_str()) {
552                    if !part_flags.contains(PartFlags::First) {
553                        warn_not_first(part);
554                    }
555                    #[cfg(feature = "hoi4")]
556                    data.verify_exists(Item::CountryTag, part);
557                    #[cfg(feature = "hoi4")]
558                    sc.replace(Scopes::Country, part.clone());
559                } else if Game::is_hoi4() && is_character_token(part.as_str(), data) {
560                    #[cfg(feature = "hoi4")]
561                    sc.replace(Scopes::Character, part.clone());
562                } else if scope_effect(part, data).is_some() {
563                    let msg = format!("`{part}` is an effect, and can't be used in a trigger");
564                    err(ErrorKey::WrongUse).msg(msg).loc(part).push();
565                    sc.close();
566                    return side_effects;
567                } else {
568                    // TODO: warn if trying to use iterator here
569                    let msg = format!("unknown token `{part}`");
570                    err(ErrorKey::UnknownField).msg(msg).loc(part).push();
571                    sc.close();
572                    return side_effects;
573                }
574            }
575        }
576    }
577
578    side_effects |= validate_trigger_rhs(key, cmp, bv, data, sc, tooltipped, negated, max_sev);
579    side_effects
580}
581
582#[allow(clippy::too_many_arguments)] // nothing can be cut
583pub fn validate_trigger_rhs(
584    key: &Token,
585    cmp: Comparator,
586    bv: &BV,
587    data: &Everything,
588    sc: &mut ScopeContext,
589    tooltipped: Tooltipped,
590    negated: bool,
591    max_sev: Severity,
592) -> bool {
593    let mut side_effects = false;
594
595    if !matches!(cmp, Comparator::Equals(Single | Question)) {
596        // At this point we don't know whether to expect a script value or a target at the
597        // right-hand side. Check for target first; a target can be a script value so that
598        // encompasses both if the `bv` is not a block.
599        if matches!(cmp, Comparator::NotEquals | Comparator::Equals(Double))
600            && bv.get_value().is_some()
601        {
602            let scopes = sc.scopes(data);
603            sc.close();
604            if let Some(token) = bv.expect_value() {
605                validate_target_ok_this(token, data, sc, scopes);
606            }
607        } else if sc.can_be(Scopes::Value, data) {
608            sc.close();
609            // TODO: check side_effects
610            #[cfg(feature = "jomini")]
611            validate_script_value(bv, data, sc);
612        } else {
613            let msg = format!("unexpected comparator {cmp}");
614            warn(ErrorKey::Validation).msg(msg).loc(key).push();
615            sc.close();
616        }
617        return side_effects;
618    }
619
620    match bv {
621        BV::Value(t) => {
622            let scopes = sc.scopes(data);
623            sc.close();
624            validate_target_ok_this(t, data, sc, scopes);
625        }
626        BV::Block(b) => {
627            sc.finalize_builder();
628            let mut vd = Validator::new(b, data);
629            vd.set_max_severity(max_sev);
630            side_effects |= validate_trigger_internal(
631                Lowercase::empty(),
632                ListType::None,
633                b,
634                data,
635                sc,
636                vd,
637                tooltipped,
638                negated,
639            );
640            sc.close();
641        }
642    }
643
644    side_effects
645}
646
647/// Implementation of the [`Trigger::Block`] variant and its friends. It takes a list of known
648/// fields and their own `Trigger` validators, and checks that the given `block` contains only
649/// fields from that list and validates them.
650///
651/// The field names may have a prefix to indicate how they are to be used.
652/// * `?` means the field is optional
653/// * `*` means the field is optional and may occur multiple times
654/// * `+` means the field is required and may occur multiple times
655///
656/// The default is that the field is required and may occur only once.
657///
658/// Returns true iff the trigger had side effects (such as saving scopes).
659fn match_trigger_fields(
660    fields: &[(&str, Trigger)],
661    block: &Block,
662    data: &Everything,
663    sc: &mut ScopeContext,
664    tooltipped: Tooltipped,
665    negated: bool,
666    max_sev: Severity,
667) -> bool {
668    let mut side_effects = false;
669    let mut vd = Validator::new(block, data);
670    vd.set_max_severity(max_sev);
671    for (field, _) in fields {
672        if let Some(opt) = field.strip_prefix('?') {
673            vd.field_any_cmp(opt);
674        } else if let Some(mlt) = field.strip_prefix('*') {
675            vd.multi_field_any_cmp(mlt);
676        } else if let Some(mlt) = field.strip_prefix('+') {
677            vd.req_field(mlt);
678            vd.multi_field_any_cmp(mlt);
679        } else {
680            vd.req_field(field);
681            vd.field_any_cmp(field);
682        }
683    }
684
685    for Field(key, cmp, bv) in block.iter_fields() {
686        for (field, trigger) in fields {
687            let fieldname = if let Some(opt) = field.strip_prefix('?') {
688                opt
689            } else if let Some(mlt) = field.strip_prefix('*') {
690                mlt
691            } else if let Some(mlt) = field.strip_prefix('+') {
692                mlt
693            } else {
694                field
695            };
696            if key.is(fieldname) {
697                side_effects |= match_trigger_bv(
698                    trigger, key, *cmp, bv, data, sc, tooltipped, negated, max_sev,
699                );
700            }
701        }
702    }
703    side_effects
704}
705
706/// Takes a [`Trigger`] and a trigger field, and validates that the constraints
707/// specified by the `Trigger` hold.
708///
709/// Returns true iff the trigger had side effects (such as saving scopes).
710#[allow(clippy::too_many_arguments)]
711fn match_trigger_bv(
712    trigger: &Trigger,
713    name: &Token,
714    cmp: Comparator,
715    bv: &BV,
716    data: &Everything,
717    sc: &mut ScopeContext,
718    tooltipped: Tooltipped,
719    negated: bool,
720    max_sev: Severity,
721) -> bool {
722    let mut side_effects = false;
723    // True iff the comparator must be Comparator::Equals
724    let mut must_be_eq = true;
725    // True iff it's probably a mistake if the comparator is Comparator::Equals
726    #[cfg(any(feature = "ck3", feature = "vic3", feature = "hoi4", feature = "eu5"))]
727    let mut warn_if_eq = false;
728    #[cfg(not(any(feature = "ck3", feature = "vic3", feature = "hoi4", feature = "eu5")))]
729    let warn_if_eq = false;
730
731    match trigger {
732        Trigger::Boolean => {
733            if let Some(token) = bv.expect_value() {
734                validate_target(token, data, sc, Scopes::Bool);
735            }
736        }
737        Trigger::CompareValue => {
738            must_be_eq = false;
739            // TODO: check side_effects
740            if Game::is_jomini() {
741                #[cfg(feature = "jomini")]
742                validate_script_value(bv, data, sc);
743            } else {
744                #[cfg(feature = "hoi4")]
745                if let Some(token) = bv.expect_value() {
746                    validate_target(token, data, sc, Scopes::Value);
747                }
748            }
749        }
750        #[cfg(any(feature = "ck3", feature = "vic3", feature = "hoi4", feature = "eu5"))]
751        Trigger::CompareValueWarnEq => {
752            must_be_eq = false;
753            warn_if_eq = true;
754            // TODO: check side_effects
755            #[cfg(feature = "jomini")]
756            validate_script_value(bv, data, sc);
757            // TODO HOI4
758        }
759        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
760        Trigger::SetValue => {
761            // TODO: check side_effects
762            validate_script_value(bv, data, sc);
763        }
764        Trigger::CompareDate => {
765            must_be_eq = false;
766            if let Some(token) = bv.expect_value() {
767                if Date::from_str(token.as_str()).is_err() {
768                    let msg = format!("{name} expects a date value");
769                    warn(ErrorKey::Validation).msg(msg).loc(token).push();
770                }
771            }
772        }
773        #[cfg(any(feature = "vic3", feature = "eu5"))]
774        Trigger::ItemOrCompareValue(i) => {
775            if let Some(token) = bv.expect_value() {
776                if !data.item_exists(*i, token.as_str()) {
777                    must_be_eq = false;
778                    validate_target(token, data, sc, Scopes::Value);
779                }
780            }
781        }
782        Trigger::Scope(s) => {
783            if let Some(token) = bv.get_value() {
784                validate_target(token, data, sc, *s);
785            } else if s.contains(Scopes::Value) {
786                // TODO: check side_effects
787                #[cfg(feature = "jomini")]
788                validate_script_value(bv, data, sc);
789                // TODO HOI4
790            } else {
791                bv.expect_value();
792            }
793        }
794        Trigger::ScopeOkThis(s) => {
795            if let Some(token) = bv.get_value() {
796                validate_target_ok_this(token, data, sc, *s);
797            } else if s.contains(Scopes::Value) {
798                // TODO: check side_effects
799                #[cfg(feature = "jomini")]
800                validate_script_value(bv, data, sc);
801                // TODO HOI4
802            } else {
803                bv.expect_value();
804            }
805        }
806        Trigger::Item(i) => {
807            if let Some(token) = bv.expect_value() {
808                data.verify_exists_max_sev(*i, token, max_sev);
809            }
810        }
811        Trigger::ScopeOrItem(s, i) => {
812            if let Some(token) = bv.expect_value() {
813                if !data.item_exists(*i, token.as_str()) {
814                    validate_target(token, data, sc, *s);
815                }
816            }
817        }
818        Trigger::Choice(choices) => {
819            if let Some(token) = bv.expect_value() {
820                if !choices.iter().any(|c| token.is(c)) {
821                    let msg = format!("unknown value {token} for {name}");
822                    let info = format!("valid values are: {}", stringify_choices(choices));
823                    warn(ErrorKey::Validation).msg(msg).info(info).loc(token).push();
824                }
825            }
826        }
827        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
828        Trigger::CompareChoice(choices) => {
829            must_be_eq = false;
830            if let Some(token) = bv.expect_value() {
831                if !choices.contains(&token.as_str()) {
832                    let msg = format!("{name} expects one of {}", stringify_choices(choices));
833                    warn(ErrorKey::Validation).msg(msg).loc(token).push();
834                }
835            }
836        }
837        #[cfg(any(feature = "vic3", feature = "eu5"))]
838        Trigger::CompareChoiceOrNumber(choices) => {
839            must_be_eq = false;
840            if let Some(token) = bv.expect_value() {
841                if !token.is_number() && !choices.contains(&token.as_str()) {
842                    validate_target(token, data, sc, Scopes::Value);
843                }
844            }
845        }
846        Trigger::Block(fields) => {
847            if let Some(block) = bv.expect_block() {
848                side_effects |=
849                    match_trigger_fields(fields, block, data, sc, tooltipped, negated, max_sev);
850            }
851        }
852        #[cfg(feature = "ck3")]
853        Trigger::ScopeOrBlock(s, fields) => match bv {
854            BV::Value(token) => {
855                validate_target(token, data, sc, *s);
856            }
857            BV::Block(block) => {
858                side_effects |=
859                    match_trigger_fields(fields, block, data, sc, tooltipped, negated, max_sev);
860            }
861        },
862        #[cfg(feature = "ck3")]
863        Trigger::ItemOrBlock(i, fields) => match bv {
864            BV::Value(token) => data.verify_exists_max_sev(*i, token, max_sev),
865            BV::Block(block) => {
866                side_effects |=
867                    match_trigger_fields(fields, block, data, sc, tooltipped, negated, max_sev);
868            }
869        },
870        #[cfg(feature = "ck3")]
871        Trigger::IdentifierOrBlock(kind, fields) => match bv {
872            BV::Value(token) => validate_identifier(token, kind, Severity::Error),
873            BV::Block(block) => {
874                side_effects |=
875                    match_trigger_fields(fields, block, data, sc, tooltipped, negated, max_sev);
876            }
877        },
878        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
879        Trigger::BlockOrCompareValue(fields) => match bv {
880            BV::Value(t) => {
881                validate_target(t, data, sc, Scopes::Value);
882                must_be_eq = false;
883            }
884            BV::Block(b) => {
885                side_effects |=
886                    match_trigger_fields(fields, b, data, sc, tooltipped, negated, max_sev);
887            }
888        },
889        #[cfg(feature = "ck3")]
890        Trigger::ScopeList(s) => {
891            if let Some(block) = bv.expect_block() {
892                let mut vd = Validator::new(block, data);
893                vd.set_max_severity(max_sev);
894                for token in vd.values() {
895                    validate_target(token, data, sc, *s);
896                }
897            }
898        }
899        #[cfg(feature = "ck3")]
900        Trigger::ScopeCompare(s) => {
901            if let Some(block) = bv.expect_block() {
902                if block.iter_items().count() != 1 {
903                    let msg = "unexpected number of items in block";
904                    warn(ErrorKey::Validation).msg(msg).loc(block).push();
905                }
906                for Field(key, _, bv) in block.iter_fields_warn() {
907                    validate_target(key, data, sc, *s);
908                    if let Some(token) = bv.expect_value() {
909                        validate_target(token, data, sc, *s);
910                    }
911                }
912            }
913        }
914        #[cfg(feature = "ck3")]
915        Trigger::CompareToScope(s) => {
916            must_be_eq = false;
917            if let Some(token) = bv.expect_value() {
918                validate_target(token, data, sc, *s);
919            }
920        }
921        Trigger::Control => {
922            if let Some(block) = bv.expect_block() {
923                let mut negated = negated;
924                let name_lc = name.as_str().to_ascii_lowercase();
925                if name_lc == "all_false"
926                    || name_lc == "not"
927                    || name_lc == "nand"
928                    || name_lc == "nor"
929                {
930                    negated = !negated;
931                }
932                let mut tooltipped = tooltipped;
933                if name_lc == "custom_description" {
934                    tooltipped = Tooltipped::No;
935                }
936                let mut vd = Validator::new(block, data);
937                vd.set_max_severity(max_sev);
938                side_effects |= validate_trigger_internal(
939                    &Lowercase::from_string_unchecked(name_lc),
940                    ListType::None,
941                    block,
942                    data,
943                    sc,
944                    vd,
945                    tooltipped,
946                    negated,
947                );
948            }
949        }
950        Trigger::Special => {
951            if name.is("exists") {
952                if let Some(token) = bv.expect_value() {
953                    let multipart = token.as_str().contains('.');
954                    if token.is("yes") || token.is("no") {
955                        let msg = "`exists = yes/no` does not work";
956                        let info = if token.is("yes") {
957                            "try `exists = this`"
958                        } else {
959                            "try `NOT = { exists = this }`"
960                        };
961                        warn(ErrorKey::Scopes).msg(msg).info(info).loc(token).push();
962                    } else if token.starts_with("scope:") && !multipart {
963                        // exists = scope:name is used to check if that scope name was set
964                        if !negated {
965                            sc.exists_scope(token.as_str().strip_prefix("scope:").unwrap(), token);
966                        }
967                    } else if token.starts_with("local_var:") && !multipart {
968                        // exists = local_var:name is used to check if that local variable was set
969                        if !negated {
970                            sc.exists_local(
971                                token.as_str().strip_prefix("local_var:").unwrap(),
972                                token,
973                            );
974                        }
975                    } else if (token.starts_with("global_var:") || token.starts_with("var:"))
976                        && !multipart
977                    {
978                        // nothing
979                    } else if token.starts_with("flag:") {
980                        // exists = flag:$REASON$ is used in vanilla just to shut up their error.log,
981                        // so accept it silently even though it's a no-op.
982                    } else {
983                        validate_target_ok_this(token, data, sc, Scopes::all_but_none());
984
985                        if tooltipped.is_tooltipped() {
986                            if let Some(firstpart) = token.as_str().strip_suffix(".holder") {
987                                let msg = format!(
988                                    "could rewrite this as `{firstpart} = {{ is_title_created = yes }}`"
989                                );
990                                let info = "it gives a nicer tooltip";
991                                tips(ErrorKey::Tooltip).msg(msg).info(info).loc(name).push();
992                            }
993                        }
994                    }
995                }
996            } else if name.is("has_local_variable") {
997                if let Some(token) = bv.expect_value() {
998                    sc.exists_local(token.as_str(), token);
999                }
1000            } else if name.is("has_local_variable_list") {
1001                if let Some(token) = bv.expect_value() {
1002                    sc.exists_local_list(token.as_str(), token);
1003                }
1004            } else if name.is("is_target_in_local_variable_list") {
1005                if let Some(block) = bv.expect_block() {
1006                    let mut vd = Validator::new(block, data);
1007                    vd.set_max_severity(max_sev);
1008                    vd.req_field("name");
1009                    vd.req_field("target");
1010                    let name = vd.field_value("name").cloned();
1011                    for value in vd.multi_field_value("target") {
1012                        let target_scopes = name.as_ref().map_or(Scopes::all_but_none(), |name| {
1013                            sc.local_list_scopes(name.as_str(), data)
1014                        });
1015                        let outscopes = validate_target_ok_this(value, data, sc, target_scopes);
1016                        if let Some(ref name) = name {
1017                            sc.define_or_expect_local_list(name, outscopes, data);
1018                        }
1019                    }
1020                }
1021            } else if name.is("is_target_in_global_variable_list") {
1022                #[cfg(feature = "jomini")]
1023                if let Some(block) = bv.expect_block() {
1024                    let mut vd = Validator::new(block, data);
1025                    vd.set_max_severity(max_sev);
1026                    vd.req_field("name");
1027                    vd.req_field("target");
1028                    let name = vd.field_value("name").cloned();
1029                    for value in vd.multi_field_value("target") {
1030                        let target_scopes = name.as_ref().map_or(Scopes::all_but_none(), |name| {
1031                            data.global_list_scopes.scopes(name.as_str())
1032                        });
1033                        let outscopes = validate_target_ok_this(value, data, sc, target_scopes);
1034                        if let Some(ref name) = name {
1035                            data.global_list_scopes.expect(name.as_str(), name, outscopes);
1036                        }
1037                    }
1038                }
1039            } else if name.is("is_target_in_variable_list") {
1040                #[cfg(feature = "jomini")]
1041                if let Some(block) = bv.expect_block() {
1042                    let mut vd = Validator::new(block, data);
1043                    vd.set_max_severity(max_sev);
1044                    vd.req_field("name");
1045                    vd.req_field("target");
1046                    let name = vd.field_value("name").cloned();
1047                    for value in vd.multi_field_value("target") {
1048                        let target_scopes = name.as_ref().map_or(Scopes::all_but_none(), |name| {
1049                            data.variable_list_scopes.scopes(name.as_str())
1050                        });
1051                        let outscopes = validate_target_ok_this(value, data, sc, target_scopes);
1052                        if let Some(ref name) = name {
1053                            data.variable_list_scopes.expect(name.as_str(), name, outscopes);
1054                        }
1055                    }
1056                }
1057            } else if name.is("local_variable_list_size") {
1058                #[cfg(feature = "jomini")]
1059                if let Some(block) = bv.expect_block() {
1060                    let mut vd = Validator::new(block, data);
1061                    vd.set_max_severity(max_sev);
1062                    vd.req_field("name");
1063                    vd.req_field("value");
1064                    if let Some(name) = vd.field_value("name") {
1065                        sc.define_or_expect_local_list(name, Scopes::all_but_none(), data);
1066                    }
1067                    vd.multi_field_validated_any_cmp("value", |bv, data| {
1068                        validate_script_value(bv, data, sc);
1069                    });
1070                }
1071            } else if name.is("custom_tooltip") {
1072                match bv {
1073                    BV::Value(t) => data.verify_exists_max_sev(Item::Localization, t, max_sev),
1074                    BV::Block(b) => {
1075                        let mut vd = Validator::new(b, data);
1076                        vd.set_max_severity(max_sev);
1077                        side_effects |= validate_trigger_internal(
1078                            &Lowercase::new(name.as_str()),
1079                            ListType::None,
1080                            b,
1081                            data,
1082                            sc,
1083                            vd,
1084                            Tooltipped::No,
1085                            negated,
1086                        );
1087                    }
1088                }
1089            } else if name.is("has_gene") {
1090                #[cfg(feature = "jomini")]
1091                if let Some(block) = bv.expect_block() {
1092                    let mut vd = Validator::new(block, data);
1093                    vd.set_max_severity(max_sev);
1094                    vd.field_item("category", Item::GeneCategory);
1095                    if let Some(category) = block.get_field_value("category") {
1096                        if let Some(template) = vd.field_value("template") {
1097                            Gene::verify_has_template(category.as_str(), template, data);
1098                        }
1099                    }
1100                }
1101            } else if name.is("save_temporary_opinion_value_as") {
1102                if let Some(block) = bv.expect_block() {
1103                    let mut vd = Validator::new(block, data);
1104                    vd.set_max_severity(max_sev);
1105                    vd.req_field("name");
1106                    vd.req_field("target");
1107                    vd.field_target("target", sc, Scopes::Character);
1108                    if let Some(name) = vd.field_value("name") {
1109                        sc.define_name_token(name.as_str(), Scopes::Value, name, Temporary::Yes);
1110                        side_effects = true;
1111                    }
1112                }
1113            } else if name.is("save_temporary_scope_value_as") {
1114                #[cfg(feature = "jomini")]
1115                if let Some(block) = bv.expect_block() {
1116                    let mut vd = Validator::new(block, data);
1117                    vd.set_max_severity(max_sev);
1118                    vd.req_field("name");
1119                    vd.req_field("value");
1120                    vd.field_validated("value", |bv, data| match bv {
1121                        BV::Value(token) => {
1122                            validate_target(token, data, sc, Scopes::primitive());
1123                        }
1124                        BV::Block(_) => validate_script_value(bv, data, sc),
1125                    });
1126                    // TODO: figure out the scope type of `value` and use that
1127                    if let Some(name) = vd.field_value("name") {
1128                        sc.define_name_token(
1129                            name.as_str(),
1130                            Scopes::primitive(),
1131                            name,
1132                            Temporary::Yes,
1133                        );
1134                        side_effects = true;
1135                    }
1136                }
1137            } else if name.is("save_temporary_scope_as") {
1138                if let Some(name) = bv.expect_value() {
1139                    sc.save_current_scope(name.as_str(), Temporary::Yes);
1140                    side_effects = true;
1141                }
1142            } else if name.is("weighted_calc_true_if") {
1143                if let Some(block) = bv.expect_block() {
1144                    let mut vd = Validator::new(block, data);
1145                    vd.set_max_severity(max_sev);
1146                    if let Some(bv) = vd.field_any_cmp("amount") {
1147                        if let Some(token) = bv.expect_value() {
1148                            token.expect_number();
1149                        }
1150                    }
1151                    for (_, block) in vd.integer_blocks() {
1152                        let vd = Validator::new(block, data);
1153                        side_effects |= validate_trigger_internal(
1154                            &Lowercase::new_unchecked("weighted_calc_true_if"),
1155                            ListType::None,
1156                            block,
1157                            data,
1158                            sc,
1159                            vd,
1160                            tooltipped,
1161                            false,
1162                        );
1163                        if block.num_items() > 1 {
1164                            let msg = "`weighted_calc_true_if` accepts only one trigger per weight (confirmed for 1.16)";
1165                            let info = "and no scripted triggers";
1166                            err(ErrorKey::Bugs).msg(msg).info(info).loc(block).push();
1167                        }
1168                    }
1169                }
1170            } else if name.is("switch") {
1171                if let Some(block) = bv.expect_block() {
1172                    let mut vd = Validator::new(block, data);
1173                    vd.set_max_severity(max_sev);
1174                    vd.req_field("trigger");
1175                    if let Some(target) = vd.field_value("trigger") {
1176                        let target = target.clone();
1177                        let mut count = 0;
1178                        vd.set_allow_questionmark_equals(true);
1179                        vd.unknown_block_fields(|key, block| {
1180                            count += 1;
1181                            if !key.is("fallback") {
1182                                let synthetic_bv = BV::Value(key.clone());
1183                                validate_trigger_key_bv(
1184                                    &target,
1185                                    Comparator::Equals(Single),
1186                                    &synthetic_bv,
1187                                    data,
1188                                    sc,
1189                                    tooltipped,
1190                                    negated,
1191                                    max_sev,
1192                                );
1193                            }
1194                            side_effects |= validate_trigger(block, data, sc, tooltipped);
1195                        });
1196                        if count == 0 {
1197                            let msg = "switch with no branches";
1198                            err(ErrorKey::Logic).msg(msg).loc(name).push();
1199                        }
1200                    }
1201                }
1202            } else if name.is("add_to_temporary_list") {
1203                if let Some(value) = bv.expect_value() {
1204                    sc.define_or_expect_list_this(value, data, Temporary::Yes);
1205                    side_effects = true;
1206                }
1207            } else if name.is("is_in_list") {
1208                if let Some(value) = bv.expect_value() {
1209                    sc.expect_list(value, data);
1210                }
1211            } else if name.is("is_researching_technology") {
1212                #[cfg(feature = "vic3")]
1213                if let Some(value) = bv.expect_value() {
1214                    if !value.is("any") {
1215                        data.verify_exists(Item::Technology, value);
1216                    }
1217                }
1218            }
1219            // TODO: time_of_year
1220        }
1221        #[cfg(feature = "hoi4")]
1222        Trigger::Iterator(ltype, outscope) => {
1223            let it_name = name.split_once('_').unwrap().1;
1224            if let Some(block) = bv.expect_block() {
1225                precheck_iterator_fields(*ltype, it_name.as_str(), block, data, sc);
1226                sc.open_scope(*outscope, name.clone());
1227                let mut vd = Validator::new(block, data);
1228                vd.set_max_severity(max_sev);
1229                side_effects |= validate_trigger_internal(
1230                    &Lowercase::new(it_name.as_str()),
1231                    *ltype,
1232                    block,
1233                    data,
1234                    sc,
1235                    vd,
1236                    tooltipped,
1237                    negated,
1238                );
1239                sc.close();
1240            }
1241        }
1242        Trigger::Identifier(kind) => {
1243            if let Some(token) = bv.expect_value() {
1244                validate_identifier(token, kind, Severity::Error);
1245            }
1246        }
1247        #[cfg(feature = "hoi4")]
1248        Trigger::Flag => {
1249            if let Some(token) = bv.expect_value() {
1250                if tooltipped.is_tooltipped() && !token.as_str().contains('@') {
1251                    data.verify_exists(Item::Localization, token);
1252                }
1253                validate_flag_name(token);
1254            }
1255        }
1256        #[cfg(feature = "hoi4")]
1257        Trigger::FlagOrBlock(fields) => {
1258            if name.is("has_unit_leader_flag") {
1259                let msg = "deprecated in favor of has_character_flag";
1260                warn(ErrorKey::Deprecated).msg(msg).loc(name).push();
1261            }
1262
1263            match bv {
1264                BV::Value(token) => {
1265                    if tooltipped.is_tooltipped() && !token.as_str().contains('@') {
1266                        data.verify_exists(Item::Localization, token);
1267                    }
1268                    validate_flag_name(token);
1269                }
1270                BV::Block(block) => {
1271                    side_effects |=
1272                        match_trigger_fields(fields, block, data, sc, tooltipped, negated, max_sev);
1273                }
1274            }
1275        }
1276        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1277        Trigger::Removed(msg, info) => {
1278            err(ErrorKey::Removed).msg(*msg).info(*info).loc(name).push();
1279        }
1280        Trigger::UncheckedValue => {
1281            bv.expect_value();
1282            side_effects = true; // have to assume it's possible
1283        }
1284        Trigger::UncheckedTodo => {
1285            side_effects = true; // have to assume it's possible
1286        }
1287    }
1288
1289    if matches!(cmp, Comparator::Equals(_)) {
1290        if warn_if_eq {
1291            let msg = format!(
1292                "`{name} {cmp}` means exactly equal to that amount, which is usually not what you want"
1293            );
1294            warn(ErrorKey::Logic).msg(msg).loc(name).push();
1295        }
1296    } else if must_be_eq {
1297        let msg = format!("unexpected comparator {cmp}");
1298        warn(ErrorKey::Validation).msg(msg).loc(name).push();
1299    }
1300    side_effects
1301}
1302
1303/// Validate that `token` is valid as the right-hand side of a field.
1304///
1305/// `outscopes` is the set of scope types that this target is allowed to produce.
1306/// * Example: in `has_claim_on = title:e_byzantium`, the target is `title:e_byzantium` and it
1307///   should produce a [`Scopes::LandedTitle`] scope in order to be valid for `has_claim_on`.
1308///
1309/// Returns the best guess about the scope type that this target resolves to.
1310pub fn validate_target_ok_this(
1311    token: &Token,
1312    data: &Everything,
1313    sc: &mut ScopeContext,
1314    outscopes: Scopes,
1315) -> Scopes {
1316    if token.is_number() {
1317        #[allow(unused_mut)] // only needs mut for hoi4
1318        let mut number_scope = Scopes::Value;
1319        #[cfg(feature = "hoi4")]
1320        if Game::is_hoi4() && data.item_exists(Item::State, token.as_str()) {
1321            number_scope |= Scopes::State;
1322        }
1323        if !outscopes.intersects(number_scope | Scopes::None) {
1324            let msg = format!("expected {outscopes}");
1325            warn(ErrorKey::Scopes).msg(msg).loc(token).push();
1326        }
1327        return number_scope;
1328    }
1329    #[cfg(feature = "hoi4")]
1330    if Game::is_hoi4() {
1331        if token.starts_with("var:")
1332            || token.starts_with("global.")
1333            || token.as_str().contains('^')
1334            || token.as_str().contains('@')
1335            || token.as_str().contains('?')
1336        {
1337            validate_variable(token, data, sc, Severity::Error);
1338            return Scopes::all_but_none();
1339        }
1340        if data.variables.variable_exists(token.as_str()) {
1341            return Scopes::all_but_none();
1342        }
1343    }
1344    let part_vec = partition(token);
1345    sc.open_builder();
1346    for i in 0..part_vec.len() {
1347        let mut part_flags = PartFlags::empty();
1348        if i == 0 {
1349            part_flags |= PartFlags::First;
1350        }
1351        if i + 1 == part_vec.len() {
1352            part_flags |= PartFlags::Last;
1353        }
1354        let part = &part_vec[i];
1355
1356        match part {
1357            Part::TokenArgument(part, func, arg) => {
1358                validate_argument(part_flags, part, func, arg, data, sc);
1359            }
1360            Part::Token(part) => {
1361                let part_lc = Lowercase::new(part.as_str());
1362                // prefixed scope transition, e.g. cp:councillor_steward
1363                if let Some((prefix, mut arg)) = part.split_once(':') {
1364                    // event_id have multiple parts separated by `.`
1365                    let is_event_id = prefix.lowercase_is("event_id");
1366                    if is_event_id {
1367                        arg = token.split_once(':').unwrap().1;
1368                    }
1369                    if !validate_prefix(part_flags, part, &prefix, &arg, data, sc) {
1370                        sc.close();
1371                        return Scopes::all();
1372                    }
1373                    if is_event_id {
1374                        break; // force last part
1375                    }
1376                } else if part_lc == "root" {
1377                    sc.replace_root();
1378                } else if part_lc == "prev" {
1379                    if !part_flags.contains(PartFlags::First) && !Game::is_imperator() {
1380                        warn_not_first(part);
1381                    }
1382                    sc.replace_prev();
1383                } else if part_lc == "this" {
1384                    sc.replace_this();
1385                } else if Game::is_hoi4() && part_lc == "from" {
1386                    #[cfg(feature = "hoi4")]
1387                    sc.replace_from();
1388                } else if data.script_value_exists(part.as_str()) {
1389                    // TODO: check side_effects
1390                    #[cfg(feature = "jomini")]
1391                    data.script_values.validate_call(part, data, sc);
1392                    sc.replace(Scopes::Value, part.clone());
1393                } else if let Some((inscopes, outscope)) = scope_to_scope(part, sc.scopes(data)) {
1394                    #[cfg(feature = "imperator")]
1395                    if let Some(inscopes) = trigger_comparevalue(part, data) {
1396                        // If a trigger of the same name exists, and it's compatible with this
1397                        // location and scope context, then that trigger takes precedence.
1398                        if part_flags.contains(PartFlags::Last)
1399                            && (inscopes.contains(Scopes::None)
1400                                || sc.scopes(data).intersects(inscopes))
1401                        {
1402                            validate_inscopes(part_flags, part, inscopes, sc, data);
1403                            sc.replace(Scopes::Value, part.clone());
1404                            continue;
1405                        }
1406                    }
1407                    validate_inscopes(part_flags, part, inscopes, sc, data);
1408                    sc.replace(outscope, part.clone());
1409                } else if let Some(inscopes) = trigger_comparevalue(part, data) {
1410                    if !part_flags.contains(PartFlags::Last) {
1411                        let msg = format!("`{part}` should be the last part");
1412                        warn(ErrorKey::Validation).msg(msg).loc(part).push();
1413                        sc.close();
1414                        return Scopes::all();
1415                    }
1416                    validate_inscopes(part_flags, part, inscopes, sc, data);
1417                    sc.replace(Scopes::Value, part.clone());
1418                } else if Game::is_hoi4() && is_country_tag(part.as_str()) {
1419                    if !part_flags.contains(PartFlags::First) {
1420                        warn_not_first(part);
1421                    }
1422                    #[cfg(feature = "hoi4")]
1423                    data.verify_exists(Item::CountryTag, part);
1424                    #[cfg(feature = "hoi4")]
1425                    sc.replace(Scopes::Country, part.clone());
1426                } else if is_character_token(part.as_str(), data) {
1427                    #[cfg(feature = "hoi4")]
1428                    sc.replace(Scopes::Character, part.clone());
1429                } else if Game::is_hoi4() && part.is_integer() {
1430                    #[cfg(feature = "hoi4")]
1431                    data.verify_exists(Item::State, part);
1432                    #[cfg(feature = "hoi4")]
1433                    sc.replace(Scopes::State, part.clone());
1434                } else {
1435                    // See if the user forgot a prefix like `faith:` or `culture:`
1436                    let mut opt_info = None;
1437                    if part_flags.contains(PartFlags::First | PartFlags::Last) {
1438                        if let Some(prefix) = needs_prefix(part.as_str(), data, outscopes) {
1439                            opt_info = Some(format!("did you mean `{prefix}:{part}` ?"));
1440                        }
1441                    }
1442
1443                    // TODO: warn if trying to use iterator here
1444                    let msg = format!("unknown token `{part}`");
1445                    err(ErrorKey::UnknownField).msg(msg).opt_info(opt_info).loc(part).push();
1446                    sc.close();
1447                    return Scopes::all();
1448                }
1449            }
1450        }
1451    }
1452    let (final_scopes, because) = sc.scopes_reason(data);
1453    if !outscopes.intersects(final_scopes | Scopes::None) {
1454        let part = &part_vec[part_vec.len() - 1];
1455        let msg = format!("`{part}` produces {final_scopes} but expected {outscopes}");
1456        // Must not be at the same location to avoid spurious error messages
1457        let opt_loc = (part.loc() != because.token().loc).then(|| because.token());
1458        let msg2 = format!("scope was {}", because.msg());
1459        warn(ErrorKey::Scopes).msg(msg).loc(part).opt_loc_msg(opt_loc, msg2).push();
1460    }
1461    sc.close();
1462    final_scopes
1463}
1464
1465/// Just like [`validate_target_ok_this`], but warns if the target is a literal `this` because that
1466/// is usually a mistake.
1467pub fn validate_target(
1468    token: &Token,
1469    data: &Everything,
1470    sc: &mut ScopeContext,
1471    outscopes: Scopes,
1472) -> Scopes {
1473    if token.is("this") {
1474        let msg = "target `this` makes no sense here";
1475        warn(ErrorKey::UseOfThis).msg(msg).loc(token).push();
1476    }
1477    validate_target_ok_this(token, data, sc, outscopes)
1478}
1479
1480/// A part in a token chain
1481#[derive(Debug, Clone, PartialEq, Eq)]
1482pub enum Part {
1483    /// A simple token
1484    Token(Token),
1485    /// Whole part, function and argument tokens
1486    TokenArgument(Token, Token, Token),
1487}
1488
1489impl std::fmt::Display for Part {
1490    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1491        match self {
1492            Part::Token(token) => token.fmt(f),
1493            Part::TokenArgument(part, _, _) => part.fmt(f),
1494        }
1495    }
1496}
1497
1498impl Part {
1499    fn loc(&self) -> Loc {
1500        match self {
1501            Part::Token(t) | Part::TokenArgument(t, _, _) => t.loc,
1502        }
1503    }
1504}
1505
1506/// This function partitions the input token into parts separated by `.`. Each part may contain either a token
1507/// or a token-argument pair when using the `()`-syntax, e.g. `"prowess_diff(liege)"`. It does not validate the tokens
1508/// or arguments, but simply parses and detects any syntactical errors.
1509pub fn partition(token: &Token) -> Vec<Part> {
1510    let mut parts = Vec::new();
1511
1512    let mut has_part_argument = false;
1513    let mut has_part_argument_erred = false;
1514    let mut paren_depth = 0;
1515    let (mut part_idx, mut part_col) = (0, 0);
1516    let (mut first_paren_idx, mut first_paren_col) = (0, 0);
1517    let (mut second_paren_idx, mut second_paren_col) = (0, 0);
1518
1519    for (col, (idx, ch)) in token.as_str().char_indices().enumerate() {
1520        let col = u32::try_from(col).expect("internal error: 2^32 columns");
1521        match ch {
1522            '.' => {
1523                if paren_depth == 0 {
1524                    if part_idx == idx {
1525                        // Empty part; err but skip it since it's likely a typo
1526                        let mut loc = token.loc;
1527                        loc.column += col;
1528                        err(ErrorKey::Validation).msg("empty part").loc(loc).push();
1529                    } else if !has_part_argument {
1530                        // The just completed part has no argument
1531                        let mut part_loc = token.loc;
1532                        part_loc.column += part_col;
1533                        #[allow(unused_mut)]
1534                        let mut part_token = token.subtoken(part_idx..idx, part_loc);
1535                        #[cfg(feature = "imperator")]
1536                        // Imperator has a `hidden:` prefix that can go before other prefixes so it
1537                        // has to be handled specially.
1538                        if let Some(hidden_arg) = part_token.strip_prefix("hidden:") {
1539                            part_token = hidden_arg;
1540                        }
1541                        parts.push(Part::Token(part_token));
1542                    }
1543                    has_part_argument = false;
1544                    has_part_argument_erred = false;
1545                    part_col = col + 1;
1546                    part_idx = idx + 1;
1547                }
1548            }
1549            '(' => {
1550                if paren_depth == 0 {
1551                    first_paren_col = col;
1552                    first_paren_idx = idx;
1553                } else if paren_depth == 1 {
1554                    second_paren_col = col;
1555                    second_paren_idx = idx;
1556                }
1557
1558                paren_depth += 1;
1559            }
1560            ')' => {
1561                if paren_depth == 0 {
1562                    // Missing opening parenthesis `(`
1563                    let mut loc = token.loc;
1564                    loc.column += col;
1565                    err(ErrorKey::Validation)
1566                        .msg("closing without opening parenthesis `(`")
1567                        .loc(loc)
1568                        .push();
1569                } else if paren_depth == 1 {
1570                    // Argument between parentheses
1571                    let mut func_loc = token.loc;
1572                    func_loc.column += part_col;
1573                    let func_token = token.subtoken(part_idx..first_paren_idx, func_loc);
1574
1575                    let mut arg_loc = token.loc;
1576                    arg_loc.column += first_paren_col + 1;
1577                    let arg_token = token.subtoken_stripped(first_paren_idx + 1..idx, arg_loc);
1578
1579                    parts.push(Part::TokenArgument(token.clone(), func_token, arg_token));
1580                    has_part_argument = true;
1581                    paren_depth -= 1;
1582                } else if paren_depth == 2 {
1583                    // Cannot have nested parentheses
1584                    let mut loc = token.loc;
1585                    loc.column += second_paren_col;
1586                    let nested_paren_token = token.subtoken(second_paren_idx..=idx, loc);
1587                    err(ErrorKey::Validation)
1588                        .msg("cannot have nested parentheses")
1589                        .loc(nested_paren_token)
1590                        .push();
1591                    paren_depth -= 1;
1592                }
1593            }
1594            _ => {
1595                // an argument can only be the last part or followed by dot `.` AND hasn't erred from it yet
1596                if has_part_argument && !has_part_argument_erred {
1597                    let mut loc = token.loc;
1598                    loc.column += col;
1599                    err(ErrorKey::Validation)
1600                        .msg("argument can only be the last part or followed by dot `.`")
1601                        .loc(loc)
1602                        .push();
1603                    has_part_argument_erred = true;
1604                }
1605            }
1606        }
1607    }
1608
1609    if paren_depth > 0 {
1610        // Missing closing parenthesis `)`
1611        let mut loc = token.loc;
1612        loc.column += first_paren_col;
1613        let broken_token = token.subtoken(first_paren_idx.., loc);
1614        err(ErrorKey::Validation)
1615            .msg("opening without closing parenthesis `)`")
1616            .loc(broken_token)
1617            .push();
1618    }
1619
1620    if part_idx == token.as_str().len() {
1621        // Trailing `.`
1622        let mut loc = token.loc;
1623        loc.column += part_col;
1624        err(ErrorKey::Validation).msg("trailing dot `.`").loc(loc).push();
1625    } else if !has_part_argument {
1626        // final part (without argument)
1627        let mut part_loc = token.loc;
1628        part_loc.column += part_col;
1629        // SAFETY: part_idx < token.as_str.len()
1630        #[allow(unused_mut)]
1631        let mut part_token = token.subtoken(part_idx.., part_loc);
1632        #[cfg(feature = "imperator")]
1633        // see above
1634        if let Some(hidden_arg) = part_token.strip_prefix("hidden:") {
1635            part_token = hidden_arg;
1636        }
1637        parts.push(Part::Token(part_token));
1638    }
1639    parts
1640}
1641
1642bitflags! {
1643    // Attributes can be applied to flags types
1644    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1645    pub struct PartFlags: u8 {
1646        const First = 0b_0000_0001;
1647        const Last = 0b_0000_0010;
1648        const Question = 0b_0000_0100;
1649    }
1650}
1651
1652#[inline]
1653pub fn warn_not_first(name: &Token) {
1654    let msg = format!("`{name}:` makes no sense except as first part");
1655    warn(ErrorKey::Validation).msg(msg).loc(name).push();
1656}
1657
1658pub fn validate_inscopes(
1659    part_flags: PartFlags,
1660    name: &Token,
1661    inscopes: Scopes,
1662    sc: &mut ScopeContext,
1663    data: &Everything,
1664) {
1665    // If the part does not use its inscope then any parts that come before it are useless
1666    // and probably indicate a mistake is being made.
1667    if inscopes == Scopes::None && !part_flags.contains(PartFlags::First) {
1668        warn_not_first(name);
1669    }
1670    sc.expect(inscopes, &Reason::Token(name.clone()), data);
1671}
1672
1673#[allow(unused_variables)] // imperator does not use sc
1674fn validate_argument_internal(
1675    arg: &Token,
1676    validation: ArgumentValue,
1677    data: &Everything,
1678    sc: &mut ScopeContext,
1679) {
1680    match validation {
1681        ArgumentValue::Item(item) => data.verify_exists(item, arg),
1682        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1683        ArgumentValue::Scope(scope) => {
1684            let stash = sc.stash_builder();
1685            validate_target_ok_this(arg, data, sc, scope);
1686            sc.unstash_builder(stash);
1687        }
1688        #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1689        ArgumentValue::ScopeOrItem(scope, item) => {
1690            if !data.item_exists(item, arg.as_str()) {
1691                let stash = sc.stash_builder();
1692                validate_target_ok_this(arg, data, sc, scope);
1693                sc.unstash_builder(stash);
1694            }
1695        }
1696        #[cfg(feature = "ck3")]
1697        ArgumentValue::TraitTrack => {
1698            if let Some((traitname, track)) = arg.split_once('|') {
1699                // TODO: verify that the track belongs to this trait
1700                data.verify_exists(Item::Trait, &traitname);
1701                data.verify_exists(Item::TraitTrack, &track);
1702            } else if !data.item_exists(Item::Trait, arg.as_str()) {
1703                let stash = sc.stash_builder();
1704                validate_target(arg, data, sc, Scopes::Trait);
1705                sc.unstash_builder(stash);
1706            }
1707        }
1708        #[cfg(feature = "eu5")]
1709        ArgumentValue::Multiple(specs) => {
1710            let args = arg.split('|');
1711            // TODO: EU5 if the arguments are all mandatory, also check for not enough arguments
1712            if args.len() > specs.len() {
1713                let msg = format!("too many arguments for trigger; expected {}", specs.len());
1714                warn(ErrorKey::Validation).msg(msg).loc(&args[specs.len()]).push();
1715            } else if args.len() < specs.len() {
1716                let msg = format!("too few arguments for trigger; expected {}", specs.len());
1717                warn(ErrorKey::Validation).msg(msg).loc(arg).push();
1718            }
1719            for (arg, spec) in args.into_iter().zip(specs) {
1720                validate_argument_internal(&arg, *spec, data, sc);
1721            }
1722        }
1723        #[cfg(any(feature = "vic3", feature = "imperator", feature = "eu5"))]
1724        ArgumentValue::Modif => {
1725            // TODO: deduce the ModifKinds from the `this` scope
1726            match Game::game() {
1727                #[cfg(feature = "vic3")]
1728                Game::Vic3 => verify_modif_exists(
1729                    arg,
1730                    data,
1731                    crate::vic3::modif::ModifKinds::all(),
1732                    Severity::Warning,
1733                ),
1734                #[cfg(feature = "imperator")]
1735                Game::Imperator => verify_modif_exists(
1736                    arg,
1737                    data,
1738                    crate::imperator::modif::ModifKinds::all(),
1739                    Severity::Warning,
1740                ),
1741                #[allow(unreachable_patterns)]
1742                _ => unreachable!(),
1743            }
1744        }
1745        #[cfg(any(feature = "vic3", feature = "ck3", feature = "eu5"))]
1746        ArgumentValue::Identifier(kind) => {
1747            validate_identifier(arg, kind, Severity::Error);
1748        }
1749        ArgumentValue::UncheckedValue => (),
1750        #[cfg(feature = "ck3")]
1751        ArgumentValue::Removed(version, info) => {
1752            let msg = format!("removed in {version}");
1753            err(ErrorKey::Removed).msg(msg).info(info).loc(arg).push();
1754        }
1755    }
1756}
1757
1758/// Validate for scope and not trigger arguments
1759pub fn validate_argument_scope(
1760    part_flags: PartFlags,
1761    (inscopes, outscopes, validation): (Scopes, Scopes, ArgumentValue),
1762    part: &Token,
1763    func: &Token,
1764    arg: &Token,
1765    data: &Everything,
1766    sc: &mut ScopeContext,
1767) {
1768    validate_inscopes(part_flags, func, inscopes, sc, data);
1769    validate_argument_internal(arg, validation, data, sc);
1770
1771    if func.lowercase_is("scope") {
1772        if part_flags.contains(PartFlags::Last | PartFlags::Question) {
1773            sc.exists_scope(arg.as_str(), part);
1774        }
1775        sc.replace_named_scope(arg.as_str(), part);
1776    } else if func.lowercase_is("local_var") {
1777        if part_flags.contains(PartFlags::Last | PartFlags::Question) {
1778            sc.exists_local(arg.as_str(), part);
1779        }
1780        sc.replace_local_variable(arg.as_str(), part);
1781    } else if func.lowercase_is("global_var") {
1782        #[cfg(feature = "jomini")]
1783        sc.replace_global_variable(arg.as_str(), part);
1784    } else if func.lowercase_is("var") {
1785        #[cfg(feature = "jomini")]
1786        sc.replace_variable(arg.as_str(), part);
1787    } else {
1788        sc.replace(outscopes, part.clone());
1789    }
1790}
1791
1792/// Validate that the argument passed through is valid, either being of a complex trigger compare value,
1793/// or a scope prefix.
1794#[allow(unreachable_code, unused_variables)]
1795pub fn validate_argument(
1796    part_flags: PartFlags,
1797    part: &Token,
1798    func: &Token,
1799    arg: &Token,
1800    data: &Everything,
1801    sc: &mut ScopeContext,
1802) {
1803    #[cfg(feature = "imperator")]
1804    if Game::is_imperator() {
1805        // Imperator does not use `()`
1806        let msg = "imperator does not support the `()` syntax";
1807        let mut opening_paren_loc = arg.loc;
1808        opening_paren_loc.column -= 1;
1809        err(ErrorKey::WrongGame).msg(msg).loc(opening_paren_loc).push();
1810        return;
1811    }
1812
1813    #[cfg(feature = "hoi4")]
1814    if Game::is_hoi4() {
1815        let msg = "hoi4 does not support the `()` syntax";
1816        let mut opening_paren_loc = arg.loc;
1817        opening_paren_loc.column -= 1;
1818        err(ErrorKey::WrongGame).msg(msg).loc(opening_paren_loc).push();
1819        return;
1820    }
1821
1822    let scope_trigger_complex: fn(&str) -> Option<(Scopes, ArgumentValue, Scopes)> =
1823        match Game::game() {
1824            #[cfg(feature = "ck3")]
1825            Game::Ck3 => crate::ck3::tables::triggers::scope_trigger_complex,
1826            #[cfg(feature = "vic3")]
1827            Game::Vic3 => crate::vic3::tables::triggers::scope_trigger_complex,
1828            #[cfg(feature = "imperator")]
1829            Game::Imperator => unreachable!(),
1830            #[cfg(feature = "eu5")]
1831            Game::Eu5 => crate::eu5::tables::triggers::scope_trigger_complex,
1832            #[cfg(feature = "hoi4")]
1833            Game::Hoi4 => unreachable!(),
1834        };
1835
1836    let func_lc = func.as_str().to_ascii_lowercase();
1837    if let Some((inscopes, validation, outscopes)) = scope_trigger_complex(&func_lc) {
1838        sc.expect(inscopes, &Reason::Token(func.clone()), data);
1839        validate_argument_internal(arg, validation, data, sc);
1840        sc.replace(outscopes, func.clone());
1841    } else if let Some(entry) = scope_prefix(func) {
1842        validate_argument_scope(part_flags, entry, part, func, arg, data, sc);
1843    } else {
1844        let msg = format!("unknown token `{func}:`");
1845        err(ErrorKey::Validation).msg(msg).loc(func).push();
1846    }
1847}
1848
1849/// Validate a `prefix:value` construct in a trigger.
1850///
1851/// Very similar to `validate_argument` because the notations are almost interchangeable in script.
1852#[allow(unreachable_code, unused_variables)]
1853pub fn validate_prefix(
1854    part_flags: PartFlags,
1855    part: &Token,
1856    prefix: &Token,
1857    arg: &Token,
1858    data: &Everything,
1859    sc: &mut ScopeContext,
1860) -> bool {
1861    fn scope_trigger_complex(prefix: &str) -> Option<(Scopes, ArgumentValue, Scopes)> {
1862        match Game::game() {
1863            #[cfg(feature = "ck3")]
1864            Game::Ck3 => crate::ck3::tables::triggers::scope_trigger_complex(prefix),
1865            #[cfg(feature = "vic3")]
1866            Game::Vic3 => crate::vic3::tables::triggers::scope_trigger_complex(prefix),
1867            #[cfg(feature = "imperator")]
1868            Game::Imperator => None,
1869            #[cfg(feature = "eu5")]
1870            Game::Eu5 => crate::eu5::tables::triggers::scope_trigger_complex(prefix),
1871            #[cfg(feature = "hoi4")]
1872            Game::Hoi4 => None,
1873        }
1874    }
1875
1876    let prefix_lc = prefix.as_str().to_ascii_lowercase();
1877    if let Some((inscopes, validation, outscopes)) = scope_trigger_complex(&prefix_lc) {
1878        sc.expect(inscopes, &Reason::Token(prefix.clone()), data);
1879        validate_argument_internal(arg, validation, data, sc);
1880        sc.replace(outscopes, prefix.clone());
1881        true
1882    } else if let Some(entry) = scope_prefix(prefix) {
1883        validate_argument_scope(part_flags, entry, part, prefix, arg, data, sc);
1884        true
1885    } else {
1886        let msg = format!("unknown prefix `{prefix:}`");
1887        err(ErrorKey::Validation).msg(msg).loc(prefix).push();
1888        false
1889    }
1890}
1891
1892/// A description of the constraints on the right-hand side of a given trigger.
1893/// In other words, how it can be used.
1894///
1895/// It is used recursively in variants like [`Trigger::Block`], where each of the sub fields have
1896/// their own `Trigger`.
1897#[derive(Copy, Clone, Debug, Eq, PartialEq)]
1898#[allow(dead_code)] // TODO: remove when hoi4 is complete
1899pub enum Trigger {
1900    /// trigger = no or trigger = yes
1901    Boolean,
1902    /// can be a script value
1903    CompareValue,
1904    /// can be a script value; warn if =
1905    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5", feature = "hoi4"))]
1906    CompareValueWarnEq,
1907    /// can be a script value; no < or >
1908    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1909    SetValue,
1910    /// value must be a valid date
1911    CompareDate,
1912    /// trigger is either = item or compared to another trigger
1913    #[cfg(any(feature = "vic3", feature = "eu5"))]
1914    ItemOrCompareValue(Item),
1915    /// trigger is compared to a scope object
1916    Scope(Scopes),
1917    /// trigger is compared to a scope object which may be `this`
1918    ScopeOkThis(Scopes),
1919    /// value is chosen from an item type
1920    Item(Item),
1921    ScopeOrItem(Scopes, Item),
1922    /// value is chosen from a list given here
1923    Choice(&'static [&'static str]),
1924    /// value is from a list given here that can be compared
1925    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1926    CompareChoice(&'static [&'static str]),
1927    /// like `CompareChoice` but value can also be just a number
1928    #[cfg(any(feature = "vic3", feature = "eu5"))]
1929    CompareChoiceOrNumber(&'static [&'static str]),
1930    /// For Block, if a field name in the array starts with ? it means that field is optional
1931    /// trigger takes a block with these fields
1932    Block(&'static [(&'static str, Trigger)]),
1933    /// trigger takes a block with these fields
1934    #[cfg(feature = "ck3")]
1935    ScopeOrBlock(Scopes, &'static [(&'static str, Trigger)]),
1936    /// trigger takes a block with these fields
1937    #[cfg(feature = "ck3")]
1938    ItemOrBlock(Item, &'static [(&'static str, Trigger)]),
1939    /// trigger takes a single identifier or a block with these fields
1940    #[cfg(feature = "ck3")]
1941    IdentifierOrBlock(&'static str, &'static [(&'static str, Trigger)]),
1942    /// can be part of a scope chain but also a standalone trigger
1943    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1944    BlockOrCompareValue(&'static [(&'static str, Trigger)]),
1945    /// trigger takes a block of values of this scope type
1946    #[cfg(feature = "ck3")]
1947    ScopeList(Scopes),
1948    /// trigger takes a block comparing two scope objects
1949    #[cfg(feature = "ck3")]
1950    ScopeCompare(Scopes),
1951    /// this is for inside a Block, where a key is compared to a scope object
1952    #[cfg(feature = "ck3")]
1953    CompareToScope(Scopes),
1954    /// trigger is an iterator that does not follow the regular pattern
1955    #[cfg(feature = "hoi4")]
1956    Iterator(ListType, Scopes),
1957    /// trigger takes a single word
1958    Identifier(&'static str),
1959    /// trigger takes a flag name
1960    #[cfg(feature = "hoi4")]
1961    Flag,
1962    /// trigger takes a flag name or a block
1963    #[cfg(feature = "hoi4")]
1964    FlagOrBlock(&'static [(&'static str, Trigger)]),
1965
1966    #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5"))]
1967    Removed(&'static str, &'static str),
1968
1969    /// this key opens another trigger block
1970    Control,
1971    /// this has specific code for validation
1972    Special,
1973
1974    UncheckedValue,
1975    #[allow(dead_code)]
1976    UncheckedTodo,
1977}
1978
1979/// This function checks if the trigger is one that can be used at the end of a scope chain on the
1980/// right-hand side of a comparator.
1981pub fn trigger_comparevalue(name: &Token, data: &Everything) -> Option<Scopes> {
1982    match (Game::game(), scope_trigger(name, data)) {
1983        #[cfg(feature = "ck3")]
1984        (
1985            Game::Ck3,
1986            Some((
1987                s,
1988                Trigger::CompareValue
1989                | Trigger::CompareValueWarnEq
1990                | Trigger::CompareDate
1991                | Trigger::SetValue
1992                | Trigger::BlockOrCompareValue(_)
1993                | Trigger::CompareChoice(_),
1994            )),
1995        ) => Some(s),
1996        #[cfg(feature = "vic3")]
1997        (
1998            Game::Vic3,
1999            Some((
2000                s,
2001                Trigger::CompareValue
2002                | Trigger::CompareValueWarnEq
2003                | Trigger::CompareDate
2004                | Trigger::BlockOrCompareValue(_)
2005                | Trigger::ItemOrCompareValue(_)
2006                | Trigger::CompareChoice(_)
2007                | Trigger::CompareChoiceOrNumber(_),
2008            )),
2009        ) => Some(s),
2010        #[cfg(feature = "eu5")]
2011        (
2012            Game::Eu5,
2013            Some((
2014                s,
2015                Trigger::CompareValue
2016                | Trigger::CompareValueWarnEq
2017                | Trigger::CompareDate
2018                | Trigger::BlockOrCompareValue(_)
2019                | Trigger::ItemOrCompareValue(_)
2020                | Trigger::CompareChoice(_)
2021                | Trigger::CompareChoiceOrNumber(_),
2022            )),
2023        ) => Some(s),
2024        #[cfg(feature = "imperator")]
2025        (Game::Imperator, Some((s, Trigger::CompareValue | Trigger::CompareDate))) => Some(s),
2026        #[cfg(feature = "hoi4")]
2027        (
2028            Game::Hoi4,
2029            Some((s, Trigger::CompareValue | Trigger::CompareValueWarnEq | Trigger::CompareDate)),
2030        ) => Some(s),
2031        _ => std::option::Option::None,
2032    }
2033}
2034
2035// This function works around the problem of needing cfg-specific conditions in an else if
2036#[inline]
2037#[allow(unused_variables)]
2038pub fn is_character_token(part: &str, data: &Everything) -> bool {
2039    #[cfg(feature = "hoi4")]
2040    if Game::is_hoi4() {
2041        return data.item_exists(Item::Character, part);
2042    }
2043    false
2044}