use crate::block::{BV, Block, Comparator, Eq::*};
use crate::context::{Reason, ScopeContext};
#[cfg(feature = "jomini")]
use crate::data::effect_localization::validate_effect_localization;
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::{ErrorKey, Severity, err, fatal, tips, warn};
use crate::scopes::{Scopes, scope_iterator};
#[cfg(feature = "jomini")]
use crate::script_value::validate_script_value;
use crate::special_tokens::SpecialTokens;
use crate::token::Token;
use crate::tooltipped::Tooltipped;
use crate::trigger::scope_trigger;
#[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 = "ck3")]
use crate::validate::validate_possibly_named_color;
#[cfg(feature = "jomini")]
use crate::validate::validate_scripted_modifier_call;
use crate::validate::{
ListType, precheck_iterator_fields, validate_identifier, validate_ifelse_sequence,
validate_inside_iterator, validate_iterator_fields, validate_scope_chain,
};
use crate::validator::{Validator, ValueValidator};
pub fn scope_effect(name: &Token, data: &Everything) -> Option<(Scopes, Effect)> {
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 = "eu5")]
Game::Eu5 => crate::eu5::tables::effects::scope_effect,
#[cfg(feature = "hoi4")]
Game::Hoi4 => crate::hoi4::tables::effects::scope_effect,
};
scope_effect(name, data)
}
pub fn validate_effect(
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
tooltipped: Tooltipped,
) -> bool {
let mut vd = Validator::new(block, data);
validate_effect_internal(
Lowercase::empty(),
ListType::None,
block,
data,
sc,
&mut vd,
tooltipped,
&mut SpecialTokens::none(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn validate_effect_internal(
caller: &Lowercase,
list_type: ListType,
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
vd: &mut Validator,
mut tooltipped: Tooltipped,
special_tokens: &mut SpecialTokens,
) -> bool {
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);
let mut has_tooltip = false;
vd.unknown_fields_cmp(|key, cmp, bv| {
has_tooltip |=
validate_effect_field(caller, key, cmp, bv, data, sc, tooltipped, special_tokens);
});
has_tooltip
}
#[allow(unused_variables)] #[allow(clippy::too_many_arguments)]
pub fn validate_effect_field(
caller: &Lowercase,
key: &Token,
cmp: Comparator,
bv: &BV,
data: &Everything,
sc: &mut ScopeContext,
tooltipped: Tooltipped,
special_tokens: &mut SpecialTokens,
) -> bool {
let mut has_tooltip = false;
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();
}
has_tooltip |= effect.validate_call(key, data, sc, tooltipped, special_tokens);
}
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 false;
}
}
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();
has_tooltip |= effect.validate_macro_expansion(
key,
&args,
data,
sc,
tooltipped,
special_tokens,
);
}
}
}
return has_tooltip && tooltipped.is_tooltipped();
}
#[cfg(feature = "jomini")]
if Game::is_jomini()
&& 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 false;
}
validate_scripted_modifier_call(key, bv, modifier, data, sc);
return false;
}
if let Some((inscopes, effect)) = scope_effect(key, data) {
sc.expect(inscopes, &Reason::Token(key.clone()), data);
#[cfg(feature = "jomini")]
if tooltipped.is_tooltipped() {
has_tooltip |= data.item_exists(Item::EffectLocalization, key.as_str());
}
match effect {
Effect::Yes => {
if let Some(token) = bv.expect_value()
&& !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()
&& let Some(number) = token.get_number()
&& 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()
&& !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()
&& !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::Vbc(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, special_tokens);
}
}
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) => {
has_tooltip |= validate_effect_control(
&Lowercase::new(key.as_str()),
b,
data,
sc,
tooltipped,
special_tokens,
);
}
},
Effect::Control => {
if let Some(block) = bv.expect_block() {
let local_has_tooltip = validate_effect_control(
&Lowercase::new(key.as_str()),
block,
data,
sc,
tooltipped,
special_tokens,
);
if local_has_tooltip && key.is("random") {
special_tokens.insert(key);
}
has_tooltip |= local_has_tooltip;
}
}
#[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);
has_tooltip |= validate_effect_internal(
&Lowercase::new(it_name.as_str()),
ltype,
block,
data,
sc,
&mut vd,
tooltipped,
special_tokens,
);
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);
}
}
#[cfg(feature = "ck3")]
Effect::Color => {
validate_possibly_named_color(bv, data);
}
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 = "eu5", feature = "hoi4"))]
Effect::UncheckedTodo => (),
}
return has_tooltip && tooltipped.is_tooltipped();
}
if let Some((it_type, it_name)) = key.split_once('_')
&& let Ok(ltype) = ListType::try_from(it_type.as_str())
&& 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 false;
}
sc.expect(inscopes, &Reason::Token(key.clone()), data);
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);
has_tooltip |= validate_effect_internal(
&Lowercase::new(it_name.as_str()),
ltype,
b,
data,
sc,
&mut vd,
tooltipped,
special_tokens,
);
sc.close();
}
return has_tooltip && tooltipped.is_tooltipped();
}
#[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());
has_tooltip |= validate_effect(block, data, sc, tooltipped);
sc.close();
}
return has_tooltip;
}
if !Game::is_imperator() && scope_trigger(key, data).is_some() {
let msg = format!("`{key}` is a trigger and can't be used as an effect");
err(ErrorKey::WrongUse).msg(msg).loc(key).push();
return false;
}
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() {
has_tooltip |= validate_effect(block, data, sc, tooltipped);
}
}
sc.close();
has_tooltip && tooltipped.is_tooltipped()
}
pub fn validate_effect_control(
caller: &Lowercase,
block: &Block,
data: &Everything,
sc: &mut ScopeContext,
mut tooltipped: Tooltipped,
special_tokens: &mut SpecialTokens,
) -> bool {
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") {
validate_effect_localization(token, data, 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`");
}
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,
special_tokens,
)
}
#[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 = "eu5", feature = "hoi4"))]
UncheckedTodo,
Choice(&'static [&'static str]),
Removed(&'static str, &'static str),
Vb(fn(&Token, &Block, &Everything, &mut ScopeContext, Validator, Tooltipped)),
Vbc(
#[allow(clippy::type_complexity)]
fn(
&Token,
&Block,
&Everything,
&mut ScopeContext,
Validator,
Tooltipped,
&mut SpecialTokens,
) -> bool,
),
Vbv(fn(&Token, &BV, &Everything, &mut ScopeContext, Tooltipped)),
Vv(fn(&Token, ValueValidator, &mut ScopeContext, Tooltipped)),
Identifier(&'static str),
#[cfg(feature = "hoi4")]
Value,
#[cfg(feature = "ck3")]
Color,
}