use crate::engine_ids as ids;
use crate::{Load, Off};
use super::items::{self, ItemLayout, NewItem};
use super::recipes::NewRecipe;
use super::{Blob, EngineFacts, Guid, Id, types};
pub const WORKSHOP_REGISTRY: Guid = Guid::parse("3baba1c2-bc82-4ab7-b20e-16dec28208af");
pub const TEMPLATE_COLLECTION: Guid = Guid::parse("377f61dc-c7b8-4e4f-830f-7d2c3e8b7fd8");
pub mod stations {
use super::{Guid, Station};
pub const WORKBENCH: Station = Station {
item: Guid::parse("2f04a05f-697d-489b-a32f-76bb3439f5bc"),
template: Guid::parse("ebf450c3-fb96-4240-a3be-8ebe0713eccd"),
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Station {
pub item: Guid,
pub template: Guid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WorkshopId(Guid);
impl WorkshopId {
#[must_use]
pub const fn new(guid: Guid) -> Self {
Self(guid)
}
#[must_use]
pub const fn guid(self) -> Guid {
self.0
}
#[must_use]
pub const fn id(self) -> u32 {
self.0.fnv1a()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StationRecipe {
pub id: Id,
pub cost: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewWorkshop {
pub like: Station,
pub item: Id,
pub template: Id,
pub workshop: Id,
pub name: Option<Guid>,
pub description: Option<Guid>,
pub station_recipe: Option<StationRecipe>,
pub debug_name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct WorkshopLayout {
pub components: usize,
pub crafting_types: [u32; 3],
pub crafting: [usize; 3],
pub interaction: [usize; 3],
pub client: [usize; 2],
pub placed_entity: usize,
pub templates: usize,
pub workshops: usize,
pub entry: usize,
pub entry_guid: usize,
pub entry_id: usize,
pub entry_name: usize,
pub entry_item: usize,
pub entry_item_ref: usize,
pub entry_npc: usize,
}
impl WorkshopLayout {
pub fn resolve(facts: &(impl EngineFacts + ?Sized)) -> Result<Self, Off> {
let at = |id: &str| facts.offset(id);
Ok(Self {
components: at(ids::TEMPLATE_COMPONENTS)?,
crafting_types: [
facts.hash(ids::CRAFTING)?,
facts.hash(ids::CRAFTING_INTERACTION)?,
facts.hash(ids::CLIENT_CRAFTING_INTERACTION)?,
],
crafting: [
at(ids::CRAFTING_WORKSHOP)?,
at(ids::CRAFTING_WORKSHOP_ID)?,
facts.size(ids::CRAFTING)?,
],
interaction: [
at(ids::CRAFTING_INTERACTION_WORKSHOP)?,
at(ids::CRAFTING_INTERACTION_WORKSHOP_ID)?,
facts.size(ids::CRAFTING_INTERACTION)?,
],
client: [
at(ids::CLIENT_CRAFTING_INTERACTION_WORKSHOP_ID)?,
facts.size(ids::CLIENT_CRAFTING_INTERACTION)?,
],
placed_entity: at(ids::ITEM_INFO_PLACED_ENTITY)?,
templates: at(ids::TEMPLATE_COLLECTION_TEMPLATES)?,
workshops: at(ids::WORKSHOP_REGISTRY_WORKSHOPS)?,
entry: facts.size(ids::WORKSHOP)?,
entry_guid: at(ids::WORKSHOP_GUID)?,
entry_id: at(ids::WORKSHOP_ID)?,
entry_name: at(ids::WORKSHOP_NAME)?,
entry_item: at(ids::WORKSHOP_ITEM)?,
entry_item_ref: at(ids::WORKSHOP_ITEM_REF)?,
entry_npc: at(ids::WORKSHOP_NPC)?,
})
}
}
const VARIANT: usize = 12;
const REFERENCE: usize = 16;
#[must_use]
pub fn clone_template(layout: &WorkshopLayout, template: &[u8], workshop: Guid) -> Option<Vec<u8>> {
let mut blob = Blob::new(template);
let (start, count) = blob.array(layout.components)?;
let id = workshop.fnv1a().to_le_bytes();
let mut patched = [false; 3];
for variant in (0..usize::try_from(count).ok()?).map(|i| start + i * VARIANT) {
let hash = blob.read_u32(variant)?;
let data = (variant + 4).checked_add(usize::try_from(blob.read_u32(variant + 4)?).ok()?)?;
let size = usize::try_from(blob.read_u32(variant + 8)?).ok()?;
let Some(slot) = layout.crafting_types.iter().position(|&ty| ty == hash) else {
continue;
};
let [guid_at, id_at, expected] = match slot {
0 => layout.crafting,
1 => layout.interaction,
_ => [usize::MAX, layout.client[0], layout.client[1]],
};
if size != expected {
return None;
}
let written = (guid_at == usize::MAX || blob.write(data + guid_at, &workshop.0))
&& blob.write(data + id_at, &id);
if !written {
return None;
}
patched[slot] = true;
}
patched.iter().all(|&p| p).then(|| blob.into_bytes())
}
#[must_use]
pub fn clone_station(
items: &ItemLayout,
layout: &WorkshopLayout,
like: &[u8],
new: &NewWorkshop,
) -> Option<Vec<u8>> {
let placed = like.get(layout.placed_entity..layout.placed_entity + REFERENCE)?;
if placed != new.like.template.0 {
return None;
}
let mut item = Blob::new(&items::clone_item(
items,
like,
&item_of(new),
new.item.guid(),
)?);
item.write(layout.placed_entity, &new.template.guid().0)
.then(|| item.into_bytes())
}
#[must_use]
pub fn list_template(
layout: &WorkshopLayout,
collection: &[u8],
template: Guid,
) -> Option<Vec<u8>> {
let mut blob = Blob::new(collection);
blob.grow_array(
layout.templates,
REFERENCE,
4,
&[],
&[(template.0.to_vec(), None)],
)?;
Some(blob.into_bytes())
}
#[must_use]
pub fn list_workshop(
layout: &WorkshopLayout,
registry: &[u8],
new: &NewWorkshop,
) -> Option<Vec<u8>> {
let mut blob = Blob::new(registry);
let (start, count) = blob.array(layout.workshops)?;
let like = new.like.item.fnv1a();
let vanilla = (0..usize::try_from(count).ok()?)
.map(|i| start + i * layout.entry)
.find(|&at| blob.read_u32(at + layout.entry_item) == Some(like))?;
let mut entry = blob.bytes().get(vanilla..vanilla + layout.entry)?.to_vec();
let mut put = |at: usize, bytes: &[u8]| {
entry
.get_mut(at..at + bytes.len())
.map(|target| target.copy_from_slice(bytes))
};
put(layout.entry_guid, &new.workshop.guid().0)?;
put(layout.entry_id, &new.workshop.guid().fnv1a().to_le_bytes())?;
put(
layout.entry_item,
&crate::ItemId::from_guid(new.item.guid())
.get()
.to_le_bytes(),
)?;
put(layout.entry_item_ref, &new.item.guid().0)?;
if let Some(name) = new.name {
put(layout.entry_name, &name.fnv1a().to_le_bytes())?;
}
blob.grow_array(layout.workshops, layout.entry, 4, &[], &[(entry, None)])?;
Some(blob.into_bytes())
}
fn item_of(new: &NewWorkshop) -> NewItem {
NewItem {
name: new.name,
description: new.description,
debug_name: new.debug_name.clone(),
..NewItem::new(new.item, new.like.item)
}
}
impl Load<'_> {
pub fn add_workshop(&mut self, new: NewWorkshop) -> Result<WorkshopId, Off> {
for id in [new.item, new.template, new.workshop] {
self.declare(id)?;
}
let layout = WorkshopLayout::resolve(self)?;
let item_layout = ItemLayout::resolve(self)?;
let workshop = WorkshopId::new(new.workshop.guid());
let (like, template) = (new.like, new.template);
self.serve(
template.key(types::TEMPLATE_RESOURCE)?,
like.template.key(types::TEMPLATE_RESOURCE)?,
move |base| clone_template(&layout, base, workshop.guid()),
)?;
let collection = TEMPLATE_COLLECTION.key(types::TEMPLATE_COLLECTION_RESOURCE)?;
self.serve(collection, collection, move |base| {
list_template(&layout, base, template.guid())
})?;
let station = new.clone();
self.serve(
new.item.key(types::ITEM_INFO)?,
like.item.key(types::ITEM_INFO)?,
move |base| clone_station(&item_layout, &layout, base, &station),
)?;
let (item, debug_name) = (new.item.guid(), new.debug_name.clone());
let registry = items::ITEM_REGISTRY.key(types::ITEM_REGISTRY_RESOURCE)?;
self.serve(registry, registry, move |base| {
items::list_item(&item_layout, base, item, &debug_name)
})?;
let recipe = new.station_recipe;
let registry = WORKSHOP_REGISTRY.key(types::WORKSHOP_REGISTRY_RESOURCE)?;
self.serve(registry, registry, move |base| {
list_workshop(&layout, base, &new)
})?;
super::menu::serve(self, move |menu, bundle| {
super::menu::list_workshop(menu, bundle, workshop)
})?;
if let Some(recipe) = recipe {
self.push_recipe(NewRecipe {
id: recipe.id,
like: like.item,
output: item,
output_count: 1,
cost: recipe.cost,
workshop: None,
})?;
}
Ok(workshop)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) const LAYOUT: WorkshopLayout = WorkshopLayout {
components: 16,
crafting_types: [CRAFTING, INTERACTION, CLIENT],
crafting: [0, 16, 20],
interaction: [12, 28, 32],
client: [12, 16],
placed_entity: 0x3c0,
templates: 0,
workshops: 8,
entry: 56,
entry_guid: 4,
entry_id: 20,
entry_name: 24,
entry_item: 28,
entry_item_ref: 32,
entry_npc: 0,
};
const ITEMS: ItemLayout = ItemLayout {
id: 0,
object: 4,
name: 0x48,
description: 0x68,
flags: 0x78,
pid_entity: 0x80,
ui_values: 0x90,
icon_image: 0x58,
ui_value: [0x28, 0, 4, 8, 0x24],
refs: 0,
names: 0x38,
};
pub(crate) const CRAFTING: u32 = 0xc1;
pub(crate) const INTERACTION: u32 = 0xc2;
pub(crate) const CLIENT: u32 = 0xc3;
const WORKSHOP_ID: Id = Id::new("test:bench.workshop");
const ITEM_ID: Id = Id::new("test:bench");
const TEMPLATE_ID: Id = Id::new("test:bench.template");
const WORKSHOP: Guid = WORKSHOP_ID.guid();
const ITEM: Guid = ITEM_ID.guid();
const TEMPLATE: Guid = TEMPLATE_ID.guid();
const NAME: Guid = Guid::parse("41b222ea-cefa-4774-a040-c43aa9f0b227");
const OLD: Guid = Guid::parse("965d3192-0287-49a2-ad51-a716139571a6");
fn new() -> NewWorkshop {
NewWorkshop {
like: stations::WORKBENCH,
item: ITEM_ID,
template: TEMPLATE_ID,
workshop: WORKSHOP_ID,
name: Some(NAME),
description: None,
station_recipe: None,
debug_name: "Bench".into(),
}
}
fn u32_at(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap())
}
pub(crate) fn station(components: &[(u32, usize)]) -> (Blob, Vec<usize>) {
let mut blob = Blob::new(&[0u8; 24]);
let variants = blob.append(&vec![0; components.len() * VARIANT], 4);
let mut data = Vec::new();
for (i, &(hash, size)) in components.iter().enumerate() {
let mut bytes = vec![0xee; size];
if size >= 20 {
bytes[..16].copy_from_slice(&OLD.0);
}
let at = blob.append(&bytes, 4);
let variant = variants + i * VARIANT;
let relative = u32::try_from(at - (variant + 4)).unwrap();
assert!(blob.write(variant, &hash.to_le_bytes()));
assert!(blob.write(variant + 4, &relative.to_le_bytes()));
assert!(blob.write(variant + 8, &u32::try_from(size).unwrap().to_le_bytes()));
data.push(at);
}
let count = u32::try_from(components.len()).unwrap();
assert!(blob.set_array(16, variants, count));
(blob, data)
}
#[test]
fn the_template_clone_crafts_at_the_new_workshop_only() {
let (blob, data) = station(&[
(0x1234_5678, 24),
(CRAFTING, 20),
(INTERACTION, 32),
(CLIENT, 16),
]);
let clone = clone_template(&LAYOUT, blob.bytes(), WORKSHOP).unwrap();
let id = WORKSHOP.fnv1a();
assert_eq!(&clone[data[0]..data[0] + 16], &OLD.0, "others stay");
assert_eq!(&clone[data[1]..data[1] + 16], &WORKSHOP.0);
assert_eq!(u32_at(&clone, data[1] + 16), id);
assert_eq!(&clone[data[2] + 12..data[2] + 28], &WORKSHOP.0);
assert_eq!(u32_at(&clone, data[2] + 28), id);
assert_eq!(
&clone[data[2]..data[2] + 12],
&blob.bytes()[data[2]..data[2] + 12],
"props in range and comfort stay"
);
assert_eq!(u32_at(&clone, data[3] + 12), id);
assert_eq!(clone.len(), blob.bytes().len());
}
#[test]
fn a_template_missing_a_component_or_of_another_size_is_refused() {
let (blob, _) = station(&[(CRAFTING, 20), (INTERACTION, 32)]);
assert!(clone_template(&LAYOUT, blob.bytes(), WORKSHOP).is_none());
let (blob, _) = station(&[(CRAFTING, 24), (INTERACTION, 32), (CLIENT, 16)]);
assert!(clone_template(&LAYOUT, blob.bytes(), WORKSHOP).is_none());
}
#[test]
fn the_station_item_places_the_new_template() {
let mut bench = vec![0u8; 0x400];
bench[0x3c0..0x3d0].copy_from_slice(&stations::WORKBENCH.template.0);
let clone = clone_station(&ITEMS, &LAYOUT, &bench, &new()).unwrap();
assert_eq!(&clone[..4], &ITEM.fnv1a().to_le_bytes());
assert_eq!(&clone[0x3c0..0x3d0], &TEMPLATE.0);
assert_eq!(&clone[0x48..0x58], &NAME.0);
bench[0x3c0] ^= 1;
assert!(clone_station(&ITEMS, &LAYOUT, &bench, &new()).is_none());
}
#[test]
fn the_registries_list_the_new_workshop_and_template_after_the_others() {
let mut blob = Blob::new(&[0u8; 24]);
let mut entry = vec![0u8; 56];
entry[4..20].copy_from_slice(&OLD.0);
entry[20..24].copy_from_slice(&OLD.fnv1a().to_le_bytes());
entry[24..28].copy_from_slice(&7u32.to_le_bytes());
entry[28..32].copy_from_slice(&stations::WORKBENCH.item.fnv1a().to_le_bytes());
entry[48..52].copy_from_slice(&3u32.to_le_bytes());
let at = blob.append(&entry, 4);
assert!(blob.set_array(8, at, 1));
let built = Blob::new(&list_workshop(&LAYOUT, blob.bytes(), &new()).unwrap());
let (start, count) = built.array(8).unwrap();
assert_eq!(count, 2);
let added = &built.bytes()[start + 56..start + 112];
assert_eq!(&added[4..20], &WORKSHOP.0);
assert_eq!(u32_at(added, 20), WORKSHOP.fnv1a());
assert_eq!(u32_at(added, 24), NAME.fnv1a());
assert_eq!(u32_at(added, 28), ITEM.fnv1a());
assert_eq!(&added[32..48], &ITEM.0);
assert_eq!(u32_at(added, 48), 3, "upgrade masks stay");
assert_eq!(&built.bytes()[start + 4..start + 20], &OLD.0);
let mut collection = Blob::new(&[0u8; 8]);
let first = collection.append(&OLD.0, 4);
assert!(collection.set_array(0, first, 1));
let listed = Blob::new(&list_template(&LAYOUT, collection.bytes(), TEMPLATE).unwrap());
let (start, count) = listed.array(0).unwrap();
assert_eq!(count, 2);
assert_eq!(&listed.bytes()[start + 16..start + 32], &TEMPLATE.0);
}
#[test]
fn a_registry_without_the_vanilla_station_builds_nothing() {
let mut blob = Blob::new(&[0u8; 24]);
assert!(blob.set_array(8, 24, 0));
assert!(list_workshop(&LAYOUT, blob.bytes(), &new()).is_none());
}
}