Skip to main content

tiger_lib/
helpers.rs

1//! Miscellaneous convenience functions.
2use 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/// Basically a named bool.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum AllowInject {
32    No,
33    Yes,
34}
35
36/// Warns about a redefinition of a database item
37pub 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
45/// Warns about an exact redefinition of a database item
46pub 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
54/// Warns about a redefinition of a database item, but only at "advice" level
55pub 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
63/// Warns about a duplicate `key = value` in a database item.
64/// `key` is the new one, `other` is the old one.
65pub 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    // Don't trace back macro invocations for duplicate field errors,
71    // because they're just confusing.
72    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
97/// The Choices enum exists to hook into the Display logic of printing to a string
98enum 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
128/// Warn if a scripted item has one of these names, and ignore it when validating.
129/// This avoids tons of errors from for example a scripted effect named `if`.
130/// Such an effect can happen accidentally with a misplaced brace or two.
131pub 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", // actually used by vanilla CK3
143];
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    // Adding 5 bytes to the capacity is just a guess.
239    // It should be 1 byte per underscore in `s`, but
240    // calculating that is more expensive than it's worth.
241    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
255/// Used for scripted triggers, effects, and modifiers. Handles the `REPLACE:` etc prefixes, and
256/// returns the name to insert under iff the new item should be inserted.
257pub 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}