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 && 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#[allow(unused_variables)] pub 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 #[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 #[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"); vd.field_any_cmp("count"); } 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"); vd.field("min"); vd.field("max"); 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#[allow(unused_variables)] pub 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 #[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"); } else if name == "owned_controlled_state" {
504 vd.field_list_items("prioritize", Item::State);
505 } else if name == "of" {
506 vd.field_value("array"); vd.field_value("value"); vd.field_value("index"); } else if name == "of_scopes" {
510 vd.field_value("array"); }
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 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"); } 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 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 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"); } 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"); } else {
732 }
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"); vd.field_any_cmp("value"); } 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 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 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 }
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
910pub 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 if let Some((prefix, arg)) = part.split_once(':') {
943 #[allow(clippy::if_same_then_else)] 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 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 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#[cfg(feature = "jomini")]
1068pub fn validate_camera_color(block: &Block, data: &Everything) {
1069 let mut count = 0;
1070 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}