use std::sync::atomic::{AtomicPtr, Ordering};
use serde::Deserialize;
use yog_abi::YogStr;
#[derive(Debug, Clone, PartialEq, Deserialize, yog_data::Schema)]
pub struct SlotLayout {
pub x: f32,
pub y: f32,
#[serde(default)]
#[data(default)]
pub accepts: Vec<String>,
}
impl SlotLayout {
pub fn at(x: f32, y: f32) -> Self {
Self { x, y, accepts: Vec::new() }
}
}
pub const SLOT_SIZE: f32 = 18.0;
pub const DEFAULT_PLAYER_INV_OFFSET: (f32, f32) = (8.0, 84.0);
pub const PLAYER_INV_TO_HOTBAR_GAP: f32 = 4.0;
const DEFAULT_GRID_COLS: usize = 9;
const DEFAULT_GRID_ORIGIN: (f32, f32) = (8.0, 18.0);
#[derive(Debug, Clone, yog_data::Schema)]
pub struct InventoryDef {
pub id: String,
pub slot_count: usize,
pub layout: Vec<SlotLayout>,
pub include_player_inventory: bool,
pub player_inv_offset: (f32, f32),
pub background_texture: Option<String>,
pub title: String,
pub background_size: Option<(f32, f32)>,
pub machine: Option<Machine>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, yog_data::Schema)]
pub struct Machine {
pub kind: String,
pub places: Vec<MachinePlace>,
pub result: usize,
}
#[derive(Debug, Clone, PartialEq, Deserialize, yog_data::Schema)]
pub struct MachinePlace {
pub slot: String,
pub index: usize,
pub inv: usize,
}
impl InventoryDef {
pub fn new(id: impl Into<String>, slot_count: usize) -> Self {
Self {
id: id.into(),
slot_count,
layout: Vec::new(),
include_player_inventory: true,
player_inv_offset: DEFAULT_PLAYER_INV_OFFSET,
background_texture: None,
title: String::new(),
background_size: None,
machine: None,
}
}
pub fn machine(mut self, machine: Machine) -> Self {
self.machine = Some(machine);
self
}
pub fn layout(mut self, layout: Vec<SlotLayout>) -> Self {
self.layout = layout;
self
}
pub fn layout_from_json(mut self, json: &str) -> Self {
if let Ok(parsed) = serde_json::from_str::<Vec<SlotLayout>>(json) {
self.layout = parsed;
}
self
}
pub fn include_player_inventory(mut self, v: bool) -> Self {
self.include_player_inventory = v;
self
}
pub fn player_inv_offset(mut self, x: f32, y: f32) -> Self {
self.player_inv_offset = (x, y);
self
}
pub fn background_texture(mut self, path: impl Into<String>) -> Self {
self.background_texture = Some(path.into());
self
}
pub fn background_size(mut self, w: f32, h: f32) -> Self {
self.background_size = Some((w, h));
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn resolved_layout(&self) -> Vec<SlotLayout> {
if !self.layout.is_empty() {
return self.layout.clone();
}
Self::default_grid(self.slot_count)
}
pub fn default_grid(slot_count: usize) -> Vec<SlotLayout> {
let (x0, y0) = DEFAULT_GRID_ORIGIN;
(0..slot_count)
.map(|i| {
let col = (i % DEFAULT_GRID_COLS) as f32;
let row = (i / DEFAULT_GRID_COLS) as f32;
SlotLayout::at(x0 + col * SLOT_SIZE, y0 + row * SLOT_SIZE)
})
.collect()
}
}
pub fn encode_layout(layout: &[SlotLayout]) -> String {
layout
.iter()
.map(|s| match s.accepts.is_empty() {
true => format!("{}:{}", s.x, s.y),
false => format!("{}:{}:{}", s.x, s.y, s.accepts.join("|")),
})
.collect::<Vec<_>>()
.join(",")
}
pub fn decode_layout(s: &str) -> Vec<SlotLayout> {
if s.is_empty() {
return Vec::new();
}
s.split(',')
.filter_map(|entry| {
let mut parts = entry.splitn(3, ':');
let (x, y) = (parts.next()?, parts.next()?);
let accepts = parts
.next()
.map(|a| a.split('|').filter(|s| !s.is_empty()).map(str::to_owned).collect())
.unwrap_or_default();
Some(SlotLayout {
x: x.parse().ok()?,
y: y.parse().ok()?,
accepts,
})
})
.collect()
}
pub const MAX_SNAPSHOT_SLOTS: usize = 64;
#[repr(C)]
#[derive(Copy, Clone)]
pub struct YogInvSlotRaw {
pub item_id: YogStr,
pub count: u32,
pub x: i32,
pub y: i32,
}
impl YogInvSlotRaw {
pub const EMPTY: Self = Self {
item_id: YogStr::EMPTY,
count: 0,
x: 0,
y: 0,
};
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct YogInvSnapshotRaw {
pub slot_count: u32,
pub slots: [YogInvSlotRaw; MAX_SNAPSHOT_SLOTS],
}
impl YogInvSnapshotRaw {
pub const EMPTY: Self = Self {
slot_count: 0,
slots: [YogInvSlotRaw::EMPTY; MAX_SNAPSHOT_SLOTS],
};
}
static SNAPSHOT: AtomicPtr<YogInvSnapshotRaw> = AtomicPtr::new(std::ptr::null_mut());
#[doc(hidden)]
#[unsafe(no_mangle)]
pub extern "C" fn yog_inventory_set_snapshot(snapshot: *const YogInvSnapshotRaw) {
SNAPSHOT.store(snapshot as *mut _, Ordering::Release);
}
#[derive(Debug, Clone, Default)]
pub struct SlotView {
pub item_id: String,
pub count: u32,
pub x: i32,
pub y: i32,
}
fn snapshot_slots() -> &'static [YogInvSlotRaw] {
let ptr = SNAPSHOT.load(Ordering::Acquire);
match unsafe { ptr.as_ref() } {
Some(snap) => &snap.slots[..(snap.slot_count as usize).min(MAX_SNAPSHOT_SLOTS)],
None => &[],
}
}
pub fn current_slot_count() -> usize {
snapshot_slots().len()
}
pub fn current_slot(index: usize) -> Option<SlotView> {
let raw = snapshot_slots().get(index)?;
let item_id = if raw.item_id.is_empty() {
String::new()
} else {
unsafe { raw.item_id.as_str() }.to_owned()
};
Some(SlotView {
item_id,
count: raw.count,
x: raw.x,
y: raw.y,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_grid_wraps_rows() {
let g = InventoryDef::new("t", DEFAULT_GRID_COLS + 1).resolved_layout();
assert_eq!(g[0], SlotLayout::at(8.0, 18.0));
assert_eq!(g[DEFAULT_GRID_COLS], SlotLayout::at(8.0, 36.0));
}
#[test]
fn layout_round_trips() {
let l = vec![
SlotLayout::at(1.5, 2.0),
SlotLayout {
accepts: vec!["minecraft:redstone".into(), "#c:dusts".into()],
..SlotLayout::at(30.0, 4.0)
},
];
assert_eq!(encode_layout(&l), "1.5:2,30:4:minecraft:redstone|#c:dusts");
assert_eq!(decode_layout(&encode_layout(&l)), l);
assert!(decode_layout("").is_empty());
assert_eq!(
decode_layout("1:2,bad,3"),
vec![SlotLayout::at(1.0, 2.0)]
);
}
#[test]
fn no_snapshot_means_no_slots() {
assert_eq!(current_slot_count(), 0);
assert!(current_slot(0).is_none());
}
}