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// ── StartupGrant ─────────────────────────────────────────────────────────────
321
322/// Grant items/books to every player once when they first join.
323/// This is the Yog-side replacement for `grant_patchi_book.json`.
324#[derive(Debug, Clone)]
325pub struct StartupGrant {
326    pub id: String,
327    pub items: Vec<String>,
328    pub book: Option<String>,
329    pub command: Option<String>,
330}
331
332impl StartupGrant {
333    pub fn new(id: impl Into<String>) -> Self {
334        Self { id: id.into(), items: Vec::new(), book: None, command: None }
335    }
336
337    pub fn item(mut self, item_id: impl Into<String>) -> Self {
338        self.items.push(item_id.into());
339        self
340    }
341
342    pub fn book(mut self, book: impl Into<String>) -> Self {
343        self.book = Some(book.into());
344        self
345    }
346
347    pub fn command(mut self, cmd: impl Into<String>) -> Self {
348        self.command = Some(cmd.into());
349        self
350    }
351
352    pub fn to_json(&self) -> String {
353        let items: Vec<String> = self.items.iter().map(|i| format!("{{\"item\":\"{}\"}}", i)).collect();
354        let mut entries = String::new();
355        if !items.is_empty() {
356            entries.push_str(&format!("[{}]", items.join(",")));
357        }
358        if let Some(book) = &self.book {
359            if !entries.is_empty() {
360                entries.push(',');
361            }
362            let book_entry = format!(
363                "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":\"{{yog_book:\\\"{}\\\"}}\"}}]}}",
364                book
365            );
366            entries.push_str(&book_entry);
367        }
368        if let Some(cmd) = &self.command {
369            if !entries.is_empty() {
370                entries.push(',');
371            }
372            entries.push_str(&format!(
373                "{{\"type\":\"minecraft:command\",\"command\":\"{}\"}}",
374                cmd.replace('"', "\\\"")
375            ));
376        }
377        format!(
378            "{{\"type\":\"yog:startup_grant\",\"entries\":{},\"id\":\"{}\"}}",
379            if entries.is_empty() { "[]".to_string() } else { entries },
380            self.id
381        )
382    }
383}
384
385// ── Blocks ───────────────────────────────────────────────────────────────────
386#[derive(Debug, Clone)]
387pub struct AdvancementReward {
388    pub id: String,
389    /// The item to grant (default: special book item when book rewards).
390    pub item: String,
391    /// Optional NBT tag to apply.
392    pub nbt: Option<String>,
393    /// If set, the reward links to a yog-book.
394    pub book: Option<String>,
395}
396
397impl AdvancementReward {
398    pub fn new(id: impl Into<String>) -> Self {
399        Self { id: id.into(), item: "minecraft:written_book".into(), nbt: None, book: None }
400    }
401
402    pub fn item(mut self, item: impl Into<String>) -> Self {
403        self.item = item.into();
404        self
405    }
406
407    pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
408        self.nbt = Some(nbt.into());
409        self
410    }
411
412    pub fn book(mut self, book: impl Into<String>) -> Self {
413        let book_str: String = book.into();
414        self.book = Some(book_str.clone());
415        self.nbt = Some(format!("{{yog_book:\"{}\",title:\"Yog Book\",author:\"Yog\"}}", book_str));
416        self
417    }
418
419    pub fn to_json(&self) -> String {
420        let nbt_part = self.nbt.as_ref().map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{}}}", n));
421        let entries = if let Some(nbt_part) = nbt_part {
422            format!("[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]", self.item, nbt_part)
423        } else {
424            format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
425        };
426        format!(
427            "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{}}}]}}",
428            entries
429        )
430    }
431}
432
433// ── Blocks ───────────────────────────────────────────────────────────────────
434
435/// A custom block to register; it also gets a matching block-item.
436#[derive(Debug, Clone)]
437pub struct BlockDef {
438    pub id: String,
439    pub hardness: f32,
440    pub resistance: f32,
441    pub name: Option<String>,
442    /// Optional collision/outline box in pixel units (0–16): `[x1,y1,z1,x2,y2,z2]`.
443    /// `None` = full cube.
444    pub shape: Option<[f32; 6]>,
445    /// Light emitted by this block (0 = none, 15 = max, like a torch).
446    pub light_level: u8,
447    /// Sound group id: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
448    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
449    /// `None` = stone (Minecraft default).
450    pub sound: Option<String>,
451    /// If `true`, the correct tool (from the block's tags) is required for drops.
452    pub requires_tool: bool,
453    /// If `true`, entities pass through this block (like flowers or torches).
454    pub no_collision: bool,
455    /// Friction coefficient. `0.0` = default (0.6). Ice = 0.989.
456    pub slipperiness: f32,
457}
458
459impl BlockDef {
460    pub fn new(id: impl Into<String>) -> Self {
461        Self {
462            id: id.into(),
463            hardness: 1.5,
464            resistance: 6.0,
465            name: None,
466            shape: None,
467            light_level: 0,
468            sound: None,
469            requires_tool: false,
470            no_collision: false,
471            slipperiness: 0.0,
472        }
473    }
474
475    /// Mining hardness and blast resistance (defaults 1.5 / 6.0).
476    pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
477        self.hardness = hardness;
478        self.resistance = resistance;
479        self
480    }
481
482    /// Display name shown in-game.
483    pub fn name(mut self, name: impl Into<String>) -> Self {
484        self.name = Some(name.into());
485        self
486    }
487
488    /// Custom hitbox/outline in pixel units (0–16).
489    pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
490        self.shape = Some([x1, y1, z1, x2, y2, z2]);
491        self
492    }
493
494    /// Emitted light level (0–15).
495    pub fn light_level(mut self, level: u8) -> Self {
496        self.light_level = level.min(15);
497        self
498    }
499
500    /// Sound group: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
501    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
502    pub fn sound(mut self, group: impl Into<String>) -> Self {
503        self.sound = Some(group.into());
504        self
505    }
506
507    /// Correct tool required for loot drops (equivalent to `requiresTool()`).
508    pub fn requires_tool(mut self) -> Self {
509        self.requires_tool = true;
510        self
511    }
512
513    /// No physical collision — entities pass through (like flowers).
514    pub fn no_collision(mut self) -> Self {
515        self.no_collision = true;
516        self
517    }
518
519    /// Friction (default 0.6). Set to 0.989 for ice-like slipperiness.
520    pub fn slipperiness(mut self, value: f32) -> Self {
521        self.slipperiness = value;
522        self
523    }
524}