use core::marker::PhantomData;
use crate::abi::{Status, Str};
use crate::inventory::Slot;
use crate::{Entity, ItemId};
use crate::{Host, Load, Off};
pub const MAX_VALUE: usize = 512;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ItemInstance {
pub pide: Entity,
pub item: ItemId,
}
impl ItemInstance {
#[must_use]
pub const fn of(slot: &Slot) -> Option<Self> {
match slot.pide {
Some(pide) if !slot.is_empty() => Some(Self {
pide,
item: slot.item,
}),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SetError {
#[error("the value does not encode: {0}")]
Encode(String),
#[error("the value takes {0} bytes, more than {MAX_VALUE}")]
TooLarge(usize),
#[error("only the server sets mod data, entity data on its game thread")]
NotServer,
#[error("the world has not loaded")]
NoWorld,
#[error("no such entity")]
NoEntity,
#[error("the entity is not found again after a load")]
NotKept,
#[error("mod data refused (status {0})")]
Refused(u32),
}
pub struct ItemData<T> {
host: Host,
kind: u32,
value: PhantomData<fn() -> T>,
}
impl<T> Clone for ItemData<T> {
fn clone(&self) -> Self {
*self
}
}
impl<T> Copy for ItemData<T> {}
impl<T> core::fmt::Debug for ItemData<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("ItemData")
.field("kind", &self.kind)
.finish_non_exhaustive()
}
}
impl<T> ItemData<T>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
#[must_use]
pub fn get(&self, item: ItemInstance) -> Option<T> {
let mut buffer = [0u8; MAX_VALUE];
let mut len = 0u32;
let status = (self.host.api().item_data_get)(
self.host.handle(),
self.kind,
item.pide,
item.item,
buffer.as_mut_ptr(),
u32::try_from(buffer.len()).unwrap_or(u32::MAX),
&raw mut len,
);
if !status.is_ok() {
return None;
}
postcard::from_bytes(buffer.get(..usize::try_from(len).ok()?)?).ok()
}
pub fn set(&self, item: ItemInstance, value: &T) -> Result<(), SetError> {
let bytes = postcard::to_allocvec(value).map_err(|e| SetError::Encode(e.to_string()))?;
if bytes.is_empty() || bytes.len() > MAX_VALUE {
return Err(SetError::TooLarge(bytes.len()));
}
self.write(item, &bytes)
}
pub fn remove(&self, item: ItemInstance) -> Result<(), SetError> {
self.write(item, &[])
}
fn write(&self, item: ItemInstance, bytes: &[u8]) -> Result<(), SetError> {
let status = (self.host.api().item_data_set)(
self.host.handle(),
self.kind,
item.pide,
item.item,
bytes.as_ptr(),
u32::try_from(bytes.len()).unwrap_or(u32::MAX),
);
match status {
Status::OK => Ok(()),
Status::REFUSED => Err(SetError::NotServer),
Status::MISSING => Err(SetError::NoWorld),
Status::NOT_KEPT => Err(SetError::NotKept),
other => Err(SetError::Refused(other.0)),
}
}
}
impl Load<'_> {
pub fn item_data<T>(&mut self, id: &str) -> Result<ItemData<T>, Off>
where
T: serde::Serialize + serde::de::DeserializeOwned,
{
let host = self.host();
let mut kind = 0;
let status = (host.api().item_data_open)(host.handle(), Str::new(id), &raw mut kind);
crate::load::ok(
status,
ItemData {
host,
kind,
value: PhantomData,
},
|| format!("item data {id:?}"),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::inventory::{InventoryCategory, SlotRef};
#[test]
fn only_items_with_an_entity_are_instances() {
let slot = |item, count, pide| Slot {
at: SlotRef::new(Entity(41), 0),
category: InventoryCategory::GENERIC,
item,
count,
pide: Entity::from_raw(pide),
};
assert!(ItemInstance::of(&slot(ItemId::new(7), 1, 900)).is_some());
assert!(ItemInstance::of(&slot(ItemId::new(7), 5, 0)).is_none());
assert!(ItemInstance::of(&slot(ItemId::new(0), 0, 900)).is_none());
}
}