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
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::sync::Arc;

use crate::block::Block;
use crate::ck3::data::provinces::ProvId;
use crate::context::ScopeContext;
use crate::desc::validate_desc;
use crate::everything::Everything;
use crate::fileset::{FileEntry, FileHandler};
use crate::helpers::{TigerHashMap, dup_error};
use crate::item::Item;
use crate::parse::ParserMemory;
use crate::pdxfile::PdxFile;
use crate::report::{ErrorKey, err, warn};
use crate::scopes::Scopes;
use crate::token::Token;
use crate::tooltipped::Tooltipped;
use crate::validate::validate_possibly_named_color;
use crate::validator::Validator;
use crate::variables::Variables;

#[derive(Clone, Debug, Default)]
pub struct Titles {
    titles: TigerHashMap<&'static str, Arc<Title>>,
    baronies: TigerHashMap<ProvId, Arc<Title>>,
}

#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub enum Tier {
    Barony,
    County,
    Duchy,
    Kingdom,
    Empire,
    Hegemony,
}

impl TryFrom<&Token> for Tier {
    type Error = std::fmt::Error;
    fn try_from(value: &Token) -> Result<Self, Self::Error> {
        let s = value.as_str();
        if s.starts_with("b_") {
            Ok(Tier::Barony)
        } else if s.starts_with("c_") {
            Ok(Tier::County)
        } else if s.starts_with("d_") {
            Ok(Tier::Duchy)
        } else if s.starts_with("k_") {
            Ok(Tier::Kingdom)
        } else if s.starts_with("e_") {
            Ok(Tier::Empire)
        } else if s.starts_with("h_") {
            Ok(Tier::Hegemony)
        } else {
            Err(std::fmt::Error)
        }
    }
}

impl TryFrom<Token> for Tier {
    type Error = std::fmt::Error;
    fn try_from(value: Token) -> Result<Self, Self::Error> {
        Tier::try_from(&value)
    }
}

impl Display for Tier {
    fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> {
        match *self {
            Tier::Barony => write!(f, "barony"),
            Tier::County => write!(f, "county"),
            Tier::Duchy => write!(f, "duchy"),
            Tier::Kingdom => write!(f, "kingdom"),
            Tier::Empire => write!(f, "empire"),
            Tier::Hegemony => write!(f, "hegemony"),
        }
    }
}

impl Titles {
    pub fn load_item(
        &mut self,
        key: Token,
        block: &Block,
        parent: Option<&'static str>,
        is_county_capital: bool,
    ) {
        if let Some(other) = self.titles.get(key.as_str())
            && other.key.loc.kind >= key.loc.kind
        {
            dup_error(&key, &other.key, "title");
        }
        let title = Arc::new(Title::new(key.clone(), block.clone(), parent, is_county_capital));
        self.titles.insert(key.as_str(), Arc::clone(&title));

        let parent_tier = Tier::try_from(&key).unwrap(); // guaranteed by caller
        if parent_tier == Tier::Barony {
            if let Some(provid) = block.get_field_integer("province") {
                if let Ok(provid) = ProvId::try_from(provid) {
                    self.baronies.insert(provid, title);
                } else {
                    err(ErrorKey::Validation)
                        .msg("province id out of range")
                        .loc(block.get_field_value("province").unwrap())
                        .push();
                }
            } else {
                err(ErrorKey::Validation).msg("barony without province id").loc(&key).push();
            }
        }

        let mut is_county_capital = parent_tier == Tier::County;
        for (k, block) in block.iter_definitions() {
            if let Ok(tier) = Tier::try_from(k) {
                if tier >= parent_tier {
                    let msg = format!("can't put a {tier} inside a {parent_tier}");
                    err(ErrorKey::TitleTier).msg(msg).loc(k).push();
                }
                self.load_item(k.clone(), block, Some(key.as_str()), is_county_capital);
                is_county_capital = false;
            }
        }
        if is_county_capital && !block.get_field_bool("landless").unwrap_or(false) {
            err(ErrorKey::Validation).msg("county with no baronies!").loc(key).push();
        }
    }

