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 crate::block::Block;
use crate::context::ScopeContext;
use crate::db::{Db, DbKind};
use crate::everything::Everything;
use crate::game::GameFlags;
use crate::item::{Item, ItemLoader};
use crate::modif::validate_modifs;
use crate::report::{ErrorKey, err};
use crate::scopes::Scopes;
use crate::token::Token;
use crate::tooltipped::Tooltipped;
use crate::validator::Validator;
use crate::vic3::modif::ModifKinds;

#[derive(Clone, Debug)]
pub struct TreatyArticle {}

inventory::submit! {
    ItemLoader::Normal(GameFlags::Vic3, Item::TreatyArticle, TreatyArticle::add)
}

impl TreatyArticle {
    pub fn add(db: &mut Db, key: Token, block: Block) {
        db.add(Item::TreatyArticle, key, block, Box::new(Self {}));
    }
}

impl DbKind for TreatyArticle {
    fn validate(&self, key: &Token, block: &Block, data: &Everything) {
        let mut vd = Validator::new(block, data);

        data.verify_exists(Item::Localization, key);
        data.verify_exists_implied(Item::Localization, &format!("{key}_desc"), key);
        data.verify_exists_implied(Item::Localization, &format!("{key}_effects_desc"), key);
        data.verify_exists_implied(Item::Localization, &format!("{key}_article_short_desc"), key);

        vd.req_field("kind");
        vd.field_choice("kind", &["directed", "mutual"]);
        let is_directed = block.get_field_value("kind").is_some_and(|k| k.is("directed"));

        vd.field_integer_range("cost", 0..);
        vd.field_numeric("relations_progress_per_day");
        vd.field_integer_range("relations_improvement_max", 0..);
        vd.field_integer_range("relations_improvement_min", 0..);

        vd.field_item("icon", Item::File);

        let flags = &[
            "is_alliance",
            "is_defensive_pact",
            "is_guarantee_independence",
            "is_support_independence",
            "is_host_power_bloc_embassy",
            "is_offer_embassy",
            "is_investment_rights",
            "is_join_power_bloc",
            "is_trade_privilege",
            "is_military_access",
            "is_military_assistance",
            "is_transit_rights",
            "is_non_colonization_agreement",
            "is_goods_transfer",
            "is_money_transfer",
            "is_monopoly_for_company",
            "is_prohibit_goods_trade_with_world_market",
            "is_no_tariffs",
            "is_no_subventions",
            "is_take_on_debt",
            "is_treaty_port",
            "is_law_commitment",
            "can_be_renegotiated",
            "can_be_enforced",
            "causes_state_transfer",
            "friendly",
            "giftable",
            "hostile",
            // undocumented
            "target_subjects_as_input",
        ];
        vd.field_list_choice("flags", flags);

        vd.field_choice(
            "usage_limit",
            &["once_per_treaty", "once_per_side", "once_per_side_with_same_inputs"],
        );

        if is_directed {
            vd.req_field("maintenance_paid_by");
            vd.field_choice("maintenance_paid_by", &["target_country", "source_country"]);
        } else {
            vd.ban_field("maintenance_paid_by", || "directed articles");
        }

        let required_inputs = &[
            "quantity",
            "goods",
            "state",
            "strategic_region",
            "company",
            "building_type",
            "law_type",
            "country",
        ];
        vd.field_list_choice("required_inputs", required_inputs);

        let required_inputs = block.get_field_list("required_inputs");
        let required_inputs = required_inputs.as_ref().map_or(&[][..], Vec::as_slice);
        for input in required_inputs {
            if input.is("quantity") {
                fn build_quantity_sc(key: &Token, is_directed: bool) -> ScopeContext {
                    let mut sc = build_article_sc(key, is_directed);
                    sc.define_name("other_country", Scopes::Country, key);
                    sc
                }

                vd.field_script_value_builder("quantity_min_value", |key| {
                    build_quantity_sc(key, is_directed)
                });
                vd.field_script_value_builder("quantity_max_value", |key| {
                    build_quantity_sc(key, is_directed)
                });
            } else {
                let valid_trigger = format!("{input}_valid_trigger");
                vd.field_trigger_builder(&valid_trigger, Tooltipped::No, |key| {
                    build_input_sc(key, input)
                });
            }
        }

        vd.field_list_items("mutual_exclusions", Item::TreatyArticle);
        vd.field_list_items("unlocked_by_technologies", Item::Technology);
        vd.field_list_items("automatically_support", Item::DiplomaticPlay);
        vd.field_validated_key_block("non_fulfillment", |key, block, data| {
            let mut vd = Validator::new(block, data);
            vd.field_choice("consequences", &["none", "withdraw", "freeze"]);
            let is_consequences_none =
                block.get_field_value("consequences").is_none_or(|t| t.is("none"));
            if is_consequences_none {
                vd.ban_field("max_consecutive_contraventions", || "withdraw/freeze");
                vd.ban_field("conditions", || "withdraw/freeze");
            } else if block
                .get_field_block("conditions")
                .is_none_or(|b| b.num_items() == 0 || b.field_value_is("always", "no"))
            {
                let msg = "at least one of the conditions triggers must be non-empty";
                err(ErrorKey::Validation).msg(msg).loc(key).push();
            }

            vd.field_integer_range("max_consecutive_contraventions", 0..);
            vd.field_validated_block("conditions", |block, data| {
                let mut vd = Validator::new(block, data);
                for interval in &["weekly", "monthly", "yearly"] {
                    vd.field_trigger_builder(interval, Tooltipped::No, |key| {
                        let mut sc = ScopeContext::new(Scopes::Country, key);
                        sc.define_name("article", Scopes::TreatyArticle, key);
                        sc
                    });
                }
            });
        });

        if !is_directed {
            vd.ban_field("source_modifier", || "directed");
            vd.ban_field("target_modifier", || "directed");
        }

        for modifier in &["source_modifier", "target_modifier", "mutual_modifier"] {
            vd.multi_field_validated_block(modifier, |block, data| {
                let vd = Validator::new(block, data);
                validate_modifs(block, data, ModifKinds::Country, vd);
            });
        }

        vd.field_trigger_rooted("visible", Tooltipped::No, Scopes::Country);
        vd.field_trigger_builder("possible", Tooltipped::Yes, |key| {
            let mut sc = ScopeContext::new(Scopes::Country, key);
            sc.define_name("other_country", Scopes::Country, key);
            sc
        });
        vd.multi_field_validated_block("requirement_to_maintain", |block, data| {
            let mut vd = Validator::new(block, data);
            vd.field_trigger_builder("trigger", Tooltipped::Yes, |key| {
                build_article_treaty_sc(key, is_directed)
            });
            vd.field_trigger_builder("show_about_to_break_warning", Tooltipped::No, |key| {
                build_article_treaty_sc(key, is_directed)
            });
        });
        vd.field_trigger_builder("can_ratify", Tooltipped::Yes, |key| {
            build_article_treaty_sc(key, is_directed)
        });

        for active in &["on_entry_into_force", "on_enforced"] {
            vd.field_effect_builder(active, Tooltipped::Yes, |key| {
                let mut sc = ScopeContext::new(Scopes::None, key);
                sc.define_name("treaty_options", Scopes::TreatyOptions, key);
                sc.define_name("article_options", Scopes::TreatyArticleOptions, key);
                sc
            });
        }

        vd.field_trigger_builder("can_withdraw", Tooltipped::Yes, |key| {
            let mut sc = ScopeContext::new(Scopes::None, key);
            sc.define_name("withdrawing_country", Scopes::Country, key);
            sc.define_name("non_withdrawing_country", Scopes::Country, key);

            if is_directed {
                sc.define_name("source_country", Scopes::Country, key);
                sc.define_name("target_country", Scopes::Country, key);
            } else {
                sc.define_name("first_country", Scopes::Country, key);
                sc.define_name("second_country", Scopes::Country, key);
            }
            sc
        });

        for deactive in &["on_withdrawal", "on_break"] {
            vd.field_effect_builder(deactive, Tooltipped::Yes, |key| {
                let mut sc = ScopeContext::new(Scopes::None, key);
                sc.define_name("treaty_options", Scopes::TreatyOptions, key);
                sc.define_name("article", Scopes::TreatyArticle, key);
                sc.define_name("withdrawing_country", Scopes::Country, key);
                sc.define_name("non_withdrawing_country", Scopes::Country, key);
                sc
            });
        }

        vd.field_validated_block("ai", |block, data| {
            validate_ai(block, data, required_inputs, is_directed);
        });

        vd.field_validated_block("wargoal", |block, data| {
            let mut vd = Validator::new(block, data);
            vd.field_integer("execution_priority");
            let contestion_types = &[
                "control_target_state",
                "control_target_country_capital",
                "control_any_target_country_state",
                "control_own_state",
                "control_own_capital",
                "control_all_own_states",
                "control_all_target_country_claims",
                "control_any_target_incorporated_state",
            ];
            vd.field_choice("contestion_type", contestion_types);

            for script_value in &["maneuvers", "infamy"] {
                vd.field_script_value_builder(script_value, |key| {
                    let mut sc = ScopeContext::new(Scopes::Country, key);
                    sc.define_name("target_country", Scopes::Country, key);
                    for input in required_inputs {
                        match input.as_str() {
                            "quantity" => sc.define_name("quantity", Scopes::Value, key),
                            "goods" => {
                                sc.define_name("goods", Scopes::Goods, key);
                                sc.define_name("market_goods", Scopes::MarketGoods, key);
                            }
                            "state" => sc.define_name("state", Scopes::State, key),
                            "strategic_region" => {
                                sc.define_name("region", Scopes::StrategicRegion, key);
                            }
                            "company" => sc.define_name("company", Scopes::Company, key),
                            "country" => sc.define_name("country", Scopes::Country, key),
                            // TODO: verify whether X or X_type scopes
                            "building_type" => sc.define_name("building", Scopes::Building, key),
                            "law_type" => sc.define_name("law", Scopes::Law, key),
                            _ => unreachable!(),
                        }
                    }
                    sc
                });
            }
        });
    }
}

