tiger_lib/gui/
validate.rs

1use crate::block::{BV, Block};
2use crate::context::ScopeContext;
3use crate::data::localization::LocaValue;
4use crate::datacontext::DataContext;
5use crate::datatype::{CodeArg, Datatype, validate_datatypes};
6use crate::everything::Everything;
7#[cfg(feature = "ck3")]
8use crate::game::Game;
9use crate::game::GameFlags;
10use crate::gui::properties::{ALIGN, BLENDMODES};
11use crate::gui::{GuiCategories, GuiValidation, PropertyContainer, WidgetProperty};
12use crate::helpers::stringify_choices;
13use crate::item::Item;
14use crate::parse::localization::ValueParser;
15use crate::report::{ErrorKey, Severity, err, warn};
16use crate::scopes::Scopes;
17use crate::token::Token;
18use crate::validator::Validator;
19
20pub fn validate_property(
21    property: WidgetProperty,
22    container: Option<PropertyContainer>,
23    key: &Token,
24    bv: &BV,
25    data: &Everything,
26    dc: &mut DataContext,
27) {
28    let game = GameFlags::game();
29    let gameflags = property.to_game_flags();
30    if !gameflags.contains(game) {
31        if property == WidgetProperty::tooltip_enabled {
32            let msg = "tooltip_enabled has been renamed to tooltip_visible";
33            err(ErrorKey::Removed).msg(msg).loc(key).push();
34        } else {
35            let msg = format!("{key} is only for {gameflags}");
36            err(ErrorKey::WrongGame).weak().msg(msg).loc(key).push();
37        }
38        return;
39    }
40    if let Some(container) = container {
41        let allowed_properties = match container {
42            PropertyContainer::BuiltinWidget(builtin) => {
43                GuiCategories::widget_as_container(builtin)
44            }
45            PropertyContainer::ComplexProperty(prop) | PropertyContainer::WidgetProperty(prop) => {
46                GuiCategories::property_as_container(prop)
47            }
48        };
49        let allowed_containers = GuiCategories::property_in_container(property);
50        if !allowed_containers.intersects(allowed_properties) {
51            let msg = format!("property {property} is not allowed in {container}");
52            err(ErrorKey::Gui).weak().msg(msg).loc(key).push();
53        }
54    }
55    match GuiValidation::from_property(property) {
56        GuiValidation::UncheckedValue | GuiValidation::Format => {
57            // TODO: validate Format as a format string
58            _ = bv.expect_value();
59        }
60        GuiValidation::DatatypeExpr | GuiValidation::Datamodel => {
61            validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
62        }
63        GuiValidation::Datacontext => {
64            validate_datatype_field(Datatype::Unknown, key, bv, data, dc, true);
65        }
66        GuiValidation::Boolean => {
67            if let Some(value) = bv.expect_value() {
68                if value.starts_with("[") {
69                    validate_datatype_field(Datatype::bool, key, bv, data, dc, false);
70                } else if !value.lowercase_is("yes") && !value.lowercase_is("no") {
71                    // TODO: decide based on the field name whether to upgrade to error?
72                    warn(ErrorKey::Validation).msg("expected yes or no").loc(value).push();
73                }
74            }
75        }
76        GuiValidation::Yes => {
77            if let Some(value) = bv.expect_value() {
78                if !value.is("yes") {
79                    warn(ErrorKey::Validation).msg("expected only yes").loc(value).push();
80                }
81            }
82        }
83        GuiValidation::Align => {
84            if let Some(value) = bv.expect_value() {
85                for part in value.split('|') {
86                    if !ALIGN.contains(&part.as_str()) {
87                        let msg = format!("unknown {key} {part}");
88                        let info = format!("known {key}s are {}", stringify_choices(ALIGN));
89                        warn(ErrorKey::Choice).msg(msg).info(info).loc(part).push();
90                    }
91                }
92            }
93        }
94        GuiValidation::Integer => {
95            if let Some(value) = bv.expect_value() {
96                if value.starts_with("[") {
97                    validate_datatype_field(Datatype::int32, key, bv, data, dc, false);
98                } else {
99                    value.expect_integer();
100                }
101            }
102        }
103        GuiValidation::UnsignedInteger => {
104            if let Some(value) = bv.expect_value() {
105                if value.starts_with("[") {
106                    validate_datatype_field(Datatype::uint32, key, bv, data, dc, false);
107                } else if let Some(i) = value.expect_integer() {
108                    if i < 0 {
109                        let msg = format!("{key} needs an unsigned integer");
110                        warn(ErrorKey::Range).msg(msg).loc(value).push();
111                    }
112                }
113            }
114        }
115        GuiValidation::Number => {
116            if let Some(value) = bv.expect_value() {
117                if value.starts_with("[") {
118                    validate_datatype_field(Datatype::float, key, bv, data, dc, false);
119                } else {
120                    value.expect_number();
121                }
122            }
123        }
124        GuiValidation::NumberOrInt32 => {
125            if let Some(value) = bv.expect_value() {
126                if value.starts_with("[") {
127                    // TODO: need a way to express it can be int32 or float
128                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
129                } else {
130                    value.expect_number();
131                }
132            }
133        }
134        GuiValidation::NumberF => {
135            if let Some(value) = bv.expect_value() {
136                if value.starts_with("[") {
137                    // TODO: need a way to express it can be int32 or float
138                    validate_datatype_field(Datatype::float, key, bv, data, dc, false);
139                } else if let Some(value) = value.strip_suffix("f") {
140                    // TODO: this f is used in vanilla; check it really works.
141                    value.expect_number();
142                } else {
143                    value.expect_number();
144                }
145            }
146        }
147        GuiValidation::NumberOrPercent => {
148            if let Some(value) = bv.expect_value() {
149                if value.starts_with("[") {
150                    // TODO: need a way to express it can be int32 or float
151                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
152                } else if let Some(value) = value.strip_suffix("%") {
153                    value.expect_number();
154                } else {
155                    value.expect_number();
156                }
157            }
158        }
159        GuiValidation::TwoNumberOrPercent => match bv {
160            BV::Value(_) => {
161                validate_datatype_field(Datatype::CVector2f, key, bv, data, dc, false);
162            }
163            BV::Block(block) => {
164                for value in block.iter_values_warn() {
165                    if let Some(value) = value.strip_suffix("%") {
166                        value.expect_number();
167                    } else {
168                        value.expect_number();
169                    }
170                }
171            }
172        },
173        GuiValidation::Numeric => {
174            if let Some(value) = bv.expect_value() {
175                if value.starts_with("[") {
176                    // TODO: need a way to express which datatypes it can be
177                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
178                } else {
179                    value.expect_number();
180                }
181            }
182        }
183        GuiValidation::CVector2f => match bv {
184            BV::Value(_) => {
185                validate_datatype_field(Datatype::CVector2f, key, bv, data, dc, false);
186            }
187            BV::Block(block) => {
188                let mut vd = Validator::new(block, data);
189                vd.set_max_severity(Severity::Warning);
190                vd.req_tokens_numbers_exactly(2);
191            }
192        },
193        GuiValidation::CVector2i => match bv {
194            BV::Value(_) => {
195                validate_datatype_field(Datatype::CVector2i, key, bv, data, dc, false);
196            }
197            BV::Block(block) => {
198                let mut vd = Validator::new(block, data);
199                vd.set_max_severity(Severity::Warning);
200                vd.req_tokens_integers_exactly(2);
201            }
202        },
203        GuiValidation::CVector3f => match bv {
204            BV::Value(_) => {
205                validate_datatype_field(Datatype::CVector3f, key, bv, data, dc, false);
206            }
207            BV::Block(block) => {
208                let mut vd = Validator::new(block, data);
209                vd.set_max_severity(Severity::Warning);
210                vd.req_tokens_numbers_exactly(3);
211            }
212        },
213        GuiValidation::CVector4f => match bv {
214            BV::Value(_) => {
215                validate_datatype_field(Datatype::CVector4f, key, bv, data, dc, false);
216            }
217            BV::Block(block) => {
218                let mut vd = Validator::new(block, data);
219                vd.set_max_severity(Severity::Warning);
220                vd.req_tokens_numbers_exactly(4);
221            }
222        },
223        GuiValidation::PointsList => match bv {
224            BV::Value(_) => {
225                validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
226            }
227            BV::Block(block) => {
228                let mut vd = Validator::new(block, data);
229                vd.set_max_severity(Severity::Warning);
230                for block in vd.blocks() {
231                    let mut vd = Validator::new(block, data);
232                    vd.set_max_severity(Severity::Warning);
233                    vd.req_tokens_numbers_exactly(2);
234                }
235            }
236        },
237        GuiValidation::Color => match bv {
238            BV::Value(_) => {
239                // TODO: can be CVector4f or CString
240                validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
241            }
242            BV::Block(block) => {
243                validate_gui_color(block, data);
244            }
245        },
246        GuiValidation::CString => {
247            validate_datatype_field(Datatype::CString, key, bv, data, dc, false);
248        }
249        GuiValidation::Item(itype) => {
250            if let Some(value) = bv.expect_value() {
251                if value.starts_with("[") {
252                    // TODO: need some way of specifying "stringable" datatypes
253                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
254                } else {
255                    data.verify_exists(itype, value);
256                }
257            }
258        }
259        GuiValidation::ItemOrBlank(itype) => {
260            if let Some(value) = bv.expect_value() {
261                if value.starts_with("[") {
262                    // TODO: need some way of specifying "stringable" datatypes
263                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
264                } else if !value.is("") {
265                    data.verify_exists(itype, value);
266                }
267            }
268        }
269        GuiValidation::Blendmode => {
270            if let Some(value) = bv.expect_value() {
271                let value_lc = value.as_str().to_ascii_lowercase();
272                if !BLENDMODES.contains(&&*value_lc) {
273                    let msg = "unknown blendmode";
274                    let info = format!("expected one of {}", stringify_choices(BLENDMODES));
275                    warn(ErrorKey::Choice).msg(msg).info(info).loc(value).push();
276                }
277            }
278        }
279        GuiValidation::MouseButton(choices) => {
280            if let Some(value) = bv.expect_value() {
281                // TODO: datatype is only really used by button_ignore.
282                // Is it valid for the others?
283                if value.starts_with("[") {
284                    // TODO: need some way of specifying "stringable" datatypes
285                    validate_datatype_field(Datatype::Unknown, key, bv, data, dc, false);
286                } else {
287                    let value_lc = value.as_str().to_ascii_lowercase();
288                    if !choices.contains(&&*value_lc) {
289                        let msg = "unknown mouse button";
290                        let info = format!("expected one of {}", stringify_choices(choices));
291                        warn(ErrorKey::Choice).msg(msg).info(info).loc(value).push();
292                    }
293                }
294            }
295        }
296        GuiValidation::MouseButtonSet(choices) => {
297            if let Some(value) = bv.expect_value() {
298                for part in value.split('|') {
299                    let part_lc = part.as_str().to_ascii_lowercase();
300                    if !choices.contains(&&*part_lc) {
301                        let msg = "unknown mouse button";
302                        let info = format!("expected one of {}", stringify_choices(choices));
303                        warn(ErrorKey::Choice).msg(msg).info(info).loc(value).push();
304                    }
305                }
306            }
307        }
308        GuiValidation::Choice(choices) => {
309            if let Some(value) = bv.expect_value() {
310                let value_lc = value.as_str().to_ascii_lowercase();
311                if !choices.contains(&&*value_lc) {
312                    let msg = "unknown value";
313                    let info = format!("expected one of {}", stringify_choices(choices));
314                    warn(ErrorKey::Choice).msg(msg).info(info).loc(value).push();
315                }
316            }
317        }
318        GuiValidation::ChoiceSet(choices) => {
319            if let Some(value) = bv.expect_value() {
320                for part in value.split('|') {
321                    let part_lc = part.as_str().to_ascii_lowercase();
322                    if !choices.contains(&&*part_lc) {
323                        let msg = "unknown value";
324                        let info = format!("expected one of {}", stringify_choices(choices));
325                        warn(ErrorKey::Choice).msg(msg).info(info).loc(value).push();
326                    }
327                }
328            }
329        }
330        GuiValidation::Widget => {
331            match bv {
332                BV::Value(value) => {
333                    data.verify_exists(Item::GuiTemplate, value);
334                    // Templates are validated separately, and this Widget field adds no context to that.
335                    // TODO: verify that this is a template containing one widget.
336                }
337                BV::Block(_block) => {
338                    // TODO: verify that this block contains only one widget (though it may also
339                    // have a recursive = yes field).
340                    // TODO: if this block is not recursive, validate it now.
341                    // TODO: perform blockoverrides on this block.
342                }
343            }
344        }
345        GuiValidation::ActionTooltip => {
346            bv.expect_block();
347        }
348        GuiValidation::ComplexProperty => {
349            // Complex properties have their own item type, where they get a GuiBlock input
350            // rather than a BV.
351            unreachable!();
352        }
353        GuiValidation::FormatOverride => {
354            if let Some(block) = bv.expect_block() {
355                let mut count = 0;
356                for value in block.iter_values_warn() {
357                    count += 1;
358                    data.verify_exists(Item::TextFormat, value);
359                    if count == 3 {
360                        let msg = "expected exactly 2 text formats";
361                        warn(ErrorKey::Validation).msg(msg).loc(value).push();
362                    }
363                }
364            }
365        }
366        GuiValidation::RawText | GuiValidation::Text => {
367            if let Some(text) = bv.expect_value() {
368                let value = ValueParser::new(vec![text]).parse();
369                validate_gui_loca(key, value, data);
370                if !text.starts_with("[") && !text.as_str().contains(' ') {
371                    // even raw text can still be a localization key sometimes
372                    data.mark_used(Item::Localization, text.as_str());
373                }
374            }
375        }
376    }
377}
378
379pub fn validate_datatype_field(
380    dtype: Datatype,
381    key: &Token,
382    bv: &BV,
383    data: &Everything,
384    dc: &mut DataContext,
385    allow_promote: bool,
386) {
387    if let Some(value) = bv.expect_value() {
388        if value.starts_with("[") {
389            let loca_value = ValueParser::new(vec![value]).parse();
390            let mut sc = ScopeContext::new(Scopes::None, key);
391            match loca_value {
392                // TODO: validate format
393                LocaValue::Code(chain, format) => {
394                    if key.is("datacontext") && chain.codes.len() == 1 {
395                        if let Some(code) = chain.codes.first() {
396                            if code.name.is("GetScriptedGui") {
397                                // Get the name from GetScriptedGui('name')
398                                if let Some(CodeArg::Literal(name)) = code.arguments.first() {
399                                    dc.set_sgui_name(name.clone());
400                                }
401                            }
402                        }
403                    }
404                    validate_datatypes(
405                        &chain,
406                        data,
407                        &mut sc,
408                        dc,
409                        dtype,
410                        None,
411                        format.as_ref(),
412                        allow_promote,
413                    );
414                }
415                LocaValue::Error => (),
416                _ => {
417                    let msg = "expected whole field to be a [ ] expression";
418                    warn(ErrorKey::Validation).msg(msg).loc(value).push();
419                }
420            }
421        } else {
422            let msg = "expected a [ ] expression here";
423            warn(ErrorKey::Validation).msg(msg).loc(value).push();
424        }
425    }
426}
427
428// TODO: can this be merged with check_loca code in localization?
429fn validate_gui_loca(key: &Token, loca_value: LocaValue, data: &Everything) {
430    match loca_value {
431        LocaValue::Concat(v) => {
432            for loca_value in v {
433                validate_gui_loca(key, loca_value, data);
434            }
435        }
436        LocaValue::Code(chain, format) => {
437            // |E is the formatting used for game concepts in ck3
438            #[cfg(feature = "ck3")]
439            if Game::is_ck3() {
440                if let Some(ref format) = format {
441                    if format.as_str().contains('E') || format.as_str().contains('e') {
442                        if let Some(concept) = chain.as_gameconcept() {
443                            data.verify_exists(Item::GameConcept, concept);
444                            return;
445                        }
446                    }
447                }
448            }
449
450            let mut sc = ScopeContext::new(Scopes::None, key);
451            validate_datatypes(
452                &chain,
453                data,
454                &mut sc,
455                &DataContext::new(),
456                Datatype::Unknown,
457                None,
458                format.as_ref(),
459                false,
460            );
461        }
462        LocaValue::Icon(token) => {
463            data.verify_exists(Item::TextIcon, &token);
464        }
465        LocaValue::Flag(token) => {
466            #[cfg(feature = "hoi4")]
467            data.verify_exists(Item::CountryTag, &token);
468            let pathname = format!("gfx/flags/{token}.tga");
469            data.verify_exists_implied(Item::File, &pathname, &token);
470        }
471        _ => (),
472    }
473}
474
475fn validate_gui_color(block: &Block, data: &Everything) {
476    let mut vd = Validator::new(block, data);
477    let mut count = 0;
478    for value in vd.values() {
479        count += 1;
480        // TODO: verify whether gui really does support precise numbers.
481        // They're used in a few places by vanilla but that doesn't mean it works...
482        // TODO: check ranges
483        value.expect_precise_number();
484    }
485    if count != 4 {
486        warn(ErrorKey::Colors).msg("expected 4 color values").loc(block).push();
487    }
488}