    pub fn scan_variables(&self, registry: &mut Variables) {
        for item in self.titles.values() {
            // Title blocks are nested, so the parent check is to avoid re-scanning subordinate titles.
            if item.parent.is_none() {
                registry.scan(&item.block);
            }
        }
    }

    pub fn exists(&self, key: &str) -> bool {
        self.titles.contains_key(key)
    }

    pub fn iter_keys(&self) -> impl Iterator<Item = &Token> {
        self.titles.values().map(|item| &item.key)
    }

    pub fn get(&self, key: &str) -> Option<Arc<Title>> {
        self.titles.get(key).cloned()
    }

    pub fn validate(&self, data: &Everything) {
        for item in self.titles.values() {
            item.validate(data);
        }
    }

    pub fn capital_of(&self, prov: ProvId) -> Option<&str> {
        self.baronies.get(&prov).and_then(|b| b.capital_of())
    }

    /// Return true iff the vassal title is in the de jure land of the liege title.
    /// A title is always in its own de jure.
    pub fn is_de_jure(&self, liege: &str, vassal: &str) -> bool {
        let mut key = vassal;
        while let Some(title) = self.titles.get(key) {
            if key == liege {
                return true;
            }
            key = if let Some(parent) = title.parent {
                parent
            } else {
                return false;
            }
        }
        false
    }
}

impl FileHandler<Block> for Titles {
    fn subpath(&self) -> PathBuf {
        PathBuf::from("common/landed_titles")
    }

    fn load_file(&self, entry: &FileEntry, parser: &ParserMemory) -> Option<Block> {
        if !entry.filename().to_string_lossy().ends_with(".txt") {
            return None;
        }

        PdxFile::read(entry, parser)
    }

    fn handle_file(&mut self, _entry: &FileEntry, mut block: Block) {
        for (key, block) in block.drain_definitions_warn() {
            if Tier::try_from(&key).is_ok() {
                self.load_item(key, &block, None, false);
            } else {
                warn(ErrorKey::Validation).msg("expected title").loc(key).push();
            }
        }
    }
}

#[derive(Clone, Debug)]
pub struct Title {
    key: Token,
    block: Block,
    pub tier: Tier,
    pub parent: Option<&'static str>,
    is_county_capital: bool, // for baronies
}

impl Title {
    pub fn new(
        key: Token,
        block: Block,
        parent: Option<&'static str>,
        is_county_capital: bool,
    ) -> Self {
        let tier = Tier::try_from(&key).unwrap(); // guaranteed by caller
        Self { key, block, tier, parent, is_county_capital }
    }

