use crate::block::{Block, Comparator, Eq::*, BV};
use crate::context::{Reason, ScopeContext};
#[cfg(feature = "jomini")]
use crate::data::effect_localization::EffectLocalization;
use crate::desc::validate_desc;
use crate::everything::Everything;
use crate::game::Game;
#[cfg(feature = "hoi4")]
use crate::hoi4::variables::validate_variable;
use crate::item::Item;
use crate::lowercase::Lowercase;
use crate::report::{err, fatal, tips, warn, ErrorKey, Severity};
use crate::scopes::{scope_iterator, Scopes};
#[cfg(feature = "jomini")]
use crate::script_value::validate_script_value;
use crate::token::Token;
use crate::tooltipped::Tooltipped;
#[cfg(any(feature = "ck3", feature = "imperator"))]
use crate::trigger::validate_target_ok_this;
use crate::trigger::{validate_target, validate_trigger};
#[cfg(any(feature = "ck3", feature = "vic3"))]
use crate::validate::validate_compare_duration;
#[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
use crate::validate::validate_modifiers;
#[cfg(feature = "jomini")]
use crate::validate::validate_scripted_modifier_call;
use crate::validate::{
precheck_iterator_fields, validate_identifier, validate_ifelse_sequence,
validate_inside_iterator, validate_iterator_fields, validate_scope_chain, ListType,
};
use crate::validator::{Validator, ValueValidator};
pub fn validate_effect(
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
tooltipped: Tooltipped,
) {
let mut vd = Validator::new(block, data);
validate_effect_internal(
Lowercase::empty(),
ListType::None,
block,
data,
sc,
&mut vd,
tooltipped,
);
}
pub fn validate_effect_internal(
caller: &Lowercase,
list_type: ListType,
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
vd: &mut Validator,
mut tooltipped: Tooltipped,
) {
vd.set_case_sensitive(false);
if caller == "if"
|| caller == "else_if"
|| caller == "else"
|| caller == "while"
|| list_type != ListType::None
{
vd.field_validated_key_block("limit", |key, block, data| {
if caller == "else" {
let msg = "`else` with a `limit` does work, but may indicate a mistake";
let info = "normally you would use `else_if` instead.";
tips(ErrorKey::IfElse).msg(msg).info(info).loc(key).push();
}
validate_trigger(block, data, sc, Tooltipped::No);
});
} else {
vd.ban_field("limit", || "if/else_if or lists");
}
#[allow(clippy::if_not_else)] if list_type != ListType::None {
vd.field_trigger("filter", Tooltipped::No, sc);
} else {
vd.ban_field("filter", || "lists");
}
validate_iterator_fields(caller, list_type, data, sc, vd, &mut tooltipped, false);
if list_type != ListType::None {
validate_inside_iterator(caller, list_type, block, data, sc, vd, tooltipped);
}
validate_ifelse_sequence(block, "if", "else_if", "else");
vd.set_allow_questionmark_equals(true);
vd.unknown_fields_cmp(|key, cmp, bv| {
validate_effect_field(caller, key, cmp, bv, data, sc, tooltipped);
});
}
#[allow(unused_variables)] pub fn validate_effect_field(
caller: &Lowercase,
key: &Token,
cmp: Comparator,
bv: &BV,
data: &Everything,
sc: &mut ScopeContext,
tooltipped: Tooltipped,
) {
if let Some(effect) = data.get_effect(key) {
match bv {
BV::Value(token) => {
if !effect.macro_parms().is_empty() {
fatal(ErrorKey::Macro).msg("expected macro arguments").loc(token).push();
} else if !token.is("yes") {
warn(ErrorKey::Validation).msg("expected just effect = yes").loc(token).push();
}
effect.validate_call(key, data, sc, tooltipped);
}
BV::Block(block) => {
let parms = effect.macro_parms();
if parms.is_empty() {
err(ErrorKey::Macro)
.msg("this scripted effect does not need macro arguments")
.info("you can just use it as effect = yes")
.loc(block)
.push();
} else {
let mut vec = Vec::new();
let mut vd = Validator::new(block, data);
for parm in &parms {
if let Some(token) = vd.field_value(parm) {
vec.push(token.clone());
} else {
let msg = format!("this scripted effect needs parameter {parm}");
err(ErrorKey::Macro).msg(msg).loc(block).push();
return;
}
}
vd.unknown_value_fields(|key, _value| {
let msg = format!("this scripted effect does not need parameter {key}");
let info = "supplying an unneeded parameter often causes a crash";
fatal(ErrorKey::Macro).msg(msg).info(info).loc(key).push();
});
let args: Vec<_> = parms.into_iter().zip(vec).collect();
effect.validate_macro_expansion(key, &args, data, sc, tooltipped);
}
}
}
return;
}
#[cfg(feature = "jomini")]
if Game::is_jomini() {
if let Some(modifier) = data.scripted_modifiers.get(key.as_str()) {
if caller != "random" && caller != "random_list" && caller != "duel" {
let msg = "cannot use scripted modifier here";
err(ErrorKey::Validation).msg(msg).loc(key).push();
return;
}
validate_scripted_modifier_call(key, bv, modifier, data, sc);
return;
}
}
let scope_effect = match Game::game() {
#[cfg(feature = "ck3")]
Game::Ck3 => crate::ck3::tables::effects::scope_effect,
#[cfg(feature = "vic3")]
Game::Vic3 => crate::vic3::tables::effects::scope_effect,
#[cfg(feature = "imperator")]
Game::Imperator => crate::imperator::tables::effects::scope_effect,
#[cfg(feature = "hoi4")]
Game::Hoi4 => crate::hoi4::tables::effects::scope_effect,
};
if let Some((inscopes, effect)) = scope_effect(key, data) {
sc.expect(inscopes, &Reason::Token(key.clone()));
match effect {
Effect::Yes => {
if let Some(token) = bv.expect_value() {
if !token.is("yes") {
let msg = format!("expected just `{key} = yes`");
warn(ErrorKey::Validation).msg(msg).loc(token).push();
}
}
}
Effect::Boolean => {
if let Some(token) = bv.expect_value() {
validate_target(token, data, sc, Scopes::Bool);
}
}
Effect::Integer => {
if let Some(token) = bv.expect_value() {
token.expect_integer();
}
}
Effect::ScriptValue | Effect::NonNegativeValue => {
if let Some(token) = bv.get_value() {
if let Some(number) = token.get_number() {
if matches!(effect, Effect::NonNegativeValue) && number < 0.0 {
if key.is("add_gold") {
let msg = "add_gold does not take negative numbers";
let info = "try remove_short_term_gold instead";
warn(ErrorKey::Range).msg(msg).info(info).loc(token).push();
} else {
let msg = format!("{key} does not take negative numbers");
warn(ErrorKey::Range).msg(msg).loc(token).push();
}
}
}
}
#[cfg(feature = "jomini")]
if Game::is_jomini() {
validate_script_value(bv, data, sc);
}
}
#[cfg(feature = "vic3")]
Effect::Date => {
if let Some(token) = bv.expect_value() {
token.expect_date();
}
}
Effect::Scope(outscopes) => {
if let Some(token) = bv.expect_value() {
validate_target(token, data, sc, outscopes);
}
}
#[cfg(any(feature = "ck3", feature = "imperator"))]
Effect::ScopeOkThis(outscopes) => {
if let Some(token) = bv.expect_value() {
validate_target_ok_this(token, data, sc, outscopes);
}
}
Effect::Item(itype) => {
if let Some(token) = bv.expect_value() {
data.verify_exists(itype, token);
}
}
Effect::ScopeOrItem(outscopes, itype) => {
if let Some(token) = bv.expect_value() {
if !data.item_exists(itype, token.as_str()) {
validate_target(token, data, sc, outscopes);
}
}
}
#[cfg(feature = "ck3")]
Effect::Target(key, outscopes) => {
if let Some(block) = bv.expect_block() {
let mut vd = Validator::new(block, data);
vd.set_case_sensitive(false);
vd.req_field(key);
vd.field_target(key, sc, outscopes);
}
}
#[cfg(any(feature = "ck3", feature = "vic3"))]
Effect::TargetValue(key, outscopes, valuekey) => {
if let Some(block) = bv.expect_block() {
let mut vd = Validator::new(block, data);
vd.set_case_sensitive(false);
vd.req_field(key);
vd.req_field(valuekey);
vd.field_target(key, sc, outscopes);
vd.field_script_value(valuekey, sc);
}
}
#[cfg(any(feature = "ck3", feature = "hoi4"))]
Effect::ItemTarget(ikey, itype, tkey, outscopes) => {
if let Some(block) = bv.expect_block() {
let mut vd = Validator::new(block, data);
vd.set_case_sensitive(false);
vd.field_item(ikey, itype);
vd.field_target(tkey, sc, outscopes);
}
}
#[cfg(feature = "ck3")]
Effect::ItemValue(key, itype) => {
if let Some(block) = bv.expect_block() {
let mut vd = Validator::new(block, data);
vd.set_case_sensitive(false);
vd.req_field(key);
vd.req_field("value");
vd.field_item(key, itype);
vd.field_script_value("value", sc);
}
}
Effect::Choice(choices) => {
if let Some(token) = bv.expect_value() {
if !choices.contains(&token.as_str()) {
let msg = format!("expected one of {}", choices.join(", "));
err(ErrorKey::Choice).msg(msg).loc(token).push();
}
}
}
#[cfg(feature = "ck3")]
Effect::Desc => validate_desc(bv, data, sc),
#[cfg(any(feature = "ck3", feature = "vic3"))]
Effect::Timespan => {
if let Some(block) = bv.expect_block() {
validate_compare_duration(block, data, sc);
}
}
Effect::Vb(f) => {
if let Some(block) = bv.expect_block() {
let mut vd = Validator::new(block, data);
vd.set_case_sensitive(false);
f(key, block, data, sc, vd, tooltipped);
}
}
Effect::Vv(f) => {
if let Some(token) = bv.expect_value() {
let vd = ValueValidator::new(token, data);
f(key, vd, sc, tooltipped);
}
}
Effect::Vbv(f) => {
f(key, bv, data, sc, tooltipped);
}
Effect::ControlOrLabel => match bv {
BV::Value(t) => {
data.verify_exists(Item::Localization, t);
data.validate_localization_sc(t.as_str(), sc);
}
BV::Block(b) => {
validate_effect_control(&Lowercase::new(key.as_str()), b, data, sc, tooltipped);
}
},
Effect::Control => {
if let Some(block) = bv.expect_block() {
validate_effect_control(
&Lowercase::new(key.as_str()),
block,
data,
sc,
tooltipped,
);
}
}
#[cfg(feature = "hoi4")]
Effect::Iterator(ltype, outscope) => {
let it_name = key.split_once('_').unwrap().1;
if let Some(block) = bv.expect_block() {
precheck_iterator_fields(ltype, it_name.as_str(), block, data, sc);
sc.open_scope(outscope, key.clone());
let mut vd = Validator::new(block, data);
validate_effect_internal(
&Lowercase::new(it_name.as_str()),
ltype,
block,
data,
sc,
&mut vd,
tooltipped,
);
}
sc.close();
}
Effect::Identifier(kind) => {
if let Some(token) = bv.expect_value() {
validate_identifier(token, kind, Severity::Error);
}
}
#[cfg(feature = "hoi4")]
Effect::Value => {
if let Some(token) = bv.expect_value() {
validate_target(token, data, sc, Scopes::Value);
}
}
Effect::Removed(version, explanation) => {
let msg = format!("`{key}` was removed in {version}");
warn(ErrorKey::Removed).msg(msg).info(explanation).loc(key).push();
}
Effect::Unchecked => (),
#[cfg(any(feature = "ck3", feature = "vic3", feature = "hoi4"))]
Effect::UncheckedTodo => (),
}
return;
}
if let Some((it_type, it_name)) = key.split_once('_') {
if let Ok(ltype) = ListType::try_from(it_type.as_str()) {
if let Some((inscopes, outscope)) = scope_iterator(&it_name, data, sc) {
if ltype.is_for_triggers() {
let msg = format!("cannot use `{it_type}_` lists in an effect");
err(ErrorKey::Validation).msg(msg).loc(key).push();
return;
}
sc.expect(inscopes, &Reason::Token(key.clone()));
if let Some(b) = bv.expect_block() {
precheck_iterator_fields(ltype, it_name.as_str(), b, data, sc);
sc.open_scope(outscope, key.clone());
let mut vd = Validator::new(b, data);
validate_effect_internal(
&Lowercase::new(it_name.as_str()),
ltype,
b,
data,
sc,
&mut vd,
tooltipped,
);
}
sc.close();
return;
}
}
}
#[cfg(feature = "hoi4")]
if Game::is_hoi4() && key.starts_with("var:") {
validate_variable(key, data, sc, Severity::Error);
if let Some(block) = bv.expect_block() {
sc.open_scope(Scopes::all_but_none(), key.clone());
validate_effect(block, data, sc, tooltipped);
sc.close();
}
return;
}
sc.open_builder();
if validate_scope_chain(key, data, sc, matches!(cmp, Comparator::Equals(Question))) {
sc.finalize_builder();
if Game::is_ck3() && key.starts_with("flag:") {
let msg = "as of 1.9, flag literals cannot be used on the left-hand side";
err(ErrorKey::Scopes).msg(msg).loc(key).push();
}
if let Some(block) = bv.expect_block() {
validate_effect(block, data, sc, tooltipped);
}
}
sc.close();
}
pub fn validate_effect_control(
caller: &Lowercase,
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
mut tooltipped: Tooltipped,
) {
let mut vd = Validator::new(block, data);
if caller == "if" || caller == "else_if" {
vd.req_field_warn("limit");
}
#[cfg(feature = "jomini")]
if Game::is_jomini()
&& (caller == "custom_description"
|| caller == "custom_description_no_bullet"
|| caller == "custom_tooltip"
|| caller == "custom_label")
{
vd.req_field("text");
if caller == "custom_tooltip" || caller == "custom_label" {
vd.field_item("text", Item::Localization);
if let Some(value) = block.get_field_value("text") {
data.validate_localization_sc(value.as_str(), sc);
}
} else if let Some(token) = vd.field_value("text") {
data.verify_exists(Item::EffectLocalization, token);
if let Some((key, block)) = data.get_key_block(Item::EffectLocalization, token.as_str())
{
EffectLocalization::validate_use(key, block, data, token, tooltipped);
}
}
vd.field_target_ok_this("subject", sc, Scopes::non_primitive());
tooltipped = Tooltipped::No;
} else {
vd.ban_field("text", || "`custom_description` or `custom_tooltip`");
vd.ban_field("subject", || "`custom_description` or `custom_tooltip`");
}
#[cfg(feature = "jomini")]
if Game::is_jomini() {
if caller == "custom_description" || caller == "custom_description_no_bullet" {
vd.field_target_ok_this("object", sc, Scopes::non_primitive());
vd.field_script_value("value", sc);
} else {
vd.ban_field("object", || "`custom_description`");
}
}
if caller == "hidden_effect" || caller == "hidden_effect_new_object" {
tooltipped = Tooltipped::No;
}
if caller == "random" {
vd.req_field("chance");
if Game::is_jomini() {
#[cfg(feature = "jomini")]
vd.field_script_value("chance", sc);
} else {
vd.field_numeric("chance");
}
} else {
vd.ban_field("chance", || "`random`");
}
#[cfg(feature = "ck3")]
if Game::is_ck3()
&& (caller == "send_interface_message"
|| caller == "send_interface_toast"
|| caller == "send_interface_popup")
{
vd.field_item("type", Item::Message);
if let Some(token) = vd.field_value("goto") {
let msg = "`goto` was removed from interface messages in 1.9";
warn(ErrorKey::Removed).msg(msg).loc(token).push();
}
vd.field("title");
vd.field("desc");
vd.field("tooltip");
vd.field("left_icon");
vd.field("right_icon");
vd.field("localization_values");
}
if caller == "while" {
if !(block.has_key("limit") || block.has_key("count")) {
let msg = "`while` needs one of `limit` or `count`";
warn(ErrorKey::Validation).msg(msg).loc(block).push();
}
if Game::is_jomini() {
#[cfg(feature = "jomini")]
vd.field_script_value("count", sc);
}
} else {
vd.ban_field("count", || "`while` and `any_` lists");
}
if caller == "random" || caller == "random_list" || caller == "duel" {
#[cfg(feature = "vic3")]
if Game::is_vic3() {
vd.field_script_value("modifier", sc);
}
#[cfg(any(feature = "imperator", feature = "ck3", feature = "hoi4"))]
if Game::is_imperator() || Game::is_ck3() || Game::is_hoi4() {
validate_modifiers(&mut vd, sc);
}
} else {
vd.ban_field("modifier", || "`random`, `random_list` or `duel`");
vd.ban_field("compare_modifier", || "`random`, `random_list` or `duel`");
vd.ban_field("opinion_modifier", || "`random`, `random_list` or `duel`");
vd.ban_field("ai_value_modifier", || "`random`, `random_list` or `duel`");
vd.ban_field("compatibility", || "`random`, `random_list` or `duel`");
}
if caller == "random_list" || caller == "duel" {
vd.field_trigger("trigger", Tooltipped::No, sc);
vd.field_bool("show_chance");
vd.field_validated_sc("desc", sc, validate_desc);
#[cfg(feature = "jomini")]
if Game::is_jomini() {
vd.field_script_value("min", sc); vd.field_script_value("max", sc); }
} else {
vd.ban_field("trigger", || "`random_list` or `duel`");
vd.ban_field("show_chance", || "`random_list` or `duel`");
}
validate_effect_internal(caller, ListType::None, block, data, sc, &mut vd, tooltipped);
#[cfg(feature = "ck3")]
if Game::is_ck3()
&& (caller == "send_interface_message"
|| caller == "send_interface_toast"
|| caller == "send_interface_popup")
{
let mut loca_sc = sc.clone();
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_ok_this(value, data, sc, Scopes::all());
loca_sc.define_name(key.as_str(), scopes, value);
});
});
vd.field_validated_sc("title", &mut loca_sc, validate_desc);
vd.field_validated_sc("desc", &mut loca_sc, validate_desc);
vd.field_validated_sc("tooltip", &mut loca_sc, validate_desc);
loca_sc.destroy();
let icon_scopes =
Scopes::Character | Scopes::LandedTitle | Scopes::Artifact | Scopes::Faith;
if let Some(token) = vd.field_value("left_icon") {
validate_target_ok_this(token, data, sc, icon_scopes);
}
if let Some(token) = vd.field_value("right_icon") {
validate_target_ok_this(token, data, sc, icon_scopes);
}
}
}
#[derive(Copy, Clone)]
#[allow(dead_code)] pub enum Effect {
Yes,
Boolean,
Integer,
ScriptValue,
#[allow(dead_code)]
NonNegativeValue,
#[cfg(feature = "vic3")]
Date,
Scope(Scopes),
#[cfg(any(feature = "ck3", feature = "imperator"))]
ScopeOkThis(Scopes),
Item(Item),
ScopeOrItem(Scopes, Item),
#[cfg(feature = "ck3")]
Target(&'static str, Scopes),
#[cfg(any(feature = "ck3", feature = "vic3"))]
TargetValue(&'static str, Scopes, &'static str),
#[cfg(any(feature = "ck3", feature = "hoi4"))]
ItemTarget(&'static str, Item, &'static str, Scopes),
#[cfg(feature = "ck3")]
ItemValue(&'static str, Item),
#[cfg(feature = "ck3")]
Desc,
#[cfg(any(feature = "ck3", feature = "vic3"))]
Timespan,
Control,
ControlOrLabel,
#[cfg(feature = "hoi4")]
Iterator(ListType, Scopes),
Unchecked,
#[cfg(any(feature = "ck3", feature = "vic3", feature = "hoi4"))]
UncheckedTodo,
Choice(&'static [&'static str]),
Removed(&'static str, &'static str),
Vb(fn(&Token, &Block, &Everything, &mut ScopeContext, Validator, Tooltipped)),
Vbv(fn(&Token, &BV, &Everything, &mut ScopeContext, Tooltipped)),
Vv(fn(&Token, ValueValidator, &mut ScopeContext, Tooltipped)),
Identifier(&'static str),
#[cfg(feature = "hoi4")]
Value,
}