use core::ffi::c_void;
use crate::abi::{ResourceKey, ResourcePatchFn, Status};
use crate::{Load, Off};
pub mod icons;
pub mod items;
pub mod menu;
pub mod recipes;
pub mod vanilla;
pub mod workshops;
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, crate::Shape)]
#[shape(api = "guid")]
pub struct Guid(pub [u8; 16]);
impl Guid {
#[must_use]
pub const fn parse(text: &str) -> Self {
let text = text.as_bytes();
assert!(text.len() == 36, "a GUID has 36 characters");
let mut raw = [0u8; 16];
let (mut i, mut out) = (0, 0);
while i < 36 {
if i == 8 || i == 13 || i == 18 || i == 23 {
assert!(text[i] == b'-', "a GUID has dashes at 8, 13, 18 and 23");
i += 1;
continue;
}
raw[out] = (digit(text[i]) << 4) | digit(text[i + 1]);
out += 1;
i += 2;
}
let bytes = [
raw[3], raw[2], raw[1], raw[0], raw[5], raw[4], raw[7], raw[6], raw[8], raw[9],
raw[10], raw[11], raw[12], raw[13], raw[14], raw[15],
];
assert!(out == 16, "a GUID has 32 hex digits");
Self(bytes)
}
#[must_use]
pub fn try_parse(text: &str) -> Option<Self> {
let bytes = text.as_bytes();
let shaped = bytes.len() == 36
&& bytes.iter().enumerate().all(|(i, &b)| {
if matches!(i, 8 | 13 | 18 | 23) {
b == b'-'
} else {
b.is_ascii_hexdigit()
}
});
shaped.then(|| Self::parse(text))
}
#[must_use]
pub const fn id(self) -> u32 {
let mut hash: u32 = 0x811c_9dc5;
let mut i = 0;
while i < 16 {
hash ^= self.0[i] as u32;
hash = hash.wrapping_mul(0x0100_0193);
i += 1;
}
hash
}
#[must_use]
pub const fn key(self, type_hash: u32) -> ResourceKey {
ResourceKey {
guid: self.0,
type_hash,
part: 0,
}
}
#[must_use]
pub const fn named(id: &str) -> Self {
assert!(is_id(id), "a content id is <mod>:<name>");
Self::hashed(id)
}
#[must_use]
pub const fn try_named(id: &str) -> Option<Self> {
if is_id(id) {
Some(Self::hashed(id))
} else {
None
}
}
pub(crate) const fn hashed(name: &str) -> Self {
let input = const_sha1::ConstSlice::new()
.push_slice(&NAMESPACE_BYTES)
.push_slice(name.as_bytes());
let hash = const_sha1::sha1_from_const_slice(&input).as_bytes();
let mut raw = [0u8; 16];
let mut i = 0;
while i < 16 {
raw[i] = hash[i];
i += 1;
}
raw[6] = (raw[6] & 0x0f) | 0x50;
raw[8] = (raw[8] & 0x3f) | 0x80;
Self::from_big_endian(raw)
}
const fn from_big_endian(raw: [u8; 16]) -> Self {
Self([
raw[3], raw[2], raw[1], raw[0], raw[5], raw[4], raw[7], raw[6], raw[8], raw[9],
raw[10], raw[11], raw[12], raw[13], raw[14], raw[15],
])
}
}
pub const NAMESPACE: Guid = Guid::parse("e2185889-0e5e-4977-9edd-d6e7bcd1393f");
const NAMESPACE_BYTES: [u8; 16] = [
0xe2, 0x18, 0x58, 0x89, 0x0e, 0x5e, 0x49, 0x77, 0x9e, 0xdd, 0xd6, 0xe7, 0xbc, 0xd1, 0x39, 0x3f,
];
#[must_use]
pub const fn is_id(id: &str) -> bool {
let bytes = id.as_bytes();
let mut colon = None;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b':' {
if colon.is_some() {
return false;
}
colon = Some(i);
} else {
let common = b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-';
let in_name = colon.is_some() && b == b'/';
if !common && !in_name && b != b'.' {
return false;
}
}
i += 1;
}
match colon {
Some(at) => {
let name = bytes.len() - at - 1;
at >= 1 && at <= 64 && name >= 1 && name <= 128
}
None => false,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Id {
name: &'static str,
guid: Guid,
}
impl Id {
#[must_use]
pub const fn new(name: &'static str) -> Self {
Self {
name,
guid: Guid::named(name),
}
}
#[must_use]
pub const fn try_new(name: &'static str) -> Option<Self> {
match Guid::try_named(name) {
Some(guid) => Some(Self { name, guid }),
None => None,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
self.name
}
#[must_use]
pub fn namespace(self) -> &'static str {
self.name
.split_once(':')
.map_or("", |(namespace, _)| namespace)
}
#[must_use]
pub const fn guid(self) -> Guid {
self.guid
}
#[must_use]
pub const fn id(self) -> u32 {
self.guid.id()
}
#[must_use]
pub const fn key(self, type_hash: u32) -> ResourceKey {
self.guid.key(type_hash)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DebugName<'a>(pub &'a str);
impl From<Id> for Guid {
fn from(id: Id) -> Self {
id.guid
}
}
impl core::fmt::Display for Id {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.name)
}
}
impl core::fmt::Display for Guid {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let b = &self.0;
write!(
f,
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-",
b[3], b[2], b[1], b[0], b[5], b[4], b[7], b[6], b[8], b[9]
)?;
b[10..].iter().try_for_each(|byte| write!(f, "{byte:02x}"))
}
}
#[expect(
clippy::panic,
reason = "const evaluation: a malformed literal fails the build"
)]
const fn digit(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => panic!("a GUID is hex"),
}
}
#[must_use]
pub fn game_resource(key: ResourceKey) -> Option<Vec<u8>> {
crate::host()?.game_resource(key)
}
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);
}
}
}
pub mod types {
pub const ITEM_INFO: u32 = 0xb5ce_8765;
pub const ITEM_REGISTRY: u32 = 0x72ab_39b6;
pub const RECIPE_REGISTRY: u32 = 0x5f53_82cf;
pub const TEMPLATE: u32 = 0x3976_8775;
pub const TEMPLATE_COLLECTION: u32 = 0xd80f_e371;
pub const WORKSHOP_REGISTRY: u32 = 0xa8ea_5924;
pub const UI_IMAGE: u32 = super::icons::UI_IMAGE;
pub const UI_BUNDLE: u32 = 0xfef9_18b8;
}
#[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(),
crate::abi::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.id(),
language.0,
crate::abi::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 {
use super::*;
#[test]
fn a_builder_reads_a_null_base_as_empty() {
let builder: Builder = Box::new(|base: &[u8]| Some(vec![u8::from(base.is_empty())]));
let user = core::ptr::from_ref(&builder).cast_mut().cast();
let (mut out, mut out_len) = (core::ptr::null::<u8>(), 0usize);
let status = unsafe { build(core::ptr::null(), 0, user, &raw mut out, &raw mut out_len) };
assert_eq!(status, Status::OK);
assert_eq!(unsafe { core::slice::from_raw_parts(out, out_len) }, [1]);
let refused = unsafe {
build(
core::ptr::null(),
0,
user,
core::ptr::null_mut(),
&raw mut out_len,
)
};
assert_eq!(refused, Status::BAD_ARGS);
}
#[test]
fn a_guid_parses_to_memory_order_and_its_game_id() {
let chest = Guid::parse("6943053a-32c1-4718-ae22-e61427ddf54d");
assert_eq!(&chest.0[..4], &[0x3a, 0x05, 0x43, 0x69]);
assert_eq!(chest.id(), 1_302_084_846);
}
#[test]
fn moved_elements_keep_pointing_at_their_data() {
let mut base = vec![0u8; 18];
base[0..4].copy_from_slice(&8u32.to_le_bytes());
base[4..8].copy_from_slice(&1u32.to_le_bytes());
base[8..12].copy_from_slice(&8u32.to_le_bytes());
base[12..16].copy_from_slice(&2u32.to_le_bytes());
base[16..18].copy_from_slice(b"ab");
let mut blob = Blob::new(&base);
let copy = blob.bytes()[8..16].to_vec();
assert_eq!(blob.grow_array(0, 8, 4, &[0], &[(copy, Some(8))]), Some(1));
let (start, count) = blob.array(0).unwrap();
assert_eq!(count, 2);
for element in [start, start + 8] {
let (text, len) = blob.array(element).unwrap();
assert_eq!(&blob.bytes()[text..text + len as usize], b"ab");
}
}
}