1use 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 }
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#[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#[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#[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 #[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 let _ = &bv;
188 let _ = &data;
189 let _ = ≻
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
200pub fn validate_color(block: &Block, _data: &Everything) {
202 let mut count = 0;
205 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 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 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#[allow(unused_variables)] pub 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#[allow(unused_variables)] pub 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 #[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 #[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"); vd.field_any_cmp("count"); } 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"); vd.field("min"); vd.field("max"); 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#[allow(unused_variables)] pub 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 #[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"); } else if name == "owned_controlled_state" {
503 vd.field_list_items("prioritize", Item::State);
504 } else if name == "of" {
505 vd.field_value("array"); vd.field_value("value"); vd.field_value("index"); } else if name == "of_scopes" {
509 vd.field_value("array"); }
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 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"); } 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 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"); } 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"); } else {
718 }
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"); vd.field_any_cmp("value"); } 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 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 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 }
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
896pub 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 if let Some((prefix, arg)) = part.split_once(':') {
929 #[allow(clippy::if_same_then_else)] 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 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 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#[cfg(feature = "jomini")]
1054pub fn validate_camera_color(block: &Block, data: &Everything) {
1055 let mut count = 0;
1056 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}