tiger-lib 1.18.0

Library used by the tools ck3-tiger, vic3-tiger, and imperator-tiger. This library holds the bulk of the code for them. It can be built either for ck3-tiger with the feature ck3, or for vic3-tiger with the feature vic3, or for imperator-tiger with the feature imperator, but not both at the same time.
Documentation
//! Miscellaneous convenience functions.
use ahash::{HashMap, HashSet, RandomState};
use bimap::BiHashMap;

use std::fmt::{Display, Formatter};
use std::str::FromStr;

use crate::game::Game;
use crate::item::Item;
#[cfg(any(feature = "vic3", feature = "eu5"))]
use crate::report::err;
use crate::report::{ErrorKey, tips, warn};
#[cfg(feature = "hoi4")]
use crate::scopes::Scopes;
use crate::token::Token;

pub type TigerHashMap<K, V> = HashMap<K, V>;
pub use ahash::HashMapExt as TigerHashMapExt;
pub type TigerHashSet<T> = HashSet<T>;
pub use ahash::HashSetExt as TigerHashSetExt;

#[macro_export]
macro_rules! set {
    ( $x:expr ) => {
        ahash::AHashSet::from($x).into()
    };
}

/// Basically a named bool.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AllowInject {
    No,
    Yes,
}

/// Warns about a redefinition of a database item
pub fn dup_error(key: &Token, other: &Token, id: &str) {
    warn(ErrorKey::DuplicateItem)
        .msg(format!("{id} is redefined by another {id}"))
        .loc(other)
        .loc_msg(key, format!("the other {id} is here"))
        .push();
}

/// Warns about an exact redefinition of a database item
pub fn exact_dup_error(key: &Token, other: &Token, id: &str) {
    warn(ErrorKey::ExactDuplicateItem)
        .msg(format!("{id} is redefined by an identical {id}"))
        .loc(other)
        .loc_msg(key, format!("the other {id} is here"))
        .push();
}

/// Warns about a redefinition of a database item, but only at "advice" level
pub fn exact_dup_advice(key: &Token, other: &Token, id: &str) {
    tips(ErrorKey::ExactDuplicateItem)
        .msg(format!("{id} is redefined by an identical {id}, which may cause problems if one of them is later changed"))
        .loc(other)
        .loc_msg(key, format!("the other {id} is here"))
        .push();
}

/// Warns about a duplicate `key = value` in a database item.
/// `key` is the new one, `other` is the old one.
pub fn dup_assign_error(key: &Token, other: &Token, allow_inject: AllowInject) {
    if allow_inject == AllowInject::Yes && key.loc.kind > other.loc.kind {
        return;
    }

    // Don't trace back macro invocations for duplicate field errors,
    // because they're just confusing.
    let mut key = key.clone();
    key.loc.link_idx = None;
    let mut other = other.clone();
    other.loc.link_idx = None;

    warn(ErrorKey::DuplicateField)
        .msg(format!("`{other}` is redefined in a following line").as_str())
        .loc(other.loc)
        .loc_msg(key.loc, "the other one is here")
        .push();
}

pub fn display_choices(f: &mut Formatter, v: &[&str], joiner: &str) -> Result<(), std::fmt::Error> {
    for i in 0..v.len() {
        write!(f, "{}", v[i])?;
        if i + 1 == v.len() {
        } else if i + 2 == v.len() {
            write!(f, " {joiner} ")?;
        } else {
            write!(f, ", ")?;
        }
    }
    Ok(())
}

/// The Choices enum exists to hook into the Display logic of printing to a string
enum Choices<'a> {
    OrChoices(&'a [&'a str]),
    AndChoices(&'a [&'a str]),
}

impl Display for Choices<'_> {
    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
        match self {
            Choices::OrChoices(cs) => display_choices(f, cs, "or"),
            Choices::AndChoices(cs) => display_choices(f, cs, "and"),
        }
    }
}

pub fn stringify_choices(v: &[&str]) -> String {
    format!("{}", Choices::OrChoices(v))
}

pub fn stringify_list(v: &[&str]) -> String {
    format!("{}", Choices::AndChoices(v))
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg(feature = "jomini")]
pub enum TriBool {
    True,
    False,
    Maybe,
}

