Skip to main content

yog_api/
registry.rs

1//! Registration hub and mod entry-point trait.
2//!
3//! [`Registry`] wraps the [`YogApi`] C table passed to `yog_mod_register` and
4//! provides an ergonomic Rust API over it.  All closures registered here are
5//! boxed on the heap and their raw pointers handed to the runtime; the runtime
6//! then drives them via C function-pointer calls.  Closures are intentionally
7//! leaked (never freed) — they live for the entire process lifetime, which is
8//! correct for a game server.
9
10use std::os::raw::c_void;
11
12use yog_abi::{
13    YogAdvancementEvent, YogApi, YogAttackEntityEvent, YogBlockBreakEvent, YogBlockDef,
14    YogChatEvent, YogCommandEvent, YogContainerCloseEvent, YogContainerOpenEvent, YogCraftEvent,
15    YogEntityDamageEvent, YogEntityDeathEvent, YogEntityInteractEvent, YogEntitySpawnEvent,
16    YogExplosionEvent, YogGfxApi, YogItemDef, YogItemPickupEvent, YogKeyPressEvent,
17    YogPacketEvent, YogPlaceBlockEvent, YogPlayerDeathEvent, YogPlayerEvent, YogPlayerMoveEvent,
18    YogPlayerRespawnEvent, YogProjectileHitEvent, YogServer, YogStr, YogStartupGrantDef,
19    YogUseBlockEvent, YogUseItemEvent, YogInventoryDef,
20};
21use yog_book::Book;
22use yog_gfx::GfxContext;
23use yog_command::CommandContext;
24use yog_core::Server;
25use yog_event::{
26    AdvancementEvent, AttackEntityEvent, BlockBreakEvent, ChatEvent, ClientTickEvent,
27    ContainerCloseEvent, ContainerOpenEvent, CraftEvent, EntityDamageEvent, EntityDeathEvent,
28    EntityInteractEvent, EntitySpawnEvent, EventPhase, ExplosionEvent,
29    ItemPickupEvent, KeyPressEvent, PlaceBlockEvent, PlayerDeathEvent, PlayerJoinEvent,
30    PlayerLeaveEvent, PlayerMoveEvent, PlayerRespawnEvent, ProjectileHitEvent, ScreenEvent,
31    UseBlockEvent, UseItemEvent,
32};
33use yog_network::{Packet, PacketEvent};
34use yog_registry::{BlockDef, FurnaceRecipe, ItemDef, ShapedRecipe, ShapelessRecipe, StartupGrant};
35
36// ── CServer — implements Server via the YogServer function table ──────────────
37
38/// A handle to the runtime's server actions, backed by [`YogServer`] fn pointers.
39/// Given to every handler as `&dyn Server`.
40pub struct CServer(pub *const YogServer);
41
42unsafe impl Send for CServer {}
43unsafe impl Sync for CServer {}
44
45macro_rules! srv {
46    ($self:ident) => { unsafe { &*$self.0 } };
47}
48
49impl Server for CServer {
50    fn broadcast(&self, message: &str) {
51        let s = srv!(self);
52        unsafe { (s.broadcast)(s.ctx, YogStr::from_str(message)) }
53    }
54
55    fn get_block(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
56        let s = srv!(self);
57        let owned = unsafe {
58            (s.get_block)(s.ctx, YogStr::from_str(dimension),
59                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z })
60        };
61        if owned.is_none() { return None; }
62        let result = unsafe {
63            String::from_utf8(
64                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
65            ).ok()
66        };
67        unsafe { (s.free_str)(owned.ptr, owned.len) };
68        result
69    }
70
71    fn set_block(&self, dimension: &str, pos: yog_core::BlockPos, block_id: &str) -> bool {
72        let s = srv!(self);
73        unsafe {
74            (s.set_block)(s.ctx, YogStr::from_str(dimension),
75                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z },
76                YogStr::from_str(block_id))
77        }
78    }
79
80    fn give_item(&self, player: &str, item_id: &str, count: u32) -> bool {
81        let s = srv!(self);
82        unsafe { (s.give_item)(s.ctx, YogStr::from_str(player), YogStr::from_str(item_id), count) }
83    }
84
85    fn teleport(&self, player: &str, x: f64, y: f64, z: f64) -> bool {
86        let s = srv!(self);
87        unsafe { (s.player_teleport)(s.ctx, YogStr::from_str(player), yog_abi::YogVec3 { x, y, z }) }
88    }
89
90    fn send_to_player(&self, player: &str, channel: &str, payload: &[u8]) -> bool {
91        let s = srv!(self);
92        unsafe {
93            (s.send_to_player)(s.ctx, YogStr::from_str(player), YogStr::from_str(channel),
94                payload.as_ptr(), payload.len() as u32)
95        }
96    }
97
98    fn send_to_server(&self, channel: &str, payload: &[u8]) -> bool {
99        let s = srv!(self);
100        unsafe {
101            (s.send_to_server)(s.ctx, YogStr::from_str(channel),
102                payload.as_ptr(), payload.len() as u32)
103        }
104    }
105
106    fn entity_teleport(&self, uuid: &str, x: f64, y: f64, z: f64) -> bool {
107        let s = srv!(self);
108        unsafe { (s.entity_teleport)(s.ctx, YogStr::from_str(uuid), yog_abi::YogVec3 { x, y, z }) }
109    }
110
111    fn entity_position(&self, uuid: &str) -> Option<(f64, f64, f64)> {
112        let s = srv!(self);
113        let mut out = yog_abi::YogVec3 { x: 0.0, y: 0.0, z: 0.0 };
114        if unsafe { (s.entity_position)(s.ctx, YogStr::from_str(uuid), &mut out) } {
115            Some((out.x, out.y, out.z))
116        } else {
117            None
118        }
119    }
120
121    fn entity_health(&self, uuid: &str) -> Option<f32> {
122        let s = srv!(self);
123        let mut hp = 0f32;
124        if unsafe { (s.entity_health)(s.ctx, YogStr::from_str(uuid), &mut hp) } { Some(hp) } else { None }
125    }
126
127    fn entity_set_health(&self, uuid: &str, health: f32) -> bool {
128        let s = srv!(self);
129        unsafe { (s.entity_set_health)(s.ctx, YogStr::from_str(uuid), health) }
130    }
131
132    fn entity_kill(&self, uuid: &str) -> bool {
133        let s = srv!(self);
134        unsafe { (s.entity_kill)(s.ctx, YogStr::from_str(uuid)) }
135    }
136
137    fn spawn_entity(&self, entity_type: &str, dimension: &str, x: f64, y: f64, z: f64) -> Option<String> {
138        let s = srv!(self);
139        let owned = unsafe {
140            (s.spawn_entity)(s.ctx, YogStr::from_str(entity_type),
141                YogStr::from_str(dimension), yog_abi::YogVec3 { x, y, z })
142        };
143        if owned.is_none() { return None; }
144        let result = unsafe {
145            String::from_utf8(
146                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
147            ).ok()
148        };
149        unsafe { (s.free_str)(owned.ptr, owned.len) };
150        result
151    }
152
153    fn entity_add_effect(&self, uuid: &str, effect_id: &str, duration_ticks: i32, amplifier: u8, show_particles: bool) -> bool {
154        let s = srv!(self);
155        unsafe { (s.entity_add_effect)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(effect_id), duration_ticks, amplifier, show_particles) }
156    }
157
158    fn entity_remove_effect(&self, uuid: &str, effect_id: &str) -> bool {
159        let s = srv!(self);
160        unsafe { (s.entity_remove_effect)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(effect_id)) }
161    }
162
163    fn entity_clear_effects(&self, uuid: &str) -> bool {
164        let s = srv!(self);
165        unsafe { (s.entity_clear_effects)(s.ctx, YogStr::from_str(uuid)) }
166    }
167
168    fn drop_loot(&self, table_id: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
169        let s = srv!(self);
170        unsafe { (s.drop_loot)(s.ctx, YogStr::from_str(table_id), YogStr::from_str(dimension), yog_abi::YogVec3 { x, y, z }) }
171    }
172
173    fn has_item_tag(&self, item_id: &str, tag_id: &str) -> bool {
174        let s = srv!(self);
175        unsafe { (s.has_item_tag)(s.ctx, YogStr::from_str(item_id), YogStr::from_str(tag_id)) }
176    }
177
178    fn has_block_tag(&self, block_id: &str, tag_id: &str) -> bool {
179        let s = srv!(self);
180        unsafe { (s.has_block_tag)(s.ctx, YogStr::from_str(block_id), YogStr::from_str(tag_id)) }
181    }
182
183    fn world_time(&self, dimension: &str) -> Option<i64> {
184        let s = srv!(self);
185        let mut t = 0i64;
186        if unsafe { (s.world_time)(s.ctx, YogStr::from_str(dimension), &mut t) } { Some(t) } else { None }
187    }
188
189    fn world_set_time(&self, dimension: &str, time: i64) -> bool {
190        let s = srv!(self);
191        unsafe { (s.set_time)(s.ctx, YogStr::from_str(dimension), time) }
192    }
193
194    fn world_is_raining(&self, dimension: &str) -> bool {
195        let s = srv!(self);
196        unsafe { (s.is_raining)(s.ctx, YogStr::from_str(dimension)) }
197    }
198
199    fn world_set_weather(&self, dimension: &str, raining: bool, duration_ticks: i32) -> bool {
200        let s = srv!(self);
201        unsafe { (s.set_weather)(s.ctx, YogStr::from_str(dimension), raining, duration_ticks) }
202    }
203
204    fn entity_velocity(&self, uuid: &str) -> Option<(f64, f64, f64)> {
205        let s = srv!(self);
206        let mut v = yog_abi::YogVec3 { x: 0.0, y: 0.0, z: 0.0 };
207        if unsafe { (s.entity_velocity)(s.ctx, YogStr::from_str(uuid), &mut v) } {
208            Some((v.x, v.y, v.z))
209        } else {
210            None
211        }
212    }
213
214    fn entity_set_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
215        let s = srv!(self);
216        unsafe { (s.entity_set_velocity)(s.ctx, YogStr::from_str(uuid), yog_abi::YogVec3 { x: vx, y: vy, z: vz }) }
217    }
218
219    fn entity_add_velocity(&self, uuid: &str, vx: f64, vy: f64, vz: f64) -> bool {
220        let s = srv!(self);
221        unsafe { (s.entity_add_velocity)(s.ctx, YogStr::from_str(uuid), yog_abi::YogVec3 { x: vx, y: vy, z: vz }) }
222    }
223
224    fn scoreboard_get(&self, objective: &str, player: &str) -> Option<i32> {
225        let s = srv!(self);
226        let mut score = 0i32;
227        if unsafe { (s.scoreboard_get)(s.ctx, YogStr::from_str(objective), YogStr::from_str(player), &mut score) } { Some(score) } else { None }
228    }
229
230    fn scoreboard_set(&self, objective: &str, player: &str, score: i32) -> bool {
231        let s = srv!(self);
232        unsafe { (s.scoreboard_set)(s.ctx, YogStr::from_str(objective), YogStr::from_str(player), score) }
233    }
234
235    fn scoreboard_add(&self, objective: &str, player: &str, delta: i32) -> Option<i32> {
236        let s = srv!(self);
237        let mut new_score = 0i32;
238        if unsafe { (s.scoreboard_add)(s.ctx, YogStr::from_str(objective), YogStr::from_str(player), delta, &mut new_score) } { Some(new_score) } else { None }
239    }
240
241    fn game_dir(&self) -> String {
242        let s = srv!(self);
243        let owned = unsafe { (s.game_dir)(s.ctx) };
244        if owned.is_none() { return String::new(); }
245        let result = unsafe {
246            String::from_utf8(
247                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
248            ).unwrap_or_default()
249        };
250        unsafe { (s.free_str)(owned.ptr, owned.len) };
251        result
252    }
253
254    fn play_sound(&self, dimension: &str, x: f64, y: f64, z: f64, sound_id: &str, volume: f32, pitch: f32) -> bool {
255        let s = srv!(self);
256        unsafe { (s.play_sound)(s.ctx, YogStr::from_str(dimension), yog_abi::YogVec3 { x, y, z }, YogStr::from_str(sound_id), volume, pitch) }
257    }
258
259    fn play_sound_to_player(&self, player: &str, sound_id: &str, volume: f32, pitch: f32) -> bool {
260        let s = srv!(self);
261        unsafe { (s.play_sound_player)(s.ctx, YogStr::from_str(player), YogStr::from_str(sound_id), volume, pitch) }
262    }
263
264    fn send_title(&self, player: &str, title: &str, subtitle: &str, fadein: i32, stay: i32, fadeout: i32) -> bool {
265        let s = srv!(self);
266        unsafe { (s.send_title)(s.ctx, YogStr::from_str(player), YogStr::from_str(title), YogStr::from_str(subtitle), fadein, stay, fadeout) }
267    }
268
269    fn entity_rotation(&self, uuid: &str) -> Option<(f32, f32)> {
270        let s = srv!(self);
271        let mut out = yog_abi::YogVec3 { x: 0.0, y: 0.0, z: 0.0 };
272        let ok = unsafe { (s.entity_rotation)(s.ctx, YogStr::from_str(uuid), &mut out) };
273        if ok { Some((out.x as f32, out.y as f32)) } else { None }
274    }
275
276    fn send_actionbar(&self, player: &str, message: &str) -> bool {
277        let s = srv!(self);
278        unsafe { (s.send_actionbar)(s.ctx, YogStr::from_str(player), YogStr::from_str(message)) }
279    }
280
281    fn kick_player(&self, player: &str, reason: &str) -> bool {
282        let s = srv!(self);
283        unsafe { (s.kick_player)(s.ctx, YogStr::from_str(player), YogStr::from_str(reason)) }
284    }
285
286    fn set_gamemode(&self, player: &str, gamemode: &str) -> bool {
287        let s = srv!(self);
288        unsafe { (s.set_gamemode)(s.ctx, YogStr::from_str(player), YogStr::from_str(gamemode)) }
289    }
290
291    fn bossbar_create(&self, id: &str, title: &str, color: &str, style: &str) -> bool {
292        let s = srv!(self);
293        unsafe { (s.bossbar_create)(s.ctx, YogStr::from_str(id), YogStr::from_str(title), YogStr::from_str(color), YogStr::from_str(style)) }
294    }
295
296    fn bossbar_remove(&self, id: &str) -> bool {
297        let s = srv!(self);
298        unsafe { (s.bossbar_remove)(s.ctx, YogStr::from_str(id)) }
299    }
300
301    fn bossbar_set_title(&self, id: &str, title: &str) -> bool {
302        let s = srv!(self);
303        unsafe { (s.bossbar_set_title)(s.ctx, YogStr::from_str(id), YogStr::from_str(title)) }
304    }
305
306    fn bossbar_set_progress(&self, id: &str, progress: f32) -> bool {
307        let s = srv!(self);
308        unsafe { (s.bossbar_set_progress)(s.ctx, YogStr::from_str(id), progress) }
309    }
310
311    fn bossbar_set_color(&self, id: &str, color: &str) -> bool {
312        let s = srv!(self);
313        unsafe { (s.bossbar_set_color)(s.ctx, YogStr::from_str(id), YogStr::from_str(color)) }
314    }
315
316    fn bossbar_add_player(&self, id: &str, player: &str) -> bool {
317        let s = srv!(self);
318        unsafe { (s.bossbar_add_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
319    }
320
321    fn bossbar_remove_player(&self, id: &str, player: &str) -> bool {
322        let s = srv!(self);
323        unsafe { (s.bossbar_remove_player)(s.ctx, YogStr::from_str(id), YogStr::from_str(player)) }
324    }
325
326    fn bossbar_set_visible(&self, id: &str, visible: bool) -> bool {
327        let s = srv!(self);
328        unsafe { (s.bossbar_set_visible)(s.ctx, YogStr::from_str(id), visible) }
329    }
330
331    fn online_players(&self) -> Vec<String> {
332        let s = srv!(self);
333        let owned = unsafe { (s.online_players)(s.ctx) };
334        if owned.is_none() { return Vec::new(); }
335        let text = unsafe {
336            String::from_utf8(
337                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
338            ).unwrap_or_default()
339        };
340        unsafe { (s.free_str)(owned.ptr, owned.len) };
341        if text.is_empty() { Vec::new() } else { text.lines().map(str::to_owned).collect() }
342    }
343
344    fn get_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos) -> Option<String> {
345        let s = srv!(self);
346        let owned = unsafe {
347            (s.get_block_nbt)(s.ctx, YogStr::from_str(dimension),
348                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z })
349        };
350        if owned.is_none() { return None; }
351        let result = unsafe {
352            String::from_utf8(
353                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
354            ).ok()
355        };
356        unsafe { (s.free_str)(owned.ptr, owned.len) };
357        result
358    }
359
360    fn set_block_nbt(&self, dimension: &str, pos: yog_core::BlockPos, snbt: &str) -> bool {
361        let s = srv!(self);
362        unsafe {
363            (s.set_block_nbt)(s.ctx, YogStr::from_str(dimension),
364                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z },
365                YogStr::from_str(snbt))
366        }
367    }
368
369    fn get_inventory_slot(&self, dimension: &str, pos: yog_core::BlockPos, slot: u32) -> Option<(String, u32)> {
370        let s = srv!(self);
371        let owned = unsafe {
372            (s.get_inventory_slot)(s.ctx, YogStr::from_str(dimension),
373                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z }, slot)
374        };
375        if owned.is_none() { return None; }
376        let text = unsafe {
377            String::from_utf8(
378                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
379            ).unwrap_or_default()
380        };
381        unsafe { (s.free_str)(owned.ptr, owned.len) };
382        let (item_id, count) = text.split_once('\t')?;
383        Some((item_id.to_owned(), count.parse().ok()?))
384    }
385
386    fn set_inventory_slot(&self, dimension: &str, pos: yog_core::BlockPos, slot: u32, item_id: &str, count: u32) -> bool {
387        let s = srv!(self);
388        unsafe {
389            (s.set_inventory_slot)(s.ctx, YogStr::from_str(dimension),
390                yog_abi::YogBlockPos { x: pos.x, y: pos.y, z: pos.z }, slot,
391                YogStr::from_str(item_id), count)
392        }
393    }
394
395    fn player_inventory(&self, player: &str) -> Vec<(u32, String, u32)> {
396        let s = srv!(self);
397        let owned = unsafe { (s.player_inventory)(s.ctx, YogStr::from_str(player)) };
398        if owned.is_none() { return Vec::new(); }
399        let text = unsafe {
400            String::from_utf8(
401                std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
402            ).unwrap_or_default()
403        };
404        unsafe { (s.free_str)(owned.ptr, owned.len) };
405        text.lines().filter_map(|line| {
406            let mut it = line.split('\t');
407            let slot: u32 = it.next()?.parse().ok()?;
408            let item_id = it.next()?.to_owned();
409            let count: u32 = it.next()?.parse().ok()?;
410            Some((slot, item_id, count))
411        }).collect()
412    }
413
414    fn player_set_slot(&self, player: &str, slot: u32, item_id: &str, count: u32) -> bool {
415        let s = srv!(self);
416        unsafe {
417            (s.player_set_slot)(s.ctx, YogStr::from_str(player), slot, YogStr::from_str(item_id), count)
418        }
419    }
420
421    fn teleport_to_dim(&self, player: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
422        let s = srv!(self);
423        unsafe {
424            (s.player_teleport_dim)(s.ctx, YogStr::from_str(player),
425                YogStr::from_str(dimension), yog_abi::YogVec3 { x, y, z })
426        }
427    }
428
429    fn entity_teleport_to_dim(&self, uuid: &str, dimension: &str, x: f64, y: f64, z: f64) -> bool {
430        let s = srv!(self);
431        unsafe {
432            (s.entity_teleport_dim)(s.ctx, YogStr::from_str(uuid),
433                YogStr::from_str(dimension), yog_abi::YogVec3 { x, y, z })
434        }
435    }
436
437    fn world_entity_count(&self, dimension: &str, entity_type: &str) -> i32 {
438        let s = srv!(self);
439        unsafe { (s.world_entity_count)(s.ctx, YogStr::from_str(dimension), YogStr::from_str(entity_type)) }
440    }
441
442    fn entity_get_nbt(&self, uuid: &str) -> Option<String> {
443        let s = srv!(self);
444        let owned = unsafe { (s.entity_get_nbt)(s.ctx, YogStr::from_str(uuid)) };
445        if owned.is_none() { return None; }
446        let result = unsafe {
447            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()).ok()
448        };
449        unsafe { (s.free_str)(owned.ptr, owned.len) };
450        result
451    }
452
453    fn entity_set_nbt(&self, uuid: &str, snbt: &str) -> bool {
454        let s = srv!(self);
455        unsafe { (s.entity_set_nbt)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(snbt)) }
456    }
457
458    fn spawn_particles(&self, dimension: &str, x: f64, y: f64, z: f64, particle_type: &str, count: i32, dx: f64, dy: f64, dz: f64, speed: f64) -> bool {
459        let s = srv!(self);
460        unsafe {
461            (s.spawn_particles)(s.ctx, YogStr::from_str(dimension),
462                yog_abi::YogVec3 { x, y, z }, YogStr::from_str(particle_type),
463                count, dx, dy, dz, speed)
464        }
465    }
466
467    fn entity_attribute_get(&self, uuid: &str, attribute_id: &str) -> Option<f64> {
468        let s = srv!(self);
469        let v = unsafe { (s.entity_attribute_get)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(attribute_id)) };
470        if v.is_nan() { None } else { Some(v) }
471    }
472
473    fn entity_attribute_set(&self, uuid: &str, attribute_id: &str, value: f64) -> bool {
474        let s = srv!(self);
475        unsafe { (s.entity_attribute_set)(s.ctx, YogStr::from_str(uuid), YogStr::from_str(attribute_id), value) }
476    }
477
478    fn get_held_item_nbt(&self, player: &str) -> Option<String> {
479        let s = srv!(self);
480        let owned = unsafe { (s.get_held_item_nbt)(s.ctx, YogStr::from_str(player)) };
481        if owned.is_none() { return None; }
482        let result = unsafe {
483            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()).ok()
484        };
485        unsafe { (s.free_str)(owned.ptr, owned.len) };
486        result
487    }
488
489    fn set_held_item_nbt(&self, player: &str, snbt: &str) -> bool {
490        let s = srv!(self);
491        unsafe { (s.set_held_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
492    }
493
494    fn get_offhand_item_nbt(&self, player: &str) -> Option<String> {
495        let s = srv!(self);
496        let owned = unsafe { (s.get_offhand_item_nbt)(s.ctx, YogStr::from_str(player)) };
497        if owned.is_none() { return None; }
498        let result = unsafe {
499            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()).ok()
500        };
501        unsafe { (s.free_str)(owned.ptr, owned.len) };
502        result
503    }
504
505    fn set_offhand_item_nbt(&self, player: &str, snbt: &str) -> bool {
506        let s = srv!(self);
507        unsafe { (s.set_offhand_item_nbt)(s.ctx, YogStr::from_str(player), YogStr::from_str(snbt)) }
508    }
509
510    fn get_slot_item(&self, player: &str, slot: u32) -> Option<(String, u32, String)> {
511        let s = srv!(self);
512        let owned = unsafe { (s.get_slot_item)(s.ctx, YogStr::from_str(player), slot) };
513        if owned.is_none() { return None; }
514        let text = unsafe {
515            String::from_utf8(std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec())
516                .unwrap_or_default()
517        };
518        unsafe { (s.free_str)(owned.ptr, owned.len) };
519        let mut it = text.splitn(3, '\t');
520        let item_id = it.next()?.to_owned();
521        let count: u32 = it.next()?.parse().ok()?;
522        let nbt = it.next().unwrap_or("{}").to_owned();
523        Some((item_id, count, nbt))
524    }
525
526    fn set_slot_item(&self, player: &str, slot: u32, item_id: &str, count: u32, snbt: &str) -> bool {
527        let s = srv!(self);
528        unsafe {
529            (s.set_slot_item)(s.ctx, YogStr::from_str(player), slot,
530                YogStr::from_str(item_id), count, YogStr::from_str(snbt))
531        }
532    }
533}
534
535// ── Trampoline helpers ────────────────────────────────────────────────────────
536//
537// Each trampoline is a `unsafe extern "C" fn` that:
538//   1. Casts `ud` back to the original boxed closure.
539//   2. Converts ABI C structs to Rust event types.
540//   3. Builds a `CServer` from the `*const YogServer`.
541//   4. Calls the closure.
542//
543// Closures are Box::into_raw'd in Registry methods — they are INTENTIONALLY
544// leaked and live for the process lifetime.
545
546// ── Trampoline helpers ────────────────────────────────────────────────────────
547//
548// All phased event trampolines share the same pattern:
549//   `phase: u8`  0 = EventPhase::Pre, 1 = EventPhase::Post
550//   Return value is meaningful only in Pre phase.
551
552macro_rules! trampoline_phased {
553    ($name:ident, $abi_ev:ty, $rust_ev:ty, |$ev:ident| $build:expr) => {
554        unsafe extern "C" fn $name<F>(
555            ud: *mut c_void, srv: *const YogServer, ev: *const $abi_ev, phase: u8,
556        ) -> bool
557        where F: Fn(&$rust_ev, EventPhase, &dyn Server) -> bool + Send + Sync,
558        {
559            let f = &*(ud as *const F);
560            let $ev = &*ev;
561            let rust_ev = $build;
562            let p = if phase == 0 { EventPhase::Pre } else { EventPhase::Post };
563            f(&rust_ev, p, &CServer(srv))
564        }
565    };
566}
567
568trampoline_phased!(trampoline_block_break, YogBlockBreakEvent, BlockBreakEvent, |ev| BlockBreakEvent {
569    player_name: ev.player.as_str().to_owned(),
570    block_id:    ev.block.as_str().to_owned(),
571    pos: yog_core::BlockPos { x: ev.pos.x, y: ev.pos.y, z: ev.pos.z },
572});
573
574trampoline_phased!(trampoline_chat, YogChatEvent, ChatEvent, |ev| ChatEvent {
575    player_name: ev.player.as_str().to_owned(),
576    message:     ev.message.as_str().to_owned(),
577});
578
579trampoline_phased!(trampoline_player_join, YogPlayerEvent, PlayerJoinEvent, |ev| PlayerJoinEvent {
580    player_name: ev.player.as_str().to_owned(),
581    uuid:        ev.uuid.as_str().to_owned(),
582});
583
584trampoline_phased!(trampoline_player_leave, YogPlayerEvent, PlayerLeaveEvent, |ev| PlayerLeaveEvent {
585    player_name: ev.player.as_str().to_owned(),
586    uuid:        ev.uuid.as_str().to_owned(),
587});
588
589trampoline_phased!(trampoline_use_item, YogUseItemEvent, UseItemEvent, |ev| UseItemEvent {
590    player_name: ev.player.as_str().to_owned(),
591    item_id:     ev.item.as_str().to_owned(),
592    sneaking: ev.sneaking,
593});
594
595trampoline_phased!(trampoline_use_block, YogUseBlockEvent, UseBlockEvent, |ev| UseBlockEvent {
596    player_name: ev.player.as_str().to_owned(),
597    block_id:    ev.block.as_str().to_owned(),
598    pos: yog_core::BlockPos { x: ev.pos.x, y: ev.pos.y, z: ev.pos.z },
599});
600
601trampoline_phased!(trampoline_attack_entity, YogAttackEntityEvent, AttackEntityEvent, |ev| AttackEntityEvent {
602    player_name: ev.player.as_str().to_owned(),
603    target_type: ev.target_type.as_str().to_owned(),
604    target_uuid: ev.target_uuid.as_str().to_owned(),
605});
606
607trampoline_phased!(trampoline_entity_damage, YogEntityDamageEvent, EntityDamageEvent, |ev| EntityDamageEvent {
608    entity_type: ev.entity_type.as_str().to_owned(),
609    uuid:        ev.uuid.as_str().to_owned(),
610    amount:      ev.amount,
611    source:      ev.source.as_str().to_owned(),
612});
613
614trampoline_phased!(trampoline_entity_death, YogEntityDeathEvent, EntityDeathEvent, |ev| EntityDeathEvent {
615    entity_type: ev.entity_type.as_str().to_owned(),
616    uuid:        ev.uuid.as_str().to_owned(),
617    source:      ev.source.as_str().to_owned(),
618});
619
620trampoline_phased!(trampoline_entity_spawn, YogEntitySpawnEvent, EntitySpawnEvent, |ev| EntitySpawnEvent {
621    entity_type: ev.entity_type.as_str().to_owned(),
622    uuid:        ev.uuid.as_str().to_owned(),
623    dimension:   ev.dimension.as_str().to_owned(),
624});
625
626trampoline_phased!(trampoline_place_block, YogPlaceBlockEvent, PlaceBlockEvent, |ev| PlaceBlockEvent {
627    player_name: ev.player.as_str().to_owned(),
628    block_id:    ev.block.as_str().to_owned(),
629    pos: yog_core::BlockPos { x: ev.pos.x, y: ev.pos.y, z: ev.pos.z },
630});
631
632trampoline_phased!(trampoline_player_death, YogPlayerDeathEvent, PlayerDeathEvent, |ev| PlayerDeathEvent {
633    player_name: ev.player.as_str().to_owned(),
634    uuid:        ev.uuid.as_str().to_owned(),
635    source:      ev.source.as_str().to_owned(),
636});
637
638trampoline_phased!(trampoline_player_respawn, YogPlayerRespawnEvent, PlayerRespawnEvent, |ev| PlayerRespawnEvent {
639    player_name: ev.player.as_str().to_owned(),
640    uuid:        ev.uuid.as_str().to_owned(),
641    at_anchor:   ev.at_anchor,
642});
643
644trampoline_phased!(trampoline_advancement, YogAdvancementEvent, AdvancementEvent, |ev| AdvancementEvent {
645    player_name:    ev.player.as_str().to_owned(),
646    uuid:           ev.uuid.as_str().to_owned(),
647    advancement_id: ev.advancement.as_str().to_owned(),
648});
649
650trampoline_phased!(trampoline_entity_interact, YogEntityInteractEvent, EntityInteractEvent, |ev| EntityInteractEvent {
651    player_name: ev.player.as_str().to_owned(),
652    player_uuid: ev.player_uuid.as_str().to_owned(),
653    entity_type: ev.entity_type.as_str().to_owned(),
654    entity_uuid: ev.entity_uuid.as_str().to_owned(),
655    hand:        ev.hand.as_str().to_owned(),
656});
657
658trampoline_phased!(trampoline_craft, YogCraftEvent, CraftEvent, |ev| CraftEvent {
659    player_name:  ev.player.as_str().to_owned(),
660    player_uuid:  ev.player_uuid.as_str().to_owned(),
661    result_item:  ev.result_item.as_str().to_owned(),
662    result_count: ev.result_count,
663});
664
665trampoline_phased!(trampoline_explosion, YogExplosionEvent, ExplosionEvent, |ev| ExplosionEvent {
666    dimension:  ev.dimension.as_str().to_owned(),
667    x:          ev.x,
668    y:          ev.y,
669    z:          ev.z,
670    power:      ev.power,
671    cause_uuid: ev.cause_uuid.as_str().to_owned(),
672});
673
674trampoline_phased!(trampoline_item_pickup, YogItemPickupEvent, ItemPickupEvent, |ev| ItemPickupEvent {
675    player_name: ev.player.as_str().to_owned(),
676    player_uuid: ev.player_uuid.as_str().to_owned(),
677    item_id:     ev.item_id.as_str().to_owned(),
678    item_count:  ev.item_count,
679    entity_uuid: ev.entity_uuid.as_str().to_owned(),
680});
681
682trampoline_phased!(trampoline_player_move, YogPlayerMoveEvent, PlayerMoveEvent, |ev| PlayerMoveEvent {
683    player_name: ev.player.as_str().to_owned(),
684    player_uuid: ev.player_uuid.as_str().to_owned(),
685    x:     ev.x,
686    y:     ev.y,
687    z:     ev.z,
688    yaw:   ev.yaw,
689    pitch: ev.pitch,
690});
691
692trampoline_phased!(trampoline_container_open, YogContainerOpenEvent, ContainerOpenEvent, |ev| ContainerOpenEvent {
693    player_name:    ev.player.as_str().to_owned(),
694    player_uuid:    ev.player_uuid.as_str().to_owned(),
695    container_type: ev.container_type.as_str().to_owned(),
696});
697
698trampoline_phased!(trampoline_container_close, YogContainerCloseEvent, ContainerCloseEvent, |ev| ContainerCloseEvent {
699    player_name: ev.player.as_str().to_owned(),
700    player_uuid: ev.player_uuid.as_str().to_owned(),
701});
702
703trampoline_phased!(trampoline_projectile_hit, YogProjectileHitEvent, ProjectileHitEvent, |ev| ProjectileHitEvent {
704    projectile_type: ev.projectile_type.as_str().to_owned(),
705    projectile_uuid: ev.projectile_uuid.as_str().to_owned(),
706    shooter_uuid:    ev.shooter_uuid.as_str().to_owned(),
707    hit_type:        ev.hit_type.as_str().to_owned(),
708    hit_entity_uuid: ev.hit_entity_uuid.as_str().to_owned(),
709    x:               ev.x,
710    y:               ev.y,
711    z:               ev.z,
712    dimension:       ev.dimension.as_str().to_owned(),
713});
714
715unsafe extern "C" fn trampoline_server_fn<F>(ud: *mut c_void, srv: *const YogServer)
716where F: Fn(&dyn Server) + Send + Sync,
717{
718    let f = &*(ud as *const F);
719    f(&CServer(srv));
720}
721
722unsafe extern "C" fn trampoline_packet<F>(ud: *mut c_void, srv: *const YogServer, ev: *const YogPacketEvent)
723where F: Fn(&PacketEvent, &dyn Server) + Send + Sync,
724{
725    let f = &*(ud as *const F);
726    let ev = &*ev;
727    let rust_ev = PacketEvent {
728        channel: ev.channel.as_str().to_owned(),
729        player:  ev.player.as_str().to_owned(),
730        payload: std::slice::from_raw_parts(ev.payload, ev.payload_len as usize).to_vec(),
731    };
732    f(&rust_ev, &CServer(srv));
733}
734
735unsafe extern "C" fn trampoline_command<F>(
736    ud: *mut c_void,
737    srv: *const YogServer,
738    ev: *const YogCommandEvent,
739    reply_buf: *mut u8,
740    reply_cap: u32,
741    reply_len: *mut u32,
742) where F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync,
743{
744    let f = &*(ud as *const F);
745    let ev = &*ev;
746    let ctx = CommandContext {
747        name:   ev.name.as_str().to_owned(),
748        args:   ev.args.as_str().to_owned(),
749        source: ev.source.as_str().to_owned(),
750        uuid:   ev.uuid.as_str().to_owned(),
751    };
752    *reply_len = 0;
753    if let Some(reply) = f(&ctx, &CServer(srv)) {
754        let bytes = reply.as_bytes();
755        let n = bytes.len().min(reply_cap as usize);
756        std::ptr::copy_nonoverlapping(bytes.as_ptr(), reply_buf, n);
757        *reply_len = n as u32;
758    }
759}
760
761unsafe extern "C" fn trampoline_scheduled<F>(ud: *mut c_void, srv: *const YogServer)
762where F: Fn(&dyn Server) + Send + Sync,
763{
764    let f = &*(ud as *const F);
765    f(&CServer(srv));
766}
767
768// ── ABI minor 10 — client-side trampolines ────────────────────────────────────
769
770unsafe extern "C" fn trampoline_client_tick<F>(ud: *mut c_void)
771where F: Fn(&ClientTickEvent) + Send + Sync,
772{
773    let f = &*(ud as *const F);
774    f(&ClientTickEvent {});
775}
776
777unsafe extern "C" fn trampoline_hud_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
778where F: Fn(&GfxContext) + Send + Sync,
779{
780    let f = &*(ud as *const F);
781    f(&GfxContext::from_raw(gfx));
782}
783
784unsafe extern "C" fn trampoline_world_render<F>(ud: *mut c_void, gfx: *const YogGfxApi)
785where F: Fn(&GfxContext) + Send + Sync,
786{
787    let f = &*(ud as *const F);
788    f(&GfxContext::from_raw(gfx));
789}
790
791unsafe extern "C" fn trampoline_key_press<F>(
792    ud: *mut c_void,
793    ev: *const YogKeyPressEvent,
794) -> bool
795where F: Fn(&KeyPressEvent) -> bool + Send + Sync,
796{
797    let f = &*(ud as *const F);
798    let e = &*ev;
799    f(&KeyPressEvent {
800        key_code:  e.key_code,
801        scan_code: e.scan_code,
802        action:    e.action,
803        modifiers: e.modifiers,
804    })
805}
806
807unsafe extern "C" fn trampoline_screen<F>(ud: *mut c_void, screen_class: YogStr) -> bool
808where F: Fn(&ScreenEvent) + Send + Sync,
809{
810    let f = &*(ud as *const F);
811    f(&ScreenEvent { screen_class: screen_class.as_str().to_owned() });
812    true
813}
814
815// ── Registry ─────────────────────────────────────────────────────────────────
816
817/// Wraps the [`YogApi`] table and provides an ergonomic registration API.
818///
819/// Obtained inside `yog_mod_register` via `export_mod!`.  Closures registered
820/// here are boxed and leaked — they live as long as the process (which is the
821/// correct lifetime for a server mod).
822/// Pointer to the runtime's `YogApi` table (a process-lifetime static in the
823/// runtime), captured when the mod registers. Lets free functions like
824/// [`installed_mods`] work outside of `register()` — e.g. in UI handlers.
825static GLOBAL_API: std::sync::atomic::AtomicPtr<YogApi> =
826    std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
827
828/// Metadata of an installed mod, as reported by the loader.
829#[derive(Debug, Clone)]
830pub struct ModInfo {
831    /// `"yog"` for .yog mods, `"platform"` for loader (Java) mods.
832    pub source:      String,
833    pub id:          String,
834    pub name:        String,
835    pub version:     String,
836    /// Comma-separated author list (may be empty).
837    pub authors:     String,
838    pub description: String,
839}
840
841/// All installed mods known to the loader: .yog mods plus, where the host
842/// exposes them, platform (Java) mods. Callable at any time after this mod's
843/// `register()` ran — including client-side UI handlers. Note that during
844/// `register()` mods that load after this one are not in the list yet; query
845/// lazily (e.g. on first render) for a complete view.
846pub fn installed_mods() -> Vec<ModInfo> {
847    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
848    if api.is_null() { return Vec::new(); }
849    let owned = unsafe { ((*api).mods_list)((*api).ctx) };
850    if owned.is_none() { return Vec::new(); }
851    let text = unsafe {
852        String::from_utf8(
853            std::slice::from_raw_parts(owned.ptr, owned.len as usize).to_vec()
854        ).unwrap_or_default()
855    };
856    unsafe { ((*api).free_str)(owned.ptr, owned.len) };
857    text.lines()
858        .filter_map(|line| {
859            let mut f = line.split('\t');
860            Some(ModInfo {
861                source:      f.next()?.to_string(),
862                id:          f.next()?.to_string(),
863                name:        f.next().unwrap_or_default().to_string(),
864                version:     f.next().unwrap_or_default().to_string(),
865                authors:     f.next().unwrap_or_default().to_string(),
866                description: f.next().unwrap_or_default().to_string(),
867            })
868        })
869        .collect()
870}
871
872/// Open the Yog UI registered as `ui_id` (client side; no-op on dedicated
873/// servers). `modal` blocks game input, `pause` pauses singleplayer. Callable
874/// from any handler after registration — e.g. an `on_client_packet` handler.
875pub fn open_ui(ui_id: &str, modal: bool, pause: bool) {
876    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
877    if api.is_null() { return; }
878    unsafe { ((*api).ui_open)((*api).ctx, YogStr::from_str(ui_id), modal, pause) }
879}
880
881/// Handle to the runtime's server-action table, usable from any handler after
882/// registration — including client-side ones that don't receive `&dyn Server`
883/// (UI event handlers, client tick). Server-world actions are no-ops client
884/// side, but networking (`send_to_server`) and similar client-safe calls work.
885pub fn server() -> Option<CServer> {
886    let api = GLOBAL_API.load(std::sync::atomic::Ordering::Acquire);
887    if api.is_null() { return None; }
888    let srv = unsafe { (*api).server };
889    if srv.is_null() { return None; }
890    Some(CServer(srv))
891}
892
893pub struct Registry {
894    api: *const YogApi,
895}
896
897// SAFETY: `api` is a static provided by the runtime, valid for process lifetime.
898unsafe impl Send for Registry {}
899unsafe impl Sync for Registry {}
900
901impl Registry {
902    /// Build from the pointer passed by the runtime. Only called by `export_mod!`.
903    pub unsafe fn from_raw(api: *const YogApi) -> Self {
904        GLOBAL_API.store(api as *mut YogApi, std::sync::atomic::Ordering::Release);
905        Self { api }
906    }
907
908    #[inline]
909    fn ctx(&self) -> *mut c_void { unsafe { (*self.api).ctx } }
910
911    // ── helpers ──────────────────────────────────────────────────────────────
912
913    fn leak<F: 'static>(f: F) -> *mut c_void {
914        Box::into_raw(Box::new(f)) as *mut c_void
915    }
916
917    // ── events ───────────────────────────────────────────────────────────────
918    //
919    // All handlers receive `(event, EventPhase, &dyn Server) -> bool`.
920    // In `Pre` phase, returning `false` cancels the action.
921    // In `Post` phase, the return value is ignored.
922    // A single registration fires for BOTH phases.
923
924    pub fn on_block_break<F>(&mut self, handler: F)
925    where F: Fn(&BlockBreakEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
926        let ud = Self::leak(handler);
927        unsafe { ((*self.api).on_block_break)(self.ctx(), ud, trampoline_block_break::<F>) }
928    }
929
930    pub fn on_chat<F>(&mut self, handler: F)
931    where F: Fn(&ChatEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
932        let ud = Self::leak(handler);
933        unsafe { ((*self.api).on_chat)(self.ctx(), ud, trampoline_chat::<F>) }
934    }
935
936    pub fn on_player_join<F>(&mut self, handler: F)
937    where F: Fn(&PlayerJoinEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
938        let ud = Self::leak(handler);
939        unsafe { ((*self.api).on_player_join)(self.ctx(), ud, trampoline_player_join::<F>) }
940    }
941
942    pub fn on_player_leave<F>(&mut self, handler: F)
943    where F: Fn(&PlayerLeaveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
944        let ud = Self::leak(handler);
945        unsafe { ((*self.api).on_player_leave)(self.ctx(), ud, trampoline_player_leave::<F>) }
946    }
947
948    pub fn on_use_item<F>(&mut self, handler: F)
949    where F: Fn(&UseItemEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
950        let ud = Self::leak(handler);
951        unsafe { ((*self.api).on_use_item)(self.ctx(), ud, trampoline_use_item::<F>) }
952    }
953
954    pub fn on_use_block<F>(&mut self, handler: F)
955    where F: Fn(&UseBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
956        let ud = Self::leak(handler);
957        unsafe { ((*self.api).on_use_block)(self.ctx(), ud, trampoline_use_block::<F>) }
958    }
959
960    pub fn on_attack_entity<F>(&mut self, handler: F)
961    where F: Fn(&AttackEntityEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
962        let ud = Self::leak(handler);
963        unsafe { ((*self.api).on_attack_entity)(self.ctx(), ud, trampoline_attack_entity::<F>) }
964    }
965
966    pub fn on_entity_damage<F>(&mut self, handler: F)
967    where F: Fn(&EntityDamageEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
968        let ud = Self::leak(handler);
969        unsafe { ((*self.api).on_entity_damage)(self.ctx(), ud, trampoline_entity_damage::<F>) }
970    }
971
972    pub fn on_entity_death<F>(&mut self, handler: F)
973    where F: Fn(&EntityDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
974        let ud = Self::leak(handler);
975        unsafe { ((*self.api).on_entity_death)(self.ctx(), ud, trampoline_entity_death::<F>) }
976    }
977
978    pub fn on_entity_spawn<F>(&mut self, handler: F)
979    where F: Fn(&EntitySpawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
980        let ud = Self::leak(handler);
981        unsafe { ((*self.api).on_entity_spawn)(self.ctx(), ud, trampoline_entity_spawn::<F>) }
982    }
983
984    pub fn on_player_place_block<F>(&mut self, handler: F)
985    where F: Fn(&PlaceBlockEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
986        let ud = Self::leak(handler);
987        unsafe { ((*self.api).on_player_place_block)(self.ctx(), ud, trampoline_place_block::<F>) }
988    }
989
990    pub fn on_player_death<F>(&mut self, handler: F)
991    where F: Fn(&PlayerDeathEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
992        let ud = Self::leak(handler);
993        unsafe { ((*self.api).on_player_death)(self.ctx(), ud, trampoline_player_death::<F>) }
994    }
995
996    pub fn on_player_respawn<F>(&mut self, handler: F)
997    where F: Fn(&PlayerRespawnEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
998        let ud = Self::leak(handler);
999        unsafe { ((*self.api).on_player_respawn)(self.ctx(), ud, trampoline_player_respawn::<F>) }
1000    }
1001
1002    pub fn on_advancement<F>(&mut self, handler: F)
1003    where F: Fn(&AdvancementEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1004        let ud = Self::leak(handler);
1005        unsafe { ((*self.api).on_advancement)(self.ctx(), ud, trampoline_advancement::<F>) }
1006    }
1007
1008    pub fn on_entity_interact<F>(&mut self, handler: F)
1009    where F: Fn(&EntityInteractEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1010        let ud = Self::leak(handler);
1011        unsafe { ((*self.api).on_entity_interact)(self.ctx(), ud, trampoline_entity_interact::<F>) }
1012    }
1013
1014    pub fn on_item_craft<F>(&mut self, handler: F)
1015    where F: Fn(&CraftEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1016        let ud = Self::leak(handler);
1017        unsafe { ((*self.api).on_item_craft)(self.ctx(), ud, trampoline_craft::<F>) }
1018    }
1019
1020    pub fn on_explosion<F>(&mut self, handler: F)
1021    where F: Fn(&ExplosionEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1022        let ud = Self::leak(handler);
1023        unsafe { ((*self.api).on_explosion)(self.ctx(), ud, trampoline_explosion::<F>) }
1024    }
1025
1026    pub fn on_item_pickup<F>(&mut self, handler: F)
1027    where F: Fn(&ItemPickupEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1028        let ud = Self::leak(handler);
1029        unsafe { ((*self.api).on_item_pickup)(self.ctx(), ud, trampoline_item_pickup::<F>) }
1030    }
1031
1032    pub fn on_player_move<F>(&mut self, handler: F)
1033    where F: Fn(&PlayerMoveEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1034        let ud = Self::leak(handler);
1035        unsafe { ((*self.api).on_player_move)(self.ctx(), ud, trampoline_player_move::<F>) }
1036    }
1037
1038    pub fn on_container_open<F>(&mut self, handler: F)
1039    where F: Fn(&ContainerOpenEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1040        let ud = Self::leak(handler);
1041        unsafe { ((*self.api).on_container_open)(self.ctx(), ud, trampoline_container_open::<F>) }
1042    }
1043
1044    pub fn on_container_close<F>(&mut self, handler: F)
1045    where F: Fn(&ContainerCloseEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1046        let ud = Self::leak(handler);
1047        unsafe { ((*self.api).on_container_close)(self.ctx(), ud, trampoline_container_close::<F>) }
1048    }
1049
1050    pub fn on_projectile_hit<F>(&mut self, handler: F)
1051    where F: Fn(&ProjectileHitEvent, EventPhase, &dyn Server) -> bool + Send + Sync + 'static {
1052        let ud = Self::leak(handler);
1053        unsafe { ((*self.api).on_projectile_hit)(self.ctx(), ud, trampoline_projectile_hit::<F>) }
1054    }
1055
1056    pub fn on_tick<F>(&mut self, listener: F)
1057    where F: Fn(&dyn Server) + Send + Sync + 'static {
1058        let ud = Self::leak(listener);
1059        unsafe { ((*self.api).on_server_tick)(self.ctx(), ud, trampoline_server_fn::<F>) }
1060    }
1061
1062    pub fn on_server_started<F>(&mut self, listener: F)
1063    where F: Fn(&dyn Server) + Send + Sync + 'static {
1064        let ud = Self::leak(listener);
1065        unsafe { ((*self.api).on_server_started)(self.ctx(), ud, trampoline_server_fn::<F>) }
1066    }
1067
1068    pub fn on_server_stopping<F>(&mut self, listener: F)
1069    where F: Fn(&dyn Server) + Send + Sync + 'static {
1070        let ud = Self::leak(listener);
1071        unsafe { ((*self.api).on_server_stopping)(self.ctx(), ud, trampoline_server_fn::<F>) }
1072    }
1073
1074    // ── commands ─────────────────────────────────────────────────────────────
1075
1076    pub fn on_command<F>(&mut self, name: impl AsRef<str>, handler: F)
1077    where F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static {
1078        let name_ys = YogStr::from_str(name.as_ref());
1079        let ud = Self::leak(handler);
1080        unsafe { ((*self.api).register_command)(self.ctx(), name_ys, ud, trampoline_command::<F>) }
1081    }
1082
1083    /// Register a command with Brigadier-typed arguments.
1084    ///
1085    /// `schema` is a space-separated list of argument types:
1086    /// `int`, `float`, `word`, `string` (greedy, must be last), `player`, `blockpos`.
1087    ///
1088    /// In the handler use `ctx.arg_int(0)`, `ctx.arg_blockpos(1)`, etc.
1089    pub fn on_typed_command<F>(&mut self, name: impl AsRef<str>, schema: impl AsRef<str>, handler: F)
1090    where F: Fn(&CommandContext, &dyn Server) -> Option<String> + Send + Sync + 'static {
1091        let name_ys   = YogStr::from_str(name.as_ref());
1092        let schema_ys = YogStr::from_str(schema.as_ref());
1093        let ud = Self::leak(handler);
1094        unsafe { ((*self.api).register_typed_command)(self.ctx(), name_ys, schema_ys, ud, trampoline_command::<F>) }
1095    }
1096
1097    // ── networking ───────────────────────────────────────────────────────────
1098
1099    pub fn on_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1100    where F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static {
1101        let ch = YogStr::from_str(channel.as_ref());
1102        let ud = Self::leak(handler);
1103        unsafe { ((*self.api).on_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1104    }
1105
1106    pub fn on_client_packet<F>(&mut self, channel: impl AsRef<str>, handler: F)
1107    where F: Fn(&PacketEvent, &dyn Server) + Send + Sync + 'static {
1108        let ch = YogStr::from_str(channel.as_ref());
1109        let ud = Self::leak(handler);
1110        unsafe { ((*self.api).on_client_packet)(self.ctx(), ch, ud, trampoline_packet::<F>) }
1111    }
1112
1113    /// Register a typed-packet handler.
1114    ///
1115    /// The payload is decoded from raw bytes using `P`'s [`Packet`] impl.
1116    /// Malformed payloads are silently dropped.
1117    pub fn on_typed_packet<P, F>(&mut self, channel: impl AsRef<str>, handler: F)
1118    where
1119        P: Packet + Send + Sync + 'static,
1120        F: Fn(&P, &dyn Server) + Send + Sync + 'static,
1121    {
1122        self.on_packet(channel, move |ev, srv| {
1123            if let Some(pkt) = P::decode(&ev.payload) {
1124                handler(&pkt, srv);
1125            }
1126        });
1127    }
1128
1129    // ── recipes ──────────────────────────────────────────────────────────────
1130
1131    fn recipe(&mut self, ns: &str, name: &str, json: &str) {
1132        unsafe {
1133            ((*self.api).register_recipe_json)(
1134                self.ctx(),
1135                YogStr::from_str(ns), YogStr::from_str(name), YogStr::from_str(json),
1136            )
1137        }
1138    }
1139
1140    /// Register a shaped crafting recipe.
1141    pub fn add_shaped_recipe(&mut self, recipe: ShapedRecipe) {
1142        let json = recipe.to_json();
1143        let (ns, name) = recipe.ns_name();
1144        self.recipe(ns, name, &json);
1145    }
1146
1147    /// Register a shapeless crafting recipe.
1148    pub fn add_shapeless_recipe(&mut self, recipe: ShapelessRecipe) {
1149        let json = recipe.to_json();
1150        let (ns, name) = recipe.ns_name();
1151        self.recipe(ns, name, &json);
1152    }
1153
1154    /// Register a furnace smelting recipe.
1155    pub fn add_furnace_recipe(&mut self, recipe: FurnaceRecipe) {
1156        let json = recipe.to_json();
1157        let (ns, name) = recipe.ns_name();
1158        self.recipe(ns, name, &json);
1159    }
1160
1161    // ── content ──────────────────────────────────────────────────────────────
1162
1163    pub fn register_item(&mut self, def: ItemDef) {
1164        // Build a C-compatible YogItemDef whose YogStr fields point into `def`'s
1165        // String storage.  We then call register_item which must copy the data
1166        // before returning (the runtime stores owned Strings).
1167        let food_nutrition  = def.food.as_ref().map_or(0, |f| f.nutrition);
1168        let food_saturation = def.food.as_ref().map_or(0.0, |f| f.saturation);
1169        let food_always_eat = def.food.as_ref().map_or(false, |f| f.can_always_eat);
1170        let c = YogItemDef {
1171            id:              YogStr::from_str(&def.id),
1172            max_stack:       def.max_stack as u32,
1173            name:            def.name.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1174            tooltip:         def.tooltip.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1175            max_damage:      def.max_damage,
1176            fire_resistant:  def.fire_resistant,
1177            fuel_ticks:      def.fuel_ticks,
1178            food_nutrition,
1179            food_saturation,
1180            food_always_eat,
1181        };
1182        unsafe { ((*self.api).register_item)(self.ctx(), &c) }
1183    }
1184
1185    pub fn register_block(&mut self, def: BlockDef) {
1186        let shape = def.shape.unwrap_or([0.0; 6]);
1187        let groups_joined = def.connect_groups.join(",");
1188        let c = YogBlockDef {
1189            id:            YogStr::from_str(&def.id),
1190            hardness:      def.hardness,
1191            resistance:    def.resistance,
1192            name:          def.name.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1193            light_level:   def.light_level,
1194            sound:         def.sound.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1195            requires_tool: def.requires_tool,
1196            no_collision:  def.no_collision,
1197            slipperiness:  def.slipperiness,
1198            shape,
1199            connects:      def.connects,
1200            connect_groups: YogStr::from_str(&groups_joined),
1201            inventory_id:  def.inventory_id.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1202        };
1203        unsafe { ((*self.api).register_block)(self.ctx(), &c) }
1204    }
1205
1206    /// Register a real Container/Menu inventory screen — see
1207    /// `yog_inventory::InventoryDef`. Attach it to a block via
1208    /// `BlockDef::inventory(id)`.
1209    pub fn register_inventory(&mut self, def: yog_inventory::InventoryDef) {
1210        let layout = yog_inventory::encode_layout(&def.resolved_layout());
1211        let c = YogInventoryDef {
1212            id:            YogStr::from_str(&def.id),
1213            slot_count:    def.slot_count as u32,
1214            layout:        YogStr::from_str(&layout),
1215            include_player_inventory: def.include_player_inventory,
1216            player_inv_x:  def.player_inv_offset.0,
1217            player_inv_y:  def.player_inv_offset.1,
1218            background_texture: def.background_texture.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1219            title:         YogStr::from_str(&def.title),
1220        };
1221        unsafe { ((*self.api).register_inventory)(self.ctx(), &c) }
1222    }
1223
1224    // ── startup grants ───────────────────────────────────────────────────────
1225
1226    /// Register a startup grant: items/books to give once when a player first joins.
1227    pub fn register_startup_grant(&mut self, grant: StartupGrant) {
1228        let items_str = grant.items.join("|");
1229        let c = YogStartupGrantDef {
1230            id:      YogStr::from_str(&grant.id),
1231            items:   YogStr::from_str(&items_str),
1232            book:    grant.book.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1233            command: grant.command.as_deref().map(YogStr::from_str).unwrap_or(YogStr::EMPTY),
1234        };
1235        unsafe { ((*self.api).register_startup_grant)(self.ctx(), &c) }
1236    }
1237
1238    /// Register a book with its JSON-serialized content.
1239    pub fn register_book(&mut self, book: &Book) {
1240        let json = book.to_json();
1241        let id = YogStr::from_str(&book.id);
1242        let j = YogStr::from_str(&json);
1243        unsafe { ((*self.api).register_book)(self.ctx(), id, j) }
1244    }
1245
1246
1247    /// Register a menu entry — the host renders a button on vanilla screens
1248    /// (TitleScreen on Fabric, ModListScreen on Forge/NeoForge) that opens `ui_id`.
1249    /// `label` is the human-readable button text (e.g. "Yog Mods").
1250    /// `ui_id` is the UI to open when clicked (e.g. "yog:modlist").
1251    /// See [`installed_mods`]. During `register()` the list only contains
1252    /// mods loaded before this one.
1253    pub fn installed_mods(&self) -> Vec<ModInfo> {
1254        installed_mods()
1255    }
1256
1257    /// Access the inter-mod communication layer.
1258    ///
1259    /// Allows exporting function pointers from this mod and importing
1260    /// function pointers from other mods.
1261    pub fn interop(&self) -> crate::Interop {
1262        crate::Interop::new(self.api)
1263    }
1264
1265    /// Raw `YogApi` pointer — for passing to interop-imported functions
1266    /// that register content through the C ABI table directly.
1267    pub fn raw_api(&self) -> *const yog_abi::YogApi {
1268        self.api
1269    }
1270
1271    pub fn register_menu_entry(&mut self, label: &str, ui_id: &str) {
1272        let l = YogStr::from_str(label);
1273        let u = YogStr::from_str(ui_id);
1274        unsafe { ((*self.api).register_menu_entry)(self.ctx(), l, u) }
1275    }
1276
1277    /// Register a UI tree with an event callback.
1278    /// `ui_id` is the unique identifier (e.g. "mymod:menu").
1279    /// `handler` receives `(ui_id, event_id)` when a widget is clicked.
1280    pub fn register_ui<F>(&mut self, ui_id: &str, handler: F)
1281    where F: Fn(&str, &str) + Send + Sync + 'static {
1282        let ud = Self::leak(handler);
1283        let id = YogStr::from_str(ui_id);
1284        let empty = YogStr::from_str("{}");
1285        unsafe {
1286            ((*self.api).register_ui)(self.ctx(), id, empty, ud, trampoline_ui_event::<F>)
1287        }
1288    }
1289
1290    // ── scheduler ────────────────────────────────────────────────────────────
1291
1292    pub fn schedule_once<F>(&self, delay_ticks: u64, handler: F)
1293    where F: Fn(&dyn Server) + Send + Sync + 'static {
1294        let ud = Self::leak(handler);
1295        unsafe { ((*self.api).schedule_once)(self.ctx(), delay_ticks, ud, trampoline_scheduled::<F>) }
1296    }
1297
1298    pub fn schedule_repeating<F>(&self, period_ticks: u64, handler: F)
1299    where F: Fn(&dyn Server) + Send + Sync + 'static {
1300        let ud = Self::leak(handler);
1301        unsafe { ((*self.api).schedule_repeating)(self.ctx(), period_ticks, ud, trampoline_scheduled::<F>) }
1302    }
1303
1304    // ── client-side hooks (ABI minor 10) ─────────────────────────────────────
1305
1306    /// Register a handler called every client tick (render thread, no server).
1307    pub fn on_client_tick<F>(&mut self, handler: F)
1308    where F: Fn(&ClientTickEvent) + Send + Sync + 'static {
1309        let ud = Self::leak(handler);
1310        unsafe { ((*self.api).on_client_tick)(self.ctx(), ud, trampoline_client_tick::<F>) }
1311    }
1312
1313    /// Register a render callback for a specific UI screen (`ui_id`).
1314    ///
1315    /// Called from `YogUIScreen.render()` — i.e. AFTER `renderBackground()` darkens
1316    /// the screen — so your UI draws on top of the dimmed world view.
1317    /// Use this instead of `on_hud_render` for book/inventory screens.
1318    ///
1319    /// Clicks are forwarded as `"click:X:Y"` via the `register_ui` handler so you
1320    /// can do hit-testing on your own stored layout.
1321    pub fn on_ui_render<F>(&mut self, ui_id: &str, handler: F)
1322    where F: Fn(&GfxContext) + Send + Sync + 'static {
1323        let ud = Self::leak(handler);
1324        let id = YogStr::from_str(ui_id);
1325        unsafe { ((*self.api).on_ui_render)(self.ctx(), id, ud, trampoline_hud_render::<F>) }
1326    }
1327
1328    /// Register a handler called every frame when the HUD is rendered.
1329    ///
1330    /// `gfx` provides full GPU pipeline access plus 2D convenience draw calls.
1331    /// `view_proj` and `camera_pos` are zero in HUD context; use `gfx.draw2d()` for HUD elements.
1332    pub fn on_hud_render<F>(&mut self, handler: F)
1333    where F: Fn(&GfxContext) + Send + Sync + 'static {
1334        let ud = Self::leak(handler);
1335        unsafe { ((*self.api).on_hud_render)(self.ctx(), ud, trampoline_hud_render::<F>) }
1336    }
1337
1338    /// Register a handler called every frame at the end of world rendering.
1339    ///
1340    /// `gfx.view_proj()` is the combined projection × view matrix (camera-relative).
1341    /// `gfx.camera_pos()` is the camera world position.
1342    /// To render at world position P, translate by `P - camera_pos` before drawing.
1343    pub fn on_world_render<F>(&mut self, handler: F)
1344    where F: Fn(&GfxContext) + Send + Sync + 'static {
1345        let ud = Self::leak(handler);
1346        unsafe { ((*self.api).on_world_render)(self.ctx(), ud, trampoline_world_render::<F>) }
1347    }
1348
1349    /// Register a handler for keyboard input (client-side).
1350    /// Return `false` to prevent Minecraft from processing the key.
1351    pub fn on_key_press<F>(&mut self, handler: F)
1352    where F: Fn(&KeyPressEvent) -> bool + Send + Sync + 'static {
1353        let ud = Self::leak(handler);
1354        unsafe { ((*self.api).on_key_press)(self.ctx(), ud, trampoline_key_press::<F>) }
1355    }
1356
1357    /// Register a handler called when a GUI screen is opened.
1358    pub fn on_screen_open<F>(&mut self, handler: F)
1359    where F: Fn(&ScreenEvent) + Send + Sync + 'static {
1360        let ud = Self::leak(handler);
1361        unsafe { ((*self.api).on_screen_open)(self.ctx(), ud, trampoline_screen::<F>) }
1362    }
1363
1364    /// Register a handler called when a GUI screen is closed.
1365    pub fn on_screen_close<F>(&mut self, handler: F)
1366    where F: Fn(&ScreenEvent) + Send + Sync + 'static {
1367        let ud = Self::leak(handler);
1368        unsafe { ((*self.api).on_screen_close)(self.ctx(), ud, trampoline_screen::<F>) }
1369    }
1370}
1371
1372unsafe extern "C" fn trampoline_ui_event<F>(ud: *mut c_void, ui_id: yog_abi::YogStr, event_id: yog_abi::YogStr)
1373where F: Fn(&str, &str) + Send + Sync + 'static
1374{
1375    let f = &*(ud as *const F);
1376    let ui = unsafe { ui_id.as_str() };
1377    let ev = unsafe { event_id.as_str() };
1378    f(ui, ev);
1379}
1380
1381// ── Mod trait ─────────────────────────────────────────────────────────────────
1382
1383/// Implemented by every Yog mod. Called once at startup to register handlers.
1384pub trait Mod {
1385    fn register(registry: &mut Registry);
1386}