use core::ffi::c_void;
use core::time::Duration;
use crate::abi::{
Header, StatChange as StatChangeData, StatDefData, StatModifierData, StatModify, StatRef,
StatSource, Status, Str,
};
use crate::buffs::BuffId;
use crate::content::items::StatFormat;
use crate::ecs::Entity;
use crate::item_data::{ItemData, ItemInstance, SetError};
use crate::{Host, Load, Off};
pub const ITEM_KIND: &str = "enshwick:stats";
#[must_use]
pub const fn key_of(id: &str) -> StatKey {
StatKey(crate::content::Guid::hashed(id).id())
}
pub mod builtin {
use super::{StatDef, StatFormat};
pub const HEALTH_MAX: &str = "enshwick:health_max";
pub const STAMINA_MAX: &str = "enshwick:stamina_max";
pub const MANA_MAX: &str = "enshwick:mana_max";
pub const DAMAGE: &str = "enshwick:damage";
pub const DAMAGE_CUT: &str = "enshwick:damage_cut";
pub const DAMAGE_PIERCE: &str = "enshwick:damage_pierce";
pub const DAMAGE_BLUNT: &str = "enshwick:damage_blunt";
pub const DAMAGE_FIRE: &str = "enshwick:damage_fire";
pub const DAMAGE_ICE: &str = "enshwick:damage_ice";
pub const DAMAGE_SHROUD: &str = "enshwick:damage_shroud";
pub const DAMAGE_LIGHTNING: &str = "enshwick:damage_lightning";
pub const DAMAGE_POISON: &str = "enshwick:damage_poison";
pub const RESISTANCE: &str = "enshwick:resistance";
pub const RESISTANCE_CUT: &str = "enshwick:resistance_cut";
pub const RESISTANCE_PIERCE: &str = "enshwick:resistance_pierce";
pub const RESISTANCE_BLUNT: &str = "enshwick:resistance_blunt";
pub const RESISTANCE_FIRE: &str = "enshwick:resistance_fire";
pub const RESISTANCE_ICE: &str = "enshwick:resistance_ice";
pub const RESISTANCE_SHROUD: &str = "enshwick:resistance_shroud";
pub const RESISTANCE_LIGHTNING: &str = "enshwick:resistance_lightning";
pub const RESISTANCE_POISON: &str = "enshwick:resistance_poison";
const MAX_FACTOR: f32 = 100.0;
const MAX_POINTS: f32 = 100_000.0;
const fn factor(text: &'static str) -> Def {
Def {
text,
multiplicative: true,
points: false,
min: 0.0,
max: MAX_FACTOR,
}
}
const fn resistance(text: &'static str) -> Def {
Def {
text,
multiplicative: false,
points: false,
min: -1.0,
max: 1.0,
}
}
const fn points(text: &'static str) -> Def {
Def {
text,
multiplicative: false,
points: true,
min: -MAX_POINTS,
max: MAX_POINTS,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Def {
text: &'static str,
multiplicative: bool,
points: bool,
min: f32,
max: f32,
}
impl Def {
#[must_use]
pub fn to_def(self) -> StatDef {
let def = if self.multiplicative {
StatDef::multiplicative(self.text)
} else {
StatDef::additive(self.text)
};
let format = if self.points {
StatFormat::Signed
} else {
StatFormat::Percent
};
def.range(self.min, self.max).format(format)
}
}
pub const ALL: [(&str, Def); 21] = [
(HEALTH_MAX, points("enshwick.stat.health_max")),
(STAMINA_MAX, points("enshwick.stat.stamina_max")),
(MANA_MAX, points("enshwick.stat.mana_max")),
(DAMAGE, factor("enshwick.stat.damage")),
(DAMAGE_CUT, factor("enshwick.stat.damage_cut")),
(DAMAGE_PIERCE, factor("enshwick.stat.damage_pierce")),
(DAMAGE_BLUNT, factor("enshwick.stat.damage_blunt")),
(DAMAGE_FIRE, factor("enshwick.stat.damage_fire")),
(DAMAGE_ICE, factor("enshwick.stat.damage_ice")),
(DAMAGE_SHROUD, factor("enshwick.stat.damage_shroud")),
(DAMAGE_LIGHTNING, factor("enshwick.stat.damage_lightning")),
(DAMAGE_POISON, factor("enshwick.stat.damage_poison")),
(RESISTANCE, resistance("enshwick.stat.resistance")),
(RESISTANCE_CUT, resistance("enshwick.stat.resistance_cut")),
(
RESISTANCE_PIERCE,
resistance("enshwick.stat.resistance_pierce"),
),
(
RESISTANCE_BLUNT,
resistance("enshwick.stat.resistance_blunt"),
),
(RESISTANCE_FIRE, resistance("enshwick.stat.resistance_fire")),
(RESISTANCE_ICE, resistance("enshwick.stat.resistance_ice")),
(
RESISTANCE_SHROUD,
resistance("enshwick.stat.resistance_shroud"),
),
(
RESISTANCE_LIGHTNING,
resistance("enshwick.stat.resistance_lightning"),
),
(
RESISTANCE_POISON,
resistance("enshwick.stat.resistance_poison"),
),
];
pub const BY_DAMAGE_TYPE: [Option<(&str, &str)>; crate::damage::DamageType::ALL.len()] = [
Some((DAMAGE_CUT, RESISTANCE_CUT)),
Some((DAMAGE_PIERCE, RESISTANCE_PIERCE)),
Some((DAMAGE_BLUNT, RESISTANCE_BLUNT)),
Some((DAMAGE_FIRE, RESISTANCE_FIRE)),
Some((DAMAGE_ICE, RESISTANCE_ICE)),
Some((DAMAGE_SHROUD, RESISTANCE_SHROUD)),
Some((DAMAGE_LIGHTNING, RESISTANCE_LIGHTNING)),
None,
Some((DAMAGE_POISON, RESISTANCE_POISON)),
None,
None,
None,
None,
];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StatType {
Additive,
Multiplicative,
Flag,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct StatDef {
pub ty: StatType,
pub default: f32,
pub min: f32,
pub max: f32,
pub text: String,
pub format: StatFormat,
}
impl StatDef {
fn of(ty: StatType, default: f32, text: &str, format: StatFormat) -> Self {
Self {
ty,
default,
min: f32::MIN,
max: f32::MAX,
text: text.to_owned(),
format,
}
}
#[must_use]
pub fn additive(text: &str) -> Self {
Self::of(StatType::Additive, 0.0, text, StatFormat::Signed)
}
#[must_use]
pub fn multiplicative(text: &str) -> Self {
Self::of(StatType::Multiplicative, 1.0, text, StatFormat::Percent)
}
#[must_use]
pub fn flag(text: &str) -> Self {
let mut def = Self::of(StatType::Flag, 0.0, text, StatFormat::Number);
def.min = 0.0;
def.max = 1.0;
def
}
#[must_use]
pub const fn range(mut self, min: f32, max: f32) -> Self {
self.min = min;
self.max = max;
self
}
#[must_use]
pub const fn starting_at(mut self, value: f32) -> Self {
self.default = value;
self
}
#[must_use]
pub const fn format(mut self, format: StatFormat) -> Self {
self.format = format;
self
}
fn check(&self) -> Result<(), String> {
let finite = self.default.is_finite() && self.min.is_finite() && self.max.is_finite();
if !finite || self.min > self.max || !(self.min..=self.max).contains(&self.default) {
return Err(format!(
"default {} is not within {}..{}",
self.default, self.min, self.max
));
}
Ok(())
}
#[must_use]
pub fn to_data(&self) -> StatDefData {
StatDefData {
header: Header::of::<StatDefData>(),
ty: match self.ty {
StatType::Additive => StatDefData::ADDITIVE,
StatType::Multiplicative => StatDefData::MULTIPLICATIVE,
StatType::Flag => StatDefData::FLAG,
},
format: match self.format {
StatFormat::Number => StatDefData::NUMBER,
StatFormat::Signed => StatDefData::SIGNED,
StatFormat::Percent => StatDefData::PERCENT,
},
reserved: 0,
default: self.default,
min: self.min,
max: self.max,
text: Str::new(&self.text),
}
}
#[must_use]
pub unsafe fn from_data(data: &StatDefData) -> Option<Self> {
let ty = match data.ty {
StatDefData::ADDITIVE => StatType::Additive,
StatDefData::MULTIPLICATIVE => StatType::Multiplicative,
StatDefData::FLAG => StatType::Flag,
_ => return None,
};
let format = match data.format {
StatDefData::SIGNED => StatFormat::Signed,
StatDefData::PERCENT => StatFormat::Percent,
_ => StatFormat::Number,
};
let text = unsafe { data.text.as_str() }?;
let def = Self {
ty,
default: data.default,
min: data.min,
max: data.max,
text: text.to_owned(),
format,
};
def.check().is_ok().then_some(def)
}
}
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
serde::Serialize,
serde::Deserialize,
)]
pub struct StatKey(pub u32);
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Stat {
index: u32,
key: StatKey,
}
impl Stat {
#[must_use]
pub const fn key(self) -> StatKey {
self.key
}
#[must_use]
pub const fn index(self) -> u32 {
self.index
}
#[must_use]
pub const fn from_ref(stat: StatRef) -> Self {
Self {
index: stat.index,
key: StatKey(stat.key),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[non_exhaustive]
pub enum Op {
Add,
Multiply,
Max,
Override,
}
impl Op {
#[must_use]
pub const fn to_raw(self) -> u8 {
match self {
Self::Add => StatModifierData::ADD,
Self::Multiply => StatModifierData::MULTIPLY,
Self::Max => StatModifierData::MAX,
Self::Override => StatModifierData::OVERRIDE,
}
}
#[must_use]
pub const fn from_raw(raw: u8) -> Option<Self> {
match raw {
StatModifierData::ADD => Some(Self::Add),
StatModifierData::MULTIPLY => Some(Self::Multiply),
StatModifierData::MAX => Some(Self::Max),
StatModifierData::OVERRIDE => Some(Self::Override),
_ => None,
}
}
}
#[must_use]
pub fn aggregate(def: &StatDef, modifiers: impl IntoIterator<Item = (Op, f32)>) -> f32 {
let mut add = 0.0f32;
let mut factor = 1.0f32;
let mut floor = f32::NEG_INFINITY;
let mut replaced = None;
for (op, value) in modifiers {
if !value.is_finite() {
continue;
}
match op {
Op::Add => add += value,
Op::Multiply => factor *= value,
Op::Max => floor = floor.max(value),
Op::Override => replaced = Some(value),
}
}
let value = replaced.unwrap_or_else(|| ((def.default + add) * factor).max(floor));
let value = if value.is_finite() {
value.clamp(def.min, def.max)
} else {
def.default
};
match def.ty {
StatType::Flag => f32::from(u8::from(value != 0.0)),
StatType::Additive | StatType::Multiplicative => value,
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Modifier {
pub stat: Stat,
pub op: Op,
pub value: f32,
}
impl Modifier {
#[must_use]
pub const fn new(stat: Stat, op: Op, value: f32) -> Self {
Self { stat, op, value }
}
const fn to_data(self) -> StatModifierData {
StatModifierData {
stat: self.stat.index,
op: self.op.to_raw(),
reserved: [0; 3],
value: self.value,
}
}
#[must_use]
pub const fn to_item(self) -> ItemModifier {
ItemModifier {
stat: self.stat.key,
op: self.op,
value: self.value,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ItemModifier {
pub stat: StatKey,
pub op: Op,
pub value: f32,
}
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ItemStats {
pub modifiers: Vec<ItemModifier>,
pub affixes: Vec<crate::affixes::Rolled>,
pub rolled: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Lasting {
UntilRemoved,
For(Duration),
Saved,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ModifyError {
#[error("only the server keeps modifiers")]
NotServer,
#[error("only players keep saved modifiers")]
NotPlayer,
#[error("modifier refused (status {0})")]
Refused(u32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Origin {
Item(ItemInstance),
Buff(BuffId),
Mod {
index: u32,
key: u32,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct Source {
pub origin: Origin,
pub stat: u32,
pub op: Op,
pub value: f32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct StatChange {
pub entity: Entity,
pub stat: Stat,
pub old: f32,
pub new: f32,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct StatInfo {
pub id: String,
pub def: StatDef,
}
const fn key_hash(key: &str) -> u32 {
key_of(key).0
}
#[derive(Clone, Copy, Debug)]
pub struct Stats {
host: Host,
items: ItemData<ItemStats>,
}
impl Stats {
#[must_use]
pub fn value(&self, entity: Entity, stat: Stat) -> f32 {
let mut out = 0.0;
let status =
(self.host.api().stat_value)(self.host.handle(), entity.0, stat.index, &raw mut out);
if status.is_ok() { out } else { 0.0 }
}
#[must_use]
pub fn is_on(&self, entity: Entity, stat: Stat) -> bool {
self.value(entity, stat) != 0.0
}
pub fn set(
&self,
entity: Entity,
key: &str,
modifier: Modifier,
lasting: Lasting,
) -> Result<(), ModifyError> {
let (flags, duration) = match lasting {
Lasting::UntilRemoved => (0, 0),
Lasting::For(d) => (0, u64::try_from(d.as_millis()).unwrap_or(u64::MAX).max(1)),
Lasting::Saved => (StatModify::SAVED, 0),
};
self.modify(entity, key, modifier.to_data(), flags, duration)
}
pub fn remove(&self, entity: Entity, key: &str, stat: Stat) -> Result<(), ModifyError> {
let data = StatModifierData {
stat: stat.index,
..StatModifierData::default()
};
self.modify(entity, key, data, StatModify::REMOVE, 0)
}
fn modify(
&self,
entity: Entity,
key: &str,
modifier: StatModifierData,
flags: u32,
duration_ms: u64,
) -> Result<(), ModifyError> {
let request = StatModify {
header: Header::of::<StatModify>(),
entity: entity.0,
key: key_hash(key),
modifier,
flags,
duration_ms,
};
match (self.host.api().stat_modify)(self.host.handle(), &raw const request) {
Status::OK => Ok(()),
Status::REFUSED if flags & StatModify::SAVED != 0 => Err(ModifyError::NotPlayer),
Status::REFUSED | Status::WRONG_THREAD => Err(ModifyError::NotServer),
other => Err(ModifyError::Refused(other.0)),
}
}
#[must_use]
pub fn item(&self, item: ItemInstance) -> ItemStats {
self.items.get(item).unwrap_or_default()
}
pub fn set_item(&self, item: ItemInstance, stats: &ItemStats) -> Result<(), SetError> {
if *stats == ItemStats::default() {
self.items.remove(item)
} else {
self.items.set(item, stats)
}
}
#[must_use]
pub fn sources(&self, entity: Entity, stat: Option<Stat>) -> Vec<Source> {
let stat = stat.map_or(u32::MAX, |s| s.index);
let mut out = vec![StatSource::default(); 32];
loop {
let mut count = 0u32;
let status = (self.host.api().stat_sources)(
self.host.handle(),
entity.0,
stat,
out.as_mut_ptr(),
u32::try_from(out.len()).unwrap_or(u32::MAX),
&raw mut count,
);
let count = count as usize;
if !status.is_ok() {
return Vec::new();
}
if count <= out.len() {
out.truncate(count);
break;
}
out = vec![StatSource::default(); count];
}
out.iter()
.filter_map(|s| {
let origin = match s.kind {
StatSource::ITEM => Origin::Item(ItemInstance {
pide: Entity(s.id),
item: s.owner,
}),
StatSource::BUFF => Origin::Buff(BuffId(s.id)),
StatSource::ENTITY => Origin::Mod {
index: s.owner,
key: s.id,
},
_ => return None,
};
Some(Source {
origin,
stat: s.stat,
op: Op::from_raw(s.op)?,
value: s.value,
})
})
.collect()
}
#[must_use]
pub fn info(&self, stat: Stat) -> Option<StatInfo> {
let mut def = StatDef::additive("").to_data();
let mut id = Str::EMPTY;
let status =
(self.host.api().stat_info)(self.host.handle(), stat.index, &raw mut def, &raw mut id);
if !status.is_ok() {
return None;
}
let def = unsafe { StatDef::from_data(&def) }?;
let id = unsafe { id.as_str() }?.to_owned();
Some(StatInfo { id, def })
}
pub fn watch(&self, entity: Entity, watch: bool) {
let _status = (self.host.api().stat_watch)(self.host.handle(), entity.0, u8::from(watch));
}
}
unsafe extern "system" fn run_changed<F>(change: *const StatChangeData, user: *mut c_void)
where
F: Fn(&StatChange),
{
let Some(change) = (unsafe { crate::abi::read_headed(change, size_of::<StatChangeData>()) })
else {
return;
};
let handler = unsafe { &*user.cast::<F>() };
let change = StatChange {
entity: Entity(change.entity),
stat: Stat::from_ref(change.stat),
old: change.old,
new: change.new,
};
crate::host::guard("panic in a stats handler", || (), || handler(&change));
}
impl Load<'_> {
fn open_stat_with(&mut self, id: &str, def: Option<&StatDef>) -> Result<Stat, Off> {
let host = self.host();
let data = def.map(StatDef::to_data);
let def_ptr = data.as_ref().map_or(core::ptr::null(), core::ptr::from_ref);
let mut out = StatRef::default();
let status = (host.api().stat_open)(host.handle(), Str::new(id), def_ptr, &raw mut out);
crate::load::ok(status, Stat::from_ref(out), || format!("stat {id:?}"))
}
pub fn define_stat(&mut self, name: &str, def: &StatDef) -> Result<Stat, Off> {
def.check()
.map_err(|why| Off::new(format!("stat {name:?}: {why}")))?;
self.open_stat_with(name, Some(def))
}
pub fn open_stat(&mut self, id: &str) -> Result<Stat, Off> {
self.open_stat_with(id, None)
}
pub fn stats(&mut self) -> Result<Stats, Off> {
let host = self.host();
Ok(Stats {
host,
items: self.item_data(ITEM_KIND)?,
})
}
pub fn buff_modifiers(&mut self, buff: BuffId, modifiers: &[Modifier]) -> Result<(), Off> {
let host = self.host();
let data: Vec<StatModifierData> = modifiers.iter().map(|m| m.to_data()).collect();
let status = (host.api().stat_buff)(
host.handle(),
buff.0,
data.as_ptr(),
u32::try_from(data.len()).unwrap_or(u32::MAX),
);
crate::load::ok(status, (), || format!("buff modifiers for {:#x}", buff.0))
}
pub fn on_stats_changed<F>(&mut self, handler: F) -> Result<(), Off>
where
F: Fn(&StatChange) + Send + Sync + 'static,
{
let host = self.host();
crate::load::hand_over(
handler,
|user| (host.api().stats_changed)(host.handle(), run_changed::<F>, user),
"stats handler",
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-5
}
fn def() -> StatDef {
StatDef::additive("t").range(-1000.0, 1000.0)
}
#[test]
fn operations_apply_in_their_order_whatever_the_input_order() {
let forward = [(Op::Add, 10.0), (Op::Multiply, 2.0), (Op::Add, 5.0)];
let backward = [(Op::Add, 5.0), (Op::Multiply, 2.0), (Op::Add, 10.0)];
assert!(close(aggregate(&def(), forward), 30.0));
assert!(close(aggregate(&def(), backward), 30.0));
}
#[test]
fn the_greatest_max_is_a_floor_and_the_last_override_wins() {
let mods = [(Op::Add, 5.0), (Op::Max, 20.0), (Op::Max, 12.0)];
assert!(close(aggregate(&def(), mods), 20.0));
let mods = [(Op::Override, 1.0), (Op::Add, 500.0), (Op::Override, 7.0)];
assert!(close(aggregate(&def(), mods), 7.0));
}
#[test]
fn the_range_holds_and_bad_values_are_skipped() {
assert!(close(aggregate(&def(), [(Op::Add, 1e9)]), 1000.0));
assert!(close(
aggregate(&def(), [(Op::Add, f32::NAN), (Op::Add, 2.0)]),
2.0
));
assert!(close(
aggregate(&def(), [(Op::Multiply, f32::INFINITY)]),
0.0
));
let factor = StatDef::multiplicative("f").range(0.0, 10.0);
assert!(close(
aggregate(&factor, [(Op::Multiply, 1.5), (Op::Multiply, 2.0)]),
3.0
));
assert!(close(aggregate(&factor, [(Op::Add, -5.0)]), 0.0));
}
#[test]
fn flags_read_as_zero_or_one() {
let flag = StatDef::flag("f");
assert!(close(aggregate(&flag, []), 0.0));
assert!(close(
aggregate(&flag, [(Op::Add, 1.0), (Op::Add, 1.0)]),
1.0
));
assert!(close(
aggregate(&flag, [(Op::Add, 1.0), (Op::Override, 0.0)]),
0.0
));
}
#[test]
fn definitions_out_of_their_own_range_are_refused() {
assert!(StatDef::additive("x").range(1.0, 2.0).check().is_err());
assert!(StatDef::additive("x").range(2.0, 1.0).check().is_err());
assert!(StatDef::multiplicative("x").check().is_ok());
assert!(
StatDef::additive("x")
.starting_at(f32::NAN)
.check()
.is_err()
);
}
#[test]
fn built_in_kinds_are_valid_and_their_keys_differ() {
let mut keys: Vec<_> = builtin::ALL.iter().map(|(id, _)| key_of(id)).collect();
for (id, def) in builtin::ALL {
assert!(def.to_def().check().is_ok(), "{id}");
assert!(id.starts_with("enshwick:"));
}
keys.sort_unstable();
keys.dedup();
assert_eq!(keys.len(), builtin::ALL.len());
}
#[test]
fn item_stats_round_trip_through_postcard() {
let stats = ItemStats {
modifiers: vec![
ItemModifier {
stat: key_of("a:b"),
op: Op::Multiply,
value: 1.25,
},
ItemModifier {
stat: key_of("a:c"),
op: Op::Override,
value: -3.0,
},
],
..ItemStats::default()
};
let bytes = postcard::to_allocvec(&stats).unwrap();
assert!(bytes.len() < 32);
assert_eq!(postcard::from_bytes::<ItemStats>(&bytes).unwrap(), stats);
}
fn deliver<F: Fn(&StatChange)>(change: &StatChangeData, handler: &F) {
unsafe { run_changed::<F>(change, core::ptr::from_ref(handler).cast_mut().cast()) };
}
#[test]
fn a_change_shorter_than_its_first_version_reaches_no_handler() {
let seen = core::cell::RefCell::new(Vec::new());
let handler = |change: &StatChange| seen.borrow_mut().push((change.entity, change.new));
let mut change = StatChangeData {
header: Header::of::<StatChangeData>(),
entity: 2,
stat: StatRef { index: 0, key: 1 },
old: 0.0,
new: 50.0,
reserved: 0,
};
deliver(&change, &handler);
change.header.size = 12;
deliver(&change, &handler);
assert_eq!(*seen.borrow(), [(Entity(2), 50.0)]);
}
}