use std::sync::Arc;
use super::menu::{self, MenuEntry};
use super::workshops::WorkshopId;
use super::{Blob, DebugName, Guid, Id, types};
use crate::{Load, Off};
pub const WORKSHOP_RECIPES: usize = 512;
pub const RESERVED_FOR_SINGLE: usize = 16;
#[must_use]
pub fn workshop_limit() -> usize {
let Some(host) = crate::host() else {
return WORKSHOP_RECIPES;
};
let api = host.api();
match (api.workshop_recipes)() {
u32::MAX => usize::MAX,
limit => usize::try_from(limit).unwrap_or(WORKSHOP_RECIPES),
}
}
pub const RECIPE_REGISTRY: Guid = Guid::parse("5e57de0f-280c-4559-8191-f02c1ba656e2");
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct NewRecipe {
pub id: Id,
pub like: Guid,
pub output: Guid,
pub output_count: u32,
pub cost: u32,
pub workshop: Option<WorkshopId>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RecipeLayout {
pub recipes: usize,
pub recipe: usize,
pub guid: usize,
pub id: usize,
pub workshop_guid: usize,
pub workshop: usize,
pub input: usize,
pub output: usize,
pub completion: usize,
pub relative: [usize; 4],
pub input_size: usize,
pub stack_size: usize,
pub stack_item: usize,
pub stack_count: usize,
}
impl RecipeLayout {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
Self::with_lookup(
|ty, name| {
load.field_offset(ty, name)
.map(|o| usize::try_from(o).unwrap_or(usize::MAX))
},
|ty| load.type_size(ty).map(|s| s as usize),
)
}
pub fn with_lookup(
field: impl Fn(&str, &str) -> Result<usize, Off>,
size: impl Fn(&str) -> Result<usize, Off>,
) -> Result<Self, Off> {
let recipe = |name: &str| field("keen::RecipeInfo", name);
let stack = |name: &str| field("keen::RecipeItemStackResource", name);
Ok(Self {
recipes: field("keen::RecipeRegistryResource", "recipes")?,
recipe: size("keen::RecipeInfo")?,
guid: recipe("recipeGuid")?,
id: recipe("recipeId")?,
workshop_guid: recipe("workshopGuid")?,
workshop: recipe("workshopId")?,
input: recipe("input")?,
output: recipe("output")?,
completion: recipe("completionRequirementQuery")?,
relative: [
recipe("input")?,
recipe("output")?,
recipe("requiredProps")?,
recipe("debugName")?,
],
input_size: size("keen::RecipeInputResource")?,
stack_size: size("keen::RecipeItemStackResource")?,
stack_item: stack("item")?,
stack_count: stack("count")?,
})
}
}
fn fnv1a(bytes: &[u8]) -> u32 {
bytes.iter().fold(0x811c_9dc5_u32, |hash, &b| {
(hash ^ u32::from(b)).wrapping_mul(0x0100_0193)
})
}
#[must_use]
pub fn add_recipe_to(
layout: &RecipeLayout,
registry: &[u8],
recipe: &NewRecipe,
limit: usize,
) -> Option<Vec<u8>> {
let mut registry = Blob::new(registry);
let (start, count) = registry.array(layout.recipes)?;
let like = (0..usize::try_from(count).ok()?)
.map(|i| start + i * layout.recipe)
.find(|&at| makes(layout, ®istry, at, recipe.like))?;
let workshop = match recipe.workshop {
Some(target) => target.id(),
None => registry.read_u32(like + layout.workshop)?,
};
if workshop_counts(layout, ®istry)?
.get(&workshop)
.copied()
.unwrap_or(0)
>= limit
{
crate::warn!("recipe skipped: workshop {workshop} holds {limit} recipes, the game's most");
return Some(registry.into_bytes());
}
let template = registry.bytes().get(like..like + layout.recipe)?.to_vec();
let inputs = scaled_inputs(layout, ®istry, like, recipe.cost)?;
let index = registry.grow_array(
layout.recipes,
layout.recipe,
8,
&layout.relative,
&[(template, Some(like))],
)?;
let (start, _) = registry.array(layout.recipes)?;
let at = start + usize::try_from(index).ok()? * layout.recipe;
let id = recipe.id.id().to_le_bytes();
let mut output = vec![0u8; layout.stack_size];
output.get_mut(..16)?.copy_from_slice(&recipe.output.0);
output
.get_mut(layout.stack_item..layout.stack_item + 4)?
.copy_from_slice(&recipe.output.id().to_le_bytes());
output
.get_mut(layout.stack_count..layout.stack_count + 4)?
.copy_from_slice(&recipe.output_count.to_le_bytes());
let input_count = u32::try_from(inputs.len() / layout.input_size.max(1)).ok()?;
let inputs_at = registry.append(&inputs, 8);
let output_at = registry.append(&output, 8);
let written = registry.write(at + layout.guid, &recipe.id.guid().0)
&& registry.write(at + layout.id, &id)
&& registry.write(at + layout.completion, &id)
&& registry.set_array(at + layout.input, inputs_at, input_count)
&& registry.set_array(at + layout.output, output_at, 1)
&& recipe
.workshop
.is_none_or(|target| retarget(layout, &mut registry, at, target));
written.then(|| registry.into_bytes())
}
fn retarget(layout: &RecipeLayout, blob: &mut Blob, at: usize, workshop: WorkshopId) -> bool {
blob.write(at + layout.workshop_guid, &workshop.guid().0)
&& blob.write(at + layout.workshop, &workshop.id().to_le_bytes())
}
fn workshop_counts(
layout: &RecipeLayout,
blob: &Blob,
) -> Option<std::collections::HashMap<u32, usize>> {
let (start, count) = blob.array(layout.recipes)?;
let mut counts = std::collections::HashMap::new();
for i in 0..usize::try_from(count).ok()? {
let workshop = blob.read_u32(start + i * layout.recipe + layout.workshop)?;
*counts.entry(workshop).or_insert(0) += 1;
}
Some(counts)
}
fn makes(layout: &RecipeLayout, registry: &Blob, at: usize, item: Guid) -> bool {
let Some((outputs, count)) = registry.array(at + layout.output) else {
return false;
};
(0..count as usize).any(|i| {
let stack = outputs + i * layout.stack_size;
registry.bytes().get(stack..stack + 16) == Some(&item.0[..])
})
}
fn vanilla_registry() -> Option<Vec<u8>> {
super::game_resource(RECIPE_REGISTRY.key(types::RECIPE_REGISTRY))
}
fn recipe_guids(layout: &RecipeLayout, registry: &[u8]) -> Option<std::collections::HashSet<Guid>> {
let blob = Blob::new(registry);
let (start, count) = blob.array(layout.recipes)?;
Some(
(0..usize::try_from(count).ok()?)
.map(|i| {
RecipeView {
blob: &blob,
at: start + i * layout.recipe,
layout,
}
.guid()
})
.collect(),
)
}
fn recipe_making(layout: &RecipeLayout, registry: &[u8], item: Guid) -> Option<Guid> {
let registry = Blob::new(registry);
let (start, count) = registry.array(layout.recipes)?;
let at = (0..usize::try_from(count).ok()?)
.map(|i| start + i * layout.recipe)
.find(|&at| makes(layout, ®istry, at, item))?;
let view = RecipeView {
blob: ®istry,
at,
layout,
};
Some(view.guid())
}
fn menu_entry(layout: &RecipeLayout, recipe: &NewRecipe) -> MenuEntry {
MenuEntry {
recipe: recipe.id.guid(),
after: vanilla_registry()
.and_then(|registry| recipe_making(layout, ®istry, recipe.like)),
workshop: recipe.workshop,
}
}
fn derived_entries(
layout: &RecipeLayout,
registry: &[u8],
salt: u32,
choose: &dyn Fn(&RecipeView<'_>) -> Option<Derived>,
) -> Option<Vec<MenuEntry>> {
let blob = Blob::new(registry);
let (start, count) = blob.array(layout.recipes)?;
Some(
(0..usize::try_from(count).ok()?)
.filter_map(|i| {
let view = RecipeView {
blob: &blob,
at: start + i * layout.recipe,
layout,
};
let derived = choose(&view)?;
Some(MenuEntry {
recipe: derived_guid(view.guid(), salt),
after: Some(view.guid()),
workshop: derived.workshop,
})
})
.collect(),
)
}
fn scaled_inputs(layout: &RecipeLayout, registry: &Blob, at: usize, cost: u32) -> Option<Vec<u8>> {
let (inputs, count) = registry.array(at + layout.input)?;
let mut bytes = registry
.bytes()
.get(inputs..inputs + usize::try_from(count).ok()? * layout.input_size)?
.to_vec();
for input in bytes.chunks_mut(layout.input_size.max(1)) {
let field = input.get_mut(layout.stack_count..layout.stack_count + 4)?;
let count = u32::from_le_bytes(field.try_into().ok()?);
field.copy_from_slice(&count.saturating_mul(cost).to_le_bytes());
}
Some(bytes)
}
#[derive(Clone, Copy, Debug)]
pub struct RecipeView<'a> {
blob: &'a Blob,
at: usize,
layout: &'a RecipeLayout,
}
impl RecipeView<'_> {
#[must_use]
pub fn guid(&self) -> Guid {
let mut guid = [0u8; 16];
if let Some(bytes) = self
.blob
.bytes()
.get(self.at + self.layout.guid..self.at + self.layout.guid + 16)
{
guid.copy_from_slice(bytes);
}
Guid(guid)
}
#[must_use]
pub fn debug_name(&self) -> String {
self.blob
.array(self.at + self.layout.relative[3])
.and_then(|(start, len)| self.blob.bytes().get(start..start + len as usize))
.map(|bytes| String::from_utf8_lossy(bytes).into_owned())
.unwrap_or_default()
}
#[must_use]
pub fn output(&self) -> Option<(Guid, u32)> {
let (outputs, count) = self.blob.array(self.at + self.layout.output)?;
if count == 0 {
return None;
}
let mut guid = [0u8; 16];
guid.copy_from_slice(self.blob.bytes().get(outputs..outputs + 16)?);
let made = self.blob.read_u32(outputs + self.layout.stack_count)?;
Some((Guid(guid), made))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Derived {
pub output: u32,
pub cost: u32,
pub workshop: Option<WorkshopId>,
}
#[must_use]
pub fn derived_guid(base: Guid, salt: u32) -> Guid {
let mut guid = base.0;
for (byte, salt) in guid[..4].iter_mut().zip(salt.to_le_bytes()) {
*byte ^= salt;
}
for (byte, salt) in guid[12..]
.iter_mut()
.zip(salt.rotate_left(16).to_le_bytes())
{
*byte ^= salt;
}
Guid(guid)
}
#[must_use]
pub fn derive_recipes_in(
layout: &RecipeLayout,
registry: &[u8],
salt: u32,
limit: usize,
choose: &dyn Fn(&RecipeView<'_>) -> Option<Derived>,
) -> Option<Vec<u8>> {
let cap = if limit == usize::MAX {
limit
} else {
limit.saturating_sub(RESERVED_FOR_SINGLE)
};
let mut blob = Blob::new(registry);
let (start, count) = blob.array(layout.recipes)?;
let mut counts = workshop_counts(layout, &blob)?;
let mut skipped = 0usize;
let mut picked: Vec<(usize, Derived, Guid)> = Vec::new();
for at in (0..usize::try_from(count).ok()?).map(|i| start + i * layout.recipe) {
let view = RecipeView {
blob: &blob,
at,
layout,
};
let Some(derived) = choose(&view) else {
continue;
};
let workshop = match derived.workshop {
Some(target) => target.id(),
None => blob.read_u32(at + layout.workshop)?,
};
let held = counts.entry(workshop).or_insert(0);
if *held >= cap {
skipped += 1;
continue;
}
*held += 1;
picked.push((at, derived, derived_guid(view.guid(), salt)));
}
if skipped > 0 {
crate::warn!(
"{skipped} derived recipes skipped: a workshop takes {limit} recipes at most, {RESERVED_FOR_SINGLE} kept for single ones; many recipes belong at a workshop or NPC of the mod's own"
);
}
if picked.is_empty() {
return Some(blob.into_bytes());
}
let extra: Vec<(Vec<u8>, Option<usize>)> = picked
.iter()
.map(|&(at, _, _)| Some((blob.bytes().get(at..at + layout.recipe)?.to_vec(), Some(at))))
.collect::<Option<_>>()?;
let mut changes = Vec::with_capacity(picked.len());
for &(at, derived, _) in &picked {
let inputs = scaled_inputs(layout, &blob, at, derived.cost)?;
let (outputs, _) = blob.array(at + layout.output)?;
let mut output = blob
.bytes()
.get(outputs..outputs + layout.stack_size)?
.to_vec();
let count_field = output.get_mut(layout.stack_count..layout.stack_count + 4)?;
let made = u32::from_le_bytes(count_field.try_into().ok()?);
count_field.copy_from_slice(&made.saturating_mul(derived.output).to_le_bytes());
changes.push((inputs, output));
}
let first = blob.grow_array(layout.recipes, layout.recipe, 8, &layout.relative, &extra)?;
let (start, _) = blob.array(layout.recipes)?;
for (index, ((_, derived, guid), (inputs, output))) in picked.iter().zip(changes).enumerate() {
let derived = *derived;
let at = start + (usize::try_from(first).ok()? + index) * layout.recipe;
let id = guid.id().to_le_bytes();
let input_count = u32::try_from(inputs.len() / layout.input_size.max(1)).ok()?;
let inputs_at = blob.append(&inputs, 8);
let output_at = blob.append(&output, 8);
let written = blob.write(at + layout.guid, &guid.0)
&& blob.write(at + layout.id, &id)
&& blob.write(at + layout.completion, &id)
&& blob.set_array(at + layout.input, inputs_at, input_count)
&& blob.set_array(at + layout.output, output_at, 1)
&& derived
.workshop
.is_none_or(|target| retarget(layout, &mut blob, at, target));
if !written {
return None;
}
}
Some(blob.into_bytes())
}
#[derive(Debug)]
pub struct RecipeEdit<'a> {
blob: &'a mut Blob,
at: usize,
layout: &'a RecipeLayout,
changed: bool,
}
impl RecipeEdit<'_> {
#[must_use]
pub fn view(&self) -> RecipeView<'_> {
RecipeView {
blob: self.blob,
at: self.at,
layout: self.layout,
}
}
pub fn scale_output(&mut self, by: u32) {
self.scale(self.layout.output, self.layout.stack_size, by);
}
pub fn scale_inputs(&mut self, by: u32) {
self.scale(self.layout.input, self.layout.input_size, by);
}
fn scale(&mut self, field: usize, size: usize, by: u32) {
let Some((start, count)) = self.blob.array(self.at + field) else {
return;
};
for i in 0..count as usize {
let at = start + i * size + self.layout.stack_count;
let Some(made) = self.blob.read_u32(at) else {
return;
};
let scaled = made.saturating_mul(by);
if scaled != made && self.blob.write(at, &scaled.to_le_bytes()) {
self.changed = true;
}
}
}
}
#[must_use]
pub fn modify_recipes_in(
layout: &RecipeLayout,
registry: &[u8],
modify: &dyn Fn(&mut RecipeEdit<'_>),
) -> Option<(Vec<u8>, usize)> {
let mut blob = Blob::new(registry);
let (start, count) = blob.array(layout.recipes)?;
let mut changed = 0;
for at in (0..usize::try_from(count).ok()?).map(|i| start + i * layout.recipe) {
let mut edit = RecipeEdit {
blob: &mut blob,
at,
layout,
changed: false,
};
modify(&mut edit);
changed += usize::from(edit.changed);
}
Some((blob.into_bytes(), changed))
}
impl Load<'_> {
pub fn add_recipe(&mut self, recipe: NewRecipe) -> Result<(), Off> {
self.declare(recipe.id)?;
let layout = RecipeLayout::resolve(self)?;
let registry = RECIPE_REGISTRY.key(types::RECIPE_REGISTRY);
self.serve(registry, registry, move |base| {
add_recipe_to(&layout, base, &recipe, workshop_limit())
})?;
menu::serve(self, move |menu, bundle| {
menu::list_recipes(menu, bundle, &[menu_entry(&layout, &recipe)])
})
}
pub fn add_recipe_with(
&mut self,
recipe: impl Fn() -> NewRecipe + Send + Sync + 'static,
) -> Result<(), Off> {
let id = recipe().id;
self.declare(id)?;
let layout = RecipeLayout::resolve(self)?;
let registry = RECIPE_REGISTRY.key(types::RECIPE_REGISTRY);
let recipe = Arc::new(recipe);
let built = Arc::clone(&recipe);
self.serve(registry, registry, move |base| {
let recipe = NewRecipe { id, ..built() };
add_recipe_to(&layout, base, &recipe, workshop_limit())
})?;
self.allow_rebuild(registry)?;
menu::serve(self, move |menu, bundle| {
let recipe = NewRecipe { id, ..recipe() };
menu::list_recipes(menu, bundle, &[menu_entry(&layout, &recipe)])
})
}
pub fn derive_recipes(
&mut self,
salt: &str,
choose: impl Fn(&RecipeView<'_>) -> Option<Derived> + Send + Sync + 'static,
) -> Result<(), Off> {
let layout = RecipeLayout::resolve(self)?;
let salt = fnv1a(salt.as_bytes());
let registry = RECIPE_REGISTRY.key(types::RECIPE_REGISTRY);
let choose = Arc::new(choose);
let chosen = Arc::clone(&choose);
self.serve(registry, registry, move |base| {
let vanilla =
vanilla_registry().and_then(|registry| recipe_guids(&layout, ®istry));
if vanilla.is_none() {
crate::warn!(
"derived recipes may copy recipes of other mods: the game's recipe registry does not read"
);
}
derive_recipes_in(&layout, base, salt, workshop_limit(), &|view| {
let added = vanilla
.as_ref()
.is_some_and(|vanilla| !vanilla.contains(&view.guid()));
if added { None } else { chosen(view) }
})
})?;
menu::serve(self, move |menu, bundle| {
let entries = vanilla_registry()
.and_then(|registry| derived_entries(&layout, ®istry, salt, &*choose));
let Some(entries) = entries else {
crate::warn!(
"derived recipes stay out of the crafting menu: the recipe registry does not read"
);
return Some(bundle.to_vec());
};
menu::list_recipes(menu, bundle, &entries)
})
}
pub fn modify_recipes(
&mut self,
modify: impl Fn(&mut RecipeEdit<'_>) + Send + Sync + 'static,
) -> Result<(), Off> {
let layout = RecipeLayout::resolve(self)?;
let registry = RECIPE_REGISTRY.key(types::RECIPE_REGISTRY);
self.serve(registry, registry, move |base| {
let (built, changed) = modify_recipes_in(&layout, base, &modify)?;
crate::info!("{changed} recipes changed");
Some(built)
})?;
self.allow_rebuild(registry)
}
}
pub fn rebuild_recipes() -> Result<(), Off> {
crate::content::rebuild(RECIPE_REGISTRY.key(types::RECIPE_REGISTRY))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Name<'a> {
Guid(Guid),
Content(Id),
Debug(DebugName<'a>),
}
impl From<Guid> for Name<'_> {
fn from(guid: Guid) -> Self {
Self::Guid(guid)
}
}
impl From<Id> for Name<'_> {
fn from(id: Id) -> Self {
Self::Content(id)
}
}
impl<'a> From<DebugName<'a>> for Name<'a> {
fn from(name: DebugName<'a>) -> Self {
Self::Debug(name)
}
}
impl Name<'_> {
fn text(self) -> std::borrow::Cow<'static, str> {
match self {
Self::Guid(guid) => guid.to_string().into(),
Self::Content(id) => id.name().into(),
Self::Debug(name) => name.0.to_owned().into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RecipeRule<'a> {
Recipe(Name<'a>),
Output(Name<'a>),
Workshop(Name<'a>),
Hand,
}
impl<'a> RecipeRule<'a> {
#[must_use]
pub fn recipe(name: impl Into<Name<'a>>) -> Self {
Self::Recipe(name.into())
}
#[must_use]
pub fn output(item: impl Into<Name<'a>>) -> Self {
Self::Output(item.into())
}
#[must_use]
pub fn workshop(item: impl Into<Name<'a>>) -> Self {
Self::Workshop(item.into())
}
fn parts(
self,
) -> (
crate::abi::RecipeRuleKind,
&'static str,
std::borrow::Cow<'static, str>,
) {
use crate::abi::RecipeRuleKind;
match self {
Self::Recipe(name) => (RecipeRuleKind::RECIPE, "recipe", name.text()),
Self::Output(name) => (RecipeRuleKind::OUTPUT, "output", name.text()),
Self::Workshop(name) => (RecipeRuleKind::WORKSHOP, "workshop", name.text()),
Self::Hand => (RecipeRuleKind::WORKSHOP, "workshop", "hand".into()),
}
}
}
fn rule(rule: RecipeRule<'_>, disable: bool) -> Result<(), Off> {
let host = crate::host().ok_or_else(|| Off::new("recipe rules: no loader"))?;
let api = host.api();
let (kind, what, name) = rule.parts();
if name.trim().is_empty() {
return Err(Off::new(format!("recipe rule {what}: no name")));
}
let status = (api.recipe_rule)(
host.handle(),
kind,
crate::abi::Str::new(&name),
u32::from(disable),
);
crate::load::ok(status, (), || format!("recipe rule {what} {name:?}"))
}
pub fn disable(rule: RecipeRule<'_>) -> Result<(), Off> {
self::rule(rule, true)
}
pub fn enable(rule: RecipeRule<'_>) -> Result<(), Off> {
self::rule(rule, false)
}
#[must_use]
pub fn is_disabled(recipe: impl Into<Guid>) -> bool {
let Some(host) = crate::host() else {
return false;
};
let api = host.api();
let mut out = 0u32;
(api.recipe_disabled)(host.handle(), recipe.into().id(), &raw mut out).is_ok() && out != 0
}
#[cfg(test)]
mod tests {
use super::*;
const LAYOUT: RecipeLayout = RecipeLayout {
recipes: 8,
recipe: 0xa8,
guid: 0,
id: 0x10,
workshop_guid: 0x14,
workshop: 0x24,
input: 0x28,
output: 0x30,
completion: 0x5c,
relative: [0x28, 0x30, 0x40, 0x78],
input_size: 48,
stack_size: 24,
stack_item: 0x10,
stack_count: 0x14,
};
const CHEST: Guid = Guid::parse("6943053a-32c1-4718-ae22-e61427ddf54d");
const LOCKER: Guid = Guid::parse("8a3f6e21-5c4b-4d7e-9f10-2b7c5e6a9d31");
const RECIPE_ID: Id = Id::new("test:recipe");
const RECIPE: Guid = RECIPE_ID.guid();
fn u32_at(bytes: &[u8], at: usize) -> u32 {
u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap())
}
#[test]
fn the_copy_costs_more_and_makes_the_new_output() {
let mut blob = Blob::new(&[0u8; 16]);
let recipe = blob.append(&[0; 0xa8], 8);
let mut input = vec![0u8; 48];
input[0x14..0x18].copy_from_slice(&5u32.to_le_bytes());
let input_at = blob.append(&input, 8);
let mut output = vec![0u8; 24];
output[..16].copy_from_slice(&CHEST.0);
let output_at = blob.append(&output, 8);
let name_at = blob.append(b"Chest\0", 1);
assert!(blob.set_array(recipe + 0x28, input_at, 1));
assert!(blob.set_array(recipe + 0x30, output_at, 1));
assert!(blob.set_array(recipe + 0x78, name_at, 5));
assert!(blob.set_array(8, recipe, 1));
let new = NewRecipe {
id: RECIPE_ID,
like: CHEST,
output: LOCKER,
output_count: 1,
cost: 2,
workshop: None,
};
let built =
Blob::new(&add_recipe_to(&LAYOUT, blob.bytes(), &new, WORKSHOP_RECIPES).unwrap());
let (recipes, count) = built.array(8).unwrap();
assert_eq!(count, 2);
let copy = recipes + 0xa8;
assert_eq!(&built.bytes()[copy..copy + 16], &RECIPE.0);
assert_eq!(u32_at(built.bytes(), copy + 0x10), RECIPE.id());
assert_eq!(u32_at(built.bytes(), copy + 0x5c), RECIPE.id());
let (inputs, _) = built.array(copy + 0x28).unwrap();
assert_eq!(u32_at(built.bytes(), inputs + 0x14), 10);
let (outputs, _) = built.array(copy + 0x30).unwrap();
assert_eq!(&built.bytes()[outputs..outputs + 16], &LOCKER.0);
assert_eq!(u32_at(built.bytes(), outputs + 0x10), LOCKER.id());
let (name, len) = built.array(copy + 0x78).unwrap();
assert_eq!(&built.bytes()[name..name + len as usize], b"Chest");
let (inputs, _) = built.array(recipes + 0x28).unwrap();
assert_eq!(u32_at(built.bytes(), inputs + 0x14), 5);
}
#[test]
fn derived_copies_double_the_output_of_the_chosen_recipes_only() {
let mut blob = Blob::new(&[0u8; 16]);
let first = blob.append(&[0; 0xa8 * 2], 8);
for (i, (name, made)) in [(&b"Block_A\0"[..], 100u32), (&b"Prop_B\0"[..], 1)]
.iter()
.enumerate()
{
let at = first + i * 0xa8;
let mut output = vec![0u8; 24];
output[0x14..0x18].copy_from_slice(&made.to_le_bytes());
let output_at = blob.append(&output, 8);
let name_at = blob.append(name, 1);
assert!(blob.set_array(at + 0x30, output_at, 1));
assert!(blob.set_array(at + 0x78, name_at, u32::try_from(name.len() - 1).unwrap()));
}
assert!(blob.set_array(8, first, 2));
let built = derive_recipes_in(&LAYOUT, blob.bytes(), 7, WORKSHOP_RECIPES, &|r| {
r.debug_name().starts_with("Block_").then_some(Derived {
output: 2,
cost: 1,
workshop: None,
})
})
.unwrap();
let built = Blob::new(&built);
let (recipes, count) = built.array(8).unwrap();
assert_eq!(count, 3);
let copy = RecipeView {
blob: &built,
at: recipes + 2 * 0xa8,
layout: &LAYOUT,
};
assert_eq!(copy.debug_name(), "Block_A");
assert_eq!(copy.output().unwrap().1, 200);
assert_eq!(copy.guid(), derived_guid(Guid([0; 16]), 7));
let original = RecipeView {
blob: &built,
at: recipes,
layout: &LAYOUT,
};
assert_eq!(original.output().unwrap().1, 100);
}
#[test]
fn a_recipe_can_move_to_another_workshop() {
let mut blob = Blob::new(&[0u8; 16]);
let recipe = blob.append(&[0; 0xa8], 8);
assert!(blob.write(recipe + 0x24, &7u32.to_le_bytes()));
let mut output = vec![0u8; 24];
output[..16].copy_from_slice(&CHEST.0);
let output_at = blob.append(&output, 8);
assert!(blob.set_array(recipe + 0x30, output_at, 1));
assert!(blob.set_array(8, recipe, 1));
let bench = WorkshopId::new(Guid::parse("5b828914-9b48-40f8-b541-72e9b942cee6"));
let new = NewRecipe {
id: RECIPE_ID,
like: CHEST,
output: LOCKER,
output_count: 1,
cost: 1,
workshop: Some(bench),
};
let built = add_recipe_to(&LAYOUT, blob.bytes(), &new, WORKSHOP_RECIPES).unwrap();
let built = Blob::new(&built);
let (recipes, _) = built.array(8).unwrap();
let copy = recipes + 0xa8;
assert_eq!(&built.bytes()[copy + 0x14..copy + 0x24], &bench.guid().0);
assert_eq!(u32_at(built.bytes(), copy + 0x24), bench.id());
assert_eq!(u32_at(built.bytes(), recipes + 0x24), 7);
let derived = derive_recipes_in(&LAYOUT, blob.bytes(), 7, 1, &|_| {
Some(Derived {
output: 2,
cost: 1,
workshop: Some(bench),
})
})
.unwrap();
assert_eq!(
Blob::new(&derived).array(8).unwrap().1,
1,
"limit 1, held 0"
);
let derived = derive_recipes_in(&LAYOUT, blob.bytes(), 7, RESERVED_FOR_SINGLE + 1, &|_| {
Some(Derived {
output: 2,
cost: 1,
workshop: Some(bench),
})
})
.unwrap();
let derived = Blob::new(&derived);
let (recipes, count) = derived.array(8).unwrap();
assert_eq!(count, 2);
assert_eq!(u32_at(derived.bytes(), recipes + 0xa8 + 0x24), bench.id());
}
#[test]
fn modified_recipes_change_in_place_and_stay_one_each() {
let mut blob = Blob::new(&[0u8; 16]);
let first = blob.append(&[0; 0xa8 * 2], 8);
for (i, name) in [&b"Block_A"[..], &b"Prop_Decoration_Chest"[..]]
.iter()
.enumerate()
{
let at = first + i * 0xa8;
let mut input = vec![0u8; 48 * 2];
input[0x14..0x18].copy_from_slice(&3u32.to_le_bytes());
input[48 + 0x14..48 + 0x18].copy_from_slice(&4u32.to_le_bytes());
let input_at = blob.append(&input, 8);
let mut output = vec![0u8; 24];
output[0x14..0x18].copy_from_slice(&10u32.to_le_bytes());
let output_at = blob.append(&output, 8);
let name_at = blob.append(name, 1);
assert!(blob.set_array(at + 0x28, input_at, 2));
assert!(blob.set_array(at + 0x30, output_at, 1));
assert!(blob.set_array(at + 0x78, name_at, u32::try_from(name.len()).unwrap()));
}
assert!(blob.set_array(8, first, 2));
let (built, changed) = modify_recipes_in(&LAYOUT, blob.bytes(), &|recipe| {
if recipe.view().debug_name().starts_with("Block_") {
recipe.scale_output(3);
recipe.scale_inputs(1);
}
})
.unwrap();
assert_eq!(changed, 1);
assert_eq!(built.len(), blob.bytes().len(), "in place, nothing added");
let built = Blob::new(&built);
let (recipes, count) = built.array(8).unwrap();
assert_eq!(count, 2);
let view = |at| RecipeView {
blob: &built,
at,
layout: &LAYOUT,
};
assert_eq!(view(recipes).output().unwrap().1, 30);
assert_eq!(view(recipes + 0xa8).output().unwrap().1, 10);
let (inputs, _) = built.array(recipes + 0x28).unwrap();
assert_eq!(u32_at(built.bytes(), inputs + 0x14), 3);
assert_eq!(u32_at(built.bytes(), inputs + 48 + 0x14), 4);
let (again, _) =
modify_recipes_in(&LAYOUT, built.bytes(), &|recipe| recipe.scale_inputs(2)).unwrap();
let again = Blob::new(&again);
let (inputs, _) = again.array(recipes + 0x28).unwrap();
assert_eq!(u32_at(again.bytes(), inputs + 48 + 0x14), 8);
assert_eq!(view(recipes).output().unwrap().1, 30);
}
#[test]
fn an_item_without_a_recipe_builds_nothing() {
let mut blob = Blob::new(&[0u8; 16]);
assert!(blob.set_array(8, 16, 0));
let new = NewRecipe {
id: RECIPE_ID,
like: CHEST,
output: LOCKER,
output_count: 1,
cost: 1,
workshop: None,
};
assert!(add_recipe_to(&LAYOUT, blob.bytes(), &new, WORKSHOP_RECIPES).is_none());
}
}