Skip to main content

yog_api/
registry.rs

1//! Registration hub and mod entry-point trait.
2//!
3//! [`Registry`] wraps the [`YogApi`] C table passed to `yog_mod_register` and
4//! provides an ergonomic Rust API over it.  All closures registered here are
5//! boxed on the heap and their raw pointers handed to the runtime; the runtime
6//! then drives them via C function-pointer calls.  Closures are intentionally
7//! leaked (never freed) — they live for the entire process lifetime, which is
8//! correct for a game server.
9
10use yog_abi::YogApi;
11use yog_command::CommandContext;
12use yog_dimensions::{GeneratedChunk, YogDimensionDef};
13use yog_event::client::net::Packet as ClientPacketEvent;
14use yog_event::server::net::Packet as PacketEvent;
15use yog_formats::book::Book;
16use yog_gfx::GfxContext;
17use yog_network::Packet;
18use yog_registry::{
19    ArmorMaterial, BlockDef, ItemDef, Recipe, SoundGroup, StartupGrant, ToolMaterial,
20};
21
22// ── Trampoline helpers ────────────────────────────────────────────────────────
23//
24// A panic must never unwind out of a trampoline: the runtime calls them
25// through `extern "C"`, so it would abort the whole game. `guard` stops it
26// at the boundary — the panic message is already in the log — and the
27// event goes on as if the mod hadn't handled it.
28
29pub(crate) fn guard<R>(fallback: R, call: impl FnOnce() -> R) -> R {
30    std::panic::catch_unwind(std::panic::AssertUnwindSafe(call)).unwrap_or_else(|_| {
31        crate::error!("a mod's callback panicked (see above); carrying on without it");
32        fallback
33    })
34}
35//
36// Each trampoline is a `unsafe extern "C" fn` that:
37//   1. Casts `ud` back to the original boxed closure.
38//   2. Converts ABI C structs to Rust event types.
39//   4. Calls the closure.
40//
41// Closures are Box::into_raw'd in Registry methods — they are INTENTIONALLY
42// leaked and live for the process lifetime.
43
44// ── ABI minor 10 — client-side trampolines ────────────────────────────────────
45
46// ── Registry ─────────────────────────────────────────────────────────────────
47
48/// Wraps the [`YogApi`] table and provides an ergonomic registration API.
49///
50/// Obtained inside `yog_mod_register` via `export_mod!`.  Closures registered
51/// here are boxed and leaked — they live as long as the process (which is the
52/// correct lifetime for a server mod).
53/// Pointer to the runtime's `YogApi` table (a process-lifetime static in the
54/// runtime), captured when the mod registers. Lets free functions like
55/// [`installed_mods`] work outside of `register()` — e.g. in UI handlers.
56static GLOBAL_API: yog_sync::atomic::AtomicPtr<YogApi> =
57    yog_sync::atomic::AtomicPtr::new(std::ptr::null_mut());
58
59/// The runtime's `YogApi` table, null before this mod registered.
60pub(crate) fn global_api() -> *mut YogApi {
61    GLOBAL_API.load(yog_sync::atomic::Ordering::Acquire)
62}
63
64pub use yog_event::game::ModInfo;
65
66/// All installed mods known to the loader: .yog mods plus, where the host
67/// exposes them, platform (Java) mods. Callable at any time after this mod's
68/// `register()` ran — including client-side UI handlers. Note that during
69/// `register()` mods that load after this one are not in the list yet; query
70/// lazily (e.g. on first render) for a complete view.
71pub fn installed_mods() -> Vec<ModInfo> {
72    crate::call(&yog_event::game::Mods {}).unwrap_or_default()
73}
74
75/// Open the Yog UI registered as `ui_id` (client side; no-op on dedicated
76/// servers). `modal` blocks game input, `pause` pauses singleplayer. Callable
77/// from any handler after registration — e.g. an `on_client_packet` handler.
78pub fn open_ui(ui_id: &str, modal: bool, pause: bool) {
79    crate::call(&yog_event::ui::Open {
80        ui_id: ui_id.to_owned(),
81        modal,
82        pause,
83    });
84}
85
86/// Close the Yog UI screen showing `ui_id` — e.g. from that screen's own
87/// "Done" button. Does nothing when another screen is open. Deferred onto the
88/// render thread, so the handler that calls it finishes first. Client side;
89/// a no-op on dedicated servers.
90pub fn close_ui(ui_id: &str) {
91    crate::call(&yog_event::ui::Close {
92        ui_id: ui_id.to_owned(),
93    });
94}
95
96/// What a [`Registry::register_ui`] handler returns: `()` (never marks
97/// events as used) or `bool` (`true` = the handler used this `key:`/`char:`
98/// event — the host then doesn't, e.g. Esc doesn't close the screen).
99pub trait UiHandled {
100    fn handled(self) -> bool;
101}
102
103impl UiHandled for () {
104    fn handled(self) -> bool {
105        false
106    }
107}
108
109impl UiHandled for bool {
110    fn handled(self) -> bool {
111        self
112    }
113}
114
115pub struct Registry {
116    api: *const YogApi,
117}
118
119// SAFETY: `api` is a static provided by the runtime, valid for process lifetime.
120unsafe impl Send for Registry {}
121unsafe impl Sync for Registry {}
122
123/// A record of this mod's (`yog_api::info!`…): the `log/record` event, which
124/// the runtime writes to the game's log.
125fn log_to_host(level: yog_logging::Level, message: &str) {
126    crate::emit(&yog_event::log::Record {
127        level: level as u32,
128        message: message.to_owned(),
129    });
130}
131
132impl Registry {
133    /// Build from the pointer passed by the runtime. Only called by `export_mod!`.
134    ///
135    /// # Safety
136    ///
137    /// `api` must point to the runtime's `YogApi` table, valid for the rest of
138    /// the process.
139    pub unsafe fn from_raw(api: *const YogApi) -> Self {
140        GLOBAL_API.store(api as *mut YogApi, yog_sync::atomic::Ordering::Release);
141        // The mod's records go to the game's log, through the runtime.
142        yog_logging::set_sink(log_to_host);
143        // The actions' functions and handles (`player::give_item`, `Player`) reach the bus through it.
144        yog_event::set_caller(crate::service::call_at);
145        // Atoms number the same in every mod: the runtime's table.
146        if !yog_data::atom::install(crate::service::ATOMS) {
147            yog_logging::warn!(
148                "atoms were made before the mod was registered: their numbers are this mod's own"
149            );
150        }
151        Self { api }
152    }
153
154    /// The runtime's table.
155    pub(crate) fn api(&self) -> &YogApi {
156        unsafe { &*self.api }
157    }
158
159    /// Hands the runtime content of `kind` named `id`, its definition a Yog
160    /// Data blob (`content/register`, one service for every kind).
161    fn register_blob(&mut self, kind: &str, id: &str, data: &[u8]) {
162        let register = yog_event::content::Register {
163            kind: kind.to_owned(),
164            id: id.to_owned(),
165            data: data.to_vec(),
166        };
167        if self.call(&register) != Some(true) {
168            crate::error!("{kind} {id}: the runtime didn't take it");
169        }
170    }
171    // ── commands ─────────────────────────────────────────────────────────────
172
173    pub fn on_command<F>(&mut self, name: impl AsRef<str>, handler: F)
174    where
175        F: Fn(&CommandContext) -> Option<String> + Send + Sync + 'static,
176    {
177        self.command(name.as_ref(), None, handler);
178    }
179
180    /// Register a command with Brigadier-typed arguments.
181    ///
182    /// `schema` is a space-separated list of argument types:
183    /// `int`, `float`, `word`, `string` (greedy, must be last), `player`, `blockpos`.
184    ///
185    /// In the handler use `ctx.arg_int(0)`, `ctx.arg_blockpos(1)`, etc.
186    pub fn on_typed_command<F>(
187        &mut self,
188        name: impl AsRef<str>,
189        schema: impl AsRef<str>,
190        handler: F,
191    ) where
192        F: Fn(&CommandContext) -> Option<String> + Send + Sync + 'static,
193    {
194        self.command(name.as_ref(), Some(schema.as_ref()), handler);
195    }
196
197    /// Declares command `name` with `schema` to the host, and answers its
198    /// runs that fit it — each way it takes arguments ranked as
199    /// [`yog_command::Fit::ORDER`] says, the first answer winning.
200    fn command<F>(&mut self, name: &str, schema: Option<&str>, handler: F)
201    where
202        F: Fn(&CommandContext) -> Option<String> + Send + Sync + 'static,
203    {
204        let declare = yog_event::command::CommandSpec {
205            name: name.to_owned(),
206            schema: schema.map(str::to_owned),
207        };
208        if self.call(&declare) != Some(true) {
209            crate::error!("command {name}: the host wasn't told of it");
210        }
211        let handler = yog_sync::Arc::new(handler);
212        for &fit in yog_command::Fit::ways(schema) {
213            let (handler, schema) = (yog_sync::Arc::clone(&handler), declare.schema.clone());
214            self.change_key_at::<yog_event::server::command::Run>(
215                name,
216                fit.rank() as i32,
217                move |run, _| {
218                    if run.reply.is_some() {
219                        return;
220                    }
221                    let Some(args) = fit.take(schema.as_deref(), &run.args) else {
222                        return;
223                    };
224                    let ctx = CommandContext {
225                        name: run.name.clone(),
226                        args,
227                        source: run.source.clone(),
228                        uuid: run.uuid.clone(),
229                        dimension: run.dimension.clone(),
230                    };
231                    run.reply = handler(&ctx);
232                },
233            );
234        }
235    }
236
237    // ── networking ───────────────────────────────────────────────────────────
238
239    /// Runs `handler` on every packet a client sends on `channel` (server side).
240    pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
241    where
242        F: Fn(&PacketEvent) + Send + Sync + 'static,
243    {
244        self.on_key::<PacketEvent, _>(channel.as_ref(), move |p, _| handler(p));
245    }
246
247    /// Runs `handler` on every packet the server sends on `channel` (client side).
248    pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
249    where
250        F: Fn(&ClientPacketEvent) + Send + Sync + 'static,
251    {
252        self.on_key::<ClientPacketEvent, _>(channel.as_ref(), move |p, _| handler(p));
253    }
254
255    /// Register a typed-packet handler.
256    ///
257    /// The payload is read as `P` (a [`Packet`]: any Yog Data type).
258    /// Malformed payloads are silently dropped.
259    pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
260    where
261        P: Packet + Send + Sync + 'static,
262        F: Fn(&P) + Send + Sync + 'static,
263    {
264        self.on_packet(channel, move |ev| {
265            if let Some(pkt) = P::from_payload(&ev.payload) {
266                handler(&pkt);
267            }
268        });
269    }
270
271    // ── content ──────────────────────────────────────────────────────────────
272
273    /// Hands the runtime content of `kind` named `id`: its definition.
274    fn content(&mut self, kind: &str, id: &str, def: &yog_data::Value) {
275        self.register_blob(kind, id, &yog_data::binary::encode(def))
276    }
277
278    /// Register a recipe (Yog's format — see [`Recipe`]; the host writes
279    /// its Minecraft version's JSON).
280    pub fn add_recipe(&mut self, recipe: Recipe) {
281        let (ns, name) = recipe.ns_name();
282        let id = format!("{ns}:{name}");
283        self.content(yog_ids::kind::RECIPE, &id, &recipe.to_data());
284    }
285
286    /// Register a recipe given as Yog's recipe in Yog Data, binary — how
287    /// `yog build` registers the mod's own kinds (`{type: "mymod:infusion",
288    /// slots: …}`), which stay the mod's data: for its machines and its books.
289    pub fn add_recipe_data(&mut self, id: &str, data: &[u8]) {
290        self.register_blob(yog_ids::kind::RECIPE, &yog_ids::qualify(id), data)
291    }
292
293    pub fn register_item(&mut self, def: ItemDef) {
294        use yog_data::Schema;
295        self.content(yog_ids::kind::ITEM, &def.id.clone(), &def.write());
296    }
297
298    pub fn register_block(&mut self, def: BlockDef) {
299        use yog_data::Schema;
300        self.content(yog_ids::kind::BLOCK, &def.id.clone(), &def.write());
301    }
302
303    /// Registers an entity type (what a `.yentity` makes). With a `model`,
304    /// the loader draws it — its body model with the clip of its state
305    /// (`clips`: `idle`, `walk`, or entity data `yog:state`).
306    pub fn register_entity(&mut self, def: yog_registry::EntityDef) {
307        use yog_data::Schema;
308        if crate::entities::drawn(&def) {
309            self.on_body(crate::entities::draw);
310        }
311        self.content(yog_ids::kind::ENTITY, &def.id.clone(), &def.write());
312    }
313
314    /// A compiled `.yentity` (the generated code calls this).
315    pub fn register_entity_data(&mut self, id: &str, data: &[u8]) {
316        match yog_data::read_bytes::<yog_registry::EntityDef>(data) {
317            Ok(def) => self.register_entity(def),
318            Err(e) => crate::error!("entity {id}: {e:?}"),
319        }
320    }
321
322    /// Add placed feature `feature` (the mod's datapack's, like an ore's
323    /// `mymod:mythril_ore`) to the biomes of tag `biomes` at generation
324    /// `step` (`underground_ores`). An ore `.ydef` does this for you.
325    pub fn add_biome_feature(&mut self, feature: &str, biomes: &str, step: &str) {
326        use yog_data::Schema;
327        let f = yog_registry::BiomeFeature {
328            feature: feature.into(),
329            biomes: biomes.into(),
330            step: step.into(),
331        };
332        self.content(yog_ids::kind::BIOME_FEATURE, feature, &f.write());
333    }
334
335    /// Register a tool material — what tool items naming it
336    /// (`ItemDef::tool`) are made of. The game's are built in.
337    pub fn register_tool_material(&mut self, material: ToolMaterial) {
338        use yog_data::Schema;
339        self.content(
340            yog_ids::kind::TOOL_MATERIAL,
341            &material.id.clone(),
342            &material.write(),
343        );
344    }
345
346    /// Register an armor material — what armor items naming it
347    /// (`ItemDef::armor`) are made of. The game's are built in.
348    pub fn register_armor_material(&mut self, material: ArmorMaterial) {
349        use yog_data::Schema;
350        self.content(
351            yog_ids::kind::ARMOR_MATERIAL,
352            &material.id.clone(),
353            &material.write(),
354        );
355    }
356
357    /// Register a sound group — what blocks naming it (`BlockDef::sound`)
358    /// sound like. The game's groups are built in.
359    pub fn register_sound_group(&mut self, group: SoundGroup) {
360        use yog_data::Schema;
361        self.content(
362            yog_ids::kind::SOUND_GROUP,
363            &group.id.clone(),
364            &group.write(),
365        );
366    }
367
368    /// Register a real Container/Menu inventory screen — see
369    /// `yog_inventory::InventoryDef`. Attach it to a block via
370    /// `BlockDef::inventory(id)`.
371    pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
372        use yog_data::Schema;
373        self.content(yog_ids::kind::INVENTORY, &def.id.clone(), &def.write());
374    }
375
376    // ── startup grants ───────────────────────────────────────────────────────
377
378    /// Register a startup grant: items/books to give once when a player first joins.
379    pub fn register_startup_grant(&mut self, grant: StartupGrant) {
380        use yog_data::Schema;
381        self.content(
382            yog_ids::kind::STARTUP_GRANT,
383            &grant.id.clone(),
384            &grant.write(),
385        );
386    }
387
388    /// Register a book.
389    pub fn register_book(&mut self, book: &Book) {
390        use yog_data::Schema;
391        self.content(yog_ids::kind::BOOK, &book.id, &book.write());
392    }
393
394    /// Register a book given as Yog Data, binary — what `yog build` makes
395    /// of a `content/**/*.ybook`; the bytes go to the runtime as they are.
396    pub fn register_book_data(&mut self, id: &str, data: &[u8]) {
397        self.register_blob(yog_ids::kind::BOOK, id, data)
398    }
399
400    /// Register a font given as a compiled `.yfont` Yog Data blob.
401    pub fn register_font_data(&mut self, id: &str, data: &[u8]) {
402        self.register_blob(yog_ids::kind::FONT, id, data)
403    }
404
405    /// Register (or patch) a dimension's *type* (sky, lighting, physics, ...).
406    ///
407    /// `def.id` need not be a new custom id — pass a vanilla id
408    /// (`"minecraft:overworld"`, `"minecraft:the_nether"`,
409    /// `"minecraft:the_end"`) to patch that dimension's type/generator
410    /// instead of creating a new one (e.g. to replace the overworld's
411    /// terrain with your own generator type).
412    ///
413    /// Must be called from `register()` — the same mod-init window as
414    /// blocks/items — so the host can declare/patch the dimension before
415    /// Minecraft's registries freeze. See the `yog-dimensions` crate docs;
416    /// set `def.generator_type` (via [`YogDimensionDef::generator_type`]) to
417    /// pick a generator type registered with
418    /// [`Registry::register_chunk_generator`].
419    pub fn register_dimension(&mut self, def: &YogDimensionDef) {
420        self.register_blob(yog_ids::kind::DIMENSION, &def.id, &yog_data::to_bytes(def));
421    }
422
423    /// Register a dimension given as Yog Data, binary — what `yog build`
424    /// makes of a `.ydef` of kind `dimension` (its `terrain` generated by
425    /// the runtime itself); the bytes go to the runtime as they are.
426    pub fn register_dimension_data(&mut self, id: &str, data: &[u8]) {
427        self.register_blob(yog_ids::kind::DIMENSION, id, data)
428    }
429
430    /// Register a named, reusable chunk-generator type under
431    /// `generator_type_id` (e.g. `"mymod:custom_terrain"`) — analogous to
432    /// how Biomes O'Plenty registers selectable world-type presets. Any
433    /// dimension (custom or vanilla) can opt into this generator via
434    /// [`YogDimensionDef::generator_type`]; the platform host may also list
435    /// registered generator type ids as options on the world-creation screen.
436    ///
437    /// `generator` is called once per chunk column that needs generating in
438    /// any dimension using this type — write whatever terrain logic you want
439    /// in the [`GeneratedChunk`] (combine as many noise layers as you like;
440    /// this crate doesn't prescribe a generation style). Its terrain, if
441    /// declared, is in the column already; another mod's handler for the
442    /// same type (ores, say) sees what this one placed.
443    pub fn register_chunk_generator<F>(&mut self, generator_type_id: &str, generator: F)
444    where
445        F: Fn(&mut GeneratedChunk) + Send + Sync + 'static,
446    {
447        self.change_key_at::<GeneratedChunk>(
448            generator_type_id,
449            yog_abi::priority::DEFAULT,
450            move |chunk, _| generator(chunk),
451        );
452    }
453
454    /// Register a menu entry — the host renders a button on vanilla screens
455    /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
456    /// `label` is the human-readable button text (e.g. "Yog Mods").
457    /// `ui_id` is the UI to open when clicked (e.g. "example:menu").
458    /// See [`installed_mods`]. During `register()` the list only contains
459    /// mods loaded before this one.
460    pub fn installed_mods(&self) -> Vec<ModInfo> {
461        installed_mods()
462    }
463
464    /// The raw `YogApi` table.
465    pub fn raw_api(&self) -> *const yog_abi::YogApi {
466        self.api
467    }
468
469    pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
470        if self.call(&yog_event::menu::MenuEntry {
471            label: label.to_owned(),
472            ui_id: ui_id.to_owned(),
473        }) != Some(true)
474        {
475            crate::error!("menu entry {ui_id}: the host wasn't told of it");
476        }
477    }
478
479    /// Register a UI tree with an event callback.
480    /// `ui_id` is the unique identifier (e.g. "mymod:menu").
481    /// `handler` receives `(ui_id, event)` for the screen's input: `click:X:Y`,
482    /// `drag:X:Y`, `release:X:Y`, `scroll:DY`, `key:KEY:MODS` (GLFW key code
483    /// and modifier bits, on press and repeat) and `char:CODEPOINT` (a typed
484    /// character). It returns `()` or a `bool`: `true` marks a `key:`/`char:`
485    /// event as used, so the host skips its default handling of the key
486    /// (Esc closing the screen, ...) — see [`UiHandled`].
487    pub fn register_ui<F, R>(&mut self, ui_id: &str, handler: F)
488    where
489        F: Fn(&str, &str) -> R + Send + Sync + 'static,
490        R: UiHandled,
491    {
492        self.before_key::<yog_event::client::ui::Input, _>(ui_id, move |input, _| {
493            !handler(&input.ui_id, &input.event).handled()
494        });
495    }
496
497    // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
498
499    /// Register a render callback for a specific UI screen (`ui_id`).
500    ///
501    /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
502    /// the screen — so your UI draws on top of the dimmed world view.
503    /// Use this instead of `on_hud_render` for book/inventory screens.
504    ///
505    /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
506    /// can do hit-testing on your own stored layout.
507    pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
508    where
509        F: Fn(&GfxContext) + Send + Sync + 'static,
510    {
511        self.on_draw::<yog_event::client::render::Ui>(Some(ui_id), handler);
512    }
513
514    /// Draw the pictures of book pages of type `drawn` whose `drawer` is
515    /// `drawer` (`"mymod:diagram"`): `handler` gets the page's `data` and
516    /// the picture's box `[x, y, w, h]` in screen pixels, every frame the
517    /// page is open.
518    ///
519    /// ```text
520    /// { type: "drawn", drawer: "mymod:diagram", title: "Wiring", height: 64,
521    ///   caption: "{u|in} → {u|out}", text: "…", data: { nodes: 3 } }
522    /// ```
523    pub fn on_book_page<F>(&mut self, drawer: &str, handler: F)
524    where
525        F: Fn(&GfxContext, &yog_data::Value, [f32; 4]) + Send + Sync + 'static,
526    {
527        self.on_key::<yog_event::client::render::Page, _>(drawer, move |page, _| {
528            if let Some(gfx) = crate::draw::frame_gfx() {
529                handler(&gfx, &page.data, page.rect);
530            }
531        });
532    }
533
534    /// A picture under the tooltips of `item` (as Hex Casting's scrolls show
535    /// their pattern): `size(item, snbt)` says whether a stack — its custom
536    /// data as SNBT — gets one and its size in GUI pixels; `draw` draws it
537    /// in the box `[x, y, w, h]` (screen pixels) while the tooltip shows.
538    pub fn on_item_tooltip<S, D>(&mut self, item: &str, size: S, draw: D)
539    where
540        S: Fn(&str, &str) -> Option<(f32, f32)> + Send + Sync + 'static,
541        D: Fn(&GfxContext, &str, &str, [f32; 4]) + Send + Sync + 'static,
542    {
543        self.provide_key::<yog_event::item::Picture>(item, move |p, _| {
544            size(&p.item, &p.snbt).map(|(w, h)| [w, h])
545        });
546        self.on_key::<yog_event::client::render::Tooltip, _>(item, move |t, _| {
547            if let Some(gfx) = crate::draw::frame_gfx() {
548                draw(&gfx, &t.item, &t.snbt, t.rect);
549            }
550        });
551    }
552
553    /// A stack's own name and tooltip, from its custom data (SNBT): `text(item,
554    /// snbt)` gives `Some((name, lines))` — `name` `None` keeps the item's;
555    /// `lines` stand instead of the item's description — or `None` for the
556    /// item's own. A written blueprint names its design:
557    ///
558    /// ```ignore
559    /// registry.on_item_text(Items::BLUEPRINT, |_, snbt| {
560    ///     let bp = Blueprint::from_nbt(snbt)?;
561    ///     Some((Some(bp.name.clone()), vec![format!("{} ports", bp.ports)]))
562    /// });
563    /// ```
564    pub fn on_item_text<F>(&mut self, item: &str, text: F)
565    where
566        F: Fn(&str, &str) -> Option<(Option<String>, Vec<String>)> + Send + Sync + 'static,
567    {
568        self.provide_key::<yog_event::item::Text>(item, move |t, _| {
569            text(&t.item, &t.snbt).map(|(name, lines)| yog_event::item::Shown { name, lines })
570        });
571    }
572
573    /// A compiled `.ymodel` (the generated code calls this): found later by
574    /// [`crate::body::model`].
575    pub fn register_body_model(&mut self, id: &str, data: &[u8]) {
576        if let Err(e) = yog_body::register_model(id, data) {
577            crate::error!("{e}");
578        }
579    }
580
581    /// A compiled `.yanim` (the generated code calls this): its clips found
582    /// later by [`crate::body::clip`].
583    pub fn register_body_anims(&mut self, id: &str, data: &[u8]) {
584        if let Err(e) = yog_body::register_anims(id, data) {
585            crate::error!("{e}");
586        }
587    }
588
589    /// Register a handler asked each frame about the body of every tracked
590    /// entity ([`crate::body::track`]): it answers with [`crate::body::Ops`]
591    /// — pose vanilla parts, turn the body, draw meshes (rigs, morphs),
592    /// disguise, hide, capture. Every mod's ops add up.
593    pub fn on_body<F>(&mut self, handler: F)
594    where
595        F: Fn(&yog_body::Frame, &mut yog_body::Ops) + Send + Sync + 'static,
596    {
597        self.change::<yog_event::client::render::Body>(move |e, _| {
598            let mut ops = yog_body::Ops::default();
599            handler(&e.frame, &mut ops);
600            e.ops.add(ops);
601        });
602    }
603
604    /// Change the loot of some tables as `modifier` says.
605    pub fn add_loot_modifier(&mut self, modifier: crate::loot::LootModifier) {
606        self.change::<yog_event::world::loot::Roll>(move |loot, _| {
607            crate::loot::apply(&modifier, loot)
608        });
609    }
610
611    /// Register an attribute every player has — what effects change and
612    /// [`yog_event::entity::attribute_value`] reads.
613    pub fn register_attribute(&mut self, def: yog_registry::AttributeDef) {
614        use yog_data::Schema;
615        self.content(yog_ids::kind::ATTRIBUTE, &def.id.clone(), &def.write());
616    }
617
618    /// Register a mob effect.
619    pub fn register_effect(&mut self, def: yog_registry::EffectDef) {
620        use yog_data::Schema;
621        self.content(yog_ids::kind::EFFECT, &def.id.clone(), &def.write());
622    }
623
624    /// Register a potion and its brewing.
625    pub fn register_potion(&mut self, def: yog_registry::PotionDef) {
626        use yog_data::Schema;
627        self.content(yog_ids::kind::POTION, &def.id.clone(), &def.write());
628    }
629
630    /// A compiled `.ydef` the game registers (`attribute`, `effect`,
631    /// `potion`) — the generated code calls this.
632    pub fn register_definition_data(&mut self, kind: &str, id: &str, data: &[u8]) {
633        self.register_blob(kind, id, data)
634    }
635
636    /// A compiled `.ydef` loot modifier (the generated code calls this).
637    pub fn add_loot_modifier_data(&mut self, id: &str, data: &[u8]) {
638        match yog_data::read_bytes::<crate::loot::LootModifier>(data) {
639            Ok(m) => self.add_loot_modifier(m),
640            Err(e) => crate::error!("loot modifier {id}: {e:?}"),
641        }
642    }
643
644    /// Register loot function `name` (`mymod:pattern_scroll`): what loot
645    /// modifiers naming it (`function`) run on each stack they add — set
646    /// its custom data, say.
647    pub fn loot_function<F>(&mut self, name: &str, function: F)
648    where
649        F: Fn(&mut crate::loot::LootStack, &crate::loot::Loot) + Send + Sync + 'static,
650    {
651        let mut f = crate::loot::functions().write();
652        f.insert(name.to_string(), yog_sync::Arc::new(function));
653    }
654
655    /// Register a handler that moves, turns or zooms the camera each frame;
656    /// it changes the [`crate::body::CameraOut`] the handlers before it made.
657    pub fn on_camera<F>(&mut self, handler: F)
658    where
659        F: Fn(&yog_body::CameraFrame, &mut yog_body::CameraOut) + Send + Sync + 'static,
660    {
661        self.change::<yog_event::client::render::Camera>(move |e, _| handler(&e.frame, &mut e.out));
662    }
663
664    /// Register a handler that may replace the local player's travel step:
665    /// `Some(velocity)` (blocks per tick, drag applied) moves the player by
666    /// it instead of vanilla's walking, swimming, falling or gliding.
667    pub fn on_travel<F>(&mut self, handler: F)
668    where
669        F: Fn(&yog_body::TravelFrame) -> Option<yog_body::Vec3> + Send + Sync + 'static,
670    {
671        self.change::<yog_event::client::game::Travel>(move |e, _| {
672            if e.velocity.is_none() {
673                e.velocity = handler(&e.frame);
674            }
675        });
676    }
677
678    /// Register a handler called every frame when the HUD is rendered.
679    ///
680    /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
681    /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
682    pub fn on_hud_render<F>(&mut self, handler: F)
683    where
684        F: Fn(&GfxContext) + Send + Sync + 'static,
685    {
686        self.on_draw::<yog_event::client::render::Hud>(None, handler);
687    }
688
689    /// Register a handler called every frame at the end of world rendering.
690    ///
691    /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
692    /// `gfx.camera_pos()` is the camera world position.
693    /// To render at world position P, translate by `P - camera_pos` before drawing.
694    pub fn on_world_render<F>(&mut self, handler: F)
695    where
696        F: Fn(&GfxContext) + Send + Sync + 'static,
697    {
698        self.on_draw::<yog_event::client::render::World>(None, handler);
699    }
700
701    /// A key binding the player sees in the game's controls (under this
702    /// mod's name) and can rebind there; the game keeps the key it's bound
703    /// to. `id` names it (`mymod:xray`), `name` is its label's translation
704    /// key (a `.yres` text's `…_KEY`), `default_key` a GLFW key code
705    /// ([`crate::client::keys`]; `None`: unbound). `handler` runs on the
706    /// client thread at each press.
707    ///
708    /// ```ignore
709    /// registry.key_binding("mymod:xray", Strings::KEY_XRAY_KEY, Some(keys::X), || toggle_xray());
710    /// ```
711    pub fn key_binding<F>(&mut self, id: &str, name: &str, default_key: Option<i32>, handler: F)
712    where
713        F: Fn() + Send + Sync + 'static,
714    {
715        let declare = yog_event::key_binding::KeyBindingSpec {
716            id: id.to_owned(),
717            name: name.to_owned(),
718            default_key: default_key.unwrap_or(crate::client::keys::UNBOUND),
719        };
720        if self.call(&declare) != Some(true) {
721            crate::error!("key binding {id}: the host wasn't told of it");
722        }
723        self.on_key::<yog_event::client::input::Binding, _>(id, move |_, _| handler());
724    }
725}
726
727// ── Mod trait ─────────────────────────────────────────────────────────────────
728
729/// Implemented by every Yog mod. Called once at startup to register handlers.
730pub trait Mod {
731    fn register(registry: &mut Registry);
732}