Skip to main content

yog_registry/
lib.rs

1//! Content registration — declare custom items and blocks from Rust.
2//!
3//! Definitions are collected at registration time and handed to the host, which
4//! registers real `Item`/`Block` objects before the game's registries freeze,
5//! puts them in a "Yog" creative tab, and applies their properties.
6//! (Textures, models and recipes are assets/data shipped in the `.yog` and
7//! served to the game.)
8//!
9//! ```
10//! # use yog_registry::{ItemDef, BlockDef, FoodDef};
11//! ItemDef::new("mymod:ruby").name("Ruby").tooltip("A shiny gem.").max_stack(16);
12//! ItemDef::new("mymod:pie").food(FoodDef::new(4, 0.3));
13//! BlockDef::new("mymod:lamp").light_level(15).sound("stone");
14//! ```
15
16// ── Items ────────────────────────────────────────────────────────────────────
17
18/// Nutritional properties for a food item.
19#[derive(Debug, Clone)]
20pub struct FoodDef {
21    /// Hunger points restored (1 unit = half a drumstick).
22    pub nutrition: u32,
23    /// Saturation modifier applied after eating.
24    pub saturation: f32,
25    /// If `true`, edible even when the hunger bar is full.
26    pub can_always_eat: bool,
27}
28
29impl FoodDef {
30    pub fn new(nutrition: u32, saturation: f32) -> Self {
31        Self { nutrition, saturation, can_always_eat: false }
32    }
33
34    pub fn can_always_eat(mut self) -> Self {
35        self.can_always_eat = true;
36        self
37    }
38}
39
40/// A custom item to register, identified by `namespace:path`.
41#[derive(Debug, Clone)]
42pub struct ItemDef {
43    pub id: String,
44    pub max_stack: u8,
45    pub name: Option<String>,
46    pub tooltip: Option<String>,
47    /// Durability. `0` = non-damageable. If set, `max_stack` is forced to 1.
48    pub max_damage: u32,
49    /// Immune to fire and lava damage (like netherite items).
50    pub fire_resistant: bool,
51    /// Furnace fuel burn time in ticks (`0` = not fuel; 200 = one coal equivalent).
52    pub fuel_ticks: u32,
53    /// Nutritional properties; `None` = not food.
54    pub food: Option<FoodDef>,
55}
56
57impl ItemDef {
58    pub fn new(id: impl Into<String>) -> Self {
59        Self {
60            id: id.into(),
61            max_stack: 64,
62            name: None,
63            tooltip: None,
64            max_damage: 0,
65            fire_resistant: false,
66            fuel_ticks: 0,
67            food: None,
68        }
69    }
70
71    /// Maximum stack size (default 64). Ignored when `max_damage` is set.
72    pub fn max_stack(mut self, n: u8) -> Self {
73        self.max_stack = n;
74        self
75    }
76
77    /// Display name shown in-game.
78    pub fn name(mut self, name: impl Into<String>) -> Self {
79        self.name = Some(name.into());
80        self
81    }
82
83    /// A tooltip line shown on hover.
84    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
85        self.tooltip = Some(tooltip.into());
86        self
87    }
88
89    /// Make this a damageable item (tool/weapon/armour). Forces stack size to 1.
90    pub fn max_damage(mut self, durability: u32) -> Self {
91        self.max_damage = durability;
92        self
93    }
94
95    /// Make this item fire-resistant (won't burn in fire or lava).
96    pub fn fire_resistant(mut self) -> Self {
97        self.fire_resistant = true;
98        self
99    }
100
101    /// Register as furnace fuel burning for `ticks` (200 ticks = 1 item smelted).
102    pub fn fuel(mut self, ticks: u32) -> Self {
103        self.fuel_ticks = ticks;
104        self
105    }
106
107    /// Make this item edible with the given nutritional properties.
108    pub fn food(mut self, food: FoodDef) -> Self {
109        self.food = Some(food);
110        self
111    }
112}
113
114// ── Recipes ──────────────────────────────────────────────────────────────────
115
116/// A shaped crafting recipe (3×3 grid with a pattern and key mapping).
117///
118/// ```
119/// # use yog_registry::ShapedRecipe;
120/// ShapedRecipe::new("yog:ruby_sword", "yog:ruby_shard", 1)
121///     .row("R  ").row("RS ").row(" S ")
122///     .key('R', "yog:ruby_shard").key('S', "minecraft:stick");
123/// ```
124#[derive(Debug, Clone)]
125pub struct ShapedRecipe {
126    pub id:     String,
127    pub output: String,
128    pub count:  u32,
129    rows:       Vec<String>,
130    keys:       Vec<(char, String)>,
131}
132
133impl ShapedRecipe {
134    pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
135        Self { id: id.into(), output: output.into(), count, rows: Vec::new(), keys: Vec::new() }
136    }
137
138    pub fn row(mut self, pattern: impl Into<String>) -> Self {
139        self.rows.push(pattern.into());
140        self
141    }
142
143    pub fn key(mut self, symbol: char, item_id: impl Into<String>) -> Self {
144        self.keys.push((symbol, item_id.into()));
145        self
146    }
147
148    /// Generate the Minecraft 1.20 recipe JSON for this recipe.
149    pub fn to_json(&self) -> String {
150        let pattern: String = self.rows.iter()
151            .map(|r| format!("\"{}\"", r))
152            .collect::<Vec<_>>()
153            .join(",");
154        let keys: String = self.keys.iter()
155            .map(|(ch, item)| format!("\"{}\":{{\"item\":\"{}\"}}", ch, item))
156            .collect::<Vec<_>>()
157            .join(",");
158        format!(
159            "{{\"type\":\"minecraft:crafting_shaped\",\"pattern\":[{}],\"key\":{{{}}},\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
160            pattern, keys, self.output, self.count
161        )
162    }
163
164    /// Split `namespace:name` from the recipe id.
165    pub fn ns_name(&self) -> (&str, &str) {
166        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
167    }
168}
169
170/// A shapeless crafting recipe (unordered ingredients).
171#[derive(Debug, Clone)]
172pub struct ShapelessRecipe {
173    pub id:          String,
174    pub output:      String,
175    pub count:       u32,
176    pub ingredients: Vec<String>,
177}
178
179impl ShapelessRecipe {
180    pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
181        Self { id: id.into(), output: output.into(), count, ingredients: Vec::new() }
182    }
183
184    pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
185        self.ingredients.push(item_id.into());
186        self
187    }
188
189    pub fn to_json(&self) -> String {
190        let ingr: String = self.ingredients.iter()
191            .map(|i| format!("{{\"item\":\"{}\"}}", i))
192            .collect::<Vec<_>>()
193            .join(",");
194        format!(
195            "{{\"type\":\"minecraft:crafting_shapeless\",\"ingredients\":[{}],\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
196            ingr, self.output, self.count
197        )
198    }
199
200    pub fn ns_name(&self) -> (&str, &str) {
201        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
202    }
203}
204
205/// A furnace smelting recipe.
206#[derive(Debug, Clone)]
207pub struct FurnaceRecipe {
208    pub id:         String,
209    pub input:      String,
210    pub output:     String,
211    pub count:      u32,
212    pub experience: f32,
213    pub cook_time:  u32,
214}
215
216impl FurnaceRecipe {
217    pub fn new(
218        id: impl Into<String>,
219        input: impl Into<String>,
220        output: impl Into<String>,
221        count: u32,
222    ) -> Self {
223        Self { id: id.into(), input: input.into(), output: output.into(), count, experience: 0.1, cook_time: 200 }
224    }
225
226    pub fn experience(mut self, xp: f32) -> Self {
227        self.experience = xp;
228        self
229    }
230
231    /// Cooking time in ticks (default 200 = 10 seconds).
232    pub fn cook_time(mut self, ticks: u32) -> Self {
233        self.cook_time = ticks;
234        self
235    }
236
237    pub fn to_json(&self) -> String {
238        format!(
239            "{{\"type\":\"minecraft:smelting\",\"ingredient\":{{\"item\":\"{}\"}},\"result\":\"{}\",\"experience\":{},\"cookingtime\":{}}}",
240            self.input, self.output, self.experience, self.cook_time
241        )
242    }
243
244    pub fn ns_name(&self) -> (&str, &str) {
245        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
246    }
247}
248
249// ── BookRecipe (like Patchouli's shapeless_book_recipe) ──────────────────────
250
251/// A shapeless recipe that produces a book from `yog-book`.
252/// Replaces `patchouli:shapeless_book_recipe`.
253#[derive(Debug, Clone)]
254pub struct BookRecipe {
255    pub id: String,
256    pub book: String,
257    pub ingredients: Vec<String>,
258}
259
260impl BookRecipe {
261    pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
262        Self { id: id.into(), book: book.into(), ingredients: Vec::new() }
263    }
264
265    pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
266        self.ingredients.push(item_id.into());
267        self
268    }
269
270    pub fn to_json(&self) -> String {
271        let ingr: String = self.ingredients.iter()
272            .map(|i| format!("{{\"item\":\"{}\"}}", i))
273            .collect::<Vec<_>>()
274            .join(",");
275        format!(
276            "{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
277            ingr, self.book
278        )
279    }
280
281    pub fn ns_name(&self) -> (&str, &str) {
282        self.id.split_once(':').unwrap_or(("yog", &self.id))
283    }
284}
285
286// ── ItemModifier ─────────────────────────────────────────────────────────────
287
288/// An item modifier applied during loot generation (like smelting, enchanted, etc.).
289#[derive(Debug, Clone)]
290pub struct ItemModifier {
291    pub id: String,
292    /// Modifier function identifier, e.g. "yog:set_count" or "hexcasting:amethyst_shard_reducer".
293    pub function: String,
294    /// Parameters as JSON object.
295    pub parameters: std::collections::HashMap<String, String>,
296}
297
298impl ItemModifier {
299    pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
300        Self { id: id.into(), function: function.into(), parameters: std::collections::HashMap::new() }
301    }
302
303    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
304        self.parameters.insert(key.into(), value.into());
305        self
306    }
307
308    pub fn to_json(&self) -> String {
309        let params: String = self.parameters.iter()
310            .map(|(k, v)| format!("\"{}\":{}", k, v))
311            .collect::<Vec<_>>()
312            .join(",");
313        format!(
314            "{{\"function\":\"{}\",{}}}",
315            self.function, if params.is_empty() { String::new() } else { params }
316        )
317    }
318}
319
320// ── AdvancementReward ────────────────────────────────────────────────────────
321
322/// A loot table entry used as an advancement reward (grants items when an
323/// advancement is completed). Replaces `patchouli:guide_book` loot.
324#[derive(Debug, Clone)]
325pub struct AdvancementReward {
326    pub id: String,
327    /// The item to grant (default: special book item when book rewards).
328    pub item: String,
329    /// Optional NBT tag to apply.
330    pub nbt: Option<String>,
331    /// If set, the reward links to a yog-book.
332    pub book: Option<String>,
333}
334
335impl AdvancementReward {
336    pub fn new(id: impl Into<String>) -> Self {
337        Self { id: id.into(), item: "minecraft:written_book".into(), nbt: None, book: None }
338    }
339
340    pub fn item(mut self, item: impl Into<String>) -> Self {
341        self.item = item.into();
342        self
343    }
344
345    pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
346        self.nbt = Some(nbt.into());
347        self
348    }
349
350    pub fn book(mut self, book: impl Into<String>) -> Self {
351        let book_str: String = book.into();
352        self.book = Some(book_str.clone());
353        self.nbt = Some(format!("{{yog_book:\"{}\",title:\"Yog Book\",author:\"Yog\"}}", book_str));
354        self
355    }
356
357    pub fn to_json(&self) -> String {
358        let nbt_part = self.nbt.as_ref().map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{}}}", n));
359        let entries = if let Some(nbt_part) = nbt_part {
360            format!("[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]", self.item, nbt_part)
361        } else {
362            format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
363        };
364        format!(
365            "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{}}}]}}",
366            entries
367        )
368    }
369}
370
371// ── Blocks ───────────────────────────────────────────────────────────────────
372
373/// A custom block to register; it also gets a matching block-item.
374#[derive(Debug, Clone)]
375pub struct BlockDef {
376    pub id: String,
377    pub hardness: f32,
378    pub resistance: f32,
379    pub name: Option<String>,
380    /// Optional collision/outline box in pixel units (0–16): `[x1,y1,z1,x2,y2,z2]`.
381    /// `None` = full cube.
382    pub shape: Option<[f32; 6]>,
383    /// Light emitted by this block (0 = none, 15 = max, like a torch).
384    pub light_level: u8,
385    /// Sound group id: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
386    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
387    /// `None` = stone (Minecraft default).
388    pub sound: Option<String>,
389    /// If `true`, the correct tool (from the block's tags) is required for drops.
390    pub requires_tool: bool,
391    /// If `true`, entities pass through this block (like flowers or torches).
392    pub no_collision: bool,
393    /// Friction coefficient. `0.0` = default (0.6). Ice = 0.989.
394    pub slipperiness: f32,
395}
396
397impl BlockDef {
398    pub fn new(id: impl Into<String>) -> Self {
399        Self {
400            id: id.into(),
401            hardness: 1.5,
402            resistance: 6.0,
403            name: None,
404            shape: None,
405            light_level: 0,
406            sound: None,
407            requires_tool: false,
408            no_collision: false,
409            slipperiness: 0.0,
410        }
411    }
412
413    /// Mining hardness and blast resistance (defaults 1.5 / 6.0).
414    pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
415        self.hardness = hardness;
416        self.resistance = resistance;
417        self
418    }
419
420    /// Display name shown in-game.
421    pub fn name(mut self, name: impl Into<String>) -> Self {
422        self.name = Some(name.into());
423        self
424    }
425
426    /// Custom hitbox/outline in pixel units (0–16).
427    pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
428        self.shape = Some([x1, y1, z1, x2, y2, z2]);
429        self
430    }
431
432    /// Emitted light level (0–15).
433    pub fn light_level(mut self, level: u8) -> Self {
434        self.light_level = level.min(15);
435        self
436    }
437
438    /// Sound group: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
439    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
440    pub fn sound(mut self, group: impl Into<String>) -> Self {
441        self.sound = Some(group.into());
442        self
443    }
444
445    /// Correct tool required for loot drops (equivalent to `requiresTool()`).
446    pub fn requires_tool(mut self) -> Self {
447        self.requires_tool = true;
448        self
449    }
450
451    /// No physical collision — entities pass through (like flowers).
452    pub fn no_collision(mut self) -> Self {
453        self.no_collision = true;
454        self
455    }
456
457    /// Friction (default 0.6). Set to 0.989 for ice-like slipperiness.
458    pub fn slipperiness(mut self, value: f32) -> Self {
459        self.slipperiness = value;
460        self
461    }
462}