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