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