use core::ffi::c_void;
use crate::abi::{ResourceKey, ResourcePatchFn, Status, Str};
use crate::{Load, Off};
pub mod categories;
pub mod changes;
pub mod containers;
pub mod icons;
mod ids;
pub mod items;
pub mod loot_tables;
pub mod menu;
pub mod recipes;
pub mod vanilla;
pub mod workshops;
pub use ids::*;
impl crate::Host {
#[must_use]
pub fn game_resource(&self, key: ResourceKey) -> Option<Vec<u8>> {
let api = self.api();
let mut buffer = vec![0u8; 64 << 10];
loop {
let mut len = 0u32;
let capacity = u32::try_from(buffer.len()).ok()?;
let status = (api.resource_read)(
self.handle(),
&raw const key,
buffer.as_mut_ptr(),
capacity,
&raw mut len,
);
let len = usize::try_from(len).ok()?;
if status.is_ok() {
buffer.truncate(len);
return Some(buffer);
}
if status != Status::BAD_ARGS || len <= buffer.len() {
return None;
}
buffer.resize(len, 0);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ResourceType(&'static str);
impl ResourceType {
#[must_use]
pub const fn id(self) -> &'static str {
self.0
}
pub fn hash(self) -> Result<u32, Off> {
crate::host()
.ok_or_else(|| Off::new(format!("type {}: outside the game", self.0)))?
.hash(self.0)
}
}
pub mod types {
use super::ResourceType;
use crate::engine_ids as ids;
pub const ITEM_INFO: ResourceType = ResourceType(ids::ITEM_INFO);
pub const ITEM_REGISTRY_RESOURCE: ResourceType = ResourceType(ids::ITEM_REGISTRY);
pub const RECIPE_REGISTRY_RESOURCE: ResourceType = ResourceType(ids::RECIPE_REGISTRY);
pub const TEMPLATE_RESOURCE: ResourceType = ResourceType(ids::TEMPLATE);
pub const TEMPLATE_COLLECTION_RESOURCE: ResourceType = ResourceType(ids::TEMPLATE_COLLECTION);
pub const WORKSHOP_REGISTRY_RESOURCE: ResourceType = ResourceType(ids::WORKSHOP_REGISTRY);
pub const UI_TEXTURE_RESOURCE: ResourceType = ResourceType(ids::UI_IMAGE);
pub const FB_UI_BUNDLE: ResourceType = ResourceType(ids::UI_BUNDLE);
}
pub trait EngineFacts {
fn offset(&self, id: &str) -> Result<usize, Off>;
fn size(&self, id: &str) -> Result<usize, Off>;
fn hash(&self, id: &str) -> Result<u32, Off>;
}
impl EngineFacts for crate::Host {
fn offset(&self, id: &str) -> Result<usize, Off> {
let mut out = 0u64;
let status = (self.api().constant)(self.handle(), Str::new(id), &raw mut out);
let offset = crate::load::ok(status, out, || format!("offset {id}"))?;
usize::try_from(offset).map_err(|_| Off::new(format!("offset {id} is out of range")))
}
fn size(&self, id: &str) -> Result<usize, Off> {
let mut out = 0u32;
let status = (self.api().type_size)(Str::new(id), &raw mut out);
let size = crate::load::ok(status, out, || format!("type {id}"))?;
usize::try_from(size).map_err(|_| Off::new(format!("type {id} is out of range")))
}
fn hash(&self, id: &str) -> Result<u32, Off> {
let mut out = 0u32;
let status = (self.api().type_hash)(Str::new(id), &raw mut out);
crate::load::ok(status, out, || format!("type {id}"))
}
}
impl EngineFacts for Load<'_> {
fn offset(&self, id: &str) -> Result<usize, Off> {
self.host().offset(id)
}
fn size(&self, id: &str) -> Result<usize, Off> {
self.host().size(id)
}
fn hash(&self, id: &str) -> Result<u32, Off> {
self.host().hash(id)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Blob(Vec<u8>);
impl Blob {
#[must_use]
pub fn new(base: &[u8]) -> Self {
Self(base.to_vec())
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.0
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.0
}
#[must_use]
pub fn read_u32(&self, at: usize) -> Option<u32> {
let bytes = self.0.get(at..at.checked_add(4)?)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
pub fn write(&mut self, at: usize, bytes: &[u8]) -> bool {
match at
.checked_add(bytes.len())
.and_then(|end| self.0.get_mut(at..end))
{
Some(target) => {
target.copy_from_slice(bytes);
true
}
None => false,
}
}
#[must_use]
pub fn array(&self, field: usize) -> Option<(usize, u32)> {
let offset = self.read_u32(field)?.cast_signed();
let count = self.read_u32(field + 4)?;
let start = field.checked_add_signed(isize::try_from(offset).ok()?)?;
Some((start, count))
}
pub fn set_array(&mut self, field: usize, start: usize, count: u32) -> bool {
let Ok(offset) = i32::try_from(start.cast_signed() - field.cast_signed()) else {
return false;
};
self.write(field, &offset.to_le_bytes()) && self.write(field + 4, &count.to_le_bytes())
}
pub fn append(&mut self, data: &[u8], align: usize) -> usize {
let start = self.0.len().next_multiple_of(align.max(1));
self.0.resize(start, 0);
self.0.extend_from_slice(data);
start
}
pub fn grow_array(
&mut self,
field: usize,
size: usize,
align: usize,
relative: &[usize],
extra: &[(Vec<u8>, Option<usize>)],
) -> Option<u32> {
let (start, count) = self.array(field)?;
let old = usize::try_from(count).ok()?;
let moved = self
.0
.get(start..start.checked_add(old.checked_mul(size)?)?)?
.to_vec();
let mut elements: Vec<(Vec<u8>, Option<usize>)> = moved
.chunks_exact(size)
.enumerate()
.map(|(i, chunk)| (chunk.to_vec(), Some(start + i * size)))
.collect();
if extra.iter().any(|(bytes, _)| bytes.len() != size) {
return None;
}
elements.extend(extra.iter().cloned());
let new_start = self.0.len().next_multiple_of(align.max(1));
self.0.resize(new_start, 0);
for (index, (mut bytes, from)) in elements.into_iter().enumerate() {
let to = new_start + index * size;
if let Some(from) = from {
let delta = from.cast_signed() - to.cast_signed();
for &at in relative {
let header = bytes.get(at..at + 4)?;
let offset = i32::from_le_bytes(header.try_into().ok()?);
if offset != 0 {
let fixed = i32::try_from(isize::try_from(offset).ok()? + delta).ok()?;
bytes
.get_mut(at..at + 4)?
.copy_from_slice(&fixed.to_le_bytes());
}
}
}
self.0.extend_from_slice(&bytes);
}
let total = count.checked_add(u32::try_from(extra.len()).ok()?)?;
self.set_array(field, new_start, total).then_some(count)
}
}
type Builder = Box<dyn Fn(&[u8]) -> Option<Vec<u8>> + Send + Sync>;
unsafe extern "system" fn build(
base: *const u8,
base_len: usize,
user: *mut c_void,
out: *mut *const u8,
out_len: *mut usize,
) -> Status {
if user.is_null() || out.is_null() || out_len.is_null() {
return Status::BAD_ARGS;
}
let builder = unsafe { &*user.cast::<Builder>() };
let base = if base.is_null() || base_len == 0 {
&[][..]
} else {
unsafe { core::slice::from_raw_parts(base, base_len) }
};
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| builder(base)));
let Ok(Some(bytes)) = built else {
return Status::REFUSED;
};
let len = bytes.len();
let bytes: *mut [u8] = Box::into_raw(bytes.into_boxed_slice());
unsafe {
out.write(bytes.cast::<u8>().cast_const());
}
unsafe {
out_len.write(len);
}
Status::OK
}
pub fn rebuild(key: ResourceKey) -> Result<(), Off> {
let host = crate::host().ok_or_else(|| Off::new("rebuild: no loader"))?;
let api = host.api();
let status = (api.rebuild_resource)(host.handle(), &raw const key);
crate::load::ok(status, (), || "rebuild resource".to_owned())
}
impl Load<'_> {
pub fn declare(&mut self, id: Id) -> Result<Guid, Off> {
let api = self.host().api();
let mut out = [0u8; 16];
let status = (api.declare_content)(self.host().handle(), Str::new(id.name()), &raw mut out);
crate::load::ok(status, Guid(out), || format!("content id {id}"))
}
pub fn add_text(
&mut self,
tag: Guid,
language: crate::i18n::Language,
text: &str,
) -> Result<(), Off> {
let api = self.host().api();
let status = (api.add_text)(
self.host().handle(),
tag.fnv1a(),
language.0,
Str::new(text),
);
crate::load::ok(status, (), || "add text".to_owned())
}
pub fn serve(
&mut self,
key: ResourceKey,
base: ResourceKey,
builder: impl Fn(&[u8]) -> Option<Vec<u8>> + Send + Sync + 'static,
) -> Result<(), Off> {
let api = self.host().api();
let builder: Box<Builder> = Box::new(Box::new(builder));
let user = Box::into_raw(builder);
let patch: ResourcePatchFn = build;
let status = (api.serve_resource)(
self.host().handle(),
&raw const key,
&raw const base,
patch,
user.cast(),
);
if !status.is_ok() {
drop(unsafe { Box::from_raw(user) });
}
crate::load::ok(status, (), || "serve resource".to_owned())
}
}
impl Load<'_> {
pub fn allow_rebuild(&mut self, key: ResourceKey) -> Result<(), Off> {
let api = self.host().api();
let status = (api.rebuild_resource)(self.host().handle(), &raw const key);
crate::load::ok(status, (), || "allow rebuild".to_owned())
}
}
#[cfg(test)]
mod tests;