1use 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 name_prefixes: TigerHashSet<&'static str>,
14 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 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 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 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 #[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 #[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 #[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 #[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 #[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
164fn 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
176fn remove_qualifiers(name: &str) -> &str {
178 if let Some((_, name)) = name.rsplit_once('.') { name } else { name }
179}
180
181fn 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
231const 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];