    pub fn validate(&self, data: &Everything) {
        // NOTE: There used to be a check that non-barony titles existed in the
        // title history, but that seems to be optional.
        data.verify_exists(Item::Localization, &self.key);
        let loca = format!("{}_adj", &self.key);
        if self.tier > Tier::Barony {
            data.verify_exists_implied(Item::Localization, &loca, &self.key);
        } else {
            data.mark_used(Item::Localization, &loca);
        }
        // The _pre is rarely defined even in vanilla
        let loca = format!("{}_pre", &self.key);
        data.mark_used(Item::Localization, &loca);
        let definite_form = self.block.field_value_is("definite_form", "yes");
        if definite_form {
            data.localization.suggest(&format!("{}_article", &self.key), &self.key);
        }

        let mut vd = Validator::new(&self.block, data);
        let mut sc = ScopeContext::new(Scopes::Character, &self.key);

        vd.field_validated("color", validate_possibly_named_color);
        vd.advice_field("color2", "no longer used");
        vd.field_bool("figurehead");
        vd.field_bool("allow_domicile");
        vd.field_bool("definite_form");
        vd.field_bool("ruler_uses_title_name");
        vd.field_bool("can_be_named_after_dynasty");
        vd.field_bool("landless");
        vd.field_bool("require_landless");
        vd.field_bool("no_automatic_claims");
        vd.field_bool("always_follows_primary_heir");
        vd.field_bool("destroy_if_invalid_heir");
        vd.field_bool("destroy_on_succession");
        vd.field_bool("delete_on_destroy");
        vd.field_bool("delete_on_gain_same_tier");
        vd.field_bool("de_jure_drift_disabled");
        vd.field_bool("ignore_titularity_for_title_weighting");
        vd.field_bool("noble_family");
        vd.field_bool("can_use_nomadic_naming");

        let mut sc_entry = ScopeContext::new(Scopes::Character, &self.key);
        sc_entry.define_name("target", Scopes::Character, &self.key);
        sc_entry.define_name("title", Scopes::LandedTitle, &self.key);
        sc_entry.define_name("liege", Scopes::Character, &self.key);
        sc_entry.define_name("vassal", Scopes::Character, &self.key);
        vd.field_validated_sc("personal_relation_entry", &mut sc_entry, validate_desc);
        vd.field_trigger_rooted("personal_relation_vassal", Tooltipped::No, Scopes::Character);

        vd.advice_field(
            "male_names",
            "replaced with `holding_regnal_male_names` and `posthumous_regnal_male_names`",
        );
        vd.advice_field(
            "female_names",
            "replaced with `holding_regnal_female_names` and `posthumous_regnal_female_names`",
        );
        vd.field_list_items("holding_regnal_male_names", Item::Localization);
        vd.field_list_items("posthumous_regnal_male_names", Item::Localization);
        vd.field_list_items("holding_regnal_female_names", Item::Localization);
        vd.field_list_items("posthumous_regnal_female_names", Item::Localization);

        vd.field_bool("disable_regnal_numbers");
        vd.advice_field("enable_regnal_numbers", "replaced with `disable_regnal_numbers` in 1.18");

        if Tier::try_from(&self.key) == Ok(Tier::Barony) {
            // TODO: check that no two baronies have the same province
            vd.field_item("province", Item::Province);
        }

        vd.field_script_value_no_breakdown("ai_primary_priority", &mut sc);

        vd.field_trigger("can_create", Tooltipped::Yes, &mut sc);
        vd.field_trigger("can_create_on_partition", Tooltipped::No, &mut sc);
        vd.field_trigger("can_destroy", Tooltipped::Yes, &mut sc);

        vd.field_validated_block("cultural_names", |block, data| {
            let mut vd = Validator::new(block, data);
            vd.unknown_value_fields(|key, token| {
                data.verify_exists(Item::NameList, key);
                data.verify_exists(Item::Localization, token);
                let loca = format!("{token}_adj");
                data.mark_used(Item::Localization, &loca);
                if definite_form {
                    let loca = format!("{token}_article");
                    data.mark_used(Item::Localization, &loca);
                }
            });
        });

        let mut has_de_jure_land = false;
        // The blocks are validated by the next level Title
        vd.unknown_block_fields(|key, _| {
            if Tier::try_from(key).is_err() {
                let msg = format!("unknown field `{key}`");
                warn(ErrorKey::UnknownField).msg(msg).loc(key).push();
            } else {
                has_de_jure_land = true;
            }
        });
        // Check capital after the above loop, to have the has_de_jure_land result.
        if let Some(token) = vd.field_value("capital") {
            data.verify_exists(Item::Title, token);
            if Tier::try_from(token) != Ok(Tier::County) {
                let msg = "capital must be a county";
                err(ErrorKey::TitleTier).msg(msg).loc(token).push();
            } else if self.tier == Tier::Duchy
                && has_de_jure_land
                && !data.titles.is_de_jure(self.key.as_str(), token.as_str())
            {
                let msg = format!("capital `{token}` is not in de jure `{}`", self.key);
                warn(ErrorKey::TitleTier).msg(msg).loc(token).push();
            }
        }
    }

    fn capital_of(&self) -> Option<&str> {
        if self.is_county_capital { self.parent } else { None }
    }
}