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_target("type", sc, Scopes::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    if let Some(role) = vd.field_value("role") {
391        if !data.item_exists(Item::CharacterRole, role.as_str())
392            && !data.item_exists(Item::CharacterArchetype, role.as_str())
393        {
394            let msg = "`{role}` not found as character role or character archetype";
395            err(ErrorKey::MissingItem).msg(msg).loc(role).push();
396        }
397    }
398    vd.field_validated_value("female", |_, mut vd| {
399        vd.maybe_bool();
400        vd.target(sc, Scopes::Character);
401    });
402    vd.field_validated_value("noble", |_, mut vd| {
403        vd.maybe_bool();
404        vd.target(sc, Scopes::Character);
405    });
406    vd.field_bool("ruler");
407    vd.field_bool("heir");
408    vd.field_bool("historical");
409    vd.field_validated("age", |bv, data| {
410        match bv {
411            BV::Value(value) => {
412                // age = integer or character scope or default
413                let mut vd = ValueValidator::new(value, data);
414                vd.maybe_is("default");
415                vd.maybe_integer();
416                vd.target(sc, Scopes::Character);
417            }
418            BV::Block(block) => {
419                // age = { min max }
420                let mut vd = Validator::new(block, data);
421                vd.req_tokens_integers_exactly(2);
422            }
423        }
424    });
425    vd.field_item_or_target("ideology", sc, Item::Ideology, Scopes::Ideology);
426    vd.field_item_or_target("holding_type", sc, Item::BuildingType, Scopes::BuildingType);
427    vd.field_item_or_target("interest_group", sc, Item::InterestGroup, Scopes::InterestGroup);
428    vd.field_item("home_region", Item::StateRegion);
429    vd.field_item("template", Item::CharacterTemplate);
430    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Character);
431    if let Some(name) = vd.field_identifier("save_scope_as", "scope name") {
432        sc.define_name_token(name.as_str(), Scopes::Character, name, Temporary::No);
433    }
434    if let Some(name) = vd.field_identifier("save_temporary_scope_as", "scope name") {
435        sc.define_name_token(name.as_str(), Scopes::Character, name, Temporary::Yes);
436    }
437    vd.field_effect_rooted("trait_generation", Tooltipped::No, Scopes::Character);
438    // The item option is undocumented
439    vd.field_item_or_target("hq", sc, Item::StrategicRegion, Scopes::Hq | Scopes::StrategicRegion);
440
441    // undocumented fields
442
443    // TODO: not known how age and birth_date interact
444    vd.field_date("birth_date");
445    vd.field_list_items("traits", Item::CharacterTrait);
446    vd.field_item("dna", Item::Dna);
447    vd.field_bool("is_general");
448    vd.field_bool("is_admiral");
449    vd.field_bool("is_agitator");
450    vd.field_bool("ig_leader");
451    vd.field_item("commander_rank", Item::CommanderRank);
452}
453
454pub fn validate_create_country(
455    _key: &Token,
456    _block: &Block,
457    _data: &Everything,
458    sc: &mut ScopeContext,
459    mut vd: Validator,
460    _tooltipped: Tooltipped,
461) {
462    vd.field_item("tag", Item::Country);
463    vd.field_target_ok_this("origin", sc, Scopes::Country);
464    vd.multi_field_target("state", sc, Scopes::State);
465    vd.multi_field_target("province", sc, Scopes::Province);
466    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Country);
467}
468
469pub fn validate_create_dynamic_country(
470    _key: &Token,
471    block: &Block,
472    _data: &Everything,
473    sc: &mut ScopeContext,
474    mut vd: Validator,
475    _tooltipped: Tooltipped,
476) {
477    vd.field_target_ok_this("origin", sc, Scopes::Country);
478    if !block.has_key("origin") {
479        vd.req_field("country_type");
480        vd.req_field("tier");
481        vd.req_field("culture");
482        vd.req_field("religion");
483        vd.req_field("capital");
484        vd.req_field("color");
485        vd.req_field("primary_unit_color");
486        vd.req_field("secondary_unit_color");
487        vd.req_field("tertiary_unit_color");
488    }
489    vd.field_item("country_type", Item::CountryType);
490    vd.field_item("tier", Item::CountryTier);
491    vd.multi_field_target("culture", sc, Scopes::Culture);
492    vd.field_target("religion", sc, Scopes::Religion);
493    vd.field_target("capital", sc, Scopes::State);
494    vd.field_item("social_hierarchy", Item::SocialHierarchy);
495    vd.field_trigger_rooted("cede_state_trigger", Tooltipped::No, Scopes::State);
496    vd.field_validated("color", validate_possibly_named_color);
497    vd.field_validated("primary_unit_color", validate_possibly_named_color);
498    vd.field_validated("secondary_unit_color", validate_possibly_named_color);
499    vd.field_validated("tertiary_unit_color", validate_possibly_named_color);
500    vd.field_effect_rooted("on_created", Tooltipped::No, Scopes::Country);
501}
502
503pub fn validate_create_diplomatic_play(
504    _key: &Token,
505    _block: &Block,
506    _data: &Everything,
507    sc: &mut ScopeContext,
508    mut vd: Validator,
509    _tooltipped: Tooltipped,
510) {
511    vd.field_localization("name", sc);
512    vd.field_integer_range("escalation", 0..=100);
513    vd.field_bool("war");
514    vd.field_item_or_target_ok_this("initiator", sc, Item::Country, Scopes::Country);
515    vd.field_item("type", Item::DiplomaticPlay);
516    vd.advice_field(
517        "handle_annexation_as_civil_war",
518        "docs say `handle_annexation_as_civil_war` but it's `annex_as_civil_war`",
519    );
520    vd.field_bool("annex_as_civil_war");
521    for field in &["add_initiator_backers", "add_target_backers"] {
522        vd.field_validated_list(field, |token, data| {
523            let mut vd = ValueValidator::new(token, data);
524            vd.maybe_item(Item::Country);
525            vd.target(sc, Scopes::Country);
526        });
527    }
528    vd.multi_field_validated_block_sc("add_war_goal", sc, validate_war_goal);
529
530    // undocumented
531
532    vd.field_target("target_state", sc, Scopes::State);
533    vd.field_target("target_country", sc, Scopes::Country);
534    vd.field_target("target_region", sc, Scopes::StrategicRegion);
535}
536
537fn validate_war_goal(block: &Block, data: &Everything, sc: &mut ScopeContext) {
538    let mut vd = Validator::new(block, data);
539    vd.set_case_sensitive(false);
540    vd.field_item_or_target_ok_this("holder", sc, Item::Country, Scopes::Country);
541    vd.field_item("type", Item::WarGoalType);
542    vd.advice_field("state", "docs say `state` but it's `target_state`");
543    vd.field_target("target_state", sc, Scopes::State);
544    vd.advice_field("country", "docs say `country` but it's `target_country`");
545    vd.field_target("target_country", sc, Scopes::Country);
546    vd.advice_field("region", "docs say `region` but it's `target_region`");
547    vd.field_target("target_region", sc, Scopes::StrategicRegion);
548    vd.field_bool("primary_demand");
549    if let Some(goal_type) = block.get_field_value("type")
550        && goal_type.is("enforce_treaty_article")
551    {
552        vd.multi_field_validated_block_sc("article", sc, validate_treaty_article);
553    }
554}
555
556pub fn validate_create_mass_migration(
557    _key: &Token,
558    _block: &Block,
559    _data: &Everything,
560    sc: &mut ScopeContext,
561    mut vd: Validator,
562    _tooltipped: Tooltipped,
563) {
564    vd.req_field("origin");
565    vd.field_target("origin", sc, Scopes::Country);
566    vd.req_field("culture");
567    vd.field_target("culture", sc, Scopes::Culture);
568}
569
570pub fn validate_create_military_formation(
571    _key: &Token,
572    block: &Block,
573    _data: &Everything,
574    sc: &mut ScopeContext,
575    mut vd: Validator,
576    _tooltipped: Tooltipped,
577) {
578    vd.field_localization("name", sc);
579    vd.field_choice("type", &["army", "fleet"]);
580    let is_fleet = block.field_value_is("type", "fleet");
581    vd.field_target("hq_region", sc, Scopes::StrategicRegion);
582    vd.field_target("supply_hub", sc, Scopes::State);
583    vd.multi_field_validated_block("combat_unit", |block, data| {
584        let mut vd = Validator::new(block, data);
585        vd.field_target("type", sc, Scopes::CombatUnitType);
586        vd.field_choice("service_type", &["regular", "conscript"]);
587        if let Some(token) = vd.field_value("service_type")
588            && is_fleet
589            && token.is("conscript")
590        {
591            let msg = "conscript is not applicable to fleets";
592            err(ErrorKey::Choice).msg(msg).loc(token).push();
593        }
594        vd.field_target("state_region", sc, Scopes::StateRegion);
595        vd.field_integer("count");
596    });
597    if is_fleet {
598        vd.ban_field("mobilization_options", || "armies");
599    }
600    vd.field_validated_list("mobilization_options", |token, data| {
601        let mut vd = ValueValidator::new(token, data);
602        vd.target(sc, Scopes::MobilizationOption);
603    });
604
605    // undocumented
606
607    if let Some(name) = vd.field_identifier("save_scope_as", "scope name") {
608        sc.define_name_token(name.as_str(), Scopes::MilitaryFormation, name, Temporary::No);
609    }
610    if let Some(name) = vd.field_identifier("save_temporary_scope_as", "scope name") {
611        sc.define_name_token(name.as_str(), Scopes::MilitaryFormation, name, Temporary::Yes);
612    }
613    vd.field_validated_block("ship", |block, data| {
614        let mut vd = Validator::new(block, data);
615        vd.field_target("type", sc, Scopes::ShipType);
616        vd.field_integer("count");
617        vd.field_target("state_region", sc, Scopes::StateRegion);
618        vd.field_bool("flagship");
619    });
620}
621
622pub fn validate_create_pop(
623    _key: &Token,
624    block: &Block,
625    _data: &Everything,
626    _sc: &mut ScopeContext,
627    mut vd: Validator,
628    _tooltipped: Tooltipped,
629) {
630    // This effect is undocumented
631
632    #[allow(clippy::integer_division)]
633    fn sum_fractions_warner<E: ErrorLoc>(sum_fractions: i64, loc: E) {
634        if sum_fractions != 100_000 {
635            let msg = format!(
636                "fractions should add to exactly 1, currently {}.{:05}",
637                sum_fractions / 100_000,
638                sum_fractions % 100_000,
639            );
640            warn(ErrorKey::Validation).msg(msg.trim_end_matches('0')).loc(loc).push();
641        }
642    }
643
644    vd.field_item("pop_type", Item::PopType);
645    vd.field_integer("size");
646
647    let mut available_cultures = TigerHashSet::default();
648
649    if let Some(token) = vd.field_value("culture") {
650        available_cultures.insert(token.clone());
651
652        vd.ban_field("cultures", || "pops with several cultures");
653        vd.field_item("culture", Item::Culture);
654    } else {
655        vd.field_validated_block("cultures", |block, data| {
656            let mut vd = Validator::new(block, data);
657            let mut sum_fractions = 0_i64;
658
659            vd.validate_item_key_values(Item::Culture, |key, mut vd| {
660                available_cultures.insert(key.clone());
661                vd.numeric_range(0.0..=1.0);
662
663                sum_fractions += vd.value().get_fixed_number().unwrap_or(0);
664            });
665
666            sum_fractions_warner(sum_fractions, block);
667        });
668    }
669
670    if block.has_key("religion") {
671        vd.ban_field("split_religion", || "pops without a `religion` field");
672        vd.field_item("religion", Item::Religion);
673    } else if block.has_key("split_religion") {
674        let mut used_cultures = TigerHashSet::default();
675
676        vd.multi_field_validated_block("split_religion", |block, data| {
677            let mut vd = Validator::new(block, data);
678            let mut only_one_culture = false;
679
680            vd.validate_item_key_blocks(Item::Culture, |key, block, data| {
681                if only_one_culture {
682                    let msg = "split_religion should contain only one culture block";
683                    err(ErrorKey::DuplicateItem).msg(msg).loc(key).push();
684                }
685                only_one_culture = true;
686
687                if !available_cultures.contains(key.as_str()) {
688                    let msg = "culture being split does not appear in pop";
689                    err(ErrorKey::FieldMissing).msg(msg).loc(key).push();
690                }
691
692                match used_cultures.get(key) {
693                    Some(duplicate) => {
694                        let msg =
695                            format!("trying to split religion of culture {key} multiple times");
696                        let msg_other = "first split here";
697                        err(ErrorKey::DuplicateField)
698                            .msg(msg)
699                            .loc(key)
700                            .loc_msg(duplicate, msg_other)
701                            .push();
702                    }
703                    None => {
704                        used_cultures.insert(key.clone());
705                    }
706                }
707
708                let mut vd = Validator::new(block, data);
709                let mut sum_fractions = 0_i64;
710
711                vd.validate_item_key_values(Item::Religion, |_, mut vd| {
712                    vd.numeric_range(0.0..=1.0);
713
714                    sum_fractions += vd.value().get_fixed_number().unwrap_or(0);
715                });
716
717                sum_fractions_warner(sum_fractions, block);
718            });
719
720            if !only_one_culture {
721                let msg = "split_religion must contain one culture block";
722                err(ErrorKey::DuplicateItem).msg(msg).loc(block).push();
723            }
724        });
725    }
726}
727
728pub fn validate_create_state(
729    _key: &Token,
730    _block: &Block,
731    _data: &Everything,
732    sc: &mut ScopeContext,
733    mut vd: Validator,
734    _tooltipped: Tooltipped,
735) {
736    // This effect is undocumented
737
738    vd.field_target("country", sc, Scopes::Country);
739    vd.field_list_items("owned_provinces", Item::Province);
740    vd.field_choice("state_type", STATE_TYPES);
741}
742
743pub fn validate_form_government(
744    _key: &Token,
745    _block: &Block,
746    _data: &Everything,
747    sc: &mut ScopeContext,
748    mut vd: Validator,
749    _tooltipped: Tooltipped,
750) {
751    vd.field_script_value("value", sc);
752    vd.multi_field_item("interest_group_type", Item::InterestGroup);
753}
754
755pub fn validate_set_secret_goal(
756    _key: &Token,
757    _block: &Block,
758    _data: &Everything,
759    sc: &mut ScopeContext,
760    mut vd: Validator,
761    _tooltipped: Tooltipped,
762) {
763    vd.req_field("country");
764    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
765    vd.req_field("secret_goal");
766    vd.field_item_or_target("country", sc, Item::Country, Scopes::Country);
767    vd.field_item("secret_goal", Item::SecretGoal);
768}
769
770pub fn validate_post_notification(
771    _key: &Token,
772    mut vd: ValueValidator,
773    sc: &mut ScopeContext,
774    _tooltipped: Tooltipped,
775) {
776    vd.item(Item::Message);
777    vd.implied_localization_sc("notification_", "_name", sc);
778    vd.implied_localization_sc("notification_", "_desc", sc);
779    vd.implied_localization_sc("notification_", "_tooltip", sc);
780}
781
782pub fn validate_progress(
783    _key: &Token,
784    _block: &Block,
785    _data: &Everything,
786    sc: &mut ScopeContext,
787    mut vd: Validator,
788    _tooltipped: Tooltipped,
789) {
790    vd.req_field("value");
791    vd.req_field("name");
792    vd.field_script_value("value", sc);
793    vd.field_item("name", Item::ScriptedProgressBar);
794}
795
796pub fn validate_join_war(
797    _key: &Token,
798    _block: &Block,
799    _data: &Everything,
800    sc: &mut ScopeContext,
801    mut vd: Validator,
802    _tooltipped: Tooltipped,
803) {
804    vd.req_field("target");
805    vd.req_field("side");
806    vd.field_target("target", sc, Scopes::Country);
807    vd.field_target("side", sc, Scopes::Country);
808}
809
810pub fn validate_create_truce(
811    _key: &Token,
812    _block: &Block,
813    _data: &Everything,
814    sc: &mut ScopeContext,
815    mut vd: Validator,
816    _tooltipped: Tooltipped,
817) {
818    vd.req_field("country");
819    vd.req_field("months");
820    vd.advice_field("tcountry", "documentation says tcountry but it's just country");
821    vd.field_target("country", sc, Scopes::Country);
822    // TODO: docs say integer, but check if script value is allowed
823    vd.field_integer("months");
824}
825
826pub fn validate_create_power_bloc(
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("name");
835    vd.req_field("map_color");
836    vd.req_field("identity");
837    // TODO: see if a full desc is allowed here. Docs just say loc key.
838    vd.field_validated_sc("name", sc, validate_desc);
839    // TODO: check if named colors are allowed
840    vd.field_validated_block("map_color", validate_color);
841    vd.field_item("identity", Item::PowerBlocIdentity);
842    vd.multi_field_item("principle", Item::Principle);
843    vd.multi_field_target("member", sc, Scopes::Country);
844
845    // undocumented
846
847    vd.field_date("founding_date");
848}
849
850pub fn validate_create_lobby(
851    _key: &Token,
852    _block: &Block,
853    _data: &Everything,
854    sc: &mut ScopeContext,
855    mut vd: Validator,
856    _tooltipped: Tooltipped,
857) {
858    vd.req_field("type");
859    vd.req_field("target");
860    vd.field_item("type", Item::PoliticalLobby);
861    vd.field_target("target", sc, Scopes::Country);
862    vd.multi_field_target("add_interest_group", sc, Scopes::InterestGroup);
863    // undocumented
864    vd.field_choice("lobby_formation_reason", LOBBY_FORMATION_REASON);
865}
866
867pub fn validate_create_movement(
868    _key: &Token,
869    _block: &Block,
870    _data: &Everything,
871    sc: &mut ScopeContext,
872    mut vd: Validator,
873    _tooltipped: Tooltipped,
874) {
875    vd.req_field("type");
876    vd.field_item("type", Item::PoliticalMovement);
877    vd.advice_field("movement_type", "docs say movement_type but it's just type");
878    vd.field_target("religion", sc, Scopes::Religion);
879    vd.field_target("culture", sc, Scopes::Culture);
880}
881
882pub fn validate_create_catalyst(
883    _key: &Token,
884    _block: &Block,
885    _data: &Everything,
886    sc: &mut ScopeContext,
887    mut vd: Validator,
888    _tooltipped: Tooltipped,
889) {
890    vd.req_field("type");
891    vd.req_field("target");
892    vd.field_item("type", Item::DiplomaticCatalyst);
893    vd.field_target("target", sc, Scopes::Country);
894}
895
896pub fn validate_change_appeasement(
897    _key: &Token,
898    _block: &Block,
899    _data: &Everything,
900    sc: &mut ScopeContext,
901    mut vd: Validator,
902    _tooltipped: Tooltipped,
903) {
904    vd.req_field("amount");
905    vd.req_field("factor");
906    vd.field_item("factor", Item::PoliticalLobbyAppeasement);
907    vd.field_script_value("amount", sc);
908}
909
910/// Validate `set_pop_wealth` and `add_pop_wealth`
911pub fn validate_pop_wealth(
912    _key: &Token,
913    _block: &Block,
914    _data: &Everything,
915    sc: &mut ScopeContext,
916    mut vd: Validator,
917    _tooltipped: Tooltipped,
918) {
919    vd.req_field("wealth_distribution");
920    vd.field_script_value("wealth_distribution", sc);
921    vd.field_bool("update_loyalties");
922}
923
924pub fn validate_kill_character(
925    _key: &Token,
926    bv: &BV,
927    data: &Everything,
928    _sc: &mut ScopeContext,
929    _tooltipped: Tooltipped,
930) {
931    match bv {
932        BV::Value(value) => {
933            // kill_character = yes
934            let mut vd = ValueValidator::new(value, data);
935            vd.bool();
936        }
937        BV::Block(block) => {
938            // kill_character = { hidden = yes value = yes }
939            let mut vd = Validator::new(block, data);
940            vd.set_case_sensitive(false);
941            vd.field_bool("value");
942            vd.field_bool("hidden");
943        }
944    }
945}
946
947// Validated `kill_population`, `kill_population_in_state`, `kill_population_percent`, and
948// `kill_population_percent_in_state`.
949pub fn validate_kill_population(
950    key: &Token,
951    _block: &Block,
952    _data: &Everything,
953    sc: &mut ScopeContext,
954    mut vd: Validator,
955    _tooltipped: Tooltipped,
956) {
957    let percent = key.is("kill_population_percent") || key.is("kill_population_percent_in_state");
958    if percent {
959        vd.field_numeric_range("percent", 0.0..=1.0);
960    } else {
961        vd.field_integer("value");
962    }
963    vd.field_target("culture", sc, Scopes::Culture);
964    vd.field_target("religion", sc, Scopes::Religion);
965    vd.field_target("interest_group", sc, Scopes::InterestGroup);
966    vd.field_item("pop_type", Item::PopType);
967    vd.field_choice("strata", STRATA);
968}
969
970pub fn validate_pop_literacy(
971    _key: &Token,
972    _block: &Block,
973    _data: &Everything,
974    sc: &mut ScopeContext,
975    mut vd: Validator,
976    _tooltipped: Tooltipped,
977) {
978    vd.field_script_value("literacy_rate", sc);
979}
980
981pub fn validate_move_partial_pop(
982    _key: &Token,
983    _block: &Block,
984    _data: &Everything,
985    sc: &mut ScopeContext,
986    mut vd: Validator,
987    _tooltipped: Tooltipped,
988) {
989    vd.req_field("state");
990    vd.field_target("state", sc, Scopes::State);
991    // TODO: verify if these can be script values. Doc example just gives numbers.
992    vd.field_script_value("population", sc);
993    vd.field_script_value("population_ratio", sc);
994}
995
996pub fn validate_set_hub_name(
997    _key: &Token,
998    _block: &Block,
999    _data: &Everything,
1000    _sc: &mut ScopeContext,
1001    mut vd: Validator,
1002    _tooltipped: Tooltipped,
1003) {
1004    vd.req_field("type");
1005    vd.req_field("name");
1006    vd.field_choice("type", &["city", "farm", "mine", "port", "wood"]);
1007    vd.field_item("name", Item::Localization);
1008}
1009
1010pub fn validate_sort(
1011    _key: &Token,
1012    _block: &Block,
1013    _data: &Everything,
1014    sc: &mut ScopeContext,
1015    mut vd: Validator,
1016    _tooltipped: Tooltipped,
1017) {
1018    vd.req_field("name");
1019    vd.req_field("order");
1020    if let Some(name) = vd.field_identifier("name", "list name") {
1021        // The "order" is evaluated in the scope of the variable list item, which is not known.
1022        sc.open_scope(Scopes::all(), name.clone());
1023        vd.field_script_value("order", sc);
1024        sc.close();
1025    }
1026}
1027
1028pub fn validate_owes_obligation(
1029    _key: &Token,
1030    _block: &Block,
1031    _data: &Everything,
1032    sc: &mut ScopeContext,
1033    mut vd: Validator,
1034    _tooltipped: Tooltipped,
1035) {
1036    vd.req_field("country");
1037    vd.req_field("setting");
1038    vd.field_target("country", sc, Scopes::Country);
1039    vd.field_bool("setting");
1040}
1041
1042pub fn validate_owner_of_provinces(
1043    _key: &Token,
1044    _block: &Block,
1045    _data: &Everything,
1046    sc: &mut ScopeContext,
1047    mut vd: Validator,
1048    _tooltipped: Tooltipped,
1049) {
1050    vd.req_field("country");
1051    vd.req_field("provinces");
1052    vd.field_target("country", sc, Scopes::Country);
1053    vd.field_list_items("provinces", Item::Province);
1054}
1055
1056pub fn validate_violate_sovereignty_join(
1057    _key: &Token,
1058    _block: &Block,
1059    _data: &Everything,
1060    sc: &mut ScopeContext,
1061    mut vd: Validator,
1062    _tooltipped: Tooltipped,
1063) {
1064    // undocumented
1065
1066    vd.req_field("violator");
1067    vd.req_field("target");
1068    vd.req_field("join_violator");
1069    vd.field_target("violator", sc, Scopes::Country);
1070    vd.field_target("target", sc, Scopes::Country);
1071    vd.field_bool("join_violator");
1072}
1073
1074pub fn validate_ruling_ig(
1075    _key: &Token,
1076    _block: &Block,
1077    data: &Everything,
1078    _sc: &mut ScopeContext,
1079    mut vd: Validator,
1080    _tooltipped: Tooltipped,
1081) {
1082    for token in vd.values() {
1083        data.verify_exists(Item::InterestGroup, token);
1084    }
1085}
1086
1087pub fn validate_start_tutorial(
1088    _key: &Token,
1089    _block: &Block,
1090    _data: &Everything,
1091    sc: &mut ScopeContext,
1092    mut vd: Validator,
1093    _tooltipped: Tooltipped,
1094) {
1095    // undocumented
1096
1097    vd.req_field("tutorial_lesson");
1098    vd.field_item("tutorial_lesson", Item::TutorialLesson);
1099    vd.field_target("journal_entry", sc, Scopes::JournalEntry);
1100}
1101
1102pub fn validate_audio_event(
1103    _key: &Token,
1104    _block: &Block,
1105    _data: &Everything,
1106    _sc: &mut ScopeContext,
1107    mut vd: Validator,
1108    _tooltipped: Tooltipped,
1109) {
1110    vd.field_value("persistent_object"); // TODO
1111    vd.field_value("event"); // TODO
1112}
1113
1114pub fn validate_withdraw(
1115    _key: &Token,
1116    _block: &Block,
1117    _data: &Everything,
1118    sc: &mut ScopeContext,
1119    mut vd: Validator,
1120    _tooltipped: Tooltipped,
1121) {
1122    vd.req_field("country");
1123    vd.field_target("country", sc, Scopes::Country);
1124    vd.field_bool("apply_break_penalties");
1125}
1126
1127pub fn validate_create_treaty(
1128    _key: &Token,
1129    block: &Block,
1130    _data: &Everything,
1131    sc: &mut ScopeContext,
1132    mut vd: Validator,
1133    _tooltipped: Tooltipped,
1134) {
1135    vd.field_localization("name", sc);
1136    vd.req_field_one_of(&["first_country", "amends"]);
1137    vd.req_field_one_of(&["second_country", "amends"]);
1138    vd.field_target_ok_this("first_country", sc, Scopes::Country);
1139    vd.field_target_ok_this("second_country", sc, Scopes::Country);
1140    vd.field_target_ok_this("amends", sc, Scopes::Treaty);
1141
1142    vd.field_bool("is_draft");
1143    if block.has_key("amends") && !block.get_field_bool("is_draft").unwrap_or(true) {
1144        let msg = "treaties that are amendments must be drafts";
1145        // SAFETY: we can only get here if the block has `is_draft = no`
1146        let loc = block.get_key("draft").unwrap();
1147        err(ErrorKey::Validation).msg(msg).loc(loc).push();
1148    }
1149
1150    vd.field_date("entered_into_force_on");
1151    vd.field_validated_block("binding_period", |block, data| {
1152        let mut vd = Validator::new(block, data);
1153        validate_optional_duration(&mut vd, sc);
1154    });
1155
1156    if !block.has_key("amends") && !block.has_key("articles_to_create") {
1157        let msg = "treaties that are not amendments must create at least one article";
1158        err(ErrorKey::Validation).msg(msg).loc(block).push();
1159    }
1160    vd.field_validated_block("articles_to_create", |block, data| {
1161        let mut vd = Validator::new(block, data);
1162        let mut count = 0;
1163        for block in vd.blocks() {
1164            count += 1;
1165            validate_treaty_article(block, data, sc);
1166        }
1167        if count == 0 {
1168            let msg = "treaties that are not amendments must create at least one article";
1169            err(ErrorKey::Validation).msg(msg).loc(block).push();
1170        }
1171    });
1172}
1173
1174pub fn validate_tariff_level(
1175    _key: &Token,
1176    _block: &Block,
1177    _data: &Everything,
1178    sc: &mut ScopeContext,
1179    mut vd: Validator,
1180    _tooltipped: Tooltipped,
1181) {
1182    vd.field_target("goods", sc, Scopes::Goods);
1183    vd.field_choice("level", TARIFF_LEVELS);
1184}
1185
1186pub fn validate_activate_building(
1187    _key: &Token,
1188    _block: &Block,
1189    _data: &Everything,
1190    _sc: &mut ScopeContext,
1191    mut vd: Validator,
1192    _tooltipped: Tooltipped,
1193) {
1194    vd.multi_field_item("building", Item::BuildingType);
1195}
1196
1197pub fn validate_execute_event_option(
1198    _key: &Token,
1199    _block: &Block,
1200    _data: &Everything,
1201    _sc: &mut ScopeContext,
1202    mut vd: Validator,
1203    _tooltipped: Tooltipped,
1204) {
1205    vd.req_field("event");
1206    vd.req_field("option");
1207    vd.field_item("event", Item::Event);
1208    // TODO: check that the event has the selected option
1209    vd.field_integer_range("option", 0..);
1210}
1211
1212pub fn validate_national_awakening(
1213    _key: &Token,
1214    _block: &Block,
1215    _data: &Everything,
1216    sc: &mut ScopeContext,
1217    mut vd: Validator,
1218    _tooltipped: Tooltipped,
1219) {
1220    vd.req_field("culture");
1221    vd.req_field("months");
1222    vd.field_target("culture", sc, Scopes::Culture);
1223    vd.field_script_value("months", sc);
1224    vd.field_target("state_region", sc, Scopes::StateRegion);
1225}
1226
1227pub fn validate_add_amendment(
1228    _key: &Token,
1229    _block: &Block,
1230    _data: &Everything,
1231    sc: &mut ScopeContext,
1232    mut vd: Validator,
1233    _tooltipped: Tooltipped,
1234) {
1235    vd.req_field("type");
1236    vd.req_field("sponsor");
1237    vd.field_item_or_target("type", sc, Item::Amendment, Scopes::AmendmentType);
1238    vd.field_target("sponsor", sc, Scopes::InterestGroup);
1239    vd.field_script_value("cooldown", sc);
1240    vd.field_script_value("timeout", sc);
1241}
1242
1243pub fn validate_spawn_entity_effect(
1244    _key: &Token,
1245    _block: &Block,
1246    _data: &Everything,
1247    sc: &mut ScopeContext,
1248    mut vd: Validator,
1249    _tooltipped: Tooltipped,
1250) {
1251    vd.req_field("name");
1252    vd.req_field("duration");
1253    vd.field_item("name", Item::Entity);
1254    vd.field_script_value("duration", sc);
1255}
1256
1257pub fn validate_teleport_to_front(
1258    _key: &Token,
1259    bv: &BV,
1260    data: &Everything,
1261    sc: &mut ScopeContext,
1262    _tooltipped: Tooltipped,
1263) {
1264    match bv {
1265        BV::Value(token) => {
1266            let mut vd = ValueValidator::new(token, data);
1267            vd.target(sc, Scopes::Front);
1268        }
1269        BV::Block(block) => {
1270            let mut vd = Validator::new(block, data);
1271            vd.field_target("front", sc, Scopes::Front);
1272            vd.field_target("base_camp", sc, Scopes::Province);
1273        }
1274    }
1275}
1276
1277pub fn validate_career_length(
1278    _key: &Token,
1279    _block: &Block,
1280    data: &Everything,
1281    sc: &mut ScopeContext,
1282    mut vd: Validator,
1283    _tooltipped: Tooltipped,
1284) {
1285    #[allow(clippy::collapsible_if)]
1286    if let Some(role) = vd.field_value("role") {
1287        if !data.item_exists(Item::CharacterRole, role.as_str())
1288            && !data.item_exists(Item::CharacterArchetype, role.as_str())
1289        {
1290            let msg = "`{role}` not found as character role or character archetype";
1291            err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1292        }
1293    }
1294    validate_optional_duration(&mut vd, sc);
1295    // TODO: this might just be a script value
1296    vd.field_list_numeric_exactly("random_range", 2);
1297}
1298
1299pub fn validate_add_character_role(
1300    _key: &Token,
1301    mut vvd: ValueValidator,
1302    _sc: &mut ScopeContext,
1303    _tooltipped: Tooltipped,
1304) {
1305    let role = vvd.value();
1306    if !vvd.data().item_exists(Item::CharacterRole, role.as_str())
1307        && !vvd.data().item_exists(Item::CharacterArchetype, role.as_str())
1308    {
1309        let msg = "`{role}` not found as character role or character archetype";
1310        err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1311    }
1312    vvd.accept();
1313}
1314
1315pub fn validate_temporary_hostilities(
1316    key: &Token,
1317    _block: &Block,
1318    _data: &Everything,
1319    sc: &mut ScopeContext,
1320    mut vd: Validator,
1321    _tooltipped: Tooltipped,
1322) {
1323    vd.req_field("country");
1324    vd.field_target("country", sc, Scopes::Country);
1325    vd.field_choice("type", &["naval", "war", "limited_war"]);
1326    if key.as_str().starts_with("enable_") {
1327        validate_optional_duration(&mut vd, sc);
1328    }
1329}
1330
1331pub fn validate_replace_character_roles(
1332    _key: &Token,
1333    _block: &Block,
1334    _data: &Everything,
1335    _sc: &mut ScopeContext,
1336    mut vd: Validator,
1337    _tooltipped: Tooltipped,
1338) {
1339    for field in &["add", "remove"] {
1340        vd.field_validated_list(field, |role, data| {
1341            if !data.item_exists(Item::CharacterRole, role.as_str())
1342                && !data.item_exists(Item::CharacterArchetype, role.as_str())
1343            {
1344                let msg = "`{role}` not found as character role or character archetype";
1345                err(ErrorKey::MissingItem).msg(msg).loc(role).push();
1346            }
1347        });
1348    }
1349    vd.field_bool("remove_all");
1350}
1351
1352pub fn validate_start_harvest_condition(
1353    _key: &Token,
1354    _block: &Block,
1355    _data: &Everything,
1356    sc: &mut ScopeContext,
1357    mut vd: Validator,
1358    _tooltipped: Tooltipped,
1359) {
1360    vd.req_field("type");
1361    vd.field_item("type", Item::HarvestConditionType);
1362    vd.field_script_value("intensity", sc);
1363    vd.field_script_value("duration", sc);
1364}