Skip to main content

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                    && let Some(num) = token.get_number()
289                {
290                    token.check_number();
291                    if num > 1.0 {
292                        let msg = "'percent' here needs to be between 0 and 1";
293                        warn(ErrorKey::Range).msg(msg).loc(token).push();
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()
348        && name == "country_with_original_tag"
349        && let Some(tag) = block.get_field_value("original_tag_to_check")
350    {
351        validate_target_ok_this(tag, data, sc, Scopes::Country);
352    }
353}
354
355/// This checks the fields that are only used in iterators.
356/// It does not check "limit" because that is shared with the if/else blocks.
357/// Returns true iff the iterator took care of its own tooltips
358#[allow(unused_variables)] // hoi4 does not use the parameters
359pub fn validate_iterator_fields(
360    caller: &Lowercase,
361    list_type: ListType,
362    data: &Everything,
363    sc: &mut ScopeContext,
364    vd: &mut Validator,
365    tooltipped: &mut Tooltipped,
366    is_svalue: bool,
367) {
368    // undocumented
369    #[cfg(feature = "jomini")]
370    if list_type == ListType::None {
371        vd.ban_field("custom", || "lists");
372    } else if vd.field_item("custom", Item::Localization) {
373        *tooltipped = Tooltipped::No;
374    }
375
376    // undocumented
377    #[cfg(feature = "jomini")]
378    if list_type != ListType::None && list_type != ListType::Any {
379        vd.multi_field_validated_block("alternative_limit", |b, data| {
380            validate_trigger(b, data, sc, *tooltipped);
381        });
382    } else {
383        vd.ban_field("alternative_limit", || "`every_`, `ordered_`, and `random_` lists");
384    }
385
386    #[cfg(feature = "jomini")]
387    if list_type == ListType::Any {
388        vd.field_any_cmp("percent"); // prechecked
389        vd.field_any_cmp("count"); // prechecked
390    } else {
391        vd.ban_field("percent", || "`any_` lists");
392        if caller != "while" {
393            vd.ban_field("count", || "`while` and `any_` lists");
394        }
395    }
396
397    #[cfg(feature = "jomini")]
398    if list_type == ListType::Ordered {
399        #[cfg(feature = "jomini")]
400        if Game::is_jomini() {
401            vd.field_script_value("order_by", sc);
402        }
403        vd.field("position"); // prechecked
404        vd.field("min"); // prechecked
405        vd.field("max"); // prechecked
406        vd.field_bool("check_range_bounds");
407    } else {
408        vd.ban_field("order_by", || "`ordered_` lists");
409        vd.ban_field("position", || "`ordered_` lists");
410        if caller != "random_list" && caller != "duel" && !is_svalue {
411            vd.ban_field("min", || "`ordered_` lists, `random_list`, and `duel`");
412            vd.ban_field("max", || "`ordered_` lists, `random_list`, and `duel`");
413        }
414        vd.ban_field("check_range_bounds", || "`ordered_` lists");
415    }
416
417    #[cfg(feature = "jomini")]
418    if list_type == ListType::Random {
419        vd.field_validated_block_sc("weight", sc, validate_modifiers_with_base);
420    } else {
421        vd.ban_field("weight", || "`random_` lists");
422    }
423
424    #[cfg(feature = "hoi4")]
425    if list_type == ListType::Every {
426        vd.field_integer("random_select_amount");
427    } else {
428        vd.ban_field("random_select_amount", || "`every_` lists");
429    }
430
431    #[cfg(feature = "hoi4")]
432    if list_type != ListType::None {
433        vd.field_item("tooltip", Item::Localization);
434    }
435
436    #[cfg(feature = "hoi4")]
437    if list_type == ListType::Every {
438        vd.field_bool("display_individual_scopes");
439    } else {
440        vd.ban_field("display_individual_scopes", || "`every_` lists");
441    }
442
443    #[cfg(feature = "hoi4")]
444    if (list_type == ListType::Every || list_type == ListType::Random)
445        && sc.scopes(data).contains(Scopes::Character | Scopes::IndustrialOrg)
446    {
447        vd.field_bool("include_invisible");
448    } else {
449        vd.ban_field("include_invisible", || "`every_` and `random_` character and mio lists");
450    }
451}
452
453/// This checks the special fields for certain iterators, like `type =` in `every_relation`.
454/// It doesn't check the generic ones like `limit` or the ordering ones for `ordered_*`.
455#[allow(unused_variables)] // vic3 does not use `tooltipped`
456pub fn validate_inside_iterator(
457    name: &Lowercase,
458    listtype: ListType,
459    block: &Block,
460    data: &Everything,
461    sc: &mut ScopeContext,
462    vd: &mut Validator,
463    tooltipped: Tooltipped,
464) {
465    // Docs say that all three can take either list or variable, but global and local lists must be variable lists.
466    #[cfg(feature = "jomini")]
467    if name == "in_list" {
468        vd.req_field_one_of(&["list", "variable"]);
469        if let Some(token) = vd.field_value("list") {
470            sc.expect_list(token, data);
471            sc.replace_list_entry(token);
472        }
473        if let Some(token) = vd.field_value("variable") {
474            sc.replace_variable_list_entry(token);
475        }
476    } else if name == "in_local_list" {
477        vd.req_field("variable");
478        vd.ban_field("list", || format!("{listtype}_in_list"));
479        if let Some(token) = vd.field_value("variable") {
480            sc.expect_local_list(token, data);
481            sc.replace_local_list_entry(token);
482        }
483    } else if name == "in_global_list" {
484        vd.req_field("variable");
485        vd.ban_field("list", || format!("{listtype}_in_list"));
486        if let Some(token) = vd.field_value("variable") {
487            sc.replace_global_list_entry(token);
488        }
489    } else {
490        vd.ban_field("list", || format!("{listtype}_in_list"));
491        vd.ban_field("variable", || {
492            format!(
493                "`{listtype}_in_list`, `{listtype}_in_local_list`, or `{listtype}_in_global_list`"
494            )
495        });
496    }
497
498    #[cfg(feature = "hoi4")]
499    if Game::is_hoi4() {
500        if name == "country_with_original_tag" {
501            vd.req_field("original_tag_to_check");
502            vd.field_value("original_tag_to_check"); // prechecked
503        } else if name == "owned_controlled_state" {
504            vd.field_list_items("prioritize", Item::State);
505        } else if name == "of" {
506            vd.field_value("array"); // TODO HOI4: check array reference
507            vd.field_value("value"); // name of temp variable
508            vd.field_value("index"); // name of temp variable
509        } else if name == "of_scopes" {
510            vd.field_value("array"); // TODO HOI4: check array reference
511        }
512    }
513
514    #[cfg(feature = "ck3")]
515    if Game::is_ck3() {
516        if name == "in_de_facto_hierarchy" || name == "in_de_jure_hierarchy" {
517            vd.field_trigger("filter", tooltipped, sc);
518            vd.field_trigger("continue", tooltipped, sc);
519        } else {
520            let only_for = || {
521                format!("`{listtype}_in_de_facto_hierarchy` or `{listtype}_in_de_jure_hierarchy`")
522            };
523            vd.ban_field("filter", only_for);
524            vd.ban_field("continue", only_for);
525        }
526
527        if name == "active_accolade" {
528            vd.field_item("accolade_parameter", Item::AccoladeParameter);
529        } else {
530            vd.ban_field("accolade_parameter", || format!("`{listtype}_{name}`"));
531        }
532
533        if name == "county_province_epidemic" || name == "province_epidemic" {
534            vd.multi_field_choice_any_cmp("intensity", OUTBREAK_INTENSITIES);
535        } else {
536            vd.ban_field("intensity", || {
537                format!("`{listtype}_county_province_epidemic` or `{listtype}_province_epidemic`")
538            });
539        }
540
541        if name == "secret" {
542            vd.field_item("type", Item::Secret);
543        }
544
545        if name == "scheme" {
546            vd.field_item("type", Item::Scheme);
547        }
548
549        if name == "task_contract"
550            || name == "character_task_contract"
551            || name == "character_active_contract"
552        {
553            vd.field_item("task_contract_type", Item::TaskContractType);
554        } else {
555            vd.ban_field("task_contract_type", || format!("`{listtype}_task_contract`, `{listtype}_character_task_contract` or `{listtype}_character_active_contract`"));
556        }
557
558        if name == "memory" {
559            vd.field_item("memory_type", Item::MemoryType);
560        } else {
561            vd.ban_field("memory_type", || format!("`{listtype}_{name}`"));
562        }
563
564        if name == "targeting_faction" {
565            vd.field_item("faction_type", Item::Faction);
566        } else {
567            vd.ban_field("faction_type", || format!("`{listtype}_{name}`"));
568        }
569
570        if name == "vassal" || name == "vassal_or_below" {
571            vd.field_item("vassal_stance", Item::VassalStance);
572        } else {
573            vd.ban_field("vassal_stance", || {
574                format!("`{listtype}_vassal` or `{listtype}_vassal_or_below`")
575            });
576        }
577
578        if name == "owned_story" {
579            vd.field_item("type", Item::Story);
580        }
581
582        if name == "held_title" {
583            // TODO: actually check the value
584            vd.field_any_cmp("title_tier");
585        } else {
586            vd.ban_field("title_tier", || format!("`{listtype}_{name}`"));
587        }
588
589        if name == "county_in_region" {
590            vd.req_field("region");
591            vd.multi_field_value("region"); // prechecked
592        } else {
593            vd.ban_field("region", || format!("`{listtype}_county_in_region`"));
594        }
595
596        if name == "court_position_candidate" {
597            vd.req_field("court_position_type");
598            vd.field_item_or_target(
599                "court_position_type",
600                sc,
601                Item::CourtPosition,
602                Scopes::CourtPositionType,
603            );
604        }
605
606        if name == "court_position_holder" {
607            vd.field_item("type", Item::CourtPosition);
608        }
609
610        if name == "relation" {
611            if !block.has_key("type") {
612                let msg = "required field `type` missing";
613                let info = format!(
614                    "Verified for 1.9.2: with no type, {listtype}_relation will do nothing."
615                );
616                err(ErrorKey::FieldMissing).strong().msg(msg).info(info).loc(block).push();
617            }
618            vd.multi_field_item("type", Item::Relation);
619        }
620
621        if name == "active_dynasty" {
622            vd.field_bool("include_inactive");
623        }
624
625        if name == "ruler" {
626            // TODO: if an iterator has a limit checking one of these, suggest using the filter.
627            vd.field_target("government_type", sc, Scopes::GovernmentType);
628            vd.field_bool("only_independent");
629            vd.field_choice("tier", &["county", "duchy", "kingdom", "empire", "hegemony"]);
630            vd.field_target("faith", sc, Scopes::Faith);
631            vd.field_target("culture", sc, Scopes::Culture);
632        }
633    }
634
635    #[cfg(feature = "ck3")]
636    if Game::is_ck3() {
637        if name == "claim" {
638            vd.field_choice("explicit", &["yes", "no", "all"]);
639            vd.field_choice("pressed", &["yes", "no", "all"]);
640        } else {
641            vd.ban_field("explicit", || format!("`{listtype}_claim`"));
642            vd.ban_field("pressed", || format!("`{listtype}_claim`"));
643        }
644    }
645
646    #[cfg(feature = "ck3")]
647    if Game::is_ck3() {
648        if name == "pool_character" {
649            vd.req_field("province");
650            if let Some(token) = vd.field_value("province") {
651                validate_target_ok_this(token, data, sc, Scopes::Province);
652            }
653        } else {
654            vd.ban_field("province", || format!("`{listtype}_pool_character`"));
655        }
656    }
657
658    #[cfg(feature = "ck3")]
659    if Game::is_ck3() {
660        if sc.can_be(Scopes::Character, data) {
661            vd.field_bool("only_if_dead");
662            vd.field_bool("even_if_dead");
663        } else {
664            vd.ban_field("only_if_dead", || "lists of characters");
665            vd.ban_field("even_if_dead", || "lists of characters");
666        }
667    }
668
669    #[cfg(feature = "ck3")]
670    if Game::is_ck3() {
671        if name == "character_struggle" {
672            vd.field_choice("involvement", &["involved", "interloper"]);
673        } else {
674            vd.ban_field("involvement", || format!("`{listtype}_character_struggle`"));
675        }
676    }
677
678    #[cfg(feature = "ck3")]
679    if Game::is_ck3() {
680        if name == "connected_county" {
681            // Undocumented
682            vd.field_bool("invert");
683            vd.field_numeric("max_naval_distance");
684            vd.field_bool("allow_one_county_land_gap");
685        } else {
686            let only_for = || format!("`{listtype}_connected_county`");
687            vd.ban_field("invert", only_for);
688            vd.ban_field("max_naval_distance", only_for);
689            vd.ban_field("allow_one_county_land_gap", only_for);
690        }
691    }
692
693    #[cfg(feature = "ck3")]
694    if Game::is_ck3() {
695        if name == "activity_phase_location"
696            || name == "activity_phase_location_future"
697            || name == "activity_phase_location_past"
698        {
699            vd.field_bool("unique");
700        } else {
701            let only_for =
702                || format!("the `{listtype}_activity_phase_location` family of iterators");
703            vd.ban_field("unique", only_for);
704        }
705    }
706
707    #[cfg(feature = "ck3")]
708    if Game::is_ck3() {
709        if name == "guest_subset" || name == "guest_subset_current_phase" {
710            vd.field_item("name", Item::GuestSubset);
711        } else {
712            vd.ban_field("name", || {
713                format!("`{listtype}_guest_subset` and `{listtype}_guest_subset_current_phase`")
714            });
715        }
716    }
717
718    #[cfg(feature = "ck3")]
719    if Game::is_ck3() {
720        if name == "guest_subset" {
721            vd.field_value("phase"); // TODO
722        } else {
723            vd.ban_field("phase", || format!("`{listtype}_guest_subset`"));
724        }
725    }
726
727    if Game::is_ck3() {
728        #[cfg(feature = "ck3")]
729        if name == "trait_in_category" {
730            vd.field_value("category"); // TODO
731        } else {
732            // Don't ban, because it's a valid trigger
733        }
734    }
735
736    #[cfg(feature = "ck3")]
737    if Game::is_ck3() {
738        if name == "succession_appointment_investors" {
739            vd.req_field("candidate");
740            vd.field_value("candidate"); // prechecked
741            vd.field_any_cmp("value"); // prechecked
742        } else {
743            vd.ban_field("candidate", || format!("`{listtype}_succession_appointment_investors`"));
744        }
745    }
746
747    #[cfg(feature = "hoi4")]
748    if Game::is_hoi4() {
749        if listtype == ListType::Random
750            && matches!(
751                name.as_str(),
752                "controlled_state"
753                    | "core_state"
754                    | "owned_controlled_state"
755                    | "owned_state"
756                    | "state"
757            )
758        {
759            vd.field_list_items("prioritize", Item::State);
760        } else {
761            vd.ban_field("prioritize", || "state `random_` iterators");
762        }
763    }
764}
765
766pub fn validate_modifiers_with_base(block: &Block, data: &Everything, sc: &mut ScopeContext) {
767    let mut vd = Validator::new(block, data);
768    if Game::is_jomini() {
769        #[cfg(feature = "jomini")]
770        {
771            vd.field_validated("base", validate_non_dynamic_script_value);
772            vd.multi_field_script_value("add", sc);
773            vd.multi_field_script_value("factor", sc);
774            vd.multi_field_script_value("min", sc);
775            vd.multi_field_script_value("max", sc);
776        }
777    } else {
778        #[cfg(feature = "hoi4")]
779        {
780            // TODO HOI4
781            vd.field_numeric("base");
782            vd.multi_field_numeric("add");
783            vd.multi_field_numeric("factor");
784        }
785    }
786    validate_modifiers(&mut vd, sc);
787    #[cfg(feature = "jomini")]
788    if Game::is_jomini() {
789        validate_scripted_modifier_calls(vd, data, sc);
790    }
791}
792
793pub fn validate_modifiers(vd: &mut Validator, sc: &mut ScopeContext) {
794    let max_sev = vd.max_severity();
795    vd.multi_field_validated_block("first_valid", |b, data| {
796        let mut vd = Validator::new(b, data);
797        vd.set_max_severity(max_sev);
798        validate_modifiers(&mut vd, sc);
799    });
800    vd.multi_field_validated_block("modifier", |b, data| {
801        let mut vd = Validator::new(b, data);
802        vd.set_max_severity(max_sev);
803        validate_trigger_internal(
804            &Lowercase::new_unchecked("modifier"),
805            ListType::None,
806            b,
807            data,
808            sc,
809            vd,
810            Tooltipped::No,
811            false,
812        );
813    });
814    #[cfg(feature = "ck3")]
815    if Game::is_ck3() {
816        vd.multi_field_validated_block_sc("compare_modifier", sc, validate_compare_modifier);
817        vd.multi_field_validated_block_sc("opinion_modifier", sc, validate_opinion_modifier);
818        vd.multi_field_validated_block_sc("ai_value_modifier", sc, validate_ai_value_modifier);
819        vd.multi_field_validated_block_sc(
820            "compatibility_modifier",
821            sc,
822            validate_compatibility_modifier,
823        );
824
825        // These are special single-use modifiers
826        vd.multi_field_validated_block_sc("scheme_modifier", sc, validate_scheme_modifier);
827        vd.multi_field_validated_block_sc("activity_modifier", sc, validate_activity_modifier);
828    }
829
830    #[cfg(feature = "jomini")]
831    if Game::is_jomini() {
832        vd.multi_field_script_value("min", sc);
833        vd.multi_field_script_value("max", sc);
834    }
835    // TODO HOI4
836}
837
838#[cfg(feature = "jomini")]
839pub fn validate_scripted_modifier_call(
840    key: &Token,
841    bv: &BV,
842    modifier: &ScriptedModifier,
843    data: &Everything,
844    sc: &mut ScopeContext,
845) {
846    match bv {
847        BV::Value(token) => {
848            if !modifier.macro_parms().is_empty() {
849                fatal(ErrorKey::Macro).msg("expected macro arguments").loc(token).push();
850            } else if !token.is("yes") {
851                warn(ErrorKey::Validation).msg("expected just modifier = yes").loc(token).push();
852            }
853            modifier.validate_call(key, data, sc);
854        }
855        BV::Block(block) => {
856            let parms = modifier.macro_parms();
857            if parms.is_empty() {
858                fatal(ErrorKey::Macro)
859                    .msg("this scripted modifier does not need macro arguments")
860                    .info("you can just use it as modifier = yes")
861                    .loc(block)
862                    .push();
863            } else {
864                let mut vec = Vec::new();
865                let mut vd = Validator::new(block, data);
866                for parm in &parms {
867                    if let Some(token) = vd.field_value(parm) {
868                        vec.push(token.clone());
869                    } else {
870                        let msg = format!("this scripted modifier needs parameter {parm}");
871                        err(ErrorKey::Macro).msg(msg).loc(block).push();
872                        return;
873                    }
874                }
875                vd.unknown_value_fields(|key, _value| {
876                    let msg = format!("this scripted modifier does not need parameter {key}");
877                    let info = "supplying an unneeded parameter often causes a crash";
878                    fatal(ErrorKey::Macro).msg(msg).info(info).loc(key).push();
879                });
880                let args: Vec<_> = parms.into_iter().zip(vec).collect();
881                modifier.validate_macro_expansion(key, &args, data, sc);
882            }
883        }
884    }
885}
886
887#[cfg(feature = "jomini")]
888pub fn validate_scripted_modifier_calls(
889    mut vd: Validator,
890    data: &Everything,
891    sc: &mut ScopeContext,
892) {
893    vd.unknown_fields(|key, bv| {
894        if let Some(modifier) = data.scripted_modifiers.get(key.as_str()) {
895            validate_scripted_modifier_call(key, bv, modifier, data, sc);
896        } else {
897            let msg = format!("unknown field `{key}`");
898            warn(ErrorKey::UnknownField).msg(msg).loc(key).push();
899        }
900    });
901}
902
903pub fn validate_ai_chance(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
904    match bv {
905        BV::Value(t) => _ = t.expect_number(),
906        BV::Block(b) => validate_modifiers_with_base(b, data, sc),
907    }
908}
909
910/// Validate the left-hand part of a `target = { target_scope }` block.
911///
912/// The caller is expected to have done `sc.open_builder()` before calling and then do `sc.close()` after calling.
913/// Returns true iff validation was complete.
914/// `qeq` is true if the scope chain is to the left of a ?= operator.
915pub fn validate_scope_chain(
916    token: &Token,
917    data: &Everything,
918    sc: &mut ScopeContext,
919    qeq: bool,
920) -> bool {
921    let part_vec = partition(token);
922    for i in 0..part_vec.len() {
923        let mut part_flags = PartFlags::empty();
924        if i == 0 {
925            part_flags |= PartFlags::First;
926        }
927        if i + 1 == part_vec.len() {
928            part_flags |= PartFlags::Last;
929        }
930        if qeq {
931            part_flags |= PartFlags::Question;
932        }
933        let part = &part_vec[i];
934
935        match part {
936            Part::TokenArgument(part, func, arg) => {
937                validate_argument(part_flags, part, func, arg, data, sc);
938            }
939            Part::Token(part) => {
940                let part_lc = Lowercase::new(part.as_str());
941                // prefixed scope transition, e.g. cp:councillor_steward
942                if let Some((prefix, arg)) = part.split_once(':') {
943                    #[allow(clippy::if_same_then_else)] // cfg attributes give a false positive here
944                    // known prefix
945                    if let Some(entry) = scope_prefix(&prefix) {
946                        validate_argument_scope(part_flags, entry, part, &prefix, &arg, data, sc);
947                    } else {
948                        let msg = format!("unknown prefix `{prefix}:`");
949                        err(ErrorKey::Validation).msg(msg).loc(prefix).push();
950                        return false;
951                    }
952                } else if part_lc == "root" {
953                    sc.replace_root();
954                } else if part_lc == "prev" {
955                    if !part_flags.contains(PartFlags::First) && !Game::is_imperator() {
956                        warn_not_first(part);
957                    }
958                    sc.replace_prev();
959                } else if part_lc == "this" {
960                    sc.replace_this();
961                } else if Game::is_hoi4() && part_lc == "from" {
962                    #[cfg(feature = "hoi4")]
963                    sc.replace_from();
964                } else if Game::is_hoi4() && is_country_tag(part.as_str()) {
965                    if !part_flags.contains(PartFlags::First) {
966                        warn_not_first(part);
967                    }
968                    #[cfg(feature = "hoi4")]
969                    data.verify_exists(Item::CountryTag, part);
970                    #[cfg(feature = "hoi4")]
971                    sc.replace(Scopes::Country, part.clone());
972                } else if is_character_token(part.as_str(), data) {
973                    #[cfg(feature = "hoi4")]
974                    sc.replace(Scopes::Character, part.clone());
975                } else if Game::is_hoi4() && part.is_integer() {
976                    // TODO HOI4: figure out if a state id has to be the whole target
977                    if !part_flags.contains(PartFlags::First) {
978                        warn_not_first(part);
979                    }
980                    #[cfg(feature = "hoi4")]
981                    data.verify_exists(Item::State, part);
982                    #[cfg(feature = "hoi4")]
983                    sc.replace(Scopes::State, part.clone());
984                } else if let Some((inscopes, outscope)) = scope_to_scope(part, sc.scopes(data)) {
985                    validate_inscopes(part_flags, part, inscopes, sc, data);
986                    sc.replace(outscope, part.clone());
987                } else {
988                    let msg = format!("unknown token `{part}`");
989                    err(ErrorKey::UnknownField).msg(msg).loc(part).push();
990                    return false;
991                }
992            }
993        }
994    }
995    true
996}
997
998pub fn validate_ifelse_sequence(block: &Block, key_if: &str, key_elseif: &str, key_else: &str) {
999    let mut seen_if = false;
1000    for (key, block) in block.iter_definitions() {
1001        if key.is(key_if) {
1002            seen_if = true;
1003            continue;
1004        } else if key.is(key_elseif) {
1005            if !seen_if {
1006                let msg = format!("`{key_elseif} without preceding `{key_if}`");
1007                warn(ErrorKey::IfElse).msg(msg).loc(key).push();
1008            }
1009            seen_if = true;
1010            continue;
1011        } else if key.is(key_else) {
1012            if !seen_if {
1013                let msg = format!("`{key_else} without preceding `{key_if}`");
1014                warn(ErrorKey::IfElse).msg(msg).loc(key).push();
1015            }
1016            if block.has_key("limit") {
1017                // `else` with a `limit`, followed by another `else`, does work.
1018                seen_if = true;
1019                continue;
1020            }
1021        }
1022        seen_if = false;
1023    }
1024}
1025
1026#[allow(dead_code)]
1027pub fn validate_numeric_range(
1028    block: &Block,
1029    data: &Everything,
1030    min: f64,
1031    max: f64,
1032    sev: Severity,
1033    conf: Confidence,
1034) {
1035    let mut vd = Validator::new(block, data);
1036    let mut count = 0;
1037    let mut prev = 0.0;
1038
1039    for token in vd.values() {
1040        if let Some(n) = token.expect_number() {
1041            count += 1;
1042            if !(min..=max).contains(&n) {
1043                let msg = format!("expected number between {min} and {max}");
1044                report(ErrorKey::Range, sev).conf(conf).msg(msg).loc(token).push();
1045            }
1046            if count == 1 {
1047                prev = n;
1048            } else if count == 2 && n < prev {
1049                let msg = "expected second number to be bigger than first number";
1050                report(ErrorKey::Range, sev).conf(conf).msg(msg).loc(token).push();
1051            } else if count == 3 {
1052                let msg = "expected exactly 2 numbers";
1053                report(ErrorKey::Range, sev).strong().msg(msg).loc(block).push();
1054            }
1055        }
1056    }
1057}
1058
1059pub fn validate_identifier(token: &Token, kind: &str, sev: Severity) {
1060    if token.as_str().contains('.') || token.as_str().contains(':') {
1061        let msg = format!("expected a {kind} here");
1062        report(ErrorKey::Validation, sev).msg(msg).loc(token).push();
1063    }
1064}
1065
1066/// Camera colors must be hsv, and value can be > 1
1067#[cfg(feature = "jomini")]
1068pub fn validate_camera_color(block: &Block, data: &Everything) {
1069    let mut count = 0;
1070    // Get the color tag, as in color = hsv { 0.5 1.0 1.0 }
1071    let tag = block.tag.as_deref().map_or("rgb", Token::as_str);
1072    if tag != "hsv" {
1073        let msg = "camera colors should be in hsv";
1074        warn(ErrorKey::Colors).msg(msg).loc(block).push();
1075        validate_color(block, data);
1076        return;
1077    }
1078
1079    for item in block.iter_items() {
1080        if let Some(t) = item.get_value() {
1081            t.check_number();
1082            if let Some(f) = t.get_number() {
1083                if count <= 1 && !(0.0..=1.0).contains(&f) {
1084                    let msg = "h and s values should be between 0.0 and 1.0";
1085                    err(ErrorKey::Colors).msg(msg).loc(t).push();
1086                }
1087            } else {
1088                let msg = "expected hsv value";
1089                err(ErrorKey::Colors).msg(msg).loc(t).push();
1090            }
1091            count += 1;
1092        }
1093    }
1094    if count != 3 {
1095        let msg = "expected 3 color values";
1096        err(ErrorKey::Colors).msg(msg).loc(block).push();
1097    }
1098}