tiger-lib 1.17.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::path::PathBuf;
use std::sync::RwLock;

use crate::block::{BV, Block};
use crate::context::ScopeContext;
use crate::everything::Everything;
use crate::fileset::{FileEntry, FileHandler};
use crate::helpers::{
    BANNED_NAMES, PrefixShould, TigerHashMap, dup_error, exact_dup_error, item_prefix_should,
};
use crate::item::Item;
use crate::parse::ParserMemory;
use crate::pdxfile::PdxFile;
use crate::report::{ErrorKey, err, warn};
use crate::scopes::Scopes;
use crate::script_value::{validate_non_dynamic_script_value, validate_script_value};
use crate::token::{Loc, Token};
use crate::variables::Variables;

#[derive(Debug, Default)]
pub struct ScriptValues {
    scope_overrides: TigerHashMap<&'static str, Scopes>,
    script_values: TigerHashMap<&'static str, ScriptValue>,
}

impl ScriptValues {
    fn load_item(&mut self, key: &Token, bv: &BV) {
        if let Some(other) = self.script_values.get(key.as_str()) {
            if other.key.loc.kind >= key.loc.kind {
                if other.bv.equivalent(bv) {
                    exact_dup_error(key, &other.key, "script value");
                } else {
                    dup_error(key, &other.key, "script value");
                }
            }
        }
        if BANNED_NAMES.contains(&key.as_str()) {
            let msg = "scriptedvalue has the same name as an important builtin";
            err(ErrorKey::NameConflict).strong().msg(msg).loc(key).push();
        } else {
            match item_prefix_should(Item::ScriptValue, key, |key| {
                self.script_values.get(key).map(|entry| &entry.key)
            }) {
                PrefixShould::Insert(name) => {
                    let scope_override = self.scope_overrides.get(name.as_str()).copied();
                    self.script_values
                        .insert(name.as_str(), ScriptValue::new(name, bv.clone(), scope_override));
                }
                #[cfg(any(feature = "vic3", feature = "eu5"))]
                PrefixShould::Inject(name) => match bv {
                    BV::Value(value) => {
                        let msg = "cannot inject a simple value";
                        err(ErrorKey::Prefixes).msg(msg).loc(value).push();
                    }
                    BV::Block(block) => {
                        self.script_values.entry(name.as_str()).and_modify(
                            |entry| match &mut entry.bv {
                                BV::Value(value) => {
                                    let msg = "cannot inject into a simple value";
                                    err(ErrorKey::Prefixes)
                                        .msg(msg)
                                        .loc(name)
                                        .loc_msg(&*value, "into here")
                                        .push();
                                }
                                BV::Block(old_block) => {
                                    old_block.append(&mut block.clone());
                                }
                            },
                        );
                    }
                },
                PrefixShould::Ignore => (),
            }
        }
    }

    pub fn scan_variables(&self, registry: &mut Variables) {
        for item in self.script_values.values() {
            if let Some(block) = &item.bv.get_block() {
                registry.scan(block);
            }
        }
    }

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

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

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

    pub fn validate_call(&self, key: &Token, data: &Everything, sc: &mut ScopeContext) {
        if let Some(item) = self.script_values.get(key.as_str()) {
            item.validate_call(key, data, sc);
        }
    }

    pub fn validate_non_dynamic_call(&self, key: &Token, data: &Everything) {
        if let Some(item) = self.script_values.get(key.as_str()) {
            item.validate_non_dynamic_call(data);
        }
    }
}

impl FileHandler<Block> for ScriptValues {
    fn config(&mut self, config: &Block) {
        if let Some(block) = config.get_field_block("scope_override") {
            for (key, token) in block.iter_assignments() {
                let mut scopes = Scopes::empty();
                if token.lowercase_is("all") {
                    scopes = Scopes::all();
                } else {
                    for part in token.split('|') {
                        if let Some(scope) = Scopes::from_snake_case(part.as_str()) {
                            scopes |= scope;
                        } else {
                            let msg = format!("unknown scope type `{part}`");
                            warn(ErrorKey::Config).msg(msg).loc(part).push();
                        }
                    }
                }
                self.scope_overrides.insert(key.as_str(), scopes);
            }
        }
    }

    fn subpath(&self) -> PathBuf {
        PathBuf::from("common/script_values")
    }

    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, block: Block) {
        for (key, bv) in block.iter_assignments_and_definitions_warn() {
            self.load_item(key, bv);
        }
    }
}

#[derive(Debug)]
pub struct ScriptValue {
    key: Token,
    bv: BV,
    cache: RwLock<TigerHashMap<Loc, ScopeContext>>,
    scope_override: Option<Scopes>,
}

impl ScriptValue {
    pub fn new(key: Token, bv: BV, scope_override: Option<Scopes>) -> Self {
        Self { key, bv, cache: RwLock::new(TigerHashMap::default()), scope_override }
    }

    pub fn cached_compat(&self, key: &Token, sc: &mut ScopeContext, data: &Everything) -> bool {
        if let Some(our_sc) = self.cache.read().unwrap().get(&key.loc) {
            sc.expect_compatibility(our_sc, key, data);
            true
        } else {
            false
        }
    }

    pub fn validate(&self, data: &Everything) {
        // For some reason, script values can be set to bools as well
        if let Some(token) = self.bv.get_value() {
            if token.is("yes") || token.is("no") {
                return;
            }
        }
        let mut sc = ScopeContext::new_unrooted(Scopes::all(), &self.key);
        sc.set_strict_scopes(false);
        if self.scope_override.is_some() {
            sc.set_no_warn(true);
        }
        self.validate_call(&self.key, data, &mut sc);
    }

    pub fn validate_call(&self, key: &Token, data: &Everything, sc: &mut ScopeContext) {
        if !self.cached_compat(key, sc, data) {
            let mut our_sc = ScopeContext::new_unrooted(Scopes::all(), &self.key);
            our_sc.set_strict_scopes(false);
            if self.scope_override.is_some() {
                our_sc.set_no_warn(true);
            }
            self.cache.write().unwrap().insert(key.loc, our_sc.clone());
            validate_script_value(&self.bv, data, &mut our_sc);
            if let Some(scopes) = self.scope_override {
                our_sc = ScopeContext::new_unrooted(scopes, key);
                our_sc.set_strict_scopes(false);
            }
            sc.expect_compatibility(&our_sc, key, data);
            self.cache.write().unwrap().insert(key.loc, our_sc);
        }
    }

    pub fn validate_non_dynamic_call(&self, data: &Everything) {
        validate_non_dynamic_script_value(&self.bv, data);
    }
}