use core::ffi::c_void;
use std::sync::{Arc, Mutex, PoisonError};
use crate::abi::{
Header, InventoryOp, InventoryOpResult, InventoryRequest, InventoryResult, ItemData,
};
use crate::ecs::{Components, Entity};
use crate::raw::read_at;
use crate::schedule::Scheduler;
use crate::world::{ComponentType, World};
use crate::{Host, Load, Off};
pub const MAX_OPS: usize = 64;
pub const MAX_INVENTORIES: usize = 16;
const OP_LOG: u32 = 0x200;
const SLOTS: u8 = 8;
const LINKS: usize = 16;
const MAX_DEPTH: u32 = 4;
const MAX_OWNERS: usize = 8;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotRef(pub u64);
impl SlotRef {
#[must_use]
pub const fn new(inventory: Entity, index: u8) -> Self {
Self(inventory.0 as u64 | (index as u64) << 32)
}
#[must_use]
#[expect(clippy::cast_possible_truncation, reason = "the low half")]
pub const fn inventory(self) -> Entity {
Entity(self.0 as u32)
}
#[must_use]
#[expect(clippy::cast_possible_truncation, reason = "a byte")]
pub const fn index(self) -> u8 {
(self.0 >> 32) as u8
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct Category(pub u8);
impl Category {
pub const UNKNOWN: Self = Self(0);
pub const CUSTOMIZATION: Self = Self(1);
pub const EQUIPMENT: Self = Self(2);
pub const CURRENCY: Self = Self(3);
pub const GENERIC: Self = Self(4);
pub const VIRTUAL: Self = Self(5);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Slot {
pub at: SlotRef,
pub category: Category,
pub item: u32,
pub count: u32,
pub pide: Option<Entity>,
}
impl Slot {
#[must_use]
pub const fn is_empty(&self) -> bool {
self.item == 0 || self.count == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ItemInfo {
pub id: u32,
pub max_stack: u16,
pub flags: u32,
pub category: u8,
}
impl ItemInfo {
#[must_use]
pub const fn has_pide(&self) -> bool {
self.flags & ItemData::HAS_PIDE != 0
}
#[must_use]
pub const fn is_container(&self) -> bool {
self.flags & ItemData::IS_CONTAINER != 0
}
#[must_use]
pub const fn is_blueprint(&self) -> bool {
self.flags & ItemData::IS_BLUEPRINT != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Bar {
pub current: u16,
pub max: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Fill(pub u16);
impl Fill {
pub const EMPTY: Self = Self(0);
pub const FULL: Self = Self(u16::MAX);
#[must_use]
pub fn of(count: u16, max: u16) -> Self {
if max == 0 {
return Self::EMPTY;
}
let parts = u32::from(count.min(max)) * u32::from(u16::MAX);
let max = u32::from(max);
Self(u16::try_from((parts + max / 2) / max).unwrap_or(u16::MAX))
}
#[must_use]
pub fn fraction(self) -> f32 {
f32::from(self.0) / f32::from(u16::MAX)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Op {
Give {
to: Entity,
item: u32,
count: u32,
fill: Option<Fill>,
},
Take {
from: Entity,
item: u32,
count: u32,
},
DestroySlot(SlotRef),
MoveSlot {
slot: SlotRef,
to: Entity,
amount: Option<u16>,
},
Renew {
slot: SlotRef,
root: Entity,
item: u32,
count: u32,
fill: Fill,
},
MoveAll {
from: Entity,
to: Entity,
include_special: bool,
},
}
impl Op {
fn raw(&self) -> InventoryOp {
let entity = |e: Entity| u64::from(e.0);
match *self {
Self::Give {
to,
item,
count,
fill,
} => InventoryOp {
kind: InventoryOp::GIVE,
flags: if fill.is_some() {
InventoryOp::FILLED
} else {
0
},
target: entity(to),
other: fill.map_or(0, |f| u64::from(f.fraction().to_bits())),
item,
count,
},
Self::Take { from, item, count } => InventoryOp {
kind: InventoryOp::TAKE,
target: entity(from),
item,
count,
..InventoryOp::default()
},
Self::DestroySlot(slot) => InventoryOp {
kind: InventoryOp::DESTROY_SLOT,
target: slot.0,
..InventoryOp::default()
},
Self::MoveSlot { slot, to, amount } => InventoryOp {
kind: InventoryOp::MOVE_SLOT,
target: slot.0,
other: entity(to),
count: amount.map_or(0, u32::from),
..InventoryOp::default()
},
Self::Renew {
slot,
root,
item,
count,
fill,
} => InventoryOp {
kind: InventoryOp::RENEW,
flags: InventoryOp::FILLED,
target: slot.0,
other: entity(root) | u64::from(fill.fraction().to_bits()) << 32,
item,
count,
},
Self::MoveAll {
from,
to,
include_special,
} => InventoryOp {
kind: InventoryOp::MOVE_ALL,
flags: if include_special {
InventoryOp::INCLUDE_SPECIAL
} else {
0
},
target: entity(from),
other: entity(to),
..InventoryOp::default()
},
}
}
const fn cost(&self) -> u32 {
match self {
Self::Give { .. } | Self::Take { .. } => 16,
Self::Renew { .. } => 17,
Self::DestroySlot(_) => 1,
Self::MoveSlot { .. } => 4,
Self::MoveAll { .. } => 256,
}
}
const fn inventories(&self) -> [Entity; 2] {
match *self {
Self::Give { to, .. } => [to, Entity::NONE],
Self::Take { from, .. } => [from, Entity::NONE],
Self::DestroySlot(slot) => [slot.inventory(), Entity::NONE],
Self::Renew { root, .. } => [root, Entity::NONE],
Self::MoveSlot { slot, to, .. } => [slot.inventory(), to],
Self::MoveAll { from, to, .. } => [from, to],
}
}
fn is_valid(&self) -> bool {
match *self {
Self::Give {
to: e, item, count, ..
}
| Self::Take {
from: e,
item,
count,
} => !e.is_none() && item != 0 && count != 0 && i32::try_from(count).is_ok(),
Self::DestroySlot(slot) => !slot.inventory().is_none(),
Self::Renew {
slot,
root,
item,
count,
..
} => {
!slot.inventory().is_none()
&& !root.is_none()
&& item != 0
&& count != 0
&& i32::try_from(count).is_ok()
}
Self::MoveSlot { slot, to, amount } => {
!slot.inventory().is_none() && !to.is_none() && amount != Some(0)
}
Self::MoveAll { from, to, .. } => !from.is_none() && !to.is_none() && from != to,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, thiserror::Error)]
#[non_exhaustive]
pub enum BatchError {
#[error("the batch is empty")]
Empty,
#[error("an operation is malformed")]
Malformed,
#[error("more than {MAX_OPS} operations")]
TooManyOps,
#[error("more than {MAX_INVENTORIES} inventories")]
TooManyInventories,
#[error("too much work for one transaction")]
TooLarge,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Batch {
ops: Vec<Op>,
allow_partial: bool,
notify: bool,
actor: Entity,
}
impl Batch {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, op: Op) -> Self {
self.ops.push(op);
self
}
pub fn push(&mut self, op: Op) {
self.ops.push(op);
}
#[must_use]
pub const fn allow_partial(mut self) -> Self {
self.allow_partial = true;
self
}
#[must_use]
pub const fn notify(mut self) -> Self {
self.notify = true;
self
}
#[must_use]
pub const fn actor(mut self, player: Entity) -> Self {
self.actor = player;
self
}
#[must_use]
pub fn ops(&self) -> &[Op] {
&self.ops
}
pub fn check(&self) -> Result<(), BatchError> {
if self.ops.is_empty() {
return Err(BatchError::Empty);
}
if !self.ops.iter().all(Op::is_valid) {
return Err(BatchError::Malformed);
}
if self.ops.len() > MAX_OPS {
return Err(BatchError::TooManyOps);
}
if inventories(&self.ops) > MAX_INVENTORIES {
return Err(BatchError::TooManyInventories);
}
if self.ops.iter().map(Op::cost).sum::<u32>() > OP_LOG {
return Err(BatchError::TooLarge);
}
Ok(())
}
#[must_use]
pub fn split(self) -> Vec<Self> {
let mut parts: Vec<Self> = Vec::new();
let mut part = Self {
ops: Vec::new(),
..self.clone()
};
for op in self.ops {
part.ops.push(op);
if part.ops.len() > 1 && part.check().is_err_and(|e| e != BatchError::Malformed) {
part.ops.pop();
let full = std::mem::replace(&mut part.ops, vec![op]);
parts.push(Self {
ops: full,
..part.clone()
});
}
}
if !part.ops.is_empty() {
parts.push(part);
}
parts
}
fn request_flags(&self) -> u32 {
let mut flags = 0;
if self.allow_partial {
flags |= InventoryRequest::ALLOW_PARTIAL;
}
if self.notify || self.ops.iter().any(|op| matches!(op, Op::Give { .. })) {
flags |= InventoryRequest::NOTIFY;
}
flags
}
fn actor_entity(&self) -> Entity {
if !self.actor.is_none() {
return self.actor;
}
self.ops
.iter()
.find_map(|op| match *op {
Op::Give { to, .. } | Op::MoveSlot { to, .. } | Op::MoveAll { to, .. } => Some(to),
_ => None,
})
.unwrap_or(Entity::NONE)
}
}
fn inventories(ops: &[Op]) -> usize {
let mut seen: Vec<Entity> = ops
.iter()
.flat_map(Op::inventories)
.filter(|e| !e.is_none())
.collect();
seen.sort_unstable();
seen.dedup();
seen.len()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Done {
Committed,
RolledBack,
Unavailable,
TimedOut,
Refused(BatchError),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct OpResult {
pub error: u32,
pub remaining: u32,
}
impl OpResult {
pub const TOO_LARGE: u32 = 0x27;
pub const NO_ROOM: u32 = 0x15;
#[must_use]
pub const fn ok(self) -> bool {
self.error == 0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Outcome {
pub done: Done,
pub ops: Vec<OpResult>,
}
impl Outcome {
#[must_use]
pub const fn is_committed(&self) -> bool {
matches!(self.done, Done::Committed)
}
const fn not_run(done: Done) -> Self {
Self {
done,
ops: Vec::new(),
}
}
unsafe fn from_raw(result: &InventoryResult) -> Self {
let done = match result.outcome {
InventoryResult::COMMITTED => Done::Committed,
InventoryResult::ROLLED_BACK => Done::RolledBack,
InventoryResult::TIMED_OUT => Done::TimedOut,
_ => Done::Unavailable,
};
let ops = if result.ops.is_null() || result.op_count == 0 {
&[][..]
} else {
unsafe { core::slice::from_raw_parts(result.ops, result.op_count as usize) }
};
Self {
done,
ops: ops
.iter()
.map(|r: &InventoryOpResult| OpResult {
error: r.error,
remaining: r.remaining,
})
.collect(),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Ticket(Arc<Mutex<Option<Outcome>>>);
impl Ticket {
#[must_use]
pub fn outcome(&self) -> Option<Outcome> {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone()
}
#[must_use]
pub fn is_done(&self) -> bool {
self.0
.lock()
.unwrap_or_else(PoisonError::into_inner)
.is_some()
}
}
type Callback = Box<dyn FnOnce(&Outcome) + Send>;
struct Pending {
ticket: Ticket,
done: Callback,
}
impl Pending {
fn complete(self, outcome: &Outcome) {
*self.ticket.0.lock().unwrap_or_else(PoisonError::into_inner) = Some(outcome.clone());
(self.done)(outcome);
}
}
unsafe extern "system" fn deliver(result: *const InventoryResult, user: *mut c_void) {
if user.is_null() {
return;
}
let pending = unsafe { Box::from_raw(user.cast::<Pending>()) };
let outcome = unsafe { crate::abi::read_headed(result, size_of::<InventoryResult>()) }
.map_or_else(
|| Outcome::not_run(Done::Unavailable),
|r| unsafe { Outcome::from_raw(&r) },
);
crate::host::guard(
"panic in an inventory callback",
|| (),
|| {
pending.complete(&outcome);
},
);
}
#[derive(Clone, Copy, Debug)]
struct Layout {
setup: ComponentType,
inventory: ComponentType,
durability: ComponentType,
fuel: ComponentType,
owner: ComponentType,
links: usize,
categories: usize,
initialized: usize,
stack: usize,
stack_count: usize,
stack_pide: usize,
durability_max: usize,
fuel_max: usize,
skip: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct Inventories {
host: Host,
world: World,
components: Components,
scheduler: Scheduler,
layout: Layout,
}
impl Inventories {
pub(crate) fn resolve(load: &mut Load<'_>) -> Result<Self, Off> {
let host = load.host();
let scheduler = load.scheduler()?;
let status = (host.api().inventory_open)(host.handle());
if !status.is_ok() {
return Err(Off::new(format!(
"inventories: unavailable on this game build (status {})",
status.0
)));
}
let world = load.world()?;
let components = Components::resolve(load)?;
let offset = |ty: &str, field: &str| {
load.field_offset(ty, field)
.and_then(|v| usize::try_from(v).map_err(|_| Off::new("offset")))
};
let layout = Layout {
setup: world.component_type(load, "keen::ecs::InventorySetup")?,
inventory: world.component_type(load, "keen::ecs::Inventory")?,
durability: world.component_type(load, "keen::ecs::NetworkDurability")?,
fuel: world.component_type(load, "keen::ecs::NetworkFuel")?,
owner: world.component_type(load, "keen::ecs::OwnerRelationship")?,
links: offset("keen::ecs::InventorySetup", "linksEntities")?,
categories: offset("keen::ecs::InventorySetup", "linksCategories")?,
initialized: offset("keen::ecs::InventorySetup", "isInitialized")?,
stack: usize::try_from(load.type_size("keen::ItemStack")?)
.map_err(|_| Off::new("ItemStack"))?,
stack_count: offset("keen::ItemStack", "data")?,
stack_pide: offset("keen::ItemStack", "data")? + size_of::<u32>(),
durability_max: offset("keen::ecs::NetworkDurability", "durabilityMax")?,
fuel_max: offset("keen::ecs::NetworkFuel", "fuelMax")?,
skip: usize::try_from(load.symbol("inventory.skip_entity")?)
.map_err(|_| Off::new("inventory.skip_entity"))?,
};
Ok(Self {
host,
world,
components,
scheduler,
layout,
})
}
#[must_use]
pub const fn read(&self) -> Reader<'_> {
Reader {
inventories: self,
ctx: core::ptr::null_mut(),
}
}
#[must_use]
pub const unsafe fn read_in(&self, ctx: *mut u64) -> Reader<'_> {
Reader {
inventories: self,
ctx,
}
}
#[must_use]
pub fn item_info(&self, item: u32) -> Option<ItemInfo> {
let mut out = ItemData {
header: Header::of::<ItemData>(),
id: 0,
max_stack: 0,
category: 0,
rarity: 0,
flags: 0,
};
let status = (self.host.api().item_info)(self.host.handle(), item, &raw mut out);
status.is_ok().then_some(ItemInfo {
id: out.id,
max_stack: out.max_stack,
flags: out.flags,
category: out.category,
})
}
pub fn submit(&self, batch: &Batch, done: impl FnOnce(&Outcome) + Send + 'static) -> Ticket {
let ticket = Ticket::default();
let pending = Pending {
ticket: ticket.clone(),
done: Box::new(done),
};
if let Err(error) = batch.check() {
self.later(pending, Done::Refused(error));
return ticket;
}
let ops: Vec<InventoryOp> = batch.ops.iter().map(Op::raw).collect();
let request = InventoryRequest {
header: Header::of::<InventoryRequest>(),
flags: batch.request_flags(),
actor: batch.actor_entity().0,
ops: ops.as_ptr(),
op_count: u32::try_from(ops.len()).unwrap_or(0),
reserved: 0,
};
let user = Box::into_raw(Box::new(pending));
let status = (self.host.api().inventory_submit)(
self.host.handle(),
&raw const request,
deliver,
user.cast(),
);
if !status.is_ok() {
let pending = unsafe { Box::from_raw(user) };
self.later(*pending, Done::Unavailable);
}
ticket
}
fn later(&self, pending: Pending, done: Done) {
self.scheduler
.next_tick(move || pending.complete(&Outcome::not_run(done)));
}
pub fn give(
&self,
to: Entity,
item: u32,
count: u32,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(
&Batch::new().with(Op::Give {
to,
item,
count,
fill: None,
}),
done,
)
}
pub fn take(
&self,
from: Entity,
item: u32,
count: u32,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(&Batch::new().with(Op::Take { from, item, count }), done)
}
pub fn destroy_slot(
&self,
slot: SlotRef,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(&Batch::new().with(Op::DestroySlot(slot)), done)
}
pub fn move_slot(
&self,
slot: SlotRef,
to: Entity,
amount: Option<u16>,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(&Batch::new().with(Op::MoveSlot { slot, to, amount }), done)
}
pub fn move_all(
&self,
from: Entity,
to: Entity,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
let all = Op::MoveAll {
from,
to,
include_special: false,
};
self.submit(&Batch::new().with(all), done)
}
pub fn renew(
&self,
root: Entity,
items: &[u32],
fill: Fill,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Option<Ticket> {
let mut batch = Batch::new();
for slot in self.read().slots(root) {
if !slot.is_empty() && slot.pide.is_none() && items.contains(&slot.item) {
batch.push(Op::Renew {
slot: slot.at,
root,
item: slot.item,
count: slot.count,
fill,
});
}
}
(!batch.ops().is_empty()).then(|| self.submit(&batch, done))
}
}
#[derive(Clone, Copy, Debug)]
pub struct Reader<'a> {
inventories: &'a Inventories,
ctx: *mut u64,
}
impl Reader<'_> {
fn component(&self, entity: Entity, ty: ComponentType) -> Option<*const u8> {
if entity.is_none() {
return None;
}
if self.ctx.is_null() {
self.inventories
.world
.component(entity, ty)
.map(<*const c_void>::cast)
} else {
unsafe {
self.inventories
.components
.component(self.ctx, entity, ty.hash())
}
.map(<*mut u8>::cast_const)
}
}
#[must_use]
pub fn slots(&self, root: Entity) -> Vec<Slot> {
let layout = &self.inventories.layout;
let skip = if layout.skip == 0 {
0
} else {
unsafe { read_at::<u32>(core::ptr::with_exposed_provenance(layout.skip), 0) }
};
let mut out = Vec::new();
self.walk(root, 0, skip, &mut out);
out
}
fn walk(&self, root: Entity, depth: u32, skip: u32, out: &mut Vec<Slot>) {
let layout = &self.inventories.layout;
let Some(setup) = self.component(root, layout.setup) else {
return;
};
if unsafe { read_at::<u8>(setup, layout.initialized) } == 0 {
return;
}
for i in 0..LINKS {
let link: u32 = unsafe { read_at(setup, layout.links + i * size_of::<u32>()) };
if link == 0 {
break;
}
if link == skip {
continue;
}
let category = Category(unsafe { read_at(setup, layout.categories + i) });
if category == Category::VIRTUAL {
if depth < MAX_DEPTH {
self.walk(Entity(link), depth + 1, skip, out);
}
continue;
}
if let Some(slots) = self.component(Entity(link), layout.inventory) {
out.extend((0..SLOTS).map(|index| {
unsafe { self.stack(slots, Entity(link), index, category) }
}));
}
}
}
unsafe fn stack(
&self,
slots: *const u8,
inventory: Entity,
index: u8,
category: Category,
) -> Slot {
let layout = &self.inventories.layout;
let at = usize::from(index) * layout.stack;
Slot {
at: SlotRef::new(inventory, index),
category,
item: unsafe { read_at(slots, at) },
count: unsafe { read_at(slots, at + layout.stack_count) },
pide: Entity::from_raw(unsafe { read_at(slots, at + layout.stack_pide) }),
}
}
#[must_use]
pub fn slot(&self, slot: SlotRef) -> Option<Slot> {
if slot.index() >= SLOTS {
return None;
}
let slots = self.component(slot.inventory(), self.inventories.layout.inventory)?;
Some(unsafe { self.stack(slots, slot.inventory(), slot.index(), Category::UNKNOWN) })
}
#[must_use]
pub fn count(&self, root: Entity, item: u32) -> u32 {
self.slots(root)
.iter()
.filter(|s| s.item == item && !s.is_empty())
.fold(0, |sum, s| sum.saturating_add(s.count))
}
#[must_use]
pub fn root(&self, slot: SlotRef) -> Option<Entity> {
let mut entity = slot.inventory();
let mut climbed = false;
for _ in 0..MAX_OWNERS {
let Some(owner) = self.component(entity, self.inventories.layout.owner) else {
break;
};
let owner = Entity(unsafe { read_at(owner, 0) });
if owner.is_none() || owner == entity {
break;
}
entity = owner;
climbed = true;
}
climbed.then_some(entity)
}
#[must_use]
pub fn durability(&self, pide: Entity) -> Option<Bar> {
let layout = &self.inventories.layout;
self.bar(pide, layout.durability, layout.durability_max)
}
#[must_use]
pub fn meter(&self, pide: Entity) -> Option<Bar> {
let layout = &self.inventories.layout;
self.bar(pide, layout.fuel, layout.fuel_max)
}
fn bar(&self, pide: Entity, ty: ComponentType, max: usize) -> Option<Bar> {
let data = self.component(pide, ty)?;
Some(Bar {
current: unsafe { read_at(data, 0) },
max: unsafe { read_at(data, max) },
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all(from: u32, to: u32) -> Op {
Op::MoveAll {
from: Entity(from),
to: Entity(to),
include_special: false,
}
}
fn give(to: u32) -> Op {
Op::Give {
to: Entity(to),
item: 7,
count: 1,
fill: None,
}
}
#[test]
fn slot_refs_pack_the_sub_inventory_low_and_the_index_high() {
let slot = SlotRef::new(Entity(u32::MAX), 7);
assert_eq!(slot.0, 0x7_ffff_ffff);
assert_eq!(slot.inventory(), Entity(u32::MAX));
assert_eq!(slot.index(), 7);
assert_eq!(SlotRef(0x41).index(), 0);
}
#[test]
fn batches_are_checked_against_the_game_limits() {
assert_eq!(Batch::new().check(), Err(BatchError::Empty));
assert_eq!(
Batch::new().with(all(5, 5)).check(),
Err(BatchError::Malformed)
);
let renew = Op::Renew {
slot: SlotRef::new(Entity(41), 2),
root: Entity(40),
item: 7,
count: 1,
fill: Fill::FULL,
};
assert_eq!(Batch::new().with(renew).check(), Ok(()));
let raw = renew.raw();
assert_eq!(
(raw.kind, raw.target, raw.other & 0xffff_ffff),
(InventoryOp::RENEW, SlotRef::new(Entity(41), 2).0, 40)
);
assert_eq!(raw.other >> 32, u64::from(1.0f32.to_bits()));
assert_eq!(
Batch::new()
.with(Op::Renew {
slot: SlotRef::new(Entity(41), 2),
root: Entity::NONE,
item: 7,
count: 1,
fill: Fill::FULL,
})
.check(),
Err(BatchError::Malformed)
);
let zero = Op::Give {
to: Entity(2),
item: 7,
count: 0,
fill: None,
};
assert_eq!(Batch::new().with(zero).check(), Err(BatchError::Malformed));
let mut many = Batch::new();
for _ in 0..=MAX_OPS {
many.push(Op::DestroySlot(SlotRef(0x41)));
}
assert_eq!(many.check(), Err(BatchError::TooManyOps));
let mut wide = Batch::new();
for to in 1..=17 {
wide.push(give(to));
}
assert_eq!(wide.check(), Err(BatchError::TooManyInventories));
let heavy = Batch::new().with(all(1, 2)).with(all(3, 2)).with(give(2));
assert_eq!(heavy.check(), Err(BatchError::TooLarge));
assert_eq!(Batch::new().with(all(1, 2)).with(all(3, 2)).check(), Ok(()));
}
#[test]
fn splitting_keeps_order_and_every_part_fits() {
let mut big = Batch::new().allow_partial().actor(Entity(2));
for from in 100..105 {
big.push(all(from, 2));
}
for to in 200..230 {
big.push(give(to));
}
let ops = big.ops().to_vec();
let parts = big.split();
assert!(parts.len() > 3);
assert!(parts.iter().all(|p| p.check().is_ok()));
assert!(
parts
.iter()
.all(|p| p.allow_partial && p.actor == Entity(2))
);
let joined: Vec<Op> = parts.iter().flat_map(|p| p.ops().to_vec()).collect();
assert_eq!(joined, ops);
assert_eq!(Batch::new().with(give(2)).split().len(), 1);
assert!(Batch::new().split().is_empty());
}
#[test]
fn requests_notify_for_gives_and_credit_the_first_destination() {
let loot = Batch::new().with(all(40, 2));
assert_eq!(loot.request_flags(), 0);
assert_eq!(
loot.clone().notify().request_flags(),
InventoryRequest::NOTIFY
);
assert_eq!(loot.actor_entity(), Entity(2));
let paid = Batch::new()
.with(Op::Take {
from: Entity(3),
item: 1,
count: 1,
})
.with(give(3))
.allow_partial();
assert_eq!(
paid.request_flags(),
InventoryRequest::ALLOW_PARTIAL | InventoryRequest::NOTIFY
);
assert_eq!(paid.actor_entity(), Entity(3));
assert_eq!(loot.actor(Entity(9)).actor_entity(), Entity(9));
}
#[test]
fn operations_become_the_loaders_records() {
let slot = SlotRef::new(Entity(0x41), 2);
let raw = Op::MoveSlot {
slot,
to: Entity(2),
amount: Some(3),
}
.raw();
assert_eq!(
(raw.kind, raw.target, raw.other, raw.count),
(InventoryOp::MOVE_SLOT, slot.0, 2, 3)
);
let special = Op::MoveAll {
from: Entity(40),
to: Entity(2),
include_special: true,
}
.raw();
assert_eq!(special.flags, InventoryOp::INCLUDE_SPECIAL);
}
#[test]
fn outcomes_read_the_loaders_result() {
let ops = [
InventoryOpResult {
error: 0,
remaining: 0,
},
InventoryOpResult {
error: 0x15,
remaining: 4,
},
];
let raw = InventoryResult {
header: Header::of::<InventoryResult>(),
outcome: InventoryResult::ROLLED_BACK,
op_count: 2,
ops: ops.as_ptr(),
};
let outcome = unsafe { Outcome::from_raw(&raw) };
assert_eq!(outcome.done, Done::RolledBack);
assert!(!outcome.is_committed());
assert_eq!(
outcome.ops[1],
OpResult {
error: OpResult::NO_ROOM,
remaining: 4
}
);
let empty = InventoryResult {
outcome: InventoryResult::TIMED_OUT,
op_count: 0,
ops: core::ptr::null(),
..raw
};
let outcome = unsafe { Outcome::from_raw(&empty) };
assert_eq!(outcome, Outcome::not_run(Done::TimedOut));
}
}