1use crate::block::{BV, Block, Comparator, Eq::*};
4use crate::context::{Reason, ScopeContext};
5#[cfg(feature = "jomini")]
6use crate::data::effect_localization::validate_effect_localization;
7use crate::desc::validate_desc;
8use crate::everything::Everything;
9use crate::game::Game;
10#[cfg(feature = "hoi4")]
11use crate::hoi4::variables::validate_variable;
12use crate::item::Item;
13use crate::lowercase::Lowercase;
14use crate::report::{ErrorKey, Severity, err, fatal, tips, warn};
15use crate::scopes::{Scopes, scope_iterator};
16#[cfg(feature = "jomini")]
17use crate::script_value::validate_script_value;
18use crate::special_tokens::SpecialTokens;
19use crate::token::Token;
20use crate::tooltipped::Tooltipped;
21use crate::trigger::scope_trigger;
22#[cfg(any(feature = "ck3", feature = "imperator"))]
23use crate::trigger::validate_target_ok_this;
24use crate::trigger::{validate_target, validate_trigger};
25#[cfg(any(feature = "ck3", feature = "vic3"))]
26use crate::validate::validate_compare_duration;
27#[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
28use crate::validate::validate_modifiers;
29#[cfg(feature = "ck3")]
30use crate::validate::validate_possibly_named_color;
31#[cfg(feature = "jomini")]
32use crate::validate::validate_scripted_modifier_call;
33use crate::validate::{
34 ListType, precheck_iterator_fields, validate_identifier, validate_ifelse_sequence,
35 validate_inside_iterator, validate_iterator_fields, validate_scope_chain,
36};
37use crate::validator::{Validator, ValueValidator};
38
39pub fn scope_effect(name: &Token, data: &Everything) -> Option<(Scopes, Effect)> {
42 let scope_effect = match Game::game() {
43 #[cfg(feature = "ck3")]
44 Game::Ck3 => crate::ck3::tables::effects::scope_effect,
45 #[cfg(feature = "vic3")]
46 Game::Vic3 => crate::vic3::tables::effects::scope_effect,
47 #[cfg(feature = "imperator")]
48 Game::Imperator => crate::imperator::tables::effects::scope_effect,
49 #[cfg(feature = "eu5")]
50 Game::Eu5 => crate::eu5::tables::effects::scope_effect,
51 #[cfg(feature = "hoi4")]
52 Game::Hoi4 => crate::hoi4::tables::effects::scope_effect,
53 };
54 scope_effect(name, data)
55}
56
57pub fn validate_effect(
62 block: &Block,
63 data: &Everything,
64 sc: &mut ScopeContext,
65 tooltipped: Tooltipped,
66) -> bool {
67 let mut vd = Validator::new(block, data);
68 validate_effect_internal(
69 Lowercase::empty(),
70 ListType::None,
71 block,
72 data,
73 sc,
74 &mut vd,
75 tooltipped,
76 &mut SpecialTokens::none(),
77 )
78}
79
80#[allow(clippy::too_many_arguments)]
92pub fn validate_effect_internal(
93 caller: &Lowercase,
94 list_type: ListType,
95 block: &Block,
96 data: &Everything,
97 sc: &mut ScopeContext,
98 vd: &mut Validator,
99 mut tooltipped: Tooltipped,
100 special_tokens: &mut SpecialTokens,
101) -> bool {
102 vd.set_case_sensitive(false);
103 if caller == "if"
104 || caller == "else_if"
105 || caller == "else"
106 || caller == "while"
107 || list_type != ListType::None
108 {
109 vd.field_validated_key_block("limit", |key, block, data| {
110 if caller == "else" {
111 let msg = "`else` with a `limit` does work, but may indicate a mistake";
112 let info = "normally you would use `else_if` instead.";
113 tips(ErrorKey::IfElse).msg(msg).info(info).loc(key).push();
114 }
115 validate_trigger(block, data, sc, Tooltipped::No);
116 });
117 } else {
118 vd.ban_field("limit", || "if/else_if or lists");
119 }
120
121 #[allow(clippy::if_not_else)] if list_type != ListType::None {
123 vd.field_trigger("filter", Tooltipped::No, sc);
124 } else {
125 vd.ban_field("filter", || "lists");
126 }
127
128 validate_iterator_fields(caller, list_type, data, sc, vd, &mut tooltipped, false);
129
130 if list_type != ListType::None {
131 validate_inside_iterator(caller, list_type, block, data, sc, vd, tooltipped);
132 }
133
134 validate_ifelse_sequence(block, "if", "else_if", "else");
135
136 vd.set_allow_questionmark_equals(true);
137 let mut has_tooltip = false;
138 vd.unknown_fields_cmp(|key, cmp, bv| {
139 has_tooltip |=
140 validate_effect_field(caller, key, cmp, bv, data, sc, tooltipped, special_tokens);
141 });
142 has_tooltip
143}
144
145#[allow(unused_variables)] #[allow(clippy::too_many_arguments)]
148pub fn validate_effect_field(
149 caller: &Lowercase,
150 key: &Token,
151 cmp: Comparator,
152 bv: &BV,
153 data: &Everything,
154 sc: &mut ScopeContext,
155 tooltipped: Tooltipped,
156 special_tokens: &mut SpecialTokens,
157) -> bool {
158 let mut has_tooltip = false;
159 if let Some(effect) = data.get_effect(key) {
160 match bv {
161 BV::Value(token) => {
162 if !effect.macro_parms().is_empty() {
163 fatal(ErrorKey::Macro).msg("expected macro arguments").loc(token).push();
164 } else if !token.is("yes") {
165 warn(ErrorKey::Validation).msg("expected just effect = yes").loc(token).push();
166 }
167 has_tooltip |= effect.validate_call(key, data, sc, tooltipped, special_tokens);
168 }
169 BV::Block(block) => {
170 let parms = effect.macro_parms();
171 if parms.is_empty() {
172 err(ErrorKey::Macro)
173 .msg("this scripted effect does not need macro arguments")
174 .info("you can just use it as effect = yes")
175 .loc(block)
176 .push();
177 } else {
178 let mut vec = Vec::new();
179 let mut vd = Validator::new(block, data);
180 for parm in &parms {
181 if let Some(token) = vd.field_value(parm) {
182 vec.push(token.clone());
183 } else {
184 let msg = format!("this scripted effect needs parameter {parm}");
185 err(ErrorKey::Macro).msg(msg).loc(block).push();
186 return false;
187 }
188 }
189 vd.unknown_value_fields(|key, _value| {
190 let msg = format!("this scripted effect does not need parameter {key}");
191 let info = "supplying an unneeded parameter often causes a crash";
192 fatal(ErrorKey::Macro).msg(msg).info(info).loc(key).push();
193 });
194 let args: Vec<_> = parms.into_iter().zip(vec).collect();
195 has_tooltip |= effect.validate_macro_expansion(
196 key,
197 &args,
198 data,
199 sc,
200 tooltipped,
201 special_tokens,
202 );
203 }
204 }
205 }
206 return has_tooltip && tooltipped.is_tooltipped();
207 }
208
209 #[cfg(feature = "jomini")]
210 if Game::is_jomini()
211 && let Some(modifier) = data.scripted_modifiers.get(key.as_str())
212 {
213 if caller != "random" && caller != "random_list" && caller != "duel" {
214 let msg = "cannot use scripted modifier here";
215 err(ErrorKey::Validation).msg(msg).loc(key).push();
216 return false;
217 }
218 validate_scripted_modifier_call(key, bv, modifier, data, sc);
219 return false;
220 }
221
222 if let Some((inscopes, effect)) = scope_effect(key, data) {
223 sc.expect(inscopes, &Reason::Token(key.clone()), data);
224 #[cfg(feature = "jomini")]
225 if tooltipped.is_tooltipped() {
226 has_tooltip |= data.item_exists(Item::EffectLocalization, key.as_str());
227 }
228 match effect {
229 Effect::Yes => {
230 if let Some(token) = bv.expect_value()
231 && !token.is("yes")
232 {
233 let msg = format!("expected just `{key} = yes`");
234 warn(ErrorKey::Validation).msg(msg).loc(token).push();
235 }
236 }
237 Effect::Boolean => {
238 if let Some(token) = bv.expect_value() {
239 validate_target(token, data, sc, Scopes::Bool);
240 }
241 }
242 Effect::Integer => {
243 if let Some(token) = bv.expect_value() {
244 token.expect_integer();
245 }
246 }
247 Effect::ScriptValue | Effect::NonNegativeValue => {
248 if let Some(token) = bv.get_value()
249 && let Some(number) = token.get_number()
250 && matches!(effect, Effect::NonNegativeValue)
251 && number < 0.0
252 {
253 if key.is("add_gold") {
254 let msg = "add_gold does not take negative numbers";
255 let info = "try remove_short_term_gold instead";
256 warn(ErrorKey::Range).msg(msg).info(info).loc(token).push();
257 } else {
258 let msg = format!("{key} does not take negative numbers");
259 warn(ErrorKey::Range).msg(msg).loc(token).push();
260 }
261 }
262 #[cfg(feature = "jomini")]
263 if Game::is_jomini() {
264 validate_script_value(bv, data, sc);
265 }
266 }
268 #[cfg(feature = "vic3")]
269 Effect::Date => {
270 if let Some(token) = bv.expect_value() {
271 token.expect_date();
272 }
273 }
274 Effect::Scope(outscopes) => {
275 if let Some(token) = bv.expect_value() {
276 validate_target(token, data, sc, outscopes);
277 }
278 }
279 #[cfg(any(feature = "ck3", feature = "imperator"))]
280 Effect::ScopeOkThis(outscopes) => {
281 if let Some(token) = bv.expect_value() {
282 validate_target_ok_this(token, data, sc, outscopes);
283 }
284 }
285 Effect::Item(itype) => {
286 if let Some(token) = bv.expect_value() {
287 data.verify_exists(itype, token);
288 }
289 }
290 Effect::ScopeOrItem(outscopes, itype) => {
291 if let Some(token) = bv.expect_value()
292 && !data.item_exists(itype, token.as_str())
293 {
294 validate_target(token, data, sc, outscopes);
295 }
296 }
297 #[cfg(feature = "ck3")]
298 Effect::Target(key, outscopes) => {
299 if let Some(block) = bv.expect_block() {
300 let mut vd = Validator::new(block, data);
301 vd.set_case_sensitive(false);
302 vd.req_field(key);
303 vd.field_target(key, sc, outscopes);
304 }
305 }
306 #[cfg(any(feature = "ck3", feature = "vic3"))]
307 Effect::TargetValue(key, outscopes, valuekey) => {
308 if let Some(block) = bv.expect_block() {
309 let mut vd = Validator::new(block, data);
310 vd.set_case_sensitive(false);
311 vd.req_field(key);
312 vd.req_field(valuekey);
313 vd.field_target(key, sc, outscopes);
314 vd.field_script_value(valuekey, sc);
315 }
316 }
317 #[cfg(any(feature = "ck3", feature = "hoi4"))]
318 Effect::ItemTarget(ikey, itype, tkey, outscopes) => {
319 if let Some(block) = bv.expect_block() {
320 let mut vd = Validator::new(block, data);
321 vd.set_case_sensitive(false);
322 vd.field_item(ikey, itype);
323 vd.field_target(tkey, sc, outscopes);
324 }
325 }
326 #[cfg(any(feature = "ck3", feature = "vic3"))]
327 Effect::ItemValue(key, itype, valuekey) => {
328 if let Some(block) = bv.expect_block() {
329 let mut vd = Validator::new(block, data);
330 vd.set_case_sensitive(false);
331 vd.req_field(key);
332 vd.req_field(valuekey);
333 vd.field_item(key, itype);
334 vd.field_script_value(valuekey, sc);
335 }
336 }
337 Effect::Choice(choices) => {
338 if let Some(token) = bv.expect_value()
339 && !choices.contains(&token.as_str())
340 {
341 let msg = format!("expected one of {}", choices.join(", "));
342 err(ErrorKey::Choice).msg(msg).loc(token).push();
343 }
344 }
345 #[cfg(feature = "ck3")]
346 Effect::Desc => validate_desc(bv, data, sc),
347 #[cfg(any(feature = "ck3", feature = "vic3"))]
348 Effect::Timespan => {
349 if let Some(block) = bv.expect_block() {
350 validate_compare_duration(block, data, sc);
351 }
352 }
353 Effect::Vb(f) => {
354 if let Some(block) = bv.expect_block() {
355 let mut vd = Validator::new(block, data);
356 vd.set_case_sensitive(false);
357 f(key, block, data, sc, vd, tooltipped);
358 }
359 }
360 Effect::Vbc(f) => {
361 if let Some(block) = bv.expect_block() {
362 let mut vd = Validator::new(block, data);
363 vd.set_case_sensitive(false);
364 f(key, block, data, sc, vd, tooltipped, special_tokens);
365 }
366 }
367 Effect::Vv(f) => {
368 if let Some(token) = bv.expect_value() {
369 let vd = ValueValidator::new(token, data);
370 f(key, vd, sc, tooltipped);
371 }
372 }
373 Effect::Vbv(f) => {
374 f(key, bv, data, sc, tooltipped);
375 }
376 Effect::ControlOrLabel => match bv {
377 BV::Value(t) => {
378 data.verify_exists(Item::Localization, t);
379 data.validate_localization_sc(t.as_str(), sc);
380 }
381 BV::Block(b) => {
382 has_tooltip |= validate_effect_control(
383 &Lowercase::new(key.as_str()),
384 b,
385 data,
386 sc,
387 tooltipped,
388 special_tokens,
389 );
390 }
391 },
392 Effect::Control => {
393 if let Some(block) = bv.expect_block() {
394 let local_has_tooltip = validate_effect_control(
395 &Lowercase::new(key.as_str()),
396 block,
397 data,
398 sc,
399 tooltipped,
400 special_tokens,
401 );
402 if local_has_tooltip && key.is("random") {
403 special_tokens.insert(key);
404 }
405 has_tooltip |= local_has_tooltip;
406 }
407 }
408 #[cfg(feature = "hoi4")]
409 Effect::Iterator(ltype, outscope) => {
410 let it_name = key.split_once('_').unwrap().1;
411 if let Some(block) = bv.expect_block() {
412 precheck_iterator_fields(ltype, it_name.as_str(), block, data, sc);
413 sc.open_scope(outscope, key.clone());
414 let mut vd = Validator::new(block, data);
415 has_tooltip |= validate_effect_internal(
416 &Lowercase::new(it_name.as_str()),
417 ltype,
418 block,
419 data,
420 sc,
421 &mut vd,
422 tooltipped,
423 special_tokens,
424 );
425 sc.close();
426 }
427 }
428 Effect::Identifier(kind) => {
429 if let Some(token) = bv.expect_value() {
430 validate_identifier(token, kind, Severity::Error);
431 }
432 }
433 #[cfg(feature = "hoi4")]
434 Effect::Value => {
435 if let Some(token) = bv.expect_value() {
436 validate_target(token, data, sc, Scopes::Value);
437 }
438 }
439 #[cfg(feature = "ck3")]
440 Effect::Color => {
441 validate_possibly_named_color(bv, data);
442 }
443 Effect::Removed(version, explanation) => {
444 let msg = format!("`{key}` was removed in {version}");
445 warn(ErrorKey::Removed).msg(msg).info(explanation).loc(key).push();
446 }
447 Effect::Unchecked => (),
448 #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5", feature = "hoi4"))]
449 Effect::UncheckedTodo => (),
450 }
451 return has_tooltip && tooltipped.is_tooltipped();
452 }
453
454 if let Some((it_type, it_name)) = key.split_once('_')
455 && let Ok(ltype) = ListType::try_from(it_type.as_str())
456 && let Some((inscopes, outscope)) = scope_iterator(&it_name, data, sc)
457 {
458 if ltype.is_for_triggers() {
459 let msg = format!("cannot use `{it_type}_` lists in an effect");
460 err(ErrorKey::Validation).msg(msg).loc(key).push();
461 return false;
462 }
463 sc.expect(inscopes, &Reason::Token(key.clone()), data);
464 if let Some(b) = bv.expect_block() {
465 precheck_iterator_fields(ltype, it_name.as_str(), b, data, sc);
466 sc.open_scope(outscope, key.clone());
467 let mut vd = Validator::new(b, data);
468 has_tooltip |= validate_effect_internal(
469 &Lowercase::new(it_name.as_str()),
470 ltype,
471 b,
472 data,
473 sc,
474 &mut vd,
475 tooltipped,
476 special_tokens,
477 );
478 sc.close();
479 }
480 return has_tooltip && tooltipped.is_tooltipped();
481 }
482
483 #[cfg(feature = "hoi4")]
484 if Game::is_hoi4() && key.starts_with("var:") {
485 validate_variable(key, data, sc, Severity::Error);
486 if let Some(block) = bv.expect_block() {
487 sc.open_scope(Scopes::all_but_none(), key.clone());
488 has_tooltip |= validate_effect(block, data, sc, tooltipped);
489 sc.close();
490 }
491 return has_tooltip;
492 }
493
494 if !Game::is_imperator() && scope_trigger(key, data).is_some() {
497 let msg = format!("`{key}` is a trigger and can't be used as an effect");
498 err(ErrorKey::WrongUse).msg(msg).loc(key).push();
499 return false;
500 }
501
502 sc.open_builder();
504 if validate_scope_chain(key, data, sc, matches!(cmp, Comparator::Equals(Question))) {
505 sc.finalize_builder();
506 if Game::is_ck3() && key.starts_with("flag:") {
507 let msg = "as of 1.9, flag literals cannot be used on the left-hand side";
508 err(ErrorKey::Scopes).msg(msg).loc(key).push();
509 }
510 if let Some(block) = bv.expect_block() {
511 has_tooltip |= validate_effect(block, data, sc, tooltipped);
512 }
513 }
514 sc.close();
515 has_tooltip && tooltipped.is_tooltipped()
516}
517
518pub fn validate_effect_control(
520 caller: &Lowercase,
521 block: &Block,
522 data: &Everything,
523 sc: &mut ScopeContext,
524 mut tooltipped: Tooltipped,
525 special_tokens: &mut SpecialTokens,
526) -> bool {
527 let mut vd = Validator::new(block, data);
528
529 if caller == "if" || caller == "else_if" {
530 vd.req_field_warn("limit");
531 }
532
533 #[cfg(feature = "jomini")]
534 if Game::is_jomini()
535 && (caller == "custom_description"
536 || caller == "custom_description_no_bullet"
537 || caller == "custom_tooltip"
538 || caller == "custom_tooltip_no_bullet"
539 || caller == "custom_label"
540 || caller == "custom_label_no_bullet")
541 {
542 vd.req_field("text");
543 if caller == "custom_tooltip"
544 || caller == "custom_tooltip_no_bullet"
545 || caller == "custom_label"
546 || caller == "custom_label_no_bullet"
547 {
548 vd.field_item("text", Item::Localization);
549 if let Some(value) = block.get_field_value("text") {
550 data.validate_localization_sc(value.as_str(), sc);
551 }
552 } else if let Some(token) = vd.field_value("text") {
553 validate_effect_localization(token, data, tooltipped);
554 }
555 vd.field_target_ok_this("subject", sc, Scopes::non_primitive());
556 tooltipped = Tooltipped::No;
557 } else {
558 vd.ban_field("text", || "`custom_description` or `custom_tooltip`");
559 vd.ban_field("subject", || "`custom_description` or `custom_tooltip`");
560 }
561
562 #[cfg(feature = "jomini")]
563 if Game::is_jomini() {
564 if caller == "custom_description" || caller == "custom_description_no_bullet" {
565 vd.field_target_ok_this("object", sc, Scopes::non_primitive());
566 vd.field_script_value("value", sc);
567 } else {
568 vd.ban_field("object", || "`custom_description`");
569 }
570 }
571
572 if caller == "hidden_effect" || caller == "hidden_effect_new_object" {
573 tooltipped = Tooltipped::No;
574 }
575
576 if caller == "random" {
577 vd.req_field("chance");
578 if Game::is_jomini() {
579 #[cfg(feature = "jomini")]
580 vd.field_script_value("chance", sc);
581 } else {
582 vd.field_numeric("chance");
584 }
585 } else {
586 vd.ban_field("chance", || "`random`");
587 }
588
589 if caller == "while" {
590 if !(block.has_key("limit") || block.has_key("count")) {
592 let msg = "`while` needs one of `limit` or `count`";
593 warn(ErrorKey::Validation).msg(msg).loc(block).push();
594 }
595
596 if Game::is_jomini() {
597 #[cfg(feature = "jomini")]
598 vd.field_script_value("count", sc);
599 }
600 } else {
601 vd.ban_field("count", || "`while` and `any_` lists");
602 }
603
604 if caller == "random" || caller == "random_list" || caller == "duel" {
605 #[cfg(feature = "vic3")]
606 if Game::is_vic3() {
607 vd.field_script_value("modifier", sc);
609 }
610 #[cfg(any(feature = "imperator", feature = "ck3", feature = "hoi4"))]
611 if Game::is_imperator() || Game::is_ck3() || Game::is_hoi4() {
612 validate_modifiers(&mut vd, sc);
613 }
614 } else {
615 vd.ban_field("modifier", || "`random`, `random_list` or `duel`");
616 vd.ban_field("compare_modifier", || "`random`, `random_list` or `duel`");
617 vd.ban_field("opinion_modifier", || "`random`, `random_list` or `duel`");
618 vd.ban_field("ai_value_modifier", || "`random`, `random_list` or `duel`");
619 vd.ban_field("compatibility", || "`random`, `random_list` or `duel`");
620 }
621
622 if caller == "random_list" || caller == "duel" {
623 vd.field_trigger("trigger", Tooltipped::No, sc);
624 vd.field_bool("show_chance");
625 vd.field_validated_sc("desc", sc, validate_desc);
626 #[cfg(feature = "jomini")]
627 if Game::is_jomini() {
628 vd.field_script_value("min", sc); vd.field_script_value("max", sc); }
631 } else {
632 vd.ban_field("trigger", || "`random_list` or `duel`");
633 vd.ban_field("show_chance", || "`random_list` or `duel`");
634 }
635
636 validate_effect_internal(
637 caller,
638 ListType::None,
639 block,
640 data,
641 sc,
642 &mut vd,
643 tooltipped,
644 special_tokens,
645 )
646}
647
648#[derive(Copy, Clone)]
657#[allow(dead_code)] pub enum Effect {
659 Yes,
661 Boolean,
665 Integer,
669 ScriptValue,
672 #[allow(dead_code)]
674 NonNegativeValue,
675 #[cfg(feature = "vic3")]
677 Date,
678 Scope(Scopes),
682 #[cfg(any(feature = "ck3", feature = "imperator"))]
687 ScopeOkThis(Scopes),
688 Item(Item),
692 ScopeOrItem(Scopes, Item),
699 #[cfg(feature = "ck3")]
704 Target(&'static str, Scopes),
705 #[cfg(any(feature = "ck3", feature = "vic3"))]
710 TargetValue(&'static str, Scopes, &'static str),
711 #[cfg(any(feature = "ck3", feature = "hoi4"))]
716 ItemTarget(&'static str, Item, &'static str, Scopes),
717 #[cfg(any(feature = "ck3", feature = "vic3"))]
722 ItemValue(&'static str, Item, &'static str),
723 #[cfg(feature = "ck3")]
727 Desc,
728 #[cfg(any(feature = "ck3", feature = "vic3"))]
732 Timespan,
733 Control,
737 ControlOrLabel,
740 #[cfg(feature = "hoi4")]
742 Iterator(ListType, Scopes),
743 Unchecked,
748 #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5", feature = "hoi4"))]
750 UncheckedTodo,
751 Choice(&'static [&'static str]),
755 Removed(&'static str, &'static str),
759 Vb(fn(&Token, &Block, &Everything, &mut ScopeContext, Validator, Tooltipped)),
761 Vbc(
763 #[allow(clippy::type_complexity)]
764 fn(
765 &Token,
766 &Block,
767 &Everything,
768 &mut ScopeContext,
769 Validator,
770 Tooltipped,
771 &mut SpecialTokens,
772 ) -> bool,
773 ),
774 Vbv(fn(&Token, &BV, &Everything, &mut ScopeContext, Tooltipped)),
776 Vv(fn(&Token, ValueValidator, &mut ScopeContext, Tooltipped)),
778 Identifier(&'static str),
781 #[cfg(feature = "hoi4")]
784 Value,
785 #[cfg(feature = "ck3")]
787 Color,
788}