mod drops;
pub use drops::{Drop, Kinds, LootContainerId, MeterRoll, Roll, Source, replace_drops};
pub(crate) use drops::{replace_group, watch_rolls};
use crate::Entity;
use crate::ecs::{ComponentType, Components, Ecs};
use crate::engine_ids as ids;
use crate::raw::read_at;
use crate::{Load, Off};
pub const SYSTEM: &str = "destroy_inventory_on_loot";
const ROW_WORDS: usize = 9;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct InteractionOfferRuntime {
pub guest_id: Option<Entity>,
pub last_accept_time: i64,
pub is_disabled: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Container {
pub entity: Entity,
pub position: [f64; 3],
pub is_empty: bool,
pub expires: bool,
pub area_loot: Option<bool>,
pub offer: Option<InteractionOfferRuntime>,
}
impl Container {
#[must_use]
pub const fn is_enemy_loot(&self) -> bool {
self.expires && matches!(self.area_loot, Some(true))
}
#[must_use]
pub const fn is_open(&self) -> bool {
self.is_enemy_loot()
&& !self.is_empty
&& matches!(
self.offer,
Some(InteractionOfferRuntime {
is_disabled: false,
..
})
)
}
}
#[derive(Clone, Copy, Debug)]
struct Layout {
transform: ComponentType,
empty_state: ComponentType,
destroy: ComponentType,
lifetime: ComponentType,
area_loot: ComponentType,
offer: ComponentType,
player_input: ComponentType,
is_empty: usize,
exclude: usize,
accept_time: usize,
accept_guest: usize,
disabled: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct Containers {
ecs: Ecs,
components: Components,
layout: Layout,
}
impl Containers {
pub fn resolve(load: &mut Load<'_>) -> Result<Self, Off> {
let ty = |id: &str| ComponentType::resolve(load, id);
let offset = |id: &str| {
load.constant(id)
.and_then(|v| usize::try_from(v).map_err(|_| Off::new(format!("offset {id}"))))
};
let layout = Layout {
transform: ty(ids::CURRENT_TRANSFORM)?,
empty_state: ty(ids::INVENTORY_EMPTY_STATE)?,
destroy: ty(ids::DESTROY_ON_LOOT)?,
lifetime: ty(ids::LIFETIME)?,
area_loot: ty(ids::AREA_LOOT)?,
offer: ty(ids::INTERACTION_OFFER)?,
player_input: ty(ids::PLAYER_INPUT)?,
is_empty: offset(ids::INVENTORY_EMPTY_STATE_EMPTY)?,
exclude: offset(ids::AREA_LOOT_EXCLUDE)?,
accept_time: offset(ids::INTERACTION_OFFER_ACCEPT_TIME)?,
accept_guest: offset(ids::INTERACTION_OFFER_ACCEPT_GUEST)?,
disabled: offset(ids::INTERACTION_OFFER_DISABLED)?,
};
Ok(Self {
ecs: Ecs::resolve(load)?,
components: Components::resolve(load)?,
layout,
})
}
pub unsafe fn each(&self, ctx: *mut u64, mut f: impl FnMut(&Container)) {
let saved = unsafe { self.ecs.row(ctx) };
unsafe { self.ecs.set_row(ctx, 0) };
let mut query = unsafe { self.ecs.query::<ROW_WORDS>(ctx) };
while query.next().is_some() {
let Some(entity) = (unsafe { self.ecs.entity(ctx) }) else {
continue;
};
if let Some(container) = unsafe { self.container(ctx, entity) } {
f(&container);
}
}
unsafe { self.ecs.set_row(ctx, saved) };
}
#[must_use]
pub unsafe fn container(&self, ctx: *mut u64, entity: Entity) -> Option<Container> {
let layout = &self.layout;
let read = |hash| unsafe { self.components.component(ctx, entity, hash) };
read(layout.destroy)?;
let state = read(layout.empty_state)?;
let position = unsafe { crate::ecs::transform_position(read(layout.transform)?) };
let empty = unsafe { read_at::<u8>(state, layout.is_empty) } != 0;
let area_loot = read(layout.area_loot).map(|data| {
unsafe { read_at::<u8>(data, layout.exclude) == 0 }
});
let offer = read(layout.offer).map(|data| InteractionOfferRuntime {
guest_id: Entity::from_raw(unsafe { read_at(data, layout.accept_guest) }),
last_accept_time: unsafe { read_at(data, layout.accept_time) },
is_disabled: unsafe { read_at::<u8>(data, layout.disabled) } != 0,
});
Some(Container {
entity,
position,
is_empty: empty,
expires: read(layout.lifetime).is_some(),
area_loot,
offer,
})
}
#[must_use]
pub unsafe fn position(&self, ctx: *mut u64, entity: Entity) -> Option<[f64; 3]> {
let data = unsafe {
self.components
.component(ctx, entity, self.layout.transform)
}?;
Some(unsafe { crate::ecs::transform_position(data) })
}
#[must_use]
pub unsafe fn is_player(&self, ctx: *mut u64, entity: Entity) -> bool {
unsafe {
self.components
.component(ctx, entity, self.layout.player_input)
}
.is_some()
}
}
#[derive(Debug)]
pub struct LootPass<'a> {
containers: &'a Containers,
ctx: *mut u64,
}
impl LootPass<'_> {
pub fn each(&self, f: impl FnMut(&Container)) {
unsafe { self.containers.each(self.ctx, f) };
}
#[must_use]
pub fn container(&self, entity: Entity) -> Option<Container> {
unsafe { self.containers.container(self.ctx, entity) }
}
#[must_use]
pub fn position(&self, entity: Entity) -> Option<[f64; 3]> {
unsafe { self.containers.position(self.ctx, entity) }
}
#[must_use]
pub fn is_player(&self, entity: Entity) -> bool {
unsafe { self.containers.is_player(self.ctx, entity) }
}
}
type PassHandler = Box<dyn Fn(&LootPass<'_>) + Send + Sync>;
static PASS: crate::fresh::Fresh<(Containers, crate::ecs::Worlds, PassHandler)> =
crate::fresh::Fresh::new();
crate::detour! {
static CHECK: unsafe extern "system" fn(ctx: *mut u64) = after_check;
}
fn after_check(next: crate::Next<unsafe extern "system" fn(*mut u64)>, ctx: *mut u64) {
unsafe { next.get()(ctx) };
if let Some((containers, worlds, handler)) = PASS.get()
&& unsafe { worlds.is_server(ctx) }
{
crate::host::guard(
"panic in a loot pass handler",
|| (),
|| handler(&LootPass { containers, ctx }),
);
}
}
impl Load<'_> {
pub fn on_loot_pass<F>(&mut self, handler: F) -> Result<(), Off>
where
F: Fn(&LootPass<'_>) + Send + Sync + 'static,
{
let containers = Containers::resolve(self)?;
let worlds = crate::ecs::Worlds::resolve(self)?;
let system = self.system(SYSTEM)?;
if PASS.set((containers, worlds, Box::new(handler))).is_err() {
return Err(Off::new("one loot pass handler per mod"));
}
unsafe { self.hook(&CHECK, system) }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn corpse() -> Container {
Container {
entity: Entity(900),
position: [0.0; 3],
is_empty: false,
expires: true,
area_loot: Some(true),
offer: Some(InteractionOfferRuntime {
guest_id: None,
last_accept_time: 0,
is_disabled: false,
}),
}
}
#[test]
fn only_open_enemy_loot_passes() {
assert!(corpse().is_open());
let chest = Container {
expires: false,
..corpse()
};
assert!(!chest.is_enemy_loot());
let dropped = Container {
area_loot: Some(false),
..corpse()
};
assert!(!dropped.is_enemy_loot());
let quest = Container {
area_loot: None,
..corpse()
};
assert!(!quest.is_enemy_loot());
let looted = Container {
is_empty: true,
..corpse()
};
assert!(looted.is_enemy_loot() && !looted.is_open());
let closed = Container {
offer: Some(InteractionOfferRuntime {
is_disabled: true,
..corpse().offer.unwrap()
}),
..corpse()
};
assert!(!closed.is_open());
let silent = Container {
offer: None,
..corpse()
};
assert!(!silent.is_open());
}
}