tiger_lib/
variables.rs

1//! A registry of all the script variables that have been defined somewhere.
2
3use crate::block::{BV, Block, Comparator, Eq::Single, Field};
4use crate::game::{Game, GameFlags};
5use crate::helpers::{TigerHashMap, TigerHashSet};
6use crate::report::{ErrorKey, Severity, report};
7use crate::token::Token;
8
9#[derive(Debug)]
10pub struct Variables {
11    names: TigerHashSet<&'static str>,
12    // For hoi4: variables that were defined with @something at the end, here without the @ part.
13    name_prefixes: TigerHashSet<&'static str>,
14    // For hoi4: variables that look like they have a country tag at the end, without the country tag.
15    name_speculative_prefixes: TigerHashSet<&'static str>,
16    lists: TigerHashSet<&'static str>,
17    list_prefixes: TigerHashSet<&'static str>,
18    list_speculative_prefixes: TigerHashSet<&'static str>,
19
20    // effect names to look for, mapped to the field inside them that contains the name.
21    create_variable: TigerHashMap<&'static str, Extract>,
22    create_list: TigerHashMap<&'static str, Extract>,
23}
24
25impl Variables {
26    pub fn new() -> Self {
27        Self {
28            names: TigerHashSet::default(),
29            name_prefixes: TigerHashSet::default(),
30            name_speculative_prefixes: TigerHashSet::default(),
31            lists: TigerHashSet::default(),
32            list_prefixes: TigerHashSet::default(),
33            list_speculative_prefixes: TigerHashSet::default(),
34            create_variable: filter_table(CREATE_VARIABLE),
35            create_list: filter_table(CREATE_LIST),
36        }
37    }
38
39    pub fn register_variable(&mut self, name: &'static str) {
40        if Game::is_hoi4() {
41            if let Some((prefix, _)) = name.split_once('@') {
42                self.name_prefixes.insert(remove_qualifiers(prefix));
43            } else {
44                let name = remove_qualifiers(name);
45                if let Some(prefix) = remove_suffix_tag(name) {
46                    self.name_speculative_prefixes.insert(prefix);
47                }
48                self.names.insert(name);
49            }
50        } else {
51            self.names.insert(name);
52        }
53    }
54
55    /// Just like `register_variable` but for lists.
56    pub fn register_list(&mut self, name: &'static str) {
57        if Game::is_hoi4() {
58            if let Some((prefix, _)) = name.split_once('@') {
59                self.list_prefixes.insert(remove_qualifiers(prefix));
60            } else {
61                let name = remove_qualifiers(name);
62                if let Some(prefix) = remove_suffix_tag(name) {
63                    self.list_speculative_prefixes.insert(prefix);
64                }
65                self.lists.insert(name);
66            }
67        } else {
68            self.lists.insert(name);
69        }
70    }
71    /// Recursively scan a block for effects that set a variable.
72    pub fn scan(&mut self, block: &Block) {
73        for Field(key, cmp, bv) in block.iter_fields() {
74            if !matches!(cmp, Comparator::Equals(Single)) {
75                continue;
76            }
77            if let Some(extract) = self.create_variable.get(key.as_str()) {
78                if let Some(name) = extract.extract(bv) {
79                    self.register_variable(name);
80                }
81            } else if let Some(extract) = self.create_list.get(key.as_str()) {
82                if let Some(name) = extract.extract(bv) {
83                    self.register_list(name);
84                }
85            } else if let Some(block) = bv.get_block() {
86                self.scan(block);
87            }
88        }
89    }
90
91    /// Check if a variable name has been previously registered,
92    /// but do not emit reports if it wasn't.
93    /// This takes a bare variable name that did not have an `@` suffix (in Hoi4).
94    #[allow(dead_code)]
95    pub fn variable_exists(&self, name: &str) -> bool {
96        if self.names.contains(name) {
97            return true;
98        }
99        if let Some(prefix) = remove_suffix_tag(name) {
100            return self.name_prefixes.contains(prefix);
101        }
102        false
103    }
104
105    /// Check if a variable name has been previously registered.
106    /// This takes a bare variable name that did not have an `@` suffix (in Hoi4).
107    #[allow(dead_code)]
108    pub fn verify_variable_exists(&self, name: &Token, sev: Severity) {
109        if let Some(prefix) = remove_suffix_tag(name.as_str()) {
110            if !self.name_prefixes.contains(prefix) && !self.names.contains(name.as_str()) {
111                let msg = format!("variable `{name}` or `{name}@TAG` was not set anywhere");
112                report(ErrorKey::Variables, sev).msg(msg).loc(name).push();
113            }
114        } else if !self.names.contains(name.as_str()) {
115            let msg = format!("variable `{name}` was not set anywhere");
116            report(ErrorKey::Variables, sev).msg(msg).loc(name).push();
117        }
118    }
119
120    /// Check if a variable name has been previously registered.
121    /// This takes a bare variable name from which an `@` suffix was removed.
122    /// This logic is specific to hoi4.
123    #[cfg(feature = "hoi4")]
124    pub fn verify_variable_prefix_exists(&self, prefix: &Token, sev: Severity) {
125        if !self.name_prefixes.contains(prefix.as_str())
126            && !self.name_speculative_prefixes.contains(prefix.as_str())
127        {
128            let msg = format!("a variable with prefix `{prefix}` was not set anywhere");
129            report(ErrorKey::Variables, sev).msg(msg).loc(prefix).push();
130        }
131    }
132
133    /// Check if a variable list name has been previously registered.
134    /// This takes a bare name that did not have an `@` suffix (in Hoi4).
135    #[allow(dead_code)]
136    pub fn verify_list_exists(&self, name: &Token, sev: Severity) {
137        let thing = if Game::is_hoi4() { "array" } else { "variable list" };
138        if let Some(prefix) = remove_suffix_tag(name.as_str()) {
139            if !self.list_prefixes.contains(prefix) && !self.lists.contains(name.as_str()) {
140                let msg = format!("{thing} `{name}` or `{name}@TAG` was not created anywhere");
141                report(ErrorKey::Variables, sev).msg(msg).loc(name).push();
142            }
143        } else if !self.lists.contains(name.as_str()) {
144            let msg = format!("{thing} `{name}` was not created anywhere");
145            report(ErrorKey::Variables, sev).msg(msg).loc(name).push();
146        }
147    }
148
149    /// Check if a variable list name has been previously registered.
150    /// This takes a bare name from which an `@` suffix was removed.
151    /// This logic is specific to hoi4.
152    /// Hoi4 calls them `arrays` but the function uses `list` for consistency with the other functions.
153    #[cfg(feature = "hoi4")]
154    pub fn verify_list_prefix_exists(&self, prefix: &Token, sev: Severity) {
155        if !self.list_prefixes.contains(prefix.as_str())
156            && !self.list_speculative_prefixes.contains(prefix.as_str())
157        {
158            let msg = format!("an array with prefix `{prefix}` was not set anywhere");
159            report(ErrorKey::Variables, sev).msg(msg).loc(prefix).push();
160        }
161    }
162}
163
164/// Create a map tuned for the current game.
165fn filter_table(
166    table: &[(&'static str, Extract, GameFlags)],
167) -> TigerHashMap<&'static str, Extract> {
168    let game = GameFlags::game();
169    table
170        .iter()
171        .filter(|(_, _, gameflags)| gameflags.contains(game))
172        .map(|(effect, extract, _)| (*effect, *extract))
173        .collect()
174}
175
176/// Return the variable name with any preceding `FROM.` etc removed.
177fn remove_qualifiers(name: &str) -> &str {
178    if let Some((_, name)) = name.rsplit_once('.') { name } else { name }
179}
180
181/// If the variable name has a country tag at the end, return it with that tag removed.
182/// Otherwise return `None`.
183fn remove_suffix_tag(name: &str) -> Option<&str> {
184    (name.len() > 3 && name.chars().rev().take(3).all(|c| c.is_ascii_uppercase()))
185        .then(|| &name[..name.len() - 3])
186}
187
188#[derive(Debug, Clone, Copy)]
189enum Extract {
190    Field(&'static str),
191    AssignOrField(&'static str),
192    InternalAssignOrField(&'static str),
193}
194
195impl Extract {
196    pub fn extract(&self, bv: &BV) -> Option<&'static str> {
197        match self {
198            Self::Field(field) => {
199                if let Some(block) = bv.get_block() {
200                    if let Some(name) = block.get_field_value(field) {
201                        return Some(name.as_str());
202                    }
203                }
204            }
205            Self::AssignOrField(field) => match bv {
206                BV::Value(name) => {
207                    return Some(name.as_str());
208                }
209                BV::Block(block) => {
210                    if let Some(name) = block.get_field_value(field) {
211                        return Some(name.as_str());
212                    }
213                }
214            },
215            Self::InternalAssignOrField(field) => {
216                if let Some(block) = bv.get_block() {
217                    if let Some(name) = block.get_field_value(field) {
218                        return Some(name.as_str());
219                    } else if block.num_items() == 1 {
220                        if let Some((name, _)) = block.iter_assignments().next() {
221                            return Some(name.as_str());
222                        }
223                    }
224                }
225            }
226        }
227        None
228    }
229}
230
231// TODO: treat temp variables like named scopes instead.
232const CREATE_VARIABLE: &[(&str, Extract, GameFlags)] = &[
233    ("set_dead_character_variable", Extract::Field("name"), GameFlags::Ck3),
234    ("set_global_variable", Extract::AssignOrField("name"), GameFlags::jomini()),
235    ("set_temp_variable", Extract::InternalAssignOrField("var"), GameFlags::Hoi4),
236    ("set_temp_variable_to_random", Extract::AssignOrField("var"), GameFlags::Hoi4),
237    ("set_variable", Extract::AssignOrField("name"), GameFlags::jomini()),
238    ("set_variable", Extract::InternalAssignOrField("var"), GameFlags::Hoi4),
239    ("set_variable_to_random", Extract::AssignOrField("var"), GameFlags::Hoi4),
240];
241
242const CREATE_LIST: &[(&str, Extract, GameFlags)] = &[
243    ("add_to_array", Extract::InternalAssignOrField("array"), GameFlags::Hoi4),
244    ("add_to_global_variable_list", Extract::Field("name"), GameFlags::jomini()),
245    ("add_to_temp_array", Extract::InternalAssignOrField("array"), GameFlags::Hoi4),
246    ("add_to_variable_list", Extract::Field("name"), GameFlags::jomini()),
247];