use std::sync::{Mutex, PoisonError};
use super::{Blob, Guid, Id, types};
use crate::abi::ItemData;
use crate::{Load, Off};
static METERS: crate::fresh::Fresh<Mutex<Vec<Meter>>> = crate::fresh::Fresh::new();
pub const ITEM_REGISTRY: Guid = Guid::parse("3064d35d-7342-40ca-bdc9-aad58f83bf45");
pub const METER_TEMPLATE_BASE: Guid = Guid::parse("39246eb2-1b9f-4881-8c60-e0e4f5d956f8");
const FUEL_SETUP: u32 = 0x139a_fbec;
#[must_use]
pub fn vanilla_name_tag(item: Guid) -> Option<Guid> {
let host = crate::host()?;
let mut offset = 0u64;
let status = (host.api().field_offset)(
crate::abi::Str::new("keen::ItemInfo"),
crate::abi::Str::new("name"),
&raw mut offset,
);
if !status.is_ok() {
return None;
}
let at = usize::try_from(offset).ok()?;
let info = host.game_resource(item.key(types::ITEM_INFO))?;
Some(Guid(info.get(at..at.checked_add(16)?)?.try_into().ok()?))
}
#[must_use]
pub fn vanilla_name(item: Guid, language: crate::i18n::Language) -> Option<&'static str> {
if crate::host()?.build().side == crate::abi::Side::SERVER {
return None;
}
crate::i18n::game_text_in(vanilla_name_tag(item)?.id(), language)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewItem {
pub id: Id,
pub like: Guid,
pub name: Option<Guid>,
pub description: Option<Guid>,
pub debug_name: String,
pub meter: Option<Meter>,
pub stats: Vec<Stat>,
pub icon: Option<super::icons::Icon>,
}
impl NewItem {
#[must_use]
pub fn new(id: Id, like: Guid) -> Self {
Self {
id,
like,
name: None,
description: None,
debug_name: id.name().to_owned(),
meter: None,
stats: Vec::new(),
icon: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Meter {
pub template: Id,
pub max: u16,
pub start: u16,
}
impl Meter {
#[must_use]
pub fn start_fill(self) -> f32 {
fill(self.start, self.max)
}
}
#[must_use]
pub fn fill(count: u16, max: u16) -> f32 {
if max == 0 {
return 0.0;
}
f32::from(count.min(max)) / f32::from(max)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Stat {
pub label: Guid,
pub value: i32,
pub format: StatFormat,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StatFormat {
Number,
Signed,
Percent,
}
impl StatFormat {
const fn game(self) -> (u8, bool) {
match self {
Self::Number => (0, false),
Self::Signed => (0, true),
Self::Percent => (1, false),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ItemLayout {
pub id: usize,
pub object: usize,
pub name: usize,
pub description: usize,
pub flags: usize,
pub pid_entity: usize,
pub ui_values: usize,
pub icon_image: usize,
pub ui_value: [usize; 5],
pub refs: usize,
pub names: usize,
}
impl ItemLayout {
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 item = |name: &str| field("keen::ItemInfo", name);
let value = |name: &str| field("keen::ImpactUiValue", name);
Ok(Self {
id: item("itemId")?,
object: item("objectId")?,
name: item("name")?,
description: item("description")?,
flags: item("flags")?,
pid_entity: item("pidEntity")?,
ui_values: item("uiValues")?,
icon_image: item("iconImage")?,
ui_value: [
size("keen::ImpactUiValue")?,
value("locaId")?,
value("valueFormat")?,
value("value")?,
value("isSigned")?,
],
refs: field("keen::ItemRegistryResource", "itemRefs")?,
names: field("keen::ItemRegistryResource", "dbgNames")?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MeterLayout {
pub components: usize,
pub fuel_setup: [usize; 3],
pub templates: usize,
}
impl MeterLayout {
pub fn resolve(load: &Load<'_>) -> Result<Self, Off> {
let field = |ty: &str, name: &str| {
load.field_offset(ty, name)
.map(|o| usize::try_from(o).unwrap_or(usize::MAX))
};
Ok(Self {
components: field("keen::ecs::TemplateResource", "components")?,
fuel_setup: [
load.type_size("keen::ecs::FuelSetup")? as usize,
field("keen::ecs::FuelSetup", "maxFuelCharges")?,
field("keen::ecs::FuelSetup", "initialFuelPercentage")?,
],
templates: field("keen::ecs::TemplateCollectionResource", "templates")?,
})
}
}
const REFERENCE: usize = 16;
const BLOB_STRING: usize = 8;
const VARIANT: usize = 12;
#[must_use]
pub fn clone_item(layout: &ItemLayout, like: &[u8], item: &NewItem, guid: Guid) -> Option<Vec<u8>> {
let mut info = Blob::new(like);
let mut written =
info.write(layout.id, &guid.id().to_le_bytes()) && info.write(layout.object, &guid.0);
if let Some(name) = item.name {
written &= info.write(layout.name, &name.0);
}
if let Some(description) = item.description {
written &= info.write(layout.description, &description.0);
}
if let Some(meter) = item.meter {
let flags = info.read_u32(layout.flags)?;
if flags & ItemData::HAS_PIDE != 0 || meter.max == 0 {
return None;
}
let flags = flags | ItemData::HAS_PIDE | ItemData::USES_FUEL;
written &= info.write(layout.flags, &flags.to_le_bytes())
&& info.write(layout.pid_entity, &meter.template.guid().0);
}
if let Some(icon) = item.icon {
written &= info.write(layout.icon_image, &icon.id.guid().0);
}
if !item.stats.is_empty() {
let [size, label, format, value, signed] = layout.ui_value;
let mut rows = Vec::with_capacity(item.stats.len());
for stat in &item.stats {
let mut row = vec![0u8; size];
let (number_format, is_signed) = stat.format.game();
let mut put = |at: usize, bytes: &[u8]| {
row.get_mut(at..at + bytes.len())
.map(|target| target.copy_from_slice(bytes))
};
put(label, &stat.label.id().to_le_bytes())?;
put(format, &[number_format])?;
put(value, &stat.value.to_le_bytes())?;
put(signed, &[u8::from(is_signed)])?;
rows.push((row, None));
}
info.grow_array(layout.ui_values, size, 4, &[], &rows)?;
}
written.then(|| info.into_bytes())
}
#[must_use]
pub fn clone_meter_template(
layout: &MeterLayout,
template: &[u8],
meter: &Meter,
) -> Option<Vec<u8>> {
let mut blob = Blob::new(template);
let (start, count) = blob.array(layout.components)?;
let [expected, max_at, fill_at] = layout.fuel_setup;
for variant in (0..usize::try_from(count).ok()?).map(|i| start + i * VARIANT) {
if blob.read_u32(variant)? != FUEL_SETUP {
continue;
}
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()?;
if size != expected {
return None;
}
let max = i32::from(meter.max);
let written = blob.write(data + max_at, &max.to_le_bytes())
&& blob.write(data + fill_at, &meter.start_fill().to_le_bytes());
return written.then(|| blob.into_bytes());
}
None
}
#[must_use]
pub fn list_meter_template(
layout: &MeterLayout,
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_item(
layout: &ItemLayout,
registry: &[u8],
guid: Guid,
debug_name: &str,
) -> Option<Vec<u8>> {
let mut registry = Blob::new(registry);
let (first, refs) = registry.array(layout.refs)?;
let (_, names) = registry.array(layout.names)?;
let listed = registry
.bytes()
.get(first..first.checked_add(usize::try_from(refs).ok()?.checked_mul(REFERENCE)?)?)?;
if let Some(other) = listed
.as_chunks::<REFERENCE>()
.0
.iter()
.map(|r| Guid(*r))
.find(|other| other.id() == guid.id() && *other != guid)
{
crate::warn!(
"item {debug_name} ({guid}) has the game id {:#010x} of item {other}; it needs another id",
guid.id()
);
return None;
}
registry.grow_array(layout.refs, REFERENCE, 4, &[], &[(guid.0.to_vec(), None)])?;
if names == refs {
let mut text = debug_name.as_bytes().to_vec();
text.push(0);
let at = registry.append(&text, 1);
let first = registry.grow_array(
layout.names,
BLOB_STRING,
4,
&[0],
&[(vec![0; BLOB_STRING], None)],
)?;
let (start, _) = registry.array(layout.names)?;
let element = start + usize::try_from(first).ok()? * BLOB_STRING;
if !registry.set_array(element, at, u32::try_from(debug_name.len()).ok()?) {
return None;
}
}
Some(registry.into_bytes())
}
impl Load<'_> {
pub fn add_item(&mut self, item: NewItem) -> Result<(), Off> {
self.declare(item.id)?;
if let Some(meter) = item.meter {
self.declare(meter.template)?;
}
for old in crate::migrations::old_guids(item.id.guid()) {
let debug_name = format!("{} (renamed)", item.debug_name);
self.add_one_item(item.clone(), old, debug_name)?;
}
let (guid, debug_name) = (item.id.guid(), item.debug_name.clone());
self.add_one_item(item, guid, debug_name)
}
fn add_one_item(&mut self, item: NewItem, guid: Guid, debug_name: String) -> Result<(), Off> {
let layout = ItemLayout::resolve(self)?;
let meter = item.meter;
if meter.is_some_and(|m| m.max == 0) {
return Err(Off::new(format!(
"item {}: a meter holds at least 1",
item.debug_name
)));
}
let like = item.like;
if let Some(icon) = item.icon {
self.add_icon(icon)?;
}
self.serve(
guid.key(types::ITEM_INFO),
like.key(types::ITEM_INFO),
move |base| {
if item.icon.is_some() && !super::icons::ready() {
let vanilla = NewItem {
icon: None,
..item.clone()
};
return clone_item(&layout, base, &vanilla, guid);
}
clone_item(&layout, base, &item, guid)
},
)?;
let registry = ITEM_REGISTRY.key(types::ITEM_REGISTRY);
self.serve(registry, registry, move |base| {
list_item(&layout, base, guid, &debug_name)
})?;
if let Some(meter) = meter {
let mut served = METERS
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(PoisonError::into_inner);
match served.iter().find(|m| m.template == meter.template) {
Some(same) if *same == meter => return Ok(()),
Some(_) => {
return Err(Off::new(format!(
"meter template {}: served already with another maximum or start",
meter.template
)));
}
None => served.push(meter),
}
drop(served);
let layout = MeterLayout::resolve(self)?;
self.serve(
meter.template.key(types::TEMPLATE),
METER_TEMPLATE_BASE.key(types::TEMPLATE),
move |base| clone_meter_template(&layout, base, &meter),
)?;
let collection = super::workshops::TEMPLATE_COLLECTION.key(types::TEMPLATE_COLLECTION);
self.serve(collection, collection, move |base| {
list_meter_template(&layout, base, meter.template.guid())
})?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const LAYOUT: 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,
};
const CHEST: Guid = Guid::parse("6943053a-32c1-4718-ae22-e61427ddf54d");
const NEW_ID: Id = Id::new("test:new");
const NEW: Guid = NEW_ID.guid();
const NAME: Guid = Guid::parse("b7c1d2e3-4f50-4a61-8b72-c3d4e5f60718");
const TEMPLATE_ID: Id = Id::new("test:charges");
const TEMPLATE: Guid = TEMPLATE_ID.guid();
fn string(blob: &Blob, header: usize) -> Vec<u8> {
let (at, len) = blob.array(header).unwrap();
blob.bytes()[at..at + len as usize].to_vec()
}
#[test]
fn a_clone_takes_the_new_identity_and_keeps_the_rest() {
let mut chest = vec![0u8; 0x98];
chest[4..20].copy_from_slice(&CHEST.0);
chest[0x70] = 9;
let item = NewItem {
name: Some(NAME),
..NewItem::new(NEW_ID, CHEST)
};
let clone = clone_item(&LAYOUT, &chest, &item, NEW).unwrap();
assert_eq!(&clone[..4], &NEW.id().to_le_bytes());
assert_eq!(&clone[4..20], &NEW.0);
assert_eq!(&clone[0x48..0x58], &NAME.0);
assert_eq!(clone[0x70], 9);
assert_eq!(clone.len(), chest.len(), "nothing grows without stats");
}
#[test]
fn a_meter_gives_the_item_an_entity_that_keeps_fuel() {
let mut staff = vec![0u8; 0x98];
staff[0x78..0x7c].copy_from_slice(&(1u32 << 8).to_le_bytes());
let meter = Meter {
template: TEMPLATE_ID,
max: 3,
start: 3,
};
let item = NewItem {
meter: Some(meter),
..NewItem::new(NEW_ID, CHEST)
};
let clone = Blob::new(&clone_item(&LAYOUT, &staff, &item, NEW).unwrap());
assert_eq!(
clone.read_u32(0x78),
Some(1 << 8 | ItemData::HAS_PIDE | ItemData::USES_FUEL)
);
assert_eq!(&clone.bytes()[0x80..0x90], &TEMPLATE.0);
let mut sword = staff.clone();
sword[0x78..0x7c].copy_from_slice(&ItemData::HAS_PIDE.to_le_bytes());
assert!(
clone_item(&LAYOUT, &sword, &item, NEW).is_none(),
"a durability entity stays"
);
}
#[test]
fn stats_follow_the_vanilla_lines() {
let mut armour = Blob::new(&[0u8; 0x98]);
let mut vanilla = vec![0u8; 0x28];
vanilla[0..4].copy_from_slice(&7u32.to_le_bytes());
let first = armour.append(&vanilla, 4);
assert!(armour.set_array(0x90, first, 1));
let item = NewItem {
stats: vec![
Stat {
label: NAME,
value: 3,
format: StatFormat::Number,
},
Stat {
label: CHEST,
value: -2,
format: StatFormat::Signed,
},
],
..NewItem::new(NEW_ID, CHEST)
};
let clone = Blob::new(&clone_item(&LAYOUT, armour.bytes(), &item, NEW).unwrap());
let (rows, count) = clone.array(0x90).unwrap();
assert_eq!(count, 3);
assert_eq!(clone.read_u32(rows), Some(7));
assert_eq!(clone.read_u32(rows + 0x28), Some(NAME.id()));
assert_eq!(clone.read_u32(rows + 0x28 + 8), Some(3));
assert_eq!(clone.bytes()[rows + 0x28 + 0x24], 0);
assert_eq!(clone.read_u32(rows + 0x50), Some(CHEST.id()));
assert_eq!(
clone.read_u32(rows + 0x50 + 8),
Some((-2i32).cast_unsigned())
);
assert_eq!(clone.bytes()[rows + 0x50 + 0x24], 1);
}
#[test]
fn fills_give_back_the_count() {
for max in 1..=20u16 {
for count in 0..=max {
let back = (f32::from(max) * fill(count, max) + 0.5).floor();
assert!(
(back - f32::from(count)).abs() < f32::EPSILON,
"{count}/{max}"
);
}
}
}
#[test]
fn a_meter_template_holds_the_maximum_and_the_start() {
let layout = MeterLayout {
components: 0,
fuel_setup: [0x28, 0x20, 0x24],
templates: 0,
};
let mut template = vec![8, 0, 0, 0, 2, 0, 0, 0];
for (hash, offset, size) in [(0x1234_u32, 20u32, 4u32), (FUEL_SETUP, 12, 0x28)] {
template.extend_from_slice(&hash.to_le_bytes());
template.extend_from_slice(&offset.to_le_bytes());
template.extend_from_slice(&size.to_le_bytes());
}
template.extend_from_slice(&[0; 4 + 0x28]);
let meter = Meter {
template: TEMPLATE_ID,
max: 5,
start: 2,
};
let clone = Blob::new(&clone_meter_template(&layout, &template, &meter).unwrap());
let setup = 20 + 4 + 12;
assert_eq!(clone.read_u32(setup + 0x20), Some(5));
assert_eq!(clone.read_u32(setup + 0x24), Some(0.4f32.to_bits()));
let wrong = MeterLayout {
fuel_setup: [0x30, 0x20, 0x24],
..layout
};
assert!(clone_meter_template(&wrong, &template, &meter).is_none());
}
#[test]
fn two_mods_can_list_items_one_after_the_other() {
let mut blob = Blob::new(&[0u8; 0x48]);
let reference = blob.append(&CHEST.0, 4);
let text = blob.append(b"Chest\0", 1);
let name = blob.append(&[0; 8], 4);
assert!(blob.set_array(name, text, 5));
assert!(blob.set_array(0, reference, 1));
assert!(blob.set_array(0x38, name, 1));
let once = list_item(&LAYOUT, blob.bytes(), NEW, "New").unwrap();
let twice = Blob::new(&list_item(&LAYOUT, &once, NAME, "Other").unwrap());
let (refs, count) = twice.array(0).unwrap();
assert_eq!(count, 3);
assert_eq!(&twice.bytes()[refs + 32..refs + 48], &NAME.0);
let (names, _) = twice.array(0x38).unwrap();
assert_eq!(string(&twice, names), b"Chest");
assert_eq!(string(&twice, names + 8), b"New");
assert_eq!(string(&twice, names + 16), b"Other");
}
}