mod points;
mod stats;
mod table;
#[cfg(doctest)]
#[doc = include_str!("../../../book/src/testing.md")]
#[expect(dead_code, reason = "the doc comment carries the chapter")]
struct BookTesting;
#[cfg(doctest)]
#[doc = include_str!("../../../book/src/migrations.md")]
#[expect(dead_code, reason = "the doc comment carries the chapter")]
struct BookMigrations;
use std::collections::{HashMap, HashSet};
use std::sync::MutexGuard;
use crate::abi::{
AltarInfo, AltarZoneSize, BedCheck, Bounds, CallerFrame, Comfort, EntityPlace, Event,
ExperiencePass, ExperienceRow, Fingerprint, Header, HookFlags, HostApi, Interaction,
InventoryOp, InventoryOpResult, InventoryResult, ItemData, Level, ModHandle, ModInfo,
PlayerInfo, RenameCall, ResourceKey, Side, SkillCheck, Status, Str, Summon, SurvivorLookup,
ZoneCheck,
};
use crate::events::Kind;
use crate::migrations::MigrationError;
use crate::{FnPtr, Hook};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Drawn {
Window {
id: String,
title: String,
},
End,
Label {
text: String,
style: u32,
},
Button {
text: String,
enabled: bool,
},
Toggle {
label: String,
on: bool,
read_only: bool,
},
Slider {
label: String,
value: f32,
min: f32,
max: f32,
steps: u32,
read_only: bool,
},
Dropdown {
label: String,
options: Vec<String>,
index: usize,
read_only: bool,
},
Progress {
fraction: f32,
},
Sound(u32),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FakeMarker {
pub position: [i64; 3],
pub label: String,
pub color: u32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum Input {
Click,
Toggle(bool),
Value(f32),
Pick(usize),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShownDialog {
pub title: String,
pub message: String,
pub detail: String,
pub buttons: u32,
}
pub type InitFn = unsafe extern "system" fn(*const HostApi, ModHandle) -> Status;
pub type ReadyFn = unsafe extern "system" fn(*const HostApi, ModHandle);
#[must_use]
pub fn address<F: FnPtr>(function: F) -> u64 {
let address: usize = unsafe { core::mem::transmute_copy(&function) };
address as u64
}
pub(crate) fn console() -> PlayerInfo {
let mut name = [0u8; 64];
name[..7].copy_from_slice(b"console");
PlayerInfo {
header: Header::of::<PlayerInfo>(),
slot: u32::MAX,
entity: 0,
key: 0,
permissions: PlayerInfo::CAN_KICK_BAN,
name_len: 7,
name,
}
}
pub(crate) fn ask(
callback: crate::abi::ActionFn,
user: usize,
kind: u32,
player: &PlayerInfo,
language: u32,
action: &str,
args: &str,
) -> String {
let call = crate::abi::ActionCall {
header: Header::of::<crate::abi::ActionCall>(),
kind,
language,
player: *player,
action: Str::new(action),
args: Str::new(args),
};
let mut reply = Str::EMPTY;
unsafe {
callback(
&raw const call,
core::ptr::with_exposed_provenance_mut(user),
&raw mut reply,
);
}
unsafe { reply.as_str() }.unwrap_or("").to_owned()
}
pub(crate) type FakeStep = (u32, String, String);
#[derive(Clone, Debug)]
pub(crate) struct FakeMigrations {
pub(crate) list: Vec<(String, Vec<FakeStep>)>,
pub(crate) map: crate::abi::MigrateFn,
pub(crate) user: usize,
}
impl FakeMigrations {
fn call(&self, migration: usize, step: usize, input: &str) -> Result<String, String> {
let (Ok(migration), Ok(step)) = (u32::try_from(migration), u32::try_from(step)) else {
return Err("too many steps".to_owned());
};
let mut out = Str::EMPTY;
let status = unsafe {
(self.map)(
core::ptr::with_exposed_provenance_mut(self.user),
migration,
step,
Str::new(input),
&raw mut out,
)
};
let text = unsafe { out.as_str() }.unwrap_or_default().to_owned();
if status.is_ok() { Ok(text) } else { Err(text) }
}
}
#[derive(Clone, Debug)]
pub(crate) struct FakeService {
pub(crate) name: String,
pub(crate) major: u32,
pub(crate) fields: Vec<(String, u64)>,
pub(crate) table: usize,
}
#[derive(Clone, Debug)]
pub struct FakeHost {
side: Side,
base: u64,
fingerprint: Fingerprint,
table: crate::abi::BuildTable,
handle: ModHandle,
symbols: HashMap<String, u64>,
systems: HashMap<String, u64>,
constants: HashMap<String, u64>,
field_offsets: HashMap<(String, String), u64>,
type_sizes: HashMap<String, u32>,
type_hashes: HashMap<String, u32>,
settings: Option<String>,
server_synced: Option<String>,
data_dir: String,
log_level: Level,
caller_frame: CallerFrame,
components: HashMap<(u32, u32), (usize, usize)>,
templates: HashMap<u32, [u8; 16]>,
players: Vec<PlayerInfo>,
mods: Vec<ModInfo>,
hosting: bool,
server_world: u64,
join_missing: Vec<ModInfo>,
replaced: Vec<ModInfo>,
language: u32,
services: Vec<FakeService>,
refused_hooks: HashSet<u64>,
code: (u64, u64),
workshop_recipes: u32,
items: HashMap<u32, ItemData>,
no_inventories: bool,
console_answers: HashMap<String, String>,
placed: Vec<(u32, EntityPlace)>,
world_key: Option<String>,
refused_writes: HashSet<u32>,
net_peers: HashSet<u64>,
icons_ready: bool,
archive: std::collections::BTreeMap<[u32; 4], Vec<u8>>,
resources: HashMap<([u8; 16], u32, u32), Vec<u8>>,
game_texts: HashMap<(u32, u32), &'static str>,
disabled_recipes: HashSet<u32>,
}
impl Default for FakeHost {
fn default() -> Self {
Self::new()
}
}
impl FakeHost {
#[must_use]
pub fn new() -> Self {
Self {
side: Side::SERVER,
base: 0x1_4000_0000,
fingerprint: Fingerprint {
guid: [0; 16],
age: 0,
},
table: crate::abi::BuildTable::EXACT,
handle: ModHandle(1),
symbols: HashMap::new(),
systems: HashMap::new(),
constants: HashMap::new(),
field_offsets: HashMap::new(),
type_sizes: HashMap::new(),
type_hashes: HashMap::new(),
settings: None,
server_synced: None,
data_dir: "mod-data/test".to_owned(),
log_level: Level::INFO,
caller_frame: CallerFrame::default(),
components: HashMap::new(),
templates: HashMap::new(),
players: Vec::new(),
mods: Vec::new(),
hosting: false,
server_world: 0,
join_missing: Vec::new(),
replaced: Vec::new(),
language: 0,
services: Vec::new(),
refused_hooks: HashSet::new(),
code: (0, 0),
workshop_recipes: 512,
items: HashMap::new(),
no_inventories: false,
console_answers: HashMap::new(),
placed: Vec::new(),
world_key: None,
refused_writes: HashSet::new(),
net_peers: HashSet::new(),
icons_ready: false,
archive: std::collections::BTreeMap::new(),
resources: HashMap::new(),
game_texts: HashMap::new(),
disabled_recipes: HashSet::new(),
}
}
#[must_use]
pub fn side(mut self, side: Side) -> Self {
self.side = side;
self
}
#[must_use]
pub fn hosting(mut self, hosting: bool) -> Self {
self.hosting = hosting;
self
}
#[must_use]
pub fn server_world(mut self, world: u64) -> Self {
self.server_world = world;
self
}
#[must_use]
pub fn base(mut self, base: u64) -> Self {
self.base = base;
self
}
#[must_use]
pub fn fingerprint(mut self, fingerprint: Fingerprint) -> Self {
self.fingerprint = fingerprint;
self
}
#[must_use]
pub fn build_table(mut self, table: crate::abi::BuildTable) -> Self {
self.table = table;
self
}
#[must_use]
pub fn handle(mut self, handle: ModHandle) -> Self {
self.handle = handle;
self
}
#[must_use]
pub fn symbol(mut self, name: &str, address: u64) -> Self {
self.symbols.insert(name.to_owned(), address);
self
}
#[must_use]
pub fn system(mut self, name: &str, address: u64) -> Self {
self.systems.insert(name.to_owned(), address);
self
}
#[must_use]
pub fn constant(mut self, name: &str, value: u64) -> Self {
self.constants.insert(name.to_owned(), value);
self
}
#[must_use]
pub fn field_offset(mut self, type_name: &str, field: &str, offset: u64) -> Self {
self.field_offsets
.insert((type_name.to_owned(), field.to_owned()), offset);
self
}
#[must_use]
pub fn type_size(mut self, type_name: &str, size: u32) -> Self {
self.type_sizes.insert(type_name.to_owned(), size);
self
}
#[must_use]
pub fn type_hash(mut self, type_name: &str, hash: u32) -> Self {
self.type_hashes.insert(type_name.to_owned(), hash);
self
}
#[must_use]
pub fn settings(mut self, text: &str) -> Self {
self.settings = Some(text.to_owned());
self
}
#[must_use]
pub fn data_dir(mut self, path: &str) -> Self {
path.clone_into(&mut self.data_dir);
self
}
#[must_use]
pub fn log_level(mut self, level: Level) -> Self {
self.log_level = level;
self
}
#[must_use]
pub fn caller_frame(mut self, frame: CallerFrame) -> Self {
self.caller_frame = frame;
self
}
#[must_use]
pub fn component(mut self, entity: u32, type_hash: u32, bytes: &[u8]) -> Self {
let mut words = vec![0u128; bytes.len().div_ceil(16).max(1)].into_boxed_slice();
for (word, chunk) in words.iter_mut().zip(bytes.chunks(16)) {
let mut le = [0u8; 16];
le[..chunk.len()].copy_from_slice(chunk);
*word = u128::from_le_bytes(le);
}
let data = Box::leak(words).as_mut_ptr();
self.components
.insert((entity, type_hash), (data.expose_provenance(), bytes.len()));
self
}
#[must_use]
pub fn net_peer(mut self, peer: crate::net::Peer) -> Self {
let raw = match peer {
crate::net::Peer::Server => crate::abi::NetMessage::SERVER,
crate::net::Peer::Player(key) => key,
};
self.net_peers.insert(raw);
self
}
#[must_use]
pub fn refuse_write(mut self, type_hash: u32) -> Self {
self.refused_writes.insert(type_hash);
self
}
#[must_use]
pub fn placed(mut self, type_hash: u32, entity: u32, position: [f64; 3]) -> Self {
self.placed.push((
type_hash,
EntityPlace {
entity,
reserved: 0,
position: crate::ecs::fixed(position),
},
));
self
}
#[must_use]
pub fn world_key(mut self, key: &str) -> Self {
self.world_key = Some(key.to_owned());
self
}
#[must_use]
pub fn template(mut self, entity: u32, guid: [u8; 16]) -> Self {
self.templates.insert(entity, guid);
self
}
#[must_use]
pub fn player(mut self, player: PlayerInfo) -> Self {
self.players.push(player);
self
}
#[must_use]
pub fn loaded(mut self, info: ModInfo) -> Self {
self.mods.push(info);
self
}
#[must_use]
pub fn join_missing(mut self, info: ModInfo) -> Self {
self.join_missing.push(info);
self
}
#[must_use]
pub fn replaced(mut self, info: ModInfo) -> Self {
self.replaced.push(info);
self
}
#[must_use]
pub const fn language(mut self, language: crate::i18n::Language) -> Self {
self.language = language.0;
self
}
#[must_use]
pub fn console_answer(mut self, word: &str, answer: &str) -> Self {
self.console_answers
.insert(word.to_owned(), answer.to_owned());
self
}
#[must_use]
pub fn service<T: crate::services::Service>(mut self, table: &'static T) -> Self {
self.services.push(FakeService {
name: T::NAME.to_owned(),
major: T::MAJOR,
fields: T::LAYOUT
.iter()
.filter_map(|f| unsafe { f.name.as_str() }.map(|n| (n.to_owned(), f.hash)))
.collect(),
table: core::ptr::from_ref(table).expose_provenance(),
});
self
}
#[must_use]
pub fn refuse_hook(mut self, target: u64) -> Self {
self.refused_hooks.insert(target);
self
}
#[must_use]
pub fn code(mut self, code: &'static [u8]) -> Self {
self.code = (code.as_ptr().addr() as u64, code.len() as u64);
self
}
#[must_use]
pub fn workshop_recipes(mut self, limit: u32) -> Self {
self.workshop_recipes = limit;
self
}
#[must_use]
pub fn item(mut self, item: u32, max_stack: u16, flags: u32) -> Self {
self.items.insert(
item,
ItemData {
header: Header::of::<ItemData>(),
id: item,
max_stack,
category: 0,
rarity: 0,
flags,
},
);
self
}
#[must_use]
pub fn item_category(mut self, item: u32, category: u8) -> Self {
if let Some(data) = self.items.get_mut(&item) {
data.category = category;
}
self
}
#[must_use]
pub fn icons_ready(mut self) -> Self {
self.icons_ready = true;
self
}
#[must_use]
pub fn archive(mut self, hash: [u32; 4], bytes: Vec<u8>) -> Self {
self.archive.insert(hash, bytes);
self
}
#[must_use]
pub fn resource(mut self, key: ResourceKey, bytes: Vec<u8>) -> Self {
self.resources
.insert((key.guid, key.type_hash, key.part), bytes);
self
}
#[must_use]
pub fn game_text(
mut self,
id: u32,
language: crate::i18n::Language,
text: &'static str,
) -> Self {
self.game_texts.insert((id, language.0), text);
self
}
#[must_use]
pub fn disabled_recipe(mut self, recipe: crate::content::Guid) -> Self {
self.disabled_recipes.insert(recipe.id());
self
}
#[must_use]
pub fn without_inventories(mut self) -> Self {
self.no_inventories = true;
self
}
#[must_use]
pub fn install(self) -> Installed {
let handle = self.handle;
let turn = table::install(self);
Installed {
handle,
_turn: turn,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct LinkedHook {
pub target: u64,
pub detour: u64,
pub priority: i32,
pub flags: HookFlags,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct SubmittedRequest {
pub ops: Vec<InventoryOp>,
pub flags: u32,
pub actor: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct ServedResource {
pub key: ResourceKey,
pub base: ResourceKey,
}
#[derive(Debug)]
pub struct Installed {
handle: ModHandle,
_turn: MutexGuard<'static, ()>,
}
impl Drop for Installed {
fn drop(&mut self) {
table::uninstall();
}
}
#[expect(
clippy::unused_self,
reason = "the fake's state is process-wide; `self` proves it is installed"
)]
impl Installed {
#[must_use]
pub fn api(&self) -> *const HostApi {
&raw const table::TABLE
}
#[must_use]
pub fn handle(&self) -> ModHandle {
self.handle
}
pub unsafe fn init(&self, init: InitFn) -> Status {
unsafe { init(self.api(), self.handle) }
}
pub unsafe fn ready(&self, ready: ReadyFn) {
unsafe { ready(self.api(), self.handle) }
}
pub unsafe fn start(&self, init: InitFn, ready: ReadyFn) -> Status {
let status = unsafe { self.init(init) };
if status.is_ok() {
unsafe { self.ready(ready) };
}
status
}
#[must_use]
pub fn logs(&self) -> Vec<(Level, String)> {
table::with(Vec::new(), |s| s.logs.clone())
}
#[must_use]
pub fn faults(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.faults.clone())
}
#[must_use]
pub fn hooks(&self) -> Vec<LinkedHook> {
table::with(Vec::new(), |s| s.hooks.clone())
}
#[must_use]
pub fn mid_hooks(&self) -> Vec<u64> {
table::with(Vec::new(), |s| s.mid_hooks.iter().map(|h| h.0).collect())
}
#[must_use = "the count says whether the mod hooks the address"]
pub fn run_at(&self, address: u64, registers: &mut crate::abi::Registers) -> usize {
let handlers: Vec<_> = table::with(Vec::new(), |s| {
s.mid_hooks
.iter()
.filter(|h| h.0 == address)
.map(|h| (h.1, h.2))
.collect()
});
for &(handler, user) in &handlers {
unsafe { handler(registers, core::ptr::with_exposed_provenance_mut(user)) };
}
handlers.len()
}
#[must_use]
pub fn detour<F: FnPtr>(&self, hook: &'static Hook<F>) -> Option<F> {
let entry = address(hook.entry());
self.hooks()
.iter()
.any(|linked| linked.detour == entry)
.then(|| hook.entry())
}
#[must_use]
pub fn subscriptions(&self) -> Vec<Event> {
table::with(Vec::new(), |s| {
s.subscriptions.iter().map(|s| s.0).collect()
})
}
#[must_use = "the count says whether the mod handles the event"]
pub fn raise<E: Kind>(&self, data: &E::Data) -> usize {
unsafe { self.raise_raw(E::EVENT, core::ptr::from_ref(data).cast()) }
}
#[must_use = "the count says whether the mod handles the event"]
pub unsafe fn raise_raw(&self, event: Event, data: *const core::ffi::c_void) -> usize {
let handlers: Vec<_> = table::with(Vec::new(), |s| {
s.subscriptions
.iter()
.filter(|s| s.0 == event)
.map(|s| (s.1, s.2))
.collect()
});
for &(callback, user) in &handlers {
unsafe { callback(event, data, core::ptr::with_exposed_provenance_mut(user)) };
}
handlers.len()
}
#[must_use]
pub fn game_event_types(&self) -> Vec<u32> {
table::with(Vec::new(), |s| s.game_events.iter().map(|g| g.0).collect())
}
#[must_use = "the count says whether the mod handles the event"]
pub fn raise_game_event(&self, type_hash: u32, bytes: &[u8]) -> usize {
let handlers: Vec<_> = table::with(Vec::new(), |s| {
s.game_events
.iter()
.filter(|g| g.0 == 0 || g.0 == type_hash)
.map(|g| (g.1, g.2))
.collect()
});
let data = crate::abi::GameEventData {
header: Header::of::<crate::abi::GameEventData>(),
type_hash,
size: u32::try_from(bytes.len()).unwrap_or(u32::MAX),
data: bytes.as_ptr(),
};
for &(callback, user) in &handlers {
unsafe {
callback(
&raw const data,
core::ptr::with_exposed_provenance_mut(user),
);
};
}
handlers.len()
}
pub fn set_settings(&self, text: &str) {
table::with((), |s| s.host.settings = Some(text.to_owned()));
}
pub fn set_server_synced(&self, synced: Option<&str>) {
table::with((), |s| s.host.server_synced = synced.map(str::to_owned));
}
#[must_use]
pub fn reported_synced(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.synced.clone())
}
#[must_use]
pub fn settings_defaults(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.defaults.clone())
}
#[must_use]
pub fn resources(&self) -> Vec<ServedResource> {
table::with(Vec::new(), |s| {
let mut keys: Vec<ServedResource> = Vec::new();
for served in &s.served {
if !keys.iter().any(|k| k.key == served.0.key) {
keys.push(served.0);
}
}
keys
})
}
#[must_use]
pub fn recipe_rules(&self) -> Vec<(crate::abi::RecipeRuleKind, String)> {
table::with(Vec::new(), |s| s.recipe_rules.clone())
}
#[must_use]
pub fn rebuilds(&self) -> Vec<ResourceKey> {
table::with(Vec::new(), |s| s.rebuilds.clone())
}
pub fn build_resource(&self, key: &ResourceKey, base: &[u8]) -> Result<Vec<u8>, Status> {
let patches: Vec<_> = table::with(Vec::new(), |s| {
s.served
.iter()
.filter(|s| s.0.key == *key)
.map(|s| (s.1, s.2))
.collect()
});
if patches.is_empty() {
return Err(Status::MISSING);
}
let mut bytes = base.to_vec();
for (patch, user) in patches {
let mut out = core::ptr::null();
let mut out_len = 0;
let status = unsafe {
patch(
bytes.as_ptr(),
bytes.len(),
core::ptr::with_exposed_provenance_mut(user),
&raw mut out,
&raw mut out_len,
)
};
if !status.is_ok() {
return Err(status);
}
bytes = if out.is_null() {
Vec::new()
} else {
unsafe { core::slice::from_raw_parts(out, out_len) }.to_vec()
};
}
Ok(bytes)
}
#[must_use]
pub fn code_patches(&self) -> Vec<(u64, Vec<u8>, Vec<u8>)> {
table::with(Vec::new(), |s| s.code_patches.clone())
}
#[must_use]
pub fn texts(&self) -> Vec<(u32, u32, String)> {
table::with(Vec::new(), |s| s.texts.clone())
}
#[must_use]
pub fn ask_actions(
&self,
kind: u32,
player: &PlayerInfo,
language: u32,
action: &str,
args: &str,
) -> Option<String> {
let (callback, user) = table::with(None, |s| s.actions)?;
Some(ask(callback, user, kind, player, language, action, args))
}
#[must_use]
pub fn console(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.console.clone())
}
#[must_use]
pub fn status(&self) -> Vec<String> {
let sources = table::with(Vec::new(), |s| s.status_sources.clone());
sources
.into_iter()
.map(|(callback, user)| {
let mut out = Str::EMPTY;
unsafe { callback(core::ptr::with_exposed_provenance_mut(user), &raw mut out) };
unsafe { out.as_str() }.unwrap_or("").to_owned()
})
.collect()
}
#[must_use]
pub fn chat(&self) -> Vec<(u64, String)> {
table::with(Vec::new(), |s| s.chat.clone())
}
pub fn set_altars(&self, altars: &[AltarInfo]) {
table::with((), |s| s.altars = altars.to_vec());
let _handled = self.raise::<crate::events::AltarsChanged>(&());
}
pub fn set_daytime(&self, info: crate::abi::DaytimeInfo) {
table::with((), |s| s.daytime = Some(info));
}
#[must_use]
pub fn daytime(&self) -> Option<crate::abi::DaytimeInfo> {
table::with(None, |s| s.daytime)
}
#[must_use]
pub fn clock_speed(&self) -> Option<f32> {
table::with(None, |s| s.clock_speed)
}
pub fn set_game_settings(&self, bytes: Vec<u8>, preset: u32) {
table::with((), |s| s.game_settings = Some((bytes, preset)));
}
#[must_use]
pub fn game_settings(&self) -> Option<(Vec<u8>, u32)> {
table::with(None, |s| s.game_settings.clone())
}
#[must_use]
pub fn kept_game_settings(&self) -> Vec<Option<u8>> {
table::with(Vec::new(), |s| s.kept_settings.clone())
}
#[must_use]
pub fn damage(&self, mut check: crate::abi::DamageCheck) -> crate::abi::DamageCheck {
let filters = table::with(Vec::new(), |s| s.damage_filters.clone());
for (callback, user) in filters {
unsafe { callback(&raw mut check, core::ptr::with_exposed_provenance_mut(user)) };
}
check
}
#[must_use]
pub fn knowledge_input(&self, mut request: crate::abi::KnowledgeInput) -> bool {
let filters = table::with(Vec::new(), |s| s.knowledge_input_filters.clone());
for (callback, user) in filters {
let verdict = unsafe {
callback(
&raw const request,
core::ptr::with_exposed_provenance_mut(user),
)
};
request.allowed = u32::from(verdict != 0);
}
request.allowed != 0
}
#[must_use]
pub fn config_schema(&self) -> Option<crate::config::Schema> {
table::with(None, |s| s.config_schema.clone())
.and_then(|text| crate::config::Schema::parse(&text).ok())
}
pub fn set_buffs(&self, types: &[crate::abi::BuffTypeInfo], holders: &[u32]) {
table::with((), |s| {
s.buff_types = types.to_vec();
s.buffs = holders.iter().map(|&e| (e, Vec::new())).collect();
});
}
#[must_use]
pub fn buffs_of(&self, entity: u32) -> Option<Vec<crate::abi::ActiveBuff>> {
table::with(None, |s| s.buffs.get(&entity).cloned())
}
pub fn set_knowledge(&self, values: &[(u32, crate::abi::KnowledgeInfo)], quests: &[u32]) {
table::with((), |s| {
s.knowledge = values.to_vec();
s.quests = quests.to_vec();
});
}
#[must_use]
pub fn grants(&self) -> Vec<(u32, u32, u32, u32)> {
table::with(Vec::new(), |s| s.grants.clone())
}
#[must_use]
pub fn revokes(&self) -> Vec<(u32, u32, u32, u32)> {
table::with(Vec::new(), |s| s.revokes.clone())
}
#[must_use]
pub fn account_changes(&self) -> Vec<(u64, u32, u32, u32, bool)> {
table::with(Vec::new(), |s| s.account_changes.clone())
}
pub fn set_weather(&self, weights: [f32; 4], fronts: &[crate::abi::WeatherFront]) {
table::with((), |s| s.weather = Some((weights, fronts.to_vec())));
}
#[must_use]
pub fn forecasts(&self) -> Vec<(u32, i64)> {
table::with(Vec::new(), |s| s.forecasts.clone())
}
pub fn set_terrain(&self, materials: &[crate::abi::TerrainMaterialInfo]) {
table::with((), |s| s.terrain = Some(materials.to_vec()));
}
#[must_use]
pub fn terrain_edits(&self) -> Vec<crate::abi::TerrainEdit> {
table::with(Vec::new(), |s| s.terrain_edits.clone())
}
#[must_use]
pub fn altars_opened(&self) -> (bool, bool) {
table::with((false, false), |s| (s.altars_opened, s.zone_control))
}
#[must_use]
pub fn zone_sizes(&self) -> Vec<AltarZoneSize> {
table::with(Vec::new(), |s| s.zone_sizes.clone())
}
#[must_use]
pub fn zone_check(&self, zones: &[Bounds], object: Bounds, margin: i64) -> bool {
let filters = table::with(Vec::new(), |s| s.zone_filters.clone());
let call = ZoneCheck {
header: Header::of::<ZoneCheck>(),
zones: zones.as_ptr(),
zone_count: zones.len(),
object,
margin,
};
filters.iter().any(|&(callback, user)| {
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(&raw const call, user) != 0 }
})
}
#[must_use]
pub fn survivor_lookup(
&self,
survivor: u32,
vanilla: u32,
purpose: u32,
entity: u32,
cell: [i64; 3],
beds: &[(u32, u32)],
) -> Option<u32> {
unsafe extern "system" fn bed_of(token: *mut core::ffi::c_void, entity: u32) -> u32 {
let beds = unsafe { &*token.cast::<&[(u32, u32)]>() };
beds.iter()
.find(|(e, _)| *e == entity)
.map_or(0, |(_, bed)| *bed)
}
let (answer, user) = table::with(None, |s| s.survivor_lookup)?;
let mut beds = beds;
let lookup = SurvivorLookup {
header: Header::of::<SurvivorLookup>(),
survivor,
vanilla,
purpose,
entity,
cell,
bed_of: Some(bed_of),
token: (&raw mut beds).cast(),
};
let mut out = 0;
let user = core::ptr::with_exposed_provenance_mut(user);
let answered = unsafe { answer(&raw const lookup, user, &raw mut out) };
(answered != 0).then_some(out)
}
#[must_use]
pub fn survivor_bed_check(&self, check: &BedCheck) -> bool {
let filters = table::with(Vec::new(), |s| s.bed_checks.clone());
filters.iter().all(|&(callback, user)| {
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(check, user) != 0 }
})
}
#[must_use]
pub fn summon(&self, summon: &Summon) -> u8 {
let filters = table::with(Vec::new(), |s| s.summons.clone());
let mut verdict = 0;
for &(callback, user) in &filters {
let user = core::ptr::with_exposed_provenance_mut(user);
match unsafe { callback(summon, user) } {
Summon::REFUSE => return Summon::REFUSE,
0 => {}
_ => verdict = Summon::TAKE,
}
}
verdict
}
#[must_use]
pub fn summoned(&self, summon: &Summon) -> usize {
let handlers = table::with(Vec::new(), |s| s.summoned.clone());
for &(callback, user) in &handlers {
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(summon, user) };
}
handlers.len()
}
#[must_use]
pub fn item_data(&self) -> Vec<(String, u32, u32, Vec<u8>)> {
table::with(Vec::new(), |s| {
s.item_values
.iter()
.map(|(&(kind, pide), (item, bytes))| {
let id = s.item_kinds.get(kind as usize).cloned().unwrap_or_default();
(id, pide, *item, bytes.clone())
})
.collect()
})
}
#[must_use]
pub fn served_content(&self) -> Vec<([u32; 4], Vec<u8>)> {
table::with(Vec::new(), |s| {
s.content.iter().map(|(h, b)| (*h, b.clone())).collect()
})
}
pub fn wear(&self, entity: u32, item: crate::item_data::ItemInstance) {
table::with((), |s| s.stats.worn.entry(entity).or_default().push(item));
}
#[must_use]
pub fn settle_stats(&self) -> usize {
stats::settle()
}
#[must_use]
pub fn stat_modifiers(&self) -> Vec<(u32, u32, u32, u8, f32, u32, u64)> {
stats::modifiers()
}
#[must_use]
pub fn summon_items(&self) -> Vec<(u32, u32)> {
table::with(Vec::new(), |s| s.summon_items.clone())
}
#[must_use]
pub fn survivor_status(&self, survivor: u32, statuses: &[u8]) -> Option<u8> {
let (callback, user) = table::with(None, |s| s.survivor_status)?;
let user = core::ptr::with_exposed_provenance_mut(user);
let answer = unsafe { callback(survivor, statuses.as_ptr(), statuses.len(), user) };
(answer & crate::abi::SURVIVOR_STATUS_SET != 0).then(|| answer.to_le_bytes()[0])
}
#[must_use]
pub fn interaction(&self, call: &Interaction) -> bool {
let filters = table::with(Vec::new(), |s| s.interaction_filters.clone());
filters.iter().all(|&(callback, user)| {
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(call, user) != 0 }
})
}
#[must_use]
pub fn skill(&self, check: &SkillCheck) -> u8 {
let skills = table::with(Vec::new(), |s| s.skills.clone());
skills
.iter()
.filter(|(node, _, _)| *node == check.node)
.map(|&(_, callback, user)| {
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(check, user) }
})
.find(|&answer| answer != SkillCheck::VANILLA)
.unwrap_or(SkillCheck::VANILLA)
}
#[must_use]
pub fn comfort(&self, comfort: &Comfort) -> i32 {
let filters = table::with(Vec::new(), |s| s.comfort_filters.clone());
filters
.iter()
.fold(comfort.value, |value, &(callback, user)| {
let call = Comfort { value, ..*comfort };
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(&raw const call, user) }
})
}
pub fn experience(
&self,
rows: &mut [crate::progress::Experience],
cap: u32,
need: fn(u32) -> u32,
) {
unsafe extern "system" fn need_for(token: *mut core::ffi::c_void, level: u32) -> u32 {
let need = unsafe { *token.cast::<fn(u32) -> u32>() };
need(level)
}
let filters = table::with(Vec::new(), |s| s.experience_filters.clone());
let mut need = need;
for &(callback, user) in &filters {
let mut pass = ExperiencePass {
header: Header::of::<ExperiencePass>(),
rows: rows.as_mut_ptr().cast::<ExperienceRow>(),
row_count: rows.len(),
level_cap: cap,
need_for: Some(need_for),
token: (&raw mut need).cast(),
};
let user = core::ptr::with_exposed_provenance_mut(user);
unsafe { callback(&raw mut pass, user) };
}
}
#[must_use]
pub fn altar_limit(&self, flame_level: u32) -> Option<u8> {
let (callback, user) = table::with(None, |s| s.altar_limit)?;
let user = core::ptr::with_exposed_provenance_mut(user);
Some(unsafe { callback(flame_level, user) })
}
#[must_use]
pub fn base_limit_raised(&self) -> bool {
table::with(false, |s| s.base_limit_raised)
}
pub fn set_grouping(&self, zones: u32, bases: u32) {
table::with((), |s| s.grouping = Some((zones, bases)));
}
#[must_use]
pub fn labels(&self) -> Vec<(u32, String)> {
table::with(Vec::new(), |s| s.labels.clone())
}
#[must_use]
pub fn destroyed(&self) -> Vec<u32> {
table::with(Vec::new(), |s| s.destroyed.clone())
}
#[must_use]
pub fn writes(&self) -> Vec<(u32, u32)> {
table::with(Vec::new(), |s| s.writes.clone())
}
#[must_use]
pub fn component_bytes(&self, entity: u32, type_hash: u32) -> Option<Vec<u8>> {
let (address, len) = table::with(None, |s| {
s.host.components.get(&(entity, type_hash)).copied()
})?;
let data: *const u8 = core::ptr::with_exposed_provenance(address);
Some(unsafe { core::slice::from_raw_parts(data, len) }.to_vec())
}
#[must_use]
pub fn emits(&self) -> Vec<(u32, Vec<u8>)> {
table::with(Vec::new(), |s| s.emits.clone())
}
#[must_use]
pub fn teleports(&self) -> Vec<crate::abi::TeleportRequest> {
table::with(Vec::new(), |s| s.teleports.clone())
}
#[must_use]
pub fn spawns(&self) -> Vec<(u32, crate::abi::SpawnRequest)> {
table::with(Vec::new(), |s| s.spawns.clone())
}
#[must_use]
pub fn craft(&self, call: &crate::abi::CraftCheck) -> bool {
let filters = table::with(Vec::new(), |s| s.craft_filters.clone());
filters.iter().all(|&(callback, user)| {
unsafe { callback(call, core::ptr::with_exposed_provenance_mut(user)) != 0 }
})
}
#[must_use]
pub fn build(&self, call: &crate::abi::BuildCheck) -> bool {
let filters = table::with(Vec::new(), |s| s.build_filters.clone());
filters.iter().all(|&(callback, user)| {
unsafe { callback(call, core::ptr::with_exposed_provenance_mut(user)) != 0 }
})
}
#[must_use]
pub fn net_sent(&self) -> Vec<(String, u64, Vec<u8>)> {
table::with(Vec::new(), |s| {
s.net_sent
.iter()
.map(|(id, peer, bytes)| {
let name = s
.channels
.iter()
.find(|c| c.1 == *id)
.map_or_else(String::new, |c| c.0.clone());
(name, *peer, bytes.clone())
})
.collect()
})
}
#[must_use]
pub fn net_deliver<M: serde::Serialize>(
&self,
name: &str,
from: crate::net::Peer,
message: &M,
) -> bool {
let Ok(bytes) = postcard::to_allocvec(message) else {
return false;
};
let channel = table::with(None, |s| {
s.channels
.iter()
.find(|c| c.0 == name)
.map(|c| (c.1, c.2, c.3))
});
let Some((id, callback, user)) = channel else {
return false;
};
let peer = match from {
crate::net::Peer::Server => crate::abi::NetMessage::SERVER,
crate::net::Peer::Player(key) => key,
};
let raw = crate::abi::NetMessage {
header: Header::of::<crate::abi::NetMessage>(),
channel: id,
reserved: 0,
peer,
data: bytes.as_ptr(),
size: u32::try_from(bytes.len()).unwrap_or(0),
reserved_tail: 0,
};
unsafe { callback(&raw const raw, core::ptr::with_exposed_provenance_mut(user)) };
true
}
#[must_use]
pub fn draw_frame(&self) -> Vec<Drawn> {
let frames = table::with(Vec::new(), |s| s.frames.clone());
let mut drawn = Vec::new();
for (token, (callback, user)) in (1u64..).zip(frames) {
table::with((), |s| s.frame = Some((token, false, Vec::new())));
let frame = crate::abi::UiFrame {
header: Header::of::<crate::abi::UiFrame>(),
token,
screen: crate::abi::UiFrame::IN_GAME,
reserved: 0,
};
unsafe {
callback(
&raw const frame,
core::ptr::with_exposed_provenance_mut(user),
);
};
if let Some((_, _, list)) = table::with(None, |s| s.frame.take()) {
drawn.extend(list);
}
}
drawn
}
#[must_use]
pub fn draw_hud(&self) -> Vec<(u32, Drawn)> {
let huds = table::with(Vec::new(), |s| s.huds.clone());
let mut drawn = Vec::new();
for (token, (callback, user)) in (1u64..).zip(huds) {
table::with((), |s| s.hud = Some((token, Vec::new())));
let frame = crate::abi::UiFrame {
header: Header::of::<crate::abi::UiFrame>(),
token,
screen: crate::abi::UiFrame::IN_GAME,
reserved: 0,
};
unsafe {
callback(
&raw const frame,
core::ptr::with_exposed_provenance_mut(user),
);
}
if let Some((_, list)) = table::with(None, |s| s.hud.take()) {
drawn.extend(list);
}
}
drawn
}
pub fn check_settings(&self, text: &str) -> Result<(), String> {
let Some((callback, user)) = table::with(None, |s| s.config_check) else {
return Ok(());
};
let mut why = Str::EMPTY;
let status = unsafe {
callback(
Str::new(text),
&raw mut why,
core::ptr::with_exposed_provenance_mut(user),
)
};
if status.is_ok() {
return Ok(());
}
Err(unsafe { why.as_str() }.unwrap_or("refused").to_owned())
}
#[must_use]
pub fn published_texts(&self) -> Option<String> {
table::with(None, |s| s.published_texts.clone())
}
#[must_use]
pub fn markers(&self) -> Vec<FakeMarker> {
table::with(Vec::new(), |s| s.markers.clone())
}
pub fn input(&self, text: &str, input: Input) {
table::with((), |s| s.inputs.push((text.to_owned(), input)));
}
#[must_use]
pub fn dialogs(&self) -> Vec<ShownDialog> {
table::with(Vec::new(), |s| {
s.dialogs.iter().map(|(d, _, _)| d.clone()).collect()
})
}
pub fn answer(&self, answer: Option<crate::ui::Response>) -> bool {
let first = table::with(None, |s| {
(!s.dialogs.is_empty()).then(|| s.dialogs.remove(0))
});
let Some((_, callback, user)) = first else {
return false;
};
let raw = answer.map_or(crate::abi::UiDialog::CLOSED, crate::ui::Response::to_raw);
unsafe { callback(raw, core::ptr::with_exposed_provenance_mut(user)) };
true
}
#[must_use]
pub fn notices(&self) -> Vec<(u64, String)> {
table::with(Vec::new(), |s| s.notices.clone())
}
#[must_use]
pub fn saves(&self) -> usize {
table::with(0, |s| s.saves)
}
#[must_use]
pub fn rename(&self, entity: u32, player: Option<&PlayerInfo>, text: &str) -> bool {
let filters = table::with(Vec::new(), |s| s.rename_filters.clone());
let call = RenameCall {
header: Header::of::<RenameCall>(),
entity,
known: u8::from(player.is_some()),
player: player.copied().unwrap_or(PlayerInfo {
header: Header::of::<PlayerInfo>(),
slot: u32::MAX,
entity: 0,
key: 0,
permissions: 0,
name_len: 0,
name: [0; 64],
}),
text: Str::new(text),
};
filters.iter().all(|&(callback, user)| {
let user = core::ptr::with_exposed_provenance_mut(user);
let allowed = unsafe { callback(&raw const call, user) };
allowed != 0
})
}
#[must_use]
pub fn published(&self) -> Vec<(String, u32)> {
table::with(Vec::new(), |s| {
s.published
.iter()
.map(|p| (p.name.clone(), p.major))
.collect()
})
}
#[must_use]
pub fn content_ids(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.content_ids.clone())
}
#[must_use]
pub fn migrations(&self) -> Vec<String> {
table::with(Vec::new(), |s| {
s.migrations
.as_ref()
.map(|m| m.list.iter().map(|(id, _)| id.clone()).collect())
.unwrap_or_default()
})
}
pub fn migrate_settings(&self, old: &str) -> Result<String, MigrationError> {
use crate::abi::MigrationStep as S;
let migrations = table::with(None, |s| s.migrations.clone());
let mut table: toml::Table = toml::from_str(old)?;
let list = migrations.as_ref().map_or(&[][..], |m| &m.list[..]);
for (index, (_, steps)) in list.iter().enumerate() {
for (step, (kind, from, to)) in steps.iter().enumerate() {
match *kind {
S::RENAME_SETTING if !table.contains_key(to) => {
if let Some(value) = table.remove(from) {
table.insert(to.clone(), value);
}
}
S::REMOVE_SETTING => {
table.remove(from);
}
S::MAP_SETTING => {
let (Some(value), Some(map)) = (table.get(from), migrations.as_ref())
else {
continue;
};
let text = crate::migrations::write_value(value)?;
let new = map
.call(index, step, &text)
.map_err(|e| MigrationError::new(format!("setting {from}: {e}")))?;
table.insert(from.clone(), crate::migrations::parse_value(&new)?);
}
_ => {}
}
}
}
toml::to_string(&table).map_err(|e| MigrationError::new(e.to_string()))
}
pub fn migrate_store(&self, name: &str, old: &str) -> Result<(String, String), MigrationError> {
use crate::abi::MigrationStep as S;
let migrations = table::with(None, |s| s.migrations.clone());
let (mut name, mut text) = (name.to_owned(), old.to_owned());
let list = migrations.as_ref().map_or(&[][..], |m| &m.list[..]);
for (index, (_, steps)) in list.iter().enumerate() {
for (step, (kind, from, to)) in steps.iter().enumerate() {
match *kind {
S::RENAME_STORE if *from == name => name.clone_from(to),
S::MAP_STORE if *from == name => {
let Some(map) = migrations.as_ref() else {
continue;
};
text = map
.call(index, step, &text)
.map_err(|e| MigrationError::new(format!("store {from}: {e}")))?;
}
_ => {}
}
}
}
Ok((name, text))
}
#[must_use]
pub fn defined_events(&self) -> Vec<String> {
table::with(Vec::new(), |s| {
s.defined_events.iter().map(|d| d.0.clone()).collect()
})
}
#[must_use]
pub fn sent_events(&self) -> Vec<String> {
table::with(Vec::new(), |s| s.sent_events.clone())
}
#[must_use = "the count says whether the mod handles the event"]
pub fn send_mod_event<E: crate::services::ModEvent>(&self, event: &E) -> usize {
let offered: Vec<(&str, u64)> = E::LAYOUT
.iter()
.filter_map(|f| unsafe { f.name.as_str() }.map(|n| (n, f.hash)))
.collect();
let handlers: Vec<_> = table::with(Vec::new(), |s| {
s.mod_event_subscriptions
.iter()
.filter(|sub| {
sub.0 == E::NAME
&& crate::services::compare(&table::view(&sub.1), &offered).is_ok()
})
.map(|sub| (sub.2, sub.3))
.collect()
});
for &(callback, user) in &handlers {
unsafe {
callback(
core::ptr::from_ref(event).cast(),
core::ptr::with_exposed_provenance_mut(user),
);
}
}
handlers.len()
}
#[must_use]
pub fn inventory_requests(&self) -> Vec<SubmittedRequest> {
table::with(Vec::new(), |s| {
s.inventory_requests
.iter()
.map(|r| r.request.clone())
.collect()
})
}
#[must_use = "false means no request was open"]
pub fn finish_inventory(&self, outcome: u32, ops: &[InventoryOpResult]) -> bool {
let open = table::with(None, |s| {
(!s.inventory_requests.is_empty()).then(|| s.inventory_requests.remove(0))
});
let Some(open) = open else {
return false;
};
let result = InventoryResult {
header: Header::of::<InventoryResult>(),
outcome,
op_count: u32::try_from(ops.len()).unwrap_or(0),
ops: ops.as_ptr(),
};
unsafe {
(open.done)(
&raw const result,
core::ptr::with_exposed_provenance_mut(open.user),
);
}
true
}
}