yog_api/mods/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::messages::service::call_at);
145 // Atoms number the same in every mod: the runtime's table.
146 if !yog_data::atom::install(crate::messages::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(®ister) != 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 permission: run.permission,
231 op: run.op,
232 };
233 run.reply = handler(&ctx);
234 },
235 );
236 }
237 }
238
239 // ── networking ───────────────────────────────────────────────────────────
240
241 /// Runs `handler` on every packet a client sends on `channel` (server side).
242 pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
243 where
244 F: Fn(&PacketEvent) + Send + Sync + 'static,
245 {
246 self.on_key::<PacketEvent, _>(channel.as_ref(), move |p, _| handler(p));
247 }
248
249 /// Runs `handler` on every packet the server sends on `channel` (client side).
250 pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
251 where
252 F: Fn(&ClientPacketEvent) + Send + Sync + 'static,
253 {
254 self.on_key::<ClientPacketEvent, _>(channel.as_ref(), move |p, _| handler(p));
255 }
256
257 /// Register a typed-packet handler.
258 ///
259 /// The payload is read as `P` (a [`Packet`]: any Yog Data type).
260 /// Malformed payloads are silently dropped.
261 pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
262 where
263 P: Packet + Send + Sync + 'static,
264 F: Fn(&P) + Send + Sync + 'static,
265 {
266 self.on_packet(channel, move |ev| {
267 if let Some(pkt) = P::from_payload(&ev.payload) {
268 handler(&pkt);
269 }
270 });
271 }
272
273 // ── content ──────────────────────────────────────────────────────────────
274
275 /// Hands the runtime content of `kind` named `id`: its definition.
276 fn content(&mut self, kind: &str, id: &str, def: &yog_data::Value) {
277 self.register_blob(kind, id, &yog_data::binary::encode(def))
278 }
279
280 /// Register a recipe (Yog's format — see [`Recipe`]; the host writes
281 /// its Minecraft version's JSON).
282 pub fn add_recipe(&mut self, recipe: Recipe) {
283 let (ns, name) = recipe.ns_name();
284 let id = format!("{ns}:{name}");
285 self.content(yog_ids::kind::RECIPE, &id, &recipe.to_data());
286 }
287
288 /// Register a recipe given as Yog's recipe in Yog Data, binary — how
289 /// `yog build` registers the mod's own kinds (`{type: "mymod:infusion",
290 /// slots: …}`), which stay the mod's data: for its machines and its books.
291 pub fn add_recipe_data(&mut self, id: &str, data: &[u8]) {
292 self.register_blob(yog_ids::kind::RECIPE, &yog_ids::qualify(id), data)
293 }
294
295 pub fn register_item(&mut self, def: ItemDef) {
296 use yog_data::Schema;
297 self.content(yog_ids::kind::ITEM, &def.id.clone(), &def.write());
298 }
299
300 pub fn register_block(&mut self, def: BlockDef) {
301 use yog_data::Schema;
302 self.content(yog_ids::kind::BLOCK, &def.id.clone(), &def.write());
303 }
304
305 /// Registers an entity type (what a `.yentity` makes). With a `model`,
306 /// the loader draws it — its body model with the clip of its state
307 /// (`clips`: `idle`, `walk`, or entity data `yog:state`).
308 pub fn register_entity(&mut self, def: yog_registry::EntityDef) {
309 use yog_data::Schema;
310 if crate::entities::drawn(&def) {
311 self.on_body(crate::entities::draw);
312 }
313 self.content(yog_ids::kind::ENTITY, &def.id.clone(), &def.write());
314 }
315
316 /// A compiled `.yentity` (the generated code calls this).
317 pub fn register_entity_data(&mut self, id: &str, data: &[u8]) {
318 match yog_data::read_bytes::<yog_registry::EntityDef>(data) {
319 Ok(def) => self.register_entity(def),
320 Err(e) => crate::error!("entity {id}: {e:?}"),
321 }
322 }
323
324 /// Add placed feature `feature` (the mod's datapack's, like an ore's
325 /// `mymod:mythril_ore`) to the biomes of tag `biomes` at generation
326 /// `step` (`underground_ores`). An ore `.ydef` does this for you.
327 pub fn add_biome_feature(&mut self, feature: &str, biomes: &str, step: &str) {
328 use yog_data::Schema;
329 let f = yog_registry::BiomeFeature {
330 feature: feature.into(),
331 biomes: biomes.into(),
332 step: step.into(),
333 };
334 self.content(yog_ids::kind::BIOME_FEATURE, feature, &f.write());
335 }
336
337 /// Register a tool material — what tool items naming it
338 /// (`ItemDef::tool`) are made of. The game's are built in.
339 pub fn register_tool_material(&mut self, material: ToolMaterial) {
340 use yog_data::Schema;
341 self.content(
342 yog_ids::kind::TOOL_MATERIAL,
343 &material.id.clone(),
344 &material.write(),
345 );
346 }
347
348 /// Register an armor material — what armor items naming it
349 /// (`ItemDef::armor`) are made of. The game's are built in.
350 pub fn register_armor_material(&mut self, material: ArmorMaterial) {
351 use yog_data::Schema;
352 self.content(
353 yog_ids::kind::ARMOR_MATERIAL,
354 &material.id.clone(),
355 &material.write(),
356 );
357 }
358
359 /// Register a sound group — what blocks naming it (`BlockDef::sound`)
360 /// sound like. The game's groups are built in.
361 pub fn register_sound_group(&mut self, group: SoundGroup) {
362 use yog_data::Schema;
363 self.content(
364 yog_ids::kind::SOUND_GROUP,
365 &group.id.clone(),
366 &group.write(),
367 );
368 }
369
370 /// Register a real Container/Menu inventory screen — see
371 /// `yog_inventory::InventoryDef`. Attach it to a block via
372 /// `BlockDef::inventory(id)`.
373 pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
374 use yog_data::Schema;
375 self.content(yog_ids::kind::INVENTORY, &def.id.clone(), &def.write());
376 }
377
378 // ── startup grants ───────────────────────────────────────────────────────
379
380 /// Register a startup grant: items/books to give once when a player first joins.
381 pub fn register_startup_grant(&mut self, grant: StartupGrant) {
382 use yog_data::Schema;
383 self.content(
384 yog_ids::kind::STARTUP_GRANT,
385 &grant.id.clone(),
386 &grant.write(),
387 );
388 }
389
390 /// Register a book.
391 pub fn register_book(&mut self, book: &Book) {
392 use yog_data::Schema;
393 self.content(yog_ids::kind::BOOK, &book.id, &book.write());
394 }
395
396 /// Register a book given as Yog Data, binary — what `yog build` makes
397 /// of a `content/**/*.ybook`; the bytes go to the runtime as they are.
398 pub fn register_book_data(&mut self, id: &str, data: &[u8]) {
399 self.register_blob(yog_ids::kind::BOOK, id, data)
400 }
401
402 /// Register a font given as a compiled `.yfont` Yog Data blob.
403 pub fn register_font_data(&mut self, id: &str, data: &[u8]) {
404 self.register_blob(yog_ids::kind::FONT, id, data)
405 }
406
407 /// Register (or patch) a dimension's *type* (sky, lighting, physics, ...).
408 ///
409 /// `def.id` need not be a new custom id — pass a vanilla id
410 /// (`"minecraft:overworld"`, `"minecraft:the_nether"`,
411 /// `"minecraft:the_end"`) to patch that dimension's type/generator
412 /// instead of creating a new one (e.g. to replace the overworld's
413 /// terrain with your own generator type).
414 ///
415 /// Must be called from `register()` — the same mod-init window as
416 /// blocks/items — so the host can declare/patch the dimension before
417 /// Minecraft's registries freeze. See the `yog-dimensions` crate docs;
418 /// set `def.generator_type` (via [`YogDimensionDef::generator_type`]) to
419 /// pick a generator type registered with
420 /// [`Registry::register_chunk_generator`].
421 pub fn register_dimension(&mut self, def: &YogDimensionDef) {
422 self.register_blob(yog_ids::kind::DIMENSION, &def.id, &yog_data::to_bytes(def));
423 }
424
425 /// Register a dimension given as Yog Data, binary — what `yog build`
426 /// makes of a `.ydef` of kind `dimension` (its `terrain` generated by
427 /// the runtime itself); the bytes go to the runtime as they are.
428 pub fn register_dimension_data(&mut self, id: &str, data: &[u8]) {
429 self.register_blob(yog_ids::kind::DIMENSION, id, data)
430 }
431
432 /// Register a named, reusable chunk-generator type under
433 /// `generator_type_id` (e.g. `"mymod:custom_terrain"`) — analogous to
434 /// how Biomes O'Plenty registers selectable world-type presets. Any
435 /// dimension (custom or vanilla) can opt into this generator via
436 /// [`YogDimensionDef::generator_type`]; the platform host may also list
437 /// registered generator type ids as options on the world-creation screen.
438 ///
439 /// `generator` is called once per chunk column that needs generating in
440 /// any dimension using this type — write whatever terrain logic you want
441 /// in the [`GeneratedChunk`] (combine as many noise layers as you like;
442 /// this crate doesn't prescribe a generation style). Its terrain, if
443 /// declared, is in the column already; another mod's handler for the
444 /// same type (ores, say) sees what this one placed.
445 pub fn register_chunk_generator<F>(&mut self, generator_type_id: &str, generator: F)
446 where
447 F: Fn(&mut GeneratedChunk) + Send + Sync + 'static,
448 {
449 self.change_key_at::<GeneratedChunk>(
450 generator_type_id,
451 yog_abi::priority::DEFAULT,
452 move |chunk, _| generator(chunk),
453 );
454 }
455
456 /// Register a menu entry — the host renders a button on vanilla screens
457 /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
458 /// `label` is the human-readable button text (e.g. "Yog Mods").
459 /// `ui_id` is the UI to open when clicked (e.g. "example:menu").
460 /// See [`installed_mods`]. During `register()` the list only contains
461 /// mods loaded before this one.
462 pub fn installed_mods(&self) -> Vec<ModInfo> {
463 installed_mods()
464 }
465
466 /// The raw `YogApi` table.
467 pub fn raw_api(&self) -> *const yog_abi::YogApi {
468 self.api
469 }
470
471 pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
472 if self.call(&yog_event::menu::MenuEntry {
473 label: label.to_owned(),
474 ui_id: ui_id.to_owned(),
475 }) != Some(true)
476 {
477 crate::error!("menu entry {ui_id}: the host wasn't told of it");
478 }
479 }
480
481 /// Register a UI tree with an event callback.
482 /// `ui_id` is the unique identifier (e.g. "mymod:menu").
483 /// `handler` receives `(ui_id, event)` for the screen's input: `click:X:Y`,
484 /// `drag:X:Y`, `release:X:Y`, `scroll:DY`, `key:KEY:MODS` (GLFW key code
485 /// and modifier bits, on press and repeat) and `char:CODEPOINT` (a typed
486 /// character). It returns `()` or a `bool`: `true` marks a `key:`/`char:`
487 /// event as used, so the host skips its default handling of the key
488 /// (Esc closing the screen, ...) — see [`UiHandled`].
489 pub fn register_ui<F, R>(&mut self, ui_id: &str, handler: F)
490 where
491 F: Fn(&str, &str) -> R + Send + Sync + 'static,
492 R: UiHandled,
493 {
494 self.before_key::<yog_event::client::ui::Input, _>(ui_id, move |input, _| {
495 !handler(&input.ui_id, &input.event).handled()
496 });
497 }
498
499 // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
500
501 /// Register a render callback for a specific UI screen (`ui_id`).
502 ///
503 /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
504 /// the screen — so your UI draws on top of the dimmed world view.
505 /// Use this instead of `on_hud_render` for book/inventory screens.
506 ///
507 /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
508 /// can do hit-testing on your own stored layout.
509 pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
510 where
511 F: Fn(&GfxContext) + Send + Sync + 'static,
512 {
513 self.on_draw::<yog_event::client::render::Ui>(Some(ui_id), handler);
514 }
515
516 /// Draw the pictures of book pages of type `drawn` whose `drawer` is
517 /// `drawer` (`"mymod:diagram"`): `handler` gets the page's `data` and
518 /// the picture's box `[x, y, w, h]` in screen pixels, every frame the
519 /// page is open.
520 ///
521 /// ```text
522 /// { type: "drawn", drawer: "mymod:diagram", title: "Wiring", height: 64,
523 /// caption: "{u|in} → {u|out}", text: "…", data: { nodes: 3 } }
524 /// ```
525 pub fn on_book_page<F>(&mut self, drawer: &str, handler: F)
526 where
527 F: Fn(&GfxContext, &yog_data::Value, [f32; 4]) + Send + Sync + 'static,
528 {
529 self.on_key::<yog_event::client::render::Page, _>(drawer, move |page, _| {
530 if let Some(gfx) = crate::messages::draw::frame_gfx() {
531 handler(&gfx, &page.data, page.rect);
532 }
533 });
534 }
535
536 /// A picture under the tooltips of `item` (as Hex Casting's scrolls show
537 /// their pattern): `size(item, snbt)` says whether a stack — its custom
538 /// data as SNBT — gets one and its size in GUI pixels; `draw` draws it
539 /// in the box `[x, y, w, h]` (screen pixels) while the tooltip shows.
540 pub fn on_item_tooltip<S, D>(&mut self, item: &str, size: S, draw: D)
541 where
542 S: Fn(&str, &str) -> Option<(f32, f32)> + Send + Sync + 'static,
543 D: Fn(&GfxContext, &str, &str, [f32; 4]) + Send + Sync + 'static,
544 {
545 self.provide_key::<yog_event::item::Picture>(item, move |p, _| {
546 size(&p.item, &p.snbt).map(|(w, h)| [w, h])
547 });
548 self.on_key::<yog_event::client::render::Tooltip, _>(item, move |t, _| {
549 if let Some(gfx) = crate::messages::draw::frame_gfx() {
550 draw(&gfx, &t.item, &t.snbt, t.rect);
551 }
552 });
553 }
554
555 /// A stack's own name and tooltip, from its custom data (SNBT): `text(item,
556 /// snbt)` gives `Some((name, lines))` — `name` `None` keeps the item's;
557 /// `lines` stand instead of the item's description — or `None` for the
558 /// item's own. A written blueprint names its design:
559 ///
560 /// ```ignore
561 /// registry.on_item_text(Items::BLUEPRINT, |_, snbt| {
562 /// let bp = Blueprint::from_nbt(snbt)?;
563 /// Some((Some(bp.name.clone()), vec![format!("{} ports", bp.ports)]))
564 /// });
565 /// ```
566 pub fn on_item_text<F>(&mut self, item: &str, text: F)
567 where
568 F: Fn(&str, &str) -> Option<(Option<String>, Vec<String>)> + Send + Sync + 'static,
569 {
570 self.provide_key::<yog_event::item::Text>(item, move |t, _| {
571 text(&t.item, &t.snbt).map(|(name, lines)| yog_event::item::Shown { name, lines })
572 });
573 }
574
575 /// A compiled `.ymodel` (the generated code calls this): found later by
576 /// [`crate::body::model`].
577 pub fn register_body_model(&mut self, id: &str, data: &[u8]) {
578 if let Err(e) = yog_body::register_model(id, data) {
579 crate::error!("{e}");
580 }
581 }
582
583 /// A compiled `.yanim` (the generated code calls this): its clips found
584 /// later by [`crate::body::clip`].
585 pub fn register_body_anims(&mut self, id: &str, data: &[u8]) {
586 if let Err(e) = yog_body::register_anims(id, data) {
587 crate::error!("{e}");
588 }
589 }
590
591 /// Register a handler asked each frame about the body of every tracked
592 /// entity ([`crate::body::track`]): it answers with [`crate::body::Ops`]
593 /// — pose vanilla parts, turn the body, draw meshes (rigs, morphs),
594 /// disguise, hide, capture. Every mod's ops add up.
595 pub fn on_body<F>(&mut self, handler: F)
596 where
597 F: Fn(&yog_body::Frame, &mut yog_body::Ops) + Send + Sync + 'static,
598 {
599 self.change::<yog_event::client::render::Body>(move |e, _| {
600 let mut ops = yog_body::Ops::default();
601 handler(&e.frame, &mut ops);
602 e.ops.add(ops);
603 });
604 }
605
606 /// Change the loot of some tables as `modifier` says.
607 pub fn add_loot_modifier(&mut self, modifier: crate::loot::LootModifier) {
608 self.change::<yog_event::world::loot::Roll>(move |loot, _| {
609 crate::loot::apply(&modifier, loot)
610 });
611 }
612
613 /// Register an attribute every player has — what effects change and
614 /// [`yog_event::entity::attribute_value`] reads.
615 pub fn register_attribute(&mut self, def: yog_registry::AttributeDef) {
616 use yog_data::Schema;
617 self.content(yog_ids::kind::ATTRIBUTE, &def.id.clone(), &def.write());
618 }
619
620 /// Register a mob effect.
621 pub fn register_effect(&mut self, def: yog_registry::EffectDef) {
622 use yog_data::Schema;
623 self.content(yog_ids::kind::EFFECT, &def.id.clone(), &def.write());
624 }
625
626 /// Register a potion and its brewing.
627 pub fn register_potion(&mut self, def: yog_registry::PotionDef) {
628 use yog_data::Schema;
629 self.content(yog_ids::kind::POTION, &def.id.clone(), &def.write());
630 }
631
632 /// A compiled `.ydef` the game registers (`attribute`, `effect`,
633 /// `potion`) — the generated code calls this.
634 pub fn register_definition_data(&mut self, kind: &str, id: &str, data: &[u8]) {
635 self.register_blob(kind, id, data)
636 }
637
638 /// A compiled `.ydef` loot modifier (the generated code calls this).
639 pub fn add_loot_modifier_data(&mut self, id: &str, data: &[u8]) {
640 match yog_data::read_bytes::<crate::loot::LootModifier>(data) {
641 Ok(m) => self.add_loot_modifier(m),
642 Err(e) => crate::error!("loot modifier {id}: {e:?}"),
643 }
644 }
645
646 /// Register loot function `name` (`mymod:pattern_scroll`): what loot
647 /// modifiers naming it (`function`) run on each stack they add — set
648 /// its custom data, say.
649 pub fn loot_function<F>(&mut self, name: &str, function: F)
650 where
651 F: Fn(&mut crate::loot::LootStack, &crate::loot::Loot) + Send + Sync + 'static,
652 {
653 let mut f = crate::loot::functions().write();
654 f.insert(name.to_string(), yog_sync::Arc::new(function));
655 }
656
657 /// Register a handler that moves, turns or zooms the camera each frame;
658 /// it changes the [`crate::body::CameraOut`] the handlers before it made.
659 pub fn on_camera<F>(&mut self, handler: F)
660 where
661 F: Fn(&yog_body::CameraFrame, &mut yog_body::CameraOut) + Send + Sync + 'static,
662 {
663 self.change::<yog_event::client::render::Camera>(move |e, _| handler(&e.frame, &mut e.out));
664 }
665
666 /// Register a handler that may replace the local player's travel step:
667 /// `Some(velocity)` (blocks per tick, drag applied) moves the player by
668 /// it instead of vanilla's walking, swimming, falling or gliding.
669 pub fn on_travel<F>(&mut self, handler: F)
670 where
671 F: Fn(&yog_body::TravelFrame) -> Option<yog_body::Vec3> + Send + Sync + 'static,
672 {
673 self.change::<yog_event::client::game::Travel>(move |e, _| {
674 if e.velocity.is_none() {
675 e.velocity = handler(&e.frame);
676 }
677 });
678 }
679
680 /// Register a handler called every frame when the HUD is rendered.
681 ///
682 /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
683 /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
684 pub fn on_hud_render<F>(&mut self, handler: F)
685 where
686 F: Fn(&GfxContext) + Send + Sync + 'static,
687 {
688 self.on_draw::<yog_event::client::render::Hud>(None, handler);
689 }
690
691 /// Register a handler called every frame at the end of world rendering.
692 ///
693 /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
694 /// `gfx.camera_pos()` is the camera world position.
695 /// To render at world position P, translate by `P - camera_pos` before drawing.
696 pub fn on_world_render<F>(&mut self, handler: F)
697 where
698 F: Fn(&GfxContext) + Send + Sync + 'static,
699 {
700 self.on_draw::<yog_event::client::render::World>(None, handler);
701 }
702
703 /// A key binding the player sees in the game's controls (under this
704 /// mod's name) and can rebind there; the game keeps the key it's bound
705 /// to. `id` names it (`mymod:xray`), `name` is its label's translation
706 /// key (a `.yres` text's `…_KEY`), `default_key` a GLFW key code
707 /// ([`crate::client::keys`]; `None`: unbound). `handler` runs on the
708 /// client thread at each press.
709 ///
710 /// ```ignore
711 /// registry.key_binding("mymod:xray", Strings::KEY_XRAY_KEY, Some(keys::X), || toggle_xray());
712 /// ```
713 pub fn key_binding<F>(&mut self, id: &str, name: &str, default_key: Option<i32>, handler: F)
714 where
715 F: Fn() + Send + Sync + 'static,
716 {
717 let declare = yog_event::key_binding::KeyBindingSpec {
718 id: id.to_owned(),
719 name: name.to_owned(),
720 default_key: default_key.unwrap_or(crate::client::keys::UNBOUND),
721 };
722 if self.call(&declare) != Some(true) {
723 crate::error!("key binding {id}: the host wasn't told of it");
724 }
725 self.on_key::<yog_event::client::input::Binding, _>(id, move |_, _| handler());
726 }
727}
728
729// ── Mod trait ─────────────────────────────────────────────────────────────────
730
731/// Implemented by every Yog mod. Called once at startup to register handlers.
732pub trait Mod {
733 fn register(registry: &mut Registry);
734}