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 {
36            nutrition,
37            saturation,
38            can_always_eat: false,
39        }
40    }
41
42    pub fn can_always_eat(mut self) -> Self {
43        self.can_always_eat = true;
44        self
45    }
46}
47
48/// A custom item to register, identified by `namespace:path`.
49#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
50#[derive(Debug, Clone)]
51pub struct ItemDef {
52    pub id: String,
53    pub max_stack: u8,
54    pub name: Option<String>,
55    pub tooltip: Option<String>,
56    /// Durability. `0` = non-damageable. If set, `max_stack` is forced to 1.
57    pub max_damage: u32,
58    /// Immune to fire and lava damage (like netherite items).
59    pub fire_resistant: bool,
60    /// Furnace fuel burn time in ticks (`0` = not fuel; 200 = one coal equivalent).
61    pub fuel_ticks: u32,
62    /// Nutritional properties; `None` = not food.
63    pub food: Option<FoodDef>,
64}
65
66impl ItemDef {
67    pub fn new(id: impl Into<String>) -> Self {
68        Self {
69            id: id.into(),
70            max_stack: 64,
71            name: None,
72            tooltip: None,
73            max_damage: 0,
74            fire_resistant: false,
75            fuel_ticks: 0,
76            food: None,
77        }
78    }
79
80    /// Maximum stack size (default 64). Ignored when `max_damage` is set.
81    pub fn max_stack(mut self, n: u8) -> Self {
82        self.max_stack = n;
83        self
84    }
85
86    /// Display name shown in-game.
87    pub fn name(mut self, name: impl Into<String>) -> Self {
88        self.name = Some(name.into());
89        self
90    }
91
92    /// A tooltip line shown on hover.
93    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
94        self.tooltip = Some(tooltip.into());
95        self
96    }
97
98    /// Make this a damageable item (tool/weapon/armour). Forces stack size to 1.
99    pub fn max_damage(mut self, durability: u32) -> Self {
100        self.max_damage = durability;
101        self
102    }
103
104    /// Make this item fire-resistant (won't burn in fire or lava).
105    pub fn fire_resistant(mut self) -> Self {
106        self.fire_resistant = true;
107        self
108    }
109
110    /// Register as furnace fuel burning for `ticks` (200 ticks = 1 item smelted).
111    pub fn fuel(mut self, ticks: u32) -> Self {
112        self.fuel_ticks = ticks;
113        self
114    }
115
116    /// Make this item edible with the given nutritional properties.
117    pub fn food(mut self, food: FoodDef) -> Self {
118        self.food = Some(food);
119        self
120    }
121}
122
123// ── Recipes ──────────────────────────────────────────────────────────────────
124
125/// A shaped crafting recipe (3×3 grid with a pattern and key mapping).
126///
127/// ```
128/// # use yog_registry::ShapedRecipe;
129/// ShapedRecipe::new("yog:ruby_sword", "yog:ruby_shard", 1)
130///     .row("R  ").row("RS ").row(" S ")
131///     .key('R', "yog:ruby_shard").key('S', "minecraft:stick");
132/// ```
133#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
134#[derive(Debug, Clone)]
135pub struct ShapedRecipe {
136    pub id: String,
137    pub output: String,
138    pub count: u32,
139    rows: Vec<String>,
140    keys: Vec<(char, String)>,
141}
142
143impl ShapedRecipe {
144    pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
145        Self {
146            id: id.into(),
147            output: output.into(),
148            count,
149            rows: Vec::new(),
150            keys: Vec::new(),
151        }
152    }
153
154    pub fn row(mut self, pattern: impl Into<String>) -> Self {
155        self.rows.push(pattern.into());
156        self
157    }
158
159    pub fn key(mut self, symbol: char, item_id: impl Into<String>) -> Self {
160        self.keys.push((symbol, item_id.into()));
161        self
162    }
163
164    /// Generate the Minecraft 1.20 recipe JSON for this recipe.
165    pub fn to_json(&self) -> String {
166        let pattern: String = self
167            .rows
168            .iter()
169            .map(|r| format!("\"{}\"", r))
170            .collect::<Vec<_>>()
171            .join(",");
172        let keys: String = self
173            .keys
174            .iter()
175            .map(|(ch, item)| format!("\"{}\":{{\"item\":\"{}\"}}", ch, item))
176            .collect::<Vec<_>>()
177            .join(",");
178        format!(
179            "{{\"type\":\"minecraft:crafting_shaped\",\"pattern\":[{}],\"key\":{{{}}},\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
180            pattern, keys, self.output, self.count
181        )
182    }
183
184    /// Split `namespace:name` from the recipe id.
185    pub fn ns_name(&self) -> (&str, &str) {
186        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
187    }
188}
189
190/// A shapeless crafting recipe (unordered ingredients).
191#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
192#[derive(Debug, Clone)]
193pub struct ShapelessRecipe {
194    pub id: String,
195    pub output: String,
196    pub count: u32,
197    pub ingredients: Vec<String>,
198}
199
200impl ShapelessRecipe {
201    pub fn new(id: impl Into<String>, output: impl Into<String>, count: u32) -> Self {
202        Self {
203            id: id.into(),
204            output: output.into(),
205            count,
206            ingredients: Vec::new(),
207        }
208    }
209
210    pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
211        self.ingredients.push(item_id.into());
212        self
213    }
214
215    pub fn to_json(&self) -> String {
216        let ingr: String = self
217            .ingredients
218            .iter()
219            .map(|i| format!("{{\"item\":\"{}\"}}", i))
220            .collect::<Vec<_>>()
221            .join(",");
222        format!(
223            "{{\"type\":\"minecraft:crafting_shapeless\",\"ingredients\":[{}],\"result\":{{\"item\":\"{}\",\"count\":{}}}}}",
224            ingr, self.output, self.count
225        )
226    }
227
228    pub fn ns_name(&self) -> (&str, &str) {
229        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
230    }
231}
232
233/// A furnace smelting recipe.
234#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
235#[derive(Debug, Clone)]
236pub struct FurnaceRecipe {
237    pub id: String,
238    pub input: String,
239    pub output: String,
240    pub count: u32,
241    pub experience: f32,
242    pub cook_time: u32,
243}
244
245impl FurnaceRecipe {
246    pub fn new(
247        id: impl Into<String>,
248        input: impl Into<String>,
249        output: impl Into<String>,
250        count: u32,
251    ) -> Self {
252        Self {
253            id: id.into(),
254            input: input.into(),
255            output: output.into(),
256            count,
257            experience: 0.1,
258            cook_time: 200,
259        }
260    }
261
262    pub fn experience(mut self, xp: f32) -> Self {
263        self.experience = xp;
264        self
265    }
266
267    /// Cooking time in ticks (default 200 = 10 seconds).
268    pub fn cook_time(mut self, ticks: u32) -> Self {
269        self.cook_time = ticks;
270        self
271    }
272
273    pub fn to_json(&self) -> String {
274        format!(
275            "{{\"type\":\"minecraft:smelting\",\"ingredient\":{{\"item\":\"{}\"}},\"result\":\"{}\",\"experience\":{},\"cookingtime\":{}}}",
276            self.input, self.output, self.experience, self.cook_time
277        )
278    }
279
280    pub fn ns_name(&self) -> (&str, &str) {
281        self.id.split_once(':').unwrap_or(("minecraft", &self.id))
282    }
283}
284
285// ── BookRecipe (like Patchouli's shapeless_book_recipe) ──────────────────────
286
287/// A shapeless recipe that produces a book from `yog-book`.
288/// Replaces `patchouli:shapeless_book_recipe`.
289#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
290#[derive(Debug, Clone)]
291pub struct BookRecipe {
292    pub id: String,
293    pub book: String,
294    pub ingredients: Vec<String>,
295}
296
297impl BookRecipe {
298    pub fn new(id: impl Into<String>, book: impl Into<String>) -> Self {
299        Self {
300            id: id.into(),
301            book: book.into(),
302            ingredients: Vec::new(),
303        }
304    }
305
306    pub fn ingredient(mut self, item_id: impl Into<String>) -> Self {
307        self.ingredients.push(item_id.into());
308        self
309    }
310
311    pub fn to_json(&self) -> String {
312        let ingr: String = self
313            .ingredients
314            .iter()
315            .map(|i| format!("{{\"item\":\"{}\"}}", i))
316            .collect::<Vec<_>>()
317            .join(",");
318        format!(
319            "{{\"type\":\"yog:crafting_book\",\"ingredients\":[{}],\"book\":\"{}\"}}",
320            ingr, self.book
321        )
322    }
323
324    pub fn ns_name(&self) -> (&str, &str) {
325        self.id.split_once(':').unwrap_or(("yog", &self.id))
326    }
327}
328
329// ── ItemModifier ─────────────────────────────────────────────────────────────
330
331/// An item modifier applied during loot generation (like smelting, enchanted, etc.).
332#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
333#[derive(Debug, Clone)]
334pub struct ItemModifier {
335    pub id: String,
336    /// Modifier function identifier, e.g. "yog:set_count" or "hexcasting:amethyst_shard_reducer".
337    pub function: String,
338    /// Parameters as JSON object.
339    pub parameters: std::collections::HashMap<String, String>,
340}
341
342impl ItemModifier {
343    pub fn new(id: impl Into<String>, function: impl Into<String>) -> Self {
344        Self {
345            id: id.into(),
346            function: function.into(),
347            parameters: std::collections::HashMap::new(),
348        }
349    }
350
351    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
352        self.parameters.insert(key.into(), value.into());
353        self
354    }
355
356    pub fn to_json(&self) -> String {
357        let params: String = self
358            .parameters
359            .iter()
360            .map(|(k, v)| format!("\"{}\":{}", k, v))
361            .collect::<Vec<_>>()
362            .join(",");
363        format!(
364            "{{\"function\":\"{}\",{}}}",
365            self.function,
366            if params.is_empty() {
367                String::new()
368            } else {
369                params
370            }
371        )
372    }
373}
374
375// ── StartupGrant ─────────────────────────────────────────────────────────────
376
377/// Grant items/books to every player once when they first join.
378/// This is the Yog-side replacement for `grant_patchi_book.json`.
379#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
380#[derive(Debug, Clone)]
381pub struct StartupGrant {
382    pub id: String,
383    pub items: Vec<String>,
384    pub book: Option<String>,
385    pub command: Option<String>,
386}
387
388impl StartupGrant {
389    pub fn new(id: impl Into<String>) -> Self {
390        Self {
391            id: id.into(),
392            items: Vec::new(),
393            book: None,
394            command: None,
395        }
396    }
397
398    pub fn item(mut self, item_id: impl Into<String>) -> Self {
399        self.items.push(item_id.into());
400        self
401    }
402
403    pub fn book(mut self, book: impl Into<String>) -> Self {
404        self.book = Some(book.into());
405        self
406    }
407
408    pub fn command(mut self, cmd: impl Into<String>) -> Self {
409        self.command = Some(cmd.into());
410        self
411    }
412
413    pub fn to_json(&self) -> String {
414        let items: Vec<String> = self
415            .items
416            .iter()
417            .map(|i| format!("{{\"item\":\"{}\"}}", i))
418            .collect();
419        let mut entries = String::new();
420        if !items.is_empty() {
421            entries.push_str(&format!("[{}]", items.join(",")));
422        }
423        if let Some(book) = &self.book {
424            if !entries.is_empty() {
425                entries.push(',');
426            }
427            let book_entry = format!(
428                "{{\"type\":\"item\",\"name\":\"minecraft:written_book\",\"functions\":[{{\"function\":\"set_nbt\",\"tag\":\"{{yog_book:\\\"{}\\\"}}\"}}]}}",
429                book
430            );
431            entries.push_str(&book_entry);
432        }
433        if let Some(cmd) = &self.command {
434            if !entries.is_empty() {
435                entries.push(',');
436            }
437            entries.push_str(&format!(
438                "{{\"type\":\"minecraft:command\",\"command\":\"{}\"}}",
439                cmd.replace('"', "\\\"")
440            ));
441        }
442        format!(
443            "{{\"type\":\"yog:startup_grant\",\"entries\":{},\"id\":\"{}\"}}",
444            if entries.is_empty() {
445                "[]".to_string()
446            } else {
447                entries
448            },
449            self.id
450        )
451    }
452}
453
454// ── Blocks ───────────────────────────────────────────────────────────────────
455#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
456#[derive(Debug, Clone)]
457pub struct AdvancementReward {
458    pub id: String,
459    /// The item to grant (default: special book item when book rewards).
460    pub item: String,
461    /// Optional NBT tag to apply.
462    pub nbt: Option<String>,
463    /// If set, the reward links to a yog-book.
464    pub book: Option<String>,
465}
466
467impl AdvancementReward {
468    pub fn new(id: impl Into<String>) -> Self {
469        Self {
470            id: id.into(),
471            item: "minecraft:written_book".into(),
472            nbt: None,
473            book: None,
474        }
475    }
476
477    pub fn item(mut self, item: impl Into<String>) -> Self {
478        self.item = item.into();
479        self
480    }
481
482    pub fn nbt(mut self, nbt: impl Into<String>) -> Self {
483        self.nbt = Some(nbt.into());
484        self
485    }
486
487    pub fn book(mut self, book: impl Into<String>) -> Self {
488        let book_str: String = book.into();
489        self.book = Some(book_str.clone());
490        self.nbt = Some(format!(
491            "{{yog_book:\"{}\",title:\"Yog Book\",author:\"Yog\"}}",
492            book_str
493        ));
494        self
495    }
496
497    pub fn to_json(&self) -> String {
498        let nbt_part = self
499            .nbt
500            .as_ref()
501            .map(|n| format!("{{\"function\":\"set_nbt\",\"tag\":{}}}", n));
502        let entries = if let Some(nbt_part) = nbt_part {
503            format!(
504                "[{{\"type\":\"item\",\"name\":\"{}\",\"functions\":[{}]}}]",
505                self.item, nbt_part
506            )
507        } else {
508            format!("[{{\"type\":\"item\",\"name\":\"{}\"}}]", self.item)
509        };
510        format!(
511            "{{\"type\":\"advancement_reward\",\"pools\":[{{\"rolls\":1,\"entries\":{}}}]}}",
512            entries
513        )
514    }
515}
516
517// ── Blocks ───────────────────────────────────────────────────────────────────
518
519/// A custom block to register; it also gets a matching block-item.
520#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
521#[derive(Debug, Clone)]
522pub struct BlockDef {
523    pub id: String,
524    pub hardness: f32,
525    pub resistance: f32,
526    pub name: Option<String>,
527    /// Optional collision/outline box in pixel units (0–16): `[x1,y1,z1,x2,y2,z2]`.
528    /// `None` = full cube.
529    pub shape: Option<[f32; 6]>,
530    /// Light emitted by this block (0 = none, 15 = max, like a torch).
531    pub light_level: u8,
532    /// Sound group id: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
533    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
534    /// `None` = stone (Minecraft default).
535    pub sound: Option<String>,
536    /// If `true`, the correct tool (from the block's tags) is required for drops.
537    pub requires_tool: bool,
538    /// If `true`, entities pass through this block (like flowers or torches).
539    pub no_collision: bool,
540    /// Friction coefficient. `0.0` = default (0.6). Ice = 0.989.
541    pub slipperiness: f32,
542    /// If `true`, this block dynamically grows arms toward neighbors it's
543    /// compatible with (fence/pipe-style): the Java host tracks N/S/E/W/U/D
544    /// boolean blockstate properties, recomputed on placement and on
545    /// neighbor change, and grows the collision/outline shape from the
546    /// `shape` core box (or a default post) out to each connected side.
547    pub connects: bool,
548    /// Connection compatibility tags (configured in code via
549    /// `.connect_groups(&[...])` — see that method's docs for how this
550    /// drives which blocks link up). Independent of `connects`: a block can
551    /// carry tags purely as a connection *target* (e.g. an ALU accepting a
552    /// Digital Cable) without dynamically growing its own shape.
553    pub connect_groups: Vec<String>,
554    /// Id of a `YogInventoryDef` this block is backed by — `None` = plain
555    /// block (default). See `yog_inventory`'s DESIGN.md.
556    pub inventory_id: Option<String>,
557}
558
559impl BlockDef {
560    pub fn new(id: impl Into<String>) -> Self {
561        Self {
562            id: id.into(),
563            hardness: 1.5,
564            resistance: 6.0,
565            name: None,
566            shape: None,
567            light_level: 0,
568            sound: None,
569            requires_tool: false,
570            no_collision: false,
571            slipperiness: 0.0,
572            connects: false,
573            connect_groups: Vec::new(),
574            inventory_id: None,
575        }
576    }
577
578    /// Mining hardness and blast resistance (defaults 1.5 / 6.0).
579    pub fn strength(mut self, hardness: f32, resistance: f32) -> Self {
580        self.hardness = hardness;
581        self.resistance = resistance;
582        self
583    }
584
585    /// Display name shown in-game.
586    pub fn name(mut self, name: impl Into<String>) -> Self {
587        self.name = Some(name.into());
588        self
589    }
590
591    /// Custom hitbox/outline in pixel units (0–16).
592    pub fn shape(mut self, x1: f32, y1: f32, z1: f32, x2: f32, y2: f32, z2: f32) -> Self {
593        self.shape = Some([x1, y1, z1, x2, y2, z2]);
594        self
595    }
596
597    /// Emitted light level (0–15).
598    pub fn light_level(mut self, level: u8) -> Self {
599        self.light_level = level.min(15);
600        self
601    }
602
603    /// Sound group: `"stone"`, `"wood"`, `"grass"`, `"sand"`, `"snow"`,
604    /// `"gravel"`, `"metal"`, `"glass"`, `"wool"`, `"nether_brick"`.
605    pub fn sound(mut self, group: impl Into<String>) -> Self {
606        self.sound = Some(group.into());
607        self
608    }
609
610    /// Correct tool required for loot drops (equivalent to `requiresTool()`).
611    pub fn requires_tool(mut self) -> Self {
612        self.requires_tool = true;
613        self
614    }
615
616    /// No physical collision — entities pass through (like flowers).
617    pub fn no_collision(mut self) -> Self {
618        self.no_collision = true;
619        self
620    }
621
622    /// Friction (default 0.6). Set to 0.989 for ice-like slipperiness.
623    pub fn slipperiness(mut self, value: f32) -> Self {
624        self.slipperiness = value;
625        self
626    }
627
628    /// Dynamically grow arms toward compatible neighbors — fence/pipe-style.
629    /// "Compatible" means the neighbor also has a `connect_groups` tag in
630    /// common (set here, or via `connect_groups` alone on a static target
631    /// block). Call `.connect_groups(&[...])` too, or this connects to
632    /// nothing. See the `connects` field doc.
633    pub fn connects_to_neighbors(mut self) -> Self {
634        self.connects = true;
635        self
636    }
637
638    /// Connection compatibility tags — two blocks link up (for
639    /// `connects_to_neighbors` arm growth, and as valid connection targets
640    /// generally) when their tag sets share at least one entry. Example: for
641    /// Yog-VLSI, `analog_cable` and `redstone_port` both carry `"analog"`;
642    /// `digital_cable` carries `"digital"`; an ALU block carries both, so it
643    /// accepts either cable while a Redstone Port only accepts the analog one.
644    pub fn connect_groups(mut self, groups: &[&str]) -> Self {
645        self.connect_groups = groups.iter().map(|s| s.to_string()).collect();
646        self
647    }
648
649    /// Back this block with a real Container/Menu inventory screen, whose
650    /// shape/layout was registered separately via `Registry::register_inventory`
651    /// — see `yog_inventory::InventoryDef`. `id` is that def's id.
652    pub fn inventory(mut self, id: impl Into<String>) -> Self {
653        self.inventory_id = Some(id.into());
654        self
655    }
656}