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        dimension: ev.dimension.as_str().to_owned(),
926    }
927);
928
929trampoline_phased!(trampoline_chat, YogChatEvent, ChatEvent, |ev| ChatEvent {
930    player_name: ev.player.as_str().to_owned(),
931    message: ev.message.as_str().to_owned(),
932    dimension: ev.dimension.as_str().to_owned(),
933});
934
935trampoline_phased!(
936    trampoline_player_join,
937    YogPlayerEvent,
938    PlayerJoinEvent,
939    |ev| PlayerJoinEvent {
940        player_name: ev.player.as_str().to_owned(),
941        uuid: ev.uuid.as_str().to_owned(),
942        dimension: ev.dimension.as_str().to_owned(),
943    }
944);
945
946trampoline_phased!(
947    trampoline_player_leave,
948    YogPlayerEvent,
949    PlayerLeaveEvent,
950    |ev| PlayerLeaveEvent {
951        player_name: ev.player.as_str().to_owned(),
952        uuid: ev.uuid.as_str().to_owned(),
953        dimension: ev.dimension.as_str().to_owned(),
954    }
955);
956
957trampoline_phased!(trampoline_use_item, YogUseItemEvent, UseItemEvent, |ev| {
958    UseItemEvent {
959        player_name: ev.player.as_str().to_owned(),
960        item_id: ev.item.as_str().to_owned(),
961        sneaking: ev.sneaking,
962        dimension: ev.dimension.as_str().to_owned(),
963    }
964});
965
966trampoline_phased!(
967    trampoline_use_block,
968    YogUseBlockEvent,
969    UseBlockEvent,
970    |ev| UseBlockEvent {
971        player_name: ev.player.as_str().to_owned(),
972        block_id: ev.block.as_str().to_owned(),
973        pos: yog_core::BlockPos {
974            x: ev.pos.x,
975            y: ev.pos.y,
976            z: ev.pos.z
977        },
978        dimension: ev.dimension.as_str().to_owned(),
979    }
980);
981
982trampoline_phased!(
983    trampoline_attack_entity,
984    YogAttackEntityEvent,
985    AttackEntityEvent,
986    |ev| AttackEntityEvent {
987        player_name: ev.player.as_str().to_owned(),
988        target_type: ev.target_type.as_str().to_owned(),
989        target_uuid: ev.target_uuid.as_str().to_owned(),
990        dimension: ev.dimension.as_str().to_owned(),
991    }
992);
993
994trampoline_phased!(
995    trampoline_entity_damage,
996    YogEntityDamageEvent,
997    EntityDamageEvent,
998    |ev| EntityDamageEvent {
999        entity_type: ev.entity_type.as_str().to_owned(),
1000        uuid: ev.uuid.as_str().to_owned(),
1001        amount: ev.amount,
1002        source: ev.source.as_str().to_owned(),
1003        dimension: ev.dimension.as_str().to_owned(),
1004    }
1005);
1006
1007trampoline_phased!(
1008    trampoline_entity_death,
1009    YogEntityDeathEvent,
1010    EntityDeathEvent,
1011    |ev| EntityDeathEvent {
1012        entity_type: ev.entity_type.as_str().to_owned(),
1013        uuid: ev.uuid.as_str().to_owned(),
1014        source: ev.source.as_str().to_owned(),
1015        dimension: ev.dimension.as_str().to_owned(),
1016    }
1017);
1018
1019trampoline_phased!(
1020    trampoline_entity_spawn,
1021    YogEntitySpawnEvent,
1022    EntitySpawnEvent,
1023    |ev| EntitySpawnEvent {
1024        entity_type: ev.entity_type.as_str().to_owned(),
1025        uuid: ev.uuid.as_str().to_owned(),
1026        dimension: ev.dimension.as_str().to_owned(),
1027    }
1028);
1029
1030trampoline_phased!(
1031    trampoline_place_block,
1032    YogPlaceBlockEvent,
1033    PlaceBlockEvent,
1034    |ev| PlaceBlockEvent {
1035        player_name: ev.player.as_str().to_owned(),
1036        block_id: ev.block.as_str().to_owned(),
1037        pos: yog_core::BlockPos {
1038            x: ev.pos.x,
1039            y: ev.pos.y,
1040            z: ev.pos.z
1041        },
1042        dimension: ev.dimension.as_str().to_owned(),
1043    }
1044);
1045
1046trampoline_phased!(
1047    trampoline_player_death,
1048    YogPlayerDeathEvent,
1049    PlayerDeathEvent,
1050    |ev| PlayerDeathEvent {
1051        player_name: ev.player.as_str().to_owned(),
1052        uuid: ev.uuid.as_str().to_owned(),
1053        source: ev.source.as_str().to_owned(),
1054        dimension: ev.dimension.as_str().to_owned(),
1055    }
1056);
1057
1058trampoline_phased!(
1059    trampoline_player_respawn,
1060    YogPlayerRespawnEvent,
1061    PlayerRespawnEvent,
1062    |ev| PlayerRespawnEvent {
1063        player_name: ev.player.as_str().to_owned(),
1064        uuid: ev.uuid.as_str().to_owned(),
1065        at_anchor: ev.at_anchor,
1066        dimension: ev.dimension.as_str().to_owned(),
1067    }
1068);
1069
1070trampoline_phased!(
1071    trampoline_advancement,
1072    YogAdvancementEvent,
1073    AdvancementEvent,
1074    |ev| AdvancementEvent {
1075        player_name: ev.player.as_str().to_owned(),
1076        uuid: ev.uuid.as_str().to_owned(),
1077        advancement_id: ev.advancement.as_str().to_owned(),
1078        dimension: ev.dimension.as_str().to_owned(),
1079    }
1080);
1081
1082trampoline_phased!(
1083    trampoline_entity_interact,
1084    YogEntityInteractEvent,
1085    EntityInteractEvent,
1086    |ev| EntityInteractEvent {
1087        player_name: ev.player.as_str().to_owned(),
1088        player_uuid: ev.player_uuid.as_str().to_owned(),
1089        entity_type: ev.entity_type.as_str().to_owned(),
1090        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1091        hand: ev.hand.as_str().to_owned(),
1092        dimension: ev.dimension.as_str().to_owned(),
1093    }
1094);
1095
1096trampoline_phased!(trampoline_craft, YogCraftEvent, CraftEvent, |ev| {
1097    CraftEvent {
1098        player_name: ev.player.as_str().to_owned(),
1099        player_uuid: ev.player_uuid.as_str().to_owned(),
1100        result_item: ev.result_item.as_str().to_owned(),
1101        result_count: ev.result_count,
1102        dimension: ev.dimension.as_str().to_owned(),
1103    }
1104});
1105
1106trampoline_phased!(
1107    trampoline_explosion,
1108    YogExplosionEvent,
1109    ExplosionEvent,
1110    |ev| ExplosionEvent {
1111        dimension: ev.dimension.as_str().to_owned(),
1112        x: ev.x,
1113        y: ev.y,
1114        z: ev.z,
1115        power: ev.power,
1116        cause_uuid: ev.cause_uuid.as_str().to_owned(),
1117    }
1118);
1119
1120trampoline_phased!(
1121    trampoline_item_pickup,
1122    YogItemPickupEvent,
1123    ItemPickupEvent,
1124    |ev| ItemPickupEvent {
1125        player_name: ev.player.as_str().to_owned(),
1126        player_uuid: ev.player_uuid.as_str().to_owned(),
1127        item_id: ev.item_id.as_str().to_owned(),
1128        item_count: ev.item_count,
1129        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1130        dimension: ev.dimension.as_str().to_owned(),
1131    }
1132);
1133
1134trampoline_phased!(
1135    trampoline_player_move,
1136    YogPlayerMoveEvent,
1137    PlayerMoveEvent,
1138    |ev| PlayerMoveEvent {
1139        player_name: ev.player.as_str().to_owned(),
1140        player_uuid: ev.player_uuid.as_str().to_owned(),
1141        x: ev.x,
1142        y: ev.y,
1143        z: ev.z,
1144        yaw: ev.yaw,
1145        pitch: ev.pitch,
1146        dimension: ev.dimension.as_str().to_owned(),
1147    }
1148);
1149
1150trampoline_phased!(
1151    trampoline_container_open,
1152    YogContainerOpenEvent,
1153    ContainerOpenEvent,
1154    |ev| ContainerOpenEvent {
1155        player_name: ev.player.as_str().to_owned(),
1156        player_uuid: ev.player_uuid.as_str().to_owned(),
1157        container_type: ev.container_type.as_str().to_owned(),
1158        dimension: ev.dimension.as_str().to_owned(),
1159    }
1160);
1161
1162trampoline_phased!(
1163    trampoline_container_close,
1164    YogContainerCloseEvent,
1165    ContainerCloseEvent,
1166    |ev| ContainerCloseEvent {
1167        player_name: ev.player.as_str().to_owned(),
1168        player_uuid: ev.player_uuid.as_str().to_owned(),
1169        dimension: ev.dimension.as_str().to_owned(),
1170    }
1171);
1172
1173trampoline_phased!(
1174    trampoline_projectile_hit,
1175    YogProjectileHitEvent,
1176    ProjectileHitEvent,
1177    |ev| ProjectileHitEvent {
1178        projectile_type: ev.projectile_type.as_str().to_owned(),
1179        projectile_uuid: ev.projectile_uuid.as_str().to_owned(),
1180        shooter_uuid: ev.shooter_uuid.as_str().to_owned(),
1181        hit_type: ev.hit_type.as_str().to_owned(),
1182        hit_entity_uuid: ev.hit_entity_uuid.as_str().to_owned(),
1183        x: ev.x,
1184        y: ev.y,
1185        z: ev.z,
1186        dimension: ev.dimension.as_str().to_owned(),
1187    }
1188);
1189
1190unsafe extern "C" fn trampoline_server_fn<F>(ud: *mut c_void, srv: *const YogServer)
1191where
1192    F: Fn(&dyn Server) + Send + Sync,
1193{
1194    let f = &*(ud as *const F);
1195    f(&CServer(srv));
1196}
1197
1198unsafe extern "C" fn trampoline_packet<F>(
1199    ud: *mut c_void,
1200    srv: *const YogServer,
1201    ev: *const YogPacketEvent,
1202) where
1203    F: Fn(&PacketEvent, &dyn Server) + Send + Sync,
1204{
1205    let f = &*(ud as *const F);
1206    let ev = &*ev;
1207    let rust_ev = PacketEvent {
1208        channel: ev.channel.as_str().to_owned(),
1209        player: ev.player.as_str().to_owned(),
1210        payload: std::slice::from_raw_parts(ev.payload, ev.payload_len as usize).to_vec(),
1211    };
1212    f(&rust_ev, &CServer(srv));
1213}
1214
1215unsafe extern "C" fn trampoline_command<F>(
1216    ud: *mut c_void,
1217    srv: *const YogServer,
1218    ev: *const YogCommandEvent,
1219    reply_buf: *mut u8,
1220    reply_cap: u32,
1221    reply_len: *mut u32,
1222) where
1223    F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync,
1224{
1225    let f = &*(ud as *const F);
1226    let ev = &*ev;
1227    let ctx = CommandContext {
1228        name: ev.name.as_str().to_owned(),
1229        args: ev.args.as_str().to_owned(),
1230        source: ev.source.as_str().to_owned(),
1231        uuid: ev.uuid.as_str().to_owned(),
1232        dimension: ev.dimension.as_str().to_owned(),
1233    };
1234    *reply_len = 0;
1235    if let Some(reply) = f(&ctx, &CServer(srv)) {
1236        let bytes = reply.as_bytes();
1237        let n = bytes.len().min(reply_cap as usize);
1238        std::ptr::copy_nonoverlapping(bytes.as_ptr(), reply_buf, n);
1239        *reply_len = n as u32;
1240    }
1241}
1242
1243unsafe extern "C" fn trampoline_scheduled<F>(ud: *mut c_void, srv: *const YogServer)
1244where
1245    F: Fn(&dyn Server) + Send + Sync,
1246{
1247    let f = &*(ud as *const F);
1248    f(&CServer(srv));
1249}
1250
1251// ── ABI minor 10 — client-side trampolines ────────────────────────────────────
1252
1253unsafe extern "C" fn trampoline_client_tick<F>(ud: *mut c_void)
1254where
1255    F: Fn(&ClientTickEvent) + Send + Sync,
1256{
1257    let f = &*(ud as *const F);
1258    f(&ClientTickEvent {});
1259}
1260
1261unsafe extern "C" fn trampoline_hud_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1262where
1263    F: Fn(&GfxContext) + Send + Sync,
1264{
1265    let f = &*(ud as *const F);
1266    f(&GfxContext::from_raw(gfx));
1267}
1268
1269unsafe extern "C" fn trampoline_world_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1270where
1271    F: Fn(&GfxContext) + Send + Sync,
1272{
1273    let f = &*(ud as *const F);
1274    f(&GfxContext::from_raw(gfx));
1275}
1276
1277unsafe extern "C" fn trampoline_key_press<F>(ud: *mut c_void, ev: *const YogKeyPressEvent) -> bool
1278where
1279    F: Fn(&KeyPressEvent) -> bool + Send + Sync,
1280{
1281    let f = &*(ud as *const F);
1282    let e = &*ev;
1283    f(&KeyPressEvent {
1284        key_code: e.key_code,
1285        scan_code: e.scan_code,
1286        action: e.action,
1287        modifiers: e.modifiers,
1288    })
1289}
1290
1291unsafe extern "C" fn trampoline_screen<F>(ud: *mut c_void, screen_class: YogStr) -> bool
1292where
1293    F: Fn(&ScreenEvent) + Send + Sync,
1294{
1295    let f = &*(ud as *const F);
1296    f(&ScreenEvent {
1297        screen_class: screen_class.as_str().to_owned(),
1298    });
1299    true
1300}
1301
1302// ── Registry ─────────────────────────────────────────────────────────────────
1303
1304/// Wraps the [`YogApi`] table and provides an ergonomic registration API.
1305///
1306/// Obtained inside `yog_mod_register` via `export_mod!`.  Closures registered
1307/// here are boxed and leaked — they live as long as the process (which is the
1308/// correct lifetime for a server mod).
1309/// Pointer to the runtime's `YogApi` table (a process-lifetime static in the
1310/// runtime), captured when the mod registers. Lets free functions like
1311/// [`installed_mods`] work outside of `register()` — e.g. in UI handlers.
1312static GLOBAL_API: std::sync::atomic::AtomicPtr<YogApi> =
1313    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
1314
1315/// Metadata of an installed mod, as reported by the loader.
1316#[derive(Debug, Clone)]
1317pub struct ModInfo {
1318    /// `"yog"` for .yog mods, `"platform"` for loader (Java) mods.
1319    pub source: String,
1320    pub id: String,
1321    pub name: String,
1322    pub version: String,
1323    /// Comma-separated author list (may be empty).
1324    pub authors: String,
1325    pub description: String,
1326}
1327
1328/// All installed mods known to the loader: .yog mods plus, where the host
1329/// exposes them, platform (Java) mods. Callable at any time after this mod's
1330/// `register()` ran — including client-side UI handlers. Note that during
1331/// `register()` mods that load after this one are not in the list yet; query
1332/// lazily (e.g. on first render) for a complete view.
1333pub fn installed_mods() -> Vec<ModInfo> {
1334    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1335    if api.is_null() {
1336        return Vec::new();
1337    }
1338    let owned = unsafe { ((*api).mods_list)((*api).ctx) };
1339    if owned.is_none() {
1340        return Vec::new();
1341    }
1342    let text = unsafe {
1343        String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
1344            .unwrap_or_default()
1345    };
1346    unsafe { ((*api).free_str)(owned.ptr, owned.len) };
1347    text.lines()
1348        .filter_map(|line| {
1349            let mut f = line.split('\t');
1350            Some(ModInfo {
1351                source: f.next()?.to_string(),
1352                id: f.next()?.to_string(),
1353                name: f.next().unwrap_or_default().to_string(),
1354                version: f.next().unwrap_or_default().to_string(),
1355                authors: f.next().unwrap_or_default().to_string(),
1356                description: f.next().unwrap_or_default().to_string(),
1357            })
1358        })
1359        .collect()
1360}
1361
1362/// Open the Yog UI registered as `ui_id` (client side; no-op on dedicated
1363/// servers). `modal` blocks game input, `pause` pauses singleplayer. Callable
1364/// from any handler after registration — e.g. an `on_client_packet` handler.
1365pub fn open_ui(ui_id: &str, modal: bool, pause: bool) {
1366    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1367    if api.is_null() {
1368        return;
1369    }
1370    unsafe { ((*api).ui_open)((*api).ctx, YogStr::from_str(ui_id), modal, pause) }
1371}
1372
1373/// Handle to the runtime's server-action table, usable from any handler after
1374/// registration — including client-side ones that don't receive `&dyn Server`
1375/// (UI event handlers, client tick). Server-world actions are no-ops client
1376/// side, but networking (`send_to_server`) and similar client-safe calls work.
1377pub fn server() -> Option<CServer> {
1378    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1379    if api.is_null() {
1380        return None;
1381    }
1382    let srv = unsafe { (*api).server };
1383    if srv.is_null() {
1384        return None;
1385    }
1386    Some(CServer(srv))
1387}
1388
1389pub struct Registry {
1390    api: *const YogApi,
1391}
1392
1393// SAFETY: `api` is a static provided by the runtime, valid for process lifetime.
1394unsafe impl Send for Registry {}
1395unsafe impl Sync for Registry {}
1396
1397impl Registry {
1398    /// Build from the pointer passed by the runtime. Only called by `export_mod!`.
1399    pub unsafe fn from_raw(api: *const YogApi) -> Self {
1400        GLOBAL_API.store(api as *mut YogApi, std::sync::atomic::Ordering::Release);
1401        Self { api }
1402    }
1403
1404    #[inline]
1405    fn ctx(&self) -> *mut c_void {
1406        unsafe { (*self.api).ctx }
1407    }
1408
1409    // ── helpers ──────────────────────────────────────────────────────────────
1410
1411    fn leak<F: 'static>(f: F) -> *mut c_void {
1412        Box::into_raw(Box::new(f)) as *mut c_void
1413    }
1414
1415    // ── events ───────────────────────────────────────────────────────────────
1416    //
1417    // All handlers receive `(event, EventPhase, &dyn Server) -> bool`.
1418    // In `Pre` phase, returning `false` cancels the action.
1419    // In `Post` phase, the return value is ignored.
1420    // A single registration fires for BOTH phases.
1421
1422    pub fn on_block_break<F>(&mut self, handler: F)
1423    where
1424        F: Fn(&BlockBreakEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1425    {
1426        let ud = Self::leak(handler);
1427        unsafe { ((*self.api).on_block_break)(self.ctx(), ud, trampoline_block_break::<F>) }
1428    }
1429
1430    pub fn on_chat<F>(&mut self, handler: F)
1431    where
1432        F: Fn(&ChatEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1433    {
1434        let ud = Self::leak(handler);
1435        unsafe { ((*self.api).on_chat)(self.ctx(), ud, trampoline_chat::<F>) }
1436    }
1437
1438    pub fn on_player_join<F>(&mut self, handler: F)
1439    where
1440        F: Fn(&PlayerJoinEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1441    {
1442        let ud = Self::leak(handler);
1443        unsafe { ((*self.api).on_player_join)(self.ctx(), ud, trampoline_player_join::<F>) }
1444    }
1445
1446    pub fn on_player_leave<F>(&mut self, handler: F)
1447    where
1448        F: Fn(&PlayerLeaveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1449    {
1450        let ud = Self::leak(handler);
1451        unsafe { ((*self.api).on_player_leave)(self.ctx(), ud, trampoline_player_leave::<F>) }
1452    }
1453
1454    pub fn on_use_item<F>(&mut self, handler: F)
1455    where
1456        F: Fn(&UseItemEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1457    {
1458        let ud = Self::leak(handler);
1459        unsafe { ((*self.api).on_use_item)(self.ctx(), ud, trampoline_use_item::<F>) }
1460    }
1461
1462    pub fn on_use_block<F>(&mut self, handler: F)
1463    where
1464        F: Fn(&UseBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1465    {
1466        let ud = Self::leak(handler);
1467        unsafe { ((*self.api).on_use_block)(self.ctx(), ud, trampoline_use_block::<F>) }
1468    }
1469
1470    pub fn on_attack_entity<F>(&mut self, handler: F)
1471    where
1472        F: Fn(&AttackEntityEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1473    {
1474        let ud = Self::leak(handler);
1475        unsafe { ((*self.api).on_attack_entity)(self.ctx(), ud, trampoline_attack_entity::<F>) }
1476    }
1477
1478    pub fn on_entity_damage<F>(&mut self, handler: F)
1479    where
1480        F: Fn(&EntityDamageEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1481    {
1482        let ud = Self::leak(handler);
1483        unsafe { ((*self.api).on_entity_damage)(self.ctx(), ud, trampoline_entity_damage::<F>) }
1484    }
1485
1486    pub fn on_entity_death<F>(&mut self, handler: F)
1487    where
1488        F: Fn(&EntityDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1489    {
1490        let ud = Self::leak(handler);
1491        unsafe { ((*self.api).on_entity_death)(self.ctx(), ud, trampoline_entity_death::<F>) }
1492    }
1493
1494    pub fn on_entity_spawn<F>(&mut self, handler: F)
1495    where
1496        F: Fn(&EntitySpawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1497    {
1498        let ud = Self::leak(handler);
1499        unsafe { ((*self.api).on_entity_spawn)(self.ctx(), ud, trampoline_entity_spawn::<F>) }
1500    }
1501
1502    pub fn on_player_place_block<F>(&mut self, handler: F)
1503    where
1504        F: Fn(&PlaceBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1505    {
1506        let ud = Self::leak(handler);
1507        unsafe { ((*self.api).on_player_place_block)(self.ctx(), ud, trampoline_place_block::<F>) }
1508    }
1509
1510    pub fn on_player_death<F>(&mut self, handler: F)
1511    where
1512        F: Fn(&PlayerDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1513    {
1514        let ud = Self::leak(handler);
1515        unsafe { ((*self.api).on_player_death)(self.ctx(), ud, trampoline_player_death::<F>) }
1516    }
1517
1518    pub fn on_player_respawn<F>(&mut self, handler: F)
1519    where
1520        F: Fn(&PlayerRespawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1521    {
1522        let ud = Self::leak(handler);
1523        unsafe { ((*self.api).on_player_respawn)(self.ctx(), ud, trampoline_player_respawn::<F>) }
1524    }
1525
1526    pub fn on_advancement<F>(&mut self, handler: F)
1527    where
1528        F: Fn(&AdvancementEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1529    {
1530        let ud = Self::leak(handler);
1531        unsafe { ((*self.api).on_advancement)(self.ctx(), ud, trampoline_advancement::<F>) }
1532    }
1533
1534    pub fn on_entity_interact<F>(&mut self, handler: F)
1535    where
1536        F: Fn(&EntityInteractEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1537    {
1538        let ud = Self::leak(handler);
1539        unsafe { ((*self.api).on_entity_interact)(self.ctx(), ud, trampoline_entity_interact::<F>) }
1540    }
1541
1542    pub fn on_item_craft<F>(&mut self, handler: F)
1543    where
1544        F: Fn(&CraftEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1545    {
1546        let ud = Self::leak(handler);
1547        unsafe { ((*self.api).on_item_craft)(self.ctx(), ud, trampoline_craft::<F>) }
1548    }
1549
1550    pub fn on_explosion<F>(&mut self, handler: F)
1551    where
1552        F: Fn(&ExplosionEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1553    {
1554        let ud = Self::leak(handler);
1555        unsafe { ((*self.api).on_explosion)(self.ctx(), ud, trampoline_explosion::<F>) }
1556    }
1557
1558    pub fn on_item_pickup<F>(&mut self, handler: F)
1559    where
1560        F: Fn(&ItemPickupEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1561    {
1562        let ud = Self::leak(handler);
1563        unsafe { ((*self.api).on_item_pickup)(self.ctx(), ud, trampoline_item_pickup::<F>) }
1564    }
1565
1566    pub fn on_player_move<F>(&mut self, handler: F)
1567    where
1568        F: Fn(&PlayerMoveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1569    {
1570        let ud = Self::leak(handler);
1571        unsafe { ((*self.api).on_player_move)(self.ctx(), ud, trampoline_player_move::<F>) }
1572    }
1573
1574    pub fn on_container_open<F>(&mut self, handler: F)
1575    where
1576        F: Fn(&ContainerOpenEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1577    {
1578        let ud = Self::leak(handler);
1579        unsafe { ((*self.api).on_container_open)(self.ctx(), ud, trampoline_container_open::<F>) }
1580    }
1581
1582    pub fn on_container_close<F>(&mut self, handler: F)
1583    where
1584        F: Fn(&ContainerCloseEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1585    {
1586        let ud = Self::leak(handler);
1587        unsafe { ((*self.api).on_container_close)(self.ctx(), ud, trampoline_container_close::<F>) }
1588    }
1589
1590    pub fn on_projectile_hit<F>(&mut self, handler: F)
1591    where
1592        F: Fn(&ProjectileHitEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
1593    {
1594        let ud = Self::leak(handler);
1595        unsafe { ((*self.api).on_projectile_hit)(self.ctx(), ud, trampoline_projectile_hit::<F>) }
1596    }
1597
1598    pub fn on_tick<F>(&mut self, listener: F)
1599    where
1600        F: Fn(&dyn Server) + Send + Sync + 'static,
1601    {
1602        let ud = Self::leak(listener);
1603        unsafe { ((*self.api).on_server_tick)(self.ctx(), ud, trampoline_server_fn::<F>) }
1604    }
1605
1606    pub fn on_server_started<F>(&mut self, listener: F)
1607    where
1608        F: Fn(&dyn Server) + Send + Sync + 'static,
1609    {
1610        let ud = Self::leak(listener);
1611        unsafe { ((*self.api).on_server_started)(self.ctx(), ud, trampoline_server_fn::<F>) }
1612    }
1613
1614    pub fn on_server_stopping<F>(&mut self, listener: F)
1615    where
1616        F: Fn(&dyn Server) + Send + Sync + 'static,
1617    {
1618        let ud = Self::leak(listener);
1619        unsafe { ((*self.api).on_server_stopping)(self.ctx(), ud, trampoline_server_fn::<F>) }
1620    }
1621
1622    // ── commands ─────────────────────────────────────────────────────────────
1623
1624    pub fn on_command<F>(&mut self, name: impl AsRef<str>, handler: F)
1625    where
1626        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
1627    {
1628        let name_ys = YogStr::from_str(name.as_ref());
1629        let ud = Self::leak(handler);
1630        unsafe { ((*self.api).register_command)(self.ctx(), name_ys, ud, trampoline_command::<F>) }
1631    }
1632
1633    /// Register a command with Brigadier-typed arguments.
1634    ///
1635    /// `schema` is a space-separated list of argument types:
1636    /// `int`, `float`, `word`, `string` (greedy, must be last), `player`, `blockpos`.
1637    ///
1638    /// In the handler use `ctx.arg_int(0)`, `ctx.arg_blockpos(1)`, etc.
1639    pub fn on_typed_command<F>(
1640        &mut self,
1641        name: impl AsRef<str>,
1642        schema: impl AsRef<str>,
1643        handler: F,
1644    ) where
1645        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
1646    {
1647        let name_ys = YogStr::from_str(name.as_ref());
1648        let schema_ys = YogStr::from_str(schema.as_ref());
1649        let ud = Self::leak(handler);
1650        unsafe {
1651            ((*self.api).register_typed_command)(
1652                self.ctx(),
1653                name_ys,
1654                schema_ys,
1655                ud,
1656                trampoline_command::<F>,
1657            )
1658        }
1659    }
1660
1661    // ── networking ───────────────────────────────────────────────────────────
1662
1663    pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1664    where
1665        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
1666    {
1667        let ch = YogStr::from_str(channel.as_ref());
1668        let ud = Self::leak(handler);
1669        unsafe { ((*self.api).on_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1670    }
1671
1672    pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1673    where
1674        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
1675    {
1676        let ch = YogStr::from_str(channel.as_ref());
1677        let ud = Self::leak(handler);
1678        unsafe { ((*self.api).on_client_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1679    }
1680
1681    /// Register a typed-packet handler.
1682    ///
1683    /// The payload is decoded from raw bytes using `P`'s [`Packet`] impl.
1684    /// Malformed payloads are silently dropped.
1685    pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
1686    where
1687        P: Packet + Send + Sync + 'static,
1688        F: Fn(&P, &dyn Server) + Send + Sync + 'static,
1689    {
1690        self.on_packet(channel, move |ev, srv| {
1691            if let Some(pkt) = P::decode(&ev.payload) {
1692                handler(&pkt, srv);
1693            }
1694        });
1695    }
1696
1697    // ── recipes ──────────────────────────────────────────────────────────────
1698
1699    fn recipe(&mut self, ns: &str, name: &str, json: &str) {
1700        unsafe {
1701            ((*self.api).register_recipe_json)(
1702                self.ctx(),
1703                YogStr::from_str(ns),
1704                YogStr::from_str(name),
1705                YogStr::from_str(json),
1706            )
1707        }
1708    }
1709
1710    /// Register a shaped crafting recipe.
1711    pub fn add_shaped_recipe(&mut self, recipe: ShapedRecipe) {
1712        let json = recipe.to_json();
1713        let (ns, name) = recipe.ns_name();
1714        self.recipe(ns, name, &json);
1715    }
1716
1717    /// Register a shapeless crafting recipe.
1718    pub fn add_shapeless_recipe(&mut self, recipe: ShapelessRecipe) {
1719        let json = recipe.to_json();
1720        let (ns, name) = recipe.ns_name();
1721        self.recipe(ns, name, &json);
1722    }
1723
1724    /// Register a furnace smelting recipe.
1725    pub fn add_furnace_recipe(&mut self, recipe: FurnaceRecipe) {
1726        let json = recipe.to_json();
1727        let (ns, name) = recipe.ns_name();
1728        self.recipe(ns, name, &json);
1729    }
1730
1731    // ── content ──────────────────────────────────────────────────────────────
1732
1733    pub fn register_item(&mut self, def: ItemDef) {
1734        // Build a C-compatible YogItemDef whose YogStr fields point into `def`'s
1735        // String storage.  We then call register_item which must copy the data
1736        // before returning (the runtime stores owned Strings).
1737        let food_nutrition = def.food.as_ref().map_or(0, |f| f.nutrition);
1738        let food_saturation = def.food.as_ref().map_or(0.0, |f| f.saturation);
1739        let food_always_eat = def.food.as_ref().map_or(false, |f| f.can_always_eat);
1740        let c = YogItemDef {
1741            id: YogStr::from_str(&def.id),
1742            max_stack: def.max_stack as u32,
1743            name: def
1744                .name
1745                .as_deref()
1746                .map(YogStr::from_str)
1747                .unwrap_or(YogStr::EMPTY),
1748            tooltip: def
1749                .tooltip
1750                .as_deref()
1751                .map(YogStr::from_str)
1752                .unwrap_or(YogStr::EMPTY),
1753            max_damage: def.max_damage,
1754            fire_resistant: def.fire_resistant,
1755            fuel_ticks: def.fuel_ticks,
1756            food_nutrition,
1757            food_saturation,
1758            food_always_eat,
1759        };
1760        unsafe { ((*self.api).register_item)(self.ctx(), &c) }
1761    }
1762
1763    pub fn register_block(&mut self, def: BlockDef) {
1764        let shape = def.shape.unwrap_or([0.0; 6]);
1765        let groups_joined = def.connect_groups.join(",");
1766        let c = YogBlockDef {
1767            id: YogStr::from_str(&def.id),
1768            hardness: def.hardness,
1769            resistance: def.resistance,
1770            name: def
1771                .name
1772                .as_deref()
1773                .map(YogStr::from_str)
1774                .unwrap_or(YogStr::EMPTY),
1775            light_level: def.light_level,
1776            sound: def
1777                .sound
1778                .as_deref()
1779                .map(YogStr::from_str)
1780                .unwrap_or(YogStr::EMPTY),
1781            requires_tool: def.requires_tool,
1782            no_collision: def.no_collision,
1783            slipperiness: def.slipperiness,
1784            shape,
1785            connects: def.connects,
1786            connect_groups: YogStr::from_str(&groups_joined),
1787            inventory_id: def
1788                .inventory_id
1789                .as_deref()
1790                .map(YogStr::from_str)
1791                .unwrap_or(YogStr::EMPTY),
1792        };
1793        unsafe { ((*self.api).register_block)(self.ctx(), &c) }
1794    }
1795
1796    /// Register a real Container/Menu inventory screen — see
1797    /// `yog_inventory::InventoryDef`. Attach it to a block via
1798    /// `BlockDef::inventory(id)`.
1799    pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
1800        let layout = yog_inventory::encode_layout(&def.resolved_layout());
1801        let c = YogInventoryDef {
1802            id: YogStr::from_str(&def.id),
1803            slot_count: def.slot_count as u32,
1804            layout: YogStr::from_str(&layout),
1805            include_player_inventory: def.include_player_inventory,
1806            player_inv_x: def.player_inv_offset.0,
1807            player_inv_y: def.player_inv_offset.1,
1808            background_texture: def
1809                .background_texture
1810                .as_deref()
1811                .map(YogStr::from_str)
1812                .unwrap_or(YogStr::EMPTY),
1813            title: YogStr::from_str(&def.title),
1814        };
1815        unsafe { ((*self.api).register_inventory)(self.ctx(), &c) }
1816    }
1817
1818    // ── startup grants ───────────────────────────────────────────────────────
1819
1820    /// Register a startup grant: items/books to give once when a player first joins.
1821    pub fn register_startup_grant(&mut self, grant: StartupGrant) {
1822        let items_str = grant.items.join("|");
1823        let c = YogStartupGrantDef {
1824            id: YogStr::from_str(&grant.id),
1825            items: YogStr::from_str(&items_str),
1826            book: grant
1827                .book
1828                .as_deref()
1829                .map(YogStr::from_str)
1830                .unwrap_or(YogStr::EMPTY),
1831            command: grant
1832                .command
1833                .as_deref()
1834                .map(YogStr::from_str)
1835                .unwrap_or(YogStr::EMPTY),
1836        };
1837        unsafe { ((*self.api).register_startup_grant)(self.ctx(), &c) }
1838    }
1839
1840    /// Register a book with its JSON-serialized content.
1841    pub fn register_book(&mut self, book: &Book) {
1842        let json = book.to_json();
1843        let id = YogStr::from_str(&book.id);
1844        let j = YogStr::from_str(&json);
1845        unsafe { ((*self.api).register_book)(self.ctx(), id, j) }
1846    }
1847
1848    /// Register a menu entry — the host renders a button on vanilla screens
1849    /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
1850    /// `label` is the human-readable button text (e.g. "Yog Mods").
1851    /// `ui_id` is the UI to open when clicked (e.g. "yog:modlist").
1852    /// See [`installed_mods`]. During `register()` the list only contains
1853    /// mods loaded before this one.
1854    pub fn installed_mods(&self) -> Vec<ModInfo> {
1855        installed_mods()
1856    }
1857
1858    /// Access the inter-mod communication layer.
1859    ///
1860    /// Allows exporting function pointers from this mod and importing
1861    /// function pointers from other mods.
1862    pub fn interop(&self) -> crate::Interop {
1863        crate::Interop::new(self.api)
1864    }
1865
1866    /// Raw `YogApi` pointer — for passing to interop-imported functions
1867    /// that register content through the C ABI table directly.
1868    pub fn raw_api(&self) -> *const yog_abi::YogApi {
1869        self.api
1870    }
1871
1872    pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
1873        let l = YogStr::from_str(label);
1874        let u = YogStr::from_str(ui_id);
1875        unsafe { ((*self.api).register_menu_entry)(self.ctx(), l, u) }
1876    }
1877
1878    /// Register a UI tree with an event callback.
1879    /// `ui_id` is the unique identifier (e.g. "mymod:menu").
1880    /// `handler` receives `(ui_id, event_id)` when a widget is clicked.
1881    pub fn register_ui<F>(&mut self, ui_id: &str, handler: F)
1882    where
1883        F: Fn(&str, &str) + Send + Sync + 'static,
1884    {
1885        let ud = Self::leak(handler);
1886        let id = YogStr::from_str(ui_id);
1887        let empty = YogStr::from_str("{}");
1888        unsafe { ((*self.api).register_ui)(self.ctx(), id, empty, ud, trampoline_ui_event::<F>) }
1889    }
1890
1891    // ── scheduler ────────────────────────────────────────────────────────────
1892
1893    pub fn schedule_once<F>(&self, delay_ticks: u64, handler: F)
1894    where
1895        F: Fn(&dyn Server) + Send + Sync + 'static,
1896    {
1897        let ud = Self::leak(handler);
1898        unsafe {
1899            ((*self.api).schedule_once)(self.ctx(), delay_ticks, ud, trampoline_scheduled::<F>)
1900        }
1901    }
1902
1903    pub fn schedule_repeating<F>(&self, period_ticks: u64, handler: F)
1904    where
1905        F: Fn(&dyn Server) + Send + Sync + 'static,
1906    {
1907        let ud = Self::leak(handler);
1908        unsafe {
1909            ((*self.api).schedule_repeating)(
1910                self.ctx(),
1911                period_ticks,
1912                ud,
1913                trampoline_scheduled::<F>,
1914            )
1915        }
1916    }
1917
1918    // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
1919
1920    /// Register a handler called every client tick (render thread, no server).
1921    pub fn on_client_tick<F>(&mut self, handler: F)
1922    where
1923        F: Fn(&ClientTickEvent) + Send + Sync + 'static,
1924    {
1925        let ud = Self::leak(handler);
1926        unsafe { ((*self.api).on_client_tick)(self.ctx(), ud, trampoline_client_tick::<F>) }
1927    }
1928
1929    /// Register a render callback for a specific UI screen (`ui_id`).
1930    ///
1931    /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
1932    /// the screen — so your UI draws on top of the dimmed world view.
1933    /// Use this instead of `on_hud_render` for book/inventory screens.
1934    ///
1935    /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
1936    /// can do hit-testing on your own stored layout.
1937    pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
1938    where
1939        F: Fn(&GfxContext) + Send + Sync + 'static,
1940    {
1941        let ud = Self::leak(handler);
1942        let id = YogStr::from_str(ui_id);
1943        unsafe { ((*self.api).on_ui_render)(self.ctx(), id, ud, trampoline_hud_render::<F>) }
1944    }
1945
1946    /// Register a handler called every frame when the HUD is rendered.
1947    ///
1948    /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
1949    /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
1950    pub fn on_hud_render<F>(&mut self, handler: F)
1951    where
1952        F: Fn(&GfxContext) + Send + Sync + 'static,
1953    {
1954        let ud = Self::leak(handler);
1955        unsafe { ((*self.api).on_hud_render)(self.ctx(), ud, trampoline_hud_render::<F>) }
1956    }
1957
1958    /// Register a handler called every frame at the end of world rendering.
1959    ///
1960    /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
1961    /// `gfx.camera_pos()` is the camera world position.
1962    /// To render at world position P, translate by `P - camera_pos` before drawing.
1963    pub fn on_world_render<F>(&mut self, handler: F)
1964    where
1965        F: Fn(&GfxContext) + Send + Sync + 'static,
1966    {
1967        let ud = Self::leak(handler);
1968        unsafe { ((*self.api).on_world_render)(self.ctx(), ud, trampoline_world_render::<F>) }
1969    }
1970
1971    /// Register a handler for keyboard input (client-side).
1972    /// Return `false` to prevent Minecraft from processing the key.
1973    pub fn on_key_press<F>(&mut self, handler: F)
1974    where
1975        F: Fn(&KeyPressEvent) -> bool + Send + Sync + 'static,
1976    {
1977        let ud = Self::leak(handler);
1978        unsafe { ((*self.api).on_key_press)(self.ctx(), ud, trampoline_key_press::<F>) }
1979    }
1980
1981    /// Register a handler called when a GUI screen is opened.
1982    pub fn on_screen_open<F>(&mut self, handler: F)
1983    where
1984        F: Fn(&ScreenEvent) + Send + Sync + 'static,
1985    {
1986        let ud = Self::leak(handler);
1987        unsafe { ((*self.api).on_screen_open)(self.ctx(), ud, trampoline_screen::<F>) }
1988    }
1989
1990    /// Register a handler called when a GUI screen is closed.
1991    pub fn on_screen_close<F>(&mut self, handler: F)
1992    where
1993        F: Fn(&ScreenEvent) + Send + Sync + 'static,
1994    {
1995        let ud = Self::leak(handler);
1996        unsafe { ((*self.api).on_screen_close)(self.ctx(), ud, trampoline_screen::<F>) }
1997    }
1998}
1999
2000unsafe extern "C" fn trampoline_ui_event<F>(
2001    ud: *mut c_void,
2002    ui_id: yog_abi::YogStr,
2003    event_id: yog_abi::YogStr,
2004) where
2005    F: Fn(&str, &str) + Send + Sync + 'static,
2006{
2007    let f = &*(ud as *const F);
2008    let ui = unsafe { ui_id.as_str() };
2009    let ev = unsafe { event_id.as_str() };
2010    f(ui, ev);
2011}
2012
2013// ── Mod trait ─────────────────────────────────────────────────────────────────
2014
2015/// Implemented by every Yog mod. Called once at startup to register handlers.
2016pub trait Mod {
2017    fn register(registry: &mut Registry);
2018}