/// Warn if a scripted item has one of these names, and ignore it when validating.
/// This avoids tons of errors from for example a scripted effect named `if`.
/// Such an effect can happen accidentally with a misplaced brace or two.
pub const BANNED_NAMES: &[&str] = &[
    "if",
    "else",
    "else_if",
    "trigger_if",
    "trigger_else",
    "trigger_else_if",
    "while",
    "limit",
    "filter",
    "switch",
    "take_hostage", // actually used by vanilla CK3
];

pub(crate) type BiTigerHashMap<L, R> = BiHashMap<L, R, RandomState, RandomState>;

#[derive(Debug, Clone)]
pub(crate) enum ActionOrEvent {
    Action(Token),
    Event(Token, &'static str, usize),
}

impl ActionOrEvent {
    pub(crate) fn new_action(key: Token) -> Self {
        Self::Action(key)
    }

    pub(crate) fn new_event(key: Token) -> Self {
        if let Some((namespace, nr)) = key.as_str().split_once('.')
            && let Ok(nr) = usize::from_str(nr)
        {
            return Self::Event(key, namespace, nr);
        }
        let namespace = key.as_str();
        Self::Event(key, namespace, 0)
    }

    pub(crate) fn token(&self) -> &Token {
        match self {
            Self::Action(token) | Self::Event(token, _, _) => token,
        }
    }
}

impl PartialEq for ActionOrEvent {
    fn eq(&self, other: &Self) -> bool {
        match self {
            Self::Action(token) => {
                if let Self::Action(other_token) = other {
                    token == other_token
                } else {
                    false
                }
            }
            Self::Event(_, namespace, nr) => {
                if let Self::Event(_, other_namespace, other_nr) = other {
                    namespace == other_namespace && nr == other_nr
                } else {
                    false
                }
            }
        }
    }
}

impl Eq for ActionOrEvent {}

impl Display for ActionOrEvent {
    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
        write!(f, "{}", self.token())
    }
}

pub fn is_country_tag(part: &str) -> bool {
    part.len() == 3 && part != "NOT" && part.chars().all(|c| c.is_ascii_uppercase())
}

#[cfg(feature = "hoi4")]
pub fn expand_scopes_hoi4(mut scopes: Scopes) -> Scopes {
    if scopes.contains(Scopes::Country) || scopes.contains(Scopes::State) {
        scopes |= Scopes::CombinedCountryAndState;
    }
    if scopes.contains(Scopes::Country) || scopes.contains(Scopes::Character) {
        scopes |= Scopes::CombinedCountryAndCharacter;
    }
    scopes
}

#[inline]
pub fn snake_case_to_camel_case(s: &str) -> String {
    let mut temp_s = String::with_capacity(s.len());
    let mut do_uppercase = true;
    for c in s.chars() {
        if c == '_' {
            do_uppercase = true;
        } else if do_uppercase {
            temp_s.push(c.to_ascii_uppercase());
            do_uppercase = false;
        } else {
            temp_s.push(c);
        }
    }
    temp_s
}

#[inline]
pub fn camel_case_to_separated_words(s: &str) -> String {
    // Adding 5 bytes to the capacity is just a guess.
    // It should be 1 byte per underscore in `s`, but
    // calculating that is more expensive than it's worth.
    let mut temp_s = String::with_capacity(s.len() + 5);
    for c in s.chars() {
        if c.is_ascii_uppercase() {
            if !temp_s.is_empty() {
                temp_s.push(' ');
            }
            temp_s.push(c.to_ascii_lowercase());
        } else {
            temp_s.push(c);
        }
    }
    temp_s
}

