use crate::game::schema::query::CompiledAliasCategory;
const KEPT_FRACTION_DIGITS: usize = 3;
const FIXED_POINT_SCALE: i64 = 1000;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct ScriptFixedPoint(i64);
pub fn script_fixed_point(text: &str) -> Option<ScriptFixedPoint> {
let (negative, digits) = split_sign(text)?;
let (integer, fraction) = match digits.split_once('.') {
Some((integer, fraction)) => (integer, fraction),
None => (digits, ""),
};
if integer.is_empty() && fraction.is_empty() {
return None;
}
if !is_ascii_digits(integer) || !is_ascii_digits(fraction) {
return None;
}
let integer = if integer.is_empty() {
0
} else {
integer.parse::<i64>().ok()?
};
let kept = &fraction[..fraction.len().min(KEPT_FRACTION_DIGITS)];
let thousandths = match kept.len() {
0 => 0,
length => kept.parse::<i64>().ok()? * 10_i64.pow((KEPT_FRACTION_DIGITS - length) as u32),
};
let magnitude = integer
.checked_mul(FIXED_POINT_SCALE)?
.checked_add(thousandths)?;
Some(ScriptFixedPoint(if negative {
-magnitude
} else {
magnitude
}))
}
const THREE_DECIMAL_EFFECTS: &[&str] = &[
"add_army_tradition",
"add_navy_tradition",
"add_mercantilism",
"add_legitimacy",
"add_war_exhaustion",
"add_inflation",
"add_corruption",
"change_innovativeness",
"add_prestige",
"add_local_autonomy",
"add_prosperity",
"add_reform_desire",
"add_years_of_income",
];
const THREE_DECIMAL_TRIGGERS: &[&str] = &["army_tradition", "prestige", "mercantilism"];
const THREE_DECIMAL_MODIFIERS: &[&str] = &[
"army_tradition_decay",
"development_cost",
"discipline",
"drill_gain_modifier",
"global_institution_spread",
"global_tax_modifier",
"global_trade_goods_size_modifier",
"global_unrest",
"harsh_treatment_cost",
"land_forcelimit_modifier",
"land_morale",
"local_autonomy",
"local_build_cost",
"local_defensiveness",
"local_development_cost",
"local_manpower_modifier",
"local_production_efficiency",
"monthly_piety",
"monthly_piety_accelerator",
"naval_forcelimit_modifier",
"naval_morale",
"prestige",
"prestige_decay",
"province_trade_power_modifier",
"trade_value_modifier",
"yearly_army_professionalism",
"yearly_corruption",
"yearly_patriarch_authority",
];
const THREE_DECIMAL_VALUE_EFFECTS: &[&str] = &["set_variable", "change_variable"];
const THREE_DECIMAL_VALUE_TRIGGERS: &[&str] = &["check_variable", "has_global_modifier_value"];
pub fn alias_reads_three_decimals(category: &CompiledAliasCategory, name: &str) -> bool {
let checked = match category {
CompiledAliasCategory::Effect => THREE_DECIMAL_EFFECTS,
CompiledAliasCategory::Trigger => THREE_DECIMAL_TRIGGERS,
CompiledAliasCategory::Modifier => THREE_DECIMAL_MODIFIERS,
CompiledAliasCategory::Link | CompiledAliasCategory::Other(_) => return false,
};
checked.contains(&name)
}
pub fn alias_value_reads_three_decimals(category: &CompiledAliasCategory, name: &str) -> bool {
let checked = match category {
CompiledAliasCategory::Effect => THREE_DECIMAL_VALUE_EFFECTS,
CompiledAliasCategory::Trigger => THREE_DECIMAL_VALUE_TRIGGERS,
_ => return false,
};
checked.contains(&name)
}
pub fn drop_unread_fraction_digits(text: &str) -> Option<String> {
script_fixed_point(text)?;
let (integer, fraction) = text.split_once('.')?;
(fraction.len() > KEPT_FRACTION_DIGITS)
.then(|| format!("{integer}.{}", &fraction[..KEPT_FRACTION_DIGITS]))
}
fn split_sign(text: &str) -> Option<(bool, &str)> {
match text.as_bytes() {
[b'-', ..] => Some((true, &text[1..])),
[b'+', ..] => Some((false, &text[1..])),
[] => None,
_ => Some((false, text)),
}
}
fn is_ascii_digits(text: &str) -> bool {
text.bytes().all(|byte| byte.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed(text: &str) -> Option<i64> {
script_fixed_point(text).map(|value| value.0)
}
#[test]
fn trailing_zeros_do_not_change_the_value_the_game_reads() {
assert_eq!(fixed("0.5"), Some(500));
assert_eq!(fixed("0.50"), Some(500));
assert_eq!(fixed("0.500"), Some(500));
assert_eq!(fixed("1"), Some(1000));
assert_eq!(fixed("1.0"), Some(1000));
assert_eq!(fixed("1.000"), Some(1000));
}
#[test]
fn the_fourth_decimal_is_truncated_rather_than_rounded() {
assert_eq!(fixed("0.1234"), Some(123));
assert_eq!(fixed("0.1239"), Some(123));
assert_eq!(fixed("0.1299"), Some(129));
assert_eq!(fixed("0.0025"), Some(2));
}
#[test]
fn distinct_values_stay_distinct() {
assert_ne!(fixed("0.5"), fixed("0.6"));
assert_ne!(fixed("1.001"), fixed("1.002"));
assert_ne!(fixed("0.5"), fixed("-0.5"));
assert_ne!(fixed("1"), fixed("10"));
}
#[test]
fn negative_values_truncate_toward_zero() {
assert_eq!(fixed("-0.1234"), Some(-123));
assert_eq!(fixed("-1.5"), Some(-1500));
assert_eq!(fixed("-0.50"), fixed("-0.5"));
}
#[test]
fn leading_zeros_and_a_missing_integer_part_are_read_as_written() {
assert_eq!(fixed("007"), Some(7000));
assert_eq!(fixed(".5"), Some(500));
assert_eq!(fixed("0.5"), fixed(".5"));
assert_eq!(fixed("5."), Some(5000));
}
#[test]
fn unmodelled_shapes_abstain() {
assert_eq!(fixed(""), None);
assert_eq!(fixed("."), None);
assert_eq!(fixed("-"), None);
assert_eq!(fixed("yes"), None);
assert_eq!(fixed("1e-3"), None);
assert_eq!(fixed("1444.11.11"), None);
assert_eq!(fixed("0x10"), None);
assert_eq!(fixed("123abc"), None);
}
#[test]
fn digits_past_the_third_are_dropped_and_nothing_else_moves() {
assert_eq!(
drop_unread_fraction_digits("0.0125").as_deref(),
Some("0.012")
);
assert_eq!(
drop_unread_fraction_digits("0.1239").as_deref(),
Some("0.123")
);
assert_eq!(
drop_unread_fraction_digits("-0.0125").as_deref(),
Some("-0.012")
);
assert_eq!(
drop_unread_fraction_digits("1.0000").as_deref(),
Some("1.000")
);
assert_eq!(
drop_unread_fraction_digits("+.12345").as_deref(),
Some("+.123")
);
}
#[test]
fn a_number_the_game_reads_whole_is_left_alone() {
for text in [
"0.5",
"0.50",
"0.125",
"1",
"-3",
"yes",
"1e-3",
"1444.11.11",
] {
assert_eq!(drop_unread_fraction_digits(text), None, "{text}");
}
}
#[test]
fn dropping_is_what_the_reader_already_does() {
for text in ["0.0125", "0.1239", "-0.0125", "1.0000", "7.12345"] {
let dropped = drop_unread_fraction_digits(text).expect("digits to drop");
assert_eq!(
script_fixed_point(&dropped),
script_fixed_point(text),
"{text}"
);
}
}
#[test]
fn only_the_checked_fields_count_as_three_decimal() {
assert!(alias_reads_three_decimals(
&CompiledAliasCategory::Effect,
"add_prestige"
));
assert!(alias_reads_three_decimals(
&CompiledAliasCategory::Modifier,
"discipline"
));
assert!(alias_reads_three_decimals(
&CompiledAliasCategory::Trigger,
"prestige"
));
assert!(!alias_reads_three_decimals(
&CompiledAliasCategory::Effect,
"add_army_professionalism"
));
assert!(!alias_reads_three_decimals(
&CompiledAliasCategory::Trigger,
"army_professionalism"
));
assert!(alias_reads_three_decimals(
&CompiledAliasCategory::Modifier,
"prestige"
));
assert!(!alias_reads_three_decimals(
&CompiledAliasCategory::Modifier,
"global_manpower_modifier"
));
}
#[test]
fn a_value_field_is_found_by_the_block_around_it() {
assert!(alias_value_reads_three_decimals(
&CompiledAliasCategory::Effect,
"set_variable"
));
assert!(alias_value_reads_three_decimals(
&CompiledAliasCategory::Trigger,
"has_global_modifier_value"
));
assert!(!alias_value_reads_three_decimals(
&CompiledAliasCategory::Effect,
"add_prestige"
));
}
}