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 if let Some(modifier) = data.scripted_modifiers.get(key.as_str()) {
212 if caller != "random" && caller != "random_list" && caller != "duel" {
213 let msg = "cannot use scripted modifier here";
214 err(ErrorKey::Validation).msg(msg).loc(key).push();
215 return false;
216 }
217 validate_scripted_modifier_call(key, bv, modifier, data, sc);
218 return false;
219 }
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 if !token.is("yes") {
232 let msg = format!("expected just `{key} = yes`");
233 warn(ErrorKey::Validation).msg(msg).loc(token).push();
234 }
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 if let Some(number) = token.get_number() {
250 if matches!(effect, Effect::NonNegativeValue) && number < 0.0 {
251 if key.is("add_gold") {
252 let msg = "add_gold does not take negative numbers";
253 let info = "try remove_short_term_gold instead";
254 warn(ErrorKey::Range).msg(msg).info(info).loc(token).push();
255 } else {
256 let msg = format!("{key} does not take negative numbers");
257 warn(ErrorKey::Range).msg(msg).loc(token).push();
258 }
259 }
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 if !data.item_exists(itype, token.as_str()) {
293 validate_target(token, data, sc, outscopes);
294 }
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(feature = "ck3")]
327 Effect::ItemValue(key, itype) => {
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("value");
333 vd.field_item(key, itype);
334 vd.field_script_value("value", sc);
335 }
336 }
337 Effect::Choice(choices) => {
338 if let Some(token) = bv.expect_value() {
339 if !choices.contains(&token.as_str()) {
340 let msg = format!("expected one of {}", choices.join(", "));
341 err(ErrorKey::Choice).msg(msg).loc(token).push();
342 }
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 }
426 sc.close();
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 if let Ok(ltype) = ListType::try_from(it_type.as_str()) {
456 if let Some((inscopes, outscope)) = scope_iterator(&it_name, data, sc) {
457 if ltype.is_for_triggers() {
458 let msg = format!("cannot use `{it_type}_` lists in an effect");
459 err(ErrorKey::Validation).msg(msg).loc(key).push();
460 return false;
461 }
462 sc.expect(inscopes, &Reason::Token(key.clone()), data);
463 if let Some(b) = bv.expect_block() {
464 precheck_iterator_fields(ltype, it_name.as_str(), b, data, sc);
465 sc.open_scope(outscope, key.clone());
466 let mut vd = Validator::new(b, data);
467 has_tooltip |= validate_effect_internal(
468 &Lowercase::new(it_name.as_str()),
469 ltype,
470 b,
471 data,
472 sc,
473 &mut vd,
474 tooltipped,
475 special_tokens,
476 );
477 }
478 sc.close();
479 return has_tooltip && tooltipped.is_tooltipped();
480 }
481 }
482 }
483
484 #[cfg(feature = "hoi4")]
485 if Game::is_hoi4() && key.starts_with("var:") {
486 validate_variable(key, data, sc, Severity::Error);
487 if let Some(block) = bv.expect_block() {
488 sc.open_scope(Scopes::all_but_none(), key.clone());
489 has_tooltip |= validate_effect(block, data, sc, tooltipped);
490 sc.close();
491 }
492 return has_tooltip;
493 }
494
495 if !Game::is_imperator() && scope_trigger(key, data).is_some() {
498 let msg = format!("`{key}` is a trigger and can't be used as an effect");
499 err(ErrorKey::WrongUse).msg(msg).loc(key).push();
500 return false;
501 }
502
503 sc.open_builder();
505 if validate_scope_chain(key, data, sc, matches!(cmp, Comparator::Equals(Question))) {
506 sc.finalize_builder();
507 if Game::is_ck3() && key.starts_with("flag:") {
508 let msg = "as of 1.9, flag literals cannot be used on the left-hand side";
509 err(ErrorKey::Scopes).msg(msg).loc(key).push();
510 }
511 if let Some(block) = bv.expect_block() {
512 has_tooltip |= validate_effect(block, data, sc, tooltipped);
513 }
514 }
515 sc.close();
516 has_tooltip && tooltipped.is_tooltipped()
517}
518
519pub fn validate_effect_control(
521 caller: &Lowercase,
522 block: &Block,
523 data: &Everything,
524 sc: &mut ScopeContext,
525 mut tooltipped: Tooltipped,
526 special_tokens: &mut SpecialTokens,
527) -> bool {
528 let mut vd = Validator::new(block, data);
529
530 if caller == "if" || caller == "else_if" {
531 vd.req_field_warn("limit");
532 }
533
534 #[cfg(feature = "jomini")]
535 if Game::is_jomini()
536 && (caller == "custom_description"
537 || caller == "custom_description_no_bullet"
538 || caller == "custom_tooltip"
539 || caller == "custom_label")
540 {
541 vd.req_field("text");
542 if caller == "custom_tooltip" || caller == "custom_label" {
543 vd.field_item("text", Item::Localization);
544 if let Some(value) = block.get_field_value("text") {
545 data.validate_localization_sc(value.as_str(), sc);
546 }
547 } else if let Some(token) = vd.field_value("text") {
548 validate_effect_localization(token, data, tooltipped);
549 }
550 vd.field_target_ok_this("subject", sc, Scopes::non_primitive());
551 tooltipped = Tooltipped::No;
552 } else {
553 vd.ban_field("text", || "`custom_description` or `custom_tooltip`");
554 vd.ban_field("subject", || "`custom_description` or `custom_tooltip`");
555 }
556
557 #[cfg(feature = "jomini")]
558 if Game::is_jomini() {
559 if caller == "custom_description" || caller == "custom_description_no_bullet" {
560 vd.field_target_ok_this("object", sc, Scopes::non_primitive());
561 vd.field_script_value("value", sc);
562 } else {
563 vd.ban_field("object", || "`custom_description`");
564 }
565 }
566
567 if caller == "hidden_effect" || caller == "hidden_effect_new_object" {
568 tooltipped = Tooltipped::No;
569 }
570
571 if caller == "random" {
572 vd.req_field("chance");
573 if Game::is_jomini() {
574 #[cfg(feature = "jomini")]
575 vd.field_script_value("chance", sc);
576 } else {
577 vd.field_numeric("chance");
579 }
580 } else {
581 vd.ban_field("chance", || "`random`");
582 }
583
584 if caller == "while" {
585 if !(block.has_key("limit") || block.has_key("count")) {
587 let msg = "`while` needs one of `limit` or `count`";
588 warn(ErrorKey::Validation).msg(msg).loc(block).push();
589 }
590
591 if Game::is_jomini() {
592 #[cfg(feature = "jomini")]
593 vd.field_script_value("count", sc);
594 }
595 } else {
596 vd.ban_field("count", || "`while` and `any_` lists");
597 }
598
599 if caller == "random" || caller == "random_list" || caller == "duel" {
600 #[cfg(feature = "vic3")]
601 if Game::is_vic3() {
602 vd.field_script_value("modifier", sc);
604 }
605 #[cfg(any(feature = "imperator", feature = "ck3", feature = "hoi4"))]
606 if Game::is_imperator() || Game::is_ck3() || Game::is_hoi4() {
607 validate_modifiers(&mut vd, sc);
608 }
609 } else {
610 vd.ban_field("modifier", || "`random`, `random_list` or `duel`");
611 vd.ban_field("compare_modifier", || "`random`, `random_list` or `duel`");
612 vd.ban_field("opinion_modifier", || "`random`, `random_list` or `duel`");
613 vd.ban_field("ai_value_modifier", || "`random`, `random_list` or `duel`");
614 vd.ban_field("compatibility", || "`random`, `random_list` or `duel`");
615 }
616
617 if caller == "random_list" || caller == "duel" {
618 vd.field_trigger("trigger", Tooltipped::No, sc);
619 vd.field_bool("show_chance");
620 vd.field_validated_sc("desc", sc, validate_desc);
621 #[cfg(feature = "jomini")]
622 if Game::is_jomini() {
623 vd.field_script_value("min", sc); vd.field_script_value("max", sc); }
626 } else {
627 vd.ban_field("trigger", || "`random_list` or `duel`");
628 vd.ban_field("show_chance", || "`random_list` or `duel`");
629 }
630
631 validate_effect_internal(
632 caller,
633 ListType::None,
634 block,
635 data,
636 sc,
637 &mut vd,
638 tooltipped,
639 special_tokens,
640 )
641}
642
643#[derive(Copy, Clone)]
652#[allow(dead_code)] pub enum Effect {
654 Yes,
656 Boolean,
660 Integer,
664 ScriptValue,
667 #[allow(dead_code)]
669 NonNegativeValue,
670 #[cfg(feature = "vic3")]
672 Date,
673 Scope(Scopes),
677 #[cfg(any(feature = "ck3", feature = "imperator"))]
682 ScopeOkThis(Scopes),
683 Item(Item),
687 ScopeOrItem(Scopes, Item),
694 #[cfg(feature = "ck3")]
699 Target(&'static str, Scopes),
700 #[cfg(any(feature = "ck3", feature = "vic3"))]
705 TargetValue(&'static str, Scopes, &'static str),
706 #[cfg(any(feature = "ck3", feature = "hoi4"))]
711 ItemTarget(&'static str, Item, &'static str, Scopes),
712 #[cfg(feature = "ck3")]
717 ItemValue(&'static str, Item),
718 #[cfg(feature = "ck3")]
722 Desc,
723 #[cfg(any(feature = "ck3", feature = "vic3"))]
727 Timespan,
728 Control,
732 ControlOrLabel,
735 #[cfg(feature = "hoi4")]
737 Iterator(ListType, Scopes),
738 Unchecked,
743 #[cfg(any(feature = "ck3", feature = "vic3", feature = "eu5", feature = "hoi4"))]
745 UncheckedTodo,
746 Choice(&'static [&'static str]),
750 Removed(&'static str, &'static str),
754 Vb(fn(&Token, &Block, &Everything, &mut ScopeContext, Validator, Tooltipped)),
756 Vbc(
758 #[allow(clippy::type_complexity)]
759 fn(
760 &Token,
761 &Block,
762 &Everything,
763 &mut ScopeContext,
764 Validator,
765 Tooltipped,
766 &mut SpecialTokens,
767 ) -> bool,
768 ),
769 Vbv(fn(&Token, &BV, &Everything, &mut ScopeContext, Tooltipped)),
771 Vv(fn(&Token, ValueValidator, &mut ScopeContext, Tooltipped)),
773 Identifier(&'static str),
776 #[cfg(feature = "hoi4")]
779 Value,
780 #[cfg(feature = "ck3")]
782 Color,
783}