#[cfg(feature = "unstable")]
use crate::Host;
#[cfg(feature = "unstable")]
use crate::ecs::ComponentType;
use crate::engine_enum::{Bits, Values};
use crate::engine_ids as ids;
use crate::game_events::values::{
AttackCategoryMask, DamageTypeMask, EquipmentSlot, ExperienceSourceType, FlameAltarActionType,
};
use crate::game_events::{EventType, Field, RawGameEvent};
#[cfg(feature = "unstable")]
use crate::raw::read_at;
use crate::{Entity, ItemId, RecipeId};
use crate::{Load, Off};
fn some(entity: Entity) -> Option<Entity> {
(!entity.is_none()).then_some(entity)
}
fn on<E, D, F>(load: &mut Load<'_>, ty: EventType, decode: D, handler: F) -> Result<(), Off>
where
D: Fn(&RawGameEvent<'_>) -> Option<E> + Send + Sync + 'static,
F: Fn(&E) + Send + Sync + 'static,
{
load.on_raw_game_event(ty, move |event| {
if let Some(typed) = decode(event) {
handler(&typed);
}
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct HitEvent {
pub root_source_id: Option<Entity>,
pub source_id: Option<Entity>,
pub target_id: Entity,
pub root_target_id: Option<Entity>,
pub weapon_pide_id: Option<Entity>,
pub health_change: i32,
pub stamina_change: i32,
pub damage_types: DamageTypeMask,
pub attack_category: AttackCategoryMask,
pub target_faction: u8,
}
impl crate::events::Kind for HitEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::HIT_EVENT)?;
let attacker: Field<Entity> = ty.field_by_id(load, ids::HIT_EVENT_ROOT_SOURCE_ID)?;
let target: Field<Entity> = ty.field_by_id(load, ids::HIT_EVENT_TARGET_ID)?;
let source: Field<Entity> = ty.field_by_id(load, ids::HIT_EVENT_SOURCE_ID)?;
let root_target: Field<Entity> = ty.field_by_id(load, ids::HIT_EVENT_ROOT_TARGET_ID)?;
let weapon: Field<Entity> = ty.field_by_id(load, ids::HIT_EVENT_WEAPON_PIDE_ID)?;
let health: Field<i32> = ty.field_by_id(load, ids::HIT_HEALTH_CHANGE)?;
let stamina: Field<i32> = ty.field_by_id(load, ids::HIT_STAMINA_CHANGE)?;
let damage: Field<u16> = ty.field_by_id(load, ids::HIT_DAMAGE_TYPES)?;
let attack: Field<u8> = ty.field_by_id(load, ids::HIT_EVENT_ATTACK_CATEGORY)?;
let faction: Field<u8> = ty.field_by_id(load, ids::HIT_TARGET_FACTION)?;
let damage_bits = Bits::resolve(load, DamageTypeMask::IDS.map(|(t, id)| (t.bits(), id)));
let attack_bits = Bits::resolve(
load,
AttackCategoryMask::IDS.map(|(c, id)| (u16::from(c.bits()), id)),
);
on(
load,
ty,
move |e| {
Some(Self {
root_source_id: some(e.get(attacker)?),
source_id: some(e.get(source)?),
target_id: e.get(target)?,
root_target_id: some(e.get(root_target)?),
weapon_pide_id: some(e.get(weapon)?),
health_change: e.get(health)?,
stamina_change: e.get(stamina)?,
damage_types: DamageTypeMask::from_bits(damage_bits.map(e.get(damage)?)),
attack_category: AttackCategoryMask::from_bits(
u8::try_from(attack_bits.map(u16::from(e.get(attack)?))).ok()?,
),
target_faction: e.get(faction)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct EntityDiedEvent {
pub dead_entity_id: Entity,
#[cfg(feature = "unstable")]
pub killer: Option<Entity>,
#[cfg(feature = "unstable")]
pub killer_player: Option<Entity>,
}
#[cfg(feature = "unstable")]
#[derive(Clone, Copy, Debug)]
struct Credit {
host: Host,
history: ComponentType,
last_index: usize,
entries: usize,
slots: usize,
entry_size: usize,
entry_attacker: usize,
experience: ComponentType,
last_player: usize,
}
#[cfg(feature = "unstable")]
impl Credit {
fn resolve(load: &mut Load<'_>) -> Result<Self, Off> {
let offset = |id: &str| {
load.constant(id).and_then(|o| {
usize::try_from(o).map_err(|_| Off::new(format!("{id}: out of range")))
})
};
let size = usize::try_from(load.type_size(ids::HIT_HISTORY)?).unwrap_or(0);
let source_size = usize::try_from(load.type_size(ids::EXPERIENCE_CREDIT)?).unwrap_or(0);
let entries = offset(ids::HIT_HISTORY_ENTRIES)?;
let entry_size = usize::try_from(load.type_size(ids::HIT_EVENT)?)
.unwrap_or(0)
.max(1);
let slots = offset(ids::HIT_HISTORY_END)?.saturating_sub(entries) / entry_size;
Ok(Self {
history: ComponentType::resolve(load, ids::HIT_HISTORY)?,
last_index: offset(ids::HIT_HISTORY_LAST_INDEX)?,
entries,
slots,
entry_size,
entry_attacker: offset(ids::HIT_EVENT_ROOT_SOURCE_ID)?,
experience: ComponentType::resolve(load, ids::EXPERIENCE_CREDIT)?,
last_player: offset(ids::EXPERIENCE_CREDIT_LAST_PLAYER)?,
host: load.host(),
})
.and_then(|credit: Self| {
let id = size_of::<u32>();
(credit.last_index + id <= size
&& credit.entries + credit.slots * credit.entry_size <= size
&& credit.entry_attacker + id <= credit.entry_size
&& credit.last_player + id <= source_size)
.then_some(credit)
.ok_or_else(|| Off::new("kill credit: an unexpected layout"))
})
}
fn killer(&self, entity: Entity) -> Option<Entity> {
let history = self.history.read(self.host, entity)?.cast::<u8>();
let last: u32 = unsafe { read_at(history, self.last_index) };
let last = usize::try_from(last).ok().filter(|&i| i < self.slots)?;
let at = self.entries + last * self.entry_size + self.entry_attacker;
some(Entity(unsafe { read_at(history, at) }))
}
fn killer_player(&self, entity: Entity) -> Option<Entity> {
let source = self.experience.read(self.host, entity)?.cast::<u8>();
some(Entity(unsafe { read_at(source, self.last_player) }))
}
}
impl crate::events::Kind for EntityDiedEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::ENTITY_DIED_EVENT)?;
let dead: Field<Entity> = ty.field_by_id(load, ids::ENTITY_DIED_EVENT_DEAD_ENTITY_ID)?;
#[cfg(feature = "unstable")]
let credit = Credit::resolve(load)
.inspect_err(|off| crate::debug!("kill credit off: {off}"))
.ok();
on(
load,
ty,
move |e| {
let entity = e.get(dead)?;
Some(Self {
dead_entity_id: entity,
#[cfg(feature = "unstable")]
killer: credit.as_ref().and_then(|c| c.killer(entity)),
#[cfg(feature = "unstable")]
killer_player: credit.as_ref().and_then(|c| c.killer_player(entity)),
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct LevelUpEvent {
pub player_entity_id: Entity,
pub level: u32,
}
impl crate::events::Kind for LevelUpEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::LEVEL_UP_EVENT)?;
let player: Field<Entity> = ty.field_by_id(load, ids::LEVEL_UP_EVENT_PLAYER_ENTITY_ID)?;
let level: Field<i32> = ty.field_by_id(load, ids::LEVEL_UP_EVENT_LEVEL)?;
on(
load,
ty,
move |e| {
Some(Self {
player_entity_id: e.get(player)?,
level: u32::try_from(e.get(level)?).ok()?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct XpGainedEvent {
pub player_entity_id: Entity,
pub amount: u32,
pub source: ExperienceSourceType,
}
impl crate::events::Kind for XpGainedEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::XP_GAINED_EVENT)?;
let player: Field<Entity> = ty.field_by_id(load, ids::XP_GAINED_EVENT_PLAYER_ENTITY_ID)?;
let amount: Field<u32> = ty.field_by_id(load, ids::XP_GAINED_EVENT_AMOUNT)?;
let source: Field<u8> = ty.field_by_id(load, ids::XP_GAINED_EVENT_SOURCE)?;
let sources = Values::resolve(load, ExperienceSourceType::IDS);
on(
load,
ty,
move |e| {
Some(Self {
player_entity_id: e.get(player)?,
amount: e.get(amount)?,
source: {
let raw = e.get(source)?;
sources
.variant(u64::from(raw))
.unwrap_or(ExperienceSourceType::Other(raw))
},
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct CraftRecipeEvent {
pub crafting_operator_id: Entity,
pub crafting_station_id: Option<Entity>,
pub recipe_id: RecipeId,
pub recipe_amount: u16,
}
impl crate::events::Kind for CraftRecipeEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::CRAFT_RECIPE_EVENT)?;
let player: Field<Entity> =
ty.field_by_id(load, ids::CRAFT_RECIPE_EVENT_CRAFTING_OPERATOR_ID)?;
let station: Field<Entity> =
ty.field_by_id(load, ids::CRAFT_RECIPE_EVENT_CRAFTING_STATION_ID)?;
let recipe: Field<RecipeId> = ty.field_by_id(load, ids::CRAFT_RECIPE_EVENT_RECIPE_ID)?;
let amount: Field<u16> = ty.field_by_id(load, ids::CRAFT_RECIPE_EVENT_RECIPE_AMOUNT)?;
on(
load,
ty,
move |e| {
Some(Self {
crafting_operator_id: e.get(player)?,
crafting_station_id: some(e.get(station)?),
recipe_id: e.get(recipe)?,
recipe_amount: e.get(amount)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct CraftRecipeSuccessEvent {
pub workshop_id: u32,
pub recipe_id: RecipeId,
}
impl crate::events::Kind for CraftRecipeSuccessEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::CRAFT_RECIPE_SUCCESS_EVENT)?;
let workshop: Field<u32> =
ty.field_by_id(load, ids::CRAFT_RECIPE_SUCCESS_EVENT_WORKSHOP_ID)?;
let recipe: Field<RecipeId> =
ty.field_by_id(load, ids::CRAFT_RECIPE_SUCCESS_EVENT_RECIPE_ID)?;
on(
load,
ty,
move |e| {
Some(Self {
workshop_id: e.get(workshop)?,
recipe_id: e.get(recipe)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
#[expect(
clippy::struct_field_names,
reason = "the fields take the game's names, which all end in Id"
)]
pub struct AcceptOfferedInteractionEvent {
pub guest_entity_id: Entity,
pub host_entity_id: Entity,
pub offer_id: u32,
}
impl crate::events::Kind for AcceptOfferedInteractionEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::ACCEPT_OFFERED_INTERACTION_EVENT)?;
let player: Field<Entity> =
ty.field_by_id(load, ids::ACCEPT_OFFERED_INTERACTION_EVENT_GUEST_ENTITY_ID)?;
let target: Field<Entity> =
ty.field_by_id(load, ids::ACCEPT_OFFERED_INTERACTION_EVENT_HOST_ENTITY_ID)?;
let offer: Field<u32> =
ty.field_by_id(load, ids::ACCEPT_OFFERED_INTERACTION_EVENT_OFFER_ID)?;
on(
load,
ty,
move |e| {
Some(Self {
guest_entity_id: e.get(player)?,
host_entity_id: e.get(target)?,
offer_id: e.get(offer)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, crate::Script)]
#[non_exhaustive]
pub struct Block {
pub owner_id: Entity,
pub position: [f32; 3],
pub volume_min: [f32; 3],
pub volume_max: [f32; 3],
pub material: u32,
}
#[derive(Clone, Copy, Debug)]
struct BlockIds {
owner_id: &'static str,
position: &'static str,
min: &'static str,
max: &'static str,
material: &'static str,
}
#[derive(Clone, Copy, Debug)]
struct BlockFields {
owner_id: Field<Entity>,
position: [Field<f32>; 3],
min: [Field<f32>; 3],
max: [Field<f32>; 3],
material: Field<u32>,
}
impl BlockFields {
fn resolve(load: &Load<'_>, ty: EventType, ids: BlockIds) -> Result<Self, Off> {
let vector = |id: &str| -> Result<[Field<f32>; 3], Off> {
let at: Field<f32> = ty.field_by_id(load, id)?;
let next = |step: u64| {
let offset = u64::from(at.offset()) + step;
ty.field_at::<f32>(offset)
.ok_or_else(|| Off::new(format!("{id}: past the end of {}", ty.name())))
};
Ok([at, next(4)?, next(8)?])
};
Ok(Self {
owner_id: ty.field_by_id(load, ids.owner_id)?,
position: vector(ids.position)?,
min: vector(ids.min)?,
max: vector(ids.max)?,
material: ty.field_by_id(load, ids.material)?,
})
}
fn read(&self, e: &RawGameEvent<'_>) -> Option<Block> {
let vector = |fields: [Field<f32>; 3]| -> Option<[f32; 3]> {
Some([e.get(fields[0])?, e.get(fields[1])?, e.get(fields[2])?])
};
Some(Block {
owner_id: e.get(self.owner_id)?,
position: vector(self.position)?,
volume_min: vector(self.min)?,
volume_max: vector(self.max)?,
material: e.get(self.material)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, crate::Script)]
#[non_exhaustive]
pub struct BuildingPlaceEvent {
pub block: Block,
pub tracking_item_id: ItemId,
}
impl crate::events::Kind for BuildingPlaceEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::BUILDING_PLACE_EVENT)?;
let block = BlockFields::resolve(
load,
ty,
BlockIds {
owner_id: ids::BUILDING_PLACE_EVENT_OWNER_ID,
position: ids::BUILDING_PLACE_EVENT_POSITION,
min: ids::BUILDING_PLACE_EVENT_VOLUME_MIN,
max: ids::BUILDING_PLACE_EVENT_VOLUME_MAX,
material: ids::BUILDING_PLACE_EVENT_MATERIAL,
},
)?;
let item: Field<ItemId> =
ty.field_by_id(load, ids::BUILDING_PLACE_EVENT_TRACKING_ITEM_ID)?;
on(
load,
ty,
move |e| {
Some(Self {
block: block.read(e)?,
tracking_item_id: e.get(item)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, crate::Script)]
#[non_exhaustive]
pub struct BuildingTearDownEvent {
pub block: Block,
}
impl crate::events::Kind for BuildingTearDownEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::BUILDING_TEAR_DOWN_EVENT)?;
let block = BlockFields::resolve(
load,
ty,
BlockIds {
owner_id: ids::BUILDING_TEAR_DOWN_EVENT_OWNER_ID,
position: ids::BUILDING_TEAR_DOWN_EVENT_POSITION,
min: ids::BUILDING_TEAR_DOWN_EVENT_VOLUME_MIN,
max: ids::BUILDING_TEAR_DOWN_EVENT_VOLUME_MAX,
material: ids::BUILDING_TEAR_DOWN_EVENT_MATERIAL,
},
)?;
on(
load,
ty,
move |e| {
Some(Self {
block: block.read(e)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, crate::Script)]
#[non_exhaustive]
pub struct BuildingPickUpEvent {
pub block: Block,
pub picked_entity_id: Option<Entity>,
}
impl crate::events::Kind for BuildingPickUpEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::BUILDING_PICK_UP_EVENT)?;
let block = BlockFields::resolve(
load,
ty,
BlockIds {
owner_id: ids::BUILDING_PICK_UP_EVENT_OWNER_ID,
position: ids::BUILDING_PICK_UP_EVENT_POSITION,
min: ids::BUILDING_PICK_UP_EVENT_VOLUME_MIN,
max: ids::BUILDING_PICK_UP_EVENT_VOLUME_MAX,
material: ids::BUILDING_PICK_UP_EVENT_MATERIAL,
},
)?;
let picked: Field<Entity> =
ty.field_by_id(load, ids::BUILDING_PICK_UP_EVENT_PICKED_ENTITY_ID)?;
on(
load,
ty,
move |e| {
Some(Self {
block: block.read(e)?,
picked_entity_id: some(e.get(picked)?),
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct FlameAltarEvent {
pub player_entity_id: Entity,
pub base_entity_id: Entity,
pub r#type: FlameAltarActionType,
}
impl crate::events::Kind for FlameAltarEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::FLAME_ALTAR_EVENT)?;
let player: Field<Entity> =
ty.field_by_id(load, ids::FLAME_ALTAR_EVENT_PLAYER_ENTITY_ID)?;
let altar: Field<Entity> = ty.field_by_id(load, ids::FLAME_ALTAR_EVENT_BASE_ENTITY_ID)?;
let kind: Field<u8> = ty.field_by_id(load, ids::FLAME_ALTAR_EVENT_TYPE)?;
let actions = Values::resolve(load, FlameAltarActionType::IDS);
on(
load,
ty,
move |e| {
Some(Self {
player_entity_id: e.get(player)?,
base_entity_id: e.get(altar)?,
r#type: {
let raw = e.get(kind)?;
actions
.variant(u64::from(raw))
.unwrap_or(FlameAltarActionType::Other(raw))
},
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct LootInteractionEvent {
pub guest_id: Entity,
pub host_id: Entity,
pub loot_all: bool,
}
impl crate::events::Kind for LootInteractionEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::LOOT_INTERACTION_EVENT)?;
let player: Field<Entity> = ty.field_by_id(load, ids::LOOT_INTERACTION_EVENT_GUEST_ID)?;
let container: Field<Entity> = ty.field_by_id(load, ids::LOOT_INTERACTION_EVENT_HOST_ID)?;
let all: Field<bool> = ty.field_by_id(load, ids::LOOT_INTERACTION_EVENT_LOOT_ALL)?;
on(
load,
ty,
move |e| {
Some(Self {
guest_id: e.get(player)?,
host_id: e.get(container)?,
loot_all: e.get(all)?,
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct HandleEquipmentSlotChangedEvent {
pub player_entity_id: Entity,
pub equipment_slot: EquipmentSlot,
}
impl crate::events::Kind for HandleEquipmentSlotChangedEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::HANDLE_EQUIPMENT_SLOT_CHANGED_EVENT)?;
let player: Field<Entity> = ty.field_by_id(
load,
ids::HANDLE_EQUIPMENT_SLOT_CHANGED_EVENT_PLAYER_ENTITY_ID,
)?;
let slot: Field<u8> = ty.field_by_id(
load,
ids::HANDLE_EQUIPMENT_SLOT_CHANGED_EVENT_EQUIPMENT_SLOT,
)?;
let slots = Values::resolve(load, EquipmentSlot::IDS);
on(
load,
ty,
move |e| {
Some(Self {
player_entity_id: e.get(player)?,
equipment_slot: {
let raw = e.get(slot)?;
slots
.variant(u64::from(raw))
.unwrap_or(EquipmentSlot::Other(raw))
},
})
},
handler,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, crate::Script)]
#[non_exhaustive]
pub struct RevivedPlayerEvent {
pub target_entity_id: Entity,
pub source_entity_id: Option<Entity>,
}
impl crate::events::Kind for RevivedPlayerEvent {
type Data = Self;
fn subscribe<F>(load: &mut Load<'_>, handler: F) -> Result<(), Off>
where
F: Fn(&Self) + Send + Sync + 'static,
{
let ty = load.game_event_type(ids::REVIVED_PLAYER_EVENT)?;
let player: Field<Entity> =
ty.field_by_id(load, ids::REVIVED_PLAYER_EVENT_TARGET_ENTITY_ID)?;
let by: Field<Entity> = ty.field_by_id(load, ids::REVIVED_PLAYER_EVENT_SOURCE_ENTITY_ID)?;
on(
load,
ty,
move |e| {
Some(Self {
target_entity_id: e.get(player)?,
source_entity_id: some(e.get(by)?),
})
},
handler,
)
}
}