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 std::os::raw::c_void;
11
12use yog_abi::{
13    YogAdvancementEvent, YogApi, YogAttackEntityEvent, YogBlockBreakEvent, YogBlockDef,
14    YogChatEvent, YogCommandEvent, YogContainerCloseEvent, YogContainerOpenEvent, YogCraftEvent,
15    YogEntityDamageEvent, YogEntityDeathEvent, YogEntityInteractEvent, YogEntitySpawnEvent,
16    YogExplosionEvent, YogGfxApi, YogInventoryDef, YogItemDef, YogItemPickupEvent,
17    YogKeyPressEvent, YogPacketEvent, YogPlaceBlockEvent, YogPlayerDeathEvent, YogPlayerEvent,
18    YogPlayerMoveEvent, YogPlayerRespawnEvent, YogProjectileHitEvent, YogServer,
19    YogStartupGrantDef, YogStr, YogUseBlockEvent, YogUseItemEvent,
20};
21use yog_book::Book;
22use yog_command::CommandContext;
23use yog_core::Server;
24use yog_event::{
25    AdvancementEvent, AttackEntityEvent, BlockBreakEvent, ChatEvent, ClientTickEvent,
26    ContainerCloseEvent, ContainerOpenEvent, CraftEvent, EntityDamageEvent, EntityDeathEvent,
27    EntityInteractEvent, EntitySpawnEvent, EventPhase, ExplosionEvent, ItemPickupEvent,
28    KeyPressEvent, PlaceBlockEvent, PlayerDeathEvent, PlayerJoinEvent, PlayerLeaveEvent,
29    PlayerMoveEvent, PlayerRespawnEvent, ProjectileHitEvent, ScreenEvent, UseBlockEvent,
30    UseItemEvent,
31};
32use yog_gfx::GfxContext;
33use yog_network::{Packet, PacketEvent};
34use yog_registry::{BlockDef, FurnaceRecipe, ItemDef, ShapedRecipe, ShapelessRecipe, StartupGrant};
35
36// ── CServer — implements Server via the YogServer function table ──────────────
37
38/// A handle to the runtime's server actions, backed by [`YogServer`] fn pointers.
39/// Given to every handler as `&dyn Server`.
40pub struct CServer(pub *const YogServer);
41
42unsafe impl Send for CServer {}
43unsafe impl Sync for CServer {}
44
45macro_rules! srv {
46    ($self:ident) => {
47        unsafe { &*$self.0 }
48    };
49}
50
51impl Server for CServer {
52    fn broadcast(&self, message: &str) {
53        let s = srv!(self);
54        unsafe { (s.broadcast)(s.ctx, YogStr::from_str(message)) }
55    }
56
57    fn get_block(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
58        let s = srv!(self);
59        let owned = unsafe {
60            (s.get_block)(
61                s.ctx,
62                YogStr::from_str(dimension),
63                yog_abi::YogBlockPos {
64                    x: pos.x,
65                    y: pos.y,
66                    z: pos.z,
67                },
68            )
69        };
70        if owned.is_none() {
71            return None;
72        }
73        let result = unsafe {
74            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
75                .ok()
76        };
77        unsafe { (s.free_str)(owned.ptr, owned.len) };
78        result
79    }
80
81    fn set_block(&self, dimension: &str, pos: yog_core::BlockPos, block_id: &str) -> bool {
82        let s = srv!(self);
83        unsafe {
84            (s.set_block)(
85                s.ctx,
86                YogStr::from_str(dimension),
87                yog_abi::YogBlockPos {
88                    x: pos.x,
89                    y: pos.y,
90                    z: pos.z,
91                },
92                YogStr::from_str(block_id),
93            )
94        }
95    }
96
97    fn give_item(&self, player: &str, item_id: &str, count: u32) -> bool {
98        let s = srv!(self);
99        unsafe {
100            (s.give_item)(
101                s.ctx,
102                YogStr::from_str(player),
103                YogStr::from_str(item_id),
104                count,
105            )
106        }
107    }
108
109    fn teleport(&self, player: &str, x: f64, y: f64, z: f64) -> bool {
110        let s = srv!(self);
111        unsafe {
112            (s.player_teleport)(
113                s.ctx,
114                YogStr::from_str(player),
115                yog_abi::YogVec3 { x, y, z },
116            )
117        }
118    }
119
120    fn send_to_player(&self, player: &str, channel: &str, payload: &[u8]) -> bool {
121        let s = srv!(self);
122        unsafe {
123            (s.send_to_player)(
124                s.ctx,
125                YogStr::from_str(player),
126                YogStr::from_str(channel),
127                payload.as_ptr(),
128                payload.len() as u32,
129            )
130        }
131    }
132
133    fn send_to_server(&self, channel: &str, payload: &[u8]) -> bool {
134        let s = srv!(self);
135        unsafe {
136            (s.send_to_server)(
137                s.ctx,
138                YogStr::from_str(channel),
139                payload.as_ptr(),
140                payload.len() as u32,
141            )
142        }
143    }
144
145    fn entity_teleport(&self, uuid: &str, x: f64, y: f64, z: f64) -> bool {
146        let s = srv!(self);
147        unsafe { (s.entity_teleport)(s.ctx, YogStr::from_str(uuid), yog_abi::YogVec3 { x, y, z }) }
148    }
149
150    fn entity_position(&self, uuid: &str) -> Option<(f64, f64, f64)> {
151        let s = srv!(self);
152        let mut out = yog_abi::YogVec3 {
153            x: 0.0,
154            y: 0.0,
155            z: 0.0,
156        };
157        if unsafe { (s.entity_position)(s.ctx, YogStr::from_str(uuid), &mut out) } {
158            Some((out.x, out.y, out.z))
159        } else {
160            None
161        }
162    }
163
164    fn entity_health(&self, uuid: &str) -> Option<f32> {
165        let s = srv!(self);
166        let mut hp = 0f32;
167        if unsafe { (s.entity_health)(s.ctx, YogStr::from_str(uuid), &mut hp) } {
168            Some(hp)
169        } else {
170            None
171        }
172    }
173
174    fn entity_set_health(&self, uuid: &str, health: f32) -> bool {
175        let s = srv!(self);
176        unsafe { (s.entity_set_health)(s.ctx, YogStr::from_str(uuid), health) }
177    }
178
179    fn entity_kill(&self, uuid: &str) -> bool {
180        let s = srv!(self);
181        unsafe { (s.entity_kill)(s.ctx, YogStr::from_str(uuid)) }
182    }
183
184    fn spawn_entity(
185        &self,
186        entity_type: &str,
187        dimension: &str,
188        x: f64,
189        y: f64,
190        z: f64,
191    ) -> Option<String> {
192        let s = srv!(self);
193        let owned = unsafe {
194            (s.spawn_entity)(
195                s.ctx,
196                YogStr::from_str(entity_type),
197                YogStr::from_str(dimension),
198                yog_abi::YogVec3 { x, y, z },
199            )
200        };
201        if owned.is_none() {
202            return None;
203        }
204        let result = unsafe {
205            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
206                .ok()
207        };
208        unsafe { (s.free_str)(owned.ptr, owned.len) };
209        result
210    }
211
212    fn entity_add_effect(
213        &self,
214        uuid: &str,
215        effect_id: &str,
216        duration_ticks: i32,
217        amplifier: u8,
218        show_particles: bool,
219    ) -> bool {
220        let s = srv!(self);
221        unsafe {
222            (s.entity_add_effect)(
223                s.ctx,
224                YogStr::from_str(uuid),
225                YogStr::from_str(effect_id),
226                duration_ticks,
227                amplifier,
228                show_particles,
229            )
230        }
231    }
232
233    fn entity_remove_effect(&self, uuid: &str, effect_id: &str) -> bool {
234        let s = srv!(self);
235        unsafe {
236            (s.entity_remove_effect)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(effect_id))
237        }
238    }
239
240    fn entity_clear_effects(&self, uuid: &str) -> bool {
241        let s = srv!(self);
242        unsafe { (s.entity_clear_effects)(s.ctx, YogStr::from_str(uuid)) }
243    }
244
245    fn drop_loot(&self, table_id: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
246        let s = srv!(self);
247        unsafe {
248            (s.drop_loot)(
249                s.ctx,
250                YogStr::from_str(table_id),
251                YogStr::from_str(dimension),
252                yog_abi::YogVec3 { x, y, z },
253            )
254        }
255    }
256
257    fn has_item_tag(&self, item_id: &str, tag_id: &str) -> bool {
258        let s = srv!(self);
259        unsafe { (s.has_item_tag)(s.ctx, YogStr::from_str(item_id), YogStr::from_str(tag_id)) }
260    }
261
262    fn has_block_tag(&self, block_id: &str, tag_id: &str) -> bool {
263        let s = srv!(self);
264        unsafe { (s.has_block_tag)(s.ctx, YogStr::from_str(block_id), YogStr::from_str(tag_id)) }
265    }
266
267    fn world_time(&self, dimension: &str) -> Option<i64> {
268        let s = srv!(self);
269        let mut t = 0i64;
270        if unsafe { (s.world_time)(s.ctx, YogStr::from_str(dimension), &mut t) } {
271            Some(t)
272        } else {
273            None
274        }
275    }
276
277    fn world_set_time(&self, dimension: &str, time: i64) -> bool {
278        let s = srv!(self);
279        unsafe { (s.set_time)(s.ctx, YogStr::from_str(dimension), time) }
280    }
281
282    fn world_is_raining(&self, dimension: &str) -> bool {
283        let s = srv!(self);
284        unsafe { (s.is_raining)(s.ctx, YogStr::from_str(dimension)) }
285    }
286
287    fn world_set_weather(&self, dimension: &str, raining: bool, duration_ticks: i32) -> bool {
288        let s = srv!(self);
289        unsafe { (s.set_weather)(s.ctx, YogStr::from_str(dimension), raining, duration_ticks) }
290    }
291
292    fn entity_velocity(&self, uuid: &str) -> Option<(f64, f64, f64)> {
293        let s = srv!(self);
294        let mut v = yog_abi::YogVec3 {
295            x: 0.0,
296            y: 0.0,
297            z: 0.0,
298        };
299        if unsafe { (s.entity_velocity)(s.ctx, YogStr::from_str(uuid), &mut v) } {
300            Some((v.x, v.y, v.z))
301        } else {
302            None
303        }
304    }
305
306    fn entity_set_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
307        let s = srv!(self);
308        unsafe {
309            (s.entity_set_velocity)(
310                s.ctx,
311                YogStr::from_str(uuid),
312                yog_abi::YogVec3 {
313                    x: vx,
314                    y: vy,
315                    z: vz,
316                },
317            )
318        }
319    }
320
321    fn entity_add_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
322        let s = srv!(self);
323        unsafe {
324            (s.entity_add_velocity)(
325                s.ctx,
326                YogStr::from_str(uuid),
327                yog_abi::YogVec3 {
328                    x: vx,
329                    y: vy,
330                    z: vz,
331                },
332            )
333        }
334    }
335
336    fn scoreboard_get(&self, objective: &str, player: &str) -> Option<i32> {
337        let s = srv!(self);
338        let mut score = 0i32;
339        if unsafe {
340            (s.scoreboard_get)(
341                s.ctx,
342                YogStr::from_str(objective),
343                YogStr::from_str(player),
344                &mut score,
345            )
346        } {
347            Some(score)
348        } else {
349            None
350        }
351    }
352
353    fn scoreboard_set(&self, objective: &str, player: &str, score: i32) -> bool {
354        let s = srv!(self);
355        unsafe {
356            (s.scoreboard_set)(
357                s.ctx,
358                YogStr::from_str(objective),
359                YogStr::from_str(player),
360                score,
361            )
362        }
363    }
364
365    fn scoreboard_add(&self, objective: &str, player: &str, delta: i32) -> Option<i32> {
366        let s = srv!(self);
367        let mut new_score = 0i32;
368        if unsafe {
369            (s.scoreboard_add)(
370                s.ctx,
371                YogStr::from_str(objective),
372                YogStr::from_str(player),
373                delta,
374                &mut new_score,
375            )
376        } {
377            Some(new_score)
378        } else {
379            None
380        }
381    }
382
383    fn game_dir(&self) -> String {
384        let s = srv!(self);
385        let owned = unsafe { (s.game_dir)(s.ctx) };
386        if owned.is_none() {
387            return String::new();
388        }
389        let result = unsafe {
390            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
391                .unwrap_or_default()
392        };
393        unsafe { (s.free_str)(owned.ptr, owned.len) };
394        result
395    }
396
397    fn play_sound(
398        &self,
399        dimension: &str,
400        x: f64,
401        y: f64,
402        z: f64,
403        sound_id: &str,
404        volume: f32,
405        pitch: f32,
406    ) -> bool {
407        let s = srv!(self);
408        unsafe {
409            (s.play_sound)(
410                s.ctx,
411                YogStr::from_str(dimension),
412                yog_abi::YogVec3 { x, y, z },
413                YogStr::from_str(sound_id),
414                volume,
415                pitch,
416            )
417        }
418    }
419
420    fn play_sound_to_player(&self, player: &str, sound_id: &str, volume: f32, pitch: f32) -> bool {
421        let s = srv!(self);
422        unsafe {
423            (s.play_sound_player)(
424                s.ctx,
425                YogStr::from_str(player),
426                YogStr::from_str(sound_id),
427                volume,
428                pitch,
429            )
430        }
431    }
432
433    fn send_title(
434        &self,
435        player: &str,
436        title: &str,
437        subtitle: &str,
438        fadein: i32,
439        stay: i32,
440        fadeout: i32,
441    ) -> bool {
442        let s = srv!(self);
443        unsafe {
444            (s.send_title)(
445                s.ctx,
446                YogStr::from_str(player),
447                YogStr::from_str(title),
448                YogStr::from_str(subtitle),
449                fadein,
450                stay,
451                fadeout,
452            )
453        }
454    }
455
456    fn entity_rotation(&self, uuid: &str) -> Option<(f32, f32)> {
457        let s = srv!(self);
458        let mut out = yog_abi::YogVec3 {
459            x: 0.0,
460            y: 0.0,
461            z: 0.0,
462        };
463        let ok = unsafe { (s.entity_rotation)(s.ctx, YogStr::from_str(uuid), &mut out) };
464        if ok {
465            Some((out.x as f32, out.y as f32))
466        } else {
467            None
468        }
469    }
470
471    fn send_actionbar(&self, player: &str, message: &str) -> bool {
472        let s = srv!(self);
473        unsafe { (s.send_actionbar)(s.ctx, YogStr::from_str(player), YogStr::from_str(message)) }
474    }
475
476    fn kick_player(&self, player: &str, reason: &str) -> bool {
477        let s = srv!(self);
478        unsafe { (s.kick_player)(s.ctx, YogStr::from_str(player), YogStr::from_str(reason)) }
479    }
480
481    fn set_gamemode(&self, player: &str, gamemode: &str) -> bool {
482        let s = srv!(self);
483        unsafe { (s.set_gamemode)(s.ctx, YogStr::from_str(player), YogStr::from_str(gamemode)) }
484    }
485
486    fn bossbar_create(&self, id: &str, title: &str, color: &str, style: &str) -> bool {
487        let s = srv!(self);
488        unsafe {
489            (s.bossbar_create)(
490                s.ctx,
491                YogStr::from_str(id),
492                YogStr::from_str(title),
493                YogStr::from_str(color),
494                YogStr::from_str(style),
495            )
496        }
497    }
498
499    fn bossbar_remove(&self, id: &str) -> bool {
500        let s = srv!(self);
501        unsafe { (s.bossbar_remove)(s.ctx, YogStr::from_str(id)) }
502    }
503
504    fn bossbar_set_title(&self, id: &str, title: &str) -> bool {
505        let s = srv!(self);
506        unsafe { (s.bossbar_set_title)(s.ctx, YogStr::from_str(id), YogStr::from_str(title)) }
507    }
508
509    fn bossbar_set_progress(&self, id: &str, progress: f32) -> bool {
510        let s = srv!(self);
511        unsafe { (s.bossbar_set_progress)(s.ctx, YogStr::from_str(id), progress) }
512    }
513
514    fn bossbar_set_color(&self, id: &str, color: &str) -> bool {
515        let s = srv!(self);
516        unsafe { (s.bossbar_set_color)(s.ctx, YogStr::from_str(id), YogStr::from_str(color)) }
517    }
518
519    fn bossbar_add_player(&self, id: &str, player: &str) -> bool {
520        let s = srv!(self);
521        unsafe { (s.bossbar_add_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
522    }
523
524    fn bossbar_remove_player(&self, id: &str, player: &str) -> bool {
525        let s = srv!(self);
526        unsafe { (s.bossbar_remove_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
527    }
528
529    fn bossbar_set_visible(&self, id: &str, visible: bool) -> bool {
530        let s = srv!(self);
531        unsafe { (s.bossbar_set_visible)(s.ctx, YogStr::from_str(id), visible) }
532    }
533
534    fn online_players(&self) -> Vec<String> {
535        let s = srv!(self);
536        let owned = unsafe { (s.online_players)(s.ctx) };
537        if owned.is_none() {
538            return Vec::new();
539        }
540        let text = unsafe {
541            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
542                .unwrap_or_default()
543        };
544        unsafe { (s.free_str)(owned.ptr, owned.len) };
545        if text.is_empty() {
546            Vec::new()
547        } else {
548            text.lines().map(str::to_owned).collect()
549        }
550    }
551
552    fn get_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
553        let s = srv!(self);
554        let owned = unsafe {
555            (s.get_block_nbt)(
556                s.ctx,
557                YogStr::from_str(dimension),
558                yog_abi::YogBlockPos {
559                    x: pos.x,
560                    y: pos.y,
561                    z: pos.z,
562                },
563            )
564        };
565        if owned.is_none() {
566            return None;
567        }
568        let result = unsafe {
569            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
570                .ok()
571        };
572        unsafe { (s.free_str)(owned.ptr, owned.len) };
573        result
574    }
575
576    fn set_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos, snbt: &str) -> bool {
577        let s = srv!(self);
578        unsafe {
579            (s.set_block_nbt)(
580                s.ctx,
581                YogStr::from_str(dimension),
582                yog_abi::YogBlockPos {
583                    x: pos.x,
584                    y: pos.y,
585                    z: pos.z,
586                },
587                YogStr::from_str(snbt),
588            )
589        }
590    }
591
592    fn get_inventory_slot(
593        &self,
594        dimension: &str,
595        pos: yog_core::BlockPos,
596        slot: u32,
597    ) -> Option<(String, u32)> {
598        let s = srv!(self);
599        let owned = unsafe {
600            (s.get_inventory_slot)(
601                s.ctx,
602                YogStr::from_str(dimension),
603                yog_abi::YogBlockPos {
604                    x: pos.x,
605                    y: pos.y,
606                    z: pos.z,
607                },
608                slot,
609            )
610        };
611        if owned.is_none() {
612            return None;
613        }
614        let text = unsafe {
615            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
616                .unwrap_or_default()
617        };
618        unsafe { (s.free_str)(owned.ptr, owned.len) };
619        let (item_id, count) = text.split_once('\t')?;
620        Some((item_id.to_owned(), count.parse().ok()?))
621    }
622
623    fn set_inventory_slot(
624        &self,
625        dimension: &str,
626        pos: yog_core::BlockPos,
627        slot: u32,
628        item_id: &str,
629        count: u32,
630    ) -> bool {
631        let s = srv!(self);
632        unsafe {
633            (s.set_inventory_slot)(
634                s.ctx,
635                YogStr::from_str(dimension),
636                yog_abi::YogBlockPos {
637                    x: pos.x,
638                    y: pos.y,
639                    z: pos.z,
640                },
641                slot,
642                YogStr::from_str(item_id),
643                count,
644            )
645        }
646    }
647
648    fn player_inventory(&self, player: &str) -> Vec<(u32, String, u32)> {
649        let s = srv!(self);
650        let owned = unsafe { (s.player_inventory)(s.ctx, YogStr::from_str(player)) };
651        if owned.is_none() {
652            return Vec::new();
653        }
654        let text = unsafe {
655            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
656                .unwrap_or_default()
657        };
658        unsafe { (s.free_str)(owned.ptr, owned.len) };
659        text.lines()
660            .filter_map(|line| {
661                let mut it = line.split('\t');
662                let slot: u32 = it.next()?.parse().ok()?;
663                let item_id = it.next()?.to_owned();
664                let count: u32 = it.next()?.parse().ok()?;
665                Some((slot, item_id, count))
666            })
667            .collect()
668    }
669
670    fn player_set_slot(&self, player: &str, slot: u32, item_id: &str, count: u32) -> bool {
671        let s = srv!(self);
672        unsafe {
673            (s.player_set_slot)(
674                s.ctx,
675                YogStr::from_str(player),
676                slot,
677                YogStr::from_str(item_id),
678                count,
679            )
680        }
681    }
682
683    fn teleport_to_dim(&self, player: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
684        let s = srv!(self);
685        unsafe {
686            (s.player_teleport_dim)(
687                s.ctx,
688                YogStr::from_str(player),
689                YogStr::from_str(dimension),
690                yog_abi::YogVec3 { x, y, z },
691            )
692        }
693    }
694
695    fn entity_teleport_to_dim(&self, uuid: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
696        let s = srv!(self);
697        unsafe {
698            (s.entity_teleport_dim)(
699                s.ctx,
700                YogStr::from_str(uuid),
701                YogStr::from_str(dimension),
702                yog_abi::YogVec3 { x, y, z },
703            )
704        }
705    }
706
707    fn world_entity_count(&self, dimension: &str, entity_type: &str) -> i32 {
708        let s = srv!(self);
709        unsafe {
710            (s.world_entity_count)(
711                s.ctx,
712                YogStr::from_str(dimension),
713                YogStr::from_str(entity_type),
714            )
715        }
716    }
717
718    fn entity_get_nbt(&self, uuid: &str) -> Option<String> {
719        let s = srv!(self);
720        let owned = unsafe { (s.entity_get_nbt)(s.ctx, YogStr::from_str(uuid)) };
721        if owned.is_none() {
722            return None;
723        }
724        let result = unsafe {
725            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
726                .ok()
727        };
728        unsafe { (s.free_str)(owned.ptr, owned.len) };
729        result
730    }
731
732    fn entity_set_nbt(&self, uuid: &str, snbt: &str) -> bool {
733        let s = srv!(self);
734        unsafe { (s.entity_set_nbt)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(snbt)) }
735    }
736
737    fn spawn_particles(
738        &self,
739        dimension: &str,
740        x: f64,
741        y: f64,
742        z: f64,
743        particle_type: &str,
744        count: i32,
745        dx: f64,
746        dy: f64,
747        dz: f64,
748        speed: f64,
749    ) -> bool {
750        let s = srv!(self);
751        unsafe {
752            (s.spawn_particles)(
753                s.ctx,
754                YogStr::from_str(dimension),
755                yog_abi::YogVec3 { x, y, z },
756                YogStr::from_str(particle_type),
757                count,
758                dx,
759                dy,
760                dz,
761                speed,
762            )
763        }
764    }
765
766    fn entity_attribute_get(&self, uuid: &str, attribute_id: &str) -> Option<f64> {
767        let s = srv!(self);
768        let v = unsafe {
769            (s.entity_attribute_get)(
770                s.ctx,
771                YogStr::from_str(uuid),
772                YogStr::from_str(attribute_id),
773            )
774        };
775        if v.is_nan() {
776            None
777        } else {
778            Some(v)
779        }
780    }
781
782    fn entity_attribute_set(&self, uuid: &str, attribute_id: &str, value: f64) -> bool {
783        let s = srv!(self);
784        unsafe {
785            (s.entity_attribute_set)(
786                s.ctx,
787                YogStr::from_str(uuid),
788                YogStr::from_str(attribute_id),
789                value,
790            )
791        }
792    }
793
794    fn get_held_item_nbt(&self, player: &str) -> Option<String> {
795        let s = srv!(self);
796        let owned = unsafe { (s.get_held_item_nbt)(s.ctx, YogStr::from_str(player)) };
797        if owned.is_none() {
798            return None;
799        }
800        let result = unsafe {
801            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
802                .ok()
803        };
804        unsafe { (s.free_str)(owned.ptr, owned.len) };
805        result
806    }
807
808    fn set_held_item_nbt(&self, player: &str, snbt: &str) -> bool {
809        let s = srv!(self);
810        unsafe { (s.set_held_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
811    }
812
813    fn get_offhand_item_nbt(&self, player: &str) -> Option<String> {
814        let s = srv!(self);
815        let owned = unsafe { (s.get_offhand_item_nbt)(s.ctx, YogStr::from_str(player)) };
816        if owned.is_none() {
817            return None;
818        }
819        let result = unsafe {
820            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
821                .ok()
822        };
823        unsafe { (s.free_str)(owned.ptr, owned.len) };
824        result
825    }
826
827    fn set_offhand_item_nbt(&self, player: &str, snbt: &str) -> bool {
828        let s = srv!(self);
829        unsafe { (s.set_offhand_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
830    }
831
832    fn get_slot_item(&self, player: &str, slot: u32) -> Option<(String, u32, String)> {
833        let s = srv!(self);
834        let owned = unsafe { (s.get_slot_item)(s.ctx, YogStr::from_str(player), slot) };
835        if owned.is_none() {
836            return None;
837        }
838        let text = unsafe {
839            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
840                .unwrap_or_default()
841        };
842        unsafe { (s.free_str)(owned.ptr, owned.len) };
843        let mut it = text.splitn(3, '\t');
844        let item_id = it.next()?.to_owned();
845        let count: u32 = it.next()?.parse().ok()?;
846        let nbt = it.next().unwrap_or("{}").to_owned();
847        Some((item_id, count, nbt))
848    }
849
850    fn set_slot_item(
851        &self,
852        player: &str,
853        slot: u32,
854        item_id: &str,
855        count: u32,
856        snbt: &str,
857    ) -> bool {
858        let s = srv!(self);
859        unsafe {
860            (s.set_slot_item)(
861                s.ctx,
862                YogStr::from_str(player),
863                slot,
864                YogStr::from_str(item_id),
865                count,
866                YogStr::from_str(snbt),
867            )
868        }
869    }
870}
871
872// ── Trampoline helpers ────────────────────────────────────────────────────────
873//
874// Each trampoline is a `unsafe extern "C" fn` that:
875//   1. Casts `ud` back to the original boxed closure.
876//   2. Converts ABI C structs to Rust event types.
877//   3. Builds a `CServer` from the `*const YogServer`.
878//   4. Calls the closure.
879//
880// Closures are Box::into_raw'd in Registry methods — they are INTENTIONALLY
881// leaked and live for the process lifetime.
882
883// ── Trampoline helpers ────────────────────────────────────────────────────────
884//
885// All phased event trampolines share the same pattern:
886//   `phase: u8`  0 = EventPhase::Pre, 1 = EventPhase::Post
887//   Return value is meaningful only in Pre phase.
888
889macro_rules! trampoline_phased {
890    ($name:ident, $abi_ev:ty, $rust_ev:ty, |$ev:ident| $build:expr) => {
891        unsafe extern "C" fn $name<F>(
892            ud: *mut c_void,
893            srv: *const YogServer,
894            ev: *const $abi_ev,
895            phase: u8,
896        ) -> bool
897        where
898            F: Fn(&$rust_ev, EventPhase, &dyn Server) -> bool + Send + Sync,
899        {
900            let f = &*(ud as *const F);
901            let $ev = &*ev;
902            let rust_ev = $build;
903            let p = if phase == 0 {
904                EventPhase::Pre
905            } else {
906                EventPhase::Post
907            };
908            f(&rust_ev, p, &CServer(srv))
909        }
910    };
911}
912
913trampoline_phased!(
914    trampoline_block_break,
915    YogBlockBreakEvent,
916    BlockBreakEvent,
917    |ev| BlockBreakEvent {
918        player_name: ev.player.as_str().to_owned(),
919        block_id: ev.block.as_str().to_owned(),
920        pos: yog_core::BlockPos {
921            x: ev.pos.x,
922            y: ev.pos.y,
923            z: ev.pos.z
924        },
925    }
926);
927
928trampoline_phased!(trampoline_chat, YogChatEvent, ChatEvent, |ev| ChatEvent {
929    player_name: ev.player.as_str().to_owned(),
930    message: ev.message.as_str().to_owned(),
931});
932
933trampoline_phased!(
934    trampoline_player_join,
935    YogPlayerEvent,
936    PlayerJoinEvent,
937    |ev| PlayerJoinEvent {
938        player_name: ev.player.as_str().to_owned(),
939        uuid: ev.uuid.as_str().to_owned(),
940    }
941);
942
943trampoline_phased!(
944    trampoline_player_leave,
945    YogPlayerEvent,
946    PlayerLeaveEvent,
947    |ev| PlayerLeaveEvent {
948        player_name: ev.player.as_str().to_owned(),
949        uuid: ev.uuid.as_str().to_owned(),
950    }
951);
952
953trampoline_phased!(trampoline_use_item, YogUseItemEvent, UseItemEvent, |ev| {
954    UseItemEvent {
955        player_name: ev.player.as_str().to_owned(),
956        item_id: ev.item.as_str().to_owned(),
957        sneaking: ev.sneaking,
958    }
959});
960
961trampoline_phased!(
962    trampoline_use_block,
963    YogUseBlockEvent,
964    UseBlockEvent,
965    |ev| UseBlockEvent {
966        player_name: ev.player.as_str().to_owned(),
967        block_id: ev.block.as_str().to_owned(),
968        pos: yog_core::BlockPos {
969            x: ev.pos.x,
970            y: ev.pos.y,
971            z: ev.pos.z
972        },
973    }
974);
975
976trampoline_phased!(
977    trampoline_attack_entity,
978    YogAttackEntityEvent,
979    AttackEntityEvent,
980    |ev| AttackEntityEvent {
981        player_name: ev.player.as_str().to_owned(),
982        target_type: ev.target_type.as_str().to_owned(),
983        target_uuid: ev.target_uuid.as_str().to_owned(),
984    }
985);
986
987trampoline_phased!(
988    trampoline_entity_damage,
989    YogEntityDamageEvent,
990    EntityDamageEvent,
991    |ev| EntityDamageEvent {
992        entity_type: ev.entity_type.as_str().to_owned(),
993        uuid: ev.uuid.as_str().to_owned(),
994        amount: ev.amount,
995        source: ev.source.as_str().to_owned(),
996    }
997);
998
999trampoline_phased!(
1000    trampoline_entity_death,
1001    YogEntityDeathEvent,
1002    EntityDeathEvent,
1003    |ev| EntityDeathEvent {
1004        entity_type: ev.entity_type.as_str().to_owned(),
1005        uuid: ev.uuid.as_str().to_owned(),
1006        source: ev.source.as_str().to_owned(),
1007    }
1008);
1009
1010trampoline_phased!(
1011    trampoline_entity_spawn,
1012    YogEntitySpawnEvent,
1013    EntitySpawnEvent,
1014    |ev| EntitySpawnEvent {
1015        entity_type: ev.entity_type.as_str().to_owned(),
1016        uuid: ev.uuid.as_str().to_owned(),
1017        dimension: ev.dimension.as_str().to_owned(),
1018    }
1019);
1020
1021trampoline_phased!(
1022    trampoline_place_block,
1023    YogPlaceBlockEvent,
1024    PlaceBlockEvent,
1025    |ev| PlaceBlockEvent {
1026        player_name: ev.player.as_str().to_owned(),
1027        block_id: ev.block.as_str().to_owned(),
1028        pos: yog_core::BlockPos {
1029            x: ev.pos.x,
1030            y: ev.pos.y,
1031            z: ev.pos.z
1032        },
1033    }
1034);
1035
1036trampoline_phased!(
1037    trampoline_player_death,
1038    YogPlayerDeathEvent,
1039    PlayerDeathEvent,
1040    |ev| PlayerDeathEvent {
1041        player_name: ev.player.as_str().to_owned(),
1042        uuid: ev.uuid.as_str().to_owned(),
1043        source: ev.source.as_str().to_owned(),
1044    }
1045);
1046
1047trampoline_phased!(
1048    trampoline_player_respawn,
1049    YogPlayerRespawnEvent,
1050    PlayerRespawnEvent,
1051    |ev| PlayerRespawnEvent {
1052        player_name: ev.player.as_str().to_owned(),
1053        uuid: ev.uuid.as_str().to_owned(),
1054        at_anchor: ev.at_anchor,
1055    }
1056);
1057
1058trampoline_phased!(
1059    trampoline_advancement,
1060    YogAdvancementEvent,
1061    AdvancementEvent,
1062    |ev| AdvancementEvent {
1063        player_name: ev.player.as_str().to_owned(),
1064        uuid: ev.uuid.as_str().to_owned(),
1065        advancement_id: ev.advancement.as_str().to_owned(),
1066    }
1067);
1068
1069trampoline_phased!(
1070    trampoline_entity_interact,
1071    YogEntityInteractEvent,
1072    EntityInteractEvent,
1073    |ev| EntityInteractEvent {
1074        player_name: ev.player.as_str().to_owned(),
1075        player_uuid: ev.player_uuid.as_str().to_owned(),
1076        entity_type: ev.entity_type.as_str().to_owned(),
1077        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1078        hand: ev.hand.as_str().to_owned(),
1079    }
1080);
1081
1082trampoline_phased!(trampoline_craft, YogCraftEvent, CraftEvent, |ev| {
1083    CraftEvent {
1084        player_name: ev.player.as_str().to_owned(),
1085        player_uuid: ev.player_uuid.as_str().to_owned(),
1086        result_item: ev.result_item.as_str().to_owned(),
1087        result_count: ev.result_count,
1088    }
1089});
1090
1091trampoline_phased!(
1092    trampoline_explosion,
1093    YogExplosionEvent,
1094    ExplosionEvent,
1095    |ev| ExplosionEvent {
1096        dimension: ev.dimension.as_str().to_owned(),
1097        x: ev.x,
1098        y: ev.y,
1099        z: ev.z,
1100        power: ev.power,
1101        cause_uuid: ev.cause_uuid.as_str().to_owned(),
1102    }
1103);
1104
1105trampoline_phased!(
1106    trampoline_item_pickup,
1107    YogItemPickupEvent,
1108    ItemPickupEvent,
1109    |ev| ItemPickupEvent {
1110        player_name: ev.player.as_str().to_owned(),
1111        player_uuid: ev.player_uuid.as_str().to_owned(),
1112        item_id: ev.item_id.as_str().to_owned(),
1113        item_count: ev.item_count,
1114        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1115    }
1116);
1117
1118trampoline_phased!(
1119    trampoline_player_move,
1120    YogPlayerMoveEvent,
1121    PlayerMoveEvent,
1122    |ev| PlayerMoveEvent {
1123        player_name: ev.player.as_str().to_owned(),
1124        player_uuid: ev.player_uuid.as_str().to_owned(),
1125        x: ev.x,
1126        y: ev.y,
1127        z: ev.z,
1128        yaw: ev.yaw,
1129        pitch: ev.pitch,
1130    }
1131);
1132
1133trampoline_phased!(
1134    trampoline_container_open,
1135    YogContainerOpenEvent,
1136    ContainerOpenEvent,
1137    |ev| ContainerOpenEvent {
1138        player_name: ev.player.as_str().to_owned(),
1139        player_uuid: ev.player_uuid.as_str().to_owned(),
1140        container_type: ev.container_type.as_str().to_owned(),
1141    }
1142);
1143
1144trampoline_phased!(
1145    trampoline_container_close,
1146    YogContainerCloseEvent,
1147    ContainerCloseEvent,
1148    |ev| ContainerCloseEvent {
1149        player_name: ev.player.as_str().to_owned(),
1150        player_uuid: ev.player_uuid.as_str().to_owned(),
1151    }
1152);
1153
1154trampoline_phased!(
1155    trampoline_projectile_hit,
1156    YogProjectileHitEvent,
1157    ProjectileHitEvent,
1158    |ev| ProjectileHitEvent {
1159        projectile_type: ev.projectile_type.as_str().to_owned(),
1160        projectile_uuid: ev.projectile_uuid.as_str().to_owned(),
1161        shooter_uuid: ev.shooter_uuid.as_str().to_owned(),
1162        hit_type: ev.hit_type.as_str().to_owned(),
1163        hit_entity_uuid: ev.hit_entity_uuid.as_str().to_owned(),
1164        x: ev.x,
1165        y: ev.y,
1166        z: ev.z,
1167        dimension: ev.dimension.as_str().to_owned(),
1168    }
1169);
1170
1171unsafe extern "C" fn trampoline_server_fn<F>(ud: *mut c_void, srv: *const YogServer)
1172where
1173    F: Fn(&dyn Server) + Send + Sync,
1174{
1175    let f = &*(ud as *const F);
1176    f(&CServer(srv));
1177}
1178
1179unsafe extern "C" fn trampoline_packet<F>(
1180    ud: *mut c_void,
1181    srv: *const YogServer,
1182    ev: *const YogPacketEvent,
1183) where
1184    F: Fn(&PacketEvent, &dyn Server) + Send + Sync,
1185{
1186    let f = &*(ud as *const F);
1187    let ev = &*ev;
1188    let rust_ev = PacketEvent {
1189        channel: ev.channel.as_str().to_owned(),
1190        player: ev.player.as_str().to_owned(),
1191        payload: std::slice::from_raw_parts(ev.payload, ev.payload_len as usize).to_vec(),
1192    };
1193    f(&rust_ev, &CServer(srv));
1194}
1195
1196unsafe extern "C" fn trampoline_command<F>(
1197    ud: *mut c_void,
1198    srv: *const YogServer,
1199    ev: *const YogCommandEvent,
1200    reply_buf: *mut u8,
1201    reply_cap: u32,
1202    reply_len: *mut u32,
1203) where
1204    F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync,
1205{
1206    let f = &*(ud as *const F);
1207    let ev = &*ev;
1208    let ctx = CommandContext {
1209        name: ev.name.as_str().to_owned(),
1210        args: ev.args.as_str().to_owned(),
1211        source: ev.source.as_str().to_owned(),
1212        uuid: ev.uuid.as_str().to_owned(),
1213    };
1214    *reply_len = 0;
1215    if let Some(reply) = f(&ctx, &CServer(srv)) {
1216        let bytes = reply.as_bytes();
1217        let n = bytes.len().min(reply_cap as usize);
1218        std::ptr::copy_nonoverlapping(bytes.as_ptr(), reply_buf, n);
1219        *reply_len = n as u32;
1220    }
1221}
1222
1223unsafe extern "C" fn trampoline_scheduled<F>(ud: *mut c_void, srv: *const YogServer)
1224where
1225    F: Fn(&dyn Server) + Send + Sync,
1226{
1227    let f = &*(ud as *const F);
1228    f(&CServer(srv));
1229}
1230
1231// ── ABI minor 10 — client-side trampolines ────────────────────────────────────
1232
1233unsafe extern "C" fn trampoline_client_tick<F>(ud: *mut c_void)
1234where
1235    F: Fn(&ClientTickEvent) + Send + Sync,
1236{
1237    let f = &*(ud as *const F);
1238    f(&ClientTickEvent {});
1239}
1240
1241unsafe extern "C" fn trampoline_hud_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1242where
1243    F: Fn(&GfxContext) + Send + Sync,
1244{
1245    let f = &*(ud as *const F);
1246    f(&GfxContext::from_raw(gfx));
1247}
1248
1249unsafe extern "C" fn trampoline_world_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1250where
1251    F: Fn(&GfxContext) + Send + Sync,
1252{
1253    let f = &*(ud as *const F);
1254    f(&GfxContext::from_raw(gfx));
1255}
1256
1257unsafe extern "C" fn trampoline_key_press<F>(ud: *mut c_void, ev: *const YogKeyPressEvent) -> bool
1258where
1259    F: Fn(&KeyPressEvent) -> bool + Send + Sync,
1260{
1261    let f = &*(ud as *const F);
1262    let e = &*ev;
1263    f(&KeyPressEvent {
1264        key_code: e.key_code,
1265        scan_code: e.scan_code,
1266        action: e.action,
1267        modifiers: e.modifiers,
1268    })
1269}
1270
1271unsafe extern "C" fn trampoline_screen<F>(ud: *mut c_void, screen_class: YogStr) -> bool
1272where
1273    F: Fn(&ScreenEvent) + Send + Sync,
1274{
1275    let f = &*(ud as *const F);
1276    f(&ScreenEvent {
1277        screen_class: screen_class.as_str().to_owned(),
1278    });
1279    true
1280}
1281
1282// ── Registry ─────────────────────────────────────────────────────────────────
1283
1284/// Wraps the [`YogApi`] table and provides an ergonomic registration API.
1285///
1286/// Obtained inside `yog_mod_register` via `export_mod!`.  Closures registered
1287/// here are boxed and leaked — they live as long as the process (which is the
1288/// correct lifetime for a server mod).
1289/// Pointer to the runtime's `YogApi` table (a process-lifetime static in the
1290/// runtime), captured when the mod registers. Lets free functions like
1291/// [`installed_mods`] work outside of `register()` — e.g. in UI handlers.
1292static GLOBAL_API: std::sync::atomic::AtomicPtr<YogApi> =
1293    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
1294
1295/// Metadata of an installed mod, as reported by the loader.
1296#[derive(Debug, Clone)]
1297pub struct ModInfo {
1298    /// `"yog"` for .yog mods, `"platform"` for loader (Java) mods.
1299    pub source: String,
1300    pub id: String,
1301    pub name: String,
1302    pub version: String,
1303    /// Comma-separated author list (may be empty).
1304    pub authors: String,
1305    pub description: String,
1306}
1307
1308/// All installed mods known to the loader: .yog mods plus, where the host
1309/// exposes them, platform (Java) mods. Callable at any time after this mod's
1310/// `register()` ran — including client-side UI handlers. Note that during
1311/// `register()` mods that load after this one are not in the list yet; query
1312/// lazily (e.g. on first render) for a complete view.
1313pub fn installed_mods() -> Vec<ModInfo> {
1314    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1315    if api.is_null() {
1316        return Vec::new();
1317    }
1318    let owned = unsafe { ((*api).mods_list)((*api).ctx) };
1319    if owned.is_none() {
1320        return Vec::new();
1321    }
1322    let text = unsafe {
1323        String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
1324            .unwrap_or_default()
1325    };
1326    unsafe { ((*api).free_str)(owned.ptr, owned.len) };
1327    text.lines()
1328        .filter_map(|line| {
1329            let mut f = line.split('\t');
1330            Some(ModInfo {
1331                source: f.next()?.to_string(),
1332                id: f.next()?.to_string(),
1333                name: f.next().unwrap_or_default().to_string(),
1334                version: f.next().unwrap_or_default().to_string(),
1335                authors: f.next().unwrap_or_default().to_string(),
1336                description: f.next().unwrap_or_default().to_string(),
1337            })
1338        })
1339        .collect()
1340}
1341
1342/// Open the Yog UI registered as `ui_id` (client side; no-op on dedicated
1343/// servers). `modal` blocks game input, `pause` pauses singleplayer. Callable
1344/// from any handler after registration — e.g. an `on_client_packet` handler.
1345pub fn open_ui(ui_id: &str, modal: bool, pause: bool) {
1346    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1347    if api.is_null() {
1348        return;
1349    }
1350    unsafe { ((*api).ui_open)((*api).ctx, YogStr::from_str(ui_id), modal, pause) }
1351}
1352
1353/// Handle to the runtime's server-action table, usable from any handler after
1354/// registration — including client-side ones that don't receive `&dyn Server`
1355/// (UI event handlers, client tick). Server-world actions are no-ops client
1356/// side, but networking (`send_to_server`) and similar client-safe calls work.
1357pub fn server() -> Option<CServer> {
1358    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1359    if api.is_null() {
1360        return None;
1361    }
1362    let srv = unsafe { (*api).server };
1363    if srv.is_null() {
1364        return None;
1365    }
1366    Some(CServer(srv))
1367}
1368
1369pub struct Registry {
1370    api: *const YogApi,
1371}
1372
1373// SAFETY: `api` is a static provided by the runtime, valid for process lifetime.
1374unsafe impl Send for Registry {}
1375unsafe impl Sync for Registry {}
1376
1377impl Registry {
1378    /// Build from the pointer passed by the runtime. Only called by `export_mod!`.
1379    pub unsafe fn from_raw(api: *const YogApi) -> Self {
1380        GLOBAL_API.store(api as *mut YogApi, std::sync::atomic::Ordering::Release);
1381        Self { api }
1382    }
1383
1384    #[inline]
1385    fn ctx(&self) -> *mut c_void {
1386        unsafe { (*self.api).ctx }
1387    }
1388
1389    // ── helpers ──────────────────────────────────────────────────────────────
1390
1391    fn leak<F: 'static>(f: F) -> *mut c_void {
1392        Box::into_raw(Box::new(f)) as *mut c_void
1393    }
1394
1395    // ── events ───────────────────────────────────────────────────────────────
1396    //
1397    // All handlers receive `(event, EventPhase, &dyn Server) -> bool`.
1398    // In `Pre` phase, returning `false` cancels the action.
1399    // In `Post` phase, the return value is ignored.
1400    // A single registration fires for BOTH phases.
1401
1402    pub fn on_block_break<F>(&mut self, handler: F)
1403    where
1404        F: Fn(&BlockBreakEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1405    {
1406        let ud = Self::leak(handler);
1407        unsafe { ((*self.api).on_block_break)(self.ctx(), ud, trampoline_block_break::<F>) }
1408    }
1409
1410    pub fn on_chat<F>(&mut self, handler: F)
1411    where
1412        F: Fn(&ChatEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1413    {
1414        let ud = Self::leak(handler);
1415        unsafe { ((*self.api).on_chat)(self.ctx(), ud, trampoline_chat::<F>) }
1416    }
1417
1418    pub fn on_player_join<F>(&mut self, handler: F)
1419    where
1420        F: Fn(&PlayerJoinEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1421    {
1422        let ud = Self::leak(handler);
1423        unsafe { ((*self.api).on_player_join)(self.ctx(), ud, trampoline_player_join::<F>) }
1424    }
1425
1426    pub fn on_player_leave<F>(&mut self, handler: F)
1427    where
1428        F: Fn(&PlayerLeaveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1429    {
1430        let ud = Self::leak(handler);
1431        unsafe { ((*self.api).on_player_leave)(self.ctx(), ud, trampoline_player_leave::<F>) }
1432    }
1433
1434    pub fn on_use_item<F>(&mut self, handler: F)
1435    where
1436        F: Fn(&UseItemEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1437    {
1438        let ud = Self::leak(handler);
1439        unsafe { ((*self.api).on_use_item)(self.ctx(), ud, trampoline_use_item::<F>) }
1440    }
1441
1442    pub fn on_use_block<F>(&mut self, handler: F)
1443    where
1444        F: Fn(&UseBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1445    {
1446        let ud = Self::leak(handler);
1447        unsafe { ((*self.api).on_use_block)(self.ctx(), ud, trampoline_use_block::<F>) }
1448    }
1449
1450    pub fn on_attack_entity<F>(&mut self, handler: F)
1451    where
1452        F: Fn(&AttackEntityEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1453    {
1454        let ud = Self::leak(handler);
1455        unsafe { ((*self.api).on_attack_entity)(self.ctx(), ud, trampoline_attack_entity::<F>) }
1456    }
1457
1458    pub fn on_entity_damage<F>(&mut self, handler: F)
1459    where
1460        F: Fn(&EntityDamageEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1461    {
1462        let ud = Self::leak(handler);
1463        unsafe { ((*self.api).on_entity_damage)(self.ctx(), ud, trampoline_entity_damage::<F>) }
1464    }
1465
1466    pub fn on_entity_death<F>(&mut self, handler: F)
1467    where
1468        F: Fn(&EntityDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1469    {
1470        let ud = Self::leak(handler);
1471        unsafe { ((*self.api).on_entity_death)(self.ctx(), ud, trampoline_entity_death::<F>) }
1472    }
1473
1474    pub fn on_entity_spawn<F>(&mut self, handler: F)
1475    where
1476        F: Fn(&EntitySpawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1477    {
1478        let ud = Self::leak(handler);
1479        unsafe { ((*self.api).on_entity_spawn)(self.ctx(), ud, trampoline_entity_spawn::<F>) }
1480    }
1481
1482    pub fn on_player_place_block<F>(&mut self, handler: F)
1483    where
1484        F: Fn(&PlaceBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1485    {
1486        let ud = Self::leak(handler);
1487        unsafe { ((*self.api).on_player_place_block)(self.ctx(), ud, trampoline_place_block::<F>) }
1488    }
1489
1490    pub fn on_player_death<F>(&mut self, handler: F)
1491    where
1492        F: Fn(&PlayerDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1493    {
1494        let ud = Self::leak(handler);
1495        unsafe { ((*self.api).on_player_death)(self.ctx(), ud, trampoline_player_death::<F>) }
1496    }
1497
1498    pub fn on_player_respawn<F>(&mut self, handler: F)
1499    where
1500        F: Fn(&PlayerRespawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1501    {
1502        let ud = Self::leak(handler);
1503        unsafe { ((*self.api).on_player_respawn)(self.ctx(), ud, trampoline_player_respawn::<F>) }
1504    }
1505
1506    pub fn on_advancement<F>(&mut self, handler: F)
1507    where
1508        F: Fn(&AdvancementEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1509    {
1510        let ud = Self::leak(handler);
1511        unsafe { ((*self.api).on_advancement)(self.ctx(), ud, trampoline_advancement::<F>) }
1512    }
1513
1514    pub fn on_entity_interact<F>(&mut self, handler: F)
1515    where
1516        F: Fn(&EntityInteractEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1517    {
1518        let ud = Self::leak(handler);
1519        unsafe { ((*self.api).on_entity_interact)(self.ctx(), ud, trampoline_entity_interact::<F>) }
1520    }
1521
1522    pub fn on_item_craft<F>(&mut self, handler: F)
1523    where
1524        F: Fn(&CraftEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1525    {
1526        let ud = Self::leak(handler);
1527        unsafe { ((*self.api).on_item_craft)(self.ctx(), ud, trampoline_craft::<F>) }
1528    }
1529
1530    pub fn on_explosion<F>(&mut self, handler: F)
1531    where
1532        F: Fn(&ExplosionEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1533    {
1534        let ud = Self::leak(handler);
1535        unsafe { ((*self.api).on_explosion)(self.ctx(), ud, trampoline_explosion::<F>) }
1536    }
1537
1538    pub fn on_item_pickup<F>(&mut self, handler: F)
1539    where
1540        F: Fn(&ItemPickupEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1541    {
1542        let ud = Self::leak(handler);
1543        unsafe { ((*self.api).on_item_pickup)(self.ctx(), ud, trampoline_item_pickup::<F>) }
1544    }
1545
1546    pub fn on_player_move<F>(&mut self, handler: F)
1547    where
1548        F: Fn(&PlayerMoveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1549    {
1550        let ud = Self::leak(handler);
1551        unsafe { ((*self.api).on_player_move)(self.ctx(), ud, trampoline_player_move::<F>) }
1552    }
1553
1554    pub fn on_container_open<F>(&mut self, handler: F)
1555    where
1556        F: Fn(&ContainerOpenEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1557    {
1558        let ud = Self::leak(handler);
1559        unsafe { ((*self.api).on_container_open)(self.ctx(), ud, trampoline_container_open::<F>) }
1560    }
1561
1562    pub fn on_container_close<F>(&mut self, handler: F)
1563    where
1564        F: Fn(&ContainerCloseEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1565    {
1566        let ud = Self::leak(handler);
1567        unsafe { ((*self.api).on_container_close)(self.ctx(), ud, trampoline_container_close::<F>) }
1568    }
1569
1570    pub fn on_projectile_hit<F>(&mut self, handler: F)
1571    where
1572        F: Fn(&ProjectileHitEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1573    {
1574        let ud = Self::leak(handler);
1575        unsafe { ((*self.api).on_projectile_hit)(self.ctx(), ud, trampoline_projectile_hit::<F>) }
1576    }
1577
1578    pub fn on_tick<F>(&mut self, listener: F)
1579    where
1580        F: Fn(&dyn Server) + Send + Sync + 'static,
1581    {
1582        let ud = Self::leak(listener);
1583        unsafe { ((*self.api).on_server_tick)(self.ctx(), ud, trampoline_server_fn::<F>) }
1584    }
1585
1586    pub fn on_server_started<F>(&mut self, listener: F)
1587    where
1588        F: Fn(&dyn Server) + Send + Sync + 'static,
1589    {
1590        let ud = Self::leak(listener);
1591        unsafe { ((*self.api).on_server_started)(self.ctx(), ud, trampoline_server_fn::<F>) }
1592    }
1593
1594    pub fn on_server_stopping<F>(&mut self, listener: F)
1595    where
1596        F: Fn(&dyn Server) + Send + Sync + 'static,
1597    {
1598        let ud = Self::leak(listener);
1599        unsafe { ((*self.api).on_server_stopping)(self.ctx(), ud, trampoline_server_fn::<F>) }
1600    }
1601
1602    // ── commands ─────────────────────────────────────────────────────────────
1603
1604    pub fn on_command<F>(&mut self, name: impl AsRef<str>, handler: F)
1605    where
1606        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
1607    {
1608        let name_ys = YogStr::from_str(name.as_ref());
1609        let ud = Self::leak(handler);
1610        unsafe { ((*self.api).register_command)(self.ctx(), name_ys, ud, trampoline_command::<F>) }
1611    }
1612
1613    /// Register a command with Brigadier-typed arguments.
1614    ///
1615    /// `schema` is a space-separated list of argument types:
1616    /// `int`, `float`, `word`, `string` (greedy, must be last), `player`, `blockpos`.
1617    ///
1618    /// In the handler use `ctx.arg_int(0)`, `ctx.arg_blockpos(1)`, etc.
1619    pub fn on_typed_command<F>(
1620        &mut self,
1621        name: impl AsRef<str>,
1622        schema: impl AsRef<str>,
1623        handler: F,
1624    ) where
1625        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
1626    {
1627        let name_ys = YogStr::from_str(name.as_ref());
1628        let schema_ys = YogStr::from_str(schema.as_ref());
1629        let ud = Self::leak(handler);
1630        unsafe {
1631            ((*self.api).register_typed_command)(
1632                self.ctx(),
1633                name_ys,
1634                schema_ys,
1635                ud,
1636                trampoline_command::<F>,
1637            )
1638        }
1639    }
1640
1641    // ── networking ───────────────────────────────────────────────────────────
1642
1643    pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1644    where
1645        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
1646    {
1647        let ch = YogStr::from_str(channel.as_ref());
1648        let ud = Self::leak(handler);
1649        unsafe { ((*self.api).on_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1650    }
1651
1652    pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1653    where
1654        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
1655    {
1656        let ch = YogStr::from_str(channel.as_ref());
1657        let ud = Self::leak(handler);
1658        unsafe { ((*self.api).on_client_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1659    }
1660
1661    /// Register a typed-packet handler.
1662    ///
1663    /// The payload is decoded from raw bytes using `P`'s [`Packet`] impl.
1664    /// Malformed payloads are silently dropped.
1665    pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
1666    where
1667        P: Packet + Send + Sync + 'static,
1668        F: Fn(&P, &dyn Server) + Send + Sync + 'static,
1669    {
1670        self.on_packet(channel, move |ev, srv| {
1671            if let Some(pkt) = P::decode(&ev.payload) {
1672                handler(&pkt, srv);
1673            }
1674        });
1675    }
1676
1677    // ── recipes ──────────────────────────────────────────────────────────────
1678
1679    fn recipe(&mut self, ns: &str, name: &str, json: &str) {
1680        unsafe {
1681            ((*self.api).register_recipe_json)(
1682                self.ctx(),
1683                YogStr::from_str(ns),
1684                YogStr::from_str(name),
1685                YogStr::from_str(json),
1686            )
1687        }
1688    }
1689
1690    /// Register a shaped crafting recipe.
1691    pub fn add_shaped_recipe(&mut self, recipe: ShapedRecipe) {
1692        let json = recipe.to_json();
1693        let (ns, name) = recipe.ns_name();
1694        self.recipe(ns, name, &json);
1695    }
1696
1697    /// Register a shapeless crafting recipe.
1698    pub fn add_shapeless_recipe(&mut self, recipe: ShapelessRecipe) {
1699        let json = recipe.to_json();
1700        let (ns, name) = recipe.ns_name();
1701        self.recipe(ns, name, &json);
1702    }
1703
1704    /// Register a furnace smelting recipe.
1705    pub fn add_furnace_recipe(&mut self, recipe: FurnaceRecipe) {
1706        let json = recipe.to_json();
1707        let (ns, name) = recipe.ns_name();
1708        self.recipe(ns, name, &json);
1709    }
1710
1711    // ── content ──────────────────────────────────────────────────────────────
1712
1713    pub fn register_item(&mut self, def: ItemDef) {
1714        // Build a C-compatible YogItemDef whose YogStr fields point into `def`'s
1715        // String storage.  We then call register_item which must copy the data
1716        // before returning (the runtime stores owned Strings).
1717        let food_nutrition = def.food.as_ref().map_or(0, |f| f.nutrition);
1718        let food_saturation = def.food.as_ref().map_or(0.0, |f| f.saturation);
1719        let food_always_eat = def.food.as_ref().map_or(false, |f| f.can_always_eat);
1720        let c = YogItemDef {
1721            id: YogStr::from_str(&def.id),
1722            max_stack: def.max_stack as u32,
1723            name: def
1724                .name
1725                .as_deref()
1726                .map(YogStr::from_str)
1727                .unwrap_or(YogStr::EMPTY),
1728            tooltip: def
1729                .tooltip
1730                .as_deref()
1731                .map(YogStr::from_str)
1732                .unwrap_or(YogStr::EMPTY),
1733            max_damage: def.max_damage,
1734            fire_resistant: def.fire_resistant,
1735            fuel_ticks: def.fuel_ticks,
1736            food_nutrition,
1737            food_saturation,
1738            food_always_eat,
1739        };
1740        unsafe { ((*self.api).register_item)(self.ctx(), &c) }
1741    }
1742
1743    pub fn register_block(&mut self, def: BlockDef) {
1744        let shape = def.shape.unwrap_or([0.0; 6]);
1745        let groups_joined = def.connect_groups.join(",");
1746        let c = YogBlockDef {
1747            id: YogStr::from_str(&def.id),
1748            hardness: def.hardness,
1749            resistance: def.resistance,
1750            name: def
1751                .name
1752                .as_deref()
1753                .map(YogStr::from_str)
1754                .unwrap_or(YogStr::EMPTY),
1755            light_level: def.light_level,
1756            sound: def
1757                .sound
1758                .as_deref()
1759                .map(YogStr::from_str)
1760                .unwrap_or(YogStr::EMPTY),
1761            requires_tool: def.requires_tool,
1762            no_collision: def.no_collision,
1763            slipperiness: def.slipperiness,
1764            shape,
1765            connects: def.connects,
1766            connect_groups: YogStr::from_str(&groups_joined),
1767            inventory_id: def
1768                .inventory_id
1769                .as_deref()
1770                .map(YogStr::from_str)
1771                .unwrap_or(YogStr::EMPTY),
1772        };
1773        unsafe { ((*self.api).register_block)(self.ctx(), &c) }
1774    }
1775
1776    /// Register a real Container/Menu inventory screen — see
1777    /// `yog_inventory::InventoryDef`. Attach it to a block via
1778    /// `BlockDef::inventory(id)`.
1779    pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
1780        let layout = yog_inventory::encode_layout(&def.resolved_layout());
1781        let c = YogInventoryDef {
1782            id: YogStr::from_str(&def.id),
1783            slot_count: def.slot_count as u32,
1784            layout: YogStr::from_str(&layout),
1785            include_player_inventory: def.include_player_inventory,
1786            player_inv_x: def.player_inv_offset.0,
1787            player_inv_y: def.player_inv_offset.1,
1788            background_texture: def
1789                .background_texture
1790                .as_deref()
1791                .map(YogStr::from_str)
1792                .unwrap_or(YogStr::EMPTY),
1793            title: YogStr::from_str(&def.title),
1794        };
1795        unsafe { ((*self.api).register_inventory)(self.ctx(), &c) }
1796    }
1797
1798    // ── startup grants ───────────────────────────────────────────────────────
1799
1800    /// Register a startup grant: items/books to give once when a player first joins.
1801    pub fn register_startup_grant(&mut self, grant: StartupGrant) {
1802        let items_str = grant.items.join("|");
1803        let c = YogStartupGrantDef {
1804            id: YogStr::from_str(&grant.id),
1805            items: YogStr::from_str(&items_str),
1806            book: grant
1807                .book
1808                .as_deref()
1809                .map(YogStr::from_str)
1810                .unwrap_or(YogStr::EMPTY),
1811            command: grant
1812                .command
1813                .as_deref()
1814                .map(YogStr::from_str)
1815                .unwrap_or(YogStr::EMPTY),
1816        };
1817        unsafe { ((*self.api).register_startup_grant)(self.ctx(), &c) }
1818    }
1819
1820    /// Register a book with its JSON-serialized content.
1821    pub fn register_book(&mut self, book: &Book) {
1822        let json = book.to_json();
1823        let id = YogStr::from_str(&book.id);
1824        let j = YogStr::from_str(&json);
1825        unsafe { ((*self.api).register_book)(self.ctx(), id, j) }
1826    }
1827
1828    /// Register a menu entry — the host renders a button on vanilla screens
1829    /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
1830    /// `label` is the human-readable button text (e.g. "Yog Mods").
1831    /// `ui_id` is the UI to open when clicked (e.g. "yog:modlist").
1832    /// See [`installed_mods`]. During `register()` the list only contains
1833    /// mods loaded before this one.
1834    pub fn installed_mods(&self) -> Vec<ModInfo> {
1835        installed_mods()
1836    }
1837
1838    /// Access the inter-mod communication layer.
1839    ///
1840    /// Allows exporting function pointers from this mod and importing
1841    /// function pointers from other mods.
1842    pub fn interop(&self) -> crate::Interop {
1843        crate::Interop::new(self.api)
1844    }
1845
1846    /// Raw `YogApi` pointer — for passing to interop-imported functions
1847    /// that register content through the C ABI table directly.
1848    pub fn raw_api(&self) -> *const yog_abi::YogApi {
1849        self.api
1850    }
1851
1852    pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
1853        let l = YogStr::from_str(label);
1854        let u = YogStr::from_str(ui_id);
1855        unsafe { ((*self.api).register_menu_entry)(self.ctx(), l, u) }
1856    }
1857
1858    /// Register a UI tree with an event callback.
1859    /// `ui_id` is the unique identifier (e.g. "mymod:menu").
1860    /// `handler` receives `(ui_id, event_id)` when a widget is clicked.
1861    pub fn register_ui<F>(&mut self, ui_id: &str, handler: F)
1862    where
1863        F: Fn(&str, &str) + Send + Sync + 'static,
1864    {
1865        let ud = Self::leak(handler);
1866        let id = YogStr::from_str(ui_id);
1867        let empty = YogStr::from_str("{}");
1868        unsafe { ((*self.api).register_ui)(self.ctx(), id, empty, ud, trampoline_ui_event::<F>) }
1869    }
1870
1871    // ── scheduler ────────────────────────────────────────────────────────────
1872
1873    pub fn schedule_once<F>(&self, delay_ticks: u64, handler: F)
1874    where
1875        F: Fn(&dyn Server) + Send + Sync + 'static,
1876    {
1877        let ud = Self::leak(handler);
1878        unsafe {
1879            ((*self.api).schedule_once)(self.ctx(), delay_ticks, ud, trampoline_scheduled::<F>)
1880        }
1881    }
1882
1883    pub fn schedule_repeating<F>(&self, period_ticks: u64, handler: F)
1884    where
1885        F: Fn(&dyn Server) + Send + Sync + 'static,
1886    {
1887        let ud = Self::leak(handler);
1888        unsafe {
1889            ((*self.api).schedule_repeating)(
1890                self.ctx(),
1891                period_ticks,
1892                ud,
1893                trampoline_scheduled::<F>,
1894            )
1895        }
1896    }
1897
1898    // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
1899
1900    /// Register a handler called every client tick (render thread, no server).
1901    pub fn on_client_tick<F>(&mut self, handler: F)
1902    where
1903        F: Fn(&ClientTickEvent) + Send + Sync + 'static,
1904    {
1905        let ud = Self::leak(handler);
1906        unsafe { ((*self.api).on_client_tick)(self.ctx(), ud, trampoline_client_tick::<F>) }
1907    }
1908
1909    /// Register a render callback for a specific UI screen (`ui_id`).
1910    ///
1911    /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
1912    /// the screen — so your UI draws on top of the dimmed world view.
1913    /// Use this instead of `on_hud_render` for book/inventory screens.
1914    ///
1915    /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
1916    /// can do hit-testing on your own stored layout.
1917    pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
1918    where
1919        F: Fn(&GfxContext) + Send + Sync + 'static,
1920    {
1921        let ud = Self::leak(handler);
1922        let id = YogStr::from_str(ui_id);
1923        unsafe { ((*self.api).on_ui_render)(self.ctx(), id, ud, trampoline_hud_render::<F>) }
1924    }
1925
1926    /// Register a handler called every frame when the HUD is rendered.
1927    ///
1928    /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
1929    /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
1930    pub fn on_hud_render<F>(&mut self, handler: F)
1931    where
1932        F: Fn(&GfxContext) + Send + Sync + 'static,
1933    {
1934        let ud = Self::leak(handler);
1935        unsafe { ((*self.api).on_hud_render)(self.ctx(), ud, trampoline_hud_render::<F>) }
1936    }
1937
1938    /// Register a handler called every frame at the end of world rendering.
1939    ///
1940    /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
1941    /// `gfx.camera_pos()` is the camera world position.
1942    /// To render at world position P, translate by `P - camera_pos` before drawing.
1943    pub fn on_world_render<F>(&mut self, handler: F)
1944    where
1945        F: Fn(&GfxContext) + Send + Sync + 'static,
1946    {
1947        let ud = Self::leak(handler);
1948        unsafe { ((*self.api).on_world_render)(self.ctx(), ud, trampoline_world_render::<F>) }
1949    }
1950
1951    /// Register a handler for keyboard input (client-side).
1952    /// Return `false` to prevent Minecraft from processing the key.
1953    pub fn on_key_press<F>(&mut self, handler: F)
1954    where
1955        F: Fn(&KeyPressEvent) -> bool + Send + Sync + 'static,
1956    {
1957        let ud = Self::leak(handler);
1958        unsafe { ((*self.api).on_key_press)(self.ctx(), ud, trampoline_key_press::<F>) }
1959    }
1960
1961    /// Register a handler called when a GUI screen is opened.
1962    pub fn on_screen_open<F>(&mut self, handler: F)
1963    where
1964        F: Fn(&ScreenEvent) + Send + Sync + 'static,
1965    {
1966        let ud = Self::leak(handler);
1967        unsafe { ((*self.api).on_screen_open)(self.ctx(), ud, trampoline_screen::<F>) }
1968    }
1969
1970    /// Register a handler called when a GUI screen is closed.
1971    pub fn on_screen_close<F>(&mut self, handler: F)
1972    where
1973        F: Fn(&ScreenEvent) + Send + Sync + 'static,
1974    {
1975        let ud = Self::leak(handler);
1976        unsafe { ((*self.api).on_screen_close)(self.ctx(), ud, trampoline_screen::<F>) }
1977    }
1978}
1979
1980unsafe extern "C" fn trampoline_ui_event<F>(
1981    ud: *mut c_void,
1982    ui_id: yog_abi::YogStr,
1983    event_id: yog_abi::YogStr,
1984) where
1985    F: Fn(&str, &str) + Send + Sync + 'static,
1986{
1987    let f = &*(ud as *const F);
1988    let ui = unsafe { ui_id.as_str() };
1989    let ev = unsafe { event_id.as_str() };
1990    f(ui, ev);
1991}
1992
1993// ── Mod trait ─────────────────────────────────────────────────────────────────
1994
1995/// Implemented by every Yog mod. Called once at startup to register handlers.
1996pub trait Mod {
1997    fn register(registry: &mut Registry);
1998}