#[cfg(test)]
mod tests {
use std::sync::atomic::AtomicU32;
use std::sync::{Mutex, OnceLock};
use enshwick::Entity;
use enshwick::abi::{
InventoryOp, InventoryOpResult, InventoryResult, ItemData, Status, TickData,
};
use enshwick::events::Ticked;
use enshwick::inventory::{
Bar, Batch, BatchError, Done, Fill, Inventories, InventoryCategory, Op, Outcome, SlotRef,
};
use enshwick::test::FakeHost;
const SETUP: u32 = 0xc24d_f82a;
const INVENTORY: u32 = 0xb195_28a9;
const DURABILITY: u32 = 0x43c1_2f80;
const OWNER: u32 = 0xd736_1788;
const FUEL: u32 = 0x1555_7d63;
const CORPSE: u32 = 40;
const PLAYER: u32 = 2;
const SWORD: u32 = 0x5e01;
const COIN: u32 = 0x0c01;
static INVENTORIES: OnceLock<Inventories> = OnceLock::new();
static SKIPPED: AtomicU32 = AtomicU32::new(44);
static OUTCOMES: Mutex<Vec<Outcome>> = Mutex::new(Vec::new());
struct Looter;
impl enshwick::Mod for Looter {
fn manifest() -> &'static enshwick::abi::Manifest {
enshwick::manifest!(id: "you.looter", side: SERVER)
}
fn init(load: &mut enshwick::Load<'_>) -> Result<Self, enshwick::Off> {
let _first = INVENTORIES.set(load.inventories()?);
Ok(Self)
}
}
enshwick::declare_mod!(Looter);
fn setup(links: &[(u32, u8)]) -> Vec<u8> {
let mut bytes = vec![0u8; 0x54];
for (i, &(entity, category)) in links.iter().enumerate() {
bytes[i * 4..i * 4 + 4].copy_from_slice(&entity.to_le_bytes());
bytes[0x40 + i] = category;
}
bytes[0x53] = 1;
bytes
}
fn stacks(stacks: &[(u32, u32, u32)]) -> Vec<u8> {
let mut bytes = vec![0u8; 0x60];
for (i, &(item, count, pide)) in stacks.iter().enumerate() {
let at = i * 12;
bytes[at..at + 4].copy_from_slice(&item.to_le_bytes());
bytes[at + 4..at + 8].copy_from_slice(&count.to_le_bytes());
bytes[at + 8..at + 12].copy_from_slice(&pide.to_le_bytes());
}
bytes
}
fn world() -> FakeHost {
FakeHost::new()
.symbol("ecs.type_index", 0x1_4064_5cc0)
.symbol("ecs.component_read_raw", 0x1_4064_6b00)
.symbol("ecs.slot_root", 0x1_4093_5420)
.symbol("inventory.skip_entity", SKIPPED.as_ptr().addr() as u64)
.constant("ecs.context_entity_world", 0x30)
.constant("ecs.registry_type_map", 0x30)
.constant("ecs.registry_type_map_fallback", 0x88)
.type_hash("current_transform", 1)
.type_hash("used_item", 2)
.type_hash("inventory_setup", SETUP)
.type_hash("inventory", INVENTORY)
.type_hash("durability", DURABILITY)
.type_hash("fuel", FUEL)
.type_hash("owner", OWNER)
.constant("inventory_setup.links", 0)
.constant("inventory_setup.categories", 0x40)
.constant("inventory_setup.initialized", 0x53)
.constant("item_stack.count", 4)
.constant("item_stack.pide", 8)
.constant("durability.max", 2)
.constant("fuel.max", 2)
.type_size("item_stack", 12)
.component(Entity(CORPSE), SETUP, &setup(&[(41, 4), (44, 4), (42, 5)]))
.component(Entity(42), SETUP, &setup(&[(43, 4)]))
.component(Entity(41), INVENTORY, &stacks(&[(SWORD, 1, 900), (COIN, 7, 0)]))
.component(Entity(43), INVENTORY, &stacks(&[(0, 0, 0), (COIN, 5, 0)]))
.component(Entity(44), INVENTORY, &stacks(&[(COIN, 99, 0)]))
.component(Entity(41), OWNER, &CORPSE.to_le_bytes())
.component(Entity(900), DURABILITY, &[40, 0, 160, 0])
.component(Entity(901), FUEL, &[2, 0, 3, 0])
.item(enshwick::ItemId::new(SWORD), 1, ItemData::HAS_PIDE)
}
fn record(outcome: &Outcome) {
OUTCOMES.lock().unwrap().push(outcome.clone());
}
#[test]
fn a_mod_reads_a_corpse_and_takes_everything() {
let loader = world().install();
assert_eq!(unsafe { loader.init(enshwick_mod_init) }, Status::OK);
let inventories = INVENTORIES.get().unwrap();
let reader = inventories.read();
let slots = reader.slots(Entity(CORPSE));
assert_eq!(slots.len(), 16);
assert_eq!(slots[0].at, SlotRef::new(Entity(41), 0));
assert_eq!(
(slots[0].item, slots[0].pide, slots[0].category),
(
enshwick::ItemId::new(SWORD),
Some(Entity(900)),
InventoryCategory::GENERIC
)
);
assert_eq!(slots[9].at, SlotRef::new(Entity(43), 1));
assert_eq!(slots.iter().filter(|s| !s.is_empty()).count(), 3);
assert_eq!(
reader.count(Entity(CORPSE), enshwick::ItemId::new(COIN)),
12
);
assert_eq!(
reader.slot(SlotRef::new(Entity(41), 1)).map(|s| s.count),
Some(7)
);
assert_eq!(
reader.root(SlotRef::new(Entity(41), 1)),
Some(Entity(CORPSE))
);
assert_eq!(reader.root(SlotRef::new(Entity(43), 1)), None);
let durability = reader.durability(Entity(900)).unwrap();
assert_eq!((durability.current, durability.max), (40, 160));
assert_eq!(reader.meter(Entity(900)), None);
assert_eq!(reader.meter(Entity(901)), Some(Bar { current: 2, max: 3 }));
assert!(
inventories
.item_info(enshwick::ItemId::new(SWORD))
.is_some_and(|i| i.has_pide() && i.max_stack_size == 1)
);
assert!(inventories.item_info(enshwick::ItemId::new(COIN)).is_none());
let ticket = inventories.move_all(Entity(CORPSE), Entity(PLAYER), record);
let requests = loader.inventory_requests();
assert_eq!(requests.len(), 1);
assert_eq!((requests[0].flags, requests[0].actor), (0, PLAYER));
let op = requests[0].ops[0];
assert_eq!(
(op.kind, op.target, op.other),
(InventoryOp::MOVE_ALL, 40, 2)
);
assert!(!ticket.is_done());
let ran = InventoryOpResult {
error: 0,
remaining: 0,
};
assert!(loader.finish_inventory(InventoryResult::COMMITTED, &[ran]));
assert!(ticket.outcome().is_some_and(|o| o.is_committed()));
let filled = Op::Give {
to: Entity(PLAYER),
item: enshwick::ItemId::new(SWORD),
count: 1,
fill: Some(Fill::of(2, 3)),
};
let _ticket = inventories.submit(&Batch::new().with(filled), record);
let op = loader.inventory_requests()[0].ops[0];
assert_eq!(
(op.kind, op.flags),
(InventoryOp::GIVE, InventoryOp::FILLED)
);
let fill = f32::from_bits(u32::try_from(op.other).unwrap());
assert!((fill * 3.0 - 2.0).abs() < 1e-3);
assert!(loader.finish_inventory(InventoryResult::COMMITTED, &[ran]));
let refused = inventories.submit(&Batch::new(), record);
assert!(loader.inventory_requests().is_empty());
assert!(!refused.is_done());
let tick = TickData::new(1);
assert_eq!(loader.raise::<Ticked>(&tick), 1);
assert_eq!(
refused.outcome().map(|o| o.done),
Some(Done::Refused(BatchError::Empty))
);
let outcomes = OUTCOMES.lock().unwrap();
assert_eq!(outcomes.len(), 3);
assert_eq!(outcomes[0].done, Done::Committed);
}
}