/// Used for scripted triggers, effects, and modifiers. Handles the `REPLACE:` etc prefixes, and
/// returns the name to insert under iff the new item should be inserted.
pub fn limited_item_prefix_should_insert<'a, 'b, F>(
    itype: Item,
    key: Token,
    get_other: F,
) -> Option<Token>
where
    F: Fn(&'a str) -> Option<&'b Token>,
{
    if Game::is_vic3() || Game::is_eu5() {
        #[allow(clippy::collapsible_else_if)]
        #[cfg(any(feature = "vic3", feature = "eu5"))]
        if let Some((prefix, name)) = key.split_once(':') {
            let other = get_other(name.as_str());
            match prefix.as_str() {
                "INJECT" | "TRY_INJECT" | "INJECT_OR_CREATE" => {
                    let msg = format!("cannot inject {itype}");
                    err(ErrorKey::Prefixes).msg(msg).loc(prefix).push();
                }
                "REPLACE" => {
                    if other.is_some() {
                        return Some(name);
                    }
                    let msg = "replacing a non-existing item";
                    err(ErrorKey::Prefixes).msg(msg).loc(name).push();
                }
                "TRY_REPLACE" => {
                    if other.is_some() {
                        return Some(name);
                    }
                }
                "REPLACE_OR_CREATE" => return Some(name),
                _ => {
                    let msg = format!("unknown prefix `{prefix}`");
                    err(ErrorKey::Prefixes).msg(msg).loc(prefix).push();
                }
            }
        } else {
            if let Some(other) = get_other(key.as_str()) {
                let msg = format!("must have prefix such as `REPLACE:` to replace {itype}");
                err(ErrorKey::Prefixes).msg(msg).loc(key).loc_msg(other, "original here").push();
            } else {
                return Some(key);
            }
        }
    } else {
        if let Some(other) = get_other(key.as_str())
            && other.loc.kind >= key.loc.kind
        {
            dup_error(&key, other, &itype.to_string());
        }
        return Some(key);
    }
    None
}

#[derive(Debug, Clone)]
#[cfg(feature = "jomini")]
pub enum PrefixShould {
    Insert(Token),
    #[cfg(any(feature = "vic3", feature = "eu5"))]
    Inject(Token),
    Ignore,
}

/// Used for prefixed items other than effects, triggers, and modifiers.
#[cfg(feature = "jomini")]
pub fn item_prefix_should<'a, 'b, F>(itype: Item, key: &Token, get_other: F) -> PrefixShould
where
    F: Fn(&'a str) -> Option<&'b Token>,
{
    if Game::is_vic3() || Game::is_eu5() {
        #[allow(clippy::collapsible_else_if)]
        #[cfg(any(feature = "vic3", feature = "eu5"))]
        if let Some((prefix, name)) = key.split_once(':') {
            let other = get_other(name.as_str());
            match prefix.as_str() {
                "INJECT" => {
                    if other.is_some() {
                        return PrefixShould::Inject(name);
                    }
                    let msg = "injecting into a non-existing item";
                    err(ErrorKey::Prefixes).msg(msg).loc(name).push();
                }
                "REPLACE" => {
                    if other.is_some() {
                        return PrefixShould::Insert(name);
                    }
                    let msg = "replacing a non-existing item";
                    err(ErrorKey::Prefixes).msg(msg).loc(name).push();
                }
                "TRY_INJECT" => {
                    if other.is_some() {
                        return PrefixShould::Inject(name);
                    }
                }
                "TRY_REPLACE" => {
                    if other.is_some() {
                        return PrefixShould::Insert(name);
                    }
                }
                "REPLACE_OR_CREATE" => return PrefixShould::Insert(name),
                "INJECT_OR_CREATE" => {
                    if other.is_some() {
                        return PrefixShould::Inject(name);
                    }
                    return PrefixShould::Insert(name);
                }
                _ => {
                    let msg = format!("unknown prefix `{prefix}`");
                    err(ErrorKey::Prefixes).msg(msg).loc(prefix).push();
                }
            }
        } else {
            if let Some(other) = get_other(key.as_str()) {
                let msg = format!("must have prefix such as `REPLACE:` to replace {itype}");
                err(ErrorKey::Prefixes).msg(msg).loc(key).loc_msg(other, "original here").push();
            } else {
                return PrefixShould::Insert(key.clone());
            }
        }
    } else {
        if let Some(other) = get_other(key.as_str())
            && other.loc.kind >= key.loc.kind
        {
            dup_error(key, other, &itype.to_string());
        }
        return PrefixShould::Insert(key.clone());
    }
    PrefixShould::Ignore
}