#![cfg_attr(
feature = "unstable",
doc = "| Unstable, with the `unstable` feature | [`world`], [`map`], [`actions`], [`console`], [`rates`], [`script`](mod@script) |"
)]
#![cfg_attr(
not(feature = "unstable"),
doc = "| Unstable, with the `unstable` feature | `world`, `map`, `actions`, `console`, `rates`, `script` |"
)]
#![doc = document_features::document_features!()]
#![cfg_attr(docsrs, feature(doc_cfg))]
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
extern crate self as enshwick;
pub mod abi;
#[cfg(feature = "unstable")]
pub mod actions;
pub mod affixes;
pub mod altars;
#[cfg(doctest)]
mod book;
pub mod buffs;
pub mod code;
pub mod config;
#[cfg(feature = "unstable")]
pub mod console;
pub mod content;
pub mod damage;
pub mod daytime;
mod declare;
pub mod ecs;
pub mod effects;
mod engine_enum;
mod engine_ids;
pub mod entity_data;
pub mod events;
pub(crate) mod fresh;
pub mod game_events;
pub mod game_settings;
mod hook;
mod host;
pub mod i18n;
pub mod interactions;
pub mod inventory;
pub mod item_data;
pub mod knowledge;
mod load;
mod locks;
pub mod loot;
#[cfg(feature = "unstable")]
pub mod map;
pub mod migrations;
pub mod mods;
pub mod net;
pub mod pacing;
pub mod packs;
pub mod points;
pub mod progress;
#[cfg(feature = "unstable")]
pub mod rates;
#[cfg(not(feature = "unstable"))]
#[expect(
unreachable_pub,
dead_code,
reason = "the rates are public only with `unstable`"
)]
mod rates;
pub mod raw;
#[cfg(doctest)]
mod refused;
pub mod schedule;
#[cfg(feature = "unstable")]
pub mod script;
#[cfg(not(feature = "unstable"))]
#[expect(
unreachable_pub,
dead_code,
reason = "the script binding is public only with `unstable`"
)]
mod script;
pub mod services;
pub mod shape;
pub mod state;
pub mod stats;
pub mod status;
pub mod survivors;
pub mod terrain;
#[cfg(feature = "test")]
pub mod test;
pub mod time;
pub mod ui;
pub mod vetoes;
pub mod voxels;
pub mod weather;
#[cfg(feature = "unstable")]
pub mod world;
mod world_error;
pub use abi::{Entity, ItemId, PlayerKey, RecipeId};
pub use enshwick_macros::Config;
#[cfg(feature = "unstable")]
pub use enshwick_macros::Script;
pub use enshwick_macros::Shape;
#[cfg(feature = "unstable")]
pub use enshwick_macros::call;
pub use enshwick_macros::event;
pub use enshwick_macros::hook;
pub use enshwick_macros::point;
#[cfg(feature = "unstable")]
pub use enshwick_macros::script;
pub use enshwick_macros::service;
#[cfg(not(feature = "unstable"))]
pub(crate) use enshwick_macros::{Script, script};
pub use hook::{FnPtr, Hook, Next};
pub use host::{Host, host};
pub use load::{Load, Mod, Off};
pub use shape::Shape;
pub use world_error::WorldError;
#[doc(hidden)]
pub mod __private {
pub use const_format::concatcp;
pub use crate::locks::Relock;
pub use crate::script::convert::{argument, record_fields};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use crate::abi::{HostApi, Level, ManifestFlags, ModHandle, Status};
use crate::{Host, Load, Mod};
pub use crate::content::recipes::derive::{RecipeEdit, modify_recipes_in};
pub use crate::engine_ids::ALL as ENGINE_IDS;
pub mod ids {
pub use crate::engine_ids::*;
}
pub const INVENTORY_LINKS: usize = 16;
pub const INVENTORY_SLOTS: u8 = 8;
pub const ATTRIBUTE_SET_INITIALISED: u8 = 1;
pub const EXPERIENCE_SOURCES: &[(crate::game_events::ExperienceSourceType, &str)] =
&crate::game_events::ExperienceSourceType::IDS;
#[must_use]
pub const fn flags(join: &[bool], reload: &[bool]) -> ManifestFlags {
let join = match join {
[true] => ManifestFlags::JOIN.0,
_ => 0,
};
let reload = match reload {
[true] => ManifestFlags::RELOAD.0,
_ => 0,
};
ManifestFlags(join | reload)
}
#[must_use]
pub const fn point_schemas(text: &'static str) -> &'static str {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
return text;
}
i += 1;
}
""
}
#[must_use]
pub const fn bytes<const N: usize>(text: &str) -> [u8; N] {
let source = text.as_bytes();
assert!(source.len() == N, "the section's size is the text's length");
let mut out = [0u8; N];
let mut i = 0;
while i < N {
out[i] = source[i];
i += 1;
}
out
}
#[must_use]
pub const fn first_text(texts: &[&'static str]) -> &'static str {
let mut i = 0;
while i < texts.len() {
if !texts[i].is_empty() {
return texts[i];
}
i += 1;
}
""
}
const fn author_names<const N: usize>(
given: &[&'static str],
cargo: &'static str,
) -> ([crate::abi::Str; N], usize) {
let mut out = [crate::abi::Str::EMPTY; N];
let mut count = 0;
if !given.is_empty() {
while count < given.len() {
assert!(!given[count].is_empty(), "an author's name is empty");
if count < N {
out[count] = crate::abi::Str::new(given[count]);
}
count += 1;
}
return (out, count);
}
let mut rest = cargo.as_bytes();
while !rest.is_empty() {
let mut cut = 0;
while cut < rest.len() && rest[cut] != b':' {
cut += 1;
}
let (name, tail) = rest.split_at(cut);
if let Ok(name) = core::str::from_utf8(name.trim_ascii())
&& !name.is_empty()
{
if count < N {
out[count] = crate::abi::Str::new(name);
}
count += 1;
}
rest = match tail.split_first() {
Some((_, after)) => after,
None => tail,
};
}
(out, count)
}
#[must_use]
pub const fn author_count(given: &[&'static str], cargo: &'static str) -> usize {
author_names::<0>(given, cargo).1
}
#[must_use]
pub const fn authors<const N: usize>(
given: &[&'static str],
cargo: &'static str,
) -> [crate::abi::Str; N] {
author_names::<N>(given, cargo).0
}
pub const fn hook_argument<T: Copy>() {}
#[must_use]
pub const fn or_default<T: Copy>(given: &[T], default: T) -> T {
match given {
[value] => *value,
_ => default,
}
}
pub fn fault_in(hook: &str) {
if let Some(host) = crate::host() {
host.fault(&format!("panic in hook {hook}"));
}
}
static MAX_LEVEL: AtomicU8 = AtomicU8::new(Level::INFO.0);
#[inline]
#[must_use]
pub fn enabled(level: Level) -> bool {
level.0 <= MAX_LEVEL.load(Ordering::Relaxed)
}
unsafe extern "system" fn refresh_level(
_event: crate::abi::Event,
_data: *const core::ffi::c_void,
_user: *mut core::ffi::c_void,
) {
if let Some(host) = crate::host() {
MAX_LEVEL.store(host.log_level().0, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub struct Once(AtomicU64);
impl Once {
#[must_use]
pub const fn new() -> Self {
Self(AtomicU64::new(0))
}
#[inline]
#[must_use]
pub fn first(&self) -> bool {
let life = crate::fresh::life();
self.0.load(Ordering::Relaxed) != life && self.0.swap(life, Ordering::Relaxed) != life
}
}
impl Default for Once {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct Every {
life: AtomicU64,
next: AtomicU64,
skipped: AtomicU64,
}
impl Every {
#[must_use]
pub const fn new() -> Self {
Self {
life: AtomicU64::new(0),
next: AtomicU64::new(0),
skipped: AtomicU64::new(0),
}
}
#[must_use]
pub fn due(&self, period: Duration) -> Option<u64> {
static EPOCH: OnceLock<Instant> = OnceLock::new();
let now = u64::try_from(EPOCH.get_or_init(Instant::now).elapsed().as_millis())
.unwrap_or(u64::MAX);
let life = crate::fresh::life();
if self.life.swap(life, Ordering::Relaxed) != life {
self.next.store(0, Ordering::Relaxed);
self.skipped.store(0, Ordering::Relaxed);
}
let next = self.next.load(Ordering::Relaxed);
let period = u64::try_from(period.as_millis()).unwrap_or(u64::MAX);
if now < next
|| self
.next
.compare_exchange(
next,
now.saturating_add(period),
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_err()
{
self.skipped.fetch_add(1, Ordering::Relaxed);
return None;
}
Some(self.skipped.swap(0, Ordering::Relaxed))
}
}
impl Default for Every {
fn default() -> Self {
Self::new()
}
}
pub fn log(level: Level, message: core::fmt::Arguments<'_>) {
if !enabled(level) {
return;
}
if let Some(host) = crate::host() {
match message.as_str() {
Some(text) => host.log(level, text),
None => host.log(level, &message.to_string()),
}
}
}
#[derive(Debug)]
pub struct Slot<M>(crate::fresh::Fresh<M>);
impl<M> Slot<M> {
#[must_use]
pub const fn new() -> Self {
Self(crate::fresh::Fresh::new())
}
#[must_use]
pub fn get(&'static self) -> Option<&'static M> {
self.0.get()
}
}
impl<M> Default for Slot<M> {
fn default() -> Self {
Self::new()
}
}
pub unsafe fn init<M: Mod>(api: *const HostApi, me: ModHandle, slot: &Slot<M>) -> Status {
let Some(host) = (unsafe { Host::new(api, me) }) else {
return Status::BAD_ARGS;
};
let host = host.install();
MAX_LEVEL.store(host.log_level().0, Ordering::Relaxed);
let _watched = host.subscribe(
crate::abi::Event::LOADER_SETTINGS_CHANGED,
refresh_level,
core::ptr::null_mut(),
);
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let joins = M::manifest().flags.contains(ManifestFlags::JOIN);
M::init(&mut Load::of(host, joins))
}));
match outcome {
Ok(Ok(state)) => match slot.0.set(state) {
Ok(()) => Status::OK,
Err(state) => {
let _dropped =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(state)));
Status::REFUSED
}
},
Ok(Err(off)) => {
host.fault(off.to_reason());
Status::REFUSED
}
Err(_) => {
host.log(Level::ERROR, "off: init panicked");
Status::FAULTED
}
}
}
#[must_use]
pub fn query<M: Mod>() -> *const crate::abi::Manifest {
std::panic::catch_unwind(M::manifest).map_or(core::ptr::null(), core::ptr::from_ref)
}
pub unsafe fn init_pack(
api: *const HostApi,
me: ModHandle,
init: impl FnOnce(&mut Load<'_>) -> Result<(), crate::Off>,
) -> Status {
let Some(host) = (unsafe { Host::new(api, me) }) else {
return Status::BAD_ARGS;
};
let outcome =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| init(&mut Load::new(host))));
match outcome {
Ok(Ok(())) => Status::OK,
Ok(Err(off)) => {
host.fault(off.to_reason());
Status::REFUSED
}
Err(_) => {
host.log(Level::ERROR, "off: init panicked");
Status::FAULTED
}
}
}
pub unsafe fn install_host(api: *const HostApi, me: ModHandle) {
if let Some(host) = unsafe { Host::new(api, me) } {
MAX_LEVEL.store(host.install().log_level().0, Ordering::Relaxed);
}
}
pub fn shutdown<M: Mod>(slot: &'static Slot<M>) {
let Some(state) = slot.get() else {
return;
};
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| state.shutdown())).is_err()
&& let Some(host) = crate::host()
{
host.fault("panic in shutdown");
}
}
pub fn ready<M: Mod>(slot: &'static Slot<M>) {
let (Some(state), Some(host)) = (slot.get(), crate::host()) else {
return;
};
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| state.ready(host))).is_err() {
host.fault("panic in ready");
}
}
}