tiger_lib/data/
gui.rs

1//! Validate files in `gui/`
2
3use std::mem::drop;
4use std::path::PathBuf;
5use std::sync::{Arc, RwLock};
6
7use crate::block::{BV, Block, BlockItem, Field};
8use crate::datacontext::DataContext;
9use crate::everything::Everything;
10use crate::fileset::{FileEntry, FileHandler};
11use crate::game::Game;
12use crate::gui::{BuiltinWidget, GuiBlock, GuiBlockFrom};
13use crate::helpers::{TigerHashMap, TigerHashSet, dup_error};
14use crate::item::Item;
15use crate::lowercase::Lowercase;
16use crate::parse::ParserMemory;
17use crate::pdxfile::PdxFile;
18use crate::report::{ErrorKey, Severity, err, fatal, untidy, warn};
19use crate::token::Token;
20use crate::validator::Validator;
21
22#[derive(Debug, Default)]
23pub struct Gui {
24    files: TigerHashMap<PathBuf, Vec<GuiWidget>>,
25    templates: TigerHashMap<&'static str, GuiTemplate>,
26    // Type keys are stored in lowercase because type lookup is case-insensitive
27    types: TigerHashMap<Lowercase<'static>, GuiType>,
28    layers: TigerHashMap<&'static str, GuiLayer>,
29    // TextIcon is in a Vec because a single icon can have multiple definitions with different
30    // iconsize parameters.
31    texticons: TigerHashMap<&'static str, Vec<TextIcon>>,
32    textformats: TigerHashMap<&'static str, TextFormat>,
33    // This is indexed by a (colorblindmode, textformatname) pair
34    textformats_colorblind: TigerHashMap<(&'static str, &'static str), TextFormat>,
35    widget_names: TigerHashSet<Token>,
36}
37
38impl Gui {
39    #[allow(clippy::collapsible_else_if)] // clippy is suggesting bad style here
40    fn load_widget(&mut self, filename: PathBuf, key: Token, mut block: Block) {
41        if key.is("texticon") {
42            if let Some(icon) = block.get_field_value("icon") {
43                self.load_texticon(icon.clone(), block);
44            } else {
45                warn(ErrorKey::FieldMissing)
46                    .strong()
47                    .msg("texticon without icon field")
48                    .loc(block)
49                    .push();
50            }
51        } else if key.is("textformatting") {
52            let colorblindmode = block.get_field_value("color_blind_mode").cloned();
53            for item in block.drain() {
54                if let BlockItem::Field(Field(key, _, BV::Value(_))) = item {
55                    if !key.is("color_blind_mode") {
56                        let msg = "unknown key in textformatting";
57                        untidy(ErrorKey::UnknownField).msg(msg).loc(key).push();
58                    }
59                } else if let BlockItem::Field(Field(key, _, BV::Block(block))) = item {
60                    if key.is("format") {
61                        if let Some(token) = block.get_field_value("name") {
62                            self.load_textformat(token.clone(), block, colorblindmode.clone());
63                        } else {
64                            warn(ErrorKey::FieldMissing)
65                                .strong()
66                                .msg("format without name field")
67                                .loc(block)
68                                .push();
69                        }
70                    } else {
71                        let msg = "unknown key in textformatting";
72                        untidy(ErrorKey::UnknownField).msg(msg).loc(key).push();
73                    }
74                } else {
75                    let msg = format!("unknown {} in textformatting", item.describe());
76                    untidy(ErrorKey::Validation).msg(msg).loc(item).push();
77                }
78            }
79        } else {
80            if let Some(name) = block.get_field_value("name") {
81                self.widget_names.insert(name.clone());
82            }
83            if let Some(guifile) = self.files.get_mut(&filename) {
84                guifile.push(GuiWidget::new(key, block));
85            } else {
86                self.files.insert(filename, vec![GuiWidget::new(key, block)]);
87            }
88        }
89    }
90
91    fn load_types(&mut self, block: &Block) {
92        #[derive(Copy, Clone)]
93        enum Expecting<'a> {
94            Type,
95            Header,
96            Body(&'a Token, &'a Token),
97        }
98
99        let mut stage = Expecting::Type;
100        for item in block.iter_items() {
101            match stage {
102                Expecting::Type => {
103                    if let Some(field) = item.get_field() {
104                        let msg = format!("unexpected {}", field.describe());
105                        if field.is_eq() {
106                            let info = "did you forget the `type` keyword?";
107                            warn(ErrorKey::ParseError).msg(msg).info(info).loc(field).push();
108                        } else {
109                            warn(ErrorKey::ParseError).msg(msg).loc(field).push();
110                        }
111                    } else if let Some(token) = item.expect_value() {
112                        if token.is("type") || token.is("local_type") {
113                            stage = Expecting::Header;
114                        } else {
115                            let msg = format!("unexpected token `{token}`");
116                            err(ErrorKey::ParseError).msg(msg).loc(token).push();
117                        }
118                    }
119                }
120                Expecting::Header => {
121                    if let Some((key, token)) = item.get_assignment() {
122                        stage = Expecting::Body(key, token);
123                    } else {
124                        err(ErrorKey::ParseError).msg("expected type header").loc(item).push();
125                        stage = Expecting::Type;
126                    }
127                }
128                Expecting::Body(name, base) => {
129                    if let Some(block) = item.expect_block() {
130                        self.load_type(name.clone(), base.clone(), block.clone());
131                    }
132                    stage = Expecting::Type;
133                }
134            }
135        }
136    }
137
138    pub fn load_type(&mut self, key: Token, base: Token, block: Block) {
139        let key_lc = Lowercase::new(key.as_str());
140
141        // With gui types the earlier one takes precedence
142        if let Some(other) = self.types.get(&key_lc) {
143            if other.key.loc.kind <= key.loc.kind {
144                dup_error(&other.key, &key, "gui type");
145            }
146            return;
147        }
148        self.types.insert(key_lc, GuiType::new(key, base, block));
149    }
150
151    pub fn load_template(&mut self, key: Token, block: Block) {
152        // With gui templates the earlier one takes precedence
153        if let Some(other) = self.templates.get(key.as_str()) {
154            if other.key.loc.kind <= key.loc.kind {
155                dup_error(&other.key, &key, "gui template");
156            }
157            return;
158        }
159        self.templates.insert(key.as_str(), GuiTemplate::new(key, block));
160    }
161
162    pub fn load_layer(&mut self, key: Token, block: Block) {
163        if let Some(other) = self.layers.get(key.as_str()) {
164            if other.key.loc.kind >= key.loc.kind {
165                dup_error(&key, &other.key, "gui layer");
166            }
167        }
168        self.layers.insert(key.as_str(), GuiLayer::new(key, block));
169    }
170
171    pub fn load_texticon(&mut self, key: Token, block: Block) {
172        // TODO: warn about exact duplicates? where the iconsize is the same
173        if let Some(vec) = self.texticons.get_mut(key.as_str()) {
174            vec.push(TextIcon::new(key, block));
175        } else {
176            self.texticons.insert(key.as_str(), vec![TextIcon::new(key, block)]);
177        }
178    }
179
180    pub fn load_textformat(&mut self, key: Token, block: Block, color_blind_mode: Option<Token>) {
181        if let Some(cbm) = color_blind_mode {
182            let index = (cbm.as_str(), key.as_str());
183            if let Some(other) = self.textformats_colorblind.get(&index) {
184                if other.key.loc.kind >= key.loc.kind {
185                    let id = format!("textformat for {cbm}");
186                    dup_error(&key, &other.key, &id);
187                }
188            }
189            self.textformats_colorblind.insert(index, TextFormat::new(key, block, Some(cbm)));
190        } else {
191            if let Some(other) = self.textformats.get(key.as_str()) {
192                if other.key.loc.kind >= key.loc.kind {
193                    dup_error(&key, &other.key, "textformat");
194                }
195            }
196            self.textformats.insert(key.as_str(), TextFormat::new(key, block, None));
197        }
198    }
199
200    pub fn template_exists(&self, key: &str) -> bool {
201        self.templates.contains_key(key)
202    }
203
204    pub fn iter_template_keys(&self) -> impl Iterator<Item = &Token> {
205        self.templates.values().map(|item| &item.key)
206    }
207
208    pub fn type_exists(&self, key: &Lowercase) -> bool {
209        self.types.contains_key(key.as_str()) || BuiltinWidget::builtin_current_game(key).is_some()
210    }
211
212    pub fn iter_type_keys(&self) -> impl Iterator<Item = &Token> {
213        self.types.values().map(|item| &item.key)
214    }
215
216    pub fn layer_exists(&self, key: &str) -> bool {
217        self.layers.contains_key(key)
218    }
219
220    pub fn iter_layer_keys(&self) -> impl Iterator<Item = &Token> {
221        self.layers.values().map(|item| &item.key)
222    }
223
224    pub fn texticon_exists(&self, key: &str) -> bool {
225        self.texticons.contains_key(key)
226    }
227
228    pub fn iter_texticon_keys(&self) -> impl Iterator<Item = &Token> {
229        self.texticons.values().flat_map(|v| v.iter().map(|item| &item.key))
230    }
231
232    pub fn textformat_exists(&self, key: &str) -> bool {
233        self.textformats.contains_key(key)
234    }
235
236    pub fn iter_textformat_keys(&self) -> impl Iterator<Item = &Token> {
237        self.textformats.values().map(|item| &item.key)
238    }
239
240    pub fn name_exists(&self, key: &str) -> bool {
241        self.widget_names.contains(key)
242    }
243
244    pub fn iter_names(&self) -> impl Iterator<Item = &Token> {
245        self.widget_names.iter()
246    }
247
248    pub fn validate(&self, data: &Everything) {
249        for items in self.files.values() {
250            for item in items {
251                item.validate(data);
252            }
253        }
254        for item in self.templates.values() {
255            item.validate(data);
256        }
257        for item in self.types.values() {
258            item.validate(data);
259        }
260        for item in self.layers.values() {
261            item.validate(data);
262        }
263        for vec in self.texticons.values() {
264            for item in vec {
265                item.validate(data);
266            }
267        }
268        for item in self.textformats.values() {
269            item.validate(data);
270        }
271        for item in self.textformats_colorblind.values() {
272            item.validate(data);
273        }
274    }
275}
276
277impl FileHandler<Block> for Gui {
278    fn subpath(&self) -> PathBuf {
279        if Game::is_hoi4() { PathBuf::from("interface") } else { PathBuf::from("gui") }
280    }
281
282    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
283        if !entry.filename().to_string_lossy().ends_with(".gui") {
284            return None;
285        }
286
287        PdxFile::read_optional_bom(entry, parser)
288    }
289
290    fn handle_file(&mut self, entry: &FileEntry, mut block: Block) {
291        #[derive(Clone, Debug)]
292        enum Expecting {
293            Widget,
294            Types,
295            TypesBody,
296            Template,
297            TemplateBody(Token),
298            Layer,
299            LayerBody(Token),
300        }
301
302        let mut expecting = Expecting::Widget;
303
304        for item in block.drain() {
305            match expecting {
306                Expecting::Widget => {
307                    if let BlockItem::Field(field) = item {
308                        if let Some((key, token)) = field.get_assignment() {
309                            if key.lowercase_is("template") || key.lowercase_is("local_template") {
310                                expecting = Expecting::TemplateBody(token.clone());
311                            } else {
312                                err(ErrorKey::ParseError)
313                                    .msg("unexpected assignment")
314                                    .loc(key)
315                                    .push();
316                            }
317                        } else if let Some((key, block)) = field.expect_into_definition() {
318                            self.load_widget(PathBuf::from(entry.filename()), key, block);
319                        }
320                    } else if let Some(token) = item.expect_value() {
321                        // TODO: figure out how local the local_template is
322                        if token.lowercase_is("template") || token.lowercase_is("local_template") {
323                            expecting = Expecting::Template;
324                        } else if token.lowercase_is("types") {
325                            expecting = Expecting::Types;
326                        } else if token.lowercase_is("layer") {
327                            expecting = Expecting::Layer;
328                        } else {
329                            let msg = format!("unexpected value `{token}`");
330                            err(ErrorKey::ParseError).msg(msg).loc(token).push();
331                        }
332                    }
333                }
334                Expecting::Types => {
335                    if let BlockItem::Field(field) = item {
336                        let msg = format!("unexpected {}", field.describe());
337                        let info = format!(
338                            "After `Types {}` there shouldn't be an `{}`",
339                            field.key(),
340                            field.cmp()
341                        );
342                        err(ErrorKey::ParseError).msg(msg).info(info).loc(field).push();
343                        expecting = Expecting::Widget;
344                    } else if item.expect_value().is_some() {
345                        expecting = Expecting::TypesBody;
346                    } else {
347                        expecting = Expecting::Widget;
348                    }
349                }
350                Expecting::TypesBody => {
351                    if let Some(block) = item.expect_block() {
352                        self.load_types(block);
353                    }
354                    expecting = Expecting::Widget;
355                }
356                Expecting::Template => {
357                    if let BlockItem::Field(field) = item {
358                        let msg = format!("unexpected {}", field.describe());
359                        let info = format!(
360                            "After `template {}` there shouldn't be an `{}`",
361                            field.key(),
362                            field.cmp()
363                        );
364                        err(ErrorKey::ParseError).msg(msg).info(info).loc(field).push();
365                        expecting = Expecting::Widget;
366                    } else if let Some(token) = item.expect_into_value() {
367                        expecting = Expecting::TemplateBody(token);
368                    } else {
369                        expecting = Expecting::Widget;
370                    }
371                }
372                Expecting::TemplateBody(token) => {
373                    if let Some(block) = item.expect_into_block() {
374                        self.load_template(token.clone(), block);
375                    }
376                    expecting = Expecting::Widget;
377                }
378                Expecting::Layer => {
379                    if let BlockItem::Field(field) = item {
380                        let msg = format!("unexpected {}", field.describe());
381                        let info = format!(
382                            "After `layer {}` there shouldn't be an `{}`",
383                            field.key(),
384                            field.cmp()
385                        );
386                        err(ErrorKey::ParseError).msg(msg).info(info).loc(field).push();
387                        expecting = Expecting::Widget;
388                    } else if let Some(token) = item.expect_into_value() {
389                        expecting = Expecting::LayerBody(token);
390                    } else {
391                        expecting = Expecting::Widget;
392                    }
393                }
394                Expecting::LayerBody(token) => {
395                    if let Some(block) = item.expect_into_block() {
396                        self.load_layer(token.clone(), block);
397                    }
398                    expecting = Expecting::Widget;
399                }
400            }
401        }
402    }
403
404    fn finalize(&mut self) {
405        for item in self.types.values() {
406            _ = item.builtin(&self.types);
407            _ = item.gui_block(&self.types, &self.templates);
408        }
409        for item in self.templates.values() {
410            _ = item.gui_block(&self.types, &self.templates);
411        }
412    }
413}
414
415#[derive(Clone, Debug)]
416struct GuiWidget {
417    key: Token,
418    block: Block,
419}
420
421impl GuiWidget {
422    pub fn new(key: Token, block: Block) -> Self {
423        Self { key, block }
424    }
425
426    pub fn validate(&self, data: &Everything) {
427        data.verify_exists(Item::GuiType, &self.key);
428        let guiblock = GuiBlock::from_block(
429            GuiBlockFrom::WidgetKey(&self.key),
430            &self.block,
431            &data.gui.types,
432            &data.gui.templates,
433        );
434        let mut dc = DataContext::new();
435        guiblock.validate(None, data, &mut dc);
436    }
437}
438
439#[derive(Clone, Debug)]
440struct TextIcon {
441    #[allow(dead_code)]
442    key: Token,
443    block: Block,
444}
445
446impl TextIcon {
447    pub fn new(key: Token, block: Block) -> Self {
448        Self { key, block }
449    }
450
451    pub fn validate(&self, data: &Everything) {
452        let mut vd = Validator::new(&self.block, data);
453        vd.set_max_severity(Severity::Warning);
454        vd.field_value("icon");
455        vd.field_validated_block("iconsize", |block, data| {
456            let mut vd = Validator::new(block, data);
457            vd.set_max_severity(Severity::Warning);
458            vd.field_item("texture", Item::File);
459            vd.field_list_integers_exactly("size", 2);
460            vd.field_list_integers_exactly("offset", 2);
461            vd.field_list_integers_exactly("framesize", 2);
462            vd.field_integer("frame");
463            vd.field_integer("fontsize");
464            vd.field_list_numeric_exactly("uv", 4);
465            vd.field_item("color", Item::TextFormat);
466        });
467    }
468}
469
470#[derive(Clone, Debug)]
471struct TextFormat {
472    #[allow(dead_code)]
473    key: Token,
474    block: Block,
475    color_blind_mode: Option<Token>,
476}
477
478impl TextFormat {
479    pub fn new(key: Token, block: Block, color_blind_mode: Option<Token>) -> Self {
480        Self { key, block, color_blind_mode }
481    }
482
483    pub fn validate(&self, data: &Everything) {
484        if self.color_blind_mode.is_some() {
485            // Color-blind modes must override existing textformats
486            data.verify_exists(Item::TextFormat, &self.key);
487        }
488        let mut vd = Validator::new(&self.block, data);
489        vd.set_max_severity(Severity::Warning);
490        vd.field_value("name");
491        vd.field_bool("override");
492        vd.field_value("format"); // TODO
493    }
494}
495
496#[derive(Debug)]
497pub struct GuiTemplate {
498    #[allow(dead_code)] // key is not used, but don't want to remove the code for storing it
499    key: Token,
500    block: Block,
501    gui_block: RwLock<Option<Arc<GuiBlock>>>,
502}
503
504impl GuiTemplate {
505    pub fn new(key: Token, block: Block) -> Self {
506        Self { key, block, gui_block: RwLock::new(None) }
507    }
508
509    pub fn validate(&self, data: &Everything) {
510        let mut dc = DataContext::new();
511        // unwrapping the Option is safe because they were all calculated during finalize
512        self.gui_block.read().unwrap().as_ref().unwrap().validate(None, data, &mut dc);
513    }
514
515    pub fn calculate_gui_block(
516        &self,
517        types: &TigerHashMap<Lowercase<'static>, GuiType>,
518        templates: &TigerHashMap<&'static str, GuiTemplate>,
519    ) -> Arc<GuiBlock> {
520        if let Ok(mut gui_block) = self.gui_block.try_write() {
521            let calc = GuiBlock::from_block(GuiBlockFrom::Template, &self.block, types, templates);
522            *gui_block = Some(Arc::clone(&calc));
523            calc
524        } else {
525            let msg = "cycle in type block definitions";
526            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
527            Arc::new(GuiBlock::default())
528        }
529    }
530
531    pub fn gui_block(
532        &self,
533        types: &TigerHashMap<Lowercase<'static>, GuiType>,
534        templates: &TigerHashMap<&'static str, GuiTemplate>,
535    ) -> Arc<GuiBlock> {
536        if let Ok(gui_block) = self.gui_block.try_read() {
537            if let Some(gui_block) = gui_block.clone() {
538                // cloning the Option Arc
539                gui_block
540            } else {
541                drop(gui_block);
542                self.calculate_gui_block(types, templates)
543            }
544        } else {
545            let msg = "cycle in type block definitions";
546            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
547            Arc::new(GuiBlock::default())
548        }
549    }
550}
551
552#[derive(Debug)]
553pub struct GuiType {
554    key: Token,
555    base: Token,
556    block: Block,
557    // Is a scrollbar = scrollbar type definition, with key the same as base and base should be a
558    // builtin type.
559    is_builtin_wrapper: bool,
560    // The outer Option is whether the result has been calculated;
561    // the inner Option is that the result might not exist.
562    #[allow(clippy::option_option)] // TODO
563    builtin: RwLock<Option<Option<BuiltinWidget>>>,
564    gui_block: RwLock<Option<Arc<GuiBlock>>>,
565}
566
567impl GuiType {
568    pub fn new(key: Token, base: Token, block: Block) -> Self {
569        let base_lc = Lowercase::new(base.as_str());
570        // Precalculate the builtin field to give the recursive builtin calculation somewhere to terminate.
571        let builtin = BuiltinWidget::builtin_current_game(&base_lc).map(Some);
572        let is_builtin_wrapper = base_lc == Lowercase::new(key.as_str());
573        Self {
574            key,
575            base,
576            block,
577            is_builtin_wrapper,
578            builtin: RwLock::new(builtin),
579            gui_block: RwLock::new(None),
580        }
581    }
582
583    pub fn validate(&self, data: &Everything) {
584        data.verify_exists(Item::GuiType, &self.base);
585        let base_lc = Lowercase::new(self.base.as_str());
586        if self.is_builtin_wrapper && BuiltinWidget::builtin_current_game(&base_lc).is_none() {
587            err(ErrorKey::Loop)
588                .msg("recursive definition of non-builtin type")
589                .loc(&self.key)
590                .push();
591        }
592        let mut dc = DataContext::new();
593        // Unwrapping the Option is safe because they were all calculated during finalize
594        self.gui_block.read().unwrap().as_ref().unwrap().validate(None, data, &mut dc);
595    }
596
597    pub fn calculate_builtin(
598        &self,
599        types: &TigerHashMap<Lowercase<'static>, GuiType>,
600    ) -> Option<BuiltinWidget> {
601        if let Ok(mut builtin) = self.builtin.try_write() {
602            let base_lc = Lowercase::new(self.base.as_str());
603            let calc = types.get(&base_lc).and_then(|t| t.builtin(types));
604            *builtin = Some(calc);
605            calc
606        } else {
607            let msg = "cycle in type definitions";
608            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
609            None
610        }
611    }
612
613    pub fn builtin(
614        &self,
615        types: &TigerHashMap<Lowercase<'static>, GuiType>,
616    ) -> Option<BuiltinWidget> {
617        if let Ok(builtin) = self.builtin.try_read() {
618            if let Some(builtin) = *builtin {
619                builtin
620            } else {
621                drop(builtin);
622                self.calculate_builtin(types)
623            }
624        } else {
625            let msg = "cycle in type definitions";
626            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
627            None
628        }
629    }
630
631    pub fn calculate_gui_block(
632        &self,
633        types: &TigerHashMap<Lowercase<'static>, GuiType>,
634        templates: &TigerHashMap<&'static str, GuiTemplate>,
635    ) -> Arc<GuiBlock> {
636        if let Ok(mut gui_block) = self.gui_block.try_write() {
637            let from = if self.is_builtin_wrapper {
638                GuiBlockFrom::TypeWrapper(&self.base)
639            } else {
640                GuiBlockFrom::TypeBase(&self.base)
641            };
642            let calc = GuiBlock::from_block(from, &self.block, types, templates);
643            *gui_block = Some(Arc::clone(&calc));
644            calc
645        } else {
646            let msg = "cycle in type block definitions";
647            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
648            Arc::new(GuiBlock::default())
649        }
650    }
651
652    pub fn gui_block(
653        &self,
654        types: &TigerHashMap<Lowercase<'static>, GuiType>,
655        templates: &TigerHashMap<&'static str, GuiTemplate>,
656    ) -> Arc<GuiBlock> {
657        if let Ok(gui_block) = self.gui_block.try_read() {
658            // cloning the Option Arc
659            if let Some(gui_block) = gui_block.clone() {
660                gui_block
661            } else {
662                drop(gui_block);
663                self.calculate_gui_block(types, templates)
664            }
665        } else {
666            let msg = "cycle in type block definitions";
667            fatal(ErrorKey::Loop).msg(msg).loc(&self.key).push();
668            Arc::new(GuiBlock::default())
669        }
670    }
671}
672
673#[derive(Clone, Debug)]
674struct GuiLayer {
675    key: Token,
676    block: Block,
677}
678
679impl GuiLayer {
680    pub fn new(key: Token, block: Block) -> Self {
681        Self { key, block }
682    }
683
684    pub fn validate(&self, data: &Everything) {
685        let mut vd = Validator::new(&self.block, data);
686        vd.field_value("priority");
687        vd.field_value("raycast_layer");
688    }
689}