tiger_lib/
script_value.rs

1//! Validation of script values, which are values that update dynamically based on rules given in
2//! their math blocks.
3//!
4//! Scriptvalues can also be non-dynamic, in the sense of being just a literal or the name of
5//! another script value.
6
7use crate::block::{BV, Block, BlockItem, Comparator, Eq::*};
8use crate::context::{Reason, ScopeContext, Temporary};
9use crate::everything::Everything;
10use crate::helpers::TriBool;
11use crate::item::Item;
12use crate::lowercase::Lowercase;
13use crate::report::{ErrorKey, Severity, err, tips, untidy, warn};
14use crate::scopes::{Scopes, scope_iterator};
15use crate::token::Token;
16use crate::tooltipped::Tooltipped;
17use crate::trigger::{validate_target_ok_this, validate_trigger, validate_trigger_key_bv};
18use crate::validate::{
19    ListType, precheck_iterator_fields, validate_ifelse_sequence, validate_inside_iterator,
20    validate_iterator_fields, validate_scope_chain,
21};
22use crate::validator::Validator;
23
24/// Validate a block that's part of a script value.
25/// * `have_value`: indicates whether this script value has had some sort of value set already.
26///   It's used to emit warnings when overwriting this value, or when assuming there is a value
27///   when there isn't one. Has values `True`, `False`, or `Maybe`.
28/// * `check_desc`: indicates whether localization errors are worth warning about. Some script values
29///   only have their breakdowns displayed while debugging, and those can have raw (non-localized)
30///   text in their descs.
31///
32/// Returns true iff this block did something useful. (Used for warnings)
33fn validate_inner(
34    mut vd: Validator,
35    block: &Block,
36    data: &Everything,
37    sc: &mut ScopeContext,
38    mut have_value: TriBool,
39    check_desc: bool,
40) -> bool {
41    if check_desc {
42        vd.field_item("desc", Item::Localization);
43        vd.field_item("format", Item::Localization);
44    } else {
45        vd.field_value("desc");
46        vd.field_value("format");
47    }
48
49    // Whether this script value has done something useful yet
50    let mut made_changes = false;
51    // Whether the script value's current value has been saved as a scope
52    let mut saved_value = false;
53
54    validate_ifelse_sequence(block, "if", "else_if", "else");
55    vd.set_allow_questionmark_equals(true);
56    vd.unknown_fields_cmp(|token, cmp, bv| {
57        if token.is("save_temporary_scope_as") {
58            // save_temporary_scope_as is now allowed in script values
59            if let Some(name) = bv.expect_value() {
60                sc.save_current_scope(name.as_str(), Temporary::Yes);
61                made_changes = true;
62            }
63        } else if token.is("save_temporary_value_as") {
64            if let Some(name) = bv.expect_value() {
65                sc.define_name_token(name.as_str(), Scopes::Value, name, Temporary::Yes);
66                made_changes = true;
67                saved_value = true;
68            }
69        } else if token.is("value") {
70            if have_value == TriBool::True && !saved_value {
71                let msg = "setting value here will overwrite the previous calculations";
72                warn(ErrorKey::Logic).msg(msg).loc(token).push();
73            }
74            have_value = TriBool::True;
75            saved_value = false;
76            validate_bv(bv, data, sc, check_desc);
77            made_changes = true;
78        } else if token.is("add") || token.is("subtract") || token.is("min") || token.is("max") {
79            have_value = TriBool::True;
80            saved_value = false;
81            validate_bv(bv, data, sc, check_desc);
82            made_changes = true;
83        } else if token.is("multiply")
84            || token.is("divide")
85            || token.is("modulo")
86            || token.is("round_to")
87        {
88            if have_value == TriBool::False {
89                let msg = format!("nothing to {token} yet");
90                warn(ErrorKey::Logic).msg(msg).loc(token).push();
91            }
92            validate_bv(bv, data, sc, check_desc);
93            made_changes = true;
94            saved_value = false;
95        } else if token.is("round") || token.is("ceiling") || token.is("floor") || token.is("abs") {
96            if have_value == TriBool::False {
97                let msg = format!("nothing to {token} yet");
98                warn(ErrorKey::Logic).msg(msg).loc(token).push();
99            }
100            if let Some(value) = bv.expect_value() {
101                #[cfg(not(feature = "imperator"))]
102                if !value.is("yes") && !value.is("no") {
103                    let msg = "expected yes or no";
104                    warn(ErrorKey::Validation).msg(msg).loc(value).push();
105                }
106                #[cfg(feature = "imperator")]
107                if !token.is("round") && !value.is("yes") && !value.is("no") {
108                    let msg = "expected yes or no";
109                    warn(ErrorKey::Validation).msg(msg).loc(value).push();
110                }
111                #[cfg(feature = "imperator")]
112                if token.is("round")
113                    && !&["yes", "no", "floor", "ceiling"].iter().any(|&v| value.is(v))
114                {
115                    // imperator allows "round = <yes/no/floor/ceiling>"
116                    let msg = "expected yes, no, floor, or ceiling";
117                    warn(ErrorKey::Validation).msg(msg).loc(value).push();
118                }
119                made_changes = true;
120                saved_value = false;
121            }
122        } else if token.is("fixed_range") || token.is("integer_range") {
123            if have_value == TriBool::True {
124                let msg = "using fixed_range here will overwrite the previous calculations";
125                warn(ErrorKey::Logic).msg(msg).loc(token).push();
126            }
127            if let Some(block) = bv.expect_block() {
128                validate_minmax_range(block, data, sc, check_desc);
129                made_changes = true;
130                saved_value = false;
131            }
132            have_value = TriBool::True;
133        } else if token.is("if") || token.is("else_if") {
134            if let Some(block) = bv.expect_block() {
135                validate_if(token, block, data, sc, check_desc);
136                made_changes = true;
137            }
138            have_value = TriBool::Maybe;
139        } else if token.is("else") {
140            if let Some(block) = bv.expect_block() {
141                validate_else(token, block, data, sc, check_desc);
142                made_changes = true;
143            }
144            have_value = TriBool::Maybe;
145        } else if token.is("switch") {
146            if let Some(block) = bv.expect_block() {
147                let mut vd = Validator::new(block, data);
148                vd.req_field("trigger");
149                if let Some(target) = vd.field_value("trigger").cloned() {
150                    vd.set_allow_questionmark_equals(true);
151                    vd.unknown_block_fields(|key, block| {
152                        if !key.is("fallback") {
153                            let synthetic_bv = BV::Value(key.clone());
154                            validate_trigger_key_bv(
155                                &target,
156                                Comparator::Equals(Single),
157                                &synthetic_bv,
158                                data,
159                                sc,
160                                Tooltipped::No,
161                                false,
162                                Severity::Error,
163                            );
164                        }
165                        let vd = Validator::new(block, data);
166                        made_changes |= validate_inner(vd, block, data, sc, have_value, check_desc);
167                    });
168                    have_value = TriBool::Maybe;
169                }
170            }
171        } else {
172            if let Some((it_type, it_name)) = token.split_once('_') {
173                if let Ok(ltype) = ListType::try_from(it_type.as_str()) {
174                    if let Some((inscopes, outscope)) = scope_iterator(&it_name, data, sc) {
175                        if ltype.is_for_triggers() {
176                            let msg = "cannot use `any_` iterators in a script value";
177                            err(ErrorKey::Validation).msg(msg).loc(token).push();
178                        }
179                        sc.expect(inscopes, &Reason::Token(token.clone()), data);
180                        if let Some(block) = bv.expect_block() {
181                            precheck_iterator_fields(ltype, it_name.as_str(), block, data, sc);
182                            sc.open_scope(outscope, token.clone());
183                            validate_iterator(ltype, &it_name, block, data, sc, check_desc);
184                            made_changes = true;
185                            sc.close();
186                            have_value = TriBool::Maybe;
187                        }
188                    }
189                    return;
190                }
191            }
192
193            // Check for target = { script_value }
194            sc.open_builder();
195            if validate_scope_chain(token, data, sc, matches!(cmp, Comparator::Equals(Question))) {
196                if let Some(block) = bv.expect_block() {
197                    sc.finalize_builder();
198                    let vd = Validator::new(block, data);
199                    made_changes |= validate_inner(vd, block, data, sc, have_value, check_desc);
200                    have_value = TriBool::Maybe;
201                }
202            }
203            sc.close();
204        }
205    });
206    made_changes
207}
208
209/// Validate a block inside an iterator that's part of a script value.
210/// Checks some fields and then relies on `validate_inner` for the heavy lifting.
211fn validate_iterator(
212    ltype: ListType,
213    it_name: &Token,
214    block: &Block,
215    data: &Everything,
216    sc: &mut ScopeContext,
217    check_desc: bool,
218) {
219    let mut side_effects = false;
220    let mut vd = Validator::new(block, data);
221    vd.field_validated_block("limit", |block, data| {
222        side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
223    });
224
225    let mut tooltipped = Tooltipped::No;
226    validate_iterator_fields(Lowercase::empty(), ltype, data, sc, &mut vd, &mut tooltipped, true);
227
228    validate_inside_iterator(
229        &Lowercase::new(it_name.as_str()),
230        ltype,
231        block,
232        data,
233        sc,
234        &mut vd,
235        Tooltipped::No,
236    );
237
238    side_effects |= validate_inner(vd, block, data, sc, TriBool::Maybe, check_desc);
239    if !side_effects {
240        let msg = "this iterator does not change the script value";
241        let info = "it should be either removed, or changed to do something useful";
242        err(ErrorKey::Logic).msg(msg).info(info).loc(block).push();
243    }
244}
245
246/// Validate one of the `fixed_range` or `integer_range` script value operators.
247/// These are rarely used.
248fn validate_minmax_range(
249    block: &Block,
250    data: &Everything,
251    sc: &mut ScopeContext,
252    check_desc: bool,
253) {
254    let mut vd = Validator::new(block, data);
255    vd.req_field("min");
256    vd.req_field("max");
257    vd.multi_field_validated("min", |bv, data| {
258        validate_bv(bv, data, sc, check_desc);
259    });
260    vd.multi_field_validated("max", |bv, data| {
261        validate_bv(bv, data, sc, check_desc);
262    });
263}
264
265/// Validate `if` or `else_if` blocks that are part of a script value.
266/// Checks the `limit` field and then relies on `validate_inner` for the heavy lifting.
267fn validate_if(
268    key: &Token,
269    block: &Block,
270    data: &Everything,
271    sc: &mut ScopeContext,
272    check_desc: bool,
273) {
274    let mut side_effects = false;
275    let mut vd = Validator::new(block, data);
276    vd.req_field_warn("limit");
277    vd.field_validated_block("limit", |block, data| {
278        side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
279    });
280    side_effects |= validate_inner(vd, block, data, sc, TriBool::Maybe, check_desc);
281
282    if !side_effects {
283        let msg = format!("this `{key}` does not change the script value");
284        // weak because in an if-if_else sequence you might want some that deliberately do nothing
285        // TODO: make this smarter so that it does not warn if followed by an else or else_if
286        err(ErrorKey::Logic).weak().msg(msg).loc(key).push();
287    }
288}
289
290/// Validate `else` blocks that are part of a script value.
291/// Just like `validate_if`, but warns if it encounters a `limit` field.
292fn validate_else(
293    key: &Token,
294    block: &Block,
295    data: &Everything,
296    sc: &mut ScopeContext,
297    check_desc: bool,
298) {
299    let mut side_effects = false;
300    let mut vd = Validator::new(block, data);
301    vd.field_validated_key_block("limit", |key, block, data| {
302        let msg = "`else` with a `limit` does work, but may indicate a mistake";
303        let info = "normally you would use `else_if` instead.";
304        tips(ErrorKey::IfElse).msg(msg).info(info).loc(key).push();
305        side_effects |= validate_trigger(block, data, sc, Tooltipped::No);
306    });
307    side_effects |= validate_inner(vd, block, data, sc, TriBool::Maybe, check_desc);
308
309    if !side_effects {
310        let msg = format!("this `{key}` does not change the script value");
311        let info = "it should be either removed, or changed to do something useful";
312        // only untidy because an empty else is probably not a logic error
313        untidy(ErrorKey::Logic).msg(msg).info(info).loc(key).push();
314    }
315}
316
317/// Validate a script value. It can be a block or a value.
318/// As a value, it may be an integer or boolean literal, or a target scope sequence, or a named script value.
319/// As a block, it may be a { min max } range, or a calculation block which is validated with `validate_inner`.
320/// (Boolean script values are rare but valid. They can't have calculation blocks.)
321pub(crate) fn validate_bv(bv: &BV, data: &Everything, sc: &mut ScopeContext, check_desc: bool) {
322    // Using validate_target_ok_this here because when chaining script values to each other, you often do `value = this`
323    match bv {
324        BV::Value(t) => {
325            validate_target_ok_this(t, data, sc, Scopes::Value | Scopes::Bool);
326        }
327        BV::Block(b) => {
328            let mut vd = Validator::new(b, data);
329            if matches!(b.iter_items().next(), Some(BlockItem::Block(_) | BlockItem::Value(_))) {
330                // It's a range like { 1 5 }
331                let vec = vd.values();
332                if vec.len() == 2 {
333                    for v in vec {
334                        validate_target_ok_this(v, data, sc, Scopes::Value | Scopes::Bool);
335                    }
336                } else {
337                    warn(ErrorKey::Validation).msg("invalid script value range").loc(b).push();
338                }
339            } else {
340                validate_inner(vd, b, data, sc, TriBool::False, check_desc);
341            }
342        }
343    }
344}
345
346pub fn validate_script_value(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
347    validate_bv(bv, data, sc, true);
348}
349
350#[allow(dead_code)]
351pub fn validate_script_value_no_breakdown(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
352    validate_bv(bv, data, sc, false);
353}
354
355/// Validate a script value that's not allowed to do calculations. It must be a literal or the name of another script value
356/// that's also not allowed to do calculations.
357pub fn validate_non_dynamic_script_value(bv: &BV, data: &Everything) {
358    if let Some(token) = bv.get_value() {
359        if token.is_number() || token.is("yes") || token.is("no") {
360            return;
361        }
362        if data.script_values.exists(token.as_str()) {
363            data.script_values.validate_non_dynamic_call(token, data);
364            return;
365        }
366    }
367    let msg = "dynamic script values are not allowed here";
368    let info = "only literal numbers or the name of a simple script value";
369    err(ErrorKey::Validation).msg(msg).info(info).loc(bv).push();
370}