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(®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 };
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}