mod containers;
mod entities;
mod geometry;
mod players;
mod rename;
mod stats;
mod vitals;
use core::ffi::c_void;
use crate::abi::{EntityPlace, Header, PlayerInfo, Status, Str};
use crate::ecs::Entity;
use crate::engine_ids as ids;
use crate::game_events::{Field, Scalar};
use crate::{Host, Load, Off};
pub use containers::ContainerKinds;
pub use geometry::near;
pub use rename::Rename;
pub use stats::{Meter, Progress, Skill, Vitals};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ComponentType(u32);
impl ComponentType {
#[must_use]
pub const fn hash(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WritableType {
ty: ComponentType,
size: u32,
name: &'static str,
}
impl WritableType {
#[must_use]
pub const fn ty(self) -> ComponentType {
self.ty
}
#[must_use]
pub const fn size(self) -> u32 {
self.size
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
pub fn field<T: Scalar>(self, load: &Load<'_>, name: &str) -> Result<Field<T>, Off> {
let offset = load.field_offset(self.name, name)?;
Field::within(self.ty.0, self.size, offset).ok_or_else(|| {
Off::new(format!(
"field {}.{name}: past the end of the type",
self.name
))
})
}
pub(crate) fn field_by_id<T: Scalar>(self, load: &Load<'_>, id: &str) -> Result<Field<T>, Off> {
let offset = load.constant(id)?;
Field::within(self.ty.0, self.size, offset)
.ok_or_else(|| Off::new(format!("field {id}: past the end of {}", self.name)))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Player {
pub entity: Entity,
pub slot: u32,
pub key: u64,
pub permissions: u8,
pub name: String,
}
impl Player {
pub(crate) fn from_info(info: &PlayerInfo) -> Self {
let name = &info.name[..usize::from(info.name_len).min(info.name.len())];
Self {
entity: Entity(info.entity),
slot: info.slot,
key: info.key,
permissions: info.permissions,
name: String::from_utf8_lossy(name).into_owned(),
}
}
#[must_use]
pub fn is_named(&self, name: &str) -> bool {
self.name.to_lowercase() == name.to_lowercase()
}
#[must_use]
pub const fn is_admin(&self) -> bool {
self.permissions & PlayerInfo::CAN_KICK_BAN != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Placed {
pub entity: Entity,
pub position: [f64; 3],
}
impl Placed {
fn from_abi(place: &EntityPlace) -> Self {
Self {
entity: Entity(place.entity),
position: crate::ecs::metres(place.position),
}
}
#[must_use]
pub fn distance(&self, point: [f64; 3]) -> f64 {
let [x, y, z] = [0, 1, 2].map(|i| self.position[i] - point[i]);
(x * x + y * y + z * z).sqrt()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Notice {
OnScreen,
Chat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum WorldError {
#[error("not on the server's game thread")]
WrongThread,
#[error("the entity is not in the world")]
Gone,
#[error("not available on this loader or game build")]
Missing,
#[error("refused by the world")]
Refused,
#[error("the entity lacks the component")]
Lacks,
}
impl WorldError {
pub(crate) fn check(status: Status, missing: Self) -> Result<(), Self> {
match status {
Status::OK => Ok(()),
Status::WRONG_THREAD => Err(Self::WrongThread),
Status::REFUSED | Status::BAD_ARGS => Err(Self::Refused),
Status::MISSING => Err(missing),
_ => Err(Self::Missing),
}
}
pub(crate) fn from_status(status: Status) -> Result<(), Self> {
Self::check(status, Self::Missing)
}
}
fn blank_info() -> PlayerInfo {
PlayerInfo {
header: Header::of::<PlayerInfo>(),
slot: 0,
entity: 0,
key: 0,
permissions: 0,
name_len: 0,
name: [0; 64],
}
}
#[derive(Clone, Copy, Debug)]
pub struct World(&'static Resolved);
const _: () = assert!(size_of::<World>() == size_of::<usize>());
#[derive(Debug)]
struct Resolved {
host: Host,
transform: ComponentType,
used_item: ComponentType,
label: Option<ComponentType>,
stats: Option<stats::Stats>,
containers: Option<containers::Containers>,
}
static RESOLVED: crate::state::ModLocal<Resolved> = crate::state::ModLocal::new();
fn type_named(host: Host, name: &str) -> Result<ComponentType, Off> {
let mut hash = 0;
let status = (host.api().type_hash)(Str::new(name), &raw mut hash);
crate::load::ok(status, ComponentType(hash), || {
format!("component type {name}")
})
}
impl World {
pub(crate) fn resolve(load: &Load<'_>) -> Result<Self, Off> {
if let Some(resolved) = RESOLVED.get() {
return Ok(Self(resolved));
}
let host = load.host();
let resolved = Resolved {
host,
transform: type_named(host, ids::CURRENT_TRANSFORM)?,
used_item: type_named(host, ids::USED_ITEM)?,
label: type_named(host, ids::LABEL).ok(),
containers: containers::Containers::resolve(host),
stats: stats::Stats::resolve(load, host)
.inspect_err(|off| crate::debug!("stat reads off: {off}"))
.ok(),
};
Ok(Self(RESOLVED.get_or_init(|| resolved)))
}
pub fn component_type(self, _load: &Load<'_>, name: &str) -> Result<ComponentType, Off> {
type_named(self.0.host, name)
}
#[must_use]
pub fn component(&self, entity: Entity, ty: ComponentType) -> Option<*const c_void> {
let mut out: *const c_void = core::ptr::null();
let status =
(self.0.host.api().component)(self.0.host.handle(), entity.0, ty.0, &raw mut out);
(status == Status::OK && !out.is_null()).then_some(out)
}
pub fn writable_type(self, load: &Load<'_>, name: &'static str) -> Result<WritableType, Off> {
Ok(WritableType {
ty: self.component_type(load, name)?,
size: load.type_size(name)?,
name,
})
}
pub fn update<R>(
&self,
entity: Entity,
ty: WritableType,
write: impl FnOnce(&mut [u8]) -> R,
) -> Result<R, WorldError> {
let len = usize::try_from(ty.size).map_err(|_| WorldError::Missing)?;
let data = self.component_mut(entity, ty.ty)?;
let mut small = [0u8; 256];
let mut large = Vec::new();
let copy = if len <= small.len() {
&mut small[..len]
} else {
large.resize(len, 0);
&mut large[..]
};
unsafe { core::ptr::copy_nonoverlapping(data.cast::<u8>(), copy.as_mut_ptr(), len) };
let result = write(copy);
let data = self.component_mut(entity, ty.ty)?;
unsafe { core::ptr::copy_nonoverlapping(copy.as_ptr(), data.cast::<u8>(), len) };
Ok(result)
}
#[must_use]
pub fn get<T: Scalar>(&self, entity: Entity, field: Field<T>) -> Option<T> {
let data = self.component(entity, ComponentType(field.type_hash()))?;
let at = usize::try_from(field.offset()).ok()?;
let mut bytes = [0u8; 8];
let len = size_of::<T>().min(bytes.len());
unsafe {
core::ptr::copy_nonoverlapping(
data.cast::<u8>().wrapping_add(at),
bytes.as_mut_ptr(),
len,
);
};
T::from_bytes(&bytes[..len])
}
pub fn set<T: Scalar>(
&self,
entity: Entity,
field: Field<T>,
value: T,
) -> Result<(), WorldError> {
let data = self.component_mut(entity, ComponentType(field.type_hash()))?;
let at = usize::try_from(field.offset()).map_err(|_| WorldError::Missing)?;
let mut bytes = [0u8; 8];
let len = size_of::<T>().min(bytes.len());
if !value.to_bytes(&mut bytes[..len]) {
return Err(WorldError::Missing);
}
unsafe {
core::ptr::copy_nonoverlapping(bytes.as_ptr(), data.cast::<u8>().wrapping_add(at), len);
};
Ok(())
}
pub(crate) fn component_mut(
&self,
entity: Entity,
ty: ComponentType,
) -> Result<*mut c_void, WorldError> {
let mut out: *mut c_void = core::ptr::null_mut();
let status =
(self.0.host.api().component_write)(self.0.host.handle(), entity.0, ty.0, &raw mut out);
match WorldError::check(status, WorldError::Gone) {
Ok(()) if !out.is_null() => Ok(out),
Ok(()) => Err(WorldError::Missing),
Err(WorldError::Gone) if self.template(entity).is_some() => Err(WorldError::Lacks),
Err(why) => Err(why),
}
}
#[must_use]
pub fn position(&self, entity: Entity) -> Option<[f64; 3]> {
let data = self.component(entity, self.0.transform)?;
Some(unsafe { crate::ecs::transform_position(data.cast()) })
}
#[must_use]
pub fn used_item(&self, entity: Entity) -> Option<u32> {
let data = self.component(entity, self.0.used_item)?;
Some(unsafe { crate::raw::read_at::<u32>(data.cast(), 0) })
}
#[must_use]
pub fn has_label(&self, entity: Entity) -> Option<bool> {
let label = self.0.label?;
let Some(data) = self.component(entity, label) else {
return Some(false);
};
Some(unsafe { crate::raw::read_at::<u64>(data.cast(), 0) } != 0)
}
#[must_use]
pub fn template(&self, entity: Entity) -> Option<[u8; 16]> {
let mut out = [0u8; 16];
let status =
(self.0.host.api().entity_template)(self.0.host.handle(), entity.0, &raw mut out);
status.is_ok().then_some(out)
}
pub fn save(self) -> Result<(), WorldError> {
WorldError::from_status((self.0.host.api().save_world)(self.0.host.handle()))
}
#[must_use]
pub fn key(self) -> Option<String> {
world_key(self.0.host)
}
}
pub(crate) fn online(host: Host) -> Vec<Player> {
const MAX: usize = 64;
let mut infos = [blank_info(); MAX];
let mut count = 0u32;
let status = (host.api().players)(
host.handle(),
infos.as_mut_ptr(),
u32::try_from(MAX).unwrap_or(0),
&raw mut count,
);
if !status.is_ok() {
return Vec::new();
}
infos
.iter()
.take(usize::try_from(count).unwrap_or(0).min(MAX))
.map(Player::from_info)
.collect()
}
pub(crate) fn world_key(host: Host) -> Option<String> {
let api = host.api();
let mut out = Str::EMPTY;
if !(api.world_key)(host.handle(), &raw mut out).is_ok() {
return None;
}
unsafe { out.as_str() }.map(str::to_owned)
}