1use ahash::{HashMap, HashSet, RandomState};
3use bimap::BiHashMap;
4
5use std::fmt::{Display, Formatter};
6use std::str::FromStr;
7
8use crate::game::Game;
9use crate::item::Item;
10#[cfg(feature = "vic3")]
11use crate::report::err;
12use crate::report::{ErrorKey, tips, warn};
13#[cfg(feature = "hoi4")]
14use crate::scopes::Scopes;
15use crate::token::Token;
16
17pub type TigerHashMap<K, V> = HashMap<K, V>;
18pub use ahash::HashMapExt as TigerHashMapExt;
19pub type TigerHashSet<T> = HashSet<T>;
20pub use ahash::HashSetExt as TigerHashSetExt;
21
22#[macro_export]
23macro_rules! set {
24 ( $x:expr ) => {
25 ahash::AHashSet::from($x).into()
26 };
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum AllowInject {
32 No,
33 Yes,
34}
35
36pub fn dup_error(key: &Token, other: &Token, id: &str) {
38 warn(ErrorKey::DuplicateItem)
39 .msg(format!("{id} is redefined by another {id}"))
40 .loc(other)
41 .loc_msg(key, format!("the other {id} is here"))
42 .push();
43}
44
45pub fn exact_dup_error(key: &Token, other: &Token, id: &str) {
47 warn(ErrorKey::ExactDuplicateItem)
48 .msg(format!("{id} is redefined by an identical {id}"))
49 .loc(other)
50 .loc_msg(key, format!("the other {id} is here"))
51 .push();
52}
53
54pub fn exact_dup_advice(key: &Token, other: &Token, id: &str) {
56 tips(ErrorKey::ExactDuplicateItem)
57 .msg(format!("{id} is redefined by an identical {id}, which may cause problems if one of them is later changed"))
58 .loc(other)
59 .loc_msg(key, format!("the other {id} is here"))
60 .push();
61}
62
63pub fn dup_assign_error(key: &Token, other: &Token, allow_inject: AllowInject) {
66 if allow_inject == AllowInject::Yes && key.loc.kind > other.loc.kind {
67 return;
68 }
69
70 let mut key = key.clone();
73 key.loc.link_idx = None;
74 let mut other = other.clone();
75 other.loc.link_idx = None;
76
77 warn(ErrorKey::DuplicateField)
78 .msg(format!("`{other}` is redefined in a following line").as_str())
79 .loc(other.loc)
80 .loc_msg(key.loc, "the other one is here")
81 .push();
82}
83
84pub fn display_choices(f: &mut Formatter, v: &[&str], joiner: &str) -> Result<(), std::fmt::Error> {
85 for i in 0..v.len() {
86 write!(f, "{}", v[i])?;
87 if i + 1 == v.len() {
88 } else if i + 2 == v.len() {
89 write!(f, " {joiner} ")?;
90 } else {
91 write!(f, ", ")?;
92 }
93 }
94 Ok(())
95}
96
97enum Choices<'a> {
99 OrChoices(&'a [&'a str]),
100 AndChoices(&'a [&'a str]),
101}
102
103impl Display for Choices<'_> {
104 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
105 match self {
106 Choices::OrChoices(cs) => display_choices(f, cs, "or"),
107 Choices::AndChoices(cs) => display_choices(f, cs, "and"),
108 }
109 }
110}
111
112pub fn stringify_choices(v: &[&str]) -> String {
113 format!("{}", Choices::OrChoices(v))
114}
115
116pub fn stringify_list(v: &[&str]) -> String {
117 format!("{}", Choices::AndChoices(v))
118}
119
120#[derive(Copy, Clone, Debug, PartialEq, Eq)]
121#[cfg(feature = "jomini")]
122pub enum TriBool {
123 True,
124 False,
125 Maybe,
126}
127
128pub const BANNED_NAMES: &[&str] = &[
132 "if",
133 "else",
134 "else_if",
135 "trigger_if",
136 "trigger_else",
137 "trigger_else_if",
138 "while",
139 "limit",
140 "filter",
141 "switch",
142 "take_hostage", ];
144
145pub(crate) type BiTigerHashMap<L, R> = BiHashMap<L, R, RandomState, RandomState>;
146
147#[derive(Debug, Clone)]
148pub(crate) enum ActionOrEvent {
149 Action(Token),
150 Event(Token, &'static str, usize),
151}
152
153impl ActionOrEvent {
154 pub(crate) fn new_action(key: Token) -> Self {
155 Self::Action(key)
156 }
157
158 pub(crate) fn new_event(key: Token) -> Self {
159 if let Some((namespace, nr)) = key.as_str().split_once('.') {
160 if let Ok(nr) = usize::from_str(nr) {
161 return Self::Event(key, namespace, nr);
162 }
163 }
164 let namespace = key.as_str();
165 Self::Event(key, namespace, 0)
166 }
167
168 pub(crate) fn token(&self) -> &Token {
169 match self {
170 Self::Action(token) | Self::Event(token, _, _) => token,
171 }
172 }
173}
174
175impl PartialEq for ActionOrEvent {
176 fn eq(&self, other: &Self) -> bool {
177 match self {
178 Self::Action(token) => {
179 if let Self::Action(other_token) = other {
180 token == other_token
181 } else {
182 false
183 }
184 }
185 Self::Event(_, namespace, nr) => {
186 if let Self::Event(_, other_namespace, other_nr) = other {
187 namespace == other_namespace && nr == other_nr
188 } else {
189 false
190 }
191 }
192 }
193 }
194}
195
196impl Eq for ActionOrEvent {}
197
198impl Display for ActionOrEvent {
199 fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
200 write!(f, "{}", self.token())
201 }
202}
203
204pub fn is_country_tag(part: &str) -> bool {
205 part.len() == 3 && part != "NOT" && part.chars().all(|c| c.is_ascii_uppercase())
206}
207
208#[cfg(feature = "hoi4")]
209pub fn expand_scopes_hoi4(mut scopes: Scopes) -> Scopes {
210 if scopes.contains(Scopes::Country) || scopes.contains(Scopes::State) {
211 scopes |= Scopes::CombinedCountryAndState;
212 }
213 if scopes.contains(Scopes::Country) || scopes.contains(Scopes::Character) {
214 scopes |= Scopes::CombinedCountryAndCharacter;
215 }
216 scopes
217}
218
219#[inline]
220pub fn snake_case_to_camel_case(s: &str) -> String {
221 let mut temp_s = String::with_capacity(s.len());
222 let mut do_uppercase = true;
223 for c in s.chars() {
224 if c == '_' {
225 do_uppercase = true;
226 } else if do_uppercase {
227 temp_s.push(c.to_ascii_uppercase());
228 do_uppercase = false;
229 } else {
230 temp_s.push(c);
231 }
232 }
233 temp_s
234}
235
236#[inline]
237pub fn camel_case_to_separated_words(s: &str) -> String {
238 let mut temp_s = String::with_capacity(s.len() + 5);
242 for c in s.chars() {
243 if c.is_ascii_uppercase() {
244 if !temp_s.is_empty() {
245 temp_s.push(' ');
246 }
247 temp_s.push(c.to_ascii_lowercase());
248 } else {
249 temp_s.push(c);
250 }
251 }
252 temp_s
253}
254
255pub fn limited_item_prefix_should_insert<'a, 'b, F>(
258 itype: Item,
259 key: Token,
260 get_other: F,
261) -> Option<Token>
262where
263 F: Fn(&'a str) -> Option<&'b Token>,
264{
265 if Game::is_vic3() {
266 #[allow(clippy::collapsible_else_if)]
267 #[cfg(feature = "vic3")]
268 if let Some((prefix, name)) = key.split_once(':') {
269 let other = get_other(name.as_str());
270 match prefix.as_str() {
271 "INJECT" | "TRY_INJECT" | "INJECT_OR_CREATE" => {
272 let msg = format!("cannot inject {itype}");
273 err(ErrorKey::Prefixes).msg(msg).loc(prefix).push();
274 }
275 "REPLACE" => {
276 if other.is_some() {
277 return Some(name);
278 }
279 let msg = "replacing a non-existing item";
280 err(ErrorKey::Prefixes).msg(msg).loc(name).push();
281 }
282 "TRY_REPLACE" => {
283 if other.is_some() {
284 return Some(name);
285 }
286 }
287 "REPLACE_OR_CREATE" => return Some(name),
288 _ => {
289 let msg = format!("unknown prefix `{prefix}`");
290 err(ErrorKey::Prefixes).msg(msg).loc(prefix).push();
291 }
292 }
293 } else {
294 if let Some(other) = get_other(key.as_str()) {
295 let msg = format!("must have prefix such as `REPLACE:` to replace {itype}");
296 err(ErrorKey::Prefixes).msg(msg).loc(key).loc_msg(other, "original here").push();
297 } else {
298 return Some(key);
299 }
300 }
301 } else {
302 if let Some(other) = get_other(key.as_str()) {
303 if other.loc.kind >= key.loc.kind {
304 dup_error(&key, other, &itype.to_string());
305 }
306 }
307 return Some(key);
308 }
309 None
310}