Skip to main content

tiger_lib/vic3/
effect_validation.rs

1use crate::block::{BV, Block};
2use crate::context::{ScopeContext, Temporary};
3use crate::desc::validate_desc;
4use crate::everything::Everything;
5use crate::helpers::TigerHashSet;
6use crate::item::Item;
7use crate::report::{ErrorKey, ErrorLoc, err, warn};
8use crate::scopes::Scopes;
9use crate::script_value::validate_script_value;
10use crate::token::Token;
11use crate::tooltipped::Tooltipped;
12use crate::trigger::validate_target;
13use crate::validate::{validate_color, validate_optional_duration, validate_possibly_named_color};
14use crate::validator::{Validator, ValueValidator};
15use crate::vic3::data::buildings::BuildingType;
16use crate::vic3::tables::misc::{LOBBY_FORMATION_REASON, STATE_TYPES, STRATA, TARIFF_LEVELS};
17use crate::vic3::validate::validate_treaty_article;
18
19pub fn validate_activate_production_method(
20    _key: &Token,
21    _block: &Block,
22    _data: &Everything,
23    _sc: &mut ScopeContext,
24    mut vd: Validator,
25    _tooltipped: Tooltipped,
26) {
27    vd.req_field("building_type");
28    vd.req_field("production_method");
29    vd.field_item("building_type", Item::BuildingType);
30    // TODO: check that the production method belongs to the building type
31    vd.field_item("production_method", Item::ProductionMethod);
32}
33
34pub fn validate_add_culture_modifier(
35    _key: &Token,
36    _block: &Block,
37    _data: &Everything,
38    sc: &mut ScopeContext,
39    mut vd: Validator,
40    _tooltipped: Tooltipped,
41) {
42    vd.req_field("culture");
43    vd.field_target("culture", sc, Scopes::Culture);
44    validate_optional_duration(&mut vd, sc);
45    vd.field_script_value("multiplier", sc); // seems to be actually an adder
46}
47
48pub fn validate_add_religion_sol_modifier(
49    _key: &Token,
50    _block: &Block,
51    _data: &Everything,
52    sc: &mut ScopeContext,
53    mut vd: Validator,
54    _tooltipped: Tooltipped,
55) {
56    vd.req_field("religion");
57    vd.field_target("religion", sc, Scopes::Religion);
58    validate_optional_duration(&mut vd, sc);
59    vd.field_script_value("multiplier", sc); // seems to be actually an adder
60}
61
62pub fn validate_add_enactment_modifier(
63    _key: &Token,
64    _block: &Block,
65    data: &Everything,
66    sc: &mut ScopeContext,
67    mut vd: Validator,
68    _tooltipped: Tooltipped,
69) {
70    vd.req_field("name");
71    vd.field_validated_value("name", |_key, mut vd| {
72        vd.item(Item::Modifier);
73        let value = vd.value();
74        data.validate_call(Item::Modifier, value, &Block::new(value.loc), sc);
75    });
76    // multiplier executes in root scope. This is probably a bug
77    vd.field_script_value_builder("multiplier", |key| sc.get_multiplier_context(key));
78}
79
80pub fn validate_add_modifier(
81    _key: &Token,
82    bv: &BV,
83    data: &Everything,
84    sc: &mut ScopeContext,
85    _tooltipped: Tooltipped,
86) {
87    match bv {
88        BV::Value(value) => {
89            data.verify_exists(Item::Modifier, value);
90            data.validate_call(Item::Modifier, value, &Block::new(value.loc), sc);
91        }
92        BV::Block(block) => {
93            let mut vd = Validator::new(block, data);
94            vd.set_case_sensitive(false);
95            vd.req_field("name");
96            vd.field_validated_value("name", |_key, mut vd| {
97                vd.item(Item::Modifier);
98                let value = vd.value();
99                data.validate_call(Item::Modifier, value, &Block::new(value.loc), sc);
100            });
101            // multiplier executes in root scope. This is probably a bug
102            vd.field_script_value_builder("multiplier", |key| sc.get_multiplier_context(key));
103            validate_optional_duration(&mut vd, sc);
104            vd.field_bool("is_decaying");
105        }
106    }
107}
108
109pub fn validate_add_journalentry(
110    _key: &Token,
111    _block: &Block,
112    _data: &Everything,
113    sc: &mut ScopeContext,
114    mut vd: Validator,
115    _tooltipped: Tooltipped,
116) {
117    vd.req_field("type");
118    vd.field_item("type", Item::JournalEntry);
119    vd.field_item("objective_subgoal", Item::ObjectiveSubgoal); // undocumented
120    vd.field_target("target", sc, Scopes::all());
121}
122
123pub fn validate_add_loyalists(
124    _key: &Token,
125    _block: &Block,
126    _data: &Everything,
127    sc: &mut ScopeContext,
128    mut vd: Validator,
129    _tooltipped: Tooltipped,
130) {
131    vd.req_field("value");
132    vd.field_script_value("value", sc);
133    vd.field_item_or_target("interest_group", sc, Item::InterestGroup, Scopes::InterestGroup);
134    vd.field_item_or_target("pop_type", sc, Item::PopType, Scopes::PopType);
135    vd.field_choice("strata", STRATA);
136    vd.field_item_or_target("culture", sc, Item::Culture, Scopes::Culture);
137    vd.field_item_or_target("religion", sc, Item::Religion, Scopes::Religion);
138}
139
140pub fn validate_add_technology_progress(
141    _key: &Token,
142    _block: &Block,
143    _data: &Everything,
144    _sc: &mut ScopeContext,
145    mut vd: Validator,
146    _tooltipped: Tooltipped,
147) {
148    vd.req_field("progress");
149    vd.field_numeric("progress");
150    vd.req_field("technology");
151    vd.field_item("technology", Item::Technology);
152}
153
154pub fn validate_add_war_goal(
155    _key: &Token,
156    block: &Block,
157    _data: &Everything,
158    sc: &mut ScopeContext,
159    mut vd: Validator,
160    _tooltipped: Tooltipped,
161) {
162    vd.req_field("holder");
163    vd.field_item_or_target("holder", sc, Item::Country, Scopes::Country);
164    vd.req_field("type");
165    vd.field_item("type", Item::WarGoalType);
166    vd.field_target("state", sc, Scopes::State);
167    // TODO: verify this; there's only one example in vanilla
168    vd.advice_field("country", "docs say `country` but it's `target_country`");
169    vd.field_target("target_country", sc, Scopes::Country);
170    vd.field_target("target_state", sc, Scopes::State);
171    vd.field_target("region", sc, Scopes::StateRegion);
172    vd.field_bool("primary_demand");
173    if let Some(goal_type) = block.get_field_value("type")
174        && goal_type.is("enforce_treaty_article")
175    {
176        vd.multi_field_validated_block_sc("article", sc, validate_treaty_article);
177    }
178}
179
180pub fn validate_remove_war_goal(
181    _key: &Token,
182    _block: &Block,
183    _data: &Everything,
184    sc: &mut ScopeContext,
185    mut vd: Validator,
186    _tooltipped: Tooltipped,
187) {
188    vd.req_field("who");
189    vd.field_item_or_target("who", sc, Item::Country, Scopes::Country);
190    vd.req_field("type");
191    vd.field_item("type", Item::WarGoalType);
192}
193
194pub fn validate_addremove_backers(
195    _key: &Token,
196    _block: &Block,
197    data: &Everything,
198    sc: &mut ScopeContext,
199    mut vd: Validator,
200    _tooltipped: Tooltipped,
201) {
202    for value in vd.values() {
203        if !data.item_exists(Item::Country, value.as_str()) {
204            validate_target(value, data, sc, Scopes::Country);
205        }
206    }
207}
208
209pub fn validate_call_election(
210    _key: &Token,
211    _block: &Block,
212    _data: &Everything,
213    sc: &mut ScopeContext,
214    mut vd: Validator,
215    _tooltipped: Tooltipped,
216) {
217    vd.req_field("months");
218    vd.field_script_value("months", sc);
219}
220
221pub fn validate_change_institution_investment_level(
222    _key: &Token,
223    _block: &Block,
224    _data: &Everything,
225    _sc: &mut ScopeContext,
226    mut vd: Validator,
227    _tooltipped: Tooltipped,
228) {
229    vd.req_field("institution");
230    vd.field_item("institution", Item::Institution);
231    vd.req_field("investment");
232    vd.field_integer("investment");
233}
234
235pub fn validate_set_institution_investment_level(
236    _key: &Token,
237    _block: &Block,
238    _data: &Everything,
239    _sc: &mut ScopeContext,
240    mut vd: Validator,
241    _tooltipped: Tooltipped,
242) {
243    vd.req_field("institution");
244    vd.field_item("institution", Item::Institution);
245    vd.req_field("level");
246    vd.field_integer("level");
247}
248
249pub fn validate_diplomatic_pact(
250    _key: &Token,
251    _block: &Block,
252    _data: &Everything,
253    sc: &mut ScopeContext,
254    mut vd: Validator,
255    _tooltipped: Tooltipped,
256) {
257    vd.req_field("country");
258    vd.req_field("type");
259    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
260    vd.field_item_or_target("country", sc, Item::Country, Scopes::Country);
261    vd.field_item_or_target("first_state", sc, Item::StateRegion, Scopes::State);
262    vd.field_item_or_target("second_state", sc, Item::StateRegion, Scopes::State);
263    vd.field_item("type", Item::DiplomaticAction);
264}
265
266pub fn validate_country_value(
267    _key: &Token,
268    _block: &Block,
269    _data: &Everything,
270    sc: &mut ScopeContext,
271    mut vd: Validator,
272    _tooltipped: Tooltipped,
273) {
274    vd.req_field("country");
275    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
276    vd.req_field("value");
277    vd.field_item_or_target("country", sc, Item::Country, Scopes::Country);
278    vd.field_script_value("value", sc);
279}
280
281pub fn validate_create_building(
282    _key: &Token,
283    block: &Block,
284    _data: &Everything,
285    sc: &mut ScopeContext,
286    mut vd: Validator,
287    _tooltipped: Tooltipped,
288) {
289    vd.req_field("building");
290    vd.field_item("building", Item::BuildingType);
291    let building = block.get_field_value("building");
292    vd.field_validated_list("activate_production_methods", |token, data| {
293        data.verify_exists(Item::ProductionMethod, token);
294        if let Some(building) = building
295            && let Some((_, block, building_item)) =
296                data.get_item::<BuildingType>(Item::BuildingType, building.as_str())
297        {
298            building_item.validate_production_method(token, building, block, data);
299        }
300    });
301    vd.field_bool("subsidized");
302    vd.field_numeric_range("reserves", 0.0..=1.0);
303    vd.field_validated_sc("level", sc, |bv, data, sc| {
304        if let Some(token) = bv.get_value()
305            && token.is("arable_land")
306        {
307            return;
308        }
309        validate_script_value(bv, data, sc);
310    });
311    vd.field_validated_block("add_ownership", |block, data| {
312        let mut vd = Validator::new(block, data);
313        vd.multi_field_validated_block("country", |block, data| {
314            let mut vd = Validator::new(block, data);
315            vd.req_field("country");
316            vd.field_target("country", sc, Scopes::Country);
317            vd.req_field("levels");
318            vd.field_integer("levels");
319        });
320        // Docs say "country" for both, but vanilla uses "building".
321        vd.multi_field_validated_block("building", |block, data| {
322            let mut vd = Validator::new(block, data);
323            vd.req_field("country");
324            vd.field_target("country", sc, Scopes::Country);
325            vd.req_field("levels");
326            vd.field_integer("levels");
327            vd.req_field("type");
328            vd.field_item("type", Item::BuildingType);
329            vd.req_field("region");
330            vd.field_item("region", Item::StateRegion);
331        });
332        // undocumented
333        vd.multi_field_validated_block("company", |block, data| {
334            let mut vd = Validator::new(block, data);
335            vd.req_field("country");
336            vd.field_target("country", sc, Scopes::Country);
337            vd.req_field("type");
338            vd.field_item("type", Item::CompanyType);
339            vd.req_field("levels");
340            vd.field_integer("levels");
341        });
342    });
343}
344
345pub fn validate_create_ship(
346    _key: &Token,
347    _block: &Block,
348    _data: &Everything,
349    sc: &mut ScopeContext,
350    mut vd: Validator,
351    _tooltipped: Tooltipped,
352) {
353    vd.req_field("type");
354    vd.field_item("type", Item::ShipType);
355    vd.field_target("fleet", sc, Scopes::MilitaryFormation);
356    vd.field_item("name", Item::Localization);
357    if let Some(name) = vd.field_identifier("save_scope_as", "scope name") {
358        sc.define_name_token(name.as_str(), Scopes::Ship, name, Temporary::No);
359    }
360}
361
362pub fn validate_create_character(
363    key: &Token,
364    block: &Block,
365    _data: &Everything,
366    sc: &mut ScopeContext,
367    mut vd: Validator,
368    _tooltipped: Tooltipped,
369) {
370    vd.field_localization("name", sc);
371    vd.field_localization("first_name", sc);
372    vd.field_localization("last_name", sc);
373    if block.has_key("name") {
374        vd.ban_field("first_name", || "characters without `name`");
375        vd.ban_field("last_name", || "characters without `name`");
376    } else if block.has_key("first_name") {
377        if !block.has_key("last_name") {
378            let msg = "character has `first_name` but no `last_name`";
379            warn(ErrorKey::Validation).msg(msg).loc(key).push();
380        }
381    } else if block.has_key("last_name") {
382        let msg = "character has `last_name` but no `first_name`";
383        warn(ErrorKey::Validation).msg(msg).loc(key).push();
384    }
385    vd.field_validated_value("culture", |_, mut vd| {
386        vd.maybe_is("primary_culture");
387        vd.item_or_target(sc, Item::Culture, Scopes::Culture);
388    });
389    vd.field_item_or_target("religion", sc, Item::Religion, Scopes::Religion);
390    vd.field_validated_value("female", |_, mut vd| {
391        vd.maybe_bool();
392        vd.target(sc, Scopes::Character);
393    });
394    vd.field_validated_value("noble", |_, mut vd| {
395        vd.maybe_bool();
396        vd.target(sc, Scopes::Character);
397    });
398    vd.field_bool("ruler");
399    vd.field_bool("heir");
400    vd.field_bool("historical");
401    vd.field_validated("age", |bv, data| {
402        match bv {
403            BV::Value(value) => {
404                // age = integer or character scope or default
405                let mut vd = ValueValidator::new(value, data);
406                vd.maybe_is("default");
407                vd.maybe_integer();
408                vd.target(sc, Scopes::Character);
409            }
410            BV::Block(block) => {
411                // age = { min max }
412                let mut vd = Validator::new(block, data);
413                vd.req_tokens_integers_exactly(2);
414            }
415        }
416    });
417    vd.field_item_or_target("ideology", sc, Item::Ideology, Scopes::Ideology);
418    vd.field_item_or_target("interest_group", sc, Item::InterestGroup, Scopes::InterestGroup);
419    vd.field_item("template", Item::CharacterTemplate);
420    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Character);
421    if let Some(name) = vd.field_identifier("save_scope_as", "scope name") {
422        sc.define_name_token(name.as_str(), Scopes::Character, name, Temporary::No);
423    }
424    vd.field_effect_rooted("trait_generation", Tooltipped::No, Scopes::Character);
425    // The item option is undocumented
426    vd.field_item_or_target("hq", sc, Item::StrategicRegion, Scopes::Hq | Scopes::StrategicRegion);
427
428    // undocumented fields
429
430    // TODO: not known how age and birth_date interact
431    vd.field_date("birth_date");
432    vd.field_list_items("traits", Item::CharacterTrait);
433    vd.field_item("dna", Item::Dna);
434    vd.field_bool("is_general");
435    vd.field_bool("is_admiral");
436    vd.field_bool("is_agitator");
437    vd.field_bool("ig_leader");
438    vd.field_item("commander_rank", Item::CommanderRank);
439}
440
441pub fn validate_create_country(
442    _key: &Token,
443    _block: &Block,
444    _data: &Everything,
445    sc: &mut ScopeContext,
446    mut vd: Validator,
447    _tooltipped: Tooltipped,
448) {
449    vd.field_item("tag", Item::Country);
450    vd.field_target_ok_this("origin", sc, Scopes::Country);
451    vd.multi_field_target("state", sc, Scopes::State);
452    vd.multi_field_target("province", sc, Scopes::Province);
453    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Country);
454}
455
456pub fn validate_create_dynamic_country(
457    _key: &Token,
458    block: &Block,
459    _data: &Everything,
460    sc: &mut ScopeContext,
461    mut vd: Validator,
462    _tooltipped: Tooltipped,
463) {
464    vd.field_target_ok_this("origin", sc, Scopes::Country);
465    if !block.has_key("origin") {
466        vd.req_field("country_type");
467        vd.req_field("tier");
468        vd.req_field("culture");
469        vd.req_field("religion");
470        vd.req_field("capital");
471        vd.req_field("color");
472        vd.req_field("primary_unit_color");
473        vd.req_field("secondary_unit_color");
474        vd.req_field("tertiary_unit_color");
475    }
476    vd.field_item("country_type", Item::CountryType);
477    vd.field_item("tier", Item::CountryTier);
478    vd.multi_field_target("culture", sc, Scopes::Culture);
479    vd.field_target("religion", sc, Scopes::Religion);
480    vd.field_target("capital", sc, Scopes::State);
481    vd.field_item("social_hierarchy", Item::SocialHierarchy);
482    vd.field_trigger_rooted("cede_state_trigger", Tooltipped::No, Scopes::State);
483    vd.field_validated("color", validate_possibly_named_color);
484    vd.field_validated("primary_unit_color", validate_possibly_named_color);
485    vd.field_validated("secondary_unit_color", validate_possibly_named_color);
486    vd.field_validated("tertiary_unit_color", validate_possibly_named_color);
487    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Country);
488}
489
490pub fn validate_create_diplomatic_play(
491    _key: &Token,
492    _block: &Block,
493    _data: &Everything,
494    sc: &mut ScopeContext,
495    mut vd: Validator,
496    _tooltipped: Tooltipped,
497) {
498    vd.field_localization("name", sc);
499    vd.field_integer_range("escalation", 0..=100);
500    vd.field_bool("war");
501    vd.field_item_or_target_ok_this("initiator", sc, Item::Country, Scopes::Country);
502    vd.field_item("type", Item::DiplomaticPlay);
503    vd.advice_field(
504        "handle_annexation_as_civil_war",
505        "docs say `handle_annexation_as_civil_war` but it's `annex_as_civil_war`",
506    );
507    vd.field_bool("annex_as_civil_war");
508    for field in &["add_initiator_backers", "add_target_backers"] {
509        vd.field_validated_list(field, |token, data| {
510            let mut vd = ValueValidator::new(token, data);
511            vd.maybe_item(Item::Country);
512            vd.target(sc, Scopes::Country);
513        });
514    }
515    vd.multi_field_validated_block_sc("add_war_goal", sc, validate_war_goal);
516
517    // undocumented
518
519    vd.field_target("target_state", sc, Scopes::State);
520    vd.field_target("target_country", sc, Scopes::Country);
521    vd.field_target("target_region", sc, Scopes::StrategicRegion);
522}
523
524fn validate_war_goal(block: &Block, data: &Everything, sc: &mut ScopeContext) {
525    let mut vd = Validator::new(block, data);
526    vd.set_case_sensitive(false);
527    vd.field_item_or_target_ok_this("holder", sc, Item::Country, Scopes::Country);
528    vd.field_item("type", Item::WarGoalType);
529    vd.advice_field("state", "docs say `state` but it's `target_state`");
530    vd.field_target("target_state", sc, Scopes::State);
531    vd.advice_field("country", "docs say `country` but it's `target_country`");
532    vd.field_target("target_country", sc, Scopes::Country);
533    vd.advice_field("region", "docs say `region` but it's `target_region`");
534    vd.field_target("target_region", sc, Scopes::StrategicRegion);
535    vd.field_bool("primary_demand");
536    if let Some(goal_type) = block.get_field_value("type")
537        && goal_type.is("enforce_treaty_article")
538    {
539        vd.multi_field_validated_block_sc("article", sc, validate_treaty_article);
540    }
541}
542
543pub fn validate_create_mass_migration(
544    _key: &Token,
545    _block: &Block,
546    _data: &Everything,
547    sc: &mut ScopeContext,
548    mut vd: Validator,
549    _tooltipped: Tooltipped,
550) {
551    vd.req_field("origin");
552    vd.field_target("origin", sc, Scopes::Country);
553    vd.req_field("culture");
554    vd.field_target("culture", sc, Scopes::Culture);
555}
556
557pub fn validate_create_military_formation(
558    _key: &Token,
559    block: &Block,
560    _data: &Everything,
561    sc: &mut ScopeContext,
562    mut vd: Validator,
563    _tooltipped: Tooltipped,
564) {
565    vd.field_localization("name", sc);
566    vd.field_choice("type", &["army", "fleet"]);
567    let is_fleet = block.field_value_is("type", "fleet");
568    vd.field_target("hq_region", sc, Scopes::StrategicRegion);
569    vd.multi_field_validated_block("combat_unit", |block, data| {
570        let mut vd = Validator::new(block, data);
571        vd.field_target("type", sc, Scopes::CombatUnitType);
572        vd.field_choice("service_type", &["regular", "conscript"]);
573        if let Some(token) = vd.field_value("service_type")
574            && is_fleet
575            && token.is("conscript")
576        {
577            let msg = "conscript is not applicable to fleets";
578            err(ErrorKey::Choice).msg(msg).loc(token).push();
579        }
580        vd.field_target("state_region", sc, Scopes::StateRegion);
581        vd.field_integer("count");
582    });
583    if is_fleet {
584        vd.ban_field("mobilization_options", || "armies");
585    }
586    vd.field_validated_list("mobilization_options", |token, data| {
587        let mut vd = ValueValidator::new(token, data);
588        vd.target(sc, Scopes::MobilizationOption);
589    });
590
591    // undocumented
592
593    if let Some(name) = vd.field_identifier("save_scope_as", "scope name") {
594        sc.define_name_token(name.as_str(), Scopes::MilitaryFormation, name, Temporary::No);
595    }
596}
597
598pub fn validate_create_pop(
599    _key: &Token,
600    block: &Block,
601    _data: &Everything,
602    _sc: &mut ScopeContext,
603    mut vd: Validator,
604    _tooltipped: Tooltipped,
605) {
606    // This effect is undocumented
607
608    #[allow(clippy::integer_division)]
609    fn sum_fractions_warner<E: ErrorLoc>(sum_fractions: i64, loc: E) {
610        if sum_fractions != 100_000 {
611            let msg = format!(
612                "fractions should add to exactly 1, currently {}.{:05}",
613                sum_fractions / 100_000,
614                sum_fractions % 100_000,
615            );
616            warn(ErrorKey::Validation).msg(msg.trim_end_matches('0')).loc(loc).push();
617        }
618    }
619
620    vd.field_item("pop_type", Item::PopType);
621    vd.field_integer("size");
622
623    let mut available_cultures = TigerHashSet::default();
624
625    if let Some(token) = vd.field_value("culture") {
626        available_cultures.insert(token.clone());
627
628        vd.ban_field("cultures", || "pops with several cultures");
629        vd.field_item("culture", Item::Culture);
630    } else {
631        vd.field_validated_block("cultures", |block, data| {
632            let mut vd = Validator::new(block, data);
633            let mut sum_fractions = 0_i64;
634
635            vd.validate_item_key_values(Item::Culture, |key, mut vd| {
636                available_cultures.insert(key.clone());
637                vd.numeric_range(0.0..=1.0);
638
639                sum_fractions += vd.value().get_fixed_number().unwrap_or(0);
640            });
641
642            sum_fractions_warner(sum_fractions, block);
643        });
644    }
645
646    if block.has_key("religion") {
647        vd.ban_field("split_religion", || "pops without a `religion` field");
648        vd.field_item("religion", Item::Religion);
649    } else if block.has_key("split_religion") {
650        let mut used_cultures = TigerHashSet::default();
651
652        vd.multi_field_validated_block("split_religion", |block, data| {
653            let mut vd = Validator::new(block, data);
654            let mut only_one_culture = false;
655
656            vd.validate_item_key_blocks(Item::Culture, |key, block, data| {
657                if only_one_culture {
658                    let msg = "split_religion should contain only one culture block";
659                    err(ErrorKey::DuplicateItem).msg(msg).loc(key).push();
660                }
661                only_one_culture = true;
662
663                if !available_cultures.contains(key.as_str()) {
664                    let msg = "culture being split does not appear in pop";
665                    err(ErrorKey::FieldMissing).msg(msg).loc(key).push();
666                }
667
668                match used_cultures.get(key) {
669                    Some(duplicate) => {
670                        let msg =
671                            format!("trying to split religion of culture {key} multiple times");
672                        let msg_other = "first split here";
673                        err(ErrorKey::DuplicateField)
674                            .msg(msg)
675                            .loc(key)
676                            .loc_msg(duplicate, msg_other)
677                            .push();
678                    }
679                    None => {
680                        used_cultures.insert(key.clone());
681                    }
682                }
683
684                let mut vd = Validator::new(block, data);
685                let mut sum_fractions = 0_i64;
686
687                vd.validate_item_key_values(Item::Religion, |_, mut vd| {
688                    vd.numeric_range(0.0..=1.0);
689
690                    sum_fractions += vd.value().get_fixed_number().unwrap_or(0);
691                });
692
693                sum_fractions_warner(sum_fractions, block);
694            });
695
696            if !only_one_culture {
697                let msg = "split_religion must contain one culture block";
698                err(ErrorKey::DuplicateItem).msg(msg).loc(block).push();
699            }
700        });
701    }
702}
703
704pub fn validate_create_state(
705    _key: &Token,
706    _block: &Block,
707    _data: &Everything,
708    sc: &mut ScopeContext,
709    mut vd: Validator,
710    _tooltipped: Tooltipped,
711) {
712    // This effect is undocumented
713
714    vd.field_target("country", sc, Scopes::Country);
715    vd.field_list_items("owned_provinces", Item::Province);
716    vd.field_choice("state_type", STATE_TYPES);
717}
718
719pub fn validate_form_government(
720    _key: &Token,
721    _block: &Block,
722    _data: &Everything,
723    sc: &mut ScopeContext,
724    mut vd: Validator,
725    _tooltipped: Tooltipped,
726) {
727    vd.field_script_value("value", sc);
728    vd.multi_field_item("interest_group_type", Item::InterestGroup);
729}
730
731pub fn validate_set_secret_goal(
732    _key: &Token,
733    _block: &Block,
734    _data: &Everything,
735    sc: &mut ScopeContext,
736    mut vd: Validator,
737    _tooltipped: Tooltipped,
738) {
739    vd.req_field("country");
740    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
741    vd.req_field("secret_goal");
742    vd.field_item_or_target("country", sc, Item::Country, Scopes::Country);
743    vd.field_item("secret_goal", Item::SecretGoal);
744}
745
746pub fn validate_post_notification(
747    _key: &Token,
748    mut vd: ValueValidator,
749    sc: &mut ScopeContext,
750    _tooltipped: Tooltipped,
751) {
752    vd.item(Item::Message);
753    vd.implied_localization_sc("notification_", "_name", sc);
754    vd.implied_localization_sc("notification_", "_desc", sc);
755    vd.implied_localization_sc("notification_", "_tooltip", sc);
756}
757
758pub fn validate_progress(
759    _key: &Token,
760    _block: &Block,
761    _data: &Everything,
762    sc: &mut ScopeContext,
763    mut vd: Validator,
764    _tooltipped: Tooltipped,
765) {
766    vd.req_field("value");
767    vd.req_field("name");
768    vd.field_script_value("value", sc);
769    vd.field_item("name", Item::ScriptedProgressBar);
770}
771
772pub fn validate_join_war(
773    _key: &Token,
774    _block: &Block,
775    _data: &Everything,
776    sc: &mut ScopeContext,
777    mut vd: Validator,
778    _tooltipped: Tooltipped,
779) {
780    vd.req_field("target");
781    vd.req_field("side");
782    vd.field_target("target", sc, Scopes::Country);
783    vd.field_target("side", sc, Scopes::Country);
784}
785
786pub fn validate_create_truce(
787    _key: &Token,
788    _block: &Block,
789    _data: &Everything,
790    sc: &mut ScopeContext,
791    mut vd: Validator,
792    _tooltipped: Tooltipped,
793) {
794    vd.req_field("country");
795    vd.req_field("months");
796    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
797    vd.field_target("country", sc, Scopes::Country);
798    // TODO: docs say integer, but check if script value is allowed
799    vd.field_integer("months");
800}
801
802pub fn validate_create_power_bloc(
803    _key: &Token,
804    _block: &Block,
805    _data: &Everything,
806    sc: &mut ScopeContext,
807    mut vd: Validator,
808    _tooltipped: Tooltipped,
809) {
810    vd.req_field("name");
811    vd.req_field("map_color");
812    vd.req_field("identity");
813    // TODO: see if a full desc is allowed here. Docs just say loc key.
814    vd.field_validated_sc("name", sc, validate_desc);
815    // TODO: check if named colors are allowed
816    vd.field_validated_block("map_color", validate_color);
817    vd.field_item("identity", Item::PowerBlocIdentity);
818    vd.multi_field_item("principle", Item::Principle);
819    vd.multi_field_target("member", sc, Scopes::Country);
820
821    // undocumented
822
823    vd.field_date("founding_date");
824}
825
826pub fn validate_create_lobby(
827    _key: &Token,
828    _block: &Block,
829    _data: &Everything,
830    sc: &mut ScopeContext,
831    mut vd: Validator,
832    _tooltipped: Tooltipped,
833) {
834    vd.req_field("type");
835    vd.req_field("target");
836    vd.field_item("type", Item::PoliticalLobby);
837    vd.field_target("target", sc, Scopes::Country);
838    vd.multi_field_target("add_interest_group", sc, Scopes::InterestGroup);
839    // undocumented
840    vd.field_choice("lobby_formation_reason", LOBBY_FORMATION_REASON);
841}
842
843pub fn validate_create_movement(
844    _key: &Token,
845    _block: &Block,
846    _data: &Everything,
847    sc: &mut ScopeContext,
848    mut vd: Validator,
849    _tooltipped: Tooltipped,
850) {
851    vd.req_field("type");
852    vd.field_item("type", Item::PoliticalMovement);
853    vd.advice_field("movement_type", "docs say movement_type but it's just type");
854    vd.field_target("religion", sc, Scopes::Religion);
855    vd.field_target("culture", sc, Scopes::Culture);
856}
857
858pub fn validate_create_catalyst(
859    _key: &Token,
860    _block: &Block,
861    _data: &Everything,
862    sc: &mut ScopeContext,
863    mut vd: Validator,
864    _tooltipped: Tooltipped,
865) {
866    vd.req_field("type");
867    vd.req_field("target");
868    vd.field_item("type", Item::DiplomaticCatalyst);
869    vd.field_target("target", sc, Scopes::Country);
870}
871
872pub fn validate_change_appeasement(
873    _key: &Token,
874    _block: &Block,
875    _data: &Everything,
876    sc: &mut ScopeContext,
877    mut vd: Validator,
878    _tooltipped: Tooltipped,
879) {
880    vd.req_field("amount");
881    vd.req_field("factor");
882    vd.field_item("factor", Item::PoliticalLobbyAppeasement);
883    vd.field_script_value("amount", sc);
884}
885
886/// Validate `set_pop_wealth` and `add_pop_wealth`
887pub fn validate_pop_wealth(
888    _key: &Token,
889    _block: &Block,
890    _data: &Everything,
891    sc: &mut ScopeContext,
892    mut vd: Validator,
893    _tooltipped: Tooltipped,
894) {
895    vd.req_field("wealth_distribution");
896    vd.field_script_value("wealth_distribution", sc);
897    vd.field_bool("update_loyalties");
898}
899
900pub fn validate_kill_character(
901    _key: &Token,
902    bv: &BV,
903    data: &Everything,
904    _sc: &mut ScopeContext,
905    _tooltipped: Tooltipped,
906) {
907    match bv {
908        BV::Value(value) => {
909            // kill_character = yes
910            let mut vd = ValueValidator::new(value, data);
911            vd.bool();
912        }
913        BV::Block(block) => {
914            // kill_character = { hidden = yes value = yes }
915            let mut vd = Validator::new(block, data);
916            vd.set_case_sensitive(false);
917            vd.field_bool("value");
918            vd.field_bool("hidden");
919        }
920    }
921}
922
923// Validated `kill_population`, `kill_population_in_state`, `kill_population_percent`, and
924// `kill_population_percent_in_state`.
925pub fn validate_kill_population(
926    key: &Token,
927    _block: &Block,
928    _data: &Everything,
929    sc: &mut ScopeContext,
930    mut vd: Validator,
931    _tooltipped: Tooltipped,
932) {
933    let percent = key.is("kill_population_percent") || key.is("kill_population_percent_in_state");
934    if percent {
935        vd.field_numeric_range("percent", 0.0..=1.0);
936    } else {
937        vd.field_integer("value");
938    }
939    vd.field_target("culture", sc, Scopes::Culture);
940    vd.field_target("religion", sc, Scopes::Religion);
941    vd.field_target("interest_group", sc, Scopes::InterestGroup);
942    vd.field_item("pop_type", Item::PopType);
943    vd.field_choice("strata", STRATA);
944}
945
946pub fn validate_pop_literacy(
947    _key: &Token,
948    _block: &Block,
949    _data: &Everything,
950    sc: &mut ScopeContext,
951    mut vd: Validator,
952    _tooltipped: Tooltipped,
953) {
954    vd.field_script_value("literacy_rate", sc);
955}
956
957pub fn validate_move_partial_pop(
958    _key: &Token,
959    _block: &Block,
960    _data: &Everything,
961    sc: &mut ScopeContext,
962    mut vd: Validator,
963    _tooltipped: Tooltipped,
964) {
965    vd.req_field("state");
966    vd.field_target("state", sc, Scopes::State);
967    // TODO: verify if these can be script values. Doc example just gives numbers.
968    vd.field_script_value("population", sc);
969    vd.field_script_value("population_ratio", sc);
970}
971
972pub fn validate_set_hub_name(
973    _key: &Token,
974    _block: &Block,
975    _data: &Everything,
976    _sc: &mut ScopeContext,
977    mut vd: Validator,
978    _tooltipped: Tooltipped,
979) {
980    vd.req_field("type");
981    vd.req_field("name");
982    vd.field_choice("type", &["city", "farm", "mine", "port", "wood"]);
983    vd.field_item("name", Item::Localization);
984}
985
986pub fn validate_sort(
987    _key: &Token,
988    _block: &Block,
989    _data: &Everything,
990    sc: &mut ScopeContext,
991    mut vd: Validator,
992    _tooltipped: Tooltipped,
993) {
994    vd.req_field("name");
995    vd.req_field("order");
996    if let Some(name) = vd.field_identifier("name", "list name") {
997        // The "order" is evaluated in the scope of the variable list item, which is not known.
998        sc.open_scope(Scopes::all(), name.clone());
999        vd.field_script_value("order", sc);
1000        sc.close();
1001    }
1002}
1003
1004pub fn validate_owes_obligation(
1005    _key: &Token,
1006    _block: &Block,
1007    _data: &Everything,
1008    sc: &mut ScopeContext,
1009    mut vd: Validator,
1010    _tooltipped: Tooltipped,
1011) {
1012    vd.req_field("country");
1013    vd.req_field("setting");
1014    vd.field_target("country", sc, Scopes::Country);
1015    vd.field_bool("setting");
1016}
1017
1018pub fn validate_owner_of_provinces(
1019    _key: &Token,
1020    _block: &Block,
1021    _data: &Everything,
1022    sc: &mut ScopeContext,
1023    mut vd: Validator,
1024    _tooltipped: Tooltipped,
1025) {
1026    vd.req_field("country");
1027    vd.req_field("provinces");
1028    vd.field_target("country", sc, Scopes::Country);
1029    vd.field_list_items("provinces", Item::Province);
1030}
1031
1032pub fn validate_violate_sovereignty_join(
1033    _key: &Token,
1034    _block: &Block,
1035    _data: &Everything,
1036    sc: &mut ScopeContext,
1037    mut vd: Validator,
1038    _tooltipped: Tooltipped,
1039) {
1040    // undocumented
1041
1042    vd.req_field("violator");
1043    vd.req_field("target");
1044    vd.req_field("join_violator");
1045    vd.field_target("violator", sc, Scopes::Country);
1046    vd.field_target("target", sc, Scopes::Country);
1047    vd.field_bool("join_violator");
1048}
1049
1050pub fn validate_ruling_ig(
1051    _key: &Token,
1052    _block: &Block,
1053    data: &Everything,
1054    _sc: &mut ScopeContext,
1055    mut vd: Validator,
1056    _tooltipped: Tooltipped,
1057) {
1058    for token in vd.values() {
1059        data.verify_exists(Item::InterestGroup, token);
1060    }
1061}
1062
1063pub fn validate_start_tutorial(
1064    _key: &Token,
1065    _block: &Block,
1066    _data: &Everything,
1067    sc: &mut ScopeContext,
1068    mut vd: Validator,
1069    _tooltipped: Tooltipped,
1070) {
1071    // undocumented
1072
1073    vd.req_field("tutorial_lesson");
1074    vd.field_item("tutorial_lesson", Item::TutorialLesson);
1075    vd.field_target("journal_entry", sc, Scopes::JournalEntry);
1076}
1077
1078pub fn validate_audio_event(
1079    _key: &Token,
1080    _block: &Block,
1081    _data: &Everything,
1082    _sc: &mut ScopeContext,
1083    mut vd: Validator,
1084    _tooltipped: Tooltipped,
1085) {
1086    vd.field_value("persistent_object"); // TODO
1087    vd.field_value("event"); // TODO
1088}
1089
1090pub fn validate_withdraw(
1091    _key: &Token,
1092    _block: &Block,
1093    _data: &Everything,
1094    sc: &mut ScopeContext,
1095    mut vd: Validator,
1096    _tooltipped: Tooltipped,
1097) {
1098    vd.req_field("country");
1099    vd.field_target("country", sc, Scopes::Country);
1100    vd.field_bool("apply_break_penalties");
1101}
1102
1103pub fn validate_create_treaty(
1104    _key: &Token,
1105    block: &Block,
1106    _data: &Everything,
1107    sc: &mut ScopeContext,
1108    mut vd: Validator,
1109    _tooltipped: Tooltipped,
1110) {
1111    vd.field_localization("name", sc);
1112    vd.req_field_one_of(&["first_country", "amends"]);
1113    vd.req_field_one_of(&["second_country", "amends"]);
1114    vd.field_target_ok_this("first_country", sc, Scopes::Country);
1115    vd.field_target_ok_this("second_country", sc, Scopes::Country);
1116    vd.field_target_ok_this("amends", sc, Scopes::Treaty);
1117
1118    vd.field_bool("is_draft");
1119    if block.has_key("amends") && !block.get_field_bool("is_draft").unwrap_or(true) {
1120        let msg = "treaties that are amendments must be drafts";
1121        // SAFETY: we can only get here if the block has `is_draft = no`
1122        let loc = block.get_key("draft").unwrap();
1123        err(ErrorKey::Validation).msg(msg).loc(loc).push();
1124    }
1125
1126    vd.field_date("entered_into_force_on");
1127    vd.field_validated_block("binding_period", |block, data| {
1128        let mut vd = Validator::new(block, data);
1129        validate_optional_duration(&mut vd, sc);
1130    });
1131
1132    if !block.has_key("amends") && !block.has_key("articles_to_create") {
1133        let msg = "treaties that are not amendments must create at least one article";
1134        err(ErrorKey::Validation).msg(msg).loc(block).push();
1135    }
1136    vd.field_validated_block("articles_to_create", |block, data| {
1137        let mut vd = Validator::new(block, data);
1138        let mut count = 0;
1139        for block in vd.blocks() {
1140            count += 1;
1141            validate_treaty_article(block, data, sc);
1142        }
1143        if count == 0 {
1144            let msg = "treaties that are not amendments must create at least one article";
1145            err(ErrorKey::Validation).msg(msg).loc(block).push();
1146        }
1147    });
1148}
1149
1150pub fn validate_tariff_level(
1151    _key: &Token,
1152    _block: &Block,
1153    _data: &Everything,
1154    sc: &mut ScopeContext,
1155    mut vd: Validator,
1156    _tooltipped: Tooltipped,
1157) {
1158    vd.field_target("goods", sc, Scopes::Goods);
1159    vd.field_choice("level", TARIFF_LEVELS);
1160}
1161
1162pub fn validate_activate_building(
1163    _key: &Token,
1164    _block: &Block,
1165    _data: &Everything,
1166    _sc: &mut ScopeContext,
1167    mut vd: Validator,
1168    _tooltipped: Tooltipped,
1169) {
1170    vd.multi_field_item("building", Item::BuildingType);
1171}
1172
1173pub fn validate_execute_event_option(
1174    _key: &Token,
1175    _block: &Block,
1176    _data: &Everything,
1177    _sc: &mut ScopeContext,
1178    mut vd: Validator,
1179    _tooltipped: Tooltipped,
1180) {
1181    vd.req_field("event");
1182    vd.req_field("option");
1183    vd.field_item("event", Item::Event);
1184    // TODO: check that the event has the selected option
1185    vd.field_integer_range("option", 0..);
1186}
1187
1188pub fn validate_national_awakening(
1189    _key: &Token,
1190    _block: &Block,
1191    _data: &Everything,
1192    sc: &mut ScopeContext,
1193    mut vd: Validator,
1194    _tooltipped: Tooltipped,
1195) {
1196    vd.req_field("culture");
1197    vd.req_field("months");
1198    vd.field_target("culture", sc, Scopes::Culture);
1199    vd.field_script_value("months", sc);
1200    vd.field_target("state_region", sc, Scopes::StateRegion);
1201}
1202
1203pub fn validate_add_amendment(
1204    _key: &Token,
1205    _block: &Block,
1206    _data: &Everything,
1207    sc: &mut ScopeContext,
1208    mut vd: Validator,
1209    _tooltipped: Tooltipped,
1210) {
1211    vd.req_field("type");
1212    vd.req_field("sponsor");
1213    vd.field_item("type", Item::Amendment);
1214    vd.field_target("sponsor", sc, Scopes::InterestGroup);
1215    vd.field_script_value("cooldown", sc);
1216    vd.field_script_value("timeout", sc);
1217}
1218
1219pub fn validate_spawn_entity_effect(
1220    _key: &Token,
1221    _block: &Block,
1222    _data: &Everything,
1223    sc: &mut ScopeContext,
1224    mut vd: Validator,
1225    _tooltipped: Tooltipped,
1226) {
1227    vd.req_field("name");
1228    vd.req_field("duration");
1229    vd.field_item("name", Item::Entity);
1230    vd.field_script_value("duration", sc);
1231}
1232
1233pub fn validate_teleport_to_front(
1234    _key: &Token,
1235    bv: &BV,
1236    data: &Everything,
1237    sc: &mut ScopeContext,
1238    _tooltipped: Tooltipped,
1239) {
1240    match bv {
1241        BV::Value(token) => {
1242            let mut vd = ValueValidator::new(token, data);
1243            vd.target(sc, Scopes::Front);
1244        }
1245        BV::Block(block) => {
1246            let mut vd = Validator::new(block, data);
1247            vd.field_target("front", sc, Scopes::Front);
1248            vd.field_target("base_camp", sc, Scopes::Province);
1249        }
1250    }
1251}
1252
1253pub fn validate_career_length(
1254    _key: &Token,
1255    _block: &Block,
1256    data: &Everything,
1257    sc: &mut ScopeContext,
1258    mut vd: Validator,
1259    _tooltipped: Tooltipped,
1260) {
1261    vd.req_field("role");
1262    #[allow(clippy::collapsible_if)]
1263    if let Some(role) = vd.field_value("role") {
1264        if !data.item_exists(Item::CharacterRole, role.as_str())
1265            && !data.item_exists(Item::CharacterArchetype, role.as_str())
1266        {
1267            let msg = "`{role}` not found as character role or character archetype";
1268            err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1269        }
1270    }
1271    validate_optional_duration(&mut vd, sc);
1272    // TODO: this might just be a script value
1273    vd.field_list_numeric_exactly("random_range", 2);
1274}
1275
1276pub fn validate_add_character_role(
1277    _key: &Token,
1278    mut vvd: ValueValidator,
1279    _sc: &mut ScopeContext,
1280    _tooltipped: Tooltipped,
1281) {
1282    let role = vvd.value();
1283    if !vvd.data().item_exists(Item::CharacterRole, role.as_str())
1284        && !vvd.data().item_exists(Item::CharacterArchetype, role.as_str())
1285    {
1286        let msg = "`{role}` not found as character role or character archetype";
1287        err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1288    }
1289    vvd.accept();
1290}
1291
1292pub fn validate_temporary_hostilities(
1293    key: &Token,
1294    _block: &Block,
1295    _data: &Everything,
1296    sc: &mut ScopeContext,
1297    mut vd: Validator,
1298    _tooltipped: Tooltipped,
1299) {
1300    vd.req_field("country");
1301    vd.field_target("country", sc, Scopes::Country);
1302    vd.field_choice("type", &["naval", "war", "limited_war"]);
1303    if key.as_str().starts_with("enable_") {
1304        validate_optional_duration(&mut vd, sc);
1305    }
1306}
1307
1308pub fn validate_replace_character_roles(
1309    _key: &Token,
1310    _block: &Block,
1311    _data: &Everything,
1312    _sc: &mut ScopeContext,
1313    mut vd: Validator,
1314    _tooltipped: Tooltipped,
1315) {
1316    for field in &["add", "remove"] {
1317        vd.field_validated_list(field, |role, data| {
1318            if !data.item_exists(Item::CharacterRole, role.as_str())
1319                && !data.item_exists(Item::CharacterArchetype, role.as_str())
1320            {
1321                let msg = "`{role}` not found as character role or character archetype";
1322                err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1323            }
1324        });
1325    }
1326    vd.field_bool("remove_all");
1327}
1328
1329pub fn validate_start_harvest_condition(
1330    _key: &Token,
1331    _block: &Block,
1332    _data: &Everything,
1333    sc: &mut ScopeContext,
1334    mut vd: Validator,
1335    _tooltipped: Tooltipped,
1336) {
1337    vd.req_field("type");
1338    vd.field_item("type", Item::HarvestConditionType);
1339    vd.field_script_value("intensity", sc);
1340    vd.field_script_value("duration", sc);
1341}