use core::ffi::c_void;
use crate::abi::{
Header, StatChange as StatChangeData, StatModifierData, StatModify, StatRef, StatSource,
Status, Str,
};
use crate::buffs::BuffTypeId;
use crate::content::items::StatFormat;
use crate::item_data::{ItemData, ItemInstance, SetError};
use crate::{Entity, ItemId};
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).fnv1a())
}
pub mod builtin;
#[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, 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,
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,
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: ItemId::new(s.owner),
}),
StatSource::BUFF => Origin::Buff(BuffTypeId(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, 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: 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 add_buff_modifiers(
&mut self,
buff: BuffTypeId,
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",
)
}
}
mod defs;
mod modifiers;
#[cfg(test)]
mod tests;
pub use defs::*;
pub use modifiers::*;