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 _ = 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 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 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 validate_datatype_field(Datatype::float, key, bv, data, dc, false);
139 } else if let Some(value) = value.strip_suffix("f") {
140 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 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 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 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 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 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 if value.starts_with("[") {
284 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 }
337 BV::Block(_block) => {
338 }
343 }
344 }
345 GuiValidation::ActionTooltip => {
346 bv.expect_block();
347 }
348 GuiValidation::ComplexProperty => {
349 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 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 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 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
428fn 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 #[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 value.expect_precise_number();
484 }
485 if count != 4 {
486 warn(ErrorKey::Colors).msg("expected 4 color values").loc(block).push();
487 }
488}