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