mod kinds;
use core::marker::PhantomData;
use crate::abi::{GameEventData, Str};
use crate::ecs::Entity;
use crate::{Load, Off};
pub use kinds::{
AltarAction, AltarUsed, AttackCategories, Block, Built, CraftRequested, Crafted, DamageTypes,
Died, EquipmentChanged, ExperienceGained, ExperienceSource, Hit, Interacted, Kind, LevelGained,
LootOpened, PickedUp, Revived, TornDown,
};
mod sealed {
pub trait Sealed {}
}
pub const EVENT_TYPE_NAMES: &[&str] = &[
"keen::ecs::HitEvent",
"keen::ecs::EntityDiedEvent",
"keen::ecs::LevelUpEvent",
"keen::ecs::XPGainedEvent",
"keen::ecs::CraftRecipeEvent",
"keen::ecs::CraftRecipeSuccessEvent",
"keen::ecs::AcceptOfferedInteractionEvent",
"keen::ecs::InteractionAcceptedEvent",
"keen::ecs::BuildingPlaceEvent",
"keen::ecs::BuildingPickUpEvent",
"keen::ecs::BuildingTearDownEvent",
"keen::ecs::FlameAltarEvent",
"keen::ecs::LootInteractionEvent",
"keen::ecs::HandleEquipmentSlotChangedEvent",
"keen::ecs::RevivedPlayerEvent",
"keen::ecs::OnDestroyNotifyEvent",
"keen::ecs::LoginEvent",
"keen::ecs::PickupEvent",
"keen::ecs::ParryEvent",
"keen::ecs::AllItemsRepairedEvent",
"keen::ecs::TransformedToLootEvent",
"keen::ecs::AppliedBuffEvent",
"keen::ecs::LogicEntityPlacedEvent",
"keen::ecs::ProhibitedHit",
"keen::ecs::PlayerSpawnInfoChangeEvent",
"keen::ecs::CookingEvent",
"keen::ecs::GameSettingsChangedEvent",
"keen::ecs::AccountActionEvent",
"keen::ecs::KillFaction",
"keen::ecs::NoiseEvent",
"keen::ecs::ResourceChangeEvent",
"keen::ecs::PuzzleCompletedEvent",
"keen::ecs::DestroyedWithoutSurfaceEvent",
"keen::ecs::NotifyEvent",
"keen::ecs::SetToggleStateEvent",
"keen::ecs::ToggleInteractionEvent",
"keen::ecs::NpcCommandEvent",
"keen::ecs::FootDown",
"keen::ecs::GenericEnemyEvent",
"keen::ecs::EnemyAttackStartedEvent",
"keen::ecs::CallForHelpEvent",
"keen::ecs::AddThreatTargetEvent",
"keen::ecs::RestoreEnemyHealthEvent",
"keen::ecs::SetEnemyVariableEvent",
"keen::ecs::StartGuardingEvent",
"keen::ecs::StopGuardingEvent",
"keen::ecs::UiWorldEvent",
"keen::ecs::UiPingEvent",
"keen::ecs::OpenHudScreenEvent",
"keen::ecs::KnowledgeGrantsExperienceEvent",
"keen::ecs::KnowledgeAnalyticsEvent",
"keen::ecs::TerraformingHitEvent",
"keen::ecs::ResourceCandidate",
"keen::ecs::HitCandidate",
"keen::ecs::CollisionEvent",
"keen::ecs::InventoryFull",
"keen::ecs::AreaDepositEvent",
"keen::ecs::ApplyPushBackEvent",
"keen::ecs::ActivateFactionPacifyEvent",
"keen::ecs::VisceralAttackExecutedEvent",
"keen::ecs::StartPlaySongEvent",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct EventType {
name: &'static str,
hash: u32,
size: u32,
}
impl EventType {
pub(crate) fn resolve(load: &Load<'_>, name: &'static str) -> Result<Self, Off> {
let api = load.host().api();
let mut hash = 0;
let status = (api.type_hash)(Str::new(name), &raw mut hash);
crate::load::ok(status, (), || format!("event type {name}"))?;
let size = load.type_size(name)?;
Ok(Self { name, hash, size })
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub const fn hash(self) -> u32 {
self.hash
}
#[must_use]
pub const fn size(self) -> u32 {
self.size
}
pub fn field<T: Scalar>(self, load: &Load<'_>, name: &str) -> Result<Field<T>, Off> {
self.inherited_field(load, self.name, name)
}
pub fn inherited_field<T: Scalar>(
self,
load: &Load<'_>,
declared_in: &str,
name: &str,
) -> Result<Field<T>, Off> {
let offset = load.field_offset(declared_in, name)?;
self.field_at(offset).ok_or_else(|| {
Off::new(format!(
"field {declared_in}.{name}: past the end of {}",
self.name
))
})
}
}
impl EventType {
pub(crate) fn field_at<T: Scalar>(self, offset: u64) -> Option<Field<T>> {
Field::within(self.hash, self.size, offset)
}
}
pub trait Scalar: sealed::Sealed + Copy + 'static {
#[doc(hidden)]
fn from_bytes(bytes: &[u8]) -> Option<Self>;
#[doc(hidden)]
fn to_bytes(self, bytes: &mut [u8]) -> bool;
}
macro_rules! scalars {
($($ty:ty),*) => {$(
impl sealed::Sealed for $ty {}
impl Scalar for $ty {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
Some(<$ty>::from_le_bytes(bytes.get(..size_of::<$ty>())?.try_into().ok()?))
}
fn to_bytes(self, bytes: &mut [u8]) -> bool {
bytes
.get_mut(..size_of::<$ty>())
.map(|out| out.copy_from_slice(&self.to_le_bytes()))
.is_some()
}
}
)*};
}
scalars!(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64);
impl sealed::Sealed for bool {}
impl Scalar for bool {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
bytes.first().map(|&b| b != 0)
}
fn to_bytes(self, bytes: &mut [u8]) -> bool {
u8::from(self).to_bytes(bytes)
}
}
impl sealed::Sealed for Entity {}
impl Scalar for Entity {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
u32::from_bytes(bytes).map(Entity)
}
fn to_bytes(self, bytes: &mut [u8]) -> bool {
self.0.to_bytes(bytes)
}
}
#[derive(Debug)]
pub struct Field<T> {
type_hash: u32,
offset: u32,
_value: PhantomData<fn() -> T>,
}
impl<T> Clone for Field<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for Field<T> {}
impl<T: Scalar> Field<T> {
pub(crate) fn within(type_hash: u32, size: u32, offset: u64) -> Option<Self> {
let end = offset.checked_add(size_of::<T>() as u64)?;
(end <= u64::from(size)).then_some(Self {
type_hash,
offset: u32::try_from(offset).ok()?,
_value: PhantomData,
})
}
pub(crate) fn read(self, bytes: &[u8]) -> Option<T> {
T::from_bytes(bytes.get(usize::try_from(self.offset).ok()?..)?)
}
pub(crate) const fn type_hash(self) -> u32 {
self.type_hash
}
}
impl<T> Field<T> {
#[must_use]
pub const fn offset(self) -> u32 {
self.offset
}
}
#[derive(Clone, Copy, Debug)]
pub struct RawGameEvent<'a> {
type_hash: u32,
bytes: &'a [u8],
}
impl<'a> RawGameEvent<'a> {
#[must_use]
pub const fn new(type_hash: u32, bytes: &'a [u8]) -> Self {
Self { type_hash, bytes }
}
#[must_use]
pub const fn type_hash(&self) -> u32 {
self.type_hash
}
#[must_use]
pub const fn bytes(&self) -> &'a [u8] {
self.bytes
}
#[must_use]
pub fn get<T: Scalar>(&self, field: Field<T>) -> Option<T> {
if field.type_hash != self.type_hash {
return None;
}
field.read(self.bytes)
}
#[must_use]
pub fn read<T: Scalar>(&self, offset: usize) -> Option<T> {
T::from_bytes(self.bytes.get(offset..)?)
}
pub(crate) unsafe fn from_raw(raw: *const GameEventData) -> Option<Self> {
let data = unsafe { raw.as_ref() }?;
let size = usize::try_from(data.size).ok()?;
let bytes = if size == 0 || data.data.is_null() {
&[][..]
} else {
unsafe { core::slice::from_raw_parts(data.data, size) }
};
Some(Self {
type_hash: data.type_hash,
bytes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_reads_only_its_own_type_inside_the_bytes() {
let field = Field::<u32> {
type_hash: 5,
offset: 8,
_value: PhantomData,
};
let mut bytes = [0u8; 12];
bytes[8..].copy_from_slice(&42u32.to_le_bytes());
assert_eq!(RawGameEvent::new(5, &bytes).get(field), Some(42));
assert_eq!(RawGameEvent::new(6, &bytes).get(field), None);
assert_eq!(RawGameEvent::new(5, &bytes[..10]).get(field), None);
}
#[test]
fn scalars_read_little_endian() {
let bytes = [0x01, 0x02, 0x03, 0x04, 0, 0, 0x80, 0x3f];
assert_eq!(u16::from_bytes(&bytes), Some(0x0201));
assert_eq!(Entity::from_bytes(&bytes), Some(Entity(0x0403_0201)));
assert_eq!(f32::from_bytes(&bytes[4..]), Some(1.0));
assert_eq!(bool::from_bytes(&bytes[4..]), Some(false));
assert_eq!(u64::from_bytes(&bytes[1..]), None);
}
#[test]
fn scalars_write_little_endian_and_only_inside_the_bytes() {
let mut bytes = [0u8; 6];
assert!(0x0403_0201u32.to_bytes(&mut bytes));
assert!(true.to_bytes(&mut bytes[4..]));
assert!(!1u16.to_bytes(&mut bytes[5..]));
assert_eq!(bytes, [1, 2, 3, 4, 1, 0]);
let field = Field::<i16>::within(9, 6, 4).unwrap();
assert!((-2i16).to_bytes(&mut bytes[4..]));
assert_eq!(field.read(&bytes), Some(-2));
assert!(Field::<u32>::within(9, 6, 4).is_none());
}
}