tiger_lib/
validate.rs

1//! Validation functions that are useful for more than one data module.
2
3use std::fmt::{Display, Formatter};
4
5use crate::block::{BV, Block};
6#[cfg(feature = "ck3")]
7use crate::ck3::tables::misc::OUTBREAK_INTENSITIES;
8#[cfg(feature = "ck3")]
9use crate::ck3::validate::{
10    validate_activity_modifier, validate_ai_value_modifier, validate_compare_modifier,
11    validate_compatibility_modifier, validate_opinion_modifier, validate_scheme_modifier,
12};
13use crate::context::ScopeContext;
14#[cfg(feature = "jomini")]
15use crate::data::scripted_modifiers::ScriptedModifier;
16use crate::everything::Everything;
17use crate::game::Game;
18use crate::helpers::is_country_tag;
19use crate::item::Item;
20use crate::lowercase::Lowercase;
21#[cfg(feature = "jomini")]
22use crate::report::fatal;
23use crate::report::{Confidence, ErrorKey, Severity, err, report, warn};
24#[cfg(any(feature = "ck3", feature = "hoi4"))]
25use crate::scopes::Scopes;
26use crate::scopes::{scope_prefix, scope_to_scope};
27#[cfg(feature = "jomini")]
28use crate::script_value::{validate_non_dynamic_script_value, validate_script_value};
29use crate::token::Token;
30use crate::tooltipped::Tooltipped;
31#[cfg(any(feature = "ck3", feature = "hoi4"))]
32use crate::trigger::validate_target_ok_this;
33#[cfg(feature = "jomini")]
34use crate::trigger::validate_trigger;
35use crate::trigger::{
36    Part, PartFlags, is_character_token, partition, validate_argument, validate_argument_scope,
37    validate_inscopes, validate_trigger_internal, warn_not_first,
38};
39use crate::validator::Validator;
40
41#[derive(Copy, Clone, Debug, PartialEq, Eq)]
42pub enum ListType {
43    None,
44    Any,
45    #[cfg(feature = "hoi4")]
46    All,
47    Every,
48    #[cfg(feature = "jomini")]
49    Ordered,
50    Random,
51}
52
53impl ListType {
54    pub fn is_for_triggers(self) -> bool {
55        match self {
56            ListType::None => false,
57            ListType::Any => true,
58            #[cfg(feature = "hoi4")]
59            ListType::All => true,
60            ListType::Every => false,
61            #[cfg(feature = "jomini")]
62            ListType::Ordered => false,
63            ListType::Random => false,
64        }
65    }
66}
67
68impl Display for ListType {
69    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
70        match self {
71            ListType::None => write!(f, ""),
72            ListType::Any => write!(f, "any"),
73            #[cfg(feature = "hoi4")]
74            ListType::All => write!(f, "all"),
75            ListType::Every => write!(f, "every"),
76            #[cfg(feature = "jomini")]
77            ListType::Ordered => write!(f, "ordered"),
78            ListType::Random => write!(f, "random"),
79        }
80    }
81}
82
83impl TryFrom<&str> for ListType {
84    type Error = std::fmt::Error;
85
86    fn try_from(from: &str) -> Result<Self, Self::Error> {
87        match from {
88            "" => Ok(ListType::None),
89            "any" => Ok(ListType::Any),
90            #[cfg(feature = "hoi4")]
91            "all" => Ok(ListType::All),
92            "every" => Ok(ListType::Every),
93            #[cfg(feature = "jomini")]
94            "ordered" => Ok(ListType::Ordered),
95            "random" => Ok(ListType::Random),
96            _ => Err(std::fmt::Error),
97        }
98    }
99}
100
101#[cfg(any(feature = "ck3", feature = "vic3"))]
102pub fn validate_compare_duration(block: &Block, data: &Everything, sc: &mut ScopeContext) {
103    let mut vd = Validator::new(block, data);
104    let mut count = 0;
105
106    for field in &["days", "weeks", "months", "years"] {
107        if let Some(bv) = vd.field_any_cmp(field) {
108            if Game::is_jomini() {
109                #[cfg(feature = "jomini")]
110                validate_script_value(bv, data, sc);
111            } else {
112                // TODO HOI4
113            }
114            count += 1;
115        }
116    }
117
118    if count != 1 {
119        let msg = "must have 1 of days, weeks, months, or years";
120        let key = if count == 0 { ErrorKey::FieldMissing } else { ErrorKey::Validation };
121        err(key).msg(msg).loc(block).push();
122    }
123}
124
125// Very similar to validate_days_weeks_months_years, but requires = instead of allowing comparators
126// "weeks" is not documented but is used all over vanilla TODO: verify
127#[cfg(feature = "jomini")]
128pub fn validate_mandatory_duration(block: &Block, vd: &mut Validator, sc: &mut ScopeContext) {
129    let mut count = 0;
130
131    for field in &["days", "weeks", "months", "years"] {
132        if vd.field_script_value(field, sc) {
133            count += 1;
134        }
135    }
136
137    if count != 1 {
138        let msg = "must have 1 of days, weeks, months, or years";
139        let key = if count == 0 { ErrorKey::FieldMissing } else { ErrorKey::Validation };
140        err(key).msg(msg).loc(block).push();
141    }
142}
143
144#[cfg(feature = "jomini")]
145pub fn validate_duration(block: &Block, data: &Everything, sc: &mut ScopeContext) {
146    let mut vd = Validator::new(block, data);
147    validate_mandatory_duration(block, &mut vd, sc);
148}
149
150// Very similar to validate_duration, but validates part of a block that may contain a duration
151// Also does not accept script values (per the documentation)
152#[cfg(feature = "jomini")]
153pub fn validate_optional_duration_int(vd: &mut Validator) {
154    let mut count = 0;
155
156    for field in &["days", "weeks", "months", "years"] {
157        vd.field_validated_value(field, |key, mut vd| {
158            vd.integer();
159            count += 1;
160            if count > 1 {
161                let msg = "must have at most 1 of days, weeks, months, or years";
162                err(ErrorKey::Validation).msg(msg).loc(key).push();
163            }
164        });
165    }
166}
167
168// Very similar to validate_days_weeks_months_years, but requires = instead of allowing comparators
169#[allow(dead_code)]
170pub fn validate_optional_duration(vd: &mut Validator, sc: &mut ScopeContext) {
171    let mut count = 0;
172
173    #[cfg(not(feature = "imperator"))]
174    let options = &["days", "weeks", "months", "years"];
175
176    // Imperator does not allow a "weeks" field and does allow a "duration" field for modifiers.
177    #[cfg(feature = "imperator")]
178    let options = &["days", "months", "years", "duration"];
179
180    for field in options {
181        vd.field_validated_key(field, |key, bv, data| {
182            if Game::is_jomini() {
183                #[cfg(feature = "jomini")]
184                validate_script_value(bv, data, sc);
185            } else {
186                // TODO HOI4
187                let _ = &bv;
188                let _ = &data;
189                let _ = &sc;
190            }
191            count += 1;
192            if count > 1 {
193                let msg = "must have at most 1 of days, weeks, months, or years";
194                err(ErrorKey::Validation).msg(msg).loc(key).push();
195            }
196        });
197    }
198}
199
200// Does not accept script values (per the documentation)
201pub fn validate_color(block: &Block, _data: &Everything) {
202    // Reports in this function are `warn` level because a bad color is just an UI issue,
203    // and normal confidence level because I'm not 100% sure of the color formats.
204    let mut count = 0;
205    // Get the color tag, as in color = hsv { 0.5 1.0 1.0 }
206    let tag = block.tag.as_deref().map_or("rgb", Token::as_str);
207    for item in block.iter_items() {
208        if let Some(t) = item.get_value() {
209            if tag == "hsv" {
210                t.check_number();
211                if let Some(f) = t.get_number() {
212                    if !(0.0..=1.0).contains(&f) {
213                        // TODO: check if integer color values actually work in hsv,
214                        // then adjust the report.
215                        let msg = "hsv values should be between 0.0 and 1.0";
216                        let mut info = "";
217                        if t.is_integer() {
218                            info = "did you mean `hsv360`?";
219                        }
220                        warn(ErrorKey::Colors).weak().msg(msg).info(info).loc(t).push();
221                    }
222                } else {
223                    warn(ErrorKey::Colors).msg("expected hsv value").loc(t).push();
224                }
225            } else if tag == "hsv360" {
226                if let Some(i) = t.get_integer() {
227                    if count == 0 && !(0..=360).contains(&i) {
228                        let msg = "hsv360 h values should be between 0 and 360";
229                        warn(ErrorKey::Colors).msg(msg).loc(t).push();
230                    } else if count > 0 && !(0..=100).contains(&i) {
231                        let msg = "hsv360 s and v values should be between 0 and 100";
232                        warn(ErrorKey::Colors).msg(msg).loc(t).push();
233                    }
234                } else {
235                    warn(ErrorKey::Colors).msg("expected hsv360 value").loc(t).push();
236                }
237            } else if let Some(i) = t.get_integer() {
238                if !(0..=255).contains(&i) {
239                    let msg = "color values should be between 0 and 255";
240                    warn(ErrorKey::Colors).msg(msg).loc(t).push();
241                }
242            } else if let Some(f) = t.get_number() {
243                t.check_number();
244                if !(0.0..=1.0).contains(&f) {
245                    let msg = "color values should be between 0.0 and 1.0";
246                    warn(ErrorKey::Colors).msg(msg).loc(t).push();
247                }
248            } else {
249                warn(ErrorKey::Colors).msg("expected color value").loc(t).push();
250            }
251            count += 1;
252        }
253    }
254    if count != 3 && count != 4 {
255        warn(ErrorKey::Colors).msg("expected 3 or 4 color values").loc(block).push();
256    }
257}
258
259#[cfg(feature = "jomini")]
260pub fn validate_possibly_named_color(bv: &BV, data: &Everything) {
261    if Game::is_hoi4() {
262        // no named colors
263        if let Some(block) = bv.expect_block() {
264            validate_color(block, data);
265        }
266    }
267    #[cfg(feature = "jomini")]
268    match bv {
269        BV::Value(token) => data.verify_exists(Item::NamedColor, token),
270        BV::Block(block) => validate_color(block, data),
271    }
272}
273
274/// Check some iterator fields *before* the list scope has opened.
275#[allow(unused_variables)] // `name` is only used for ck3
276pub fn precheck_iterator_fields(
277    ltype: ListType,
278    name: &str,
279    block: &Block,
280    data: &Everything,
281    sc: &mut ScopeContext,
282) {
283    match ltype {
284        ListType::Any => {
285            #[cfg(feature = "jomini")]
286            if let Some(bv) = block.get_field("percent") {
287                if let Some(token) = bv.get_value() {
288                    if let Some(num) = token.get_number() {
289                        token.check_number();
290                        if num > 1.0 {
291                            let msg = "'percent' here needs to be between 0 and 1";
292                            warn(ErrorKey::Range).msg(msg).loc(token).push();
293                        }
294                    }
295                }
296                validate_script_value(bv, data, sc);
297            }
298            #[cfg(feature = "jomini")]
299            if let Some(bv) = block.get_field("count") {
300                match bv {
301                    BV::Value(token) if token.is("all") => (),
302                    bv => validate_script_value(bv, data, sc),
303                }
304            }
305        }
306        #[cfg(feature = "hoi4")]
307        ListType::All => {}
308        #[cfg(feature = "jomini")]
309        ListType::Ordered => {
310            for field in &["min", "max"] {
311                if let Some(bv) = block.get_field(field) {
312                    validate_script_value(bv, data, sc);
313                }
314            }
315            if let Some(bv) = block.get_field("position") {
316                if let Some(token) = bv.get_value() {
317                    if !token.is("end") {
318                        validate_script_value(bv, data, sc);
319                    }
320                } else {
321                    validate_script_value(bv, data, sc);
322                }
323            }
324        }
325        ListType::Random | ListType::Every | ListType::None => (),
326    }
327
328    #[cfg(feature = "ck3")]
329    if Game::is_ck3() && name == "county_in_region" {
330        for region in block.get_field_values("region") {
331            if !data.item_exists(Item::Region, region.as_str()) {
332                validate_target_ok_this(region, data, sc, Scopes::GeographicalRegion);
333            }
334        }
335    }
336    #[cfg(feature = "ck3")]
337    if Game::is_ck3() && name == "succession_appointment_investors" {
338        if let Some(candidate) = block.get_field_value("candidate") {
339            validate_target_ok_this(candidate, data, sc, Scopes::Character);
340        }
341        if let Some(value) = block.get_field("value") {
342            validate_script_value(value, data, sc);
343        }
344    }
345
346    #[cfg(feature = "hoi4")]
347    if Game::is_hoi4() && name == "country_with_original_tag" {
348        if let Some(tag) = block.get_field_value("original_tag_to_check") {
349            validate_target_ok_this(tag, data, sc, Scopes::Country);
350        }
351    }
352}
353
354/// This checks the fields that are only used in iterators.
355/// It does not check "limit" because that is shared with the if/else blocks.
356/// Returns true iff the iterator took care of its own tooltips
357#[allow(unused_variables)] // hoi4 does not use the parameters
358pub fn validate_iterator_fields(
359    caller: &Lowercase,
360    list_type: ListType,
361    data: &Everything,
362    sc: &mut ScopeContext,
363    vd: &mut Validator,
364    tooltipped: &mut Tooltipped,
365    is_svalue: bool,
366) {
367    // undocumented
368    #[cfg(feature = "jomini")]
369    if list_type == ListType::None {
370        vd.ban_field("custom", || "lists");
371    } else if vd.field_item("custom", Item::Localization) {
372        *tooltipped = Tooltipped::No;
373    }
374
375    // undocumented
376    #[cfg(feature = "jomini")]
377    if list_type != ListType::None && list_type != ListType::Any {
378        vd.multi_field_validated_block("alternative_limit", |b, data| {
379            validate_trigger(b, data, sc, *tooltipped);
380        });
381    } else {
382        vd.ban_field("alternative_limit", || "`every_`, `ordered_`, and `random_` lists");
383    }
384
385    #[cfg(feature = "jomini")]
386    if list_type == ListType::Any {
387        vd.field_any_cmp("percent"); // prechecked
388        vd.field_any_cmp("count"); // prechecked
389    } else {
390        vd.ban_field("percent", || "`any_` lists");
391        if caller != "while" {
392            vd.ban_field("count", || "`while` and `any_` lists");
393        }
394    }
395
396    #[cfg(feature = "jomini")]
397    if list_type == ListType::Ordered {
398        #[cfg(feature = "jomini")]
399        if Game::is_jomini() {
400            vd.field_script_value("order_by", sc);
401        }
402        vd.field("position"); // prechecked
403        vd.field("min"); // prechecked
404        vd.field("max"); // prechecked
405        vd.field_bool("check_range_bounds");
406    } else {
407        vd.ban_field("order_by", || "`ordered_` lists");
408        vd.ban_field("position", || "`ordered_` lists");
409        if caller != "random_list" && caller != "duel" && !is_svalue {
410            vd.ban_field("min", || "`ordered_` lists, `random_list`, and `duel`");
411            vd.ban_field("max", || "`ordered_` lists, `random_list`, and `duel`");
412        }
413        vd.ban_field("check_range_bounds", || "`ordered_` lists");
414    }
415
416    #[cfg(feature = "jomini")]
417    if list_type == ListType::Random {
418        vd.field_validated_block_sc("weight", sc, validate_modifiers_with_base);
419    } else {
420        vd.ban_field("weight", || "`random_` lists");
421    }
422
423    #[cfg(feature = "hoi4")]
424    if list_type == ListType::Every {
425        vd.field_integer("random_select_amount");
426    } else {
427        vd.ban_field("random_select_amount", || "`every_` lists");
428    }
429
430    #[cfg(feature = "hoi4")]
431    if list_type != ListType::None {
432        vd.field_item("tooltip", Item::Localization);
433    }
434
435    #[cfg(feature = "hoi4")]
436    if list_type == ListType::Every {
437        vd.field_bool("display_individual_scopes");
438    } else {
439        vd.ban_field("display_individual_scopes", || "`every_` lists");
440    }
441
442    #[cfg(feature = "hoi4")]
443    if (list_type == ListType::Every || list_type == ListType::Random)
444        && sc.scopes(data).contains(Scopes::Character | Scopes::IndustrialOrg)
445    {
446        vd.field_bool("include_invisible");
447    } else {
448        vd.ban_field("include_invisible", || "`every_` and `random_` character and mio lists");
449    }
450}
451
452/// This checks the special fields for certain iterators, like `type =` in `every_relation`.
453/// It doesn't check the generic ones like `limit` or the ordering ones for `ordered_*`.
454#[allow(unused_variables)] // vic3 does not use `tooltipped`
455pub fn validate_inside_iterator(
456    name: &Lowercase,
457    listtype: ListType,
458    block: &Block,
459    data: &Everything,
460    sc: &mut ScopeContext,
461    vd: &mut Validator,
462    tooltipped: Tooltipped,
463) {
464    // Docs say that all three can take either list or variable, but global and local lists must be variable lists.
465    #[cfg(feature = "jomini")]
466    if name == "in_list" {
467        vd.req_field_one_of(&["list", "variable"]);
468        if let Some(token) = vd.field_value("list") {
469            sc.expect_list(token, data);
470            sc.replace_list_entry(token);
471        }
472        if let Some(token) = vd.field_value("variable") {
473            sc.replace_variable_list_entry(token);
474        }
475    } else if name == "in_local_list" {
476        vd.req_field("variable");
477        vd.ban_field("list", || format!("{listtype}_in_list"));
478        if let Some(token) = vd.field_value("variable") {
479            sc.expect_local_list(token, data);
480            sc.replace_local_list_entry(token);
481        }
482    } else if name == "in_global_list" {
483        vd.req_field("variable");
484        vd.ban_field("list", || format!("{listtype}_in_list"));
485        if let Some(token) = vd.field_value("variable") {
486            sc.replace_global_list_entry(token);
487        }
488    } else {
489        vd.ban_field("list", || format!("{listtype}_in_list"));
490        vd.ban_field("variable", || {
491            format!(
492                "`{listtype}_in_list`, `{listtype}_in_local_list`, or `{listtype}_in_global_list`"
493            )
494        });
495    }
496
497    #[cfg(feature = "hoi4")]
498    if Game::is_hoi4() {
499        if name == "country_with_original_tag" {
500            vd.req_field("original_tag_to_check");
501            vd.field_value("original_tag_to_check"); // prechecked
502        } else if name == "owned_controlled_state" {
503            vd.field_list_items("prioritize", Item::State);
504        } else if name == "of" {
505            vd.field_value("array"); // TODO HOI4: check array reference
506            vd.field_value("value"); // name of temp variable
507            vd.field_value("index"); // name of temp variable
508        } else if name == "of_scopes" {
509            vd.field_value("array"); // TODO HOI4: check array reference
510        }
511    }
512
513    #[cfg(feature = "ck3")]
514    if Game::is_ck3() {
515        if name == "in_de_facto_hierarchy" || name == "in_de_jure_hierarchy" {
516            vd.field_trigger("filter", tooltipped, sc);
517            vd.field_trigger("continue", tooltipped, sc);
518        } else {
519            let only_for = || {
520                format!("`{listtype}_in_de_facto_hierarchy` or `{listtype}_in_de_jure_hierarchy`")
521            };
522            vd.ban_field("filter", only_for);
523            vd.ban_field("continue", only_for);
524        }
525
526        if name == "active_accolade" {
527            vd.field_item("accolade_parameter", Item::AccoladeParameter);
528        } else {
529            vd.ban_field("accolade_parameter", || format!("`{listtype}_{name}`"));
530        }
531
532        if name == "county_province_epidemic" || name == "province_epidemic" {
533            vd.multi_field_choice_any_cmp("intensity", OUTBREAK_INTENSITIES);
534        } else {
535            vd.ban_field("intensity", || {
536                format!("`{listtype}_county_province_epidemic` or `{listtype}_province_epidemic`")
537            });
538        }
539
540        if name == "secret" {
541            vd.field_item("type", Item::Secret);
542        }
543
544        if name == "scheme" {
545            vd.field_item("type", Item::Scheme);
546        }
547
548        if name == "task_contract"
549            || name == "character_task_contract"
550            || name == "character_active_contract"
551        {
552            vd.field_item("task_contract_type", Item::TaskContractType);
553        } else {
554            vd.ban_field("task_contract_type", || format!("`{listtype}_task_contract`, `{listtype}_character_task_contract` or `{listtype}_character_active_contract`"));
555        }
556
557        if name == "memory" {
558            vd.field_item("memory_type", Item::MemoryType);
559        } else {
560            vd.ban_field("memory_type", || format!("`{listtype}_{name}`"));
561        }
562
563        if name == "targeting_faction" {
564            vd.field_item("faction_type", Item::Faction);
565        } else {
566            vd.ban_field("faction_type", || format!("`{listtype}_{name}`"));
567        }
568
569        if name == "vassal" || name == "vassal_or_below" {
570            vd.field_item("vassal_stance", Item::VassalStance);
571        } else {
572            vd.ban_field("vassal_stance", || {
573                format!("`{listtype}_vassal` or `{listtype}_vassal_or_below`")
574            });
575        }
576
577        if name == "owned_story" {
578            vd.field_item("type", Item::Story);
579        }
580
581        if name == "held_title" {
582            // TODO: actually check the value
583            vd.field_any_cmp("title_tier");
584        } else {
585            vd.ban_field("title_tier", || format!("`{listtype}_{name}`"));
586        }
587
588        if name == "county_in_region" {
589            vd.req_field("region");
590            vd.multi_field_value("region"); // prechecked
591        } else {
592            vd.ban_field("region", || format!("`{listtype}_county_in_region`"));
593        }
594
595        if name == "court_position_candidate" {
596            vd.req_field("court_position_type");
597            vd.field_item_or_target(
598                "court_position_type",
599                sc,
600                Item::CourtPosition,
601                Scopes::CourtPositionType,
602            );
603        }
604
605        if name == "court_position_holder" {
606            vd.field_item("type", Item::CourtPosition);
607        }
608
609        if name == "relation" {
610            if !block.has_key("type") {
611                let msg = "required field `type` missing";
612                let info = format!(
613                    "Verified for 1.9.2: with no type, {listtype}_relation will do nothing."
614                );
615                err(ErrorKey::FieldMissing).strong().msg(msg).info(info).loc(block).push();
616            }
617            vd.multi_field_item("type", Item::Relation);
618        }
619    }
620
621    #[cfg(feature = "ck3")]
622    if Game::is_ck3() {
623        if name == "claim" {
624            vd.field_choice("explicit", &["yes", "no", "all"]);
625            vd.field_choice("pressed", &["yes", "no", "all"]);
626        } else {
627            vd.ban_field("explicit", || format!("`{listtype}_claim`"));
628            vd.ban_field("pressed", || format!("`{listtype}_claim`"));
629        }
630    }
631
632    #[cfg(feature = "ck3")]
633    if Game::is_ck3() {
634        if name == "pool_character" {
635            vd.req_field("province");
636            if let Some(token) = vd.field_value("province") {
637                validate_target_ok_this(token, data, sc, Scopes::Province);
638            }
639        } else {
640            vd.ban_field("province", || format!("`{listtype}_pool_character`"));
641        }
642    }
643
644    #[cfg(feature = "ck3")]
645    if Game::is_ck3() {
646        if sc.can_be(Scopes::Character, data) {
647            vd.field_bool("only_if_dead");
648            vd.field_bool("even_if_dead");
649        } else {
650            vd.ban_field("only_if_dead", || "lists of characters");
651            vd.ban_field("even_if_dead", || "lists of characters");
652        }
653    }
654
655    #[cfg(feature = "ck3")]
656    if Game::is_ck3() {
657        if name == "character_struggle" {
658            vd.field_choice("involvement", &["involved", "interloper"]);
659        } else {
660            vd.ban_field("involvement", || format!("`{listtype}_character_struggle`"));
661        }
662    }
663
664    #[cfg(feature = "ck3")]
665    if Game::is_ck3() {
666        if name == "connected_county" {
667            // Undocumented
668            vd.field_bool("invert");
669            vd.field_numeric("max_naval_distance");
670            vd.field_bool("allow_one_county_land_gap");
671        } else {
672            let only_for = || format!("`{listtype}_connected_county`");
673            vd.ban_field("invert", only_for);
674            vd.ban_field("max_naval_distance", only_for);
675            vd.ban_field("allow_one_county_land_gap", only_for);
676        }
677    }
678
679    #[cfg(feature = "ck3")]
680    if Game::is_ck3() {
681        if name == "activity_phase_location"
682            || name == "activity_phase_location_future"
683            || name == "activity_phase_location_past"
684        {
685            vd.field_bool("unique");
686        } else {
687            let only_for =
688                || format!("the `{listtype}_activity_phase_location` family of iterators");
689            vd.ban_field("unique", only_for);
690        }
691    }
692
693    #[cfg(feature = "ck3")]
694    if Game::is_ck3() {
695        if name == "guest_subset" || name == "guest_subset_current_phase" {
696            vd.field_item("name", Item::GuestSubset);
697        } else {
698            vd.ban_field("name", || {
699                format!("`{listtype}_guest_subset` and `{listtype}_guest_subset_current_phase`")
700            });
701        }
702    }
703
704    #[cfg(feature = "ck3")]
705    if Game::is_ck3() {
706        if name == "guest_subset" {
707            vd.field_value("phase"); // TODO
708        } else {
709            vd.ban_field("phase", || format!("`{listtype}_guest_subset`"));
710        }
711    }
712
713    if Game::is_ck3() {
714        #[cfg(feature = "ck3")]
715        if name == "trait_in_category" {
716            vd.field_value("category"); // TODO
717        } else {
718            // Don't ban, because it's a valid trigger
719        }
720    }
721
722    #[cfg(feature = "ck3")]
723    if Game::is_ck3() {
724        if name == "succession_appointment_investors" {
725            vd.req_field("candidate");
726            vd.field_value("candidate"); // prechecked
727            vd.field_any_cmp("value"); // prechecked
728        } else {
729            vd.ban_field("candidate", || format!("`{listtype}_succession_appointment_investors`"));
730        }
731    }
732
733    #[cfg(feature = "hoi4")]
734    if Game::is_hoi4() {
735        if listtype == ListType::Random
736            && matches!(
737                name.as_str(),
738                "controlled_state"
739                    | "core_state"
740                    | "owned_controlled_state"
741                    | "owned_state"
742                    | "state"
743            )
744        {
745            vd.field_list_items("prioritize", Item::State);
746        } else {
747            vd.ban_field("prioritize", || "state `random_` iterators");
748        }
749    }
750}
751
752pub fn validate_modifiers_with_base(block: &Block, data: &Everything, sc: &mut ScopeContext) {
753    let mut vd = Validator::new(block, data);
754    if Game::is_jomini() {
755        #[cfg(feature = "jomini")]
756        {
757            vd.field_validated("base", validate_non_dynamic_script_value);
758            vd.multi_field_script_value("add", sc);
759            vd.multi_field_script_value("factor", sc);
760            vd.multi_field_script_value("min", sc);
761            vd.multi_field_script_value("max", sc);
762        }
763    } else {
764        #[cfg(feature = "hoi4")]
765        {
766            // TODO HOI4
767            vd.field_numeric("base");
768            vd.multi_field_numeric("add");
769            vd.multi_field_numeric("factor");
770        }
771    }
772    validate_modifiers(&mut vd, sc);
773    #[cfg(feature = "jomini")]
774    if Game::is_jomini() {
775        validate_scripted_modifier_calls(vd, data, sc);
776    }
777}
778
779pub fn validate_modifiers(vd: &mut Validator, sc: &mut ScopeContext) {
780    let max_sev = vd.max_severity();
781    vd.multi_field_validated_block("first_valid", |b, data| {
782        let mut vd = Validator::new(b, data);
783        vd.set_max_severity(max_sev);
784        validate_modifiers(&mut vd, sc);
785    });
786    vd.multi_field_validated_block("modifier", |b, data| {
787        let mut vd = Validator::new(b, data);
788        vd.set_max_severity(max_sev);
789        validate_trigger_internal(
790            &Lowercase::new_unchecked("modifier"),
791            ListType::None,
792            b,
793            data,
794            sc,
795            vd,
796            Tooltipped::No,
797            false,
798        );
799    });
800    #[cfg(feature = "ck3")]
801    if Game::is_ck3() {
802        vd.multi_field_validated_block_sc("compare_modifier", sc, validate_compare_modifier);
803        vd.multi_field_validated_block_sc("opinion_modifier", sc, validate_opinion_modifier);
804        vd.multi_field_validated_block_sc("ai_value_modifier", sc, validate_ai_value_modifier);
805        vd.multi_field_validated_block_sc(
806            "compatibility_modifier",
807            sc,
808            validate_compatibility_modifier,
809        );
810
811        // These are special single-use modifiers
812        vd.multi_field_validated_block_sc("scheme_modifier", sc, validate_scheme_modifier);
813        vd.multi_field_validated_block_sc("activity_modifier", sc, validate_activity_modifier);
814    }
815
816    #[cfg(feature = "jomini")]
817    if Game::is_jomini() {
818        vd.multi_field_script_value("min", sc);
819        vd.multi_field_script_value("max", sc);
820    }
821    // TODO HOI4
822}
823
824#[cfg(feature = "jomini")]
825pub fn validate_scripted_modifier_call(
826    key: &Token,
827    bv: &BV,
828    modifier: &ScriptedModifier,
829    data: &Everything,
830    sc: &mut ScopeContext,
831) {
832    match bv {
833        BV::Value(token) => {
834            if !modifier.macro_parms().is_empty() {
835                fatal(ErrorKey::Macro).msg("expected macro arguments").loc(token).push();
836            } else if !token.is("yes") {
837                warn(ErrorKey::Validation).msg("expected just modifier = yes").loc(token).push();
838            }
839            modifier.validate_call(key, data, sc);
840        }
841        BV::Block(block) => {
842            let parms = modifier.macro_parms();
843            if parms.is_empty() {
844                fatal(ErrorKey::Macro)
845                    .msg("this scripted modifier does not need macro arguments")
846                    .info("you can just use it as modifier = yes")
847                    .loc(block)
848                    .push();
849            } else {
850                let mut vec = Vec::new();
851                let mut vd = Validator::new(block, data);
852                for parm in &parms {
853                    if let Some(token) = vd.field_value(parm) {
854                        vec.push(token.clone());
855                    } else {
856                        let msg = format!("this scripted modifier needs parameter {parm}");
857                        err(ErrorKey::Macro).msg(msg).loc(block).push();
858                        return;
859                    }
860                }
861                vd.unknown_value_fields(|key, _value| {
862                    let msg = format!("this scripted modifier does not need parameter {key}");
863                    let info = "supplying an unneeded parameter often causes a crash";
864                    fatal(ErrorKey::Macro).msg(msg).info(info).loc(key).push();
865                });
866                let args: Vec<_> = parms.into_iter().zip(vec).collect();
867                modifier.validate_macro_expansion(key, &args, data, sc);
868            }
869        }
870    }
871}
872
873#[cfg(feature = "jomini")]
874pub fn validate_scripted_modifier_calls(
875    mut vd: Validator,
876    data: &Everything,
877    sc: &mut ScopeContext,
878) {
879    vd.unknown_fields(|key, bv| {
880        if let Some(modifier) = data.scripted_modifiers.get(key.as_str()) {
881            validate_scripted_modifier_call(key, bv, modifier, data, sc);
882        } else {
883            let msg = format!("unknown field `{key}`");
884            warn(ErrorKey::UnknownField).msg(msg).loc(key).push();
885        }
886    });
887}
888
889pub fn validate_ai_chance(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
890    match bv {
891        BV::Value(t) => _ = t.expect_number(),
892        BV::Block(b) => validate_modifiers_with_base(b, data, sc),
893    }
894}
895
896/// Validate the left-hand part of a `target = { target_scope }` block.
897///
898/// The caller is expected to have done `sc.open_builder()` before calling and then do `sc.close()` after calling.
899/// Returns true iff validation was complete.
900/// `qeq` is true if the scope chain is to the left of a ?= operator.
901pub fn validate_scope_chain(
902    token: &Token,
903    data: &Everything,
904    sc: &mut ScopeContext,
905    qeq: bool,
906) -> bool {
907    let part_vec = partition(token);
908    for i in 0..part_vec.len() {
909        let mut part_flags = PartFlags::empty();
910        if i == 0 {
911            part_flags |= PartFlags::First;
912        }
913        if i + 1 == part_vec.len() {
914            part_flags |= PartFlags::Last;
915        }
916        if qeq {
917            part_flags |= PartFlags::Question;
918        }
919        let part = &part_vec[i];
920
921        match part {
922            Part::TokenArgument(part, func, arg) => {
923                validate_argument(part_flags, part, func, arg, data, sc);
924            }
925            Part::Token(part) => {
926                let part_lc = Lowercase::new(part.as_str());
927                // prefixed scope transition, e.g. cp:councillor_steward
928                if let Some((prefix, arg)) = part.split_once(':') {
929                    #[allow(clippy::if_same_then_else)] // cfg attributes give a false positive here
930                    // known prefix
931                    if let Some(entry) = scope_prefix(&prefix) {
932                        validate_argument_scope(part_flags, entry, part, &prefix, &arg, data, sc);
933                    } else {
934                        let msg = format!("unknown prefix `{prefix}:`");
935                        err(ErrorKey::Validation).msg(msg).loc(prefix).push();
936                        return false;
937                    }
938                } else if part_lc == "root" {
939                    sc.replace_root();
940                } else if part_lc == "prev" {
941                    if !part_flags.contains(PartFlags::First) && !Game::is_imperator() {
942                        warn_not_first(part);
943                    }
944                    sc.replace_prev();
945                } else if part_lc == "this" {
946                    sc.replace_this();
947                } else if Game::is_hoi4() && part_lc == "from" {
948                    #[cfg(feature = "hoi4")]
949                    sc.replace_from();
950                } else if Game::is_hoi4() && is_country_tag(part.as_str()) {
951                    if !part_flags.contains(PartFlags::First) {
952                        warn_not_first(part);
953                    }
954                    #[cfg(feature = "hoi4")]
955                    data.verify_exists(Item::CountryTag, part);
956                    #[cfg(feature = "hoi4")]
957                    sc.replace(Scopes::Country, part.clone());
958                } else if is_character_token(part.as_str(), data) {
959                    #[cfg(feature = "hoi4")]
960                    sc.replace(Scopes::Character, part.clone());
961                } else if Game::is_hoi4() && part.is_integer() {
962                    // TODO HOI4: figure out if a state id has to be the whole target
963                    if !part_flags.contains(PartFlags::First) {
964                        warn_not_first(part);
965                    }
966                    #[cfg(feature = "hoi4")]
967                    data.verify_exists(Item::State, part);
968                    #[cfg(feature = "hoi4")]
969                    sc.replace(Scopes::State, part.clone());
970                } else if let Some((inscopes, outscope)) = scope_to_scope(part, sc.scopes(data)) {
971                    validate_inscopes(part_flags, part, inscopes, sc, data);
972                    sc.replace(outscope, part.clone());
973                } else {
974                    let msg = format!("unknown token `{part}`");
975                    err(ErrorKey::UnknownField).msg(msg).loc(part).push();
976                    return false;
977                }
978            }
979        }
980    }
981    true
982}
983
984pub fn validate_ifelse_sequence(block: &Block, key_if: &str, key_elseif: &str, key_else: &str) {
985    let mut seen_if = false;
986    for (key, block) in block.iter_definitions() {
987        if key.is(key_if) {
988            seen_if = true;
989            continue;
990        } else if key.is(key_elseif) {
991            if !seen_if {
992                let msg = format!("`{key_elseif} without preceding `{key_if}`");
993                warn(ErrorKey::IfElse).msg(msg).loc(key).push();
994            }
995            seen_if = true;
996            continue;
997        } else if key.is(key_else) {
998            if !seen_if {
999                let msg = format!("`{key_else} without preceding `{key_if}`");
1000                warn(ErrorKey::IfElse).msg(msg).loc(key).push();
1001            }
1002            if block.has_key("limit") {
1003                // `else` with a `limit`, followed by another `else`, does work.
1004                seen_if = true;
1005                continue;
1006            }
1007        }
1008        seen_if = false;
1009    }
1010}
1011
1012#[allow(dead_code)]
1013pub fn validate_numeric_range(
1014    block: &Block,
1015    data: &Everything,
1016    min: f64,
1017    max: f64,
1018    sev: Severity,
1019    conf: Confidence,
1020) {
1021    let mut vd = Validator::new(block, data);
1022    let mut count = 0;
1023    let mut prev = 0.0;
1024
1025    for token in vd.values() {
1026        if let Some(n) = token.expect_number() {
1027            count += 1;
1028            if !(min..=max).contains(&n) {
1029                let msg = format!("expected number between {min} and {max}");
1030                report(ErrorKey::Range, sev).conf(conf).msg(msg).loc(token).push();
1031            }
1032            if count == 1 {
1033                prev = n;
1034            } else if count == 2 && n < prev {
1035                let msg = "expected second number to be bigger than first number";
1036                report(ErrorKey::Range, sev).conf(conf).msg(msg).loc(token).push();
1037            } else if count == 3 {
1038                let msg = "expected exactly 2 numbers";
1039                report(ErrorKey::Range, sev).strong().msg(msg).loc(block).push();
1040            }
1041        }
1042    }
1043}
1044
1045pub fn validate_identifier(token: &Token, kind: &str, sev: Severity) {
1046    if token.as_str().contains('.') || token.as_str().contains(':') {
1047        let msg = format!("expected a {kind} here");
1048        report(ErrorKey::Validation, sev).msg(msg).loc(token).push();
1049    }
1050}
1051
1052/// Camera colors must be hsv, and value can be > 1
1053#[cfg(feature = "jomini")]
1054pub fn validate_camera_color(block: &Block, data: &Everything) {
1055    let mut count = 0;
1056    // Get the color tag, as in color = hsv { 0.5 1.0 1.0 }
1057    let tag = block.tag.as_deref().map_or("rgb", Token::as_str);
1058    if tag != "hsv" {
1059        let msg = "camera colors should be in hsv";
1060        warn(ErrorKey::Colors).msg(msg).loc(block).push();
1061        validate_color(block, data);
1062        return;
1063    }
1064
1065    for item in block.iter_items() {
1066        if let Some(t) = item.get_value() {
1067            t.check_number();
1068            if let Some(f) = t.get_number() {
1069                if count <= 1 && !(0.0..=1.0).contains(&f) {
1070                    let msg = "h and s values should be between 0.0 and 1.0";
1071                    err(ErrorKey::Colors).msg(msg).loc(t).push();
1072                }
1073            } else {
1074                let msg = "expected hsv value";
1075                err(ErrorKey::Colors).msg(msg).loc(t).push();
1076            }
1077            count += 1;
1078        }
1079    }
1080    if count != 3 {
1081        let msg = "expected 3 color values";
1082        err(ErrorKey::Colors).msg(msg).loc(block).push();
1083    }
1084}