Skip to main content

yog_inventory/
lib.rs

1//! yog-inventory — real Minecraft Container/Menu screens for Yog mods.
2//!
3//! Unlike `yog-ui` (a HUD-drawn overlay screen with no real item slots), this
4//! crate describes actual vanilla-style inventory screens: a block's own
5//! slots plus, optionally, the player's own inventory grid underneath —
6//! backed by a real Minecraft `BlockEntity`/`AbstractContainerMenu` on the
7//! Java side, with real drag-and-drop and network sync for free.
8//!
9//! See `DESIGN.md` for the full architecture and phased plan. This crate
10//! currently covers the data model (phase 2); the Java-side `BlockEntity`/
11//! `Menu`/`Screen` plumbing lands in later phases.
12
13use serde::Deserialize;
14
15/// Pixel position of a single slot in vanilla GUI coordinate space (the same
16/// space vanilla screens use: origin at the panel's top-left corner, one
17/// vanilla slot = 18×18px).
18#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
19pub struct SlotLayout {
20    pub x: f32,
21    pub y: f32,
22}
23
24/// Vanilla spacing between adjacent slots in the default grid.
25pub const SLOT_SIZE: f32 = 18.0;
26
27/// Default position of the player's main inventory (3×9) relative to the
28/// panel's top-left corner — matches vanilla furnace/chest-style screens.
29pub const DEFAULT_PLAYER_INV_OFFSET: (f32, f32) = (8.0, 84.0);
30/// Extra vertical gap between the player's main inventory and their hotbar,
31/// on top of the normal per-row `SLOT_SIZE` spacing (also a vanilla convention).
32pub const PLAYER_INV_TO_HOTBAR_GAP: f32 = 4.0;
33
34/// Describes one inventory-backed screen: how many slots a mod block has,
35/// where they sit, whether the player's own inventory is appended below,
36/// and which background texture to draw (vanilla default, or a mod's own).
37#[derive(Debug, Clone)]
38pub struct InventoryDef {
39    pub id: String,
40    pub slot_count: usize,
41    /// Per-slot pixel positions. Empty = auto-generate a default grid (see
42    /// [`InventoryDef::default_grid`]).
43    pub layout: Vec<SlotLayout>,
44    /// Appends the player's main inventory (3×9) + hotbar (9) below the
45    /// custom slots — no armor, no offhand. Native vanilla slot rendering.
46    pub include_player_inventory: bool,
47    pub player_inv_offset: (f32, f32),
48    /// `None` = default vanilla-style panel texture.
49    pub background_texture: Option<String>,
50    pub title: String,
51}
52
53impl InventoryDef {
54    pub fn new(id: impl Into<String>, slot_count: usize) -> Self {
55        Self {
56            id: id.into(),
57            slot_count,
58            layout: Vec::new(),
59            include_player_inventory: true,
60            player_inv_offset: DEFAULT_PLAYER_INV_OFFSET,
61            background_texture: None,
62            title: String::new(),
63        }
64    }
65
66    /// Explicit per-slot pixel positions, overriding the default grid.
67    pub fn layout(mut self, layout: Vec<SlotLayout>) -> Self {
68        self.layout = layout;
69        self
70    }
71
72    /// Remap the slot grid from a JSON array of `{"x":.., "y":..}` objects —
73    /// one entry per slot, in slot-index order. Lets users/mod-packs override
74    /// the layout without recompiling. Invalid or short JSON is ignored
75    /// (falls back to whatever layout was set before, or the default grid).
76    pub fn layout_from_json(mut self, json: &str) -> Self {
77        if let Ok(parsed) = serde_json::from_str::<Vec<SlotLayout>>(json) {
78            self.layout = parsed;
79        }
80        self
81    }
82
83    /// Whether the player's main inventory + hotbar (no armor/offhand) is
84    /// appended below the custom slots. Default: `true`.
85    pub fn include_player_inventory(mut self, v: bool) -> Self {
86        self.include_player_inventory = v;
87        self
88    }
89
90    pub fn player_inv_offset(mut self, x: f32, y: f32) -> Self {
91        self.player_inv_offset = (x, y);
92        self
93    }
94
95    /// Custom background texture (resource path); `None` keeps the default
96    /// vanilla-style panel.
97    pub fn background_texture(mut self, path: impl Into<String>) -> Self {
98        self.background_texture = Some(path.into());
99        self
100    }
101
102    pub fn title(mut self, title: impl Into<String>) -> Self {
103        self.title = title.into();
104        self
105    }
106
107    /// Resolve the effective slot layout: the explicit one if set, otherwise
108    /// a default vanilla-style grid (9 columns, wrapping downward, 18px
109    /// spacing, starting at (8, 18) — same convention as vanilla containers).
110    pub fn resolved_layout(&self) -> Vec<SlotLayout> {
111        if !self.layout.is_empty() {
112            return self.layout.clone();
113        }
114        Self::default_grid(self.slot_count)
115    }
116
117    /// Default vanilla-style grid: up to 9 columns, 18px spacing, starting at
118    /// (8, 18) — leaves room for a title above, matches vanilla containers.
119    pub fn default_grid(slot_count: usize) -> Vec<SlotLayout> {
120        const COLS: usize = 9;
121        (0..slot_count)
122            .map(|i| {
123                let col = (i % COLS) as f32;
124                let row = (i / COLS) as f32;
125                SlotLayout { x: 8.0 + col * SLOT_SIZE, y: 18.0 + row * SLOT_SIZE }
126            })
127            .collect()
128    }
129}
130
131/// Encode an [`InventoryDef`]'s slot layout as `"x:y,x:y,..."` — the
132/// wire format used when handing the layout to the Java host (mirrors how
133/// `BlockDef::connect_groups` is comma-joined for its ABI struct).
134pub fn encode_layout(layout: &[SlotLayout]) -> String {
135    layout.iter().map(|s| format!("{}:{}", s.x, s.y)).collect::<Vec<_>>().join(",")
136}
137
138/// Inverse of [`encode_layout`]. Malformed entries are skipped.
139pub fn decode_layout(s: &str) -> Vec<SlotLayout> {
140    if s.is_empty() { return Vec::new(); }
141    s.split(',').filter_map(|pair| {
142        let (x, y) = pair.split_once(':')?;
143        Some(SlotLayout { x: x.parse().ok()?, y: y.parse().ok()? })
144    }).collect()
145}