fn build_input_sc(key: &Token, input: &Token) -> ScopeContext {
    let mut sc = ScopeContext::new(Scopes::Country, key);
    sc.define_name("other_country", Scopes::Country, key);
    sc.define_name("article", Scopes::TreatyArticle, key);
    let input_scope = match input.as_str() {
        "goods" => Scopes::Goods,
        "state" => Scopes::State,
        "strategic_region" => Scopes::StrategicRegion,
        "company" => Scopes::Company,
        "building_type" => Scopes::BuildingType,
        "law_type" => Scopes::LawType,
        "country" => Scopes::Country,
        _ => unreachable!(),
    };
    sc.define_name("input", input_scope, key);

    if input.is("goods") {
        sc.define_name("market_goods", Scopes::MarketGoods, key);
    }
    sc
}

fn build_article_sc(key: &Token, is_directed: bool) -> ScopeContext {
    let mut sc = ScopeContext::new(Scopes::Country, key);
    sc.define_name("article", Scopes::TreatyArticle, key);

    if is_directed {
        sc.define_name("source_country", Scopes::Country, key);
        sc.define_name("target_country", Scopes::Country, key);
    } else {
        sc.define_name("first_country", Scopes::Country, key);
        sc.define_name("second_country", Scopes::Country, key);
    }
    sc
}

