tiger_lib/
desc.rs

1//! Validator for triggered description blocks that compose a description from multiple
2//! localization keys.
3//!
4//! Such desc blocks are accepted in many places in the game script.
5//!
6//! The main entry point is [`validate_desc`].
7
8use crate::block::{BV, Block, Comparator, Eq::Single};
9use crate::context::ScopeContext;
10use crate::everything::Everything;
11use crate::item::Item;
12use crate::report::{ErrorKey, Severity, warn};
13use crate::token::Token;
14use crate::tooltipped::Tooltipped;
15use crate::trigger::{validate_trigger, validate_trigger_key_bv};
16use crate::validator::Validator;
17
18/// Internal function to recurse over the complex description block logic.
19///
20/// `caller` is the name of the key that opened this block.
21/// `block` is the block or sub-block being validated.
22/// `sc` is the scope in which to evaluate any triggers found.
23/// `f` is the closure to run over any strings found.
24fn validate_desc_map_block(
25    caller: &str,
26    block: &Block,
27    data: &Everything,
28    sc: &mut ScopeContext,
29    f: &impl Fn(&Token, &Everything, &mut ScopeContext),
30) {
31    let mut vd = Validator::new(block, data);
32    let mut seen_desc = false;
33    let mut seen_unconditional_desc = false;
34    vd.unknown_fields(|key, bv| {
35        if key.is("desc") || key.is("first_valid") || key.is("random_valid") {
36            if seen_desc && caller == "triggered_desc" {
37                let msg = "multiple descs in one triggered_desc";
38                let info = "only the last one will be shown";
39                warn(ErrorKey::DuplicateField).msg(msg).info(info).loc(key).push();
40            }
41            if seen_unconditional_desc && caller == "first_valid" {
42                let msg = "multiple unconditional desc in one first_valid";
43                let info = "only the first one will be shown";
44                warn(ErrorKey::DuplicateField).msg(msg).info(info).loc(key).push();
45            }
46            if key.is("desc") {
47                match bv {
48                    BV::Value(token) => {
49                        if !token.as_str().contains(' ') {
50                            f(token, data, sc);
51                        }
52                    }
53                    BV::Block(block) => {
54                        validate_desc_map_block(key.as_str(), block, data, sc, f);
55                    }
56                }
57                // first_valid and random_valid are not unconditional because all their triggers might fail
58                seen_unconditional_desc = true;
59            } else if let Some(block) = bv.expect_block() {
60                validate_desc_map_block(key.as_str(), block, data, sc, f);
61            }
62            seen_desc = true;
63        } else if key.is("triggered_desc") {
64            if let Some(block) = bv.expect_block() {
65                if seen_desc && caller == "triggered_desc" {
66                    let msg = "multiple descs in one triggered_desc";
67                    let info = "only the last one will be shown";
68                    warn(ErrorKey::DuplicateField).msg(msg).info(info).loc(key).push();
69                }
70                validate_desc_map_block(key.as_str(), block, data, sc, f);
71                seen_desc = true;
72            }
73        } else if key.is("trigger") {
74            if let Some(block) = bv.expect_block() {
75                if caller != "triggered_desc" {
76                    let msg = "`trigger` is only for `triggered_desc";
77                    warn(ErrorKey::Validation).msg(msg).loc(key).push();
78                }
79                validate_trigger(block, data, sc, Tooltipped::No);
80            }
81        } else if key.is("switch") {
82            if let Some(block) = bv.expect_block() {
83                // See also `validate_switch` function which is for effects.
84                let mut vd = Validator::new(block, data);
85                vd.req_field("trigger");
86                if let Some(target) = vd.field_value("trigger").cloned() {
87                    let mut count = 0;
88                    vd.set_allow_questionmark_equals(true);
89                    vd.unknown_block_fields(|key, block| {
90                        count += 1;
91                        if !key.is("fallback") {
92                            let synthetic_bv = BV::Value(key.clone());
93                            validate_trigger_key_bv(
94                                &target,
95                                Comparator::Equals(Single),
96                                &synthetic_bv,
97                                data,
98                                sc,
99                                Tooltipped::No,
100                                false,
101                                Severity::Warning,
102                            );
103                        }
104
105                        validate_desc_map_block("switch", block, data, sc, f);
106                    });
107                    if count == 0 {
108                        let msg = "switch with no branches";
109                        warn(ErrorKey::Logic).msg(msg).loc(key).push();
110                    }
111                }
112            }
113        } else {
114            warn(ErrorKey::UnknownField).msg("unexpected key in description").loc(key).push();
115        }
116    });
117}
118
119/// Like [`validate_desc`], but allows the caller to decide what to do with the strings found in
120/// the description. This is useful for example for description blocks that resolve an icon name
121/// rather than a description.
122pub fn validate_desc_map<F: Fn(&Token, &Everything, &mut ScopeContext)>(
123    bv: &BV,
124    data: &Everything,
125    sc: &mut ScopeContext,
126    f: F,
127) {
128    match bv {
129        BV::Value(t) => {
130            if !t.as_str().contains(' ') {
131                f(t, data, sc);
132            }
133        }
134        BV::Block(b) => {
135            validate_desc_map_block("", b, data, sc, &f);
136        }
137    }
138}
139
140/// Validate a complex description, which may be a simple localization key or a block containing
141/// items like `triggered_desc` or `first_valid`.
142pub fn validate_desc(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
143    validate_desc_map(bv, data, sc, |token, data, sc| {
144        data.verify_exists(Item::Localization, token);
145        data.validate_localization_sc(token.as_str(), sc);
146    });
147}