use core::ffi::c_void;
use crate::abi::{Header, InventoryOp, InventoryRequest, ItemData};
use crate::ecs::{ComponentType, Components};
use crate::engine_ids as ids;
use crate::raw::read_at;
use crate::schedule::Scheduler;
use crate::{Entity, ItemId};
use crate::{Host, Load, Off};
use super::ops::{Batch, Op};
use super::results::{Done, OpError, OpErrors, Outcome, Pending, Ticket, deliver};
use super::slots::{Bar, Fill, InventoryCategory, ItemInfo, Slot, SlotRef};
use super::{LINKS, MAX_DEPTH, MAX_OWNERS, SLOTS};
#[derive(Clone, Copy, Debug)]
pub(super) 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,
components: Components,
scheduler: Scheduler,
layout: Layout,
errors: OpErrors,
}
impl crate::script::Bound for Inventories {
const NAME: &'static str = "inventory";
const DOC: &'static str =
"Inventories on the server: their slots and counts, and items given, taken and moved.";
const PARTS: &'static [&'static [crate::script::Function<Self>]] = &[Self::SCRIPT_STOCK];
fn resolve(load: &mut Load<'_>) -> Result<Self, Off> {
load.inventories()
}
}
#[crate::script(stock)]
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 components = Components::resolve(load)?;
let offset = |id: &str| {
load.constant(id)
.and_then(|v| usize::try_from(v).map_err(|_| Off::new(format!("offset {id}"))))
};
let layout = Layout {
setup: ComponentType::resolve(load, ids::INVENTORY_SETUP)?,
inventory: ComponentType::resolve(load, ids::INVENTORY)?,
durability: ComponentType::resolve(load, ids::DURABILITY)?,
fuel: ComponentType::resolve(load, ids::FUEL)?,
owner: ComponentType::resolve(load, ids::OWNER)?,
links: offset(ids::INVENTORY_SETUP_LINKS)?,
categories: offset(ids::INVENTORY_SETUP_CATEGORIES)?,
initialized: offset(ids::INVENTORY_SETUP_INITIALIZED)?,
stack: usize::try_from(load.type_size(ids::ITEM_STACK)?)
.map_err(|_| Off::new(ids::ITEM_STACK))?,
stack_count: offset(ids::ITEM_STACK_COUNT)?,
stack_pide: offset(ids::ITEM_STACK_PIDE)?,
durability_max: offset(ids::DURABILITY_MAX)?,
fuel_max: offset(ids::FUEL_MAX)?,
skip: usize::try_from(load.symbol("inventory.skip_entity")?)
.map_err(|_| Off::new("inventory.skip_entity"))?,
};
Ok(Self {
host,
components,
scheduler,
layout,
errors: OpErrors::resolve(load, OpError::IDS),
})
}
#[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]
#[script]
pub fn item_info(&self, item: ItemId) -> Option<ItemInfo> {
let mut out = ItemData {
header: Header::of::<ItemData>(),
id: ItemId::new(0),
max_stack: 0,
category: 0,
rarity: 0,
flags: 0,
placed_entity: [0; 16],
};
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_size: 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),
errors: self.errors,
};
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(),
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
}
#[must_use]
#[script]
pub fn slots(&self, root: Entity) -> Vec<Slot> {
self.read().slots(root)
}
#[must_use]
#[script]
pub fn count(&self, root: Entity, item: ItemId) -> u32 {
self.read().count(root, item)
}
fn later(&self, pending: Pending, done: Done) {
self.scheduler
.next_tick(move || pending.complete(&Outcome::not_run(done)));
}
#[script]
pub fn give(
&self,
to: Entity,
item: ItemId,
count: u32,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(
&Batch::new().with(Op::Give {
to,
item,
count,
fill: None,
}),
done,
)
}
#[script]
pub fn take(
&self,
from: Entity,
item: ItemId,
count: u32,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(&Batch::new().with(Op::Take { from, item, count }), done)
}
#[script]
pub fn destroy_slot(
&self,
slot: SlotRef,
done: impl FnOnce(&Outcome) + Send + 'static,
) -> Ticket {
self.submit(&Batch::new().with(Op::DestroySlot(slot)), done)
}
#[script]
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)
}
#[script]
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: &[ItemId],
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() {
ty.read(self.inventories.host, entity)
.map(<*const c_void>::cast)
} else {
unsafe { self.inventories.components.component(self.ctx, entity, ty) }
.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 = InventoryCategory(unsafe { read_at(setup, layout.categories + i) });
if category == InventoryCategory::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: InventoryCategory,
) -> 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(),
InventoryCategory::INVALID,
)
})
}
#[must_use]
pub fn count(&self, root: Entity, item: ItemId) -> 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) },
})
}
}