tiger_lib/vic3/data/
treaty_articles.rs

1use crate::block::Block;
2use crate::context::ScopeContext;
3use crate::db::{Db, DbKind};
4use crate::everything::Everything;
5use crate::game::GameFlags;
6use crate::item::{Item, ItemLoader};
7use crate::modif::validate_modifs;
8use crate::report::{ErrorKey, err};
9use crate::scopes::Scopes;
10use crate::token::Token;
11use crate::tooltipped::Tooltipped;
12use crate::validator::Validator;
13use crate::vic3::modif::ModifKinds;
14
15#[derive(Clone, Debug)]
16pub struct TreatyArticle {}
17
18inventory::submit! {
19    ItemLoader::Normal(GameFlags::Vic3, Item::TreatyArticle, TreatyArticle::add)
20}
21
22impl TreatyArticle {
23    pub fn add(db: &mut Db, key: Token, block: Block) {
24        db.add(Item::TreatyArticle, key, block, Box::new(Self {}));
25    }
26}
27
28impl DbKind for TreatyArticle {
29    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
30        let mut vd = Validator::new(block, data);
31
32        data.verify_exists(Item::Localization, key);
33        data.verify_exists_implied(Item::Localization, &format!("{key}_desc"), key);
34        data.verify_exists_implied(Item::Localization, &format!("{key}_effects_desc"), key);
35        data.verify_exists_implied(Item::Localization, &format!("{key}_article_short_desc"), key);
36
37        vd.req_field("kind");
38        vd.field_choice("kind", &["directed", "mutual"]);
39        let is_directed = block.get_field_value("kind").is_some_and(|k| k.is("directed"));
40
41        vd.field_integer_range("cost", 0..);
42        vd.field_numeric("relations_progress_per_day");
43        vd.field_integer_range("relations_improvement_max", 0..);
44        vd.field_integer_range("relations_improvement_min", 0..);
45
46        vd.field_item("icon", Item::File);
47
48        let flags = &[
49            "is_alliance",
50            "is_defensive_pact",
51            "is_guarantee_independence",
52            "is_support_independence",
53            "is_host_power_bloc_embassy",
54            "is_offer_embassy",
55            "is_investment_rights",
56            "is_join_power_bloc",
57            "is_trade_privilege",
58            "is_military_access",
59            "is_military_assistance",
60            "is_transit_rights",
61            "is_non_colonization_agreement",
62            "is_goods_transfer",
63            "is_money_transfer",
64            "is_monopoly_for_company",
65            "is_prohibit_goods_trade_with_world_market",
66            "is_no_tariffs",
67            "is_no_subventions",
68            "is_take_on_debt",
69            "is_treaty_port",
70            "is_law_commitment",
71            "can_be_renegotiated",
72            "can_be_enforced",
73            "causes_state_transfer",
74            "friendly",
75            "giftable",
76            "hostile",
77            // undocumented
78            "target_subjects_as_input",
79        ];
80        vd.field_list_choice("flags", flags);
81
82        vd.field_choice(
83            "usage_limit",
84            &["once_per_treaty", "once_per_side", "once_per_side_with_same_inputs"],
85        );
86
87        if is_directed {
88            vd.req_field("maintenance_paid_by");
89            vd.field_choice("maintenance_paid_by", &["target_country", "source_country"]);
90        } else {
91            vd.ban_field("maintenance_paid_by", || "directed articles");
92        }
93
94        let required_inputs = &[
95            "quantity",
96            "goods",
97            "state",
98            "strategic_region",
99            "company",
100            "building_type",
101            "law_type",
102            "country",
103        ];
104        vd.field_list_choice("required_inputs", required_inputs);
105
106        let required_inputs = block.get_field_list("required_inputs");
107        let required_inputs = required_inputs.as_ref().map_or(&[][..], Vec::as_slice);
108        for input in required_inputs {
109            if input.is("quantity") {
110                fn build_quantity_sc(key: &Token, is_directed: bool) -> ScopeContext {
111                    let mut sc = build_article_sc(key, is_directed);
112                    sc.define_name("other_country", Scopes::Country, key);
113                    sc
114                }
115
116                vd.field_script_value_builder("quantity_min_value", |key| {
117                    build_quantity_sc(key, is_directed)
118                });
119                vd.field_script_value_builder("quantity_max_value", |key| {
120                    build_quantity_sc(key, is_directed)
121                });
122            } else {
123                let valid_trigger = format!("{input}_valid_trigger");
124                vd.field_trigger_builder(&valid_trigger, Tooltipped::No, |key| {
125                    build_input_sc(key, input)
126                });
127            }
128        }
129
130        vd.field_list_items("mutual_exclusions", Item::TreatyArticle);
131        vd.field_list_items("unlocked_by_technologies", Item::Technology);
132        vd.field_list_items("automatically_support", Item::DiplomaticPlay);
133        vd.field_validated_key_block("non_fulfillment", |key, block, data| {
134            let mut vd = Validator::new(block, data);
135            vd.field_choice("consequences", &["none", "withdraw", "freeze"]);
136            let is_consequences_none =
137                block.get_field_value("consequences").is_none_or(|t| t.is("none"));
138            if is_consequences_none {
139                vd.ban_field("max_consecutive_contraventions", || "withdraw/freeze");
140                vd.ban_field("conditions", || "withdraw/freeze");
141            } else if block
142                .get_field_block("conditions")
143                .is_none_or(|b| b.num_items() == 0 || b.field_value_is("always", "no"))
144            {
145                let msg = "at least one of the conditions triggers must be non-empty";
146                err(ErrorKey::Validation).msg(msg).loc(key).push();
147            }
148
149            vd.field_integer_range("max_consecutive_contraventions", 0..);
150            vd.field_validated_block("conditions", |block, data| {
151                let mut vd = Validator::new(block, data);
152                for interval in &["weekly", "monthly", "yearly"] {
153                    vd.field_trigger_builder(interval, Tooltipped::No, |key| {
154                        let mut sc = ScopeContext::new(Scopes::Country, key);
155                        sc.define_name("article", Scopes::TreatyArticle, key);
156                        sc
157                    });
158                }
159            });
160        });
161
162        if !is_directed {
163            vd.ban_field("source_modifier", || "directed");
164            vd.ban_field("target_modifier", || "directed");
165        }
166
167        for modifier in &["source_modifier", "target_modifier", "mutual_modifier"] {
168            vd.multi_field_validated_block(modifier, |block, data| {
169                let vd = Validator::new(block, data);
170                validate_modifs(block, data, ModifKinds::Country, vd);
171            });
172        }
173
174        vd.field_trigger_rooted("visible", Tooltipped::No, Scopes::Country);
175        vd.field_trigger_builder("possible", Tooltipped::Yes, |key| {
176            let mut sc = ScopeContext::new(Scopes::Country, key);
177            sc.define_name("other_country", Scopes::Country, key);
178            sc
179        });
180        vd.multi_field_validated_block("requirement_to_maintain", |block, data| {
181            let mut vd = Validator::new(block, data);
182            vd.field_trigger_builder("trigger", Tooltipped::Yes, |key| {
183                build_article_treaty_sc(key, is_directed)
184            });
185            vd.field_trigger_builder("show_about_to_break_warning", Tooltipped::No, |key| {
186                build_article_treaty_sc(key, is_directed)
187            });
188        });
189        vd.field_trigger_builder("can_ratify", Tooltipped::Yes, |key| {
190            build_article_treaty_sc(key, is_directed)
191        });
192
193        for active in &["on_entry_into_force", "on_enforced"] {
194            vd.field_effect_builder(active, Tooltipped::Yes, |key| {
195                let mut sc = ScopeContext::new(Scopes::None, key);
196                sc.define_name("treaty_options", Scopes::TreatyOptions, key);
197                sc.define_name("article_options", Scopes::TreatyArticleOptions, key);
198                sc
199            });
200        }
201
202        vd.field_trigger_builder("can_withdraw", Tooltipped::Yes, |key| {
203            let mut sc = ScopeContext::new(Scopes::None, key);
204            sc.define_name("withdrawing_country", Scopes::Country, key);
205            sc.define_name("non_withdrawing_country", Scopes::Country, key);
206
207            if is_directed {
208                sc.define_name("source_country", Scopes::Country, key);
209                sc.define_name("target_country", Scopes::Country, key);
210            } else {
211                sc.define_name("first_country", Scopes::Country, key);
212                sc.define_name("second_country", Scopes::Country, key);
213            }
214            sc
215        });
216
217        for deactive in &["on_withdrawal", "on_break"] {
218            vd.field_effect_builder(deactive, Tooltipped::Yes, |key| {
219                let mut sc = ScopeContext::new(Scopes::None, key);
220                sc.define_name("treaty_options", Scopes::TreatyOptions, key);
221                sc.define_name("article", Scopes::TreatyArticle, key);
222                sc.define_name("withdrawing_country", Scopes::Country, key);
223                sc.define_name("non_withdrawing_country", Scopes::Country, key);
224                sc
225            });
226        }
227
228        vd.field_validated_block("ai", |block, data| {
229            validate_ai(block, data, required_inputs, is_directed);
230        });
231
232        vd.field_validated_block("wargoal", |block, data| {
233            let mut vd = Validator::new(block, data);
234            vd.field_integer("execution_priority");
235            let contestion_types = &[
236                "control_target_state",
237                "control_target_country_capital",
238                "control_any_target_country_state",
239                "control_own_state",
240                "control_own_capital",
241                "control_all_own_states",
242                "control_all_target_country_claims",
243                "control_any_target_incorporated_state",
244            ];
245            vd.field_choice("contestion_type", contestion_types);
246
247            for script_value in &["maneuvers", "infamy"] {
248                vd.field_script_value_builder(script_value, |key| {
249                    let mut sc = ScopeContext::new(Scopes::Country, key);
250                    sc.define_name("target_country", Scopes::Country, key);
251                    for input in required_inputs {
252                        match input.as_str() {
253                            "quantity" => sc.define_name("quantity", Scopes::Value, key),
254                            "goods" => {
255                                sc.define_name("goods", Scopes::Goods, key);
256                                sc.define_name("market_goods", Scopes::MarketGoods, key);
257                            }
258                            "state" => sc.define_name("state", Scopes::State, key),
259                            "strategic_region" => {
260                                sc.define_name("region", Scopes::StrategicRegion, key);
261                            }
262                            "company" => sc.define_name("company", Scopes::Company, key),
263                            "country" => sc.define_name("country", Scopes::Country, key),
264                            // TODO: verify whether X or X_type scopes
265                            "building_type" => sc.define_name("building", Scopes::Building, key),
266                            "law_type" => sc.define_name("law", Scopes::Law, key),
267                            _ => unreachable!(),
268                        }
269                    }
270                    sc
271                });
272            }
273        });
274    }
275}
276
277fn build_input_sc(key: &Token, input: &Token) -> ScopeContext {
278    let mut sc = ScopeContext::new(Scopes::Country, key);
279    sc.define_name("other_country", Scopes::Country, key);
280    sc.define_name("article", Scopes::TreatyArticle, key);
281    let input_scope = match input.as_str() {
282        "goods" => Scopes::Goods,
283        "state" => Scopes::State,
284        "strategic_region" => Scopes::StrategicRegion,
285        "company" => Scopes::Company,
286        "building_type" => Scopes::BuildingType,
287        "law_type" => Scopes::LawType,
288        "country" => Scopes::Country,
289        _ => unreachable!(),
290    };
291    sc.define_name("input", input_scope, key);
292
293    if input.is("goods") {
294        sc.define_name("market_goods", Scopes::MarketGoods, key);
295    }
296    sc
297}
298
299fn build_article_sc(key: &Token, is_directed: bool) -> ScopeContext {
300    let mut sc = ScopeContext::new(Scopes::Country, key);
301    sc.define_name("article", Scopes::TreatyArticle, key);
302
303    if is_directed {
304        sc.define_name("source_country", Scopes::Country, key);
305        sc.define_name("target_country", Scopes::Country, key);
306    } else {
307        sc.define_name("first_country", Scopes::Country, key);
308        sc.define_name("second_country", Scopes::Country, key);
309    }
310    sc
311}
312
313fn build_article_treaty_sc(key: &Token, is_directed: bool) -> ScopeContext {
314    let mut sc = build_article_sc(key, is_directed);
315    sc.define_name("treaty", Scopes::Treaty, key);
316    sc
317}
318
319fn validate_ai(block: &Block, data: &Everything, required_inputs: &[Token], is_directed: bool) {
320    let mut vd = Validator::new(block, data);
321    vd.field_script_value_rooted("evaluation_chance", Scopes::Country);
322
323    for input in required_inputs {
324        if input.is("quantity") {
325            vd.field_script_value_builder("quantity_input_value", |key| {
326                let mut sc = ScopeContext::new(Scopes::Country, key);
327                sc.define_name("other_country", Scopes::Country, key);
328                sc.define_name("article", Scopes::TreatyArticle, key);
329                sc
330            });
331        } else {
332            let filter = format!("{input}_input_filter");
333            vd.field_trigger_builder(&filter, Tooltipped::No, |key| build_input_sc(key, input));
334        }
335    }
336
337    vd.field_list_choice("article_ai_usage", &["offer", "request"]);
338    let categories = &[
339        "economy",
340        "trade",
341        "military",
342        "military_defense",
343        "ideology",
344        "expansion",
345        "power_bloc",
346        "other",
347        "none",
348    ];
349    vd.field_list_choice("treaty_categories", categories);
350    vd.field_integer("combined_acceptance_cap_max");
351    vd.field_integer("combined_acceptance_cap_min");
352    vd.field_script_value_builder("inherent_accept_score", |key| {
353        build_article_sc(key, is_directed)
354    });
355    vd.field_script_value_builder("contextual_accept_score", |key| {
356        build_article_treaty_sc(key, is_directed)
357    });
358    vd.field_script_value_builder("proposal_weight", |key| build_article_sc(key, is_directed));
359
360    vd.field_script_value_builder("wargoal_score_multiplier", |key| {
361        build_article_sc(key, is_directed)
362    });
363}