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::report::{tips, warn, ErrorKey};
9#[cfg(feature = "hoi4")]
10use crate::scopes::Scopes;
11use crate::token::Token;
12
13pub type TigerHashMap<K, V> = HashMap<K, V>;
14pub use ahash::HashMapExt as TigerHashMapExt;
15pub type TigerHashSet<T> = HashSet<T>;
16pub use ahash::HashSetExt as TigerHashSetExt;
17
18#[macro_export]
19macro_rules! set {
20    ( $x:expr ) => {
21        ahash::AHashSet::from($x).into()
22    };
23}
24
25/// Warns about a redefinition of a database item
26pub fn dup_error(key: &Token, other: &Token, id: &str) {
27    warn(ErrorKey::DuplicateItem)
28        .msg(format!("{id} is redefined by another {id}"))
29        .loc(other)
30        .loc_msg(key, format!("the other {id} is here"))
31        .push();
32}
33
34/// Warns about an exact redefinition of a database item
35pub fn exact_dup_error(key: &Token, other: &Token, id: &str) {
36    warn(ErrorKey::ExactDuplicateItem)
37        .msg(format!("{id} is redefined by an identical {id}"))
38        .loc(other)
39        .loc_msg(key, format!("the other {id} is here"))
40        .push();
41}
42
43/// Warns about a redefinition of a database item, but only at "advice" level
44pub fn exact_dup_advice(key: &Token, other: &Token, id: &str) {
45    tips(ErrorKey::ExactDuplicateItem)
46        .msg(format!("{id} is redefined by an identical {id}, which may cause problems if one of them is later changed"))
47        .loc(other)
48        .loc_msg(key, format!("the other {id} is here"))
49        .push();
50}
51
52/// Warns about a duplicate `key = value` in a database item
53pub fn dup_assign_error(key: &Token, other: &Token) {
54    // Don't trace back macro invocations for duplicate field errors,
55    // because they're just confusing.
56    let mut key = key.clone();
57    key.loc.link_idx = None;
58    let mut other = other.clone();
59    other.loc.link_idx = None;
60
61    warn(ErrorKey::DuplicateField)
62        .msg(format!("`{other}` is redefined in a following line").as_str())
63        .loc(other.loc)
64        .loc_msg(key.loc, "the other one is here")
65        .push();
66}
67
68pub fn display_choices(f: &mut Formatter, v: &[&str], joiner: &str) -> Result<(), std::fmt::Error> {
69    for i in 0..v.len() {
70        write!(f, "{}", v[i])?;
71        if i + 1 == v.len() {
72        } else if i + 2 == v.len() {
73            write!(f, " {joiner} ")?;
74        } else {
75            write!(f, ", ")?;
76        }
77    }
78    Ok(())
79}
80
81/// The Choices enum exists to hook into the Display logic of printing to a string
82enum Choices<'a> {
83    OrChoices(&'a [&'a str]),
84    AndChoices(&'a [&'a str]),
85}
86
87impl Display for Choices<'_> {
88    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
89        match self {
90            Choices::OrChoices(cs) => display_choices(f, cs, "or"),
91            Choices::AndChoices(cs) => display_choices(f, cs, "and"),
92        }
93    }
94}
95
96pub fn stringify_choices(v: &[&str]) -> String {
97    format!("{}", Choices::OrChoices(v))
98}
99
100pub fn stringify_list(v: &[&str]) -> String {
101    format!("{}", Choices::AndChoices(v))
102}
103
104#[derive(Copy, Clone, Debug, PartialEq, Eq)]
105#[cfg(feature = "jomini")]
106pub enum TriBool {
107    True,
108    False,
109    Maybe,
110}
111
112/// Warn if a scripted item has one of these names, and ignore it when validating.
113/// This avoids tons of errors from for example a scripted effect named `if`.
114/// Such an effect can happen accidentally with a misplaced brace or two.
115pub const BANNED_NAMES: &[&str] = &[
116    "if",
117    "else",
118    "else_if",
119    "trigger_if",
120    "trigger_else",
121    "trigger_else_if",
122    "while",
123    "limit",
124    "filter",
125    "switch",
126    "take_hostage", // actually used by vanilla CK3
127];
128
129pub(crate) type BiTigerHashMap<L, R> = BiHashMap<L, R, RandomState, RandomState>;
130
131#[derive(Debug, Clone)]
132pub(crate) enum ActionOrEvent {
133    Action(Token),
134    Event(Token, &'static str, usize),
135}
136
137impl ActionOrEvent {
138    pub(crate) fn new_action(key: Token) -> Self {
139        Self::Action(key)
140    }
141
142    pub(crate) fn new_event(key: Token) -> Self {
143        if let Some((namespace, nr)) = key.as_str().split_once('.') {
144            if let Ok(nr) = usize::from_str(nr) {
145                return Self::Event(key, namespace, nr);
146            }
147        }
148        let namespace = key.as_str();
149        Self::Event(key, namespace, 0)
150    }
151
152    pub(crate) fn token(&self) -> &Token {
153        match self {
154            Self::Action(token) | Self::Event(token, _, _) => token,
155        }
156    }
157}
158
159impl PartialEq for ActionOrEvent {
160    fn eq(&self, other: &Self) -> bool {
161        match self {
162            Self::Action(token) => {
163                if let Self::Action(other_token) = other {
164                    token == other_token
165                } else {
166                    false
167                }
168            }
169            Self::Event(_, namespace, nr) => {
170                if let Self::Event(_, other_namespace, other_nr) = other {
171                    namespace == other_namespace && nr == other_nr
172                } else {
173                    false
174                }
175            }
176        }
177    }
178}
179
180impl Eq for ActionOrEvent {}
181
182impl Display for ActionOrEvent {
183    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
184        write!(f, "{}", self.token())
185    }
186}
187
188pub fn is_country_tag(part: &str) -> bool {
189    part.len() == 3 && part != "NOT" && part.chars().all(|c| c.is_ascii_uppercase())
190}
191
192#[cfg(feature = "hoi4")]
193pub fn expand_scopes_hoi4(mut scopes: Scopes) -> Scopes {
194    if scopes.contains(Scopes::Country) || scopes.contains(Scopes::State) {
195        scopes |= Scopes::CombinedCountryAndState;
196    }
197    if scopes.contains(Scopes::Country) || scopes.contains(Scopes::Character) {
198        scopes |= Scopes::CombinedCountryAndCharacter;
199    }
200    scopes
201}
202
203#[inline]
204pub fn snake_case_to_camel_case(s: &str) -> String {
205    let mut temp_s = String::with_capacity(s.len());
206    let mut do_uppercase = true;
207    for c in s.chars() {
208        if c == '_' {
209            do_uppercase = true;
210        } else if do_uppercase {
211            temp_s.push(c.to_ascii_uppercase());
212            do_uppercase = false;
213        } else {
214            temp_s.push(c);
215        }
216    }
217    temp_s
218}
219
220#[inline]
221pub fn camel_case_to_separated_words(s: &str) -> String {
222    // Adding 5 bytes to the capacity is just a guess.
223    // It should be 1 byte per underscore in `s`, but
224    // calculating that is more expensive than it's worth.
225    let mut temp_s = String::with_capacity(s.len() + 5);
226    for c in s.chars() {
227        if c.is_ascii_uppercase() {
228            if !temp_s.is_empty() {
229                temp_s.push(' ');
230            }
231            temp_s.push(c.to_ascii_lowercase());
232        } else {
233            temp_s.push(c);
234        }
235    }
236    temp_s
237}