tiger_lib/
effect_validation.rs

1//! Validators for effects that are generic across multiple games.
2
3use crate::block::{BV, Block, Comparator, Eq::Single};
4use crate::context::{ScopeContext, Temporary};
5use crate::desc::validate_desc;
6use crate::effect::{validate_effect, validate_effect_control};
7use crate::everything::Everything;
8#[cfg(feature = "jomini")]
9use crate::game::Game;
10use crate::item::Item;
11use crate::lowercase::Lowercase;
12use crate::report::{ErrorKey, Severity, err, warn};
13use crate::scopes::Scopes;
14#[cfg(feature = "jomini")]
15use crate::script_value::validate_script_value;
16use crate::special_tokens::SpecialTokens;
17use crate::token::Token;
18use crate::tooltipped::Tooltipped;
19use crate::trigger::{validate_target_ok_this, validate_trigger_key_bv};
20use crate::validate::{validate_identifier, validate_optional_duration};
21use crate::validator::{Validator, ValueValidator};
22
23#[allow(dead_code)]
24#[cfg(feature = "imperator")]
25pub fn validate_add_to_list_imperator(
26    key: &Token,
27    mut vd: ValueValidator,
28    sc: &mut ScopeContext,
29    _tooltipped: Tooltipped,
30) {
31    let temp = if key.as_str().contains("_temporary_") { Temporary::Yes } else { Temporary::No };
32    vd.identifier("list name");
33    sc.define_or_expect_list_this(vd.value(), vd.data(), temp);
34    vd.accept();
35}
36
37#[allow(dead_code)]
38#[cfg(any(feature = "ck3", feature = "vic3"))]
39pub fn validate_add_to_list(
40    key: &Token,
41    bv: &BV,
42    data: &Everything,
43    sc: &mut ScopeContext,
44    _tooltipped: Tooltipped,
45) {
46    let temp = if key.as_str().contains("_temporary_") { Temporary::Yes } else { Temporary::No };
47    match bv {
48        BV::Value(name) => {
49            validate_identifier(name, "list name", Severity::Error);
50            sc.define_or_expect_list_this(name, data, temp);
51        }
52        BV::Block(block) => {
53            let mut vd = Validator::new(block, data);
54            vd.req_field("name");
55            vd.req_field("value");
56            if let Some(target) = vd.field_value("value").cloned() {
57                if let Some(name) = vd.field_value("name") {
58                    validate_identifier(name, "list name", Severity::Error);
59                    let target_scopes = sc.local_list_scopes(name.as_str(), data);
60                    let outscopes = validate_target_ok_this(&target, data, sc, target_scopes);
61                    sc.define_or_expect_list(name, outscopes, data, temp);
62                }
63            }
64        }
65    }
66}
67
68/// A specific validator for the three `add_to_variable_list` effects (`global`, `local`, and default).
69/// It is also used for the three `remove_list_variable` effects.
70#[cfg(feature = "jomini")]
71pub fn validate_add_to_variable_list(
72    key: &Token,
73    _block: &Block,
74    data: &Everything,
75    sc: &mut ScopeContext,
76    mut vd: Validator,
77    _tooltipped: Tooltipped,
78) {
79    vd.req_field("name");
80    vd.req_field("target");
81    if let Some(target) = vd.field_value("target").cloned() {
82        if let Some(name) = vd.field_value("name") {
83            validate_identifier(name, "list name", Severity::Error);
84            // It would be better if the step from scopes() to expect() could be done atomically for
85            // the global and normal variables, but unfortunately that could lead to deadlocks if
86            // the target itself refers to other variables.
87            if key.as_str().contains("_local_") {
88                let target_scopes = sc.local_list_scopes(name.as_str(), data);
89                let outscopes = validate_target_ok_this(&target, data, sc, target_scopes);
90                sc.define_or_expect_local_list(name, outscopes, data);
91            } else if key.as_str().contains("_global_") {
92                let target_scopes = data.global_list_scopes.scopes(name.as_str());
93                let outscopes = validate_target_ok_this(&target, data, sc, target_scopes);
94                data.global_list_scopes.expect(name.as_str(), name, outscopes);
95            } else {
96                let target_scopes = data.variable_list_scopes.scopes(name.as_str());
97                let outscopes = validate_target_ok_this(&target, data, sc, target_scopes);
98                data.variable_list_scopes.expect(name.as_str(), name, outscopes);
99            }
100        }
101    }
102    if key.starts_with("add_") && (Game::is_ck3() || Game::is_vic3()) {
103        validate_optional_duration(&mut vd, sc);
104    }
105}
106
107/// A specific validator for the three `change_variable` effects (`global`, `local`, and default).
108#[cfg(feature = "jomini")]
109pub fn validate_change_variable(
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("name");
118    if let Some(name) = vd.field_value("name") {
119        validate_identifier(name, "variable name", Severity::Error);
120        if key.as_str().contains("_local_") {
121            sc.expect_local(name, Scopes::Value, data);
122        } else if key.as_str().contains("_global_") {
123            data.global_scopes.expect(name.as_str(), name, Scopes::Value);
124        } else {
125            data.variable_scopes.expect(name.as_str(), name, Scopes::Value);
126        }
127    }
128    vd.field_script_value("add", sc);
129    vd.field_script_value("subtract", sc);
130    vd.field_script_value("multiply", sc);
131    vd.field_script_value("divide", sc);
132    vd.field_script_value("modulo", sc);
133    vd.field_script_value("min", sc);
134    vd.field_script_value("max", sc);
135}
136
137/// A specific validator for the three `clamp_variable` effects (`global`, `local`, and default).
138#[cfg(feature = "jomini")]
139pub fn validate_clamp_variable(
140    key: &Token,
141    _block: &Block,
142    data: &Everything,
143    sc: &mut ScopeContext,
144    mut vd: Validator,
145    _tooltipped: Tooltipped,
146) {
147    vd.req_field("name");
148    if let Some(name) = vd.field_value("name") {
149        validate_identifier(name, "variable name", Severity::Error);
150        if key.as_str().contains("_local_") {
151            sc.expect_local(name, Scopes::Value, data);
152        } else if key.as_str().contains("_global_") {
153            data.global_scopes.expect(name.as_str(), name, Scopes::Value);
154        } else {
155            data.variable_scopes.expect(name.as_str(), name, Scopes::Value);
156        }
157    }
158    vd.field_script_value("min", sc);
159    vd.field_script_value("max", sc);
160}
161
162/// A specific validator for the `random_list` effect, which has a unique syntax.
163#[cfg(feature = "jomini")]
164pub fn validate_random_list(
165    key: &Token,
166    _block: &Block,
167    data: &Everything,
168    sc: &mut ScopeContext,
169    mut vd: Validator,
170    tooltipped: Tooltipped,
171    special_tokens: &mut SpecialTokens,
172) -> bool {
173    let caller = Lowercase::new(key.as_str());
174    vd.field_integer("pick");
175    vd.field_bool("unique"); // don't know what this does
176    vd.field_validated_sc("desc", sc, validate_desc);
177    let mut has_tooltip = false;
178    vd.unknown_block_fields(|key, block| {
179        if let Some(n) = key.expect_number() {
180            if n < 0.0 {
181                let msg = "negative weights make the whole `random_list` fail";
182                err(ErrorKey::Range).strong().msg(msg).loc(key).push();
183            } else if n > 0.0 && n < 1.0 {
184                let msg = "fractional weights are treated as just 0 in `random_list`";
185                err(ErrorKey::Range).strong().msg(msg).loc(key).push();
186            } else if n.fract() != 0.0 {
187                let msg = "fractions are discarded in `random_list` weights";
188                warn(ErrorKey::Range).strong().msg(msg).loc(key).push();
189            }
190            has_tooltip |=
191                validate_effect_control(&caller, block, data, sc, tooltipped, special_tokens);
192        }
193    });
194    if has_tooltip && key.is("random_list") {
195        special_tokens.insert(key);
196    }
197    has_tooltip
198}
199
200#[cfg(feature = "jomini")]
201pub fn validate_remove_from_list(
202    _key: &Token,
203    mut vd: ValueValidator,
204    sc: &mut ScopeContext,
205    _tooltipped: Tooltipped,
206) {
207    vd.identifier("list name");
208    sc.expect_list(vd.value(), vd.data());
209    vd.accept();
210}
211
212/// A specific validator for the three `round_variable` effects (`global`, `local`, and default).
213#[cfg(feature = "jomini")]
214pub fn validate_round_variable(
215    key: &Token,
216    _block: &Block,
217    data: &Everything,
218    sc: &mut ScopeContext,
219    mut vd: Validator,
220    _tooltipped: Tooltipped,
221) {
222    vd.req_field("name");
223    vd.req_field("nearest");
224    if let Some(name) = vd.field_value("name") {
225        validate_identifier(name, "variable name", Severity::Error);
226        if key.as_str().contains("_local_") {
227            sc.expect_local(name, Scopes::Value, data);
228        } else if key.as_str().contains("_global_") {
229            data.global_scopes.expect(name.as_str(), name, Scopes::Value);
230        } else {
231            data.variable_scopes.expect(name.as_str(), name, Scopes::Value);
232        }
233    }
234    vd.field_script_value("nearest", sc);
235}
236
237#[cfg(feature = "jomini")]
238pub fn validate_save_scope(
239    key: &Token,
240    mut vd: ValueValidator,
241    sc: &mut ScopeContext,
242    _tooltipped: Tooltipped,
243) {
244    let temp = if key.as_str().contains("_temporary_") { Temporary::Yes } else { Temporary::No };
245    vd.identifier("scope name");
246    sc.save_current_scope(vd.value().as_str(), temp);
247    vd.accept();
248}
249
250/// A specific validator for the `save_scope_value` effect.
251#[cfg(feature = "jomini")]
252pub fn validate_save_scope_value(
253    key: &Token,
254    _block: &Block,
255    _data: &Everything,
256    sc: &mut ScopeContext,
257    mut vd: Validator,
258    _tooltipped: Tooltipped,
259) {
260    let temp = if key.as_str().contains("_temporary_") { Temporary::Yes } else { Temporary::No };
261    vd.req_field("name");
262    vd.req_field("value");
263    if let Some(name) = vd.field_identifier_or_flag("name", sc) {
264        // TODO: examine `value` field to check its real scope type
265        sc.define_name_token(name.as_str(), Scopes::primitive(), name, temp);
266    }
267    vd.field_script_value_or_flag("value", sc);
268}
269
270/// A specific validator for the three `set_variable` effects (`global`, `local`, and default).
271#[cfg(feature = "jomini")]
272pub fn validate_set_variable(
273    key: &Token,
274    bv: &BV,
275    data: &Everything,
276    sc: &mut ScopeContext,
277    _tooltipped: Tooltipped,
278) {
279    match bv {
280        BV::Value(token) => {
281            validate_identifier(token, "variable name", Severity::Error);
282            if key.as_str().contains("_local_") {
283                sc.set_local_variable(token, Scopes::Bool);
284            } else if key.as_str().contains("_global_") {
285                data.global_scopes.expect(token.as_str(), token, Scopes::Bool);
286            } else {
287                data.variable_scopes.expect(token.as_str(), token, Scopes::Bool);
288            }
289        }
290        BV::Block(block) => {
291            let mut vd = Validator::new(block, data);
292            vd.set_case_sensitive(false);
293            vd.req_field("name");
294            let name = vd.field_identifier("name", "variable name").cloned();
295            vd.field_validated("value", |bv, data| match bv {
296                BV::Value(token) => {
297                    if let Some(name) = &name {
298                        if key.as_str().contains("_local_") {
299                            let target_scopes = sc.local_variable_scopes(name.as_str(), data);
300                            let outscopes = validate_target_ok_this(token, data, sc, target_scopes);
301                            sc.set_local_variable(name, outscopes);
302                        } else if key.as_str().contains("_global_") {
303                            let target_scopes = data.global_scopes.scopes(name.as_str());
304                            let outscopes = validate_target_ok_this(token, data, sc, target_scopes);
305                            data.global_scopes.expect(name.as_str(), name, outscopes);
306                        } else {
307                            let target_scopes = data.variable_scopes.scopes(name.as_str());
308                            let outscopes = validate_target_ok_this(token, data, sc, target_scopes);
309                            data.variable_scopes.expect(name.as_str(), name, outscopes);
310                        }
311                    }
312                }
313                BV::Block(_) => {
314                    #[cfg(feature = "jomini")]
315                    if Game::is_jomini() {
316                        validate_script_value(bv, data, sc);
317                        if let Some(name) = &name {
318                            if key.as_str().contains("_local_") {
319                                sc.set_local_variable(name, Scopes::Value);
320                            } else if key.as_str().contains("_global_") {
321                                data.global_scopes.expect(name.as_str(), name, Scopes::Value);
322                            } else {
323                                data.variable_scopes.expect(name.as_str(), name, Scopes::Value);
324                            }
325                        }
326                    }
327                    // TODO HOI4
328                }
329            });
330            validate_optional_duration(&mut vd, sc);
331        }
332    }
333}
334
335/// A specific validator for the `switch` effect, which has a unique syntax.
336#[cfg(feature = "jomini")]
337pub fn validate_switch(
338    key: &Token,
339    _block: &Block,
340    data: &Everything,
341    sc: &mut ScopeContext,
342    mut vd: Validator,
343    tooltipped: Tooltipped,
344) {
345    vd.set_case_sensitive(true);
346    vd.req_field("trigger");
347    if let Some(target) = vd.field_value("trigger").cloned() {
348        let mut count = 0;
349        vd.set_allow_questionmark_equals(true);
350        vd.unknown_block_fields(|key, block| {
351            count += 1;
352            if !key.is("fallback") {
353                // Pretend the switch was written as a series of trigger = key lines
354                let synthetic_bv = BV::Value(key.clone());
355                validate_trigger_key_bv(
356                    &target,
357                    Comparator::Equals(Single),
358                    &synthetic_bv,
359                    data,
360                    sc,
361                    tooltipped,
362                    false,
363                    Severity::Error,
364                );
365            }
366
367            validate_effect(block, data, sc, tooltipped);
368        });
369        if count == 0 {
370            let msg = "switch with no branches";
371            err(ErrorKey::Logic).msg(msg).loc(key).push();
372        }
373    }
374}
375
376#[cfg(feature = "jomini")]
377pub fn validate_trigger_event(
378    _key: &Token,
379    bv: &BV,
380    data: &Everything,
381    sc: &mut ScopeContext,
382    _tooltipped: Tooltipped,
383) {
384    match bv {
385        BV::Value(token) => {
386            data.verify_exists(Item::Event, token);
387            data.event_check_scope(token, sc);
388            if let Some(mut event_sc) = sc.root_for_event(token, data) {
389                data.event_validate_call(token, &mut event_sc);
390            }
391        }
392        BV::Block(block) => {
393            let mut vd = Validator::new(block, data);
394            vd.set_case_sensitive(false);
395            vd.field_event("id", sc);
396            vd.field_action("on_action", sc);
397            #[cfg(feature = "ck3")]
398            if Game::is_ck3() {
399                vd.field_target("saved_event_id", sc, Scopes::Flag);
400                vd.field_date("trigger_on_next_date");
401                vd.field_bool("delayed");
402            }
403            #[cfg(feature = "vic3")]
404            if Game::is_vic3() {
405                vd.field_bool("popup");
406            }
407            validate_optional_duration(&mut vd, sc);
408        }
409    }
410}