use crate::abi::{InventoryOp, InventoryRequest};
use crate::{Entity, ItemId};
use super::slots::{Fill, SlotRef};
use super::{MAX_INVENTORIES, MAX_OPS, OP_LOG, inventories};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Op {
Give {
to: Entity,
item: ItemId,
count: u32,
fill: Option<Fill>,
},
Take {
from: Entity,
item: ItemId,
count: u32,
},
DestroySlot(SlotRef),
MoveSlot {
slot: SlotRef,
to: Entity,
amount: Option<u16>,
},
Renew {
slot: SlotRef,
root: Entity,
item: ItemId,
count: u32,
fill: Fill,
},
MoveAll {
from: Entity,
to: Entity,
include_special: bool,
},
}
impl Op {
pub(super) 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,
}
}
pub(super) 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.get() != 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.get() != 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 {
pub(super) ops: Vec<Op>,
pub(super) allow_partial: bool,
notify: bool,
pub(super) 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
}
pub(super) 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
}
pub(super) 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)
}
}