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::{BV, Block};
use crate::ck3::validate::{
    validate_ai_targets, validate_cost, validate_quick_trigger, validate_theme_background,
};
use crate::context::ScopeContext;
use crate::db::{Db, DbKind};
use crate::desc::validate_desc;
use crate::effect::validate_effect;
use crate::everything::Everything;
use crate::game::GameFlags;
use crate::item::{Item, ItemLoader};
use crate::report::{ErrorKey, warn};
use crate::scopes::Scopes;
use crate::token::Token;
use crate::tooltipped::Tooltipped;
use crate::trigger::{validate_target, validate_trigger};
use crate::validate::{validate_ai_chance, validate_duration, validate_modifiers_with_base};
use crate::validator::Validator;

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

inventory::submit! {
    ItemLoader::Normal(GameFlags::Ck3, Item::CharacterInteraction, CharacterInteraction::add)
}

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

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

        // You're expected to use scope:actor and scope:recipient instead of root
        let mut sc = ScopeContext::new(Scopes::None, key);
        sc.define_name("actor", Scopes::Character, key);
        sc.define_name("recipient", Scopes::Character, key);
        sc.define_name("hook", Scopes::Bool, key);
        // TODO: figure out when these are available
        sc.define_name("secondary_actor", Scopes::Character, key);
        sc.define_name("secondary_recipient", Scopes::Character, key);
        sc.define_name("intermediary", Scopes::Character, key);
        // TODO: figure out if there's a better way than exhaustively matching on "interface" and "special_interaction"
        if let Some(target_type) = block.get_field_value("target_type") {
            if target_type.is("artifact") {
                sc.define_name("target", Scopes::Artifact, target_type);
            } else if target_type.is("title") {
                sc.define_name("target", Scopes::LandedTitle, target_type);
                sc.define_name("landed_title", Scopes::LandedTitle, target_type);
            }
        } else if let Some(interface) = block.get_field_value("interface") {
            if interface.is("interfere_in_war") || interface.is("call_ally") {
                sc.define_name("target", Scopes::War, interface);
            } else if interface.is("blackmail") {
                sc.define_name("target", Scopes::Secret, interface);
            } else if interface.is("council_task_interaction") {
                sc.define_name("target", Scopes::CouncilTask, interface);
            } else if interface.is("create_claimant_faction_against") {
                sc.define_name("landed_title", Scopes::LandedTitle, interface);
            } else if interface.is("modify_vassal_contract") {
                sc.define_list("changed_obligations", Scopes::VassalObligationLevel, interface);
            }
        } else if let Some(special) = block.get_field_value("special_interaction") {
            if special.is("invite_to_council_interaction") {
                sc.define_name("target", Scopes::CouncilTask, special);
            } else if special.is("end_war_attacker_victory_interaction")
                || special.is("end_war_attacker_defeat_interaction")
                || special.is("end_war_white_peace_interaction")
            {
                sc.define_name("war", Scopes::War, special);
            } else if special.is("remove_scheme_interaction")
                || special.is("invite_to_scheme_interaction")
            {
                sc.define_name("scheme", Scopes::Scheme, special);
            }
        }
        for block in block.get_field_blocks("send_option") {
            if let Some(token) = block.get_field_value("flag") {
                sc.define_name(token.as_str(), Scopes::Bool, token);
            }
        }

        vd.field_validated_block("localization_values", |block, data| {
            let mut vd = Validator::new(block, data);
            vd.unknown_value_fields(|key, value| {
                let scopes = validate_target(value, data, &mut sc, Scopes::all());
                sc.define_name(key.as_str(), scopes, value);
            });
        });

        // Validate this early, to update the saved scopes in `sc`
        // TODO: figure out when exactly `redirect` is run
        vd.field_effect("redirect", Tooltipped::No, &mut sc);

        vd.field_bool("ai_instant_response");
        // Let ai_set_target set scope:target if it wants
        vd.field_effect("ai_set_target", Tooltipped::No, &mut sc);
        vd.multi_field_validated_block("ai_targets", validate_ai_targets);
        vd.field_validated_block("ai_target_quick_trigger", validate_quick_trigger);

        vd.field_numeric("interface_priority");
        vd.field_bool("common_interaction");
        if !block.get_field_bool("hidden").unwrap_or(false) {
            vd.req_field("category");
        }
        vd.field_item("category", Item::CharacterInteractionCategory);

        if !vd.multi_field_validated_sc("icon", &mut sc, validate_icon) {
            data.mark_used_icon("NGameIcons|CHARACTER_INTERACTION_ICON_PATH", key, ".dds");
        }
        vd.field_icon("alert_icon", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");
        vd.field_icon("icon_small", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");

        vd.field_validated_key("override_background", |key, bv, data| {
            let mut sc = ScopeContext::new(Scopes::Character, key);
            validate_theme_background(bv, data, &mut sc);
        });

        vd.field_trigger("is_highlighted", Tooltipped::No, &mut sc.clone());
        vd.field_validated_sc("highlighted_reason", &mut sc, validate_desc);

        vd.field_value("special_interaction");
        vd.field_value("special_ai_interaction");

        vd.field_bool("ai_intermediary_maybe");
        vd.field_bool("ai_maybe");
        vd.field_integer("ai_min_reply_days");
        vd.field_integer("ai_max_reply_days");

        vd.field_value("interface"); // TODO
        vd.field_list_choice(
            "custom_character_sort",
            &["candidate_score", "governor_efficiency", "obedience", "merit"],
        );
        vd.field_item("scheme", Item::Scheme);
        vd.field_bool("popup_on_receive");
        vd.field_bool("pause_on_receive");
        vd.field_bool("force_notification");
        vd.field_bool("ai_accept_negotiation");
        vd.field_bool("secondary_scopes_optional");

        vd.field_bool("hidden");

        vd.field_validated_sc("use_diplomatic_range", &mut sc.clone(), validate_bool_or_trigger);
        vd.field_bool("can_send_despite_rejection");
        vd.field_bool("ignores_pending_interaction_block");

        // The cooldowns seem to be in actor scope
        vd.field_validated_block_rerooted("cooldown", &sc, Scopes::Character, validate_duration);
        vd.field_validated_block_rerooted(
            "cooldown_against_recipient",
            &sc,
            Scopes::Character,
            validate_duration,
        );
        // undocumented, but used in marriage interaction
        vd.field_validated_block_rerooted(
            "recipient_recieve_cooldown",
            &sc,
            Scopes::Character,
            validate_duration,
        );
        vd.field_validated_block_rerooted(
            "category_cooldown",
            &sc,
            Scopes::Character,
            validate_duration,
        );
        vd.field_validated_block_rerooted(
            "category_cooldown_against_recipient",
            &sc,
            Scopes::Character,
            validate_duration,
        );

        vd.field_validated_block_rerooted(
            "ignore_recipient_recieve_cooldown",
            &sc,
            Scopes::Character,
            |block, data, sc| {
                validate_trigger(block, data, sc, Tooltipped::No);
            },
        );

        // TODO: The ai_ name check is a heuristic. It would be better to check if the
        // is_shown trigger requires scope:actor to be is_ai = yes. But that's a long way off.
        if !key.as_str().starts_with("ai_") && !block.get_field_bool("hidden").unwrap_or(false) {
            if !block.has_key("name") {
                data.verify_exists(Item::Localization, key);
            }
            if !block.has_key("desc") {
                data.localization.suggest(&format!("{key}_desc"), key);
            }
        }
        vd.field_validated_value("extra_icon", |k, mut vd| {
            vd.item(Item::File);
            let loca = format!("{key}_extra_icon");
            data.verify_exists_implied(Item::Localization, &loca, k);
        });
        vd.field_trigger("should_use_extra_icon", Tooltipped::No, &mut sc.clone());
        vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
        vd.field_trigger("is_valid", Tooltipped::Yes, &mut sc.clone());
        vd.field_trigger(
            "is_valid_showing_failures_only",
            Tooltipped::FailuresOnly,
            &mut sc.clone(),
        );
        vd.field_trigger(
            "has_valid_target_showing_failures_only",
            Tooltipped::FailuresOnly,
            &mut sc.clone(),
        );
        vd.field_trigger("has_valid_target", Tooltipped::Yes, &mut sc.clone());

        vd.field_trigger("can_send", Tooltipped::Yes, &mut sc.clone());
        vd.field_trigger("can_be_blocked", Tooltipped::Yes, &mut sc.clone());

        vd.field_validated_key_block("populate_actor_list", |k, block, data| {
            // TODO: this loca check and the one for recipient_secondary have a lot of false positives in vanilla.
            // Not sure why.
            let loca = format!("actor_secondary_{key}");
            data.verify_exists_implied(Item::Localization, &loca, k);
            validate_effect(block, data, &mut sc.clone(), Tooltipped::No);
        });
        vd.field_validated_key_block("populate_recipient_list", |k, block, data| {
            let loca = format!("recipient_secondary_{key}");
            data.verify_exists_implied(Item::Localization, &loca, k);
            validate_effect(block, data, &mut sc.clone(), Tooltipped::No);
        });

        vd.multi_field_validated_block("send_option", |b, data| {
            let mut vd = Validator::new(b, data);
            vd.req_field("flag");
            // If localization field is not set, then flag is used as the localization
            if vd.field_localization("localization", &mut sc) {
                vd.field_value("flag");
            } else {
                vd.field_localization("flag", &mut sc);
            }
            vd.field_trigger("is_shown", Tooltipped::No, &mut sc.clone());
            vd.field_trigger("is_valid", Tooltipped::FailuresOnly, &mut sc.clone());
            vd.field_trigger("starts_enabled", Tooltipped::No, &mut sc.clone());
            vd.field_trigger("can_be_changed", Tooltipped::No, &mut sc.clone());
            vd.field_validated_sc("current_description", &mut sc.clone(), validate_desc);
            vd.field_bool("can_invalidate_interaction");

            // undocumented

            vd.field_script_value("scheme_preview_success_chance", &mut sc);
            vd.field_script_value("scheme_preview_success_chance_max", &mut sc);
            vd.field_script_value("scheme_preview_speed", &mut sc);
        });

        vd.field_bool("send_options_exclusive");
        vd.field_effect("on_send", Tooltipped::Yes, &mut sc);
        vd.field_effect("on_accept", Tooltipped::Yes, &mut sc);
        vd.field_effect("on_decline", Tooltipped::Yes, &mut sc);
        vd.field_effect("on_blocked_effect", Tooltipped::No, &mut sc);
        vd.field_effect("pre_auto_accept", Tooltipped::No, &mut sc);
        vd.field_effect("on_auto_accept", Tooltipped::Yes, &mut sc);
        vd.field_effect("on_intermediary_accept", Tooltipped::Yes, &mut sc);
        vd.field_effect("on_intermediary_decline", Tooltipped::Yes, &mut sc);

        vd.field_integer("ai_frequency"); // months
        vd.field_validated_key_block("ai_frequency_by_tier", |key, b, data| {
            let mut vd = Validator::new(b, data);
            for tier in &["barony", "county", "duchy", "kingdom", "empire", "hegemony"] {
                vd.req_field(tier);
                vd.field_integer(tier);
            }
            if block.has_key("ai_frequency") {
                let msg = "must not have both `ai_frequency` and `ai_frequency_by_tier`";
                warn(ErrorKey::Validation).msg(msg).loc(key).push();
            }
        });

        // This is in character scope with no other named scopes builtin
        vd.field_trigger_rooted("ai_potential", Tooltipped::Yes, Scopes::Character);
        if let Some(token) = block.get_key("ai_potential") {
            if block.get_field_integer("ai_frequency").unwrap_or(0) == 0
                && !key.is("revoke_title_interaction")
                && !block.has_key("ai_frequency_by_tier")
            {
                let msg = "`ai_potential` will not be used if `ai_frequency` is 0";
                warn(ErrorKey::Unneeded).msg(msg).loc(token).push();
            }
            let msg = "should use `is_available` instead of `ai_potential`";
            warn(ErrorKey::Deprecated).msg(msg).loc(token).push();
        }
        vd.field_trigger_rooted("is_available", Tooltipped::Yes, Scopes::Character);
        vd.field_validated_sc("ai_intermediary_accept", &mut sc.clone(), validate_ai_chance);

        // These seem to be in character scope
        vd.field_validated_block_rerooted(
            "ai_accept",
            &sc,
            Scopes::Character,
            validate_modifiers_with_base,
        );
        vd.field_validated_block_rerooted(
            "ai_will_do",
            &sc,
            Scopes::Character,
            validate_modifiers_with_base,
        );

        vd.field_validated_sc("name", &mut sc.clone(), validate_desc);
        vd.field_validated_sc("desc", &mut sc.clone(), validate_desc);
        vd.field_choice("greeting", &["negative", "positive"]);
        vd.field_validated_sc("prompt", &mut sc.clone(), validate_desc);
        vd.field_validated_sc("intermediary_notification_text", &mut sc.clone(), validate_desc);
        vd.field_validated_sc("notification_text", &mut sc.clone(), validate_desc);
        vd.field_validated_sc("on_decline_summary", &mut sc.clone(), validate_desc);
        vd.field_localization("answer_block_key", &mut sc);
        vd.field_localization("answer_accept_key", &mut sc);
        vd.field_localization("answer_reject_key", &mut sc);
        vd.field_localization("answer_acknowledge_key", &mut sc);
        vd.field_localization("options_heading", &mut sc);
        vd.field_localization("pre_answer_maybe_breakdown_key", &mut sc);
        vd.field_localization("pre_answer_no_breakdown_key", &mut sc);
        vd.field_localization("pre_answer_yes_breakdown_key", &mut sc);
        vd.field_localization("pre_answer_maybe_key", &mut sc);
        vd.field_localization("pre_answer_no_key", &mut sc);
        vd.field_localization("pre_answer_yes_key", &mut sc);
        vd.field_localization("intermediary_breakdown_maybe", &mut sc);
        vd.field_localization("intermediary_breakdown_no", &mut sc);
        vd.field_localization("intermediary_breakdown_yes", &mut sc);
        vd.field_localization("intermediary_answer_accept_key", &mut sc);
        vd.field_localization("intermediary_answer_reject_key", &mut sc);
        vd.field_localization("reply_item_key", &mut sc);
        vd.field_localization("send_name", &mut sc);

        vd.field_bool("needs_recipient_to_open");
        vd.field_bool("show_effects_in_notification");
        vd.field_bool("diarch_interaction");
        vd.field_validated_sc("auto_accept", &mut sc.clone(), validate_bool_or_trigger);

        vd.field_choice(
            "target_type",
            &["artifact", "title", "men_at_arms", "court_position_type", "count"],
        );
        vd.field_value("target_filter"); // TODO

        // root is the character being picked
        vd.field_validated_block_rerooted(
            "can_be_picked",
            &sc,
            Scopes::Character,
            |block, data, sc| {
                validate_trigger(block, data, sc, Tooltipped::Yes);
            },
        );
        vd.field_trigger("can_be_picked_title", Tooltipped::Yes, &mut sc.clone());
        vd.field_trigger("can_be_picked_artifact", Tooltipped::Yes, &mut sc.clone());
        vd.field_trigger("can_be_picked_regiment", Tooltipped::Yes, &mut sc.clone());

        vd.field_trigger("needs_confirmation", Tooltipped::No, &mut sc.clone());

        // Experimentation showed that even the cost block has scope none
        vd.field_validated_block_rerooted("cost", &sc, Scopes::None, validate_cost);

        vd.field_list("filter_tags");

        // undocumented

        vd.field_bool("shows_military_strength");
    }
}

fn validate_bool_or_trigger(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
    match bv {
        BV::Value(t) => {
            if !t.is("yes") && !t.is("no") {
                warn(ErrorKey::Validation).msg("expected yes or no").loc(t).push();
            }
        }
        BV::Block(b) => {
            validate_trigger(b, data, sc, Tooltipped::No);
        }
    }
}

fn validate_icon(bv: &BV, data: &Everything, sc: &mut ScopeContext) {
    match bv {
        BV::Value(token) => {
            data.verify_icon("NGameIcons|CHARACTER_INTERACTION_ICON_PATH", token, ".dds");
        }
        BV::Block(block) => {
            let mut vd = Validator::new(block, data);
            vd.req_field("reference");

            vd.field_trigger("trigger", Tooltipped::No, sc);
            vd.field_icon("reference", "NGameIcons|CHARACTER_INTERACTION_ICON_PATH", ".dds");
        }
    }
}