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//!
13//! What the open screen's slots hold right now is the bus's
14//! `client::ScreenSlots` service (`yog_api::service::call`).
15
16/// Pixel position of a single slot in vanilla GUI coordinate space (the same
17/// space vanilla screens use: origin at the panel's top-left corner, one
18/// vanilla slot = 18×18px).
19#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
20pub struct SlotLayout {
21    pub x: f32,
22    pub y: f32,
23    /// What the slot takes: item ids (`minecraft:redstone`) and item tags
24    /// (`#c:dusts`). Empty = anything.
25    #[data(default)]
26    pub accepts: Vec<String>,
27}
28
29impl SlotLayout {
30    /// A slot at `(x, y)` taking anything.
31    pub fn at(x: f32, y: f32) -> Self {
32        Self {
33            x,
34            y,
35            accepts: Vec::new(),
36        }
37    }
38}
39
40/// Vanilla spacing between adjacent slots in the default grid.
41pub const SLOT_SIZE: f32 = 18.0;
42
43/// Default position of the player's main inventory (3×9) relative to the
44/// panel's top-left corner — matches vanilla furnace/chest-style screens.
45pub const DEFAULT_PLAYER_INV_OFFSET: (f32, f32) = (8.0, 84.0);
46/// Extra vertical gap between the player's main inventory and their hotbar,
47/// on top of the normal per-row `SLOT_SIZE` spacing (also a vanilla convention).
48pub const PLAYER_INV_TO_HOTBAR_GAP: f32 = 4.0;
49/// Columns of [`InventoryDef::default_grid`] (a vanilla container row).
50const DEFAULT_GRID_COLS: usize = 9;
51/// Top-left slot of [`InventoryDef::default_grid`] — leaves room for the
52/// title above, like vanilla containers.
53const DEFAULT_GRID_ORIGIN: (f32, f32) = (8.0, 18.0);
54
55/// Describes one inventory-backed screen: how many slots a mod block has,
56/// where they sit, whether the player's own inventory is appended below,
57/// and which background texture to draw (vanilla default, or a mod's own).
58#[derive(Debug, Clone, yog_data::Schema)]
59pub struct InventoryDef {
60    pub id: String,
61    pub slot_count: usize,
62    /// Per-slot pixel positions. Empty = auto-generate a default grid (see
63    /// [`InventoryDef::default_grid`]).
64    pub layout: Vec<SlotLayout>,
65    /// Appends the player's main inventory (3×9) + hotbar (9) below the
66    /// custom slots — no armor, no offhand. Native vanilla slot rendering.
67    pub include_player_inventory: bool,
68    pub player_inv_offset: (f32, f32),
69    /// `None` = default vanilla-style panel texture.
70    pub background_texture: Option<String>,
71    pub title: String,
72    /// Screen size to center the container on — `None` uses the vanilla
73    /// default (176×166). Set this when a custom `on_ui_render` overlay
74    /// draws decoration at a different size, so the real vanilla `Slot`s
75    /// (positioned by `layout`/`player_inv_offset` relative to the
76    /// container's top-left corner) land under that decoration instead of
77    /// the two disagreeing about where the top-left corner even is.
78    pub background_size: Option<(f32, f32)>,
79    /// A machine's part of it (`.yui` `<RecipeArea kind="…">`): its slots
80    /// make recipes of a kind, like a crafting table.
81    pub machine: Option<Machine>,
82}
83
84/// A machine in an inventory: it makes recipes of `kind` from what's in
85/// its `places` — each a slot of the inventory holding one ingredient of
86/// one of the kind's slots — into its `result` slot.
87#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
88pub struct Machine {
89    pub kind: String,
90    pub places: Vec<MachinePlace>,
91    pub result: usize,
92}
93
94/// One ingredient's place: the `index`-th of the kind's slot `slot`, in the
95/// inventory's slot `inv`.
96#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
97pub struct MachinePlace {
98    pub slot: String,
99    pub index: usize,
100    pub inv: usize,
101}
102
103impl InventoryDef {
104    pub fn new(id: impl Into<String>, slot_count: usize) -> Self {
105        Self {
106            id: id.into(),
107            slot_count,
108            layout: Vec::new(),
109            include_player_inventory: true,
110            player_inv_offset: DEFAULT_PLAYER_INV_OFFSET,
111            background_texture: None,
112            title: String::new(),
113            background_size: None,
114            machine: None,
115        }
116    }
117
118    /// Makes part of it a machine (see [`Machine`]).
119    pub fn machine(mut self, machine: Machine) -> Self {
120        self.machine = Some(machine);
121        self
122    }
123
124    /// Explicit per-slot pixel positions, overriding the default grid.
125    pub fn layout(mut self, layout: Vec<SlotLayout>) -> Self {
126        self.layout = layout;
127        self
128    }
129
130    /// Remap the slot grid from Yog Data text (JSON too): a list of
131    /// `{x, y}` — one entry per slot, in slot-index order. Lets users/mod-packs
132    /// override the layout without recompiling. Text that doesn't read is
133    /// ignored (falls back to whatever layout was set before, or the default grid).
134    pub fn layout_from_text(mut self, text: &str) -> Self {
135        let parsed = yog_data::text::parse_value(text).ok();
136        if let Some(layout) = parsed.and_then(|v| yog_data::read_value(&v).ok()) {
137            self.layout = layout;
138        }
139        self
140    }
141
142    /// Whether the player's main inventory + hotbar (no armor/offhand) is
143    /// appended below the custom slots. Default: `true`.
144    pub fn include_player_inventory(mut self, v: bool) -> Self {
145        self.include_player_inventory = v;
146        self
147    }
148
149    pub fn player_inv_offset(mut self, x: f32, y: f32) -> Self {
150        self.player_inv_offset = (x, y);
151        self
152    }
153
154    /// Custom background texture (resource path); `None` keeps the default
155    /// vanilla-style panel.
156    pub fn background_texture(mut self, path: impl Into<String>) -> Self {
157        self.background_texture = Some(path.into());
158        self
159    }
160
161    /// Size (in GUI pixels) a custom `on_ui_render` overlay draws its
162    /// decoration at — used to center the real vanilla container on the
163    /// same footprint so its `Slot`s land under that decoration. Leave
164    /// unset to keep the vanilla default 176×166 centering.
165    pub fn background_size(mut self, w: f32, h: f32) -> Self {
166        self.background_size = Some((w, h));
167        self
168    }
169
170    pub fn title(mut self, title: impl Into<String>) -> Self {
171        self.title = title.into();
172        self
173    }
174
175    /// Resolve the effective slot layout: the explicit one if set, otherwise
176    /// [`InventoryDef::default_grid`].
177    pub fn resolved_layout(&self) -> Vec<SlotLayout> {
178        if !self.layout.is_empty() {
179            return self.layout.clone();
180        }
181        Self::default_grid(self.slot_count)
182    }
183
184    /// Default vanilla-style grid: up to 9 columns, 18px spacing, starting at
185    /// (8, 18) — leaves room for a title above, matches vanilla containers.
186    pub fn default_grid(slot_count: usize) -> Vec<SlotLayout> {
187        let (x0, y0) = DEFAULT_GRID_ORIGIN;
188        (0..slot_count)
189            .map(|i| {
190                let col = (i % DEFAULT_GRID_COLS) as f32;
191                let row = (i / DEFAULT_GRID_COLS) as f32;
192                SlotLayout::at(x0 + col * SLOT_SIZE, y0 + row * SLOT_SIZE)
193            })
194            .collect()
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn default_grid_wraps_rows() {
204        let g = InventoryDef::new("t", DEFAULT_GRID_COLS + 1).resolved_layout();
205        assert_eq!(g[0], SlotLayout::at(8.0, 18.0));
206        assert_eq!(g[DEFAULT_GRID_COLS], SlotLayout::at(8.0, 36.0));
207    }
208
209    #[test]
210    fn layout_reads_from_text() {
211        let def = InventoryDef::new("t", 2)
212            .layout_from_text(r##"[{"x": 1.5, "y": 2}, {x: 30, y: 4, accepts: ["#c:dusts"]}]"##);
213        assert_eq!(def.layout[0], SlotLayout::at(1.5, 2.0));
214        assert_eq!(def.layout[1].accepts, ["#c:dusts"]);
215        let kept = def.clone().layout_from_text("not a layout");
216        assert_eq!(kept.layout, def.layout);
217    }
218}