fn build_article_treaty_sc(key: &Token, is_directed: bool) -> ScopeContext {
    let mut sc = build_article_sc(key, is_directed);
    sc.define_name("treaty", Scopes::Treaty, key);
    sc
}

fn validate_ai(block: &Block, data: &Everything, required_inputs: &[Token], is_directed: bool) {
    let mut vd = Validator::new(block, data);
    vd.field_script_value_rooted("evaluation_chance", Scopes::Country);

    for input in required_inputs {
        if input.is("quantity") {
            vd.field_script_value_builder("quantity_input_value", |key| {
                let mut sc = ScopeContext::new(Scopes::Country, key);
                sc.define_name("other_country", Scopes::Country, key);
                sc.define_name("article", Scopes::TreatyArticle, key);
                sc
            });
        } else {
            let filter = format!("{input}_input_filter");
            vd.field_trigger_builder(&filter, Tooltipped::No, |key| build_input_sc(key, input));
        }
    }

    vd.field_list_choice("article_ai_usage", &["offer", "request"]);
    let categories = &[
        "economy",
        "trade",
        "military",
        "military_defense",
        "ideology",
        "expansion",
        "power_bloc",
        "other",
        "none",
    ];
    vd.field_list_choice("treaty_categories", categories);
    vd.field_integer("combined_acceptance_cap_max");
    vd.field_integer("combined_acceptance_cap_min");
    vd.field_script_value_builder("inherent_accept_score", |key| {
        build_article_sc(key, is_directed)
    });
    vd.field_script_value_builder("contextual_accept_score", |key| {
        build_article_treaty_sc(key, is_directed)
    });
    vd.field_script_value_builder("proposal_weight", |key| build_article_sc(key, is_directed));

    vd.field_script_value_builder("wargoal_score_multiplier", |key| {
        build_article_sc(key, is_directed)
    });
}