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, 
14    YogChatEvent, YogCommandEvent, YogContainerCloseEvent, YogContainerOpenEvent, YogCraftEvent,
15    YogEntityDamageEvent, YogEntityDeathEvent, YogEntityInteractEvent, YogEntitySpawnEvent,
16    YogExplosionEvent, YogGfxApi, YogItemInsertEvent, YogItemPickupEvent, YogMachineCraftEvent,
17    YogKeyPressEvent, YogPacketEvent, YogPlaceBlockEvent, YogPlayerDeathEvent, YogPlayerEvent,
18    YogPlayerMoveEvent, YogPlayerRespawnEvent, YogProjectileHitEvent, YogServer,
19    YogStr, YogUseBlockEvent, YogUseItemEvent,
20};
21use yog_book::Book;
22use yog_command::CommandContext;
23use yog_core::Server;
24use yog_dimensions::{ChunkWriter, YogDimensionDef};
25use yog_event::{
26    AdvancementEvent, AttackEntityEvent, BlockBreakEvent, ChatEvent, ClientTickEvent,
27    ContainerCloseEvent, ContainerOpenEvent, CraftEvent, EntityDamageEvent, EntityDeathEvent,
28    EntityInteractEvent, EntitySpawnEvent, EventPhase, ExplosionEvent, ItemInsertEvent, ItemPickupEvent,
29    KeyPressEvent, PlaceBlockEvent, PlayerDeathEvent, PlayerJoinEvent, PlayerLeaveEvent,
30    PlayerMoveEvent, PlayerRespawnEvent, ProjectileHitEvent, ScreenEvent, UseBlockEvent,
31    UseItemEvent,
32};
33use yog_gfx::GfxContext;
34use yog_network::{Packet, PacketEvent};
35use yog_registry::{ArmorMaterial, BlockDef, ItemDef, Recipe, SoundGroup, StartupGrant, ToolMaterial};
36
37// ── CServer — implements Server via the YogServer function table ──────────────
38
39/// A handle to the runtime's server actions, backed by [`YogServer`] fn pointers.
40/// Given to every handler as `&dyn Server`.
41pub struct CServer(pub *const YogServer);
42
43unsafe impl Send for CServer {}
44unsafe impl Sync for CServer {}
45
46macro_rules! srv {
47    ($self:ident) => {
48        unsafe { &*$self.0 }
49    };
50}
51
52impl Server for CServer {
53    fn broadcast(&self, message: &str) {
54        let s = srv!(self);
55        unsafe { (s.broadcast)(s.ctx, YogStr::from_str(message)) }
56    }
57
58    fn get_block(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
59        let s = srv!(self);
60        let owned = unsafe {
61            (s.get_block)(
62                s.ctx,
63                YogStr::from_str(dimension),
64                yog_abi::YogBlockPos {
65                    x: pos.x,
66                    y: pos.y,
67                    z: pos.z,
68                },
69            )
70        };
71
72        take_owned(s.free_str, owned)
73    }
74
75    fn set_block(&self, dimension: &str, pos: yog_core::BlockPos, block_id: &str) -> bool {
76        let s = srv!(self);
77        unsafe {
78            (s.set_block)(
79                s.ctx,
80                YogStr::from_str(dimension),
81                yog_abi::YogBlockPos {
82                    x: pos.x,
83                    y: pos.y,
84                    z: pos.z,
85                },
86                YogStr::from_str(block_id),
87            )
88        }
89    }
90
91    fn give_item(&self, player: &str, item_id: &str, count: u32) -> bool {
92        let s = srv!(self);
93        unsafe {
94            (s.give_item)(
95                s.ctx,
96                YogStr::from_str(player),
97                YogStr::from_str(item_id),
98                count,
99            )
100        }
101    }
102
103    fn teleport(&self, player: &str, x: f64, y: f64, z: f64) -> bool {
104        let s = srv!(self);
105        unsafe {
106            (s.player_teleport)(
107                s.ctx,
108                YogStr::from_str(player),
109                yog_abi::YogVec3 { x, y, z },
110            )
111        }
112    }
113
114    fn send_to_player(&self, player: &str, channel: &str, payload: &[u8]) -> bool {
115        let s = srv!(self);
116        unsafe {
117            (s.send_to_player)(
118                s.ctx,
119                YogStr::from_str(player),
120                YogStr::from_str(channel),
121                payload.as_ptr(),
122                payload.len() as u32,
123            )
124        }
125    }
126
127    fn send_to_server(&self, channel: &str, payload: &[u8]) -> bool {
128        let s = srv!(self);
129        unsafe {
130            (s.send_to_server)(
131                s.ctx,
132                YogStr::from_str(channel),
133                payload.as_ptr(),
134                payload.len() as u32,
135            )
136        }
137    }
138
139    fn entity_teleport(&self, uuid: &str, x: f64, y: f64, z: f64) -> bool {
140        let s = srv!(self);
141        unsafe { (s.entity_teleport)(s.ctx, YogStr::from_str(uuid), yog_abi::YogVec3 { x, y, z }) }
142    }
143
144    fn entity_position(&self, uuid: &str) -> Option<(f64, f64, f64)> {
145        let s = srv!(self);
146        let mut out = yog_abi::YogVec3 {
147            x: 0.0,
148            y: 0.0,
149            z: 0.0,
150        };
151        if unsafe { (s.entity_position)(s.ctx, YogStr::from_str(uuid), &mut out) } {
152            Some((out.x, out.y, out.z))
153        } else {
154            None
155        }
156    }
157
158    fn entity_health(&self, uuid: &str) -> Option<f32> {
159        let s = srv!(self);
160        let mut hp = 0f32;
161        if unsafe { (s.entity_health)(s.ctx, YogStr::from_str(uuid), &mut hp) } {
162            Some(hp)
163        } else {
164            None
165        }
166    }
167
168    fn entity_set_health(&self, uuid: &str, health: f32) -> bool {
169        let s = srv!(self);
170        unsafe { (s.entity_set_health)(s.ctx, YogStr::from_str(uuid), health) }
171    }
172
173    fn entity_kill(&self, uuid: &str) -> bool {
174        let s = srv!(self);
175        unsafe { (s.entity_kill)(s.ctx, YogStr::from_str(uuid)) }
176    }
177
178    fn spawn_entity(
179        &self,
180        entity_type: &str,
181        dimension: &str,
182        x: f64,
183        y: f64,
184        z: f64,
185    ) -> Option<String> {
186        let s = srv!(self);
187        let owned = unsafe {
188            (s.spawn_entity)(
189                s.ctx,
190                YogStr::from_str(entity_type),
191                YogStr::from_str(dimension),
192                yog_abi::YogVec3 { x, y, z },
193            )
194        };
195
196        take_owned(s.free_str, owned)
197    }
198
199    fn entity_add_effect(
200        &self,
201        uuid: &str,
202        effect_id: &str,
203        duration_ticks: i32,
204        amplifier: u8,
205        show_particles: bool,
206    ) -> bool {
207        let s = srv!(self);
208        unsafe {
209            (s.entity_add_effect)(
210                s.ctx,
211                YogStr::from_str(uuid),
212                YogStr::from_str(effect_id),
213                duration_ticks,
214                amplifier,
215                show_particles,
216            )
217        }
218    }
219
220    fn entity_remove_effect(&self, uuid: &str, effect_id: &str) -> bool {
221        let s = srv!(self);
222        unsafe {
223            (s.entity_remove_effect)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(effect_id))
224        }
225    }
226
227    fn entity_clear_effects(&self, uuid: &str) -> bool {
228        let s = srv!(self);
229        unsafe { (s.entity_clear_effects)(s.ctx, YogStr::from_str(uuid)) }
230    }
231
232    fn drop_loot(&self, table_id: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
233        let s = srv!(self);
234        unsafe {
235            (s.drop_loot)(
236                s.ctx,
237                YogStr::from_str(table_id),
238                YogStr::from_str(dimension),
239                yog_abi::YogVec3 { x, y, z },
240            )
241        }
242    }
243
244    fn has_item_tag(&self, item_id: &str, tag_id: &str) -> bool {
245        let s = srv!(self);
246        unsafe { (s.has_item_tag)(s.ctx, YogStr::from_str(item_id), YogStr::from_str(tag_id)) }
247    }
248
249    fn has_block_tag(&self, block_id: &str, tag_id: &str) -> bool {
250        let s = srv!(self);
251        unsafe { (s.has_block_tag)(s.ctx, YogStr::from_str(block_id), YogStr::from_str(tag_id)) }
252    }
253
254    fn world_time(&self, dimension: &str) -> Option<i64> {
255        let s = srv!(self);
256        let mut t = 0i64;
257        if unsafe { (s.world_time)(s.ctx, YogStr::from_str(dimension), &mut t) } {
258            Some(t)
259        } else {
260            None
261        }
262    }
263
264    fn world_set_time(&self, dimension: &str, time: i64) -> bool {
265        let s = srv!(self);
266        unsafe { (s.set_time)(s.ctx, YogStr::from_str(dimension), time) }
267    }
268
269    fn world_is_raining(&self, dimension: &str) -> bool {
270        let s = srv!(self);
271        unsafe { (s.is_raining)(s.ctx, YogStr::from_str(dimension)) }
272    }
273
274    fn world_set_weather(&self, dimension: &str, raining: bool, duration_ticks: i32) -> bool {
275        let s = srv!(self);
276        unsafe { (s.set_weather)(s.ctx, YogStr::from_str(dimension), raining, duration_ticks) }
277    }
278
279    fn entity_velocity(&self, uuid: &str) -> Option<(f64, f64, f64)> {
280        let s = srv!(self);
281        let mut v = yog_abi::YogVec3 {
282            x: 0.0,
283            y: 0.0,
284            z: 0.0,
285        };
286        if unsafe { (s.entity_velocity)(s.ctx, YogStr::from_str(uuid), &mut v) } {
287            Some((v.x, v.y, v.z))
288        } else {
289            None
290        }
291    }
292
293    fn entity_set_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
294        let s = srv!(self);
295        unsafe {
296            (s.entity_set_velocity)(
297                s.ctx,
298                YogStr::from_str(uuid),
299                yog_abi::YogVec3 {
300                    x: vx,
301                    y: vy,
302                    z: vz,
303                },
304            )
305        }
306    }
307
308    fn entity_add_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
309        let s = srv!(self);
310        unsafe {
311            (s.entity_add_velocity)(
312                s.ctx,
313                YogStr::from_str(uuid),
314                yog_abi::YogVec3 {
315                    x: vx,
316                    y: vy,
317                    z: vz,
318                },
319            )
320        }
321    }
322
323    fn scoreboard_get(&self, objective: &str, player: &str) -> Option<i32> {
324        let s = srv!(self);
325        let mut score = 0i32;
326        if unsafe {
327            (s.scoreboard_get)(
328                s.ctx,
329                YogStr::from_str(objective),
330                YogStr::from_str(player),
331                &mut score,
332            )
333        } {
334            Some(score)
335        } else {
336            None
337        }
338    }
339
340    fn scoreboard_set(&self, objective: &str, player: &str, score: i32) -> bool {
341        let s = srv!(self);
342        unsafe {
343            (s.scoreboard_set)(
344                s.ctx,
345                YogStr::from_str(objective),
346                YogStr::from_str(player),
347                score,
348            )
349        }
350    }
351
352    fn scoreboard_add(&self, objective: &str, player: &str, delta: i32) -> Option<i32> {
353        let s = srv!(self);
354        let mut new_score = 0i32;
355        if unsafe {
356            (s.scoreboard_add)(
357                s.ctx,
358                YogStr::from_str(objective),
359                YogStr::from_str(player),
360                delta,
361                &mut new_score,
362            )
363        } {
364            Some(new_score)
365        } else {
366            None
367        }
368    }
369
370    fn game_dir(&self) -> String {
371        let s = srv!(self);
372        let owned = unsafe { (s.game_dir)(s.ctx) };
373        take_owned(s.free_str, owned).unwrap_or_default()
374    }
375
376    fn play_sound(
377        &self,
378        dimension: &str,
379        x: f64,
380        y: f64,
381        z: f64,
382        sound_id: &str,
383        volume: f32,
384        pitch: f32,
385    ) -> bool {
386        let s = srv!(self);
387        unsafe {
388            (s.play_sound)(
389                s.ctx,
390                YogStr::from_str(dimension),
391                yog_abi::YogVec3 { x, y, z },
392                YogStr::from_str(sound_id),
393                volume,
394                pitch,
395            )
396        }
397    }
398
399    fn play_sound_to_player(&self, player: &str, sound_id: &str, volume: f32, pitch: f32) -> bool {
400        let s = srv!(self);
401        unsafe {
402            (s.play_sound_player)(
403                s.ctx,
404                YogStr::from_str(player),
405                YogStr::from_str(sound_id),
406                volume,
407                pitch,
408            )
409        }
410    }
411
412    fn send_title(
413        &self,
414        player: &str,
415        title: &str,
416        subtitle: &str,
417        fadein: i32,
418        stay: i32,
419        fadeout: i32,
420    ) -> bool {
421        let s = srv!(self);
422        unsafe {
423            (s.send_title)(
424                s.ctx,
425                YogStr::from_str(player),
426                YogStr::from_str(title),
427                YogStr::from_str(subtitle),
428                fadein,
429                stay,
430                fadeout,
431            )
432        }
433    }
434
435    fn entity_rotation(&self, uuid: &str) -> Option<(f32, f32)> {
436        let s = srv!(self);
437        let mut out = yog_abi::YogVec3 {
438            x: 0.0,
439            y: 0.0,
440            z: 0.0,
441        };
442        let ok = unsafe { (s.entity_rotation)(s.ctx, YogStr::from_str(uuid), &mut out) };
443        if ok {
444            Some((out.x as f32, out.y as f32))
445        } else {
446            None
447        }
448    }
449
450    fn send_actionbar(&self, player: &str, message: &str) -> bool {
451        let s = srv!(self);
452        unsafe { (s.send_actionbar)(s.ctx, YogStr::from_str(player), YogStr::from_str(message)) }
453    }
454
455    fn kick_player(&self, player: &str, reason: &str) -> bool {
456        let s = srv!(self);
457        unsafe { (s.kick_player)(s.ctx, YogStr::from_str(player), YogStr::from_str(reason)) }
458    }
459
460    fn set_gamemode(&self, player: &str, gamemode: &str) -> bool {
461        let s = srv!(self);
462        unsafe { (s.set_gamemode)(s.ctx, YogStr::from_str(player), YogStr::from_str(gamemode)) }
463    }
464
465    fn bossbar_create(&self, id: &str, title: &str, color: &str, style: &str) -> bool {
466        let s = srv!(self);
467        unsafe {
468            (s.bossbar_create)(
469                s.ctx,
470                YogStr::from_str(id),
471                YogStr::from_str(title),
472                YogStr::from_str(color),
473                YogStr::from_str(style),
474            )
475        }
476    }
477
478    fn bossbar_remove(&self, id: &str) -> bool {
479        let s = srv!(self);
480        unsafe { (s.bossbar_remove)(s.ctx, YogStr::from_str(id)) }
481    }
482
483    fn bossbar_set_title(&self, id: &str, title: &str) -> bool {
484        let s = srv!(self);
485        unsafe { (s.bossbar_set_title)(s.ctx, YogStr::from_str(id), YogStr::from_str(title)) }
486    }
487
488    fn bossbar_set_progress(&self, id: &str, progress: f32) -> bool {
489        let s = srv!(self);
490        unsafe { (s.bossbar_set_progress)(s.ctx, YogStr::from_str(id), progress) }
491    }
492
493    fn bossbar_set_color(&self, id: &str, color: &str) -> bool {
494        let s = srv!(self);
495        unsafe { (s.bossbar_set_color)(s.ctx, YogStr::from_str(id), YogStr::from_str(color)) }
496    }
497
498    fn bossbar_add_player(&self, id: &str, player: &str) -> bool {
499        let s = srv!(self);
500        unsafe { (s.bossbar_add_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
501    }
502
503    fn bossbar_remove_player(&self, id: &str, player: &str) -> bool {
504        let s = srv!(self);
505        unsafe { (s.bossbar_remove_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
506    }
507
508    fn bossbar_set_visible(&self, id: &str, visible: bool) -> bool {
509        let s = srv!(self);
510        unsafe { (s.bossbar_set_visible)(s.ctx, YogStr::from_str(id), visible) }
511    }
512
513    fn online_players(&self) -> Vec<String> {
514        let s = srv!(self);
515        let owned = unsafe { (s.online_players)(s.ctx) };
516        let Some(text) = take_owned(s.free_str, owned) else {
517            return Vec::new();
518        };
519        if text.is_empty() {
520            Vec::new()
521        } else {
522            text.lines().map(str::to_owned).collect()
523        }
524    }
525
526    fn get_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
527        let s = srv!(self);
528        let owned = unsafe {
529            (s.get_block_nbt)(
530                s.ctx,
531                YogStr::from_str(dimension),
532                yog_abi::YogBlockPos {
533                    x: pos.x,
534                    y: pos.y,
535                    z: pos.z,
536                },
537            )
538        };
539
540        take_owned(s.free_str, owned)
541    }
542
543    fn set_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos, snbt: &str) -> bool {
544        let s = srv!(self);
545        unsafe {
546            (s.set_block_nbt)(
547                s.ctx,
548                YogStr::from_str(dimension),
549                yog_abi::YogBlockPos {
550                    x: pos.x,
551                    y: pos.y,
552                    z: pos.z,
553                },
554                YogStr::from_str(snbt),
555            )
556        }
557    }
558
559    fn get_inventory_slot(
560        &self,
561        dimension: &str,
562        pos: yog_core::BlockPos,
563        slot: u32,
564    ) -> Option<(String, u32, String)> {
565        let s = srv!(self);
566        let owned = unsafe {
567            (s.get_inventory_slot)(
568                s.ctx,
569                YogStr::from_str(dimension),
570                yog_abi::YogBlockPos {
571                    x: pos.x,
572                    y: pos.y,
573                    z: pos.z,
574                },
575                slot,
576            )
577        };
578        let text = take_owned(s.free_str, owned)?;
579        let mut it = text.splitn(3, '\t');
580        let item_id = it.next()?.to_owned();
581        let count = it.next()?.parse().ok()?;
582        let snbt = it.next().unwrap_or("{}").to_owned();
583        Some((item_id, count, snbt))
584    }
585
586    fn set_inventory_slot(
587        &self,
588        dimension: &str,
589        pos: yog_core::BlockPos,
590        slot: u32,
591        item_id: &str,
592        count: u32,
593        snbt: &str,
594    ) -> bool {
595        // The ABI's item id carries the SNBT after a tab.
596        let item = if snbt.is_empty() { item_id.to_string() } else { format!("{item_id}\t{snbt}") };
597        let s = srv!(self);
598        unsafe {
599            (s.set_inventory_slot)(
600                s.ctx,
601                YogStr::from_str(dimension),
602                yog_abi::YogBlockPos {
603                    x: pos.x,
604                    y: pos.y,
605                    z: pos.z,
606                },
607                slot,
608                YogStr::from_str(&item),
609                count,
610            )
611        }
612    }
613
614    fn player_inventory(&self, player: &str) -> Vec<(u32, String, u32)> {
615        let s = srv!(self);
616        let owned = unsafe { (s.player_inventory)(s.ctx, YogStr::from_str(player)) };
617        let Some(text) = take_owned(s.free_str, owned) else {
618            return Vec::new();
619        };
620        text.lines()
621            .filter_map(|line| {
622                let mut it = line.split('\t');
623                let slot: u32 = it.next()?.parse().ok()?;
624                let item_id = it.next()?.to_owned();
625                let count: u32 = it.next()?.parse().ok()?;
626                Some((slot, item_id, count))
627            })
628            .collect()
629    }
630
631    fn player_set_slot(&self, player: &str, slot: u32, item_id: &str, count: u32) -> bool {
632        let s = srv!(self);
633        unsafe {
634            (s.player_set_slot)(
635                s.ctx,
636                YogStr::from_str(player),
637                slot,
638                YogStr::from_str(item_id),
639                count,
640            )
641        }
642    }
643
644    fn teleport_to_dim(&self, player: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
645        let s = srv!(self);
646        unsafe {
647            (s.player_teleport_dim)(
648                s.ctx,
649                YogStr::from_str(player),
650                YogStr::from_str(dimension),
651                yog_abi::YogVec3 { x, y, z },
652            )
653        }
654    }
655
656    fn entity_teleport_to_dim(&self, uuid: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
657        let s = srv!(self);
658        unsafe {
659            (s.entity_teleport_dim)(
660                s.ctx,
661                YogStr::from_str(uuid),
662                YogStr::from_str(dimension),
663                yog_abi::YogVec3 { x, y, z },
664            )
665        }
666    }
667
668    fn world_entity_count(&self, dimension: &str, entity_type: &str) -> i32 {
669        let s = srv!(self);
670        unsafe {
671            (s.world_entity_count)(
672                s.ctx,
673                YogStr::from_str(dimension),
674                YogStr::from_str(entity_type),
675            )
676        }
677    }
678
679    fn entity_get_nbt(&self, uuid: &str) -> Option<String> {
680        let s = srv!(self);
681        let owned = unsafe { (s.entity_get_nbt)(s.ctx, YogStr::from_str(uuid)) };
682
683        take_owned(s.free_str, owned)
684    }
685
686    fn entity_set_nbt(&self, uuid: &str, snbt: &str) -> bool {
687        let s = srv!(self);
688        unsafe { (s.entity_set_nbt)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(snbt)) }
689    }
690
691    fn spawn_particles(
692        &self,
693        dimension: &str,
694        x: f64,
695        y: f64,
696        z: f64,
697        particle_type: &str,
698        count: i32,
699        dx: f64,
700        dy: f64,
701        dz: f64,
702        speed: f64,
703    ) -> bool {
704        let s = srv!(self);
705        unsafe {
706            (s.spawn_particles)(
707                s.ctx,
708                YogStr::from_str(dimension),
709                yog_abi::YogVec3 { x, y, z },
710                YogStr::from_str(particle_type),
711                count,
712                dx,
713                dy,
714                dz,
715                speed,
716            )
717        }
718    }
719
720    fn entity_attribute_get(&self, uuid: &str, attribute_id: &str) -> Option<f64> {
721        let s = srv!(self);
722        let v = unsafe {
723            (s.entity_attribute_get)(
724                s.ctx,
725                YogStr::from_str(uuid),
726                YogStr::from_str(attribute_id),
727            )
728        };
729        if v.is_nan() { None } else { Some(v) }
730    }
731
732    fn entity_attribute_set(&self, uuid: &str, attribute_id: &str, value: f64) -> bool {
733        let s = srv!(self);
734        unsafe {
735            (s.entity_attribute_set)(
736                s.ctx,
737                YogStr::from_str(uuid),
738                YogStr::from_str(attribute_id),
739                value,
740            )
741        }
742    }
743
744    fn get_held_item_nbt(&self, player: &str) -> Option<String> {
745        let s = srv!(self);
746        let owned = unsafe { (s.get_held_item_nbt)(s.ctx, YogStr::from_str(player)) };
747
748        take_owned(s.free_str, owned)
749    }
750
751    fn set_held_item_nbt(&self, player: &str, snbt: &str) -> bool {
752        let s = srv!(self);
753        unsafe { (s.set_held_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
754    }
755
756    fn get_offhand_item_nbt(&self, player: &str) -> Option<String> {
757        let s = srv!(self);
758        let owned = unsafe { (s.get_offhand_item_nbt)(s.ctx, YogStr::from_str(player)) };
759
760        take_owned(s.free_str, owned)
761    }
762
763    fn set_offhand_item_nbt(&self, player: &str, snbt: &str) -> bool {
764        let s = srv!(self);
765        unsafe { (s.set_offhand_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
766    }
767
768    fn get_slot_item(&self, player: &str, slot: u32) -> Option<(String, u32, String)> {
769        let s = srv!(self);
770        let owned = unsafe { (s.get_slot_item)(s.ctx, YogStr::from_str(player), slot) };
771        let text = take_owned(s.free_str, owned)?;
772        let mut it = text.splitn(3, '\t');
773        let item_id = it.next()?.to_owned();
774        let count: u32 = it.next()?.parse().ok()?;
775        let nbt = it.next().unwrap_or("{}").to_owned();
776        Some((item_id, count, nbt))
777    }
778
779    fn set_slot_item(
780        &self,
781        player: &str,
782        slot: u32,
783        item_id: &str,
784        count: u32,
785        snbt: &str,
786    ) -> bool {
787        let s = srv!(self);
788        unsafe {
789            (s.set_slot_item)(
790                s.ctx,
791                YogStr::from_str(player),
792                slot,
793                YogStr::from_str(item_id),
794                count,
795                YogStr::from_str(snbt),
796            )
797        }
798    }
799
800    // ── world queries & effects (ABI minor 1) ────────────────────────────────
801
802    fn entity_info(&self, uuid: &str) -> Option<yog_core::EntityInfo> {
803        let s = srv!(self);
804        let owned = unsafe { (s.entity_info)(s.ctx, YogStr::from_str(uuid)) };
805        take_owned(s.free_str, owned).and_then(|l| yog_core::EntityInfo::decode(&l))
806    }
807
808    fn entities_in_box(
809        &self,
810        dimension: &str,
811        min: (f64, f64, f64),
812        max: (f64, f64, f64),
813    ) -> Vec<String> {
814        let s = srv!(self);
815        let owned =
816            unsafe { (s.entities_in_box)(s.ctx, YogStr::from_str(dimension), v3(min), v3(max)) };
817        take_owned(s.free_str, owned)
818            .map(|t| {
819                t.lines()
820                    .filter(|l| !l.is_empty())
821                    .map(str::to_owned)
822                    .collect()
823            })
824            .unwrap_or_default()
825    }
826
827    fn raycast_block(
828        &self,
829        dimension: &str,
830        from: (f64, f64, f64),
831        to: (f64, f64, f64),
832    ) -> Option<yog_core::BlockHit> {
833        let s = srv!(self);
834        let owned =
835            unsafe { (s.raycast_block)(s.ctx, YogStr::from_str(dimension), v3(from), v3(to)) };
836        take_owned(s.free_str, owned).and_then(|l| yog_core::BlockHit::decode(&l))
837    }
838
839    fn raycast_entity(
840        &self,
841        dimension: &str,
842        from: (f64, f64, f64),
843        to: (f64, f64, f64),
844        exclude: &str,
845    ) -> Option<String> {
846        let s = srv!(self);
847        let owned = unsafe {
848            (s.raycast_entity)(
849                s.ctx,
850                YogStr::from_str(dimension),
851                v3(from),
852                v3(to),
853                YogStr::from_str(exclude),
854            )
855        };
856        take_owned(s.free_str, owned).filter(|u| !u.is_empty())
857    }
858
859    fn block_info(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<yog_core::BlockInfo> {
860        let s = srv!(self);
861        let owned = unsafe { (s.block_info)(s.ctx, YogStr::from_str(dimension), bp(pos)) };
862        take_owned(s.free_str, owned).and_then(|l| yog_core::BlockInfo::decode(&l))
863    }
864
865    fn break_block(
866        &self,
867        dimension: &str,
868        pos: yog_core::BlockPos,
869        drop: bool,
870        breaker: &str,
871    ) -> bool {
872        let s = srv!(self);
873        unsafe {
874            (s.break_block)(
875                s.ctx,
876                YogStr::from_str(dimension),
877                bp(pos),
878                drop,
879                YogStr::from_str(breaker),
880            )
881        }
882    }
883
884    fn place_block_from_slot(
885        &self,
886        player: &str,
887        slot: u32,
888        dimension: &str,
889        pos: yog_core::BlockPos,
890    ) -> bool {
891        let s = srv!(self);
892        unsafe {
893            (s.place_block_from_slot)(
894                s.ctx,
895                YogStr::from_str(player),
896                slot,
897                YogStr::from_str(dimension),
898                bp(pos),
899            )
900        }
901    }
902
903    fn bonemeal(&self, dimension: &str, pos: yog_core::BlockPos) -> bool {
904        let s = srv!(self);
905        unsafe { (s.bonemeal)(s.ctx, YogStr::from_str(dimension), bp(pos)) }
906    }
907
908    fn explode(
909        &self,
910        dimension: &str,
911        x: f64,
912        y: f64,
913        z: f64,
914        power: f32,
915        fire: bool,
916        source: &str,
917    ) -> bool {
918        let s = srv!(self);
919        unsafe {
920            (s.explode)(
921                s.ctx,
922                YogStr::from_str(dimension),
923                v3((x, y, z)),
924                power,
925                fire,
926                YogStr::from_str(source),
927            )
928        }
929    }
930
931    fn tellraw(&self, player: &str, json: &str) -> bool {
932        let s = srv!(self);
933        unsafe { (s.tellraw)(s.ctx, YogStr::from_str(player), YogStr::from_str(json)) }
934    }
935
936    fn entity_damage(&self, uuid: &str, amount: f32, damage_type: &str, attacker: &str) -> bool {
937        let s = srv!(self);
938        unsafe {
939            (s.entity_damage)(
940                s.ctx,
941                YogStr::from_str(uuid),
942                amount,
943                YogStr::from_str(damage_type),
944                YogStr::from_str(attacker),
945            )
946        }
947    }
948
949    fn entity_set_fire_ticks(&self, uuid: &str, ticks: i32) -> bool {
950        let s = srv!(self);
951        unsafe { (s.entity_set_fire_ticks)(s.ctx, YogStr::from_str(uuid), ticks) }
952    }
953
954    fn player_add_xp(&self, player: &str, amount: i32) -> bool {
955        let s = srv!(self);
956        unsafe { (s.player_add_xp)(s.ctx, YogStr::from_str(player), amount) }
957    }
958
959    fn player_set_may_fly(&self, player: &str, may_fly: bool) -> bool {
960        let s = srv!(self);
961        unsafe { (s.player_set_may_fly)(s.ctx, YogStr::from_str(player), may_fly) }
962    }
963
964    fn player_info(&self, player: &str) -> Option<yog_core::PlayerInfo> {
965        let s = srv!(self);
966        let owned = unsafe { (s.player_info)(s.ctx, YogStr::from_str(player)) };
967        take_owned(s.free_str, owned).and_then(|l| yog_core::PlayerInfo::decode(&l))
968    }
969
970    fn spawn_item(
971        &self,
972        dimension: &str,
973        pos: (f64, f64, f64),
974        vel: (f64, f64, f64),
975        item_id: &str,
976        count: u32,
977        snbt: &str,
978    ) -> Option<String> {
979        let s = srv!(self);
980        let owned = unsafe {
981            (s.spawn_item)(
982                s.ctx,
983                YogStr::from_str(dimension),
984                v3(pos),
985                v3(vel),
986                YogStr::from_str(item_id),
987                count,
988                YogStr::from_str(snbt),
989            )
990        };
991        take_owned(s.free_str, owned).filter(|u| !u.is_empty())
992    }
993
994    fn item_entity_stack(&self, uuid: &str) -> Option<(String, u32, String)> {
995        let s = srv!(self);
996        let owned = unsafe { (s.item_entity_stack)(s.ctx, YogStr::from_str(uuid)) };
997        let text = take_owned(s.free_str, owned)?;
998        let mut it = text.splitn(3, '\t');
999        let item_id = it.next()?.to_owned();
1000        let count = it.next()?.parse().ok()?;
1001        let nbt = it.next().unwrap_or("{}").to_owned();
1002        Some((item_id, count, nbt))
1003    }
1004
1005    fn item_entity_set_stack(&self, uuid: &str, item_id: &str, count: u32, snbt: &str) -> bool {
1006        let s = srv!(self);
1007        unsafe {
1008            (s.item_entity_set_stack)(
1009                s.ctx,
1010                YogStr::from_str(uuid),
1011                YogStr::from_str(item_id),
1012                count,
1013                YogStr::from_str(snbt),
1014            )
1015        }
1016    }
1017
1018    fn actionbar_json(&self, player: &str, json: &str) -> bool {
1019        let s = srv!(self);
1020        unsafe { (s.actionbar_json)(s.ctx, YogStr::from_str(player), YogStr::from_str(json)) }
1021    }
1022
1023    fn use_item_on_block(
1024        &self,
1025        dimension: &str,
1026        pos: yog_core::BlockPos,
1027        face: yog_core::Face,
1028        item_id: &str,
1029        player: &str,
1030    ) -> bool {
1031        let s = srv!(self);
1032        unsafe {
1033            (s.use_item_on_block)(
1034                s.ctx,
1035                YogStr::from_str(dimension),
1036                bp(pos),
1037                face.index(),
1038                YogStr::from_str(item_id),
1039                YogStr::from_str(player),
1040            )
1041        }
1042    }
1043
1044    fn pour_fluid(&self, dimension: &str, pos: yog_core::BlockPos, bucket_item_id: &str) -> bool {
1045        let s = srv!(self);
1046        unsafe {
1047            (s.pour_fluid)(
1048                s.ctx,
1049                YogStr::from_str(dimension),
1050                bp(pos),
1051                YogStr::from_str(bucket_item_id),
1052            )
1053        }
1054    }
1055
1056    fn has_entity_tag(&self, entity_type: &str, tag_id: &str) -> bool {
1057        let s = srv!(self);
1058        unsafe {
1059            (s.has_entity_tag)(
1060                s.ctx,
1061                YogStr::from_str(entity_type),
1062                YogStr::from_str(tag_id),
1063            )
1064        }
1065    }
1066
1067    fn item_block(&self, item_id: &str) -> Option<String> {
1068        let s = srv!(self);
1069        let owned = unsafe { (s.item_block)(s.ctx, YogStr::from_str(item_id)) };
1070        take_owned(s.free_str, owned).filter(|b| !b.is_empty())
1071    }
1072
1073    fn place_feature(&self, dimension: &str, pos: yog_core::BlockPos, feature_id: &str) -> bool {
1074        let s = srv!(self);
1075        unsafe {
1076            (s.place_feature)(
1077                s.ctx,
1078                YogStr::from_str(dimension),
1079                bp(pos),
1080                YogStr::from_str(feature_id),
1081            )
1082        }
1083    }
1084
1085    fn set_item_cooldown(&self, player: &str, item_id: &str, ticks: u32) -> bool {
1086        let s = srv!(self);
1087        unsafe { (s.set_item_cooldown)(s.ctx, YogStr::from_str(player), YogStr::from_str(item_id), ticks.min(i32::MAX as u32) as i32) }
1088    }
1089
1090    fn entity_vehicle(&self, uuid: &str) -> Option<String> {
1091        let s = srv!(self);
1092        let owned = unsafe { (s.entity_vehicle)(s.ctx, YogStr::from_str(uuid)) };
1093        take_owned(s.free_str, owned).filter(|v| !v.is_empty())
1094    }
1095
1096    fn entity_passengers(&self, uuid: &str) -> Vec<String> {
1097        let s = srv!(self);
1098        let owned = unsafe { (s.entity_passengers)(s.ctx, YogStr::from_str(uuid)) };
1099        take_owned(s.free_str, owned)
1100            .map(|t| t.lines().filter(|l| !l.is_empty()).map(str::to_string).collect())
1101            .unwrap_or_default()
1102    }
1103
1104    fn entity_mount(&self, rider: &str, vehicle: &str, force: bool) -> bool {
1105        let s = srv!(self);
1106        unsafe { (s.entity_mount)(s.ctx, YogStr::from_str(rider), YogStr::from_str(vehicle), force) }
1107    }
1108
1109    fn entity_dismount(&self, uuid: &str) -> bool {
1110        let s = srv!(self);
1111        unsafe { (s.entity_dismount)(s.ctx, YogStr::from_str(uuid)) }
1112    }
1113
1114    fn entity_data(&self, uuid: &str, key: &str) -> Option<Vec<u8>> {
1115        let s = srv!(self);
1116        let mut out = Vec::new();
1117        let found = unsafe {
1118            (s.entity_data_get)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(key), &mut out as *mut Vec<u8> as *mut c_void, crate::body::collect)
1119        };
1120        found.then_some(out)
1121    }
1122
1123    fn set_entity_data(&self, uuid: &str, key: &str, value: Option<&[u8]>, persist: bool) -> bool {
1124        let s = srv!(self);
1125        let flags = value.map_or(0, |_| 1) | if persist { 2 } else { 0 };
1126        let v = value.unwrap_or(&[]);
1127        unsafe { (s.entity_data_set)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(key), v.as_ptr(), v.len() as u32, flags) }
1128    }
1129
1130    fn entity_set_dimensions(&self, uuid: &str, size: Option<(f32, f32, f32)>) -> bool {
1131        let s = srv!(self);
1132        let (w, h, e) = size.unwrap_or((-1.0, 0.0, 0.0));
1133        unsafe { (s.entity_set_dimensions)(s.ctx, YogStr::from_str(uuid), w, h, e) }
1134    }
1135
1136    fn entity_set_fall_distance(&self, uuid: &str, distance: f32) -> bool {
1137        let s = srv!(self);
1138        unsafe { (s.entity_set_fall_distance)(s.ctx, YogStr::from_str(uuid), distance) }
1139    }
1140
1141    fn redstone_signal(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<u8> {
1142        let s = srv!(self);
1143        let p = unsafe { (s.redstone_signal)(s.ctx, YogStr::from_str(dimension), bp(pos)) };
1144        (0..=15).contains(&p).then_some(p as u8)
1145    }
1146
1147    fn redstone_power(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<u8> {
1148        let s = srv!(self);
1149        let p = unsafe { (s.redstone_power)(s.ctx, YogStr::from_str(dimension), bp(pos)) };
1150        (0..=15).contains(&p).then_some(p as u8)
1151    }
1152
1153    fn set_redstone_output(&self, dimension: &str, pos: yog_core::BlockPos, level: u8) -> bool {
1154        let s = srv!(self);
1155        unsafe { (s.set_redstone_output)(s.ctx, YogStr::from_str(dimension), bp(pos), level.min(15) as i32) }
1156    }
1157
1158    fn get_block_state(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
1159        let s = srv!(self);
1160        let owned = unsafe { (s.get_block_state)(s.ctx, YogStr::from_str(dimension), bp(pos)) };
1161        take_owned(s.free_str, owned).filter(|b| !b.is_empty())
1162    }
1163
1164    fn set_block_state(&self, dimension: &str, pos: yog_core::BlockPos, state: &str) -> bool {
1165        let s = srv!(self);
1166        unsafe { (s.set_block_state)(s.ctx, YogStr::from_str(dimension), bp(pos), YogStr::from_str(state)) }
1167    }
1168
1169    fn world_is_thundering(&self, dimension: &str) -> bool {
1170        let s = srv!(self);
1171        unsafe { (s.world_is_thundering)(s.ctx, YogStr::from_str(dimension)) }
1172    }
1173
1174    fn world_set_thunder(&self, dimension: &str, thundering: bool, duration_ticks: i32) -> bool {
1175        let s = srv!(self);
1176        unsafe { (s.world_set_thunder)(s.ctx, YogStr::from_str(dimension), thundering, duration_ticks) }
1177    }
1178
1179    fn fill_region(&self, dimension: &str, from: yog_core::BlockPos, to: yog_core::BlockPos, state: &str) -> Option<u32> {
1180        let s = srv!(self);
1181        let n = unsafe { (s.fill_region)(s.ctx, YogStr::from_str(dimension), bp(from), bp(to), YogStr::from_str(state)) };
1182        u32::try_from(n).ok()
1183    }
1184
1185    fn scan_region(&self, dimension: &str, from: yog_core::BlockPos, to: yog_core::BlockPos) -> Vec<(yog_core::BlockPos, String)> {
1186        let s = srv!(self);
1187        let owned = unsafe { (s.scan_region)(s.ctx, YogStr::from_str(dimension), bp(from), bp(to)) };
1188        let Some(text) = take_owned(s.free_str, owned) else {
1189            return Vec::new();
1190        };
1191        text.lines()
1192            .filter_map(|line| {
1193                let mut it = line.splitn(4, ' ');
1194                let (x, y, z) = (it.next()?.parse().ok()?, it.next()?.parse().ok()?, it.next()?.parse().ok()?);
1195                Some((yog_core::BlockPos { x, y, z }, it.next()?.to_string()))
1196            })
1197            .collect()
1198    }
1199}
1200
1201/// Copy a runtime-owned string out and free it; `None` for NONE.
1202pub(crate) fn take_owned(
1203    free_str: unsafe extern "C" fn(ptr: *mut u8, len: u32),
1204    owned: yog_abi::YogOwnedStr,
1205) -> Option<String> {
1206    if owned.is_none() {
1207        return None;
1208    }
1209    // SAFETY: a non-NONE owned string is `len` valid bytes until freed.
1210    let text = unsafe {
1211        String::from_utf8_lossy(std::slice::from_raw_parts(owned.ptr, owned.len as usize))
1212            .into_owned()
1213    };
1214    unsafe { free_str(owned.ptr, owned.len) };
1215    Some(text)
1216}
1217
1218fn v3((x, y, z): (f64, f64, f64)) -> yog_abi::YogVec3 {
1219    yog_abi::YogVec3 { x, y, z }
1220}
1221
1222fn bp(pos: yog_core::BlockPos) -> yog_abi::YogBlockPos {
1223    yog_abi::YogBlockPos {
1224        x: pos.x,
1225        y: pos.y,
1226        z: pos.z,
1227    }
1228}
1229
1230// ── Trampoline helpers ────────────────────────────────────────────────────────
1231//
1232// A panic must never unwind out of a trampoline: the runtime calls them
1233// through `extern "C"`, so it would abort the whole game. `guard` stops it
1234// at the boundary — the panic message is already in the log — and the
1235// event goes on as if the mod hadn't handled it.
1236
1237fn guard<R>(fallback: R, call: impl FnOnce() -> R) -> R {
1238    std::panic::catch_unwind(std::panic::AssertUnwindSafe(call)).unwrap_or_else(|_| {
1239        crate::error!("a mod's callback panicked (see above); carrying on without it");
1240        fallback
1241    })
1242}
1243//
1244// Each trampoline is a `unsafe extern "C" fn` that:
1245//   1. Casts `ud` back to the original boxed closure.
1246//   2. Converts ABI C structs to Rust event types.
1247//   3. Builds a `CServer` from the `*const YogServer`.
1248//   4. Calls the closure.
1249//
1250// Closures are Box::into_raw'd in Registry methods — they are INTENTIONALLY
1251// leaked and live for the process lifetime.
1252
1253// ── Trampoline helpers ────────────────────────────────────────────────────────
1254//
1255// All phased event trampolines share the same pattern:
1256//   `phase: u8`  0 = EventPhase::Pre, 1 = EventPhase::Post
1257//   Return value is meaningful only in Pre phase.
1258
1259macro_rules! trampoline_phased {
1260    ($name:ident, $abi_ev:ty, $rust_ev:ty, |$ev:ident| $build:expr_2021) => {
1261        unsafe extern "C" fn $name<F>(
1262            ud: *mut c_void,
1263            srv: *const YogServer,
1264            ev: *const $abi_ev,
1265            phase: u8,
1266        ) -> bool
1267        where
1268            F: Fn(&$rust_ev, EventPhase, &dyn Server) -> bool + Send + Sync,
1269        {
1270            unsafe {
1271                let f = &*(ud as *const F);
1272                let $ev = &*ev;
1273                let rust_ev = $build;
1274                let p = if phase == 0 {
1275                    EventPhase::Pre
1276                } else {
1277                    EventPhase::Post
1278                };
1279                guard(true, || f(&rust_ev, p, &CServer(srv)))
1280            }
1281        }
1282    };
1283}
1284
1285trampoline_phased!(
1286    trampoline_block_break,
1287    YogBlockBreakEvent,
1288    BlockBreakEvent,
1289    |ev| BlockBreakEvent {
1290        player_name: ev.player.as_str().to_owned(),
1291        block_id: ev.block.as_str().to_owned(),
1292        pos: yog_core::BlockPos {
1293            x: ev.pos.x,
1294            y: ev.pos.y,
1295            z: ev.pos.z
1296        },
1297        dimension: ev.dimension.as_str().to_owned(),
1298    }
1299);
1300
1301trampoline_phased!(trampoline_chat, YogChatEvent, ChatEvent, |ev| ChatEvent {
1302    player_name: ev.player.as_str().to_owned(),
1303    message: ev.message.as_str().to_owned(),
1304    dimension: ev.dimension.as_str().to_owned(),
1305});
1306
1307trampoline_phased!(
1308    trampoline_player_join,
1309    YogPlayerEvent,
1310    PlayerJoinEvent,
1311    |ev| PlayerJoinEvent {
1312        player_name: ev.player.as_str().to_owned(),
1313        uuid: ev.uuid.as_str().to_owned(),
1314        dimension: ev.dimension.as_str().to_owned(),
1315    }
1316);
1317
1318trampoline_phased!(
1319    trampoline_player_leave,
1320    YogPlayerEvent,
1321    PlayerLeaveEvent,
1322    |ev| PlayerLeaveEvent {
1323        player_name: ev.player.as_str().to_owned(),
1324        uuid: ev.uuid.as_str().to_owned(),
1325        dimension: ev.dimension.as_str().to_owned(),
1326    }
1327);
1328
1329trampoline_phased!(trampoline_use_item, YogUseItemEvent, UseItemEvent, |ev| {
1330    UseItemEvent {
1331        player_name: ev.player.as_str().to_owned(),
1332        item_id: ev.item.as_str().to_owned(),
1333        sneaking: ev.sneaking,
1334        dimension: ev.dimension.as_str().to_owned(),
1335    }
1336});
1337
1338trampoline_phased!(
1339    trampoline_use_block,
1340    YogUseBlockEvent,
1341    UseBlockEvent,
1342    |ev| UseBlockEvent {
1343        player_name: ev.player.as_str().to_owned(),
1344        block_id: ev.block.as_str().to_owned(),
1345        pos: yog_core::BlockPos {
1346            x: ev.pos.x,
1347            y: ev.pos.y,
1348            z: ev.pos.z
1349        },
1350        dimension: ev.dimension.as_str().to_owned(),
1351        sneaking: ev.sneaking,
1352    }
1353);
1354
1355trampoline_phased!(
1356    trampoline_attack_entity,
1357    YogAttackEntityEvent,
1358    AttackEntityEvent,
1359    |ev| AttackEntityEvent {
1360        player_name: ev.player.as_str().to_owned(),
1361        target_type: ev.target_type.as_str().to_owned(),
1362        target_uuid: ev.target_uuid.as_str().to_owned(),
1363        dimension: ev.dimension.as_str().to_owned(),
1364    }
1365);
1366
1367trampoline_phased!(
1368    trampoline_entity_damage,
1369    YogEntityDamageEvent,
1370    EntityDamageEvent,
1371    |ev| EntityDamageEvent {
1372        entity_type: ev.entity_type.as_str().to_owned(),
1373        uuid: ev.uuid.as_str().to_owned(),
1374        amount: ev.amount,
1375        source: ev.source.as_str().to_owned(),
1376        dimension: ev.dimension.as_str().to_owned(),
1377    }
1378);
1379
1380trampoline_phased!(
1381    trampoline_entity_death,
1382    YogEntityDeathEvent,
1383    EntityDeathEvent,
1384    |ev| EntityDeathEvent {
1385        entity_type: ev.entity_type.as_str().to_owned(),
1386        uuid: ev.uuid.as_str().to_owned(),
1387        source: ev.source.as_str().to_owned(),
1388        dimension: ev.dimension.as_str().to_owned(),
1389    }
1390);
1391
1392trampoline_phased!(
1393    trampoline_entity_spawn,
1394    YogEntitySpawnEvent,
1395    EntitySpawnEvent,
1396    |ev| EntitySpawnEvent {
1397        entity_type: ev.entity_type.as_str().to_owned(),
1398        uuid: ev.uuid.as_str().to_owned(),
1399        dimension: ev.dimension.as_str().to_owned(),
1400    }
1401);
1402
1403trampoline_phased!(
1404    trampoline_place_block,
1405    YogPlaceBlockEvent,
1406    PlaceBlockEvent,
1407    |ev| PlaceBlockEvent {
1408        player_name: ev.player.as_str().to_owned(),
1409        block_id: ev.block.as_str().to_owned(),
1410        pos: yog_core::BlockPos {
1411            x: ev.pos.x,
1412            y: ev.pos.y,
1413            z: ev.pos.z
1414        },
1415        dimension: ev.dimension.as_str().to_owned(),
1416        sneaking: ev.sneaking,
1417    }
1418);
1419
1420trampoline_phased!(
1421    trampoline_player_death,
1422    YogPlayerDeathEvent,
1423    PlayerDeathEvent,
1424    |ev| PlayerDeathEvent {
1425        player_name: ev.player.as_str().to_owned(),
1426        uuid: ev.uuid.as_str().to_owned(),
1427        source: ev.source.as_str().to_owned(),
1428        dimension: ev.dimension.as_str().to_owned(),
1429    }
1430);
1431
1432trampoline_phased!(
1433    trampoline_player_respawn,
1434    YogPlayerRespawnEvent,
1435    PlayerRespawnEvent,
1436    |ev| PlayerRespawnEvent {
1437        player_name: ev.player.as_str().to_owned(),
1438        uuid: ev.uuid.as_str().to_owned(),
1439        at_anchor: ev.at_anchor,
1440        dimension: ev.dimension.as_str().to_owned(),
1441    }
1442);
1443
1444trampoline_phased!(
1445    trampoline_advancement,
1446    YogAdvancementEvent,
1447    AdvancementEvent,
1448    |ev| AdvancementEvent {
1449        player_name: ev.player.as_str().to_owned(),
1450        uuid: ev.uuid.as_str().to_owned(),
1451        advancement_id: ev.advancement.as_str().to_owned(),
1452        dimension: ev.dimension.as_str().to_owned(),
1453    }
1454);
1455
1456trampoline_phased!(
1457    trampoline_entity_interact,
1458    YogEntityInteractEvent,
1459    EntityInteractEvent,
1460    |ev| EntityInteractEvent {
1461        player_name: ev.player.as_str().to_owned(),
1462        player_uuid: ev.player_uuid.as_str().to_owned(),
1463        entity_type: ev.entity_type.as_str().to_owned(),
1464        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1465        hand: ev.hand.as_str().to_owned(),
1466        dimension: ev.dimension.as_str().to_owned(),
1467    }
1468);
1469
1470trampoline_phased!(trampoline_craft, YogCraftEvent, CraftEvent, |ev| {
1471    CraftEvent {
1472        player_name: ev.player.as_str().to_owned(),
1473        player_uuid: ev.player_uuid.as_str().to_owned(),
1474        result_item: ev.result_item.as_str().to_owned(),
1475        result_count: ev.result_count,
1476        dimension: ev.dimension.as_str().to_owned(),
1477    }
1478});
1479
1480/// A machine — an inventory whose `.yui` has a `<RecipeArea kind="…">` —
1481/// making a recipe of its kind, like a crafting table: the result shows as
1482/// soon as its slots hold a recipe's ingredients, and taking it takes them.
1483///
1484/// - `Pre` — whether it may: `false` and the result doesn't show (a
1485///   workbench short of what the recipe's `values` cost). `player` is empty.
1486/// - `Post` — a player took the result; the host takes the ingredients, the mod
1487///   its share (the workbench's resources).
1488#[derive(Debug, Clone)]
1489pub struct MachineCraftEvent {
1490    /// The machine's inventory, e.g. `"mymod:press"`.
1491    pub machine: String,
1492    /// The recipe's kind (`"mymod:pressing"`) and id.
1493    pub kind: String,
1494    pub recipe: String,
1495    pub result_item: String,
1496    pub result_count: u32,
1497    /// The recipe's values: `{ "energy": 4 }`.
1498    pub values: yog_data::Value,
1499    pub pos: yog_core::BlockPos,
1500    pub dimension: String,
1501    pub player: String,
1502}
1503
1504trampoline_phased!(trampoline_machine_craft, YogMachineCraftEvent, MachineCraftEvent, |ev| {
1505    MachineCraftEvent {
1506        machine: ev.machine.as_str().to_owned(),
1507        kind: ev.kind.as_str().to_owned(),
1508        recipe: ev.recipe.as_str().to_owned(),
1509        result_item: ev.result_item.as_str().to_owned(),
1510        result_count: ev.result_count,
1511        values: yog_data::json::from_json_text(ev.values.as_str()).unwrap_or(yog_data::Value::Null),
1512        pos: yog_core::BlockPos { x: ev.pos.x, y: ev.pos.y, z: ev.pos.z },
1513        dimension: ev.dimension.as_str().to_owned(),
1514        player: ev.player.as_str().to_owned(),
1515    }
1516});
1517
1518trampoline_phased!(
1519    trampoline_explosion,
1520    YogExplosionEvent,
1521    ExplosionEvent,
1522    |ev| ExplosionEvent {
1523        dimension: ev.dimension.as_str().to_owned(),
1524        x: ev.x,
1525        y: ev.y,
1526        z: ev.z,
1527        power: ev.power,
1528        cause_uuid: ev.cause_uuid.as_str().to_owned(),
1529    }
1530);
1531
1532trampoline_phased!(
1533    trampoline_item_insert,
1534    YogItemInsertEvent,
1535    ItemInsertEvent,
1536    |ev| ItemInsertEvent {
1537        block_id: ev.block.as_str().to_owned(),
1538        pos: yog_core::BlockPos {
1539            x: ev.pos.x,
1540            y: ev.pos.y,
1541            z: ev.pos.z
1542        },
1543        dimension: ev.dimension.as_str().to_owned(),
1544        item_id: ev.item_id.as_str().to_owned(),
1545        count: ev.count,
1546        nbt: ev.nbt.as_str().to_owned(),
1547    }
1548);
1549
1550trampoline_phased!(
1551    trampoline_item_pickup,
1552    YogItemPickupEvent,
1553    ItemPickupEvent,
1554    |ev| ItemPickupEvent {
1555        player_name: ev.player.as_str().to_owned(),
1556        player_uuid: ev.player_uuid.as_str().to_owned(),
1557        item_id: ev.item_id.as_str().to_owned(),
1558        item_count: ev.item_count,
1559        entity_uuid: ev.entity_uuid.as_str().to_owned(),
1560        dimension: ev.dimension.as_str().to_owned(),
1561    }
1562);
1563
1564trampoline_phased!(
1565    trampoline_player_move,
1566    YogPlayerMoveEvent,
1567    PlayerMoveEvent,
1568    |ev| PlayerMoveEvent {
1569        player_name: ev.player.as_str().to_owned(),
1570        player_uuid: ev.player_uuid.as_str().to_owned(),
1571        x: ev.x,
1572        y: ev.y,
1573        z: ev.z,
1574        yaw: ev.yaw,
1575        pitch: ev.pitch,
1576        dimension: ev.dimension.as_str().to_owned(),
1577    }
1578);
1579
1580trampoline_phased!(
1581    trampoline_container_open,
1582    YogContainerOpenEvent,
1583    ContainerOpenEvent,
1584    |ev| ContainerOpenEvent {
1585        player_name: ev.player.as_str().to_owned(),
1586        player_uuid: ev.player_uuid.as_str().to_owned(),
1587        container_type: ev.container_type.as_str().to_owned(),
1588        dimension: ev.dimension.as_str().to_owned(),
1589    }
1590);
1591
1592trampoline_phased!(
1593    trampoline_container_close,
1594    YogContainerCloseEvent,
1595    ContainerCloseEvent,
1596    |ev| ContainerCloseEvent {
1597        player_name: ev.player.as_str().to_owned(),
1598        player_uuid: ev.player_uuid.as_str().to_owned(),
1599        dimension: ev.dimension.as_str().to_owned(),
1600    }
1601);
1602
1603trampoline_phased!(
1604    trampoline_projectile_hit,
1605    YogProjectileHitEvent,
1606    ProjectileHitEvent,
1607    |ev| ProjectileHitEvent {
1608        projectile_type: ev.projectile_type.as_str().to_owned(),
1609        projectile_uuid: ev.projectile_uuid.as_str().to_owned(),
1610        shooter_uuid: ev.shooter_uuid.as_str().to_owned(),
1611        hit_type: ev.hit_type.as_str().to_owned(),
1612        hit_entity_uuid: ev.hit_entity_uuid.as_str().to_owned(),
1613        x: ev.x,
1614        y: ev.y,
1615        z: ev.z,
1616        dimension: ev.dimension.as_str().to_owned(),
1617    }
1618);
1619
1620unsafe extern "C" fn trampoline_server_fn<F>(ud: *mut c_void, srv: *const YogServer)
1621where
1622    F: Fn(&dyn Server) + Send + Sync,
1623{
1624    unsafe {
1625        let f = &*(ud as *const F);
1626        guard((), || f(&CServer(srv)));
1627    }
1628}
1629
1630unsafe extern "C" fn trampoline_packet<F>(
1631    ud: *mut c_void,
1632    srv: *const YogServer,
1633    ev: *const YogPacketEvent,
1634) where
1635    F: Fn(&PacketEvent, &dyn Server) + Send + Sync,
1636{
1637    unsafe {
1638        let f = &*(ud as *const F);
1639        let ev = &*ev;
1640        let rust_ev = PacketEvent {
1641            channel: ev.channel.as_str().to_owned(),
1642            player: ev.player.as_str().to_owned(),
1643            // (A null pointer with no bytes is fine for C, not for a slice.)
1644            payload: if ev.payload.is_null() || ev.payload_len == 0 {
1645                Vec::new()
1646            } else {
1647                std::slice::from_raw_parts(ev.payload, ev.payload_len as usize).to_vec()
1648            },
1649        };
1650        guard((), || f(&rust_ev, &CServer(srv)));
1651    }
1652}
1653
1654unsafe extern "C" fn trampoline_command<F>(
1655    ud: *mut c_void,
1656    srv: *const YogServer,
1657    ev: *const YogCommandEvent,
1658    reply_buf: *mut u8,
1659    reply_cap: u32,
1660    reply_len: *mut u32,
1661) where
1662    F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync,
1663{
1664    unsafe {
1665        let f = &*(ud as *const F);
1666        let ev = &*ev;
1667        let ctx = CommandContext {
1668            name: ev.name.as_str().to_owned(),
1669            args: ev.args.as_str().to_owned(),
1670            source: ev.source.as_str().to_owned(),
1671            uuid: ev.uuid.as_str().to_owned(),
1672            dimension: ev.dimension.as_str().to_owned(),
1673        };
1674        *reply_len = 0;
1675        if let Some(reply) = guard(None, || f(&ctx, &CServer(srv))) {
1676            let bytes = reply.as_bytes();
1677            // As much as fits, cut between characters (it's read as UTF-8).
1678            let mut n = bytes.len().min(reply_cap as usize);
1679            while !reply.is_char_boundary(n) {
1680                n -= 1;
1681            }
1682            std::ptr::copy_nonoverlapping(bytes.as_ptr(), reply_buf, n);
1683            *reply_len = n as u32;
1684        }
1685    }
1686}
1687
1688unsafe extern "C" fn trampoline_scheduled<F>(ud: *mut c_void, srv: *const YogServer)
1689where
1690    F: Fn(&dyn Server) + Send + Sync,
1691{
1692    unsafe {
1693        let f = &*(ud as *const F);
1694        guard((), || f(&CServer(srv)));
1695    }
1696}
1697
1698// ── ABI minor 10 — client-side trampolines ────────────────────────────────────
1699
1700unsafe extern "C" fn trampoline_client_tick<F>(ud: *mut c_void)
1701where
1702    F: Fn(&ClientTickEvent) + Send + Sync,
1703{
1704    unsafe {
1705        let f = &*(ud as *const F);
1706        guard((), || f(&ClientTickEvent {}));
1707    }
1708}
1709
1710unsafe extern "C" fn trampoline_hud_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1711where
1712    F: Fn(&GfxContext) + Send + Sync,
1713{
1714    unsafe {
1715        let f = &*(ud as *const F);
1716        guard((), || f(&GfxContext::from_raw(gfx)));
1717    }
1718}
1719
1720/// What `Registry::on_item_tooltip` keeps: the size and the draw closures.
1721struct TooltipPicture<S, D> {
1722    size: S,
1723    draw: D,
1724}
1725
1726unsafe extern "C" fn trampoline_tooltip_size<S, D>(ud: *mut c_void, item: YogStr, snbt: YogStr, w: *mut f32, h: *mut f32) -> bool
1727where
1728    S: Fn(&str, &str) -> Option<(f32, f32)> + Send + Sync,
1729{
1730    unsafe {
1731        let t = &*(ud as *const TooltipPicture<S, D>);
1732        match guard(None, || (t.size)(item.as_str(), snbt.as_str())) {
1733            Some((pw, ph)) => {
1734                (*w, *h) = (pw, ph);
1735                true
1736            }
1737            None => false,
1738        }
1739    }
1740}
1741
1742unsafe extern "C" fn trampoline_item_text<F>(ud: *mut c_void, item: YogStr, snbt: YogStr, sink: *mut c_void, write: yog_abi::YogTextSink) -> bool
1743where
1744    F: Fn(&str, &str) -> Option<(Option<String>, Vec<String>)> + Send + Sync,
1745{
1746    unsafe {
1747        let f = &*(ud as *const F);
1748        let Some((name, lines)) = guard(None, || f(item.as_str(), snbt.as_str())) else { return false };
1749        let mut text = name.unwrap_or_default();
1750        for l in lines {
1751            text.push('\n');
1752            text.push_str(&l.replace('\n', " "));
1753        }
1754        write(sink, YogStr::from_str(&text));
1755        true
1756    }
1757}
1758
1759unsafe extern "C" fn trampoline_entity_tick<F>(ud: *mut c_void, srv: *const yog_abi::YogServer, uuid: YogStr)
1760where
1761    F: Fn(&dyn Server, &str) + Send + Sync,
1762{
1763    unsafe {
1764        let f = &*(ud as *const F);
1765        let s = CServer(srv);
1766        guard((), || f(&s, uuid.as_str()));
1767    }
1768}
1769
1770unsafe extern "C" fn trampoline_loot<F>(
1771    ud: *mut c_void,
1772    srv: *const yog_abi::YogServer,
1773    loot: *const u8,
1774    len: usize,
1775    sink: *mut c_void,
1776    write: yog_abi::YogBytesSink,
1777) where
1778    F: Fn(&dyn Server, &mut crate::loot::Loot) + Send + Sync,
1779{
1780    use yog_data::Schema;
1781    unsafe {
1782        let f = &*(ud as *const F);
1783        let Ok(mut loot) = yog_data::read_bytes::<crate::loot::Loot>(std::slice::from_raw_parts(loot, len)) else { return };
1784        let before = loot.items.clone();
1785        let s = CServer(srv);
1786        guard((), || f(&s, &mut loot));
1787        if loot.items != before {
1788            let bytes = yog_data::binary::encode(&loot.items.write());
1789            write(sink, bytes.as_ptr(), bytes.len());
1790        }
1791    }
1792}
1793
1794unsafe extern "C" fn trampoline_body<F>(ud: *mut c_void, frame: *const u8, len: usize, sink: *mut c_void, write: yog_abi::YogBytesSink)
1795where
1796    F: Fn(&yog_body::Frame, &mut yog_body::Ops) + Send + Sync,
1797{
1798    unsafe {
1799        let f = &*(ud as *const F);
1800        let Some(frame) = yog_body::Frame::decode(std::slice::from_raw_parts(frame, len)) else { return };
1801        let mut ops = yog_body::Ops::default();
1802        guard((), || f(&frame, &mut ops));
1803        if !ops.is_empty() {
1804            write(sink, ops.0.as_ptr(), ops.0.len());
1805        }
1806    }
1807}
1808
1809unsafe extern "C" fn trampoline_camera<F>(ud: *mut c_void, frame: *const yog_abi::YogCameraFrame, out: *mut yog_abi::YogCameraOut)
1810where
1811    F: Fn(&yog_body::CameraFrame, &mut yog_body::CameraOut) + Send + Sync,
1812{
1813    unsafe {
1814        let f = &*(ud as *const F);
1815        let c = &*frame;
1816        let frame = yog_body::CameraFrame {
1817            uuid: c.uuid.as_str().to_owned(),
1818            partial: c.partial,
1819            pos: c.pos,
1820            yaw: c.yaw,
1821            pitch: c.pitch,
1822            detached: c.detached,
1823            flags: c.flags,
1824        };
1825        let o = &mut *out;
1826        let mut cam = yog_body::CameraOut {
1827            local_offset: yog_body::Vec3::from_array(o.local_offset),
1828            world_offset: yog_body::Vec3::from_array(o.world_offset),
1829            yaw: o.yaw,
1830            pitch: o.pitch,
1831            roll: o.roll,
1832            fov: o.fov,
1833        };
1834        guard((), || f(&frame, &mut cam));
1835        *o = yog_abi::YogCameraOut {
1836            local_offset: cam.local_offset.to_array(),
1837            world_offset: cam.world_offset.to_array(),
1838            yaw: cam.yaw,
1839            pitch: cam.pitch,
1840            roll: cam.roll,
1841            fov: cam.fov,
1842        };
1843    }
1844}
1845
1846unsafe extern "C" fn trampoline_travel<F>(ud: *mut c_void, frame: *const yog_abi::YogTravelFrame, velocity: *mut [f32; 3]) -> bool
1847where
1848    F: Fn(&yog_body::TravelFrame) -> Option<yog_body::Vec3> + Send + Sync,
1849{
1850    unsafe {
1851        let f = &*(ud as *const F);
1852        let t = &*frame;
1853        let frame = yog_body::TravelFrame {
1854            uuid: t.uuid.as_str().to_owned(),
1855            velocity: yog_body::Vec3::from_array(t.velocity),
1856            look: yog_body::Vec3::from_array(t.look),
1857            yaw: t.yaw,
1858            pitch: t.pitch,
1859            forward: t.forward,
1860            strafe: t.strafe,
1861            jumping: t.jumping,
1862            sneaking: t.sneaking,
1863            gravity: t.gravity,
1864            flags: t.flags,
1865        };
1866        match guard(None, || f(&frame)) {
1867            Some(v) => {
1868                *velocity = v.to_array();
1869                true
1870            }
1871            None => false,
1872        }
1873    }
1874}
1875
1876#[allow(clippy::too_many_arguments)]
1877unsafe extern "C" fn trampoline_tooltip_draw<S, D>(ud: *mut c_void, gfx: *const YogGfxApi, item: YogStr, snbt: YogStr, x: f32, y: f32, w: f32, h: f32)
1878where
1879    D: Fn(&GfxContext, &str, &str, [f32; 4]) + Send + Sync,
1880{
1881    unsafe {
1882        let t = &*(ud as *const TooltipPicture<S, D>);
1883        guard((), || (t.draw)(&GfxContext::from_raw(gfx), item.as_str(), snbt.as_str(), [x, y, w, h]));
1884    }
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888unsafe extern "C" fn trampoline_book_page<F>(ud: *mut c_void, gfx: *const YogGfxApi, data: *const u8, len: usize, x: f32, y: f32, w: f32, h: f32)
1889where
1890    F: Fn(&GfxContext, &yog_data::Value, [f32; 4]) + Send + Sync,
1891{
1892    unsafe {
1893        let f = &*(ud as *const F);
1894        let bytes = if data.is_null() { &[][..] } else { std::slice::from_raw_parts(data, len) };
1895        let value = yog_data::binary::decode(bytes).unwrap_or(yog_data::Value::Null);
1896        guard((), || f(&GfxContext::from_raw(gfx), &value, [x, y, w, h]));
1897    }
1898}
1899
1900unsafe extern "C" fn trampoline_world_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
1901where
1902    F: Fn(&GfxContext) + Send + Sync,
1903{
1904    unsafe {
1905        let f = &*(ud as *const F);
1906        guard((), || f(&GfxContext::from_raw(gfx)));
1907    }
1908}
1909
1910unsafe extern "C" fn trampoline_key_press<F>(ud: *mut c_void, ev: *const YogKeyPressEvent) -> bool
1911where
1912    F: Fn(&KeyPressEvent) -> bool + Send + Sync,
1913{
1914    unsafe {
1915        let f = &*(ud as *const F);
1916        let e = &*ev;
1917        let ev = KeyPressEvent { key_code: e.key_code, scan_code: e.scan_code, action: e.action, modifiers: e.modifiers };
1918        guard(false, || f(&ev))
1919    }
1920}
1921
1922unsafe extern "C" fn trampoline_screen<F>(ud: *mut c_void, screen_class: YogStr) -> bool
1923where
1924    F: Fn(&ScreenEvent) + Send + Sync,
1925{
1926    unsafe {
1927        let f = &*(ud as *const F);
1928        let ev = ScreenEvent { screen_class: screen_class.as_str().to_owned() };
1929        guard((), || f(&ev));
1930        true
1931    }
1932}
1933
1934// ── Registry ─────────────────────────────────────────────────────────────────
1935
1936/// Wraps the [`YogApi`] table and provides an ergonomic registration API.
1937///
1938/// Obtained inside `yog_mod_register` via `export_mod!`.  Closures registered
1939/// here are boxed and leaked — they live as long as the process (which is the
1940/// correct lifetime for a server mod).
1941/// Pointer to the runtime's `YogApi` table (a process-lifetime static in the
1942/// runtime), captured when the mod registers. Lets free functions like
1943/// [`installed_mods`] work outside of `register()` — e.g. in UI handlers.
1944static GLOBAL_API: std::sync::atomic::AtomicPtr<YogApi> =
1945    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
1946
1947/// The runtime's `YogApi` table, null before this mod registered.
1948pub(crate) fn global_api() -> *mut YogApi {
1949    GLOBAL_API.load(std::sync::atomic::Ordering::Acquire)
1950}
1951
1952/// Metadata of an installed mod, as reported by the loader.
1953#[derive(Debug, Clone)]
1954pub struct ModInfo {
1955    /// `"yog"` for .yog mods, `"platform"` for loader (Java) mods.
1956    pub source: String,
1957    pub id: String,
1958    pub name: String,
1959    pub version: String,
1960    /// Comma-separated author list (may be empty).
1961    pub authors: String,
1962    pub description: String,
1963}
1964
1965/// All installed mods known to the loader: .yog mods plus, where the host
1966/// exposes them, platform (Java) mods. Callable at any time after this mod's
1967/// `register()` ran — including client-side UI handlers. Note that during
1968/// `register()` mods that load after this one are not in the list yet; query
1969/// lazily (e.g. on first render) for a complete view.
1970pub fn installed_mods() -> Vec<ModInfo> {
1971    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1972    if api.is_null() {
1973        return Vec::new();
1974    }
1975    let owned = unsafe { ((*api).mods_list)((*api).ctx) };
1976    let Some(text) = take_owned(unsafe { (*api).free_str }, owned) else {
1977        return Vec::new();
1978    };
1979    text.lines()
1980        .filter_map(|line| {
1981            let mut f = line.split('\t');
1982            Some(ModInfo {
1983                source: f.next()?.to_string(),
1984                id: f.next()?.to_string(),
1985                name: f.next().unwrap_or_default().to_string(),
1986                version: f.next().unwrap_or_default().to_string(),
1987                authors: f.next().unwrap_or_default().to_string(),
1988                description: f.next().unwrap_or_default().to_string(),
1989            })
1990        })
1991        .collect()
1992}
1993
1994/// Open the Yog UI registered as `ui_id` (client side; no-op on dedicated
1995/// servers). `modal` blocks game input, `pause` pauses singleplayer. Callable
1996/// from any handler after registration — e.g. an `on_client_packet` handler.
1997pub fn open_ui(ui_id: &str, modal: bool, pause: bool) {
1998    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
1999    if api.is_null() {
2000        return;
2001    }
2002    unsafe { ((*api).ui_open)((*api).ctx, YogStr::from_str(ui_id), modal, pause) }
2003}
2004
2005/// Close the Yog UI screen showing `ui_id` — e.g. from that screen's own
2006/// "Done" button. Does nothing when another screen is open. Deferred onto the
2007/// render thread, so the handler that calls it finishes first. Client side;
2008/// a no-op on dedicated servers.
2009pub fn close_ui(ui_id: &str) {
2010    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
2011    if api.is_null() {
2012        return;
2013    }
2014    unsafe { ((*api).ui_close)((*api).ctx, YogStr::from_str(ui_id)) }
2015}
2016
2017/// What a [`Registry::register_ui`] handler returns: `()` (never marks
2018/// events as used) or `bool` (`true` = the handler used this `key:`/`char:`
2019/// event — the host then doesn't, e.g. Esc doesn't close the screen).
2020pub trait UiHandled {
2021    fn handled(self) -> bool;
2022}
2023
2024impl UiHandled for () {
2025    fn handled(self) -> bool {
2026        false
2027    }
2028}
2029
2030impl UiHandled for bool {
2031    fn handled(self) -> bool {
2032        self
2033    }
2034}
2035
2036/// Handle to the runtime's server-action table, usable from any handler after
2037/// registration — including client-side ones that don't receive `&dyn Server`
2038/// (UI event handlers, client tick). Server-world actions are no-ops client
2039/// side, but networking (`send_to_server`) and similar client-safe calls work.
2040pub fn server() -> Option<CServer> {
2041    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
2042    if api.is_null() {
2043        return None;
2044    }
2045    let srv = unsafe { (*api).server };
2046    if srv.is_null() {
2047        return None;
2048    }
2049    Some(CServer(srv))
2050}
2051
2052pub struct Registry {
2053    api: *const YogApi,
2054}
2055
2056// SAFETY: `api` is a static provided by the runtime, valid for process lifetime.
2057unsafe impl Send for Registry {}
2058unsafe impl Sync for Registry {}
2059
2060impl Registry {
2061    /// Build from the pointer passed by the runtime. Only called by `export_mod!`.
2062    ///
2063    /// # Safety
2064    ///
2065    /// `api` must point to the runtime's `YogApi` table, valid for the rest of
2066    /// the process.
2067    pub unsafe fn from_raw(api: *const YogApi) -> Self {
2068        GLOBAL_API.store(api as *mut YogApi, std::sync::atomic::Ordering::Release);
2069        Self { api }
2070    }
2071
2072    #[inline]
2073    fn ctx(&self) -> *mut c_void {
2074        unsafe { (*self.api).ctx }
2075    }
2076
2077    // ── helpers ──────────────────────────────────────────────────────────────
2078
2079    fn leak<F: 'static>(f: F) -> *mut c_void {
2080        Box::into_raw(Box::new(f)) as *mut c_void
2081    }
2082
2083    // ── events ───────────────────────────────────────────────────────────────
2084    //
2085    // All handlers receive `(event, EventPhase, &dyn Server) -> bool`.
2086    // In `Pre` phase, returning `false` cancels the action.
2087    // In `Post` phase, the return value is ignored.
2088    // A single registration fires for BOTH phases.
2089
2090    pub fn on_block_break<F>(&mut self, handler: F)
2091    where
2092        F: Fn(&BlockBreakEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2093    {
2094        let ud = Self::leak(handler);
2095        unsafe { ((*self.api).on_block_break)(self.ctx(), ud, trampoline_block_break::<F>) }
2096    }
2097
2098    pub fn on_chat<F>(&mut self, handler: F)
2099    where
2100        F: Fn(&ChatEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2101    {
2102        let ud = Self::leak(handler);
2103        unsafe { ((*self.api).on_chat)(self.ctx(), ud, trampoline_chat::<F>) }
2104    }
2105
2106    pub fn on_player_join<F>(&mut self, handler: F)
2107    where
2108        F: Fn(&PlayerJoinEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2109    {
2110        let ud = Self::leak(handler);
2111        unsafe { ((*self.api).on_player_join)(self.ctx(), ud, trampoline_player_join::<F>) }
2112    }
2113
2114    pub fn on_player_leave<F>(&mut self, handler: F)
2115    where
2116        F: Fn(&PlayerLeaveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2117    {
2118        let ud = Self::leak(handler);
2119        unsafe { ((*self.api).on_player_leave)(self.ctx(), ud, trampoline_player_leave::<F>) }
2120    }
2121
2122    pub fn on_use_item<F>(&mut self, handler: F)
2123    where
2124        F: Fn(&UseItemEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2125    {
2126        let ud = Self::leak(handler);
2127        unsafe { ((*self.api).on_use_item)(self.ctx(), ud, trampoline_use_item::<F>) }
2128    }
2129
2130    pub fn on_use_block<F>(&mut self, handler: F)
2131    where
2132        F: Fn(&UseBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2133    {
2134        let ud = Self::leak(handler);
2135        unsafe { ((*self.api).on_use_block)(self.ctx(), ud, trampoline_use_block::<F>) }
2136    }
2137
2138    pub fn on_attack_entity<F>(&mut self, handler: F)
2139    where
2140        F: Fn(&AttackEntityEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2141    {
2142        let ud = Self::leak(handler);
2143        unsafe { ((*self.api).on_attack_entity)(self.ctx(), ud, trampoline_attack_entity::<F>) }
2144    }
2145
2146    pub fn on_entity_damage<F>(&mut self, handler: F)
2147    where
2148        F: Fn(&EntityDamageEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2149    {
2150        let ud = Self::leak(handler);
2151        unsafe { ((*self.api).on_entity_damage)(self.ctx(), ud, trampoline_entity_damage::<F>) }
2152    }
2153
2154    pub fn on_entity_death<F>(&mut self, handler: F)
2155    where
2156        F: Fn(&EntityDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2157    {
2158        let ud = Self::leak(handler);
2159        unsafe { ((*self.api).on_entity_death)(self.ctx(), ud, trampoline_entity_death::<F>) }
2160    }
2161
2162    pub fn on_entity_spawn<F>(&mut self, handler: F)
2163    where
2164        F: Fn(&EntitySpawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2165    {
2166        let ud = Self::leak(handler);
2167        unsafe { ((*self.api).on_entity_spawn)(self.ctx(), ud, trampoline_entity_spawn::<F>) }
2168    }
2169
2170    pub fn on_player_place_block<F>(&mut self, handler: F)
2171    where
2172        F: Fn(&PlaceBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2173    {
2174        let ud = Self::leak(handler);
2175        unsafe { ((*self.api).on_player_place_block)(self.ctx(), ud, trampoline_place_block::<F>) }
2176    }
2177
2178    pub fn on_player_death<F>(&mut self, handler: F)
2179    where
2180        F: Fn(&PlayerDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2181    {
2182        let ud = Self::leak(handler);
2183        unsafe { ((*self.api).on_player_death)(self.ctx(), ud, trampoline_player_death::<F>) }
2184    }
2185
2186    pub fn on_player_respawn<F>(&mut self, handler: F)
2187    where
2188        F: Fn(&PlayerRespawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2189    {
2190        let ud = Self::leak(handler);
2191        unsafe { ((*self.api).on_player_respawn)(self.ctx(), ud, trampoline_player_respawn::<F>) }
2192    }
2193
2194    pub fn on_advancement<F>(&mut self, handler: F)
2195    where
2196        F: Fn(&AdvancementEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2197    {
2198        let ud = Self::leak(handler);
2199        unsafe { ((*self.api).on_advancement)(self.ctx(), ud, trampoline_advancement::<F>) }
2200    }
2201
2202    pub fn on_entity_interact<F>(&mut self, handler: F)
2203    where
2204        F: Fn(&EntityInteractEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2205    {
2206        let ud = Self::leak(handler);
2207        unsafe { ((*self.api).on_entity_interact)(self.ctx(), ud, trampoline_entity_interact::<F>) }
2208    }
2209
2210    pub fn on_item_craft<F>(&mut self, handler: F)
2211    where
2212        F: Fn(&CraftEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2213    {
2214        let ud = Self::leak(handler);
2215        unsafe { ((*self.api).on_item_craft)(self.ctx(), ud, trampoline_craft::<F>) }
2216    }
2217
2218    /// A machine making a recipe — see [`MachineCraftEvent`].
2219    pub fn on_machine_craft<F>(&mut self, handler: F)
2220    where
2221        F: Fn(&MachineCraftEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2222    {
2223        let ud = Self::leak(handler);
2224        unsafe { ((*self.api).on_machine_craft)(self.ctx(), ud, trampoline_machine_craft::<F>) }
2225    }
2226
2227    pub fn on_explosion<F>(&mut self, handler: F)
2228    where
2229        F: Fn(&ExplosionEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2230    {
2231        let ud = Self::leak(handler);
2232        unsafe { ((*self.api).on_explosion)(self.ctx(), ud, trampoline_explosion::<F>) }
2233    }
2234
2235    /// A hopper or dropper offers an item to a block that accepts items
2236    /// (`block.accepts_items`): `Pre` returns whether it takes it, `Post`
2237    /// takes it.
2238    pub fn on_item_insert<F>(&mut self, handler: F)
2239    where
2240        F: Fn(&ItemInsertEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2241    {
2242        let ud = Self::leak(handler);
2243        unsafe { ((*self.api).on_item_insert)(self.ctx(), ud, trampoline_item_insert::<F>) }
2244    }
2245
2246    pub fn on_item_pickup<F>(&mut self, handler: F)
2247    where
2248        F: Fn(&ItemPickupEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2249    {
2250        let ud = Self::leak(handler);
2251        unsafe { ((*self.api).on_item_pickup)(self.ctx(), ud, trampoline_item_pickup::<F>) }
2252    }
2253
2254    pub fn on_player_move<F>(&mut self, handler: F)
2255    where
2256        F: Fn(&PlayerMoveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2257    {
2258        let ud = Self::leak(handler);
2259        unsafe { ((*self.api).on_player_move)(self.ctx(), ud, trampoline_player_move::<F>) }
2260    }
2261
2262    pub fn on_container_open<F>(&mut self, handler: F)
2263    where
2264        F: Fn(&ContainerOpenEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2265    {
2266        let ud = Self::leak(handler);
2267        unsafe { ((*self.api).on_container_open)(self.ctx(), ud, trampoline_container_open::<F>) }
2268    }
2269
2270    pub fn on_container_close<F>(&mut self, handler: F)
2271    where
2272        F: Fn(&ContainerCloseEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2273    {
2274        let ud = Self::leak(handler);
2275        unsafe { ((*self.api).on_container_close)(self.ctx(), ud, trampoline_container_close::<F>) }
2276    }
2277
2278    pub fn on_projectile_hit<F>(&mut self, handler: F)
2279    where
2280        F: Fn(&ProjectileHitEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static,
2281    {
2282        let ud = Self::leak(handler);
2283        unsafe { ((*self.api).on_projectile_hit)(self.ctx(), ud, trampoline_projectile_hit::<F>) }
2284    }
2285
2286    pub fn on_tick<F>(&mut self, listener: F)
2287    where
2288        F: Fn(&dyn Server) + Send + Sync + 'static,
2289    {
2290        let ud = Self::leak(listener);
2291        unsafe { ((*self.api).on_server_tick)(self.ctx(), ud, trampoline_server_fn::<F>) }
2292    }
2293
2294    pub fn on_server_started<F>(&mut self, listener: F)
2295    where
2296        F: Fn(&dyn Server) + Send + Sync + 'static,
2297    {
2298        let ud = Self::leak(listener);
2299        unsafe { ((*self.api).on_server_started)(self.ctx(), ud, trampoline_server_fn::<F>) }
2300    }
2301
2302    pub fn on_server_stopping<F>(&mut self, listener: F)
2303    where
2304        F: Fn(&dyn Server) + Send + Sync + 'static,
2305    {
2306        let ud = Self::leak(listener);
2307        unsafe { ((*self.api).on_server_stopping)(self.ctx(), ud, trampoline_server_fn::<F>) }
2308    }
2309
2310    // ── commands ─────────────────────────────────────────────────────────────
2311
2312    pub fn on_command<F>(&mut self, name: impl AsRef<str>, handler: F)
2313    where
2314        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
2315    {
2316        let name_ys = YogStr::from_str(name.as_ref());
2317        let ud = Self::leak(handler);
2318        unsafe { ((*self.api).register_command)(self.ctx(), name_ys, ud, trampoline_command::<F>) }
2319    }
2320
2321    /// Register a command with Brigadier-typed arguments.
2322    ///
2323    /// `schema` is a space-separated list of argument types:
2324    /// `int`, `float`, `word`, `string` (greedy, must be last), `player`, `blockpos`.
2325    ///
2326    /// In the handler use `ctx.arg_int(0)`, `ctx.arg_blockpos(1)`, etc.
2327    pub fn on_typed_command<F>(
2328        &mut self,
2329        name: impl AsRef<str>,
2330        schema: impl AsRef<str>,
2331        handler: F,
2332    ) where
2333        F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static,
2334    {
2335        let name_ys = YogStr::from_str(name.as_ref());
2336        let schema_ys = YogStr::from_str(schema.as_ref());
2337        let ud = Self::leak(handler);
2338        unsafe {
2339            ((*self.api).register_typed_command)(
2340                self.ctx(),
2341                name_ys,
2342                schema_ys,
2343                ud,
2344                trampoline_command::<F>,
2345            );
2346        }
2347    }
2348
2349    // ── networking ───────────────────────────────────────────────────────────
2350
2351    pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
2352    where
2353        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
2354    {
2355        let ch = YogStr::from_str(channel.as_ref());
2356        let ud = Self::leak(handler);
2357        unsafe { ((*self.api).on_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
2358    }
2359
2360    pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
2361    where
2362        F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static,
2363    {
2364        let ch = YogStr::from_str(channel.as_ref());
2365        let ud = Self::leak(handler);
2366        unsafe { ((*self.api).on_client_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
2367    }
2368
2369    /// Register a typed-packet handler.
2370    ///
2371    /// The payload is decoded from raw bytes using `P`'s [`Packet`] impl.
2372    /// Malformed payloads are silently dropped.
2373    pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
2374    where
2375        P: Packet + Send + Sync + 'static,
2376        F: Fn(&P, &dyn Server) + Send + Sync + 'static,
2377    {
2378        self.on_packet(channel, move |ev, srv| {
2379            if let Some(pkt) = P::decode(&ev.payload) {
2380                handler(&pkt, srv);
2381            }
2382        });
2383    }
2384
2385    // ── content ──────────────────────────────────────────────────────────────
2386
2387    /// Hands the runtime content of `kind` named `id`: its definition as a
2388    /// Yog Data blob (`register_content`, one ABI call for every kind).
2389    fn content(&mut self, kind: &str, id: &str, def: &yog_data::Value) {
2390        let blob = yog_data::binary::encode(def);
2391        unsafe { ((*self.api).register_content)(self.ctx(), YogStr::from_str(kind), YogStr::from_str(id), blob.as_ptr(), blob.len()) }
2392    }
2393
2394    /// JSON text as Yog Data (recipes and books are written as JSON by their crates).
2395    fn json(text: &str) -> yog_data::Value {
2396        yog_data::json::from_json_text(text).unwrap_or_default()
2397    }
2398
2399    /// Register a recipe (Yog's format — see [`Recipe`]; the host writes
2400    /// its Minecraft version's JSON).
2401    pub fn add_recipe(&mut self, recipe: Recipe) {
2402        let (ns, name) = recipe.ns_name();
2403        let id = format!("{ns}:{name}");
2404        self.content("recipe", &id, &recipe.to_data());
2405    }
2406
2407    /// Register a recipe given as Yog's recipe JSON — how `yog build`
2408    /// registers the mod's own kinds (`{"type": "mymod:infusion", "slots":
2409    /// …}`), which stay the mod's data: for its machines and its books.
2410    pub fn add_recipe_json(&mut self, id: &str, json: &str) {
2411        let id = if id.contains(':') { id.to_string() } else { format!("minecraft:{id}") };
2412        self.content("recipe", &id, &Self::json(json));
2413    }
2414
2415    pub fn register_item(&mut self, def: ItemDef) {
2416        use yog_data::Schema;
2417        self.content("item", &def.id.clone(), &def.write());
2418    }
2419
2420    pub fn register_block(&mut self, def: BlockDef) {
2421        use yog_data::Schema;
2422        self.content("block", &def.id.clone(), &def.write());
2423    }
2424
2425    /// Registers an entity type (what a `.yentity` makes). With a `model`,
2426    /// the loader draws it — its body model with the clip of its state
2427    /// (`clips`: `idle`, `walk`, or entity data `yog:state`).
2428    pub fn register_entity(&mut self, def: yog_registry::EntityDef) {
2429        use yog_data::Schema;
2430        if crate::entities::drawn(&def) {
2431            self.on_body(crate::entities::draw);
2432        }
2433        self.content("entity", &def.id.clone(), &def.write());
2434    }
2435
2436    /// A compiled `.yentity` (the generated code calls this).
2437    pub fn register_entity_data(&mut self, id: &str, data: &[u8]) {
2438        match yog_data::read_bytes::<yog_registry::EntityDef>(data) {
2439            Ok(def) => self.register_entity(def),
2440            Err(e) => crate::error!("entity {id}: {e:?}"),
2441        }
2442    }
2443
2444    /// Calls `handler` each tick of every entity of `entity_type` (a mod's
2445    /// type with `ticks`), with its uuid — server side.
2446    pub fn on_entity_tick<F>(&mut self, entity_type: &str, handler: F)
2447    where
2448        F: Fn(&dyn Server, &str) + Send + Sync + 'static,
2449    {
2450        let ud = Self::leak(handler);
2451        unsafe { ((*self.api).on_entity_tick)(self.ctx(), YogStr::from_str(entity_type), ud, trampoline_entity_tick::<F>) }
2452    }
2453
2454    /// Add placed feature `feature` (the mod's datapack's, like an ore's
2455    /// `mymod:mythril_ore`) to the biomes of tag `biomes` at generation
2456    /// `step` (`underground_ores`). An ore `.ydef` does this for you.
2457    pub fn add_biome_feature(&mut self, feature: &str, biomes: &str, step: &str) {
2458        use yog_data::Schema;
2459        let f = yog_registry::BiomeFeature { feature: feature.into(), biomes: biomes.into(), step: step.into() };
2460        self.content("biome_feature", feature, &f.write());
2461    }
2462
2463    /// Register a tool material — what tool items naming it
2464    /// (`ItemDef::tool`) are made of. The game's are built in.
2465    pub fn register_tool_material(&mut self, material: ToolMaterial) {
2466        use yog_data::Schema;
2467        self.content("tool_material", &material.id.clone(), &material.write());
2468    }
2469
2470    /// Register an armor material — what armor items naming it
2471    /// (`ItemDef::armor`) are made of. The game's are built in.
2472    pub fn register_armor_material(&mut self, material: ArmorMaterial) {
2473        use yog_data::Schema;
2474        self.content("armor_material", &material.id.clone(), &material.write());
2475    }
2476
2477    /// Register a sound group — what blocks naming it (`BlockDef::sound`)
2478    /// sound like. The game's groups are built in.
2479    pub fn register_sound_group(&mut self, group: SoundGroup) {
2480        use yog_data::Schema;
2481        self.content("sound_group", &group.id.clone(), &group.write());
2482    }
2483
2484    /// Register a real Container/Menu inventory screen — see
2485    /// `yog_inventory::InventoryDef`. Attach it to a block via
2486    /// `BlockDef::inventory(id)`.
2487    pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
2488        use yog_data::Schema;
2489        self.content("inventory", &def.id.clone(), &def.write());
2490    }
2491
2492    // ── startup grants ───────────────────────────────────────────────────────
2493
2494    /// Register a startup grant: items/books to give once when a player first joins.
2495    pub fn register_startup_grant(&mut self, grant: StartupGrant) {
2496        use yog_data::Schema;
2497        self.content("startup_grant", &grant.id.clone(), &grant.write());
2498    }
2499
2500    /// Register a book.
2501    pub fn register_book(&mut self, book: &Book) {
2502        use yog_data::Schema;
2503        self.content("book", &book.id, &book.write());
2504    }
2505
2506    /// Register a book given as Yog Data, binary — what `yog build` makes
2507    /// of a `content/**/*.ybook`; the bytes go to the runtime as they are.
2508    pub fn register_book_data(&mut self, id: &str, data: &[u8]) {
2509        unsafe { ((*self.api).register_content)(self.ctx(), YogStr::from_str("book"), YogStr::from_str(id), data.as_ptr(), data.len()) }
2510    }
2511
2512    /// Register (or patch) a dimension's *type* (sky, lighting, physics, ...).
2513    ///
2514    /// `def.id` need not be a new custom id — pass a vanilla id
2515    /// (`"minecraft:overworld"`, `"minecraft:the_nether"`,
2516    /// `"minecraft:the_end"`) to patch that dimension's type/generator
2517    /// instead of creating a new one (e.g. to replace the overworld's
2518    /// terrain with your own generator type).
2519    ///
2520    /// Must be called from `register()` — the same mod-init window as
2521    /// blocks/items — so the host can declare/patch the dimension before
2522    /// Minecraft's registries freeze. See the `yog-dimensions` crate docs;
2523    /// set `def.generator_type` (via [`YogDimensionDef::generator_type`]) to
2524    /// pick a generator type registered with
2525    /// [`Registry::register_chunk_generator`].
2526    pub fn register_dimension(&mut self, def: &YogDimensionDef) {
2527        let value = Self::json(&def.to_json());
2528        self.content("dimension", &def.id.clone(), &value);
2529    }
2530
2531    /// Register a dimension given as Yog Data, binary — what `yog build`
2532    /// makes of a `.ydef` of kind `dimension` (its `terrain` generated by
2533    /// the runtime itself); the bytes go to the runtime as they are.
2534    pub fn register_dimension_data(&mut self, id: &str, data: &[u8]) {
2535        unsafe { ((*self.api).register_content)(self.ctx(), YogStr::from_str("dimension"), YogStr::from_str(id), data.as_ptr(), data.len()) }
2536    }
2537
2538    /// Register a named, reusable chunk-generator type under
2539    /// `generator_type_id` (e.g. `"mymod:custom_terrain"`) — analogous to
2540    /// how Biomes O'Plenty registers selectable world-type presets. Any
2541    /// dimension (custom or vanilla) can opt into this generator via
2542    /// [`YogDimensionDef::generator_type`]; the platform host may also list
2543    /// registered generator type ids as options on the world-creation screen.
2544    ///
2545    /// `generator` is called once per chunk column that needs generating in
2546    /// any dimension using this type — write whatever terrain logic you want
2547    /// using the [`ChunkWriter`] handle (combine as many noise layers as you
2548    /// like; this crate doesn't prescribe a generation style).
2549    pub fn register_chunk_generator<F>(&mut self, generator_type_id: &str, generator: F)
2550    where
2551        F: Fn(&ChunkWriter) + Send + Sync + 'static,
2552    {
2553        let ud = Self::leak(generator);
2554        let id = YogStr::from_str(generator_type_id);
2555        unsafe {
2556            ((*self.api).register_chunk_generator)(
2557                self.ctx(),
2558                id,
2559                ud,
2560                trampoline_chunk_generator::<F>,
2561            );
2562        }
2563    }
2564
2565    /// Register a menu entry — the host renders a button on vanilla screens
2566    /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
2567    /// `label` is the human-readable button text (e.g. "Yog Mods").
2568    /// `ui_id` is the UI to open when clicked (e.g. "example:menu").
2569    /// See [`installed_mods`]. During `register()` the list only contains
2570    /// mods loaded before this one.
2571    pub fn installed_mods(&self) -> Vec<ModInfo> {
2572        installed_mods()
2573    }
2574
2575    /// Access the inter-mod communication layer.
2576    ///
2577    /// Allows exporting function pointers from this mod and importing
2578    /// function pointers from other mods.
2579    pub fn interop(&self) -> crate::Interop {
2580        crate::Interop::new(self.api)
2581    }
2582
2583    /// Raw `YogApi` pointer — for passing to interop-imported functions
2584    /// that register content through the C ABI table directly.
2585    pub fn raw_api(&self) -> *const yog_abi::YogApi {
2586        self.api
2587    }
2588
2589    pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
2590        let l = YogStr::from_str(label);
2591        let u = YogStr::from_str(ui_id);
2592        unsafe { ((*self.api).register_menu_entry)(self.ctx(), l, u) }
2593    }
2594
2595    /// Register a UI tree with an event callback.
2596    /// `ui_id` is the unique identifier (e.g. "mymod:menu").
2597    /// `handler` receives `(ui_id, event)` for the screen's input: `click:X:Y`,
2598    /// `drag:X:Y`, `release:X:Y`, `scroll:DY`, `key:KEY:MODS` (GLFW key code
2599    /// and modifier bits, on press and repeat) and `char:CODEPOINT` (a typed
2600    /// character). It returns `()` or a `bool`: `true` marks a `key:`/`char:`
2601    /// event as used, so the host skips its default handling of the key
2602    /// (Esc closing the screen, ...) — see [`UiHandled`].
2603    pub fn register_ui<F, R>(&mut self, ui_id: &str, handler: F)
2604    where
2605        F: Fn(&str, &str) -> R + Send + Sync + 'static,
2606        R: UiHandled,
2607    {
2608        let ud = Self::leak(handler);
2609        let id = YogStr::from_str(ui_id);
2610        let empty = YogStr::from_str("{}");
2611        unsafe { ((*self.api).register_ui)(self.ctx(), id, empty, ud, trampoline_ui_event::<F, R>) }
2612    }
2613
2614    // ── scheduler ────────────────────────────────────────────────────────────
2615
2616    pub fn schedule_once<F>(&self, delay_ticks: u64, handler: F)
2617    where
2618        F: Fn(&dyn Server) + Send + Sync + 'static,
2619    {
2620        let ud = Self::leak(handler);
2621        unsafe {
2622            ((*self.api).schedule_once)(self.ctx(), delay_ticks, ud, trampoline_scheduled::<F>);
2623        }
2624    }
2625
2626    pub fn schedule_repeating<F>(&self, period_ticks: u64, handler: F)
2627    where
2628        F: Fn(&dyn Server) + Send + Sync + 'static,
2629    {
2630        let ud = Self::leak(handler);
2631        unsafe {
2632            ((*self.api).schedule_repeating)(
2633                self.ctx(),
2634                period_ticks,
2635                ud,
2636                trampoline_scheduled::<F>,
2637            );
2638        }
2639    }
2640
2641    // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
2642
2643    /// Register a handler called every client tick (render thread, no server).
2644    pub fn on_client_tick<F>(&mut self, handler: F)
2645    where
2646        F: Fn(&ClientTickEvent) + Send + Sync + 'static,
2647    {
2648        let ud = Self::leak(handler);
2649        unsafe { ((*self.api).on_client_tick)(self.ctx(), ud, trampoline_client_tick::<F>) }
2650    }
2651
2652    /// Register a render callback for a specific UI screen (`ui_id`).
2653    ///
2654    /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
2655    /// the screen — so your UI draws on top of the dimmed world view.
2656    /// Use this instead of `on_hud_render` for book/inventory screens.
2657    ///
2658    /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
2659    /// can do hit-testing on your own stored layout.
2660    pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
2661    where
2662        F: Fn(&GfxContext) + Send + Sync + 'static,
2663    {
2664        let ud = Self::leak(handler);
2665        let id = YogStr::from_str(ui_id);
2666        unsafe { ((*self.api).on_ui_render)(self.ctx(), id, ud, trampoline_hud_render::<F>) }
2667    }
2668
2669    /// Draw the pictures of book pages of type `drawn` whose `drawer` is
2670    /// `drawer` (`"mymod:diagram"`): `handler` gets the page's `data` and
2671    /// the picture's box `[x, y, w, h]` in screen pixels, every frame the
2672    /// page is open.
2673    ///
2674    /// ```text
2675    /// { type: "drawn", drawer: "mymod:diagram", title: "Wiring", height: 64,
2676    ///   caption: "{u|in} → {u|out}", text: "…", data: { nodes: 3 } }
2677    /// ```
2678    pub fn on_book_page<F>(&mut self, drawer: &str, handler: F)
2679    where
2680        F: Fn(&GfxContext, &yog_data::Value, [f32; 4]) + Send + Sync + 'static,
2681    {
2682        let ud = Self::leak(handler);
2683        let id = YogStr::from_str(drawer);
2684        unsafe { ((*self.api).on_book_page)(self.ctx(), id, ud, trampoline_book_page::<F>) }
2685    }
2686
2687    /// A picture under the tooltips of `item` (as Hex Casting's scrolls show
2688    /// their pattern): `size(item, snbt)` says whether a stack — its custom
2689    /// data as SNBT — gets one and its size in GUI pixels; `draw` draws it
2690    /// in the box `[x, y, w, h]` (screen pixels) while the tooltip shows.
2691    pub fn on_item_tooltip<S, D>(&mut self, item: &str, size: S, draw: D)
2692    where
2693        S: Fn(&str, &str) -> Option<(f32, f32)> + Send + Sync + 'static,
2694        D: Fn(&GfxContext, &str, &str, [f32; 4]) + Send + Sync + 'static,
2695    {
2696        let ud = Self::leak(TooltipPicture { size, draw });
2697        let id = YogStr::from_str(item);
2698        unsafe {
2699            ((*self.api).on_item_tooltip)(self.ctx(), id, ud, trampoline_tooltip_size::<S, D>, trampoline_tooltip_draw::<S, D>)
2700        }
2701    }
2702
2703    /// A stack's own name and tooltip, from its custom data (SNBT): `text(item,
2704    /// snbt)` gives `Some((name, lines))` — `name` `None` keeps the item's;
2705    /// `lines` stand instead of the item's description — or `None` for the
2706    /// item's own. A written blueprint names its design:
2707    ///
2708    /// ```ignore
2709    /// registry.on_item_text(Items::BLUEPRINT, |_, snbt| {
2710    ///     let bp = Blueprint::from_nbt(snbt)?;
2711    ///     Some((Some(bp.name.clone()), vec![format!("{} ports", bp.ports)]))
2712    /// });
2713    /// ```
2714    pub fn on_item_text<F>(&mut self, item: &str, text: F)
2715    where
2716        F: Fn(&str, &str) -> Option<(Option<String>, Vec<String>)> + Send + Sync + 'static,
2717    {
2718        let ud = Self::leak(text);
2719        let id = YogStr::from_str(item);
2720        unsafe { ((*self.api).on_item_text)(self.ctx(), id, ud, trampoline_item_text::<F>) }
2721    }
2722
2723    /// A compiled `.ymodel` (the generated code calls this): found later by
2724    /// [`crate::body::model`].
2725    pub fn register_body_model(&mut self, id: &str, data: &[u8]) {
2726        if let Err(e) = yog_body::register_model(id, data) {
2727            crate::error!("{e}");
2728        }
2729    }
2730
2731    /// A compiled `.yanim` (the generated code calls this): its clips found
2732    /// later by [`crate::body::clip`].
2733    pub fn register_body_anims(&mut self, id: &str, data: &[u8]) {
2734        if let Err(e) = yog_body::register_anims(id, data) {
2735            crate::error!("{e}");
2736        }
2737    }
2738
2739    /// Register a handler asked each frame about the body of every tracked
2740    /// entity ([`crate::body::track`]): it answers with [`crate::body::Ops`]
2741    /// — pose vanilla parts, turn the body, draw meshes (rigs, morphs),
2742    /// disguise, hide, capture. Every mod's ops add up.
2743    pub fn on_body<F>(&mut self, handler: F)
2744    where
2745        F: Fn(&yog_body::Frame, &mut yog_body::Ops) + Send + Sync + 'static,
2746    {
2747        let ud = Self::leak(handler);
2748        unsafe { ((*self.api).on_body)(self.ctx(), ud, trampoline_body::<F>) }
2749    }
2750
2751    /// Register a handler that sees every loot table rolled — a chest's, a
2752    /// block's drops, a mob's — and may change what it gives
2753    /// ([`crate::loot::Loot::items`]); each handler sees the one before's
2754    /// result. Server side. For adding and scaling items on some tables, a
2755    /// [`crate::loot::LootModifier`] says it as data.
2756    pub fn on_loot<F>(&mut self, handler: F)
2757    where
2758        F: Fn(&dyn Server, &mut crate::loot::Loot) + Send + Sync + 'static,
2759    {
2760        let ud = Self::leak(handler);
2761        unsafe { ((*self.api).on_loot)(self.ctx(), ud, trampoline_loot::<F>) }
2762    }
2763
2764    /// Change the loot of some tables as `modifier` says.
2765    pub fn add_loot_modifier(&mut self, modifier: crate::loot::LootModifier) {
2766        self.on_loot(move |srv, loot| crate::loot::apply(srv, &modifier, loot));
2767    }
2768
2769    /// A compiled `.ydef` loot modifier (the generated code calls this).
2770    pub fn add_loot_modifier_data(&mut self, id: &str, data: &[u8]) {
2771        match yog_data::read_bytes::<crate::loot::LootModifier>(data) {
2772            Ok(m) => self.add_loot_modifier(m),
2773            Err(e) => crate::error!("loot modifier {id}: {e:?}"),
2774        }
2775    }
2776
2777    /// Register loot function `name` (`mymod:pattern_scroll`): what loot
2778    /// modifiers naming it (`function`) run on each stack they add — set
2779    /// its custom data, say.
2780    pub fn loot_function<F>(&mut self, name: &str, function: F)
2781    where
2782        F: Fn(&dyn Server, &mut crate::loot::LootStack, &crate::loot::Loot) + Send + Sync + 'static,
2783    {
2784        if let Ok(mut f) = crate::loot::functions().write() {
2785            f.insert(name.to_string(), std::sync::Arc::new(function));
2786        }
2787    }
2788
2789    /// Register a handler that moves, turns or zooms the camera each frame;
2790    /// it changes the [`crate::body::CameraOut`] the handlers before it made.
2791    pub fn on_camera<F>(&mut self, handler: F)
2792    where
2793        F: Fn(&yog_body::CameraFrame, &mut yog_body::CameraOut) + Send + Sync + 'static,
2794    {
2795        let ud = Self::leak(handler);
2796        unsafe { ((*self.api).on_camera)(self.ctx(), ud, trampoline_camera::<F>) }
2797    }
2798
2799    /// Register a handler that may replace the local player's travel step:
2800    /// `Some(velocity)` (blocks per tick, drag applied) moves the player by
2801    /// it instead of vanilla's walking, swimming, falling or gliding.
2802    pub fn on_travel<F>(&mut self, handler: F)
2803    where
2804        F: Fn(&yog_body::TravelFrame) -> Option<yog_body::Vec3> + Send + Sync + 'static,
2805    {
2806        let ud = Self::leak(handler);
2807        unsafe { ((*self.api).on_travel)(self.ctx(), ud, trampoline_travel::<F>) }
2808    }
2809
2810    /// Register a handler called every frame when the HUD is rendered.
2811    ///
2812    /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
2813    /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
2814    pub fn on_hud_render<F>(&mut self, handler: F)
2815    where
2816        F: Fn(&GfxContext) + Send + Sync + 'static,
2817    {
2818        let ud = Self::leak(handler);
2819        unsafe { ((*self.api).on_hud_render)(self.ctx(), ud, trampoline_hud_render::<F>) }
2820    }
2821
2822    /// Register a handler called every frame at the end of world rendering.
2823    ///
2824    /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
2825    /// `gfx.camera_pos()` is the camera world position.
2826    /// To render at world position P, translate by `P - camera_pos` before drawing.
2827    pub fn on_world_render<F>(&mut self, handler: F)
2828    where
2829        F: Fn(&GfxContext) + Send + Sync + 'static,
2830    {
2831        let ud = Self::leak(handler);
2832        unsafe { ((*self.api).on_world_render)(self.ctx(), ud, trampoline_world_render::<F>) }
2833    }
2834
2835    /// Register a handler for keyboard input (client-side).
2836    /// Return `false` to prevent Minecraft from processing the key.
2837    pub fn on_key_press<F>(&mut self, handler: F)
2838    where
2839        F: Fn(&KeyPressEvent) -> bool + Send + Sync + 'static,
2840    {
2841        let ud = Self::leak(handler);
2842        unsafe { ((*self.api).on_key_press)(self.ctx(), ud, trampoline_key_press::<F>) }
2843    }
2844
2845    /// Register a handler called when a GUI screen is opened.
2846    pub fn on_screen_open<F>(&mut self, handler: F)
2847    where
2848        F: Fn(&ScreenEvent) + Send + Sync + 'static,
2849    {
2850        let ud = Self::leak(handler);
2851        unsafe { ((*self.api).on_screen_open)(self.ctx(), ud, trampoline_screen::<F>) }
2852    }
2853
2854    /// Register a handler called when a GUI screen is closed.
2855    pub fn on_screen_close<F>(&mut self, handler: F)
2856    where
2857        F: Fn(&ScreenEvent) + Send + Sync + 'static,
2858    {
2859        let ud = Self::leak(handler);
2860        unsafe { ((*self.api).on_screen_close)(self.ctx(), ud, trampoline_screen::<F>) }
2861    }
2862}
2863
2864unsafe extern "C" fn trampoline_ui_event<F, R>(
2865    ud: *mut c_void,
2866    ui_id: yog_abi::YogStr,
2867    event_id: yog_abi::YogStr,
2868) where
2869    F: Fn(&str, &str) -> R + Send + Sync + 'static,
2870    R: UiHandled,
2871{
2872    let f = unsafe { &*(ud as *const F) };
2873    let ui = unsafe { ui_id.as_str() };
2874    let ev = unsafe { event_id.as_str() };
2875    if guard(false, || f(ui, ev).handled()) {
2876        let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
2877        if !api.is_null() {
2878            unsafe { ((*api).ui_consume_event)((*api).ctx) }
2879        }
2880    }
2881}
2882
2883unsafe extern "C" fn trampoline_chunk_generator<F>(
2884    ud: *mut c_void,
2885    writer: *const yog_abi::YogChunkWriterApi,
2886) where
2887    F: Fn(&ChunkWriter) + Send + Sync + 'static,
2888{
2889    let f = unsafe { &*(ud as *const F) };
2890    let writer = unsafe { ChunkWriter::from_raw(writer) };
2891    guard((), || f(&writer));
2892}
2893
2894// ── Mod trait ─────────────────────────────────────────────────────────────────
2895
2896/// Implemented by every Yog mod. Called once at startup to register handlers.
2897pub trait Mod {
2898    fn register(registry: &mut Registry);
2899}
2900
2901#[cfg(test)]
2902mod trampoline_tests {
2903    use super::*;
2904    use yog_abi::{YogBlockPos, YogChatEvent, YogCommandEvent, YogPacketEvent};
2905
2906    fn ud<F>(f: &F) -> *mut c_void {
2907        f as *const F as *mut c_void
2908    }
2909
2910    fn chat<F: Fn(&ChatEvent, EventPhase, &dyn Server) -> bool + Send + Sync>(f: &F, ev: &YogChatEvent, phase: u8) -> bool {
2911        unsafe { trampoline_chat::<F>(ud(f), std::ptr::null(), ev, phase) }
2912    }
2913
2914    fn block_break<F: Fn(&BlockBreakEvent, EventPhase, &dyn Server) -> bool + Send + Sync>(f: &F, ev: &YogBlockBreakEvent) -> bool {
2915        unsafe { trampoline_block_break::<F>(ud(f), std::ptr::null(), ev, 0) }
2916    }
2917
2918    fn command<F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync>(f: &F, ev: &YogCommandEvent, buf: &mut [u8]) -> u32 {
2919        let mut len = 99u32;
2920        unsafe { trampoline_command::<F>(ud(f), std::ptr::null(), ev, buf.as_mut_ptr(), buf.len() as u32, &mut len) };
2921        len
2922    }
2923
2924    fn packet<F: Fn(&PacketEvent, &dyn Server) + Send + Sync>(f: &F, ev: &YogPacketEvent) {
2925        unsafe { trampoline_packet::<F>(ud(f), std::ptr::null(), ev) }
2926    }
2927
2928    #[test]
2929    fn events_arrive_converted_with_their_phase() {
2930        let seen = std::sync::Mutex::new(Vec::new());
2931        let handler = |ev: &ChatEvent, p: EventPhase, _: &dyn Server| {
2932            seen.lock().unwrap().push((ev.player_name.clone(), ev.message.clone(), p));
2933            ev.message != "cancel me"
2934        };
2935        let ev = |msg: &'static str| YogChatEvent {
2936            player: YogStr::from_str("Dev"),
2937            message: YogStr::from_str(msg),
2938            dimension: YogStr::from_str("minecraft:overworld"),
2939        };
2940        assert!(chat(&handler, &ev("hi"), 0));
2941        assert!(!chat(&handler, &ev("cancel me"), 0));
2942        chat(&handler, &ev("after"), 1);
2943        let seen = seen.into_inner().unwrap();
2944        assert_eq!(seen[0], ("Dev".into(), "hi".into(), EventPhase::Pre));
2945        assert_eq!(seen[2].2, EventPhase::Post);
2946    }
2947
2948    #[test]
2949    fn a_panicking_handler_allows_the_action() {
2950        let handler = |_: &BlockBreakEvent, _: EventPhase, _: &dyn Server| -> bool { panic!("mod bug") };
2951        let ev = YogBlockBreakEvent {
2952            player: YogStr::from_str("Dev"),
2953            block: YogStr::from_str("minecraft:stone"),
2954            pos: YogBlockPos { x: 1, y: 2, z: 3 },
2955            dimension: YogStr::from_str("minecraft:overworld"),
2956        };
2957        assert!(block_break(&handler, &ev));
2958    }
2959
2960    #[test]
2961    fn command_replies_fit_the_buffer_whole_characters_only() {
2962        let handler = |c: &CommandContext, _: &dyn Server| Some(format!("{}: привет", c.args));
2963        let ev = YogCommandEvent {
2964            name: YogStr::from_str("hex"),
2965            args: YogStr::from_str("x"),
2966            source: YogStr::from_str("Dev"),
2967            uuid: YogStr::EMPTY,
2968            dimension: YogStr::EMPTY,
2969        };
2970        let mut buf = [0u8; 6];
2971        let len = command(&handler, &ev, &mut buf);
2972        // "x: п" is 5 bytes; "пр" wouldn't fit whole in 6.
2973        assert_eq!(std::str::from_utf8(&buf[..len as usize]), Ok("x: п"));
2974    }
2975
2976    #[test]
2977    fn empty_packets_need_no_payload_pointer() {
2978        let got = std::sync::Mutex::new(None);
2979        let handler = |p: &PacketEvent, _: &dyn Server| *got.lock().unwrap() = Some(p.payload.len());
2980        let ev = YogPacketEvent {
2981            channel: YogStr::from_str("mymod:ping"),
2982            player: YogStr::from_str("Dev"),
2983            payload: std::ptr::null(),
2984            payload_len: 0,
2985        };
2986        packet(&handler, &ev);
2987        assert_eq!(*got.lock().unwrap(), Some(0));
2988    }
2989}