use core::time::Duration;
use crate::abi::{ActiveBuff, BuffRequest, BuffTypeInfo, Header, Status};
use crate::ecs::Entity;
use crate::world::WorldError;
use crate::{Host, Load};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BuffTypeId(pub u32);
impl BuffTypeId {
#[must_use]
pub const fn from_guid(guid: [u8; 16]) -> Self {
Self(crate::content::Guid(guid).id())
}
#[must_use]
pub const fn named(id: &str) -> Self {
Self::from_guid(crate::content::Guid::named(id).0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum BuffSlotType {
Food,
Buff,
Debuff,
Rested,
TerrainDebuff,
Other(u16),
}
impl BuffSlotType {
const fn from_slot(slot: u16) -> Self {
match slot {
BuffTypeInfo::FOOD => Self::Food,
BuffTypeInfo::BUFF => Self::Buff,
BuffTypeInfo::DEBUFF => Self::Debuff,
BuffTypeInfo::RESTED => Self::Rested,
BuffTypeInfo::TERRAIN => Self::TerrainDebuff,
other => Self::Other(other),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[expect(
clippy::struct_field_names,
reason = "the fields take the game's names, `buffTypeId` among them"
)]
pub struct BuffType {
pub buff_type_id: BuffTypeId,
pub slot: BuffSlotType,
pub default_life_time: Duration,
pub is_saved: bool,
pub hide_in_ui: bool,
pub name: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[expect(
clippy::struct_field_names,
reason = "the fields take the game's names, `buffEntityId` among them"
)]
pub struct Buff {
pub type_id: BuffTypeId,
pub buff_entity_id: Entity,
pub slot: BuffSlotType,
pub level: Option<u32>,
pub life_time: Duration,
pub remaining: Duration,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Apply {
pub duration: Option<Duration>,
pub level: Option<u8>,
}
impl Apply {
#[must_use]
pub const fn new() -> Self {
Self {
duration: None,
level: None,
}
}
#[must_use]
pub const fn lasting(mut self, duration: Duration) -> Self {
self.duration = Some(duration);
self
}
#[must_use]
pub const fn level(mut self, level: u8) -> Self {
self.level = Some(level);
self
}
}
#[derive(Clone, Copy, Debug)]
pub struct Buffs {
host: Host,
}
fn nanos(duration: i64) -> Duration {
Duration::from_nanos(u64::try_from(duration).unwrap_or(0))
}
impl Buffs {
#[must_use]
pub fn types(&self) -> Vec<BuffType> {
let blank = BuffTypeInfo {
header: Header::of::<BuffTypeInfo>(),
id: 0,
slot: 0,
flags: 0,
name: 0,
duration: 0,
};
let fill = |out: *mut BuffTypeInfo, capacity: u32, count: *mut u32| {
(self.host.api().buff_types)(self.host.handle(), out, capacity, count)
};
list(blank, fill)
.unwrap_or_default()
.iter()
.map(|info| BuffType {
buff_type_id: BuffTypeId(info.id),
slot: BuffSlotType::from_slot(info.slot),
default_life_time: nanos(info.duration),
is_saved: info.flags & BuffTypeInfo::SAVED != 0,
hide_in_ui: info.flags & BuffTypeInfo::HIDDEN != 0,
name: info.name,
})
.collect()
}
pub fn of(&self, entity: Entity) -> Result<Vec<Buff>, WorldError> {
let blank = ActiveBuff {
header: Header::of::<ActiveBuff>(),
id: 0,
entity: 0,
slot: 0,
level: 0,
duration: 0,
remaining: 0,
};
let fill = |out: *mut ActiveBuff, capacity: u32, count: *mut u32| {
(self.host.api().buffs)(self.host.handle(), entity.0, out, capacity, count)
};
Ok(list(blank, fill)?
.iter()
.map(|buff| Buff {
type_id: BuffTypeId(buff.id),
buff_entity_id: Entity(buff.entity),
slot: BuffSlotType::from_slot(u16::try_from(buff.slot).unwrap_or(u16::MAX)),
level: (buff.level != 0).then_some(buff.level),
life_time: nanos(buff.duration),
remaining: nanos(buff.remaining),
})
.collect())
}
#[must_use]
pub fn has(&self, entity: Entity, id: BuffTypeId) -> bool {
self.of(entity)
.is_ok_and(|on| on.iter().any(|buff| buff.type_id == id))
}
pub fn apply(&self, entity: Entity, id: BuffTypeId, how: Apply) -> Result<(), WorldError> {
let duration = how.duration.map_or(0, |duration| {
i64::try_from(duration.as_nanos())
.unwrap_or(i64::MAX)
.max(1)
});
let request = BuffRequest {
header: Header::of::<BuffRequest>(),
entity: entity.0,
id: id.0,
level: how.level.map_or(0, u32::from),
reserved: 0,
duration,
};
WorldError::check(
(self.host.api().apply_buff)(self.host.handle(), &raw const request),
WorldError::Lacks,
)
}
pub fn remove(&self, entity: Entity, id: BuffTypeId) -> Result<bool, WorldError> {
let mut removed = 0u8;
WorldError::check(
(self.host.api().remove_buff)(self.host.handle(), entity.0, id.0, &raw mut removed),
WorldError::Lacks,
)?;
Ok(removed != 0)
}
}
fn list<T: Copy>(
blank: T,
fill: impl Fn(*mut T, u32, *mut u32) -> Status,
) -> Result<Vec<T>, WorldError> {
let mut items = vec![blank; 64];
loop {
let mut count = 0u32;
let capacity = u32::try_from(items.len()).unwrap_or(u32::MAX);
WorldError::check(
fill(items.as_mut_ptr(), capacity, &raw mut count),
WorldError::Lacks,
)?;
let count = count as usize;
if count <= items.len() {
items.truncate(count);
return Ok(items);
}
items = vec![blank; count];
}
}
impl Load<'_> {
#[must_use]
pub fn buffs(&mut self) -> Buffs {
Buffs { host: self.host() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_guid_hashes_as_the_game_hashes_it() {
assert_eq!(BuffTypeId::from_guid([0; 16]), BuffTypeId(0x6969_1905));
}
}