Skip to main content

yog_runtime/
lib.rs

1//! Yog runtime — the native library loaded by the Fabric host.
2//!
3//! Exposes JNI entry points (`Java_dev_yog_NativeBridge_*`) that the host calls,
4//! and a stable C ABI (`YogApi` / `YogServer`) that mods program against.
5//!
6//! Architecture:
7//!   - `YogServer`  — a `#[repr(C)]` table of standalone JNI-calling functions
8//!     that mods call to mutate the world.
9//!   - `YogApi`     — a `#[repr(C)]` table of registration functions; mods call
10//!     them inside `yog_mod_register` to subscribe to events.
11//!   - `RuntimeHandlers` — the runtime's internal event/handler storage.
12//!     Filled during `nativeInit` (write), read-only after. The scheduler sub-
13//!     state uses an inner `Mutex` for safe addition during event dispatch.
14
15use std::collections::HashMap;
16use std::num::NonZeroU32;
17use std::os::raw::c_void;
18use std::path::{Path, PathBuf};
19use std::sync::{Mutex, OnceLock, RwLock};
20
21use glow::HasContext;
22use jcall::JArg;
23use jni::objects::{JByteArray, JClass, JFloatArray, JString};
24use jni::sys::{jdouble, jfloat, jint, jstring};
25use jni::{JNIEnv, JavaVM};
26use libloading::Symbol;
27use yog_hot_reload::{HotReloader, LoadedModule, ModuleGeneration, ModuleRegistry};
28
29use yog_abi::{
30    ABI_VERSION, YogAdvancementEvent, YogAdvancementFn, YogApi, YogAttackEntityFn, YogBlockBreakFn,
31    YogBlockDef, YogBlockPos, YogChatFn, YogClientFn, YogCommandFn, YogContainerCloseEvent,
32    YogContainerCloseFn, YogContainerOpenEvent, YogContainerOpenFn, YogCraftEvent, YogCraftFn,
33    YogEntityDamageFn, YogEntityDeathFn, YogEntityInteractEvent, YogEntityInteractFn,
34    YogEntitySpawnFn, YogExplosionEvent, YogExplosionFn, YogGfxApi, YogHudRenderFn, YogItemDef,
35    YogItemPickupEvent, YogItemPickupFn, YogKeyPressEvent, YogKeyPressFn, YogOwnedStr, YogPacketFn,
36    YogPlaceBlockEvent, YogPlaceBlockFn, YogPlayerDeathEvent, YogPlayerDeathFn, YogPlayerFn,
37    YogPlayerMoveEvent, YogPlayerMoveFn, YogPlayerRespawnEvent, YogPlayerRespawnFn,
38    YogProjectileHitEvent, YogProjectileHitFn, YogScheduledFn, YogScreenFn, YogServer, YogServerFn,
39    YogStartupGrantDef, YogStr, YogUseBlockFn, YogUseItemFn, YogVec3, YogWorldRenderFn,
40};
41use yog_registry::{BlockDef, FoodDef, ItemDef};
42
43// ── Static globals ────────────────────────────────────────────────────────────
44
45/// Cached JVM handle for any-thread callbacks.
46static JAVA_VM: OnceLock<JavaVM> = OnceLock::new();
47/// Loaded mod libraries, keyed by mod id — owns the safe `dlopen`/`dlclose`
48/// swap lifecycle (see `yog-hot-reload`) instead of a flat keep-alive `Vec`,
49/// so a reload can find "the current generation of this mod" and safely
50/// retire the previous one.
51static HOT_RELOADER: OnceLock<HotReloader<RuntimeModuleRegistry>> = OnceLock::new();
52/// The mod currently inside its own `yog_mod_register` call. Every `api_*`
53/// registration function reads this to tag what it stores with a
54/// `LoadedModule` handle, instead of requiring mods to pass a mod id
55/// through the stable C ABI (an ABI break, and edits to every mod's
56/// source). Valid because a mod's entire registration always happens
57/// synchronously inside one `yog_mod_register` call — see `load_mod_lib`
58/// and `Java_dev_yog_NativeBridge_nativeHotReload`.
59static CURRENT_REGISTRATION: Mutex<Option<LoadedModule>> = Mutex::new(None);
60/// Metadata of loaded .yog mods: [id, name, version, authors, description,
61/// name_key, description_key] (the keys empty unless the mod's yog.toml
62/// gives its name/description as `.yres` texts).
63static MOD_INFOS: Mutex<Vec<[String; 7]>> = Mutex::new(Vec::new());
64/// Inter-mod symbol registry: mod_id → (symbol_name → function pointer).
65/// Filled during `yog_mod_register` (export calls), read during import calls.
66static INTEROP_SYMBOLS: OnceLock<Mutex<HashMap<String, HashMap<String, usize>>>> = OnceLock::new();
67/// Stable server table (populated once in nativeInit, then read-only).
68static SERVER: OnceLock<YogServer> = OnceLock::new();
69/// Stable api table. Mods capture this pointer at registration (yog-api's
70/// `installed_mods()` calls through it later, e.g. from UI render) — so it
71/// must outlive `nativeInit`, not live on its stack. `ctx` inside points at
72/// `HANDLERS`' own `RwLock`, published *before* any mod registers (unlike
73/// the old design, where it pointed at a temporary pre-move `Box` that
74/// went stale the instant `nativeInit` finished) — so it stays valid for a
75/// hot-reload's later re-registration too.
76static API_TABLE: OnceLock<YogApi> = OnceLock::new();
77/// All registered handlers + content. A `RwLock`, not a plain value: unlike
78/// the original one-shot-then-immutable design, a hot-reload needs to
79/// append fresh entries for a reloaded mod after this has long been
80/// published. Read sites (`handlers()`) take a read guard; registration
81/// (initial or reload) takes a write guard — both deref straight through to
82/// `RuntimeHandlers`, so existing `handlers().field` call sites are
83/// unaffected by this type change.
84static HANDLERS: OnceLock<RwLock<RuntimeHandlers>> = OnceLock::new();
85
86// ── OpenGL context (client-side, render thread only) ─────────────────────────
87
88struct GlCtx(glow::Context);
89unsafe impl Send for GlCtx {}
90unsafe impl Sync for GlCtx {}
91
92/// Initialized by `nativeGlInit` on the render thread.  `None` on dedicated server.
93static GL: OnceLock<GlCtx> = OnceLock::new();
94
95// Raw GL function pointers for GL_ARB_get_program_binary (not exposed by glow 0.13).
96// Captured during the glow loader callback in `nativeGlInit`.
97// `None` when the extension is unavailable (very old drivers).
98static GL_GET_PROGRAM_BINARY: OnceLock<Option<usize>> = OnceLock::new();
99static GL_PROGRAM_BINARY: OnceLock<Option<usize>> = OnceLock::new();
100static GL_GET_PROGRAM_IV: OnceLock<Option<usize>> = OnceLock::new();
101
102// ── Handler storage ───────────────────────────────────────────────────────────
103
104#[derive(Debug, Clone)]
105pub struct UiLayer {
106    pub id: String,
107    pub parent: Option<String>,
108    pub modal: bool,
109    pub pause_game: bool,
110    pub visible: bool,
111    pub enabled: bool,
112    pub z_index: i32,
113}
114
115/// One command registration: owning module, typed-args schema (if any),
116/// user data and callback.
117type CommandEntry = (LoadedModule, Option<String>, *mut c_void, YogCommandFn);
118
119// Every callback table below is tagged with the `LoadedModule` that
120// registered it (see `current_registration()`), so a hot-reload's dispatch
121// sites can skip a retiring module's stale entries (`LoadedModule::
122// is_retiring()`) before they're physically swept by `RuntimeModuleRegistry
123// ::purge`. Content registries — `items`/`blocks`/`inventories`/`books`/
124// `recipes`/`dimensions`/`startup_grants`/`menu_entries` — are deliberately
125// NOT tagged: they're one-time definitions copied into Minecraft's own
126// registries at startup, not callbacks invoked later, and Minecraft itself
127// has no live registry-unregister mechanism to hot-reload them against
128// regardless of what this crate does.
129#[derive(Default)]
130struct RuntimeHandlers {
131    block_break: Vec<(LoadedModule, *mut c_void, YogBlockBreakFn)>,
132    chat: Vec<(LoadedModule, *mut c_void, YogChatFn)>,
133    player_join: Vec<(LoadedModule, *mut c_void, YogPlayerFn)>,
134    player_leave: Vec<(LoadedModule, *mut c_void, YogPlayerFn)>,
135    use_item: Vec<(LoadedModule, *mut c_void, YogUseItemFn)>,
136    use_block: Vec<(LoadedModule, *mut c_void, YogUseBlockFn)>,
137    attack_entity: Vec<(LoadedModule, *mut c_void, YogAttackEntityFn)>,
138    entity_damage: Vec<(LoadedModule, *mut c_void, YogEntityDamageFn)>,
139    entity_death: Vec<(LoadedModule, *mut c_void, YogEntityDeathFn)>,
140    entity_spawn: Vec<(LoadedModule, *mut c_void, YogEntitySpawnFn)>,
141    player_place_block: Vec<(LoadedModule, *mut c_void, YogPlaceBlockFn)>,
142    player_death: Vec<(LoadedModule, *mut c_void, YogPlayerDeathFn)>,
143    player_respawn: Vec<(LoadedModule, *mut c_void, YogPlayerRespawnFn)>,
144    advancement: Vec<(LoadedModule, *mut c_void, YogAdvancementFn)>,
145    entity_interact: Vec<(LoadedModule, *mut c_void, YogEntityInteractFn)>,
146    item_craft: Vec<(LoadedModule, *mut c_void, YogCraftFn)>,
147    explosion: Vec<(LoadedModule, *mut c_void, YogExplosionFn)>,
148    item_pickup: Vec<(LoadedModule, *mut c_void, YogItemPickupFn)>,
149    player_move: Vec<(LoadedModule, *mut c_void, YogPlayerMoveFn)>,
150    container_open: Vec<(LoadedModule, *mut c_void, YogContainerOpenFn)>,
151    container_close: Vec<(LoadedModule, *mut c_void, YogContainerCloseFn)>,
152    projectile_hit: Vec<(LoadedModule, *mut c_void, YogProjectileHitFn)>,
153    client_tick: Vec<(LoadedModule, *mut c_void, YogClientFn)>,
154    hud_render: Vec<(LoadedModule, *mut c_void, YogHudRenderFn)>,
155    world_render: Vec<(LoadedModule, *mut c_void, YogWorldRenderFn)>,
156    key_press: Vec<(LoadedModule, *mut c_void, YogKeyPressFn)>,
157    screen_open: Vec<(LoadedModule, *mut c_void, YogScreenFn)>,
158    screen_close: Vec<(LoadedModule, *mut c_void, YogScreenFn)>,
159    server_tick: Vec<(LoadedModule, *mut c_void, YogServerFn)>,
160    server_started: Vec<(LoadedModule, *mut c_void, YogServerFn)>,
161    server_stopping: Vec<(LoadedModule, *mut c_void, YogServerFn)>,
162    /// All handlers registered under one command name, in registration order.
163    /// `schema` is `None` for a plain `on_command` (bare, zero-argument)
164    /// registration and `Some(schema)` for `on_typed_command`. Kept as a
165    /// `Vec` (not a single slot) so a mod can register several subcommands —
166    /// e.g. differing arg counts, or several bare/typed registrations sharing
167    /// one top-level name — without later registrations silently clobbering
168    /// earlier ones (see `nativeOnCommand`, which tries each entry whose
169    /// schema's arg count matches the actual invocation, in order, until one
170    /// produces a reply).
171    commands: HashMap<String, Vec<CommandEntry>>,
172    recipes: Vec<(String, String, String)>,
173    packets: HashMap<String, (LoadedModule, *mut c_void, YogPacketFn)>,
174    client_packets: HashMap<String, (LoadedModule, *mut c_void, YogPacketFn)>,
175    items: Vec<ItemDef>,
176    blocks: Vec<BlockDef>,
177    inventories: Vec<yog_inventory::InventoryDef>,
178    books: HashMap<String, String>,
179    book_renderers: Mutex<HashMap<String, yog_book::BookRenderer>>,
180    /// Declared dimension-type defs: id -> `YogDimensionDef` JSON. Actual
181    /// `LevelStem`/`ServerLevel` creation is a host (Java) responsibility —
182    /// see `yog-dimensions` crate docs.
183    dimensions: HashMap<String, String>,
184    chunk_generators: HashMap<String, (LoadedModule, *mut c_void, yog_abi::YogChunkGeneratorFn)>,
185    ui_handlers: HashMap<String, (LoadedModule, *mut c_void, yog_abi::YogUIEventFn)>,
186    ui_render_handlers: HashMap<String, Vec<(LoadedModule, *mut c_void, YogHudRenderFn)>>,
187    menu_entries: Vec<(String, String)>, // (label, ui_id) for vanilla screen buttons
188    pub active_uis: Mutex<Vec<UiLayer>>,
189    startup_grants: Vec<yog_registry::StartupGrant>,
190    /// Players whose startup grants are pending: (name, uuid, ticks_waited).
191    /// Granting happens on a server tick after join — giving items directly in
192    /// the join callback fails for the host of a freshly created world.
193    pending_grants: Mutex<Vec<(String, String, u32)>>,
194    scheduler: Mutex<SchedulerState>,
195}
196
197// All fn ptrs are C-ABI; ud pointers are from Box::into_raw of Send+Sync closures.
198unsafe impl Send for RuntimeHandlers {}
199unsafe impl Sync for RuntimeHandlers {}
200
201#[derive(Default)]
202struct SchedulerState {
203    once_tasks: Vec<OnceTask>,
204    repeating_tasks: Vec<RepeatingTask>,
205}
206
207struct OnceTask {
208    module: LoadedModule,
209    delay_remaining: u64,
210    ud: *mut c_void,
211    f: YogScheduledFn,
212}
213struct RepeatingTask {
214    module: LoadedModule,
215    period: u64,
216    ticks_left: u64,
217    ud: *mut c_void,
218    f: YogScheduledFn,
219}
220
221unsafe impl Send for SchedulerState {}
222unsafe impl Sync for SchedulerState {}
223unsafe impl Send for OnceTask {}
224unsafe impl Send for RepeatingTask {}
225
226/// A mod callback, tagged with the module that registered it.
227type Entry<F> = (LoadedModule, *mut c_void, F);
228
229/// Calls `call(ud, f)` for every entry whose module is still live, until
230/// one returns `false`; whether none did.
231fn each_while<F: Copy>(entries: &[Entry<F>], mut call: impl FnMut(*mut c_void, F) -> bool) -> bool {
232    for (module, ud, f) in entries {
233        if module.is_retiring() {
234            continue;
235        }
236        let Some(_call) = module.enter() else {
237            continue;
238        };
239        if !call(*ud, *f) {
240            return false;
241        }
242    }
243    true
244}
245
246/// Calls `call(ud, f)` for every entry whose module is still live (a
247/// post-phase handler's result is ignored).
248fn each<F: Copy, R>(entries: &[Entry<F>], mut call: impl FnMut(*mut c_void, F) -> R) {
249    each_while(entries, |ud, f| {
250        call(ud, f);
251        true
252    });
253}
254
255/// Pre-phase dispatch: whether every live handler allows the action. The
256/// first `false` cancels it and skips the rest; a panicking mod allows it.
257fn allowed<F: Copy>(entries: &[Entry<F>], call: impl FnMut(*mut c_void, F) -> bool) -> bool {
258    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| each_while(entries, call)))
259        .unwrap_or(true)
260}
261
262fn handlers() -> std::sync::RwLockReadGuard<'static, RuntimeHandlers> {
263    HANDLERS
264        .get()
265        .expect("yog: nativeInit not called yet")
266        .read()
267        .expect("handlers lock poisoned")
268}
269
270fn current_registration() -> LoadedModule {
271    CURRENT_REGISTRATION
272        .lock()
273        .unwrap()
274        .clone()
275        .expect("a yog-runtime api_* function was called outside of yog_mod_register")
276}
277
278fn hot_reloader() -> &'static HotReloader<RuntimeModuleRegistry> {
279    HOT_RELOADER.get().expect("yog: nativeInit not called yet")
280}
281
282/// The `yog-hot-reload` host contract: `retarget` needs no work here because
283/// `LoadedModule::is_retiring()` already makes a superseded generation's
284/// dispatch-table entries inert (every `handlers().<field>` loop checks it)
285/// the instant `HotReloader::reload` calls `retire` on the old module —
286/// well before this is even invoked. `purge` is the actual safety-net
287/// sweep, dropping every entry tagged with a retired generation right
288/// before its library is hard-unloaded.
289/// Runs `$body` with `$list` bound to each per-event handler list of `$h`.
290macro_rules! for_each_handler_list {
291    ($h:expr_2021, |$list:ident| $body:expr_2021) => {{
292        {
293            let $list = &mut $h.block_break;
294            $body;
295        }
296        {
297            let $list = &mut $h.chat;
298            $body;
299        }
300        {
301            let $list = &mut $h.player_join;
302            $body;
303        }
304        {
305            let $list = &mut $h.player_leave;
306            $body;
307        }
308        {
309            let $list = &mut $h.use_item;
310            $body;
311        }
312        {
313            let $list = &mut $h.use_block;
314            $body;
315        }
316        {
317            let $list = &mut $h.attack_entity;
318            $body;
319        }
320        {
321            let $list = &mut $h.entity_damage;
322            $body;
323        }
324        {
325            let $list = &mut $h.entity_death;
326            $body;
327        }
328        {
329            let $list = &mut $h.entity_spawn;
330            $body;
331        }
332        {
333            let $list = &mut $h.player_place_block;
334            $body;
335        }
336        {
337            let $list = &mut $h.player_death;
338            $body;
339        }
340        {
341            let $list = &mut $h.player_respawn;
342            $body;
343        }
344        {
345            let $list = &mut $h.advancement;
346            $body;
347        }
348        {
349            let $list = &mut $h.entity_interact;
350            $body;
351        }
352        {
353            let $list = &mut $h.item_craft;
354            $body;
355        }
356        {
357            let $list = &mut $h.explosion;
358            $body;
359        }
360        {
361            let $list = &mut $h.item_pickup;
362            $body;
363        }
364        {
365            let $list = &mut $h.player_move;
366            $body;
367        }
368        {
369            let $list = &mut $h.container_open;
370            $body;
371        }
372        {
373            let $list = &mut $h.container_close;
374            $body;
375        }
376        {
377            let $list = &mut $h.projectile_hit;
378            $body;
379        }
380        {
381            let $list = &mut $h.client_tick;
382            $body;
383        }
384        {
385            let $list = &mut $h.hud_render;
386            $body;
387        }
388        {
389            let $list = &mut $h.world_render;
390            $body;
391        }
392        {
393            let $list = &mut $h.key_press;
394            $body;
395        }
396        {
397            let $list = &mut $h.screen_open;
398            $body;
399        }
400        {
401            let $list = &mut $h.screen_close;
402            $body;
403        }
404        {
405            let $list = &mut $h.server_tick;
406            $body;
407        }
408        {
409            let $list = &mut $h.server_started;
410            $body;
411        }
412        {
413            let $list = &mut $h.server_stopping;
414            $body;
415        }
416    }};
417}
418
419struct RuntimeModuleRegistry;
420
421impl ModuleRegistry for RuntimeModuleRegistry {
422    fn retarget(&self, _mod_id: &str, _from: ModuleGeneration, _to: ModuleGeneration) {}
423
424    fn purge(&self, generation: ModuleGeneration) {
425        let Some(lock) = HANDLERS.get() else { return };
426        let mut h = lock.write().expect("handlers lock poisoned");
427        let h = &mut *h;
428        for_each_handler_list!(h, |list| list
429            .retain(|(m, ..)| m.generation() != generation));
430        for entries in h.commands.values_mut() {
431            entries.retain(|(m, ..)| m.generation() != generation);
432        }
433        h.packets.retain(|_, (m, ..)| m.generation() != generation);
434        h.client_packets
435            .retain(|_, (m, ..)| m.generation() != generation);
436        h.chunk_generators
437            .retain(|_, (m, ..)| m.generation() != generation);
438        h.ui_handlers
439            .retain(|_, (m, ..)| m.generation() != generation);
440        for entries in h.ui_render_handlers.values_mut() {
441            entries.retain(|(m, ..)| m.generation() != generation);
442        }
443        h.scheduler
444            .lock()
445            .expect("scheduler poisoned")
446            .once_tasks
447            .retain(|t| t.module.generation() != generation);
448        h.scheduler
449            .lock()
450            .expect("scheduler poisoned")
451            .repeating_tasks
452            .retain(|t| t.module.generation() != generation);
453    }
454}
455
456// ── JNI helpers ──────────────────────────────────────────────────────────────
457
458fn guard(label: &str, f: impl FnOnce()) {
459    if std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).is_err() {
460        yog_logging::error!("a mod panicked handling `{}` (ignored)", label);
461    }
462}
463
464/// Reads a Java string, returning `$or` (default: `Default::default()`)
465/// from the enclosing function when it can't be read.
466macro_rules! jstr {
467    ($env:expr_2021, $s:expr_2021) => {
468        jstr!($env, $s, Default::default())
469    };
470    ($env:expr_2021, $s:expr_2021, $or:expr_2021) => {
471        match $env.get_string(&$s) {
472            Ok(s) => String::from(s),
473            Err(_) => return $or,
474        }
475    };
476}
477
478fn get_env() -> Option<jni::AttachGuard<'static>> {
479    JAVA_VM.get()?.attach_current_thread().ok()
480}
481
482/// `<class>.<name>(args...)` on this thread's env, `Option<R>` — see
483/// [`jcall!`]; `None` without a JVM too.
484macro_rules! host {
485    ($class:literal, $name:literal, [$($arg:expr_2021),* $(,)?]) => {
486        match $crate::get_env() {
487            Some(mut env) => $crate::jcall::jcall!(&mut env, $class, $name, [$($arg),*]),
488            None => None,
489        }
490    };
491}
492
493/// `NativeBridge.<name>(args...)` — see [`host!`].
494macro_rules! bridge {
495    ($name:literal, [$($arg:expr_2021),* $(,)?]) => {
496        host!("dev/yog/NativeBridge", $name, [$($arg),*])
497    };
498}
499
500/// A host string reply handed to a mod (`None`/`null` → [`YogOwnedStr::NONE`]).
501fn owned(s: Option<String>) -> YogOwnedStr {
502    s.map_or(YogOwnedStr::NONE, YogOwnedStr::from_string)
503}
504
505/// The first `N` tab-separated values of a host reply.
506fn parse_tsv<T: std::str::FromStr, const N: usize>(s: &str) -> Option<[T; N]> {
507    let values: Vec<T> = s
508        .split('\t')
509        .take(N)
510        .map(|v| v.parse().ok())
511        .collect::<Option<_>>()?;
512    values.try_into().ok()
513}
514
515// ── Free-str allocator used by YogOwnedStr ────────────────────────────────────
516
517unsafe extern "C" fn yog_free_str(ptr: *mut u8, len: u32) {
518    unsafe {
519        if !ptr.is_null() {
520            drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(
521                ptr,
522                len as usize,
523            )));
524        }
525    }
526}
527
528// ── YogServer standalone functions (one per action) ───────────────────────────
529//
530// ctx is unused here — all state is in the JAVA_VM static.
531
532unsafe extern "C" fn srv_broadcast(_ctx: *mut c_void, msg: YogStr) {
533    unsafe {
534        let _: Option<()> = bridge!("broadcast", [S(msg)]);
535    }
536}
537
538unsafe extern "C" fn srv_get_block(
539    _ctx: *mut c_void,
540    dim: YogStr,
541    pos: YogBlockPos,
542) -> YogOwnedStr {
543    unsafe { owned(bridge!("getBlock", [S(dim), I(pos.x), I(pos.y), I(pos.z)])) }
544}
545
546unsafe extern "C" fn srv_set_block(
547    _ctx: *mut c_void,
548    dim: YogStr,
549    pos: YogBlockPos,
550    block: YogStr,
551) -> bool {
552    unsafe {
553        bridge!("setBlock", [S(dim), I(pos.x), I(pos.y), I(pos.z), S(block)]).unwrap_or(false)
554    }
555}
556
557unsafe extern "C" fn srv_world_time(_ctx: *mut c_void, dim: YogStr, out: *mut i64) -> bool {
558    unsafe {
559        match bridge!("worldTime", [S(dim)]) {
560            Some(v) if v != i64::MIN => {
561                *out = v;
562                true
563            }
564            _ => false,
565        }
566    }
567}
568
569unsafe extern "C" fn srv_set_time(_ctx: *mut c_void, dim: YogStr, time: i64) -> bool {
570    unsafe { bridge!("worldSetTime", [S(dim), J(time)]).unwrap_or(false) }
571}
572
573unsafe extern "C" fn srv_is_raining(_ctx: *mut c_void, dim: YogStr) -> bool {
574    unsafe { bridge!("worldIsRaining", [S(dim)]).unwrap_or(false) }
575}
576
577unsafe extern "C" fn srv_set_weather(
578    _ctx: *mut c_void,
579    dim: YogStr,
580    raining: bool,
581    dur: i32,
582) -> bool {
583    unsafe { bridge!("worldSetWeather", [S(dim), Z(raining), I(dur)]).unwrap_or(false) }
584}
585
586unsafe extern "C" fn srv_give_item(
587    _ctx: *mut c_void,
588    player: YogStr,
589    item: YogStr,
590    count: u32,
591) -> bool {
592    unsafe { bridge!("giveItem", [S(player), S(item), I(count as i32)]).unwrap_or(false) }
593}
594
595unsafe extern "C" fn srv_player_teleport(_ctx: *mut c_void, player: YogStr, pos: YogVec3) -> bool {
596    unsafe { bridge!("teleport", [S(player), D(pos.x), D(pos.y), D(pos.z)]).unwrap_or(false) }
597}
598
599unsafe extern "C" fn srv_send_to_player(
600    _ctx: *mut c_void,
601    player: YogStr,
602    channel: YogStr,
603    data: *const u8,
604    len: u32,
605) -> bool {
606    unsafe {
607        bridge!(
608            "sendToPlayer",
609            [
610                S(player),
611                S(channel),
612                Bytes(std::slice::from_raw_parts(data, len as usize))
613            ]
614        )
615        .unwrap_or(false)
616    }
617}
618
619unsafe extern "C" fn srv_send_to_server(
620    _ctx: *mut c_void,
621    channel: YogStr,
622    data: *const u8,
623    len: u32,
624) -> bool {
625    unsafe {
626        host!(
627            "dev/yog/YogClient",
628            "sendToServer",
629            [
630                S(channel),
631                Bytes(std::slice::from_raw_parts(data, len as usize))
632            ]
633        )
634        .unwrap_or(false)
635    }
636}
637
638unsafe extern "C" fn srv_kick_player(_ctx: *mut c_void, player: YogStr, reason: YogStr) -> bool {
639    unsafe { bridge!("kickPlayer", [S(player), S(reason)]).unwrap_or(false) }
640}
641
642unsafe extern "C" fn srv_set_gamemode(_ctx: *mut c_void, player: YogStr, mode: YogStr) -> bool {
643    unsafe { bridge!("setGamemode", [S(player), S(mode)]).unwrap_or(false) }
644}
645
646unsafe extern "C" fn srv_send_title(
647    _ctx: *mut c_void,
648    player: YogStr,
649    title: YogStr,
650    sub: YogStr,
651    fi: i32,
652    stay: i32,
653    fo: i32,
654) -> bool {
655    unsafe {
656        bridge!(
657            "sendTitle",
658            [S(player), S(title), S(sub), I(fi), I(stay), I(fo)]
659        )
660        .unwrap_or(false)
661    }
662}
663
664unsafe extern "C" fn srv_send_actionbar(_ctx: *mut c_void, player: YogStr, msg: YogStr) -> bool {
665    unsafe { bridge!("sendActionbar", [S(player), S(msg)]).unwrap_or(false) }
666}
667
668unsafe extern "C" fn srv_play_sound(
669    _ctx: *mut c_void,
670    dim: YogStr,
671    pos: YogVec3,
672    sound: YogStr,
673    vol: f32,
674    pitch: f32,
675) -> bool {
676    unsafe {
677        bridge!(
678            "playSound",
679            [
680                S(dim),
681                D(pos.x),
682                D(pos.y),
683                D(pos.z),
684                S(sound),
685                F(vol),
686                F(pitch)
687            ]
688        )
689        .unwrap_or(false)
690    }
691}
692
693unsafe extern "C" fn srv_play_sound_player(
694    _ctx: *mut c_void,
695    player: YogStr,
696    sound: YogStr,
697    vol: f32,
698    pitch: f32,
699) -> bool {
700    unsafe {
701        bridge!("playSoundToPlayer", [S(player), S(sound), F(vol), F(pitch)]).unwrap_or(false)
702    }
703}
704
705unsafe extern "C" fn srv_entity_teleport(_ctx: *mut c_void, uuid: YogStr, pos: YogVec3) -> bool {
706    unsafe { bridge!("entityTeleport", [S(uuid), D(pos.x), D(pos.y), D(pos.z)]).unwrap_or(false) }
707}
708
709unsafe extern "C" fn srv_entity_rotation(
710    _ctx: *mut c_void,
711    uuid: YogStr,
712    out: *mut YogVec3,
713) -> bool {
714    unsafe {
715        let Some(s): Option<String> = bridge!("entityRotation", [S(uuid)]) else {
716            return false;
717        };
718        match parse_tsv::<f64, 2>(&s) {
719            Some([yaw, pitch]) => {
720                *out = YogVec3 {
721                    x: yaw,
722                    y: pitch,
723                    z: 0.0,
724                };
725                true
726            }
727            None => false,
728        }
729    }
730}
731
732unsafe extern "C" fn srv_entity_position(
733    _ctx: *mut c_void,
734    uuid: YogStr,
735    out: *mut YogVec3,
736) -> bool {
737    unsafe {
738        let Some(s): Option<String> = bridge!("entityPosition", [S(uuid)]) else {
739            return false;
740        };
741        match parse_tsv::<f64, 3>(&s) {
742            Some([x, y, z]) => {
743                *out = YogVec3 { x, y, z };
744                true
745            }
746            None => false,
747        }
748    }
749}
750
751unsafe extern "C" fn srv_entity_health(_ctx: *mut c_void, uuid: YogStr, out: *mut f32) -> bool {
752    unsafe {
753        match bridge!("entityHealth", [S(uuid)]) {
754            Some(v) if !f64::is_nan(v) => {
755                *out = v as f32;
756                true
757            }
758            _ => false,
759        }
760    }
761}
762
763unsafe extern "C" fn srv_entity_set_health(_ctx: *mut c_void, uuid: YogStr, hp: f32) -> bool {
764    unsafe { bridge!("entitySetHealth", [S(uuid), D(hp as f64)]).unwrap_or(false) }
765}
766
767unsafe extern "C" fn srv_entity_kill(_ctx: *mut c_void, uuid: YogStr) -> bool {
768    unsafe { bridge!("entityKill", [S(uuid)]).unwrap_or(false) }
769}
770
771unsafe extern "C" fn srv_spawn_entity(
772    _ctx: *mut c_void,
773    type_id: YogStr,
774    dim: YogStr,
775    pos: YogVec3,
776) -> YogOwnedStr {
777    unsafe {
778        owned(bridge!(
779            "spawnEntity",
780            [S(type_id), S(dim), D(pos.x), D(pos.y), D(pos.z)]
781        ))
782    }
783}
784
785unsafe extern "C" fn srv_entity_add_effect(
786    _ctx: *mut c_void,
787    uuid: YogStr,
788    fx: YogStr,
789    dur: i32,
790    amp: u8,
791    particles: bool,
792) -> bool {
793    unsafe {
794        bridge!(
795            "entityAddEffect",
796            [S(uuid), S(fx), I(dur), I(amp as i32), Z(particles)]
797        )
798        .unwrap_or(false)
799    }
800}
801
802unsafe extern "C" fn srv_entity_remove_effect(_ctx: *mut c_void, uuid: YogStr, fx: YogStr) -> bool {
803    unsafe { bridge!("entityRemoveEffect", [S(uuid), S(fx)]).unwrap_or(false) }
804}
805
806unsafe extern "C" fn srv_entity_clear_effects(_ctx: *mut c_void, uuid: YogStr) -> bool {
807    unsafe { bridge!("entityClearEffects", [S(uuid)]).unwrap_or(false) }
808}
809
810unsafe extern "C" fn srv_entity_velocity(
811    _ctx: *mut c_void,
812    uuid: YogStr,
813    out: *mut YogVec3,
814) -> bool {
815    unsafe {
816        let Some(s): Option<String> = bridge!("entityVelocity", [S(uuid)]) else {
817            return false;
818        };
819        match parse_tsv::<f64, 3>(&s) {
820            Some([x, y, z]) => {
821                *out = YogVec3 { x, y, z };
822                true
823            }
824            None => false,
825        }
826    }
827}
828
829unsafe extern "C" fn srv_entity_set_velocity(
830    _ctx: *mut c_void,
831    uuid: YogStr,
832    vel: YogVec3,
833) -> bool {
834    unsafe {
835        bridge!("entitySetVelocity", [S(uuid), D(vel.x), D(vel.y), D(vel.z)]).unwrap_or(false)
836    }
837}
838
839unsafe extern "C" fn srv_entity_add_velocity(
840    _ctx: *mut c_void,
841    uuid: YogStr,
842    vel: YogVec3,
843) -> bool {
844    unsafe {
845        bridge!("entityAddVelocity", [S(uuid), D(vel.x), D(vel.y), D(vel.z)]).unwrap_or(false)
846    }
847}
848
849unsafe extern "C" fn srv_has_item_tag(_ctx: *mut c_void, item: YogStr, tag: YogStr) -> bool {
850    unsafe { bridge!("hasItemTag", [S(item), S(tag)]).unwrap_or(false) }
851}
852
853unsafe extern "C" fn srv_has_block_tag(_ctx: *mut c_void, block: YogStr, tag: YogStr) -> bool {
854    unsafe { bridge!("hasBlockTag", [S(block), S(tag)]).unwrap_or(false) }
855}
856
857unsafe extern "C" fn srv_drop_loot(
858    _ctx: *mut c_void,
859    table: YogStr,
860    dim: YogStr,
861    pos: YogVec3,
862) -> bool {
863    unsafe {
864        bridge!("dropLoot", [S(table), S(dim), D(pos.x), D(pos.y), D(pos.z)]).unwrap_or(false)
865    }
866}
867
868unsafe extern "C" fn srv_scoreboard_get(
869    _ctx: *mut c_void,
870    obj: YogStr,
871    player: YogStr,
872    out: *mut i32,
873) -> bool {
874    unsafe {
875        match bridge!("scoreboardGet", [S(obj), S(player)]) {
876            Some(v) if v != i32::MIN => {
877                *out = v;
878                true
879            }
880            _ => false,
881        }
882    }
883}
884
885unsafe extern "C" fn srv_scoreboard_set(
886    _ctx: *mut c_void,
887    obj: YogStr,
888    player: YogStr,
889    score: i32,
890) -> bool {
891    unsafe { bridge!("scoreboardSet", [S(obj), S(player), I(score)]).unwrap_or(false) }
892}
893
894unsafe extern "C" fn srv_scoreboard_add(
895    _ctx: *mut c_void,
896    obj: YogStr,
897    player: YogStr,
898    delta: i32,
899    out: *mut i32,
900) -> bool {
901    unsafe {
902        match bridge!("scoreboardAdd", [S(obj), S(player), I(delta)]) {
903            Some(v) if v != i32::MIN => {
904                *out = v;
905                true
906            }
907            _ => false,
908        }
909    }
910}
911
912unsafe extern "C" fn srv_bossbar_create(
913    _ctx: *mut c_void,
914    id: YogStr,
915    title: YogStr,
916    color: YogStr,
917    style: YogStr,
918) -> bool {
919    unsafe { bridge!("bossbarCreate", [S(id), S(title), S(color), S(style)]).unwrap_or(false) }
920}
921
922unsafe extern "C" fn srv_bossbar_remove(_ctx: *mut c_void, id: YogStr) -> bool {
923    unsafe { bridge!("bossbarRemove", [S(id)]).unwrap_or(false) }
924}
925
926unsafe extern "C" fn srv_bossbar_set_title(_ctx: *mut c_void, id: YogStr, title: YogStr) -> bool {
927    unsafe { bridge!("bossbarSetTitle", [S(id), S(title)]).unwrap_or(false) }
928}
929
930unsafe extern "C" fn srv_bossbar_set_progress(
931    _ctx: *mut c_void,
932    id: YogStr,
933    progress: f32,
934) -> bool {
935    unsafe { bridge!("bossbarSetProgress", [S(id), F(progress)]).unwrap_or(false) }
936}
937
938unsafe extern "C" fn srv_bossbar_set_color(_ctx: *mut c_void, id: YogStr, color: YogStr) -> bool {
939    unsafe { bridge!("bossbarSetColor", [S(id), S(color)]).unwrap_or(false) }
940}
941
942unsafe extern "C" fn srv_bossbar_add_player(_ctx: *mut c_void, id: YogStr, player: YogStr) -> bool {
943    unsafe { bridge!("bossbarAddPlayer", [S(id), S(player)]).unwrap_or(false) }
944}
945
946unsafe extern "C" fn srv_bossbar_remove_player(
947    _ctx: *mut c_void,
948    id: YogStr,
949    player: YogStr,
950) -> bool {
951    unsafe { bridge!("bossbarRemovePlayer", [S(id), S(player)]).unwrap_or(false) }
952}
953
954unsafe extern "C" fn srv_bossbar_set_visible(_ctx: *mut c_void, id: YogStr, visible: bool) -> bool {
955    unsafe { bridge!("bossbarSetVisible", [S(id), Z(visible)]).unwrap_or(false) }
956}
957
958unsafe extern "C" fn srv_get_block_nbt(
959    _ctx: *mut c_void,
960    dim: YogStr,
961    pos: YogBlockPos,
962) -> YogOwnedStr {
963    unsafe {
964        owned(bridge!(
965            "getBlockNbt",
966            [S(dim), I(pos.x), I(pos.y), I(pos.z)]
967        ))
968    }
969}
970
971unsafe extern "C" fn srv_set_block_nbt(
972    _ctx: *mut c_void,
973    dim: YogStr,
974    pos: YogBlockPos,
975    snbt: YogStr,
976) -> bool {
977    unsafe {
978        bridge!(
979            "setBlockNbt",
980            [S(dim), I(pos.x), I(pos.y), I(pos.z), S(snbt)]
981        )
982        .unwrap_or(false)
983    }
984}
985
986unsafe extern "C" fn srv_get_inventory_slot(
987    _ctx: *mut c_void,
988    dim: YogStr,
989    pos: yog_abi::YogBlockPos,
990    slot: u32,
991) -> YogOwnedStr {
992    unsafe {
993        owned(bridge!(
994            "getInventorySlot",
995            [S(dim), I(pos.x), I(pos.y), I(pos.z), I(slot as i32)]
996        ))
997    }
998}
999
1000unsafe extern "C" fn srv_set_inventory_slot(
1001    _ctx: *mut c_void,
1002    dim: YogStr,
1003    pos: yog_abi::YogBlockPos,
1004    slot: u32,
1005    item_id: YogStr,
1006    count: u32,
1007) -> bool {
1008    unsafe {
1009        bridge!(
1010            "setInventorySlot",
1011            [
1012                S(dim),
1013                I(pos.x),
1014                I(pos.y),
1015                I(pos.z),
1016                I(slot as i32),
1017                S(item_id),
1018                I(count as i32)
1019            ]
1020        )
1021        .unwrap_or(false)
1022    }
1023}
1024
1025unsafe extern "C" fn srv_player_inventory(_ctx: *mut c_void, player: YogStr) -> YogOwnedStr {
1026    unsafe { owned(bridge!("playerInventory", [S(player)])) }
1027}
1028
1029unsafe extern "C" fn srv_player_set_slot(
1030    _ctx: *mut c_void,
1031    player: YogStr,
1032    slot: u32,
1033    item_id: YogStr,
1034    count: u32,
1035) -> bool {
1036    unsafe {
1037        bridge!(
1038            "playerSetSlot",
1039            [S(player), I(slot as i32), S(item_id), I(count as i32)]
1040        )
1041        .unwrap_or(false)
1042    }
1043}
1044
1045unsafe extern "C" fn srv_player_teleport_dim(
1046    _ctx: *mut c_void,
1047    player: YogStr,
1048    dim: YogStr,
1049    pos: YogVec3,
1050) -> bool {
1051    unsafe {
1052        bridge!(
1053            "teleportToDim",
1054            [S(player), S(dim), D(pos.x), D(pos.y), D(pos.z)]
1055        )
1056        .unwrap_or(false)
1057    }
1058}
1059
1060unsafe extern "C" fn srv_entity_teleport_dim(
1061    _ctx: *mut c_void,
1062    uuid: YogStr,
1063    dim: YogStr,
1064    pos: YogVec3,
1065) -> bool {
1066    unsafe {
1067        bridge!(
1068            "entityTeleportToDim",
1069            [S(uuid), S(dim), D(pos.x), D(pos.y), D(pos.z)]
1070        )
1071        .unwrap_or(false)
1072    }
1073}
1074
1075unsafe extern "C" fn srv_online_players(_ctx: *mut c_void) -> YogOwnedStr {
1076    unsafe { owned(bridge!("onlinePlayers", [])) }
1077}
1078
1079unsafe extern "C" fn srv_world_entity_count(
1080    _ctx: *mut c_void,
1081    dim: YogStr,
1082    entity_type: YogStr,
1083) -> i32 {
1084    unsafe { bridge!("worldEntityCount", [S(dim), S(entity_type)]).unwrap_or(-1) }
1085}
1086
1087unsafe extern "C" fn srv_game_dir(_ctx: *mut c_void) -> YogOwnedStr {
1088    unsafe { owned(bridge!("gameDir", [])) }
1089}
1090
1091unsafe extern "C" fn srv_entity_get_nbt(_ctx: *mut c_void, uuid: YogStr) -> YogOwnedStr {
1092    unsafe { owned(bridge!("entityGetNbt", [S(uuid)])) }
1093}
1094
1095unsafe extern "C" fn srv_entity_set_nbt(_ctx: *mut c_void, uuid: YogStr, snbt: YogStr) -> bool {
1096    unsafe { bridge!("entitySetNbt", [S(uuid), S(snbt)]).unwrap_or(false) }
1097}
1098
1099unsafe extern "C" fn srv_spawn_particles(
1100    _ctx: *mut c_void,
1101    dim: YogStr,
1102    pos: YogVec3,
1103    particle_type: YogStr,
1104    count: i32,
1105    dx: f64,
1106    dy: f64,
1107    dz: f64,
1108    speed: f64,
1109) -> bool {
1110    unsafe {
1111        bridge!(
1112            "spawnParticles",
1113            [
1114                S(dim),
1115                D(pos.x),
1116                D(pos.y),
1117                D(pos.z),
1118                S(particle_type),
1119                I(count),
1120                D(dx),
1121                D(dy),
1122                D(dz),
1123                D(speed)
1124            ]
1125        )
1126        .unwrap_or(false)
1127    }
1128}
1129
1130unsafe extern "C" fn srv_entity_attribute_get(
1131    _ctx: *mut c_void,
1132    uuid: YogStr,
1133    attr: YogStr,
1134) -> f64 {
1135    unsafe { bridge!("entityAttributeGet", [S(uuid), S(attr)]).unwrap_or(f64::NAN) }
1136}
1137
1138unsafe extern "C" fn srv_entity_attribute_set(
1139    _ctx: *mut c_void,
1140    uuid: YogStr,
1141    attr: YogStr,
1142    value: f64,
1143) -> bool {
1144    unsafe { bridge!("entityAttributeSet", [S(uuid), S(attr), D(value)]).unwrap_or(false) }
1145}
1146
1147unsafe extern "C" fn srv_get_held_item_nbt(_ctx: *mut c_void, player: YogStr) -> YogOwnedStr {
1148    unsafe { owned(bridge!("getHeldItemNbt", [S(player)])) }
1149}
1150
1151unsafe extern "C" fn srv_set_held_item_nbt(
1152    _ctx: *mut c_void,
1153    player: YogStr,
1154    snbt: YogStr,
1155) -> bool {
1156    unsafe { bridge!("setHeldItemNbt", [S(player), S(snbt)]).unwrap_or(false) }
1157}
1158
1159unsafe extern "C" fn srv_get_offhand_item_nbt(_ctx: *mut c_void, player: YogStr) -> YogOwnedStr {
1160    unsafe { owned(bridge!("getOffhandItemNbt", [S(player)])) }
1161}
1162
1163unsafe extern "C" fn srv_set_offhand_item_nbt(
1164    _ctx: *mut c_void,
1165    player: YogStr,
1166    snbt: YogStr,
1167) -> bool {
1168    unsafe { bridge!("setOffhandItemNbt", [S(player), S(snbt)]).unwrap_or(false) }
1169}
1170
1171unsafe extern "C" fn srv_get_slot_item(
1172    _ctx: *mut c_void,
1173    player: YogStr,
1174    slot: u32,
1175) -> YogOwnedStr {
1176    unsafe { owned(bridge!("getSlotItem", [S(player), I(slot as i32)])) }
1177}
1178
1179unsafe extern "C" fn srv_set_slot_item(
1180    _ctx: *mut c_void,
1181    player: YogStr,
1182    slot: u32,
1183    item_id: YogStr,
1184    count: u32,
1185    snbt: YogStr,
1186) -> bool {
1187    unsafe {
1188        bridge!(
1189            "setSlotItem",
1190            [
1191                S(player),
1192                I(slot as i32),
1193                S(item_id),
1194                I(count as i32),
1195                S(snbt)
1196            ]
1197        )
1198        .unwrap_or(false)
1199    }
1200}
1201
1202// ── ABI minor 14 — low-level GPU pipeline ────────────────────────────────────
1203//
1204// All raw GL calls go through `glow::Context` stored in GL.
1205// `draw2d_*` functions still call JNI (NativeDraw) for MC text/texture rendering.
1206// Everything is called on the render thread — no synchronization needed.
1207
1208// ── helpers ───────────────────────────────────────────────────────────────────
1209
1210fn gl_draw_mode(mode: u8) -> u32 {
1211    match mode {
1212        1 => glow::LINES,
1213        2 => glow::LINE_STRIP,
1214        3 => glow::TRIANGLE_STRIP,
1215        4 => glow::TRIANGLE_FAN,
1216        _ => glow::TRIANGLES,
1217    }
1218}
1219
1220fn gl_attr_type(dtype: u8) -> u32 {
1221    match dtype {
1222        1 => glow::UNSIGNED_BYTE,
1223        2 => glow::INT,
1224        3 => glow::UNSIGNED_INT,
1225        _ => glow::FLOAT,
1226    }
1227}
1228
1229unsafe fn compile_shader(gl: &glow::Context, stage: u32, src: &str) -> Option<glow::NativeShader> {
1230    unsafe {
1231        let sh = gl.create_shader(stage).ok()?;
1232        gl.shader_source(sh, src);
1233        gl.compile_shader(sh);
1234        if !gl.get_shader_compile_status(sh) {
1235            yog_logging::error!("yog-gfx shader compile: {}", gl.get_shader_info_log(sh));
1236            gl.delete_shader(sh);
1237            return None;
1238        }
1239        Some(sh)
1240    }
1241}
1242
1243// ── GPU buffers ───────────────────────────────────────────────────────────────
1244
1245unsafe extern "C" fn gfx_buf_create() -> u32 {
1246    unsafe {
1247        let Some(g) = GL.get() else { return 0 };
1248        g.0.create_buffer().map(|b| b.0.get()).unwrap_or(0)
1249    }
1250}
1251
1252unsafe extern "C" fn gfx_buf_delete(handle: u32) {
1253    unsafe {
1254        let Some(g) = GL.get() else { return };
1255        let Some(n) = NonZeroU32::new(handle) else {
1256            return;
1257        };
1258        g.0.delete_buffer(glow::NativeBuffer(n));
1259    }
1260}
1261
1262unsafe extern "C" fn gfx_buf_data(handle: u32, bytes: *const u8, len: u32, dynamic: bool) {
1263    unsafe {
1264        let Some(g) = GL.get() else { return };
1265        let Some(n) = NonZeroU32::new(handle) else {
1266            return;
1267        };
1268        let gl = &g.0;
1269        let data = std::slice::from_raw_parts(bytes, len as usize);
1270        let usage = if dynamic {
1271            glow::DYNAMIC_DRAW
1272        } else {
1273            glow::STATIC_DRAW
1274        };
1275        gl.bind_buffer(glow::ARRAY_BUFFER, Some(glow::NativeBuffer(n)));
1276        gl.buffer_data_u8_slice(glow::ARRAY_BUFFER, data, usage);
1277        gl.bind_buffer(glow::ARRAY_BUFFER, None);
1278    }
1279}
1280
1281unsafe extern "C" fn gfx_buf_subdata(handle: u32, offset: u32, bytes: *const u8, len: u32) {
1282    unsafe {
1283        let Some(g) = GL.get() else { return };
1284        let Some(n) = NonZeroU32::new(handle) else {
1285            return;
1286        };
1287        let gl = &g.0;
1288        let data = std::slice::from_raw_parts(bytes, len as usize);
1289        gl.bind_buffer(glow::ARRAY_BUFFER, Some(glow::NativeBuffer(n)));
1290        gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, offset as i32, data);
1291        gl.bind_buffer(glow::ARRAY_BUFFER, None);
1292    }
1293}
1294
1295// ── Vertex arrays ─────────────────────────────────────────────────────────────
1296
1297unsafe extern "C" fn gfx_vao_create() -> u32 {
1298    unsafe {
1299        let Some(g) = GL.get() else { return 0 };
1300        g.0.create_vertex_array().map(|v| v.0.get()).unwrap_or(0)
1301    }
1302}
1303
1304unsafe extern "C" fn gfx_vao_delete(handle: u32) {
1305    unsafe {
1306        let Some(g) = GL.get() else { return };
1307        let Some(n) = NonZeroU32::new(handle) else {
1308            return;
1309        };
1310        g.0.delete_vertex_array(glow::NativeVertexArray(n));
1311    }
1312}
1313
1314unsafe extern "C" fn gfx_vao_attrib(
1315    vao: u32,
1316    vbo: u32,
1317    index: u32,
1318    components: u8,
1319    dtype: u8,
1320    normalized: bool,
1321    stride: u32,
1322    offset: u32,
1323) {
1324    unsafe {
1325        let Some(g) = GL.get() else { return };
1326        let (Some(vn), Some(bn)) = (NonZeroU32::new(vao), NonZeroU32::new(vbo)) else {
1327            return;
1328        };
1329        let gl = &g.0;
1330        gl.bind_vertex_array(Some(glow::NativeVertexArray(vn)));
1331        gl.bind_buffer(glow::ARRAY_BUFFER, Some(glow::NativeBuffer(bn)));
1332        let gl_type = gl_attr_type(dtype);
1333        if dtype == 2 || dtype == 3 {
1334            gl.vertex_attrib_pointer_i32(
1335                index,
1336                components as i32,
1337                gl_type,
1338                stride as i32,
1339                offset as i32,
1340            );
1341        } else {
1342            gl.vertex_attrib_pointer_f32(
1343                index,
1344                components as i32,
1345                gl_type,
1346                normalized,
1347                stride as i32,
1348                offset as i32,
1349            );
1350        }
1351        gl.enable_vertex_attrib_array(index);
1352        gl.bind_vertex_array(None);
1353    }
1354}
1355
1356unsafe extern "C" fn gfx_vao_set_ebo(vao: u32, ebo: u32) {
1357    unsafe {
1358        let Some(g) = GL.get() else { return };
1359        let (Some(vn), Some(en)) = (NonZeroU32::new(vao), NonZeroU32::new(ebo)) else {
1360            return;
1361        };
1362        let gl = &g.0;
1363        gl.bind_vertex_array(Some(glow::NativeVertexArray(vn)));
1364        gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(glow::NativeBuffer(en)));
1365        gl.bind_vertex_array(None);
1366    }
1367}
1368
1369// ── Shader programs ───────────────────────────────────────────────────────────
1370
1371/// Returns a path under `~/.cache/yog/shaders/<hash>.ysc` for the given GLSL source pair,
1372/// creating the directory if needed.  Returns `None` if the home directory is unknown.
1373fn shader_cache_path(vert: &str, frag: &str) -> Option<PathBuf> {
1374    use std::hash::{Hash, Hasher};
1375    let mut h = std::collections::hash_map::DefaultHasher::new();
1376    vert.hash(&mut h);
1377    frag.hash(&mut h);
1378    let hash = h.finish();
1379    let dir = std::env::var("HOME")
1380        .map(|home| {
1381            PathBuf::from(home)
1382                .join(".cache")
1383                .join("yog")
1384                .join("shaders")
1385        })
1386        .ok()?;
1387    std::fs::create_dir_all(&dir).ok()?;
1388    Some(dir.join(format!("{hash:016x}.ysc")))
1389}
1390
1391/// Try restoring a program from a binary blob (`[4 LE bytes: GL format][binary…]`).
1392/// Returns `Some(handle)` if the driver accepts the binary, `None` otherwise.
1393///
1394/// Uses `glProgramBinary` (GL 4.1 / ARB_get_program_binary) via the raw pointer
1395/// captured in `nativeGlInit`.  Returns `None` if the extension is unavailable.
1396unsafe fn load_shader_binary(gl: &glow::Context, data: &[u8]) -> Option<glow::NativeProgram> {
1397    unsafe {
1398        if data.len() < 4 {
1399            return None;
1400        }
1401        type ProgramBinaryFn = unsafe extern "system" fn(u32, u32, *const c_void, i32);
1402        let fn_ptr = (*GL_PROGRAM_BINARY.get()?)?;
1403        let program_binary: ProgramBinaryFn = std::mem::transmute(fn_ptr);
1404
1405        let fmt = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1406        let prog = gl.create_program().ok()?;
1407        program_binary(
1408            prog.0.get(),
1409            fmt,
1410            data[4..].as_ptr() as *const c_void,
1411            (data.len() - 4) as i32,
1412        );
1413        if gl.get_program_link_status(prog) {
1414            Some(prog)
1415        } else {
1416            gl.delete_program(prog);
1417            None
1418        }
1419    }
1420}
1421
1422/// Read the compiled binary of a linked program.  Returns empty `Vec` if unsupported.
1423unsafe fn get_shader_binary(prog: glow::NativeProgram) -> (Vec<u8>, u32) {
1424    unsafe {
1425        type GetProgramivFn = unsafe extern "system" fn(u32, u32, *mut i32);
1426        type GetProgramBinaryFn =
1427            unsafe extern "system" fn(u32, i32, *mut i32, *mut u32, *mut c_void);
1428
1429        let Some(get_iv_raw) = GL_GET_PROGRAM_IV.get().and_then(|v| *v) else {
1430            return (vec![], 0);
1431        };
1432        let Some(get_bin_raw) = GL_GET_PROGRAM_BINARY.get().and_then(|v| *v) else {
1433            return (vec![], 0);
1434        };
1435        let get_program_iv: GetProgramivFn = std::mem::transmute(get_iv_raw);
1436        let get_program_binary: GetProgramBinaryFn = std::mem::transmute(get_bin_raw);
1437
1438        // Query binary size via glGetProgramiv(GL_PROGRAM_BINARY_LENGTH).
1439        const PROGRAM_BINARY_LENGTH: u32 = 0x8741;
1440        let mut size: i32 = 0;
1441        get_program_iv(prog.0.get(), PROGRAM_BINARY_LENGTH, &mut size);
1442        if size <= 0 {
1443            return (vec![], 0);
1444        }
1445
1446        let mut buf = vec![0u8; size as usize];
1447        let mut actual_len: i32 = 0;
1448        let mut fmt: u32 = 0;
1449        get_program_binary(
1450            prog.0.get(),
1451            size,
1452            &mut actual_len,
1453            &mut fmt,
1454            buf.as_mut_ptr() as *mut c_void,
1455        );
1456        buf.truncate(actual_len.max(0) as usize);
1457        (buf, fmt)
1458    }
1459}
1460
1461unsafe extern "C" fn gfx_prog_create(vert: YogStr, frag: YogStr, out: *mut u32) -> bool {
1462    unsafe {
1463        let Some(g) = GL.get() else { return false };
1464        let gl = &g.0;
1465        let vert_s = vert.as_str();
1466        let frag_s = frag.as_str();
1467
1468        // Fast path: binary shader cache — avoids GLSL re-compilation on subsequent launches.
1469        let cache_path = shader_cache_path(vert_s, frag_s);
1470        if let Some(ref path) = cache_path
1471            && let Ok(data) = std::fs::read(path)
1472        {
1473            if let Some(prog) = load_shader_binary(gl, &data) {
1474                *out = prog.0.get();
1475                return true;
1476            }
1477            // Cache stale (driver updated?); remove and fall through to GLSL compile.
1478            let _ = std::fs::remove_file(path);
1479        }
1480
1481        // GLSL compile path.
1482        let vs = match compile_shader(gl, glow::VERTEX_SHADER, vert_s) {
1483            Some(s) => s,
1484            None => return false,
1485        };
1486        let fs = match compile_shader(gl, glow::FRAGMENT_SHADER, frag_s) {
1487            Some(s) => s,
1488            None => {
1489                gl.delete_shader(vs);
1490                return false;
1491            }
1492        };
1493        let prog = match gl.create_program() {
1494            Ok(p) => p,
1495            Err(e) => {
1496                yog_logging::error!("yog-gfx: create_program: {}", e);
1497                gl.delete_shader(vs);
1498                gl.delete_shader(fs);
1499                return false;
1500            }
1501        };
1502        gl.attach_shader(prog, vs);
1503        gl.attach_shader(prog, fs);
1504        gl.link_program(prog);
1505        gl.detach_shader(prog, vs);
1506        gl.detach_shader(prog, fs);
1507        gl.delete_shader(vs);
1508        gl.delete_shader(fs);
1509        if !gl.get_program_link_status(prog) {
1510            yog_logging::error!("yog-gfx: shader link: {}", gl.get_program_info_log(prog));
1511            gl.delete_program(prog);
1512            return false;
1513        }
1514
1515        // Persist binary so the next launch skips GLSL compilation entirely.
1516        if let Some(ref path) = cache_path {
1517            let (binary, fmt) = get_shader_binary(prog);
1518            if !binary.is_empty() {
1519                let mut blob = fmt.to_le_bytes().to_vec();
1520                blob.extend_from_slice(&binary);
1521                let _ = std::fs::write(path, &blob);
1522            }
1523        }
1524
1525        *out = prog.0.get();
1526        true
1527    }
1528}
1529
1530unsafe extern "C" fn gfx_prog_delete(handle: u32) {
1531    unsafe {
1532        let Some(g) = GL.get() else { return };
1533        let Some(n) = NonZeroU32::new(handle) else {
1534            return;
1535        };
1536        g.0.delete_program(glow::NativeProgram(n));
1537    }
1538}
1539
1540macro_rules! with_prog {
1541    ($handle:expr_2021, |$gl:ident, $prog:ident, $loc:ident ($name:expr_2021)| $body:expr_2021) => {{
1542        let Some(g) = GL.get() else { return };
1543        let Some(n) = NonZeroU32::new($handle) else {
1544            return;
1545        };
1546        let $gl = &g.0;
1547        let $prog = glow::NativeProgram(n);
1548        $gl.use_program(Some($prog));
1549        let $loc = $gl.get_uniform_location($prog, $name.as_str());
1550        $body
1551    }};
1552}
1553
1554unsafe extern "C" fn gfx_prog_uniform_1i(prog: u32, name: YogStr, v: i32) {
1555    unsafe {
1556        with_prog!(prog, |gl, _p, loc(name)| gl.uniform_1_i32(loc.as_ref(), v));
1557    }
1558}
1559unsafe extern "C" fn gfx_prog_uniform_1f(prog: u32, name: YogStr, v: f32) {
1560    unsafe {
1561        with_prog!(prog, |gl, _p, loc(name)| gl.uniform_1_f32(loc.as_ref(), v));
1562    }
1563}
1564unsafe extern "C" fn gfx_prog_uniform_2f(prog: u32, name: YogStr, x: f32, y: f32) {
1565    unsafe {
1566        with_prog!(prog, |gl, _p, loc(name)| gl.uniform_2_f32(
1567            loc.as_ref(),
1568            x,
1569            y
1570        ));
1571    }
1572}
1573unsafe extern "C" fn gfx_prog_uniform_3f(prog: u32, name: YogStr, x: f32, y: f32, z: f32) {
1574    unsafe {
1575        with_prog!(prog, |gl, _p, loc(name)| gl.uniform_3_f32(
1576            loc.as_ref(),
1577            x,
1578            y,
1579            z
1580        ));
1581    }
1582}
1583unsafe extern "C" fn gfx_prog_uniform_4f(prog: u32, name: YogStr, x: f32, y: f32, z: f32, w: f32) {
1584    unsafe {
1585        with_prog!(prog, |gl, _p, loc(name)| gl.uniform_4_f32(
1586            loc.as_ref(),
1587            x,
1588            y,
1589            z,
1590            w
1591        ));
1592    }
1593}
1594unsafe extern "C" fn gfx_prog_uniform_mat4(prog: u32, name: YogStr, col_major: *const f32) {
1595    unsafe {
1596        with_prog!(prog, |gl, _p, loc(name)| {
1597            let data = std::slice::from_raw_parts(col_major, 16);
1598            gl.uniform_matrix_4_f32_slice(loc.as_ref(), false, data);
1599        });
1600    }
1601}
1602
1603// ── Textures ──────────────────────────────────────────────────────────────────
1604
1605unsafe extern "C" fn gfx_tex_create(w: u32, h: u32, rgba: *const u8, linear: bool) -> u32 {
1606    unsafe {
1607        let Some(g) = GL.get() else { return 0 };
1608        let gl = &g.0;
1609        let tex = match gl.create_texture() {
1610            Ok(t) => t,
1611            Err(_) => return 0,
1612        };
1613        gl.bind_texture(glow::TEXTURE_2D, Some(tex));
1614        let filter = if linear { glow::LINEAR } else { glow::NEAREST };
1615        gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, filter as i32);
1616        gl.tex_parameter_i32(glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, filter as i32);
1617        gl.tex_parameter_i32(
1618            glow::TEXTURE_2D,
1619            glow::TEXTURE_WRAP_S,
1620            glow::CLAMP_TO_EDGE as i32,
1621        );
1622        gl.tex_parameter_i32(
1623            glow::TEXTURE_2D,
1624            glow::TEXTURE_WRAP_T,
1625            glow::CLAMP_TO_EDGE as i32,
1626        );
1627        let pixels = std::slice::from_raw_parts(rgba, (w * h * 4) as usize);
1628        gl.tex_image_2d(
1629            glow::TEXTURE_2D,
1630            0,
1631            glow::RGBA8 as i32,
1632            w as i32,
1633            h as i32,
1634            0,
1635            glow::RGBA,
1636            glow::UNSIGNED_BYTE,
1637            glow::PixelUnpackData::Slice(Some(pixels)),
1638        );
1639        gl.bind_texture(glow::TEXTURE_2D, None);
1640        tex.0.get()
1641    }
1642}
1643
1644unsafe extern "C" fn gfx_tex_delete(handle: u32) {
1645    unsafe {
1646        let Some(g) = GL.get() else { return };
1647        let Some(n) = NonZeroU32::new(handle) else {
1648            return;
1649        };
1650        g.0.delete_texture(glow::NativeTexture(n));
1651    }
1652}
1653
1654unsafe extern "C" fn gfx_tex_bind(unit: u32, handle: u32) {
1655    unsafe {
1656        let Some(g) = GL.get() else { return };
1657        let gl = &g.0;
1658        gl.active_texture(glow::TEXTURE0 + unit);
1659        match NonZeroU32::new(handle) {
1660            Some(n) => gl.bind_texture(glow::TEXTURE_2D, Some(glow::NativeTexture(n))),
1661            None => gl.bind_texture(glow::TEXTURE_2D, None),
1662        }
1663    }
1664}
1665
1666unsafe extern "C" fn gfx_tex_from_mc(id: YogStr) -> u32 {
1667    unsafe { host!("dev/yog/NativeDraw", "getMcTextureId", [S(id)]).map_or(0, |id: i32| id as u32) }
1668}
1669
1670// ── Draw calls ────────────────────────────────────────────────────────────────
1671
1672unsafe extern "C" fn gfx_draw_arrays(vao: u32, prog: u32, mode: u8, first: u32, count: u32) {
1673    unsafe {
1674        let Some(g) = GL.get() else { return };
1675        let (Some(vn), Some(pn)) = (NonZeroU32::new(vao), NonZeroU32::new(prog)) else {
1676            return;
1677        };
1678        let gl = &g.0;
1679        gl.use_program(Some(glow::NativeProgram(pn)));
1680        gl.bind_vertex_array(Some(glow::NativeVertexArray(vn)));
1681        gl.draw_arrays(gl_draw_mode(mode), first as i32, count as i32);
1682        gl.bind_vertex_array(None);
1683    }
1684}
1685
1686unsafe extern "C" fn gfx_draw_elements(
1687    vao: u32,
1688    ebo: u32,
1689    prog: u32,
1690    mode: u8,
1691    count: u32,
1692    u32_idx: bool,
1693) {
1694    unsafe {
1695        let Some(g) = GL.get() else { return };
1696        let (Some(vn), Some(pn)) = (NonZeroU32::new(vao), NonZeroU32::new(prog)) else {
1697            return;
1698        };
1699        let gl = &g.0;
1700        gl.use_program(Some(glow::NativeProgram(pn)));
1701        gl.bind_vertex_array(Some(glow::NativeVertexArray(vn)));
1702        // EBO is stored in the VAO; ebo param is informational for safety but not re-bound here.
1703        let _ = ebo;
1704        let idx_type = if u32_idx {
1705            glow::UNSIGNED_INT
1706        } else {
1707            glow::UNSIGNED_SHORT
1708        };
1709        gl.draw_elements(gl_draw_mode(mode), count as i32, idx_type, 0);
1710        gl.bind_vertex_array(None);
1711    }
1712}
1713
1714// ── Render state ──────────────────────────────────────────────────────────────
1715
1716unsafe extern "C" fn gfx_set_blend(enabled: bool, src: u32, dst: u32) {
1717    unsafe {
1718        let Some(g) = GL.get() else { return };
1719        let gl = &g.0;
1720        if enabled {
1721            gl.enable(glow::BLEND);
1722            gl.blend_func(src, dst);
1723        } else {
1724            gl.disable(glow::BLEND);
1725        }
1726    }
1727}
1728
1729unsafe extern "C" fn gfx_set_depth(test: bool, write: bool) {
1730    unsafe {
1731        let Some(g) = GL.get() else { return };
1732        let gl = &g.0;
1733        if test {
1734            gl.enable(glow::DEPTH_TEST);
1735        } else {
1736            gl.disable(glow::DEPTH_TEST);
1737        }
1738        gl.depth_mask(write);
1739        // Disable face culling: our 2D quads use CW winding (back-face) due to y-flip.
1740        if !test {
1741            gl.disable(glow::CULL_FACE);
1742        }
1743    }
1744}
1745
1746unsafe extern "C" fn gfx_set_scissor(x: i32, y: i32, w: i32, h: i32) {
1747    unsafe {
1748        let Some(g) = GL.get() else { return };
1749        let gl = &g.0;
1750        gl.enable(glow::SCISSOR_TEST);
1751        gl.scissor(x, y, w, h);
1752    }
1753}
1754
1755unsafe extern "C" fn gfx_clear_scissor() {
1756    unsafe {
1757        let Some(g) = GL.get() else { return };
1758        g.0.disable(glow::SCISSOR_TEST);
1759    }
1760}
1761
1762unsafe extern "C" fn gfx_set_viewport(x: i32, y: i32, w: i32, h: i32) {
1763    unsafe {
1764        let Some(g) = GL.get() else { return };
1765        g.0.viewport(x, y, w, h);
1766    }
1767}
1768
1769// ── 2D convenience (JNI — uses MC's DrawContext / text renderer) ──────────────
1770
1771unsafe extern "C" fn gfx_draw2d_rect(x1: f32, y1: f32, x2: f32, y2: f32, color: u32) {
1772    unsafe {
1773        let _: Option<()> = host!(
1774            "dev/yog/NativeDraw",
1775            "drawRect",
1776            [F(x1), F(y1), F(x2), F(y2), I(color as i32)]
1777        );
1778    }
1779}
1780
1781unsafe extern "C" fn gfx_draw2d_gradient(
1782    x1: f32,
1783    y1: f32,
1784    x2: f32,
1785    y2: f32,
1786    top: u32,
1787    bottom: u32,
1788) {
1789    unsafe {
1790        let _: Option<()> = host!(
1791            "dev/yog/NativeDraw",
1792            "drawGradientRect",
1793            [F(x1), F(y1), F(x2), F(y2), I(top as i32), I(bottom as i32)]
1794        );
1795    }
1796}
1797
1798unsafe extern "C" fn gfx_draw2d_text(text: YogStr, x: f32, y: f32, color: u32, shadow: bool) {
1799    unsafe {
1800        let _: Option<()> = host!(
1801            "dev/yog/NativeDraw",
1802            "drawText",
1803            [S(text), F(x), F(y), I(color as i32), Z(shadow)]
1804        );
1805    }
1806}
1807
1808unsafe extern "C" fn gfx_draw2d_text_font(
1809    text: YogStr,
1810    font: YogStr,
1811    x: f32,
1812    y: f32,
1813    color: u32,
1814    shadow: bool,
1815) {
1816    unsafe {
1817        let _: Option<()> = host!(
1818            "dev/yog/NativeDraw",
1819            "drawTextFont",
1820            [S(text), S(font), F(x), F(y), I(color as i32), Z(shadow)]
1821        );
1822    }
1823}
1824
1825unsafe extern "C" fn gfx_text_width(text: YogStr, font: YogStr) -> f32 {
1826    unsafe { host!("dev/yog/NativeDraw", "textWidth", [S(text), S(font)]).unwrap_or(0.0) }
1827}
1828
1829unsafe extern "C" fn gfx_draw2d_item(id: YogStr, x: f32, y: f32, size: f32) {
1830    unsafe {
1831        let _: Option<()> = host!(
1832            "dev/yog/NativeDraw",
1833            "drawItem",
1834            [S(id), F(x), F(y), F(size)]
1835        );
1836    }
1837}
1838
1839// ── Static GFX function table (function pointers only — per-frame data filled at call time) ──
1840
1841static GFX_FN_TABLE: YogGfxApi = YogGfxApi {
1842    // Per-frame fields zeroed in the static; actual values are set on the stack per render call.
1843    screen_w: 0,
1844    screen_h: 0,
1845    delta_tick: 0.0,
1846    scale_factor: 1.0,
1847    view_proj: [0.0; 16],
1848    camera_pos: [0.0; 3],
1849    player_pos: [0.0; 3],
1850    _pad1: 0.0,
1851    buf_create: gfx_buf_create,
1852    buf_delete: gfx_buf_delete,
1853    buf_data: gfx_buf_data,
1854    buf_subdata: gfx_buf_subdata,
1855    vao_create: gfx_vao_create,
1856    vao_delete: gfx_vao_delete,
1857    vao_attrib: gfx_vao_attrib,
1858    vao_set_ebo: gfx_vao_set_ebo,
1859    prog_create: gfx_prog_create,
1860    prog_delete: gfx_prog_delete,
1861    prog_uniform_1i: gfx_prog_uniform_1i,
1862    prog_uniform_1f: gfx_prog_uniform_1f,
1863    prog_uniform_2f: gfx_prog_uniform_2f,
1864    prog_uniform_3f: gfx_prog_uniform_3f,
1865    prog_uniform_4f: gfx_prog_uniform_4f,
1866    prog_uniform_mat4: gfx_prog_uniform_mat4,
1867    tex_create: gfx_tex_create,
1868    tex_delete: gfx_tex_delete,
1869    tex_bind: gfx_tex_bind,
1870    tex_from_mc: gfx_tex_from_mc,
1871    draw_arrays: gfx_draw_arrays,
1872    draw_elements: gfx_draw_elements,
1873    set_blend: gfx_set_blend,
1874    set_depth: gfx_set_depth,
1875    set_scissor: gfx_set_scissor,
1876    clear_scissor: gfx_clear_scissor,
1877    set_viewport: gfx_set_viewport,
1878    draw2d_rect: gfx_draw2d_rect,
1879    draw2d_gradient: gfx_draw2d_gradient,
1880    draw2d_text: gfx_draw2d_text,
1881    draw2d_item: gfx_draw2d_item,
1882    draw2d_text_font: gfx_draw2d_text_font,
1883    text_width: gfx_text_width,
1884    draw2d_image: images::gfx_draw2d_image,
1885    image_size: images::gfx_image_size,
1886    draw2d_svg: svg::gfx_draw2d_svg,
1887    draw2d_entity: svg::gfx_draw2d_entity,
1888};
1889
1890// ── YogApi registration functions ─────────────────────────────────────────────
1891//
1892// ctx is *const RwLock<RuntimeHandlers> (cast from *mut c_void) — see
1893// `current_registration()` for how each entry gets tagged with the
1894// `LoadedModule` that's registering it.
1895
1896macro_rules! api_event {
1897    ($name:ident, $field:ident, $fn_ty:ty) => {
1898        unsafe extern "C" fn $name(ctx: *mut c_void, ud: *mut c_void, h: $fn_ty) {
1899            unsafe {
1900                let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1901                let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1902                handlers.$field.push((current_registration(), ud, h));
1903            }
1904        }
1905    };
1906}
1907
1908api_event!(api_on_block_break, block_break, YogBlockBreakFn);
1909api_event!(api_on_chat, chat, YogChatFn);
1910api_event!(api_on_player_join, player_join, YogPlayerFn);
1911api_event!(api_on_player_leave, player_leave, YogPlayerFn);
1912api_event!(api_on_use_item, use_item, YogUseItemFn);
1913api_event!(api_on_use_block, use_block, YogUseBlockFn);
1914api_event!(api_on_attack_entity, attack_entity, YogAttackEntityFn);
1915api_event!(api_on_entity_damage, entity_damage, YogEntityDamageFn);
1916api_event!(api_on_entity_death, entity_death, YogEntityDeathFn);
1917api_event!(api_on_entity_spawn, entity_spawn, YogEntitySpawnFn);
1918api_event!(
1919    api_on_player_place_block,
1920    player_place_block,
1921    YogPlaceBlockFn
1922);
1923api_event!(api_on_player_death, player_death, YogPlayerDeathFn);
1924api_event!(api_on_player_respawn, player_respawn, YogPlayerRespawnFn);
1925api_event!(api_on_advancement, advancement, YogAdvancementFn);
1926api_event!(api_on_entity_interact, entity_interact, YogEntityInteractFn);
1927api_event!(api_on_item_craft, item_craft, YogCraftFn);
1928api_event!(api_on_explosion, explosion, YogExplosionFn);
1929api_event!(api_on_item_pickup, item_pickup, YogItemPickupFn);
1930api_event!(api_on_player_move, player_move, YogPlayerMoveFn);
1931api_event!(api_on_container_open, container_open, YogContainerOpenFn);
1932api_event!(api_on_container_close, container_close, YogContainerCloseFn);
1933api_event!(api_on_projectile_hit, projectile_hit, YogProjectileHitFn);
1934api_event!(api_on_client_tick, client_tick, YogClientFn);
1935api_event!(api_on_hud_render, hud_render, YogHudRenderFn);
1936api_event!(api_on_world_render, world_render, YogWorldRenderFn);
1937api_event!(api_on_key_press, key_press, YogKeyPressFn);
1938api_event!(api_on_screen_open, screen_open, YogScreenFn);
1939api_event!(api_on_screen_close, screen_close, YogScreenFn);
1940api_event!(api_on_server_tick, server_tick, YogServerFn);
1941api_event!(api_on_server_started, server_started, YogServerFn);
1942api_event!(api_on_server_stopping, server_stopping, YogServerFn);
1943
1944unsafe extern "C" fn api_on_packet(
1945    ctx: *mut c_void,
1946    channel: YogStr,
1947    ud: *mut c_void,
1948    h: YogPacketFn,
1949) {
1950    unsafe {
1951        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1952        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1953        handlers
1954            .packets
1955            .insert(channel.as_str().to_owned(), (current_registration(), ud, h));
1956    }
1957}
1958
1959unsafe extern "C" fn api_on_client_packet(
1960    ctx: *mut c_void,
1961    channel: YogStr,
1962    ud: *mut c_void,
1963    h: YogPacketFn,
1964) {
1965    unsafe {
1966        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1967        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1968        handlers
1969            .client_packets
1970            .insert(channel.as_str().to_owned(), (current_registration(), ud, h));
1971    }
1972}
1973
1974unsafe extern "C" fn api_register_command(
1975    ctx: *mut c_void,
1976    name: YogStr,
1977    ud: *mut c_void,
1978    h: YogCommandFn,
1979) {
1980    unsafe {
1981        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1982        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1983        handlers
1984            .commands
1985            .entry(name.as_str().to_owned())
1986            .or_default()
1987            .push((current_registration(), None, ud, h));
1988    }
1989}
1990
1991unsafe extern "C" fn api_register_typed_command(
1992    ctx: *mut c_void,
1993    name: YogStr,
1994    schema: YogStr,
1995    ud: *mut c_void,
1996    h: YogCommandFn,
1997) {
1998    unsafe {
1999        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2000        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2001        let n = name.as_str().to_owned();
2002        handlers.commands.entry(n).or_default().push((
2003            current_registration(),
2004            Some(schema.as_str().to_owned()),
2005            ud,
2006            h,
2007        ));
2008    }
2009}
2010
2011unsafe extern "C" fn api_register_recipe_json(
2012    ctx: *mut c_void,
2013    namespace: YogStr,
2014    name: YogStr,
2015    json: YogStr,
2016) {
2017    unsafe {
2018        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2019        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2020        let ns = namespace.as_str().to_owned();
2021        let n = name.as_str().to_owned();
2022        let j = json.as_str().to_owned();
2023        // Feed the recipe to already-registered book renderers (crafting pages).
2024        let full_id = format!("{ns}:{n}");
2025        for r in handlers.book_renderers.lock().unwrap().values_mut() {
2026            r.add_recipe(full_id.clone(), &j);
2027        }
2028        handlers.recipes.push((ns, n, j));
2029    }
2030}
2031
2032unsafe extern "C" fn api_register_item(ctx: *mut c_void, def: *const YogItemDef) {
2033    unsafe {
2034        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2035        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2036        let d = &*def;
2037        let food = if d.food_nutrition > 0 {
2038            Some(FoodDef {
2039                nutrition: d.food_nutrition,
2040                saturation: d.food_saturation,
2041                can_always_eat: d.food_always_eat,
2042            })
2043        } else {
2044            None
2045        };
2046        handlers.items.push(ItemDef {
2047            id: d.id.as_str().to_owned(),
2048            max_stack: d.max_stack as u8,
2049            name: if d.name.is_empty() {
2050                None
2051            } else {
2052                Some(d.name.as_str().to_owned())
2053            },
2054            tooltip: if d.tooltip.is_empty() {
2055                None
2056            } else {
2057                Some(d.tooltip.as_str().to_owned())
2058            },
2059            max_damage: d.max_damage,
2060            fire_resistant: d.fire_resistant,
2061            fuel_ticks: d.fuel_ticks,
2062            food,
2063        });
2064    }
2065}
2066
2067unsafe extern "C" fn api_register_block(ctx: *mut c_void, def: *const YogBlockDef) {
2068    unsafe {
2069        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2070        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2071        let d = &*def;
2072        handlers.blocks.push(BlockDef {
2073            id: d.id.as_str().to_owned(),
2074            hardness: d.hardness,
2075            resistance: d.resistance,
2076            name: if d.name.is_empty() {
2077                None
2078            } else {
2079                Some(d.name.as_str().to_owned())
2080            },
2081            light_level: d.light_level,
2082            sound: if d.sound.is_empty() {
2083                None
2084            } else {
2085                Some(d.sound.as_str().to_owned())
2086            },
2087            requires_tool: d.requires_tool,
2088            no_collision: d.no_collision,
2089            slipperiness: d.slipperiness,
2090            shape: if d.shape == [0.0f32; 6] {
2091                None
2092            } else {
2093                Some(d.shape)
2094            },
2095            connects: d.connects,
2096            connect_groups: if d.connect_groups.is_empty() {
2097                Vec::new()
2098            } else {
2099                d.connect_groups
2100                    .as_str()
2101                    .split(',')
2102                    .map(str::to_owned)
2103                    .collect()
2104            },
2105            inventory_id: if d.inventory_id.is_empty() {
2106                None
2107            } else {
2108                Some(d.inventory_id.as_str().to_owned())
2109            },
2110            redstone_source: d.redstone_source,
2111        });
2112    }
2113}
2114
2115unsafe extern "C" fn api_register_inventory(
2116    ctx: *mut c_void,
2117    def: *const yog_abi::YogInventoryDef,
2118) {
2119    unsafe {
2120        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2121        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2122        let d = &*def;
2123        handlers.inventories.push(yog_inventory::InventoryDef {
2124            id: d.id.as_str().to_owned(),
2125            slot_count: d.slot_count as usize,
2126            layout: yog_inventory::decode_layout(d.layout.as_str()),
2127            include_player_inventory: d.include_player_inventory,
2128            player_inv_offset: (d.player_inv_x, d.player_inv_y),
2129            background_texture: if d.background_texture.is_empty() {
2130                None
2131            } else {
2132                Some(d.background_texture.as_str().to_owned())
2133            },
2134            title: d.title.as_str().to_owned(),
2135            background_size: if d.background_w > 0.0 && d.background_h > 0.0 {
2136                Some((d.background_w, d.background_h))
2137            } else {
2138                None
2139            },
2140        });
2141    }
2142}
2143
2144unsafe extern "C" fn api_schedule_once(
2145    ctx: *mut c_void,
2146    delay_ticks: u64,
2147    ud: *mut c_void,
2148    h: YogScheduledFn,
2149) {
2150    unsafe {
2151        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2152        let handlers = handlers_lock.write().expect("handlers lock poisoned");
2153        handlers
2154            .scheduler
2155            .lock()
2156            .expect("scheduler poisoned")
2157            .once_tasks
2158            .push(OnceTask {
2159                module: current_registration(),
2160                delay_remaining: delay_ticks,
2161                ud,
2162                f: h,
2163            });
2164    }
2165}
2166
2167unsafe extern "C" fn api_schedule_repeating(
2168    ctx: *mut c_void,
2169    period_ticks: u64,
2170    ud: *mut c_void,
2171    h: YogScheduledFn,
2172) {
2173    unsafe {
2174        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2175        let handlers = handlers_lock.write().expect("handlers lock poisoned");
2176        handlers
2177            .scheduler
2178            .lock()
2179            .expect("scheduler poisoned")
2180            .repeating_tasks
2181            .push(RepeatingTask {
2182                module: current_registration(),
2183                period: period_ticks,
2184                ticks_left: period_ticks,
2185                ud,
2186                f: h,
2187            });
2188    }
2189}
2190
2191unsafe extern "C" fn api_register_startup_grant(
2192    ctx: *mut c_void,
2193    grant: *const YogStartupGrantDef,
2194) {
2195    unsafe {
2196        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2197        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2198        let g = &*grant;
2199        let items: Vec<String> = if g.items.is_empty() {
2200            Vec::new()
2201        } else {
2202            {
2203                g.items
2204                    .as_str()
2205                    .split('|')
2206                    .map(|s: &str| s.to_owned())
2207                    .collect()
2208            }
2209        };
2210        let book = if g.book.is_empty() {
2211            None
2212        } else {
2213            Some(g.book.as_str().to_owned())
2214        };
2215        let command = if g.command.is_empty() {
2216            None
2217        } else {
2218            Some(g.command.as_str().to_owned())
2219        };
2220        handlers.startup_grants.push(yog_registry::StartupGrant {
2221            id: g.id.as_str().to_owned(),
2222            items,
2223            book,
2224            command,
2225        });
2226    }
2227}
2228
2229unsafe extern "C" fn api_register_book(ctx: *mut c_void, book_id: YogStr, book_json: YogStr) {
2230    unsafe {
2231        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2232        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2233        let id = book_id.as_str().to_owned();
2234        let json = book_json.as_str().to_owned();
2235        handlers.books.insert(id.clone(), json.clone());
2236        // Parse JSON → Book → BookRenderer so rendering works without Java round-trip.
2237        match serde_json::from_str::<yog_book::Book>(&json) {
2238            Ok(book) => {
2239                let mut renderer = yog_book::BookRenderer::new(book);
2240                // i18n books carry translation keys; shown in the game language.
2241                renderer.set_translator(translate);
2242                // Recipes may register before the book — replay them into the renderer.
2243                for (ns, name, j) in &handlers.recipes {
2244                    renderer.add_recipe(format!("{ns}:{name}"), j);
2245                }
2246                handlers.book_renderers.lock().unwrap().insert(id, renderer);
2247            }
2248            Err(e) => {
2249                yog_logging::warn!(
2250                    "book '{}': failed to parse JSON for Rust renderer: {}",
2251                    id,
2252                    e
2253                );
2254            }
2255        }
2256    }
2257}
2258
2259unsafe extern "C" fn api_register_dimension_json(ctx: *mut c_void, id: YogStr, json: YogStr) {
2260    unsafe {
2261        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2262        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2263        let id = id.as_str().to_owned();
2264        let json = json.as_str().to_owned();
2265        yog_logging::info!("registered dimension type: {}", id);
2266        handlers.dimensions.insert(id, json);
2267    }
2268}
2269
2270unsafe extern "C" fn api_register_chunk_generator(
2271    ctx: *mut c_void,
2272    id: YogStr,
2273    ud: *mut c_void,
2274    h: yog_abi::YogChunkGeneratorFn,
2275) {
2276    unsafe {
2277        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2278        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2279        let id = id.as_str().to_owned();
2280        yog_logging::info!("registered chunk generator for dimension: {}", id);
2281        handlers
2282            .chunk_generators
2283            .insert(id, (current_registration(), ud, h));
2284    }
2285}
2286
2287unsafe extern "C" fn api_register_ui(
2288    ctx: *mut c_void,
2289    ui_id: YogStr,
2290    _layout_json: YogStr,
2291    ud: *mut c_void,
2292    h: yog_abi::YogUIEventFn,
2293) {
2294    unsafe {
2295        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2296        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2297        let id = ui_id.as_str().to_owned();
2298        handlers
2299            .ui_handlers
2300            .insert(id.clone(), (current_registration(), ud, h));
2301        yog_logging::info!("registered UI handler: {}", id);
2302    }
2303}
2304
2305unsafe extern "C" fn api_on_ui_render(
2306    ctx: *mut c_void,
2307    ui_id: YogStr,
2308    ud: *mut c_void,
2309    h: YogHudRenderFn,
2310) {
2311    unsafe {
2312        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2313        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2314        let id = ui_id.as_str().to_owned();
2315        handlers
2316            .ui_render_handlers
2317            .entry(id)
2318            .or_default()
2319            .push((current_registration(), ud, h));
2320    }
2321}
2322
2323/// Strip TSV separators so one mod can't corrupt the line format.
2324fn tsv_clean(v: &str) -> String {
2325    v.replace(['\t', '\n', '\r'], " ")
2326}
2327
2328unsafe extern "C" fn api_ui_open(_ctx: *mut c_void, ui_id: YogStr, modal: bool, pause: bool) {
2329    unsafe {
2330        if ui_id.as_str().is_empty() {
2331            return;
2332        }
2333        // Java side schedules onto the render thread; no-op on dedicated servers.
2334        let _: Option<()> = bridge!("openUI", [S(ui_id), Z(modal), Z(pause)]);
2335    }
2336}
2337
2338unsafe extern "C" fn api_ui_close(_ctx: *mut c_void, ui_id: YogStr) {
2339    unsafe {
2340        // Java side defers onto the render thread (after the current event) and
2341        // checks the open screen is that Yog UI; no-op on dedicated servers.
2342        let _: Option<()> = bridge!("closeUI", [S(ui_id)]);
2343    }
2344}
2345
2346unsafe extern "C" fn api_ui_consume_event(_ctx: *mut c_void) {
2347    ui_jni::consume_event();
2348}
2349
2350unsafe extern "C" fn api_translate(_ctx: *mut c_void, key: YogStr) -> YogOwnedStr {
2351    unsafe { translate(key.as_str()).map_or(YogOwnedStr::NONE, YogOwnedStr::from_string) }
2352}
2353
2354/// The game's text for translation `key` in the current language — `None`
2355/// when no lang file has it (or there's no JVM).
2356fn translate(key: &str) -> Option<String> {
2357    if key.is_empty() {
2358        return None;
2359    }
2360    unsafe { bridge!("translate", [S(YogStr::from_str(key))]) }
2361}
2362
2363unsafe extern "C" fn api_mods_list(_ctx: *mut c_void) -> YogOwnedStr {
2364    unsafe {
2365        let mut lines: Vec<String> = Vec::new();
2366        let infos = MOD_INFOS.lock().expect("mod infos lock poisoned").clone();
2367        for m in &infos {
2368            // A `.yres` name/description shows in the player's language.
2369            let localized = |text: &str, key: &str| {
2370                translate(key).map_or_else(|| text.to_string(), |t| tsv_clean(&t))
2371            };
2372            lines.push(format!(
2373                "yog\t{}\t{}\t{}\t{}\t{}",
2374                m[0],
2375                localized(&m[1], &m[5]),
2376                m[2],
2377                m[3],
2378                localized(&m[4], &m[6])
2379            ));
2380        }
2381        // Loader-side (Java) mods, if the host provides them.
2382        let platform: Option<String> = bridge!("listPlatformMods", []);
2383        for line in platform.iter().flat_map(|s| s.lines()) {
2384            if !line.trim().is_empty() {
2385                lines.push(format!("platform\t{line}"));
2386            }
2387        }
2388        YogOwnedStr::from_string(lines.join("\n"))
2389    }
2390}
2391
2392unsafe extern "C" fn api_register_menu_entry(ctx: *mut c_void, label: YogStr, ui_id: YogStr) {
2393    unsafe {
2394        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2395        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2396        let l = label.as_str().to_owned();
2397        let u = ui_id.as_str().to_owned();
2398        handlers.menu_entries.push((l, u));
2399    }
2400}
2401
2402#[unsafe(no_mangle)]
2403pub extern "system" fn Java_dev_yog_NativeBridge_nativeBookJson<'l>(
2404    mut env: JNIEnv<'l>,
2405    _class: JClass<'l>,
2406    book_id: JString<'l>,
2407) -> jstring {
2408    let id = jstr!(env, book_id, std::ptr::null_mut());
2409    let json = handlers()
2410        .books
2411        .get(&id)
2412        .cloned()
2413        .unwrap_or_else(|| "null".to_string());
2414    env.new_string(json)
2415        .map(jni::objects::JString::into_raw)
2416        .unwrap_or(std::ptr::null_mut())
2417}
2418
2419// ── World queries & effects (ABI minor 1) ────────────────────────────────────
2420//
2421// Each of these forwards to a `NativeBridge` static of the same shape; the JNI
2422// signature is derived from the argument list so the two cannot drift apart.
2423
2424fn vec_args(v: YogVec3) -> [JArg<'static>; 3] {
2425    [JArg::D(v.x), JArg::D(v.y), JArg::D(v.z)]
2426}
2427
2428fn pos_args(p: YogBlockPos) -> [JArg<'static>; 3] {
2429    [JArg::I(p.x), JArg::I(p.y), JArg::I(p.z)]
2430}
2431
2432unsafe extern "C" fn srv_entity_info(_ctx: *mut c_void, uuid: YogStr) -> YogOwnedStr {
2433    unsafe { owned(bridge!("entityInfo", [JArg::S(uuid)])) }
2434}
2435
2436unsafe extern "C" fn srv_entities_in_box(
2437    _ctx: *mut c_void,
2438    dim: YogStr,
2439    min: YogVec3,
2440    max: YogVec3,
2441) -> YogOwnedStr {
2442    unsafe {
2443        let [a, b, c] = vec_args(min);
2444        let [d, e, f] = vec_args(max);
2445        owned(bridge!("entitiesInBox", [JArg::S(dim), a, b, c, d, e, f]))
2446    }
2447}
2448
2449unsafe extern "C" fn srv_raycast_block(
2450    _ctx: *mut c_void,
2451    dim: YogStr,
2452    from: YogVec3,
2453    to: YogVec3,
2454) -> YogOwnedStr {
2455    unsafe {
2456        let [a, b, c] = vec_args(from);
2457        let [d, e, f] = vec_args(to);
2458        owned(bridge!("raycastBlock", [JArg::S(dim), a, b, c, d, e, f]))
2459    }
2460}
2461
2462unsafe extern "C" fn srv_raycast_entity(
2463    _ctx: *mut c_void,
2464    dim: YogStr,
2465    from: YogVec3,
2466    to: YogVec3,
2467    exclude: YogStr,
2468) -> YogOwnedStr {
2469    unsafe {
2470        let [a, b, c] = vec_args(from);
2471        let [d, e, f] = vec_args(to);
2472        owned(bridge!(
2473            "raycastEntity",
2474            [JArg::S(dim), a, b, c, d, e, f, JArg::S(exclude)]
2475        ))
2476    }
2477}
2478
2479unsafe extern "C" fn srv_block_info(
2480    _ctx: *mut c_void,
2481    dim: YogStr,
2482    pos: YogBlockPos,
2483) -> YogOwnedStr {
2484    unsafe {
2485        let [x, y, z] = pos_args(pos);
2486        owned(bridge!("blockInfo", [JArg::S(dim), x, y, z]))
2487    }
2488}
2489
2490unsafe extern "C" fn srv_break_block(
2491    _ctx: *mut c_void,
2492    dim: YogStr,
2493    pos: YogBlockPos,
2494    drop: bool,
2495    breaker: YogStr,
2496) -> bool {
2497    unsafe {
2498        let [x, y, z] = pos_args(pos);
2499        bridge!(
2500            "breakBlock",
2501            [JArg::S(dim), x, y, z, JArg::Z(drop), JArg::S(breaker)]
2502        )
2503        .unwrap_or(false)
2504    }
2505}
2506
2507unsafe extern "C" fn srv_place_block_from_slot(
2508    _ctx: *mut c_void,
2509    player: YogStr,
2510    slot: u32,
2511    dim: YogStr,
2512    pos: YogBlockPos,
2513) -> bool {
2514    unsafe {
2515        let [x, y, z] = pos_args(pos);
2516        bridge!(
2517            "placeBlockFromSlot",
2518            [JArg::S(player), JArg::I(slot as i32), JArg::S(dim), x, y, z]
2519        )
2520        .unwrap_or(false)
2521    }
2522}
2523
2524unsafe extern "C" fn srv_bonemeal(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> bool {
2525    unsafe {
2526        let [x, y, z] = pos_args(pos);
2527        bridge!("bonemeal", [JArg::S(dim), x, y, z]).unwrap_or(false)
2528    }
2529}
2530
2531unsafe extern "C" fn srv_explode(
2532    _ctx: *mut c_void,
2533    dim: YogStr,
2534    pos: YogVec3,
2535    power: f32,
2536    fire: bool,
2537    source: YogStr,
2538) -> bool {
2539    unsafe {
2540        let [x, y, z] = vec_args(pos);
2541        bridge!(
2542            "explode",
2543            [
2544                JArg::S(dim),
2545                x,
2546                y,
2547                z,
2548                JArg::F(power),
2549                JArg::Z(fire),
2550                JArg::S(source),
2551            ]
2552        )
2553        .unwrap_or(false)
2554    }
2555}
2556
2557unsafe extern "C" fn srv_tellraw(_ctx: *mut c_void, player: YogStr, json: YogStr) -> bool {
2558    unsafe { bridge!("tellraw", [JArg::S(player), JArg::S(json)]).unwrap_or(false) }
2559}
2560
2561unsafe extern "C" fn srv_entity_damage(
2562    _ctx: *mut c_void,
2563    uuid: YogStr,
2564    amount: f32,
2565    damage_type: YogStr,
2566    attacker: YogStr,
2567) -> bool {
2568    unsafe {
2569        bridge!(
2570            "entityDamage",
2571            [
2572                JArg::S(uuid),
2573                JArg::F(amount),
2574                JArg::S(damage_type),
2575                JArg::S(attacker),
2576            ]
2577        )
2578        .unwrap_or(false)
2579    }
2580}
2581
2582unsafe extern "C" fn srv_entity_set_fire_ticks(
2583    _ctx: *mut c_void,
2584    uuid: YogStr,
2585    ticks: i32,
2586) -> bool {
2587    unsafe { bridge!("entitySetFireTicks", [JArg::S(uuid), JArg::I(ticks)]).unwrap_or(false) }
2588}
2589
2590unsafe extern "C" fn srv_player_add_xp(_ctx: *mut c_void, player: YogStr, amount: i32) -> bool {
2591    unsafe { bridge!("playerAddXp", [JArg::S(player), JArg::I(amount)]).unwrap_or(false) }
2592}
2593
2594unsafe extern "C" fn srv_player_set_may_fly(
2595    _ctx: *mut c_void,
2596    player: YogStr,
2597    may_fly: bool,
2598) -> bool {
2599    unsafe { bridge!("playerSetMayFly", [JArg::S(player), JArg::Z(may_fly)]).unwrap_or(false) }
2600}
2601
2602unsafe extern "C" fn srv_player_info(_ctx: *mut c_void, player: YogStr) -> YogOwnedStr {
2603    unsafe { owned(bridge!("playerInfo", [JArg::S(player)])) }
2604}
2605
2606unsafe extern "C" fn srv_spawn_item(
2607    _ctx: *mut c_void,
2608    dim: YogStr,
2609    pos: YogVec3,
2610    vel: YogVec3,
2611    item_id: YogStr,
2612    count: u32,
2613    snbt: YogStr,
2614) -> YogOwnedStr {
2615    unsafe {
2616        let [a, b, c] = vec_args(pos);
2617        let [d, e, f] = vec_args(vel);
2618        owned(bridge!(
2619            "spawnItem",
2620            [
2621                JArg::S(dim),
2622                a,
2623                b,
2624                c,
2625                d,
2626                e,
2627                f,
2628                JArg::S(item_id),
2629                JArg::I(count as i32),
2630                JArg::S(snbt),
2631            ]
2632        ))
2633    }
2634}
2635
2636unsafe extern "C" fn srv_item_entity_stack(_ctx: *mut c_void, uuid: YogStr) -> YogOwnedStr {
2637    unsafe { owned(bridge!("itemEntityStack", [JArg::S(uuid)])) }
2638}
2639
2640unsafe extern "C" fn srv_actionbar_json(_ctx: *mut c_void, player: YogStr, json: YogStr) -> bool {
2641    unsafe { bridge!("actionbarJson", [JArg::S(player), JArg::S(json)]).unwrap_or(false) }
2642}
2643
2644unsafe extern "C" fn srv_use_item_on_block(
2645    _ctx: *mut c_void,
2646    dim: YogStr,
2647    pos: YogBlockPos,
2648    face: u8,
2649    item_id: YogStr,
2650    player: YogStr,
2651) -> bool {
2652    unsafe {
2653        let [x, y, z] = pos_args(pos);
2654        bridge!(
2655            "useItemOnBlock",
2656            [
2657                JArg::S(dim),
2658                x,
2659                y,
2660                z,
2661                JArg::I(face as i32),
2662                JArg::S(item_id),
2663                JArg::S(player),
2664            ]
2665        )
2666        .unwrap_or(false)
2667    }
2668}
2669
2670unsafe extern "C" fn srv_pour_fluid(
2671    _ctx: *mut c_void,
2672    dim: YogStr,
2673    pos: YogBlockPos,
2674    bucket_item_id: YogStr,
2675) -> bool {
2676    unsafe {
2677        let [x, y, z] = pos_args(pos);
2678        bridge!(
2679            "pourFluid",
2680            [JArg::S(dim), x, y, z, JArg::S(bucket_item_id)]
2681        )
2682        .unwrap_or(false)
2683    }
2684}
2685
2686unsafe extern "C" fn srv_has_entity_tag(
2687    _ctx: *mut c_void,
2688    entity_type: YogStr,
2689    tag: YogStr,
2690) -> bool {
2691    unsafe { bridge!("hasEntityTag", [JArg::S(entity_type), JArg::S(tag)]).unwrap_or(false) }
2692}
2693
2694unsafe extern "C" fn srv_item_block(_ctx: *mut c_void, item_id: YogStr) -> YogOwnedStr {
2695    unsafe { owned(bridge!("itemBlock", [JArg::S(item_id)])) }
2696}
2697
2698unsafe extern "C" fn srv_place_feature(
2699    _ctx: *mut c_void,
2700    dim: YogStr,
2701    pos: YogBlockPos,
2702    feature_id: YogStr,
2703) -> bool {
2704    unsafe {
2705        let [x, y, z] = pos_args(pos);
2706        bridge!("placeFeature", [JArg::S(dim), x, y, z, JArg::S(feature_id)]).unwrap_or(false)
2707    }
2708}
2709
2710unsafe extern "C" fn srv_item_entity_set_stack(
2711    _ctx: *mut c_void,
2712    uuid: YogStr,
2713    item_id: YogStr,
2714    count: u32,
2715    snbt: YogStr,
2716) -> bool {
2717    unsafe {
2718        bridge!(
2719            "itemEntitySetStack",
2720            [
2721                JArg::S(uuid),
2722                JArg::S(item_id),
2723                JArg::I(count as i32),
2724                JArg::S(snbt),
2725            ]
2726        )
2727        .unwrap_or(false)
2728    }
2729}
2730
2731// ── Table constructors ────────────────────────────────────────────────────────
2732
2733fn build_server_table() -> YogServer {
2734    YogServer {
2735        ctx: std::ptr::null_mut(),
2736        abi_version: ABI_VERSION,
2737        size: std::mem::size_of::<YogServer>() as u32,
2738        free_str: yog_free_str,
2739        broadcast: srv_broadcast,
2740        get_block: srv_get_block,
2741        set_block: srv_set_block,
2742        world_time: srv_world_time,
2743        set_time: srv_set_time,
2744        is_raining: srv_is_raining,
2745        set_weather: srv_set_weather,
2746        give_item: srv_give_item,
2747        player_teleport: srv_player_teleport,
2748        send_to_player: srv_send_to_player,
2749        send_to_server: srv_send_to_server,
2750        kick_player: srv_kick_player,
2751        set_gamemode: srv_set_gamemode,
2752        send_title: srv_send_title,
2753        send_actionbar: srv_send_actionbar,
2754        play_sound: srv_play_sound,
2755        play_sound_player: srv_play_sound_player,
2756        entity_teleport: srv_entity_teleport,
2757        entity_position: srv_entity_position,
2758        entity_rotation: srv_entity_rotation,
2759        entity_health: srv_entity_health,
2760        entity_set_health: srv_entity_set_health,
2761        entity_kill: srv_entity_kill,
2762        spawn_entity: srv_spawn_entity,
2763        entity_add_effect: srv_entity_add_effect,
2764        entity_remove_effect: srv_entity_remove_effect,
2765        entity_clear_effects: srv_entity_clear_effects,
2766        entity_velocity: srv_entity_velocity,
2767        entity_set_velocity: srv_entity_set_velocity,
2768        entity_add_velocity: srv_entity_add_velocity,
2769        has_item_tag: srv_has_item_tag,
2770        has_block_tag: srv_has_block_tag,
2771        drop_loot: srv_drop_loot,
2772        scoreboard_get: srv_scoreboard_get,
2773        scoreboard_set: srv_scoreboard_set,
2774        scoreboard_add: srv_scoreboard_add,
2775        bossbar_create: srv_bossbar_create,
2776        bossbar_remove: srv_bossbar_remove,
2777        bossbar_set_title: srv_bossbar_set_title,
2778        bossbar_set_progress: srv_bossbar_set_progress,
2779        bossbar_set_color: srv_bossbar_set_color,
2780        bossbar_add_player: srv_bossbar_add_player,
2781        bossbar_remove_player: srv_bossbar_remove_player,
2782        bossbar_set_visible: srv_bossbar_set_visible,
2783        game_dir: srv_game_dir,
2784        get_block_nbt: srv_get_block_nbt,
2785        set_block_nbt: srv_set_block_nbt,
2786        get_inventory_slot: srv_get_inventory_slot,
2787        set_inventory_slot: srv_set_inventory_slot,
2788        player_inventory: srv_player_inventory,
2789        player_set_slot: srv_player_set_slot,
2790        player_teleport_dim: srv_player_teleport_dim,
2791        entity_teleport_dim: srv_entity_teleport_dim,
2792        online_players: srv_online_players,
2793        world_entity_count: srv_world_entity_count,
2794        entity_get_nbt: srv_entity_get_nbt,
2795        entity_set_nbt: srv_entity_set_nbt,
2796        spawn_particles: srv_spawn_particles,
2797        entity_attribute_get: srv_entity_attribute_get,
2798        entity_attribute_set: srv_entity_attribute_set,
2799        get_held_item_nbt: srv_get_held_item_nbt,
2800        set_held_item_nbt: srv_set_held_item_nbt,
2801        get_offhand_item_nbt: srv_get_offhand_item_nbt,
2802        set_offhand_item_nbt: srv_set_offhand_item_nbt,
2803        get_slot_item: srv_get_slot_item,
2804        set_slot_item: srv_set_slot_item,
2805        entity_info: srv_entity_info,
2806        entities_in_box: srv_entities_in_box,
2807        raycast_block: srv_raycast_block,
2808        raycast_entity: srv_raycast_entity,
2809        block_info: srv_block_info,
2810        break_block: srv_break_block,
2811        place_block_from_slot: srv_place_block_from_slot,
2812        bonemeal: srv_bonemeal,
2813        explode: srv_explode,
2814        tellraw: srv_tellraw,
2815        entity_damage: srv_entity_damage,
2816        entity_set_fire_ticks: srv_entity_set_fire_ticks,
2817        player_add_xp: srv_player_add_xp,
2818        player_set_may_fly: srv_player_set_may_fly,
2819        player_info: srv_player_info,
2820        spawn_item: srv_spawn_item,
2821        item_entity_stack: srv_item_entity_stack,
2822        item_entity_set_stack: srv_item_entity_set_stack,
2823        actionbar_json: srv_actionbar_json,
2824        use_item_on_block: srv_use_item_on_block,
2825        pour_fluid: srv_pour_fluid,
2826        has_entity_tag: srv_has_entity_tag,
2827        item_block: srv_item_block,
2828        place_feature: srv_place_feature,
2829        set_item_cooldown: srv_set_item_cooldown,
2830        entity_teleport_with_passengers: srv_entity_teleport_with_passengers,
2831        redstone_power: srv_redstone_power,
2832        set_redstone_output: srv_set_redstone_output,
2833        get_block_state: srv_get_block_state,
2834        set_block_state: srv_set_block_state,
2835        world_is_thundering: srv_world_is_thundering,
2836        world_set_thunder: srv_world_set_thunder,
2837        fill_region: srv_fill_region,
2838        scan_region: srv_scan_region,
2839    }
2840}
2841
2842// ── ABI minor 3 ──────────────────────────────────────────────────────────────
2843
2844unsafe extern "C" fn srv_set_item_cooldown(_ctx: *mut c_void, player: YogStr, item_id: YogStr, ticks: i32) -> bool {
2845    unsafe { bridge!("setItemCooldown", [S(player), S(item_id), I(ticks)]).unwrap_or(false) }
2846}
2847
2848unsafe extern "C" fn srv_entity_teleport_with_passengers(_ctx: *mut c_void, uuid: YogStr, dim: YogStr, pos: YogVec3) -> bool {
2849    unsafe {
2850        bridge!("entityTeleportWithPassengers", [S(uuid), S(dim), D(pos.x), D(pos.y), D(pos.z)]).unwrap_or(false)
2851    }
2852}
2853
2854unsafe extern "C" fn srv_redstone_power(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> i32 {
2855    unsafe { bridge!("redstonePower", [S(dim), I(pos.x), I(pos.y), I(pos.z)]).unwrap_or(-1) }
2856}
2857
2858unsafe extern "C" fn srv_set_redstone_output(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos, level: i32) -> bool {
2859    unsafe { bridge!("setRedstoneOutput", [S(dim), I(pos.x), I(pos.y), I(pos.z), I(level)]).unwrap_or(false) }
2860}
2861
2862unsafe extern "C" fn srv_get_block_state(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> YogOwnedStr {
2863    unsafe { owned(bridge!("getBlockState", [S(dim), I(pos.x), I(pos.y), I(pos.z)])) }
2864}
2865
2866unsafe extern "C" fn srv_set_block_state(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos, state: YogStr) -> bool {
2867    unsafe { bridge!("setBlockState", [S(dim), I(pos.x), I(pos.y), I(pos.z), S(state)]).unwrap_or(false) }
2868}
2869
2870unsafe extern "C" fn srv_world_is_thundering(_ctx: *mut c_void, dim: YogStr) -> bool {
2871    unsafe { bridge!("worldIsThundering", [S(dim)]).unwrap_or(false) }
2872}
2873
2874unsafe extern "C" fn srv_world_set_thunder(_ctx: *mut c_void, dim: YogStr, thundering: bool, duration_ticks: i32) -> bool {
2875    unsafe { bridge!("worldSetThunder", [S(dim), Z(thundering), I(duration_ticks)]).unwrap_or(false) }
2876}
2877
2878unsafe extern "C" fn srv_fill_region(
2879    _ctx: *mut c_void,
2880    dim: YogStr,
2881    from: YogBlockPos,
2882    to: YogBlockPos,
2883    state: YogStr,
2884) -> i32 {
2885    unsafe {
2886        bridge!(
2887            "fillRegion",
2888            [S(dim), I(from.x), I(from.y), I(from.z), I(to.x), I(to.y), I(to.z), S(state)]
2889        )
2890        .unwrap_or(-1)
2891    }
2892}
2893
2894unsafe extern "C" fn srv_scan_region(_ctx: *mut c_void, dim: YogStr, from: YogBlockPos, to: YogBlockPos) -> YogOwnedStr {
2895    unsafe { owned(bridge!("scanRegion", [S(dim), I(from.x), I(from.y), I(from.z), I(to.x), I(to.y), I(to.z)])) }
2896}
2897
2898// ── Inter-mod communication ──────────────────────────────────────────────────
2899
2900unsafe extern "C" fn interop_export_impl(
2901    _ctx: *mut c_void,
2902    mod_id: YogStr,
2903    symbol: YogStr,
2904    ptr: *const c_void,
2905) {
2906    unsafe {
2907        let mod_id = mod_id.as_str();
2908        let sym = symbol.as_str();
2909        let registry = INTEROP_SYMBOLS.get_or_init(|| Mutex::new(HashMap::new()));
2910        registry
2911            .lock()
2912            .expect("interop symbols lock poisoned")
2913            .entry(mod_id.to_string())
2914            .or_default()
2915            .insert(sym.to_string(), ptr as usize);
2916    }
2917}
2918
2919unsafe extern "C" fn interop_import_impl(
2920    _ctx: *mut c_void,
2921    mod_id: YogStr,
2922    symbol: YogStr,
2923) -> *const c_void {
2924    unsafe {
2925        let mod_id = mod_id.as_str();
2926        let sym = symbol.as_str();
2927        let registry = INTEROP_SYMBOLS.get_or_init(|| Mutex::new(HashMap::new()));
2928        registry
2929            .lock()
2930            .expect("interop symbols lock poisoned")
2931            .get(mod_id)
2932            .and_then(|m: &HashMap<String, usize>| m.get(sym))
2933            .map(|&ptr| ptr as *const c_void)
2934            .unwrap_or(std::ptr::null())
2935    }
2936}
2937
2938fn build_api_table(ctx: *const RwLock<RuntimeHandlers>, server: *const YogServer) -> YogApi {
2939    YogApi {
2940        abi_version: ABI_VERSION,
2941        size: std::mem::size_of::<YogApi>() as u32,
2942        ctx: ctx as *mut c_void,
2943        server,
2944        on_block_break: api_on_block_break,
2945        on_chat: api_on_chat,
2946        on_player_join: api_on_player_join,
2947        on_player_leave: api_on_player_leave,
2948        on_use_item: api_on_use_item,
2949        on_use_block: api_on_use_block,
2950        on_attack_entity: api_on_attack_entity,
2951        on_entity_damage: api_on_entity_damage,
2952        on_entity_death: api_on_entity_death,
2953        on_entity_spawn: api_on_entity_spawn,
2954        on_player_place_block: api_on_player_place_block,
2955        on_player_death: api_on_player_death,
2956        on_player_respawn: api_on_player_respawn,
2957        on_advancement: api_on_advancement,
2958        on_entity_interact: api_on_entity_interact,
2959        on_item_craft: api_on_item_craft,
2960        on_explosion: api_on_explosion,
2961        on_item_pickup: api_on_item_pickup,
2962        on_player_move: api_on_player_move,
2963        on_container_open: api_on_container_open,
2964        on_container_close: api_on_container_close,
2965        on_projectile_hit: api_on_projectile_hit,
2966        on_server_tick: api_on_server_tick,
2967        on_server_started: api_on_server_started,
2968        on_server_stopping: api_on_server_stopping,
2969        on_packet: api_on_packet,
2970        on_client_packet: api_on_client_packet,
2971        register_command: api_register_command,
2972        register_typed_command: api_register_typed_command,
2973        register_recipe_json: api_register_recipe_json,
2974        register_item: api_register_item,
2975        register_block: api_register_block,
2976        schedule_once: api_schedule_once,
2977        schedule_repeating: api_schedule_repeating,
2978        on_client_tick: api_on_client_tick,
2979        on_hud_render: api_on_hud_render,
2980        on_key_press: api_on_key_press,
2981        on_screen_open: api_on_screen_open,
2982        on_screen_close: api_on_screen_close,
2983        on_world_render: api_on_world_render,
2984        register_startup_grant: api_register_startup_grant,
2985        register_book: api_register_book,
2986        register_ui: api_register_ui,
2987        on_ui_render: api_on_ui_render,
2988        register_menu_entry: api_register_menu_entry,
2989        mods_list: api_mods_list,
2990        free_str: yog_free_str,
2991        ui_open: api_ui_open,
2992        register_inventory: api_register_inventory,
2993        interop_export: interop_export_impl,
2994        interop_import: interop_import_impl,
2995        register_dimension_json: api_register_dimension_json,
2996        register_chunk_generator: api_register_chunk_generator,
2997        ui_close: api_ui_close,
2998        ui_consume_event: api_ui_consume_event,
2999        translate: api_translate,
3000        ui_open_flags: client_api::api_ui_open_flags,
3001        client_play_sound: client_api::api_client_play_sound,
3002        client_stop_sound: client_api::api_client_stop_sound,
3003        client_held_item: client_api::api_client_held_item,
3004        client_key_down: client_api::api_client_key_down,
3005    }
3006}
3007
3008// ── Mod loading ───────────────────────────────────────────────────────────────
3009
3010fn platform_tag() -> String {
3011    format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
3012}
3013
3014type AbiVersionFn = unsafe extern "C" fn() -> u32;
3015/// Second arg is a null-terminated C string: the mod's `id` from its manifest.
3016type RegisterFn = unsafe extern "C" fn(*const YogApi, *const std::os::raw::c_char);
3017
3018fn load_mods(dir: &Path, api: &YogApi) {
3019    let entries = match std::fs::read_dir(dir) {
3020        Ok(e) => e,
3021        Err(_) => {
3022            yog_logging::info!("no mods directory at {} — none loaded", dir.display());
3023            return;
3024        }
3025    };
3026
3027    // First pass: collect (mod_id, dependencies, archive_path)
3028    struct ModMeta {
3029        id: String,
3030        deps: Vec<String>,
3031        yog_path: PathBuf,
3032    }
3033    let mut metas: Vec<ModMeta> = Vec::new();
3034    for entry in entries.flatten() {
3035        let path = entry.path();
3036        if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3037            continue;
3038        }
3039        let info = read_mod_info(&path);
3040        let deps = read_mod_deps(&path);
3041        metas.push(ModMeta {
3042            id: info[0].clone(),
3043            deps,
3044            yog_path: path,
3045        });
3046    }
3047
3048    // Topological sort by dependencies (Kahn's algorithm)
3049    let ids: Vec<String> = metas.iter().map(|m| m.id.clone()).collect();
3050    let deps_list: Vec<Vec<String>> = metas.iter().map(|m| m.deps.clone()).collect();
3051    let sorted = match topo_sort_mods(&ids, &deps_list) {
3052        Some(v) => v,
3053        None => {
3054            yog_logging::error!("circular dependency detected — loading in file order");
3055            (0..metas.len()).collect()
3056        }
3057    };
3058
3059    let mut loaded: Vec<LoadedModule> = Vec::new();
3060    for idx in sorted {
3061        let meta = &metas[idx];
3062        let lib_path = match extract_yog(&meta.yog_path) {
3063            Some(p) => p,
3064            None => {
3065                yog_logging::error!(
3066                    "no native for {} in {}",
3067                    platform_tag(),
3068                    meta.yog_path.display()
3069                );
3070                continue;
3071            }
3072        };
3073        if let Some(module) = load_mod_lib(&lib_path, api, &meta.id) {
3074            loaded.push(module);
3075            MOD_INFOS
3076                .lock()
3077                .expect("mod infos lock poisoned")
3078                .push(read_mod_info(&meta.yog_path));
3079        }
3080    }
3081    yog_logging::info!("loaded {} mod(s) from {}", loaded.len(), dir.display());
3082
3083    // Final import resolution pass — after all mods loaded, each mod's pending
3084    // imports can be resolved against the now-complete interop table.
3085    resolve_all_imports(api, &loaded);
3086}
3087
3088/// After all mods loaded, call `__yog_resolve_imports_final` on each library
3089/// to resolve any imports that weren't satisfied during initial registration.
3090fn resolve_all_imports(api: &YogApi, loaded: &[LoadedModule]) {
3091    for module in loaded {
3092        module.with_library(|lib| unsafe {
3093            type ResolveFn = unsafe extern "C" fn(*const YogApi);
3094            if let Ok(resolve) = lib.get::<ResolveFn>(b"__yog_resolve_imports_final") {
3095                resolve(api as *const YogApi);
3096            }
3097        });
3098    }
3099}
3100
3101/// Kahn's algorithm — returns indices in dependency order, or None on cycle.
3102fn topo_sort_mods(ids: &[String], deps_list: &[Vec<String>]) -> Option<Vec<usize>> {
3103    let n = ids.len();
3104    let name_to_idx: HashMap<&str, usize> = ids
3105        .iter()
3106        .enumerate()
3107        .map(|(i, id)| (id.as_str(), i))
3108        .collect();
3109    let mut in_degree = vec![0u32; n];
3110    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
3111    for (i, deps) in deps_list.iter().enumerate() {
3112        for dep in deps {
3113            if let Some(&j) = name_to_idx.get(dep.as_str()) {
3114                adj[j].push(i);
3115                in_degree[i] += 1;
3116            }
3117        }
3118    }
3119    let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
3120    let mut order = Vec::with_capacity(n);
3121    while let Some(u) = queue.pop() {
3122        order.push(u);
3123        for &v in &adj[u] {
3124            in_degree[v] -= 1;
3125            if in_degree[v] == 0 {
3126                queue.push(v);
3127            }
3128        }
3129    }
3130    if order.len() == n { Some(order) } else { None }
3131}
3132
3133/// Loads `path` as a fresh generation of `mod_id` via the shared
3134/// `HotReloader`, validates its ABI, and calls its `yog_mod_register` —
3135/// tagging every handler it registers with the returned `LoadedModule` (see
3136/// `current_registration()`). On any failure the module (if it got as far
3137/// as being tracked by the `HotReloader`) is retired immediately: nothing
3138/// can be executing inside a library that never successfully registered,
3139/// so this always hard-unloads right away rather than lingering.
3140fn load_mod_lib(path: &Path, api: &YogApi, mod_id: &str) -> Option<LoadedModule> {
3141    let module = match unsafe { hot_reloader().load_initial(mod_id, path) } {
3142        Ok(m) => m,
3143        Err(e) => {
3144            yog_logging::error!("failed to load {}: {}", path.display(), e);
3145            return None;
3146        }
3147    };
3148
3149    let abi = module.with_library(|lib| unsafe {
3150        let abi: Symbol<AbiVersionFn> = lib.get(b"yog_abi_version").ok()?;
3151        Some(abi())
3152    });
3153    match abi.flatten() {
3154        Some(v) if v == ABI_VERSION => {}
3155        Some(v) => {
3156            yog_logging::error!(
3157                "{}: ABI {} incompatible with runtime ABI {}",
3158                path.display(),
3159                v,
3160                ABI_VERSION
3161            );
3162            module.retire(|| {});
3163            return None;
3164        }
3165        None => {
3166            yog_logging::error!("{} is not a Yog mod (no yog_abi_version)", path.display());
3167            module.retire(|| {});
3168            return None;
3169        }
3170    }
3171
3172    if let Err(e) = invoke_mod_register(&module, api, mod_id) {
3173        yog_logging::error!("{}: {}", path.display(), e);
3174        module.retire(|| {});
3175        return None;
3176    }
3177
3178    Some(module)
3179}
3180
3181/// Calls a loaded module's `yog_mod_register`, tagging everything it
3182/// registers with `module` via `CURRENT_REGISTRATION` for the duration of
3183/// the call — used for both a mod's initial load and a later hot-reload.
3184fn invoke_mod_register(module: &LoadedModule, api: &YogApi, mod_id: &str) -> Result<(), String> {
3185    module
3186        .with_library(|lib| unsafe {
3187            let register: Symbol<RegisterFn> = lib
3188                .get(b"yog_mod_register")
3189                .map_err(|_| "missing yog_mod_register".to_string())?;
3190            let mod_id_c = std::ffi::CString::new(mod_id).unwrap();
3191            *CURRENT_REGISTRATION.lock().unwrap() = Some(module.clone());
3192            register(api as *const YogApi, mod_id_c.as_ptr());
3193            *CURRENT_REGISTRATION.lock().unwrap() = None;
3194            Ok(())
3195        })
3196        .unwrap_or_else(|| Err("module was unloaded before registration could run".to_string()))
3197}
3198
3199/// Metadata for a mod file: [id, name, version, authors, description,
3200/// name_key, description_key].
3201/// For .yog archives this comes from the bundled yog.toml; bare native libs
3202/// fall back to the file stem.
3203fn read_mod_info(path: &Path) -> [String; 7] {
3204    let stem = path
3205        .file_stem()
3206        .map(|s| s.to_string_lossy().into_owned())
3207        .unwrap_or_default();
3208    let mut info = [
3209        stem.clone(),
3210        stem,
3211        String::new(),
3212        String::new(),
3213        String::new(),
3214        String::new(),
3215        String::new(),
3216    ];
3217    if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3218        return info;
3219    }
3220    let Some(text) = (|| {
3221        let file = std::fs::File::open(path).ok()?;
3222        let mut archive = zip::ZipArchive::new(file).ok()?;
3223        let mut entry = archive.by_name("yog.toml").ok()?;
3224        let mut text = String::new();
3225        std::io::Read::read_to_string(&mut entry, &mut text).ok()?;
3226        Some(text)
3227    })() else {
3228        return info;
3229    };
3230    // Minimal parse — enough for id/name/version/description/authors without
3231    // pulling in a TOML dependency. Source manifests keep these under [mod];
3232    // the packaged manifest (written by yog-cli) has them at top level, so
3233    // keys before any section header count too.
3234    let mut in_mod = true;
3235    for line in text.lines() {
3236        let line = line.trim();
3237        if line.starts_with('[') {
3238            in_mod = line == "[mod]";
3239            continue;
3240        }
3241        if !in_mod {
3242            continue;
3243        }
3244        let Some((key, value)) = line.split_once('=') else {
3245            continue;
3246        };
3247        let key = key.trim();
3248        let value = value.trim();
3249        let unquote = |v: &str| v.trim().trim_matches('"').to_string();
3250        match key {
3251            "id" => info[0] = tsv_clean(&unquote(value)),
3252            "name" => info[1] = tsv_clean(&unquote(value)),
3253            "version" => info[2] = tsv_clean(&unquote(value)),
3254            "description" => info[4] = tsv_clean(&unquote(value)),
3255            "name_key" => info[5] = unquote(value),
3256            "description_key" => info[6] = unquote(value),
3257            "authors" => {
3258                let list = value.trim_start_matches('[').trim_end_matches(']');
3259                let authors: Vec<String> = list
3260                    .split(',')
3261                    .map(unquote)
3262                    .filter(|a| !a.is_empty())
3263                    .collect();
3264                info[3] = tsv_clean(&authors.join(", "));
3265            }
3266            _ => {}
3267        }
3268    }
3269    info
3270}
3271
3272/// Parse `[dependencies]` section from yog.toml inside a .yog archive.
3273fn read_mod_deps(path: &Path) -> Vec<String> {
3274    if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3275        return Vec::new();
3276    }
3277    let text = (|| {
3278        let file = std::fs::File::open(path).ok()?;
3279        let mut archive = zip::ZipArchive::new(file).ok()?;
3280        let mut entry = archive.by_name("yog.toml").ok()?;
3281        let mut text = String::new();
3282        std::io::Read::read_to_string(&mut entry, &mut text).ok()?;
3283        Some(text)
3284    })();
3285    let Some(text) = text else { return Vec::new() };
3286    let mut deps = Vec::new();
3287    let mut in_deps = false;
3288    for line in text.lines() {
3289        let line = line.trim();
3290        if line.starts_with('[') {
3291            in_deps = line == "[dependencies]";
3292            continue;
3293        }
3294        if !in_deps {
3295            continue;
3296        }
3297        // `mod_id = "1.0"` or `mod_id = "*"` — we only care about the key (mod_id)
3298        if let Some((key, _)) = line.split_once('=') {
3299            deps.push(key.trim().to_string());
3300        }
3301    }
3302    deps
3303}
3304
3305fn extract_yog(path: &Path) -> Option<PathBuf> {
3306    let file = std::fs::File::open(path).ok()?;
3307    let mut archive = zip::ZipArchive::new(file).ok()?;
3308    let prefix = format!("natives/{}/", platform_tag());
3309    let mut entry_name = None;
3310    for i in 0..archive.len() {
3311        let f = archive.by_index(i).ok()?;
3312        let name = f.name().ok()?;
3313        if name.starts_with(&prefix) && !name.ends_with('/') {
3314            entry_name = Some(name.to_string());
3315            break;
3316        }
3317    }
3318    let entry_name = entry_name?;
3319    let ext = Path::new(&entry_name)
3320        .extension()
3321        .and_then(|e| e.to_str())
3322        .unwrap_or("bin");
3323    let stem = path.file_stem()?.to_string_lossy().into_owned();
3324    // A fresh name per extraction: a hot reload must never truncate the
3325    // library the game still has mapped, and `dlopen` of an already-loaded
3326    // path would just hand back the old module.
3327    static EXTRACTIONS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
3328    let n = EXTRACTIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3329    let out =
3330        std::env::temp_dir().join(format!("yog-{}-{}-{}.{}", stem, std::process::id(), n, ext));
3331    let mut entry = archive.by_name(&entry_name).ok()?;
3332    let mut out_file = std::fs::File::create(&out).ok()?;
3333    std::io::copy(&mut entry, &mut out_file).ok()?;
3334    Some(out)
3335}
3336
3337// ── Dispatcher helpers ────────────────────────────────────────────────────────
3338
3339fn srv_ptr() -> *const YogServer {
3340    SERVER.get().expect("yog: SERVER not initialised") as *const YogServer
3341}
3342
3343// ── JNI entry points ──────────────────────────────────────────────────────────
3344
3345#[unsafe(no_mangle)]
3346pub extern "system" fn Java_dev_yog_NativeBridge_nativeInit<'l>(
3347    mut env: JNIEnv<'l>,
3348    _class: JClass<'l>,
3349    mods_dir: JString<'l>,
3350) {
3351    if let Ok(vm) = env.get_java_vm() {
3352        let _ = JAVA_VM.set(vm);
3353    }
3354
3355    let dir = env
3356        .get_string(&mods_dir)
3357        .map(String::from)
3358        .unwrap_or_default();
3359
3360    // Build YogServer and store in static (gets a stable address).
3361    let _ = SERVER.set(build_server_table());
3362    let server_ptr = SERVER.get().unwrap() as *const YogServer;
3363
3364    // Publish HANDLERS *before* any mod registers — its address is then
3365    // `'static`-stable for the runtime's entire lifetime, unlike the old
3366    // design (a temporary heap `Box` moved into `HANDLERS` only after
3367    // `nativeInit` returned, silently invalidating the `ctx` pointer any
3368    // later re-registration — e.g. a hot-reload — would have needed).
3369    let handlers_lock = HANDLERS.get_or_init(|| RwLock::new(RuntimeHandlers::default()));
3370    let handlers_ptr = handlers_lock as *const RwLock<RuntimeHandlers>;
3371
3372    let api: &'static YogApi = API_TABLE.get_or_init(|| build_api_table(handlers_ptr, server_ptr));
3373
3374    let _ = HOT_RELOADER.get_or_init(|| HotReloader::new(RuntimeModuleRegistry));
3375
3376    guard("mod loading", || {
3377        load_mods(Path::new(&dir), api);
3378    });
3379
3380    control_socket::start_if_requested();
3381
3382    yog_logging::info!("runtime initialised — the gate is open.");
3383}
3384
3385#[unsafe(no_mangle)]
3386pub extern "system" fn Java_dev_yog_NativeBridge_nativeHotReload<'l>(
3387    mut env: JNIEnv<'l>,
3388    _class: JClass<'l>,
3389    mod_id: JString<'l>,
3390    new_yog_path: JString<'l>,
3391) -> jni::sys::jboolean {
3392    let Ok(mod_id) = env.get_string(&mod_id).map(String::from) else {
3393        return 0;
3394    };
3395    let Ok(yog_path) = env.get_string(&new_yog_path).map(String::from) else {
3396        return 0;
3397    };
3398    let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3399        trigger_hot_reload(&mod_id, Path::new(&yog_path))
3400    }))
3401    .unwrap_or(false);
3402    ok as jni::sys::jboolean
3403}
3404
3405/// Rebuilds and re-registers `mod_id` from a freshly built `.yog` at
3406/// `new_yog_path`: extracts the current platform's native, loads it as a
3407/// new generation via the shared `HotReloader` (which retires the previous
3408/// generation per its hybrid soft/hard-unload policy — hard-unloaded right
3409/// away if nothing's mid-call, deferred otherwise), then calls its
3410/// `yog_mod_register` again so its handlers get tagged under the new
3411/// generation. How this actually gets triggered — a keybind, a file-watch,
3412/// an IDE-driven socket — is a separate, later concern; this only needs to
3413/// work correctly once called.
3414fn trigger_hot_reload(mod_id: &str, new_yog_path: &Path) -> bool {
3415    let Some(native_path) = extract_yog(new_yog_path) else {
3416        yog_logging::error!(
3417            "hot-reload {}: no native for {} in {}",
3418            mod_id,
3419            platform_tag(),
3420            new_yog_path.display()
3421        );
3422        return false;
3423    };
3424
3425    let api = API_TABLE.get().expect("yog: nativeInit not called yet");
3426
3427    let module = match unsafe { hot_reloader().reload(mod_id, &native_path) } {
3428        Ok(m) => m,
3429        Err(e) => {
3430            yog_logging::error!(
3431                "hot-reload {}: failed to load {}: {}",
3432                mod_id,
3433                native_path.display(),
3434                e
3435            );
3436            return false;
3437        }
3438    };
3439
3440    let abi = module.with_library(|lib| unsafe {
3441        let abi: Symbol<AbiVersionFn> = lib.get(b"yog_abi_version").ok()?;
3442        Some(abi())
3443    });
3444    match abi.flatten() {
3445        Some(v) if v == ABI_VERSION => {}
3446        other => {
3447            yog_logging::error!(
3448                "hot-reload {}: ABI {:?} incompatible with runtime ABI {} — mod has no active handlers until a compatible reload succeeds",
3449                mod_id,
3450                other,
3451                ABI_VERSION
3452            );
3453            return false;
3454        }
3455    }
3456
3457    if let Err(e) = invoke_mod_register(&module, api, mod_id) {
3458        yog_logging::error!("hot-reload {}: {}", mod_id, e);
3459        return false;
3460    }
3461
3462    yog_logging::info!("hot-reloaded mod {}", mod_id);
3463    true
3464}
3465
3466#[unsafe(no_mangle)]
3467pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnBlockBreak<'l>(
3468    mut env: JNIEnv<'l>,
3469    _class: JClass<'l>,
3470    player: JString<'l>,
3471    block: JString<'l>,
3472    x: jint,
3473    y: jint,
3474    z: jint,
3475    dimension: JString<'l>,
3476) {
3477    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
3478    let ev = yog_abi::YogBlockBreakEvent {
3479        player: YogStr::from_str(&p),
3480        block: YogStr::from_str(&b),
3481        pos: YogBlockPos { x, y, z },
3482        dimension: YogStr::from_str(&d),
3483    };
3484    let srv = srv_ptr();
3485    guard("on_block_break", || {
3486        each(&handlers().block_break, |ud, f| unsafe {
3487            f(ud, srv, &ev, 1)
3488        });
3489    });
3490}
3491
3492#[unsafe(no_mangle)]
3493pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnChat<'l>(
3494    mut env: JNIEnv<'l>,
3495    _class: JClass<'l>,
3496    player: JString<'l>,
3497    message: JString<'l>,
3498    dimension: JString<'l>,
3499) {
3500    let (p, m, d) = (
3501        jstr!(env, player),
3502        jstr!(env, message),
3503        jstr!(env, dimension),
3504    );
3505    let ev = yog_abi::YogChatEvent {
3506        player: YogStr::from_str(&p),
3507        message: YogStr::from_str(&m),
3508        dimension: YogStr::from_str(&d),
3509    };
3510    let srv = srv_ptr();
3511    guard("on_chat", || {
3512        each(&handlers().chat, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3513    });
3514}
3515
3516#[unsafe(no_mangle)]
3517pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerJoin<'l>(
3518    mut env: JNIEnv<'l>,
3519    _class: JClass<'l>,
3520    player: JString<'l>,
3521    uuid: JString<'l>,
3522    dimension: JString<'l>,
3523) {
3524    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
3525    let ev = yog_abi::YogPlayerEvent {
3526        player: YogStr::from_str(&p),
3527        uuid: YogStr::from_str(&u),
3528        dimension: YogStr::from_str(&d),
3529    };
3530    let srv = srv_ptr();
3531    guard("on_player_join", || {
3532        each(&handlers().player_join, |ud, f| unsafe {
3533            f(ud, srv, &ev, 1)
3534        });
3535    });
3536    // Queue startup grants: giving items directly inside the join callback
3537    // fails for the host of a freshly created world (player not fully in the
3538    // world yet), so grants are processed a few server ticks later.
3539    let h = handlers();
3540    if !h.startup_grants.is_empty() {
3541        h.pending_grants
3542            .lock()
3543            .expect("pending_grants poisoned")
3544            .push((p, u, 0));
3545    }
3546}
3547
3548/// The command tag a player carries once startup grant `id` was given. It's
3549/// saved with the player's own data, so it follows the player — not their
3550/// name or uuid (a dev client gets a new name each launch, while a
3551/// singleplayer world's owner stays the same player).
3552fn grant_tag(id: &str) -> String {
3553    format!("yog.grant.{id}")
3554}
3555
3556/// Try to fulfil pending startup grants. Called each server tick; a grant is
3557/// only marked as given (tagged on the player) after `give_item` succeeds.
3558fn process_pending_grants() {
3559    let h = handlers();
3560    let mut pending = match h.pending_grants.try_lock() {
3561        Ok(g) => g,
3562        Err(_) => return,
3563    };
3564    if pending.is_empty() {
3565        return;
3566    }
3567    // Wait a couple of ticks after join before the first attempt.
3568    const FIRST_TRY_TICK: u32 = 2;
3569    // Give up after 30 seconds — the player probably left.
3570    const MAX_TICKS: u32 = 600;
3571
3572    let mut still_pending = Vec::new();
3573    for (p, u, ticks) in pending.drain(..) {
3574        if ticks < FIRST_TRY_TICK {
3575            still_pending.push((p, u, ticks + 1));
3576            continue;
3577        }
3578        let player = YogStr::from_str(&p);
3579        let mut all_ok = true;
3580        for sg in &h.startup_grants {
3581            let tag = grant_tag(&sg.id);
3582            let tag = YogStr::from_str(&tag);
3583            if unsafe { bridge!("playerHasTag", [S(player), S(tag)]) }.unwrap_or(false) {
3584                continue;
3585            }
3586            let book = sg.book.as_ref().map(|_| "minecraft:written_book");
3587            let mut ok = true;
3588            for item_id in sg.items.iter().map(String::as_str).chain(book) {
3589                ok &= unsafe { srv_give_item(std::ptr::null_mut(), player, YogStr::from_str(item_id), 1) };
3590            }
3591            if ok && unsafe { bridge!("addPlayerTag", [S(player), S(tag)]) }.unwrap_or(false) {
3592                yog_logging::info!("startup grant {} given to {}", sg.id, p);
3593            } else {
3594                all_ok = false;
3595            }
3596        }
3597        if !all_ok && ticks < MAX_TICKS {
3598            still_pending.push((p, u, ticks + 1));
3599        }
3600    }
3601    *pending = still_pending;
3602}
3603
3604#[unsafe(no_mangle)]
3605pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerLeave<'l>(
3606    mut env: JNIEnv<'l>,
3607    _class: JClass<'l>,
3608    player: JString<'l>,
3609    uuid: JString<'l>,
3610    dimension: JString<'l>,
3611) {
3612    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
3613    let ev = yog_abi::YogPlayerEvent {
3614        player: YogStr::from_str(&p),
3615        uuid: YogStr::from_str(&u),
3616        dimension: YogStr::from_str(&d),
3617    };
3618    let srv = srv_ptr();
3619    guard("on_player_leave", || {
3620        each(&handlers().player_leave, |ud, f| unsafe {
3621            f(ud, srv, &ev, 1)
3622        });
3623    });
3624}
3625
3626#[unsafe(no_mangle)]
3627pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseItem<'l>(
3628    mut env: JNIEnv<'l>,
3629    _class: JClass<'l>,
3630    player: JString<'l>,
3631    item: JString<'l>,
3632    sneaking: jni::sys::jboolean,
3633    dimension: JString<'l>,
3634) {
3635    let (p, i, d) = (jstr!(env, player), jstr!(env, item), jstr!(env, dimension));
3636    let ev = yog_abi::YogUseItemEvent {
3637        player: YogStr::from_str(&p),
3638        item: YogStr::from_str(&i),
3639        sneaking: sneaking != 0,
3640        dimension: YogStr::from_str(&d),
3641    };
3642    let srv = srv_ptr();
3643    guard("on_use_item", || {
3644        each(&handlers().use_item, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3645    });
3646}
3647
3648#[unsafe(no_mangle)]
3649pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseItemPre<'l>(
3650    mut env: JNIEnv<'l>,
3651    _class: JClass<'l>,
3652    player: JString<'l>,
3653    item: JString<'l>,
3654    sneaking: jni::sys::jboolean,
3655    dimension: JString<'l>,
3656) -> jni::sys::jboolean {
3657    let h = handlers();
3658    if h.use_item.is_empty() {
3659        return 1;
3660    }
3661    let p = jstr!(env, player, 1);
3662    let i = jstr!(env, item, 1);
3663    let d = jstr!(env, dimension, 1);
3664    let ev = yog_abi::YogUseItemEvent {
3665        player: YogStr::from_str(&p),
3666        item: YogStr::from_str(&i),
3667        sneaking: sneaking != 0,
3668        dimension: YogStr::from_str(&d),
3669    };
3670    let srv = srv_ptr();
3671    allowed(&h.use_item, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3672}
3673
3674#[unsafe(no_mangle)]
3675pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseBlock<'l>(
3676    mut env: JNIEnv<'l>,
3677    _class: JClass<'l>,
3678    player: JString<'l>,
3679    block: JString<'l>,
3680    x: jint,
3681    y: jint,
3682    z: jint,
3683    dimension: JString<'l>,
3684) {
3685    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
3686    let ev = yog_abi::YogUseBlockEvent {
3687        player: YogStr::from_str(&p),
3688        block: YogStr::from_str(&b),
3689        pos: YogBlockPos { x, y, z },
3690        dimension: YogStr::from_str(&d),
3691    };
3692    let srv = srv_ptr();
3693    guard("on_use_block", || {
3694        each(&handlers().use_block, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3695    });
3696}
3697
3698#[unsafe(no_mangle)]
3699pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseBlockPre<'l>(
3700    mut env: JNIEnv<'l>,
3701    _class: JClass<'l>,
3702    player: JString<'l>,
3703    block: JString<'l>,
3704    x: jint,
3705    y: jint,
3706    z: jint,
3707    dimension: JString<'l>,
3708) -> jni::sys::jboolean {
3709    let h = handlers();
3710    if h.use_block.is_empty() {
3711        return 1;
3712    }
3713    let p = jstr!(env, player, 1);
3714    let b = jstr!(env, block, 1);
3715    let d = jstr!(env, dimension, 1);
3716    let ev = yog_abi::YogUseBlockEvent {
3717        player: YogStr::from_str(&p),
3718        block: YogStr::from_str(&b),
3719        pos: YogBlockPos { x, y, z },
3720        dimension: YogStr::from_str(&d),
3721    };
3722    let srv = srv_ptr();
3723    allowed(&h.use_block, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3724}
3725
3726#[unsafe(no_mangle)]
3727pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnAttackEntity<'l>(
3728    mut env: JNIEnv<'l>,
3729    _class: JClass<'l>,
3730    player: JString<'l>,
3731    target_type: JString<'l>,
3732    target_uuid: JString<'l>,
3733    dimension: JString<'l>,
3734) {
3735    let (p, tt, tu, d) = (
3736        jstr!(env, player),
3737        jstr!(env, target_type),
3738        jstr!(env, target_uuid),
3739        jstr!(env, dimension),
3740    );
3741    let ev = yog_abi::YogAttackEntityEvent {
3742        player: YogStr::from_str(&p),
3743        target_type: YogStr::from_str(&tt),
3744        target_uuid: YogStr::from_str(&tu),
3745        dimension: YogStr::from_str(&d),
3746    };
3747    let srv = srv_ptr();
3748    guard("on_attack_entity", || {
3749        each(&handlers().attack_entity, |ud, f| unsafe {
3750            f(ud, srv, &ev, 1)
3751        });
3752    });
3753}
3754
3755#[unsafe(no_mangle)]
3756pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDamage<'l>(
3757    mut env: JNIEnv<'l>,
3758    _class: JClass<'l>,
3759    entity_type: JString<'l>,
3760    uuid: JString<'l>,
3761    amount: jfloat,
3762    source: JString<'l>,
3763    dimension: JString<'l>,
3764) {
3765    let (et, u, s, d) = (
3766        jstr!(env, entity_type),
3767        jstr!(env, uuid),
3768        jstr!(env, source),
3769        jstr!(env, dimension),
3770    );
3771    let ev = yog_abi::YogEntityDamageEvent {
3772        entity_type: YogStr::from_str(&et),
3773        uuid: YogStr::from_str(&u),
3774        amount,
3775        source: YogStr::from_str(&s),
3776        dimension: YogStr::from_str(&d),
3777    };
3778    let srv = srv_ptr();
3779    guard("on_entity_damage", || {
3780        each(&handlers().entity_damage, |ud, f| unsafe {
3781            f(ud, srv, &ev, 1)
3782        });
3783    });
3784}
3785
3786#[unsafe(no_mangle)]
3787pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDeath<'l>(
3788    mut env: JNIEnv<'l>,
3789    _class: JClass<'l>,
3790    entity_type: JString<'l>,
3791    uuid: JString<'l>,
3792    source: JString<'l>,
3793    dimension: JString<'l>,
3794) {
3795    let (et, u, s, d) = (
3796        jstr!(env, entity_type),
3797        jstr!(env, uuid),
3798        jstr!(env, source),
3799        jstr!(env, dimension),
3800    );
3801    let ev = yog_abi::YogEntityDeathEvent {
3802        entity_type: YogStr::from_str(&et),
3803        uuid: YogStr::from_str(&u),
3804        source: YogStr::from_str(&s),
3805        dimension: YogStr::from_str(&d),
3806    };
3807    let srv = srv_ptr();
3808    guard("on_entity_death", || {
3809        each(&handlers().entity_death, |ud, f| unsafe {
3810            f(ud, srv, &ev, 1)
3811        });
3812    });
3813}
3814
3815#[unsafe(no_mangle)]
3816pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnTick<'l>(
3817    _env: JNIEnv<'l>,
3818    _class: JClass<'l>,
3819) {
3820    let h = handlers();
3821    let srv = srv_ptr();
3822    guard("on_tick", || {
3823        each(&h.server_tick, |ud, f| unsafe { f(ud, srv) });
3824    });
3825
3826    guard("startup_grants", process_pending_grants);
3827
3828    // Scheduler — once tasks
3829    {
3830        let mut sched = h.scheduler.lock().expect("scheduler poisoned");
3831        let mut to_fire: Vec<(LoadedModule, *mut c_void, YogScheduledFn)> = Vec::new();
3832        let mut remaining = Vec::new();
3833        for task in sched.once_tasks.drain(..) {
3834            if task.delay_remaining == 0 {
3835                to_fire.push((task.module.clone(), task.ud, task.f));
3836            } else {
3837                remaining.push(OnceTask {
3838                    delay_remaining: task.delay_remaining - 1,
3839                    ..task
3840                });
3841            }
3842        }
3843        sched.once_tasks = remaining;
3844        drop(sched);
3845        for (module, ud, f) in to_fire {
3846            if module.is_retiring() {
3847                continue;
3848            }
3849            let Some(_call) = module.enter() else {
3850                continue;
3851            };
3852            guard("schedule_once", || unsafe { f(ud, srv) });
3853        }
3854    }
3855
3856    // Scheduler — repeating tasks
3857    {
3858        let mut sched = h.scheduler.lock().expect("scheduler poisoned");
3859        let mut to_fire: Vec<(LoadedModule, *mut c_void, YogScheduledFn)> = Vec::new();
3860        for task in &mut sched.repeating_tasks {
3861            if task.ticks_left == 0 {
3862                to_fire.push((task.module.clone(), task.ud, task.f));
3863                task.ticks_left = task.period;
3864            } else {
3865                task.ticks_left -= 1;
3866            }
3867        }
3868        drop(sched);
3869        for (module, ud, f) in to_fire {
3870            if module.is_retiring() {
3871                continue;
3872            }
3873            let Some(_call) = module.enter() else {
3874                continue;
3875            };
3876            guard("schedule_repeating", || unsafe { f(ud, srv) });
3877        }
3878    }
3879}
3880
3881#[unsafe(no_mangle)]
3882pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnServerStarted<'l>(
3883    _env: JNIEnv<'l>,
3884    _class: JClass<'l>,
3885    _world_dir: JString<'l>,
3886) {
3887    // Grants pending for a previous world's players are void.
3888    handlers().pending_grants.lock().expect("pending_grants poisoned").clear();
3889    let srv = srv_ptr();
3890    guard("on_server_started", || {
3891        each(&handlers().server_started, |ud, f| unsafe { f(ud, srv) });
3892    });
3893}
3894
3895#[unsafe(no_mangle)]
3896pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnServerStopping<'l>(
3897    _env: JNIEnv<'l>,
3898    _class: JClass<'l>,
3899) {
3900    let srv = srv_ptr();
3901    guard("on_server_stopping", || {
3902        each(&handlers().server_stopping, |ud, f| unsafe { f(ud, srv) });
3903    });
3904}
3905
3906#[unsafe(no_mangle)]
3907pub extern "system" fn Java_dev_yog_NativeBridge_nativeCommandNames<'l>(
3908    env: JNIEnv<'l>,
3909    _class: JClass<'l>,
3910) -> jstring {
3911    let names = handlers()
3912        .commands
3913        .keys()
3914        .cloned()
3915        .collect::<Vec<_>>()
3916        .join("\n");
3917    env.new_string(names)
3918        .map(jni::objects::JString::into_raw)
3919        .unwrap_or(std::ptr::null_mut())
3920}
3921
3922#[unsafe(no_mangle)]
3923pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnBlockBreakPre<'l>(
3924    mut env: JNIEnv<'l>,
3925    _class: JClass<'l>,
3926    player: JString<'l>,
3927    block: JString<'l>,
3928    x: jint,
3929    y: jint,
3930    z: jint,
3931    dimension: JString<'l>,
3932) -> jni::sys::jboolean {
3933    let h = handlers();
3934    if h.block_break.is_empty() {
3935        return 1;
3936    }
3937    let p = jstr!(env, player, 1);
3938    let b = jstr!(env, block, 1);
3939    let d = jstr!(env, dimension, 1);
3940    let ev = yog_abi::YogBlockBreakEvent {
3941        player: YogStr::from_str(&p),
3942        block: YogStr::from_str(&b),
3943        pos: YogBlockPos { x, y, z },
3944        dimension: YogStr::from_str(&d),
3945    };
3946    let srv = srv_ptr();
3947    allowed(&h.block_break, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3948}
3949
3950#[unsafe(no_mangle)]
3951pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnChatPre<'l>(
3952    mut env: JNIEnv<'l>,
3953    _class: JClass<'l>,
3954    player: JString<'l>,
3955    message: JString<'l>,
3956    dimension: JString<'l>,
3957) -> jni::sys::jboolean {
3958    let h = handlers();
3959    if h.chat.is_empty() {
3960        return 1;
3961    }
3962    let p = jstr!(env, player, 1);
3963    let m = jstr!(env, message, 1);
3964    let d = jstr!(env, dimension, 1);
3965    let ev = yog_abi::YogChatEvent {
3966        player: YogStr::from_str(&p),
3967        message: YogStr::from_str(&m),
3968        dimension: YogStr::from_str(&d),
3969    };
3970    let srv = srv_ptr();
3971    allowed(&h.chat, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3972}
3973
3974#[unsafe(no_mangle)]
3975pub extern "system" fn Java_dev_yog_NativeBridge_nativeRecipeJsons<'l>(
3976    env: JNIEnv<'l>,
3977    _class: JClass<'l>,
3978) -> jstring {
3979    let s = handlers()
3980        .recipes
3981        .iter()
3982        .map(|(ns, name, json)| format!("{ns}\t{name}\t{json}"))
3983        .collect::<Vec<_>>()
3984        .join("\n");
3985    env.new_string(s)
3986        .map(jni::objects::JString::into_raw)
3987        .unwrap_or(std::ptr::null_mut())
3988}
3989
3990#[unsafe(no_mangle)]
3991pub extern "system" fn Java_dev_yog_NativeBridge_nativeDimensionJsons<'l>(
3992    env: JNIEnv<'l>,
3993    _class: JClass<'l>,
3994) -> jstring {
3995    // One line per declared dimension: `id \t def_json`. The host converts
3996    // `def_json` (our YogDimensionDef shape) into vanilla dimension_type /
3997    // dimension datapack JSON — see `yog-dimensions` crate docs.
3998    let s = handlers()
3999        .dimensions
4000        .iter()
4001        .map(|(id, json)| format!("{id}\t{json}"))
4002        .collect::<Vec<_>>()
4003        .join("\n");
4004    env.new_string(s)
4005        .map(jni::objects::JString::into_raw)
4006        .unwrap_or(std::ptr::null_mut())
4007}
4008
4009/// Called by a registered chunk-generator closure (via `ChunkWriter::set_block`)
4010/// to place one block in the chunk currently being generated. Calls back
4011/// into `YogCallbackChunkGenerator.setBlockInGeneratingChunk` on the Java
4012/// side — same thread, since the whole round trip is synchronous.
4013unsafe extern "C" fn chunk_writer_set_block(
4014    _ctx: *mut c_void,
4015    x: i32,
4016    y: i32,
4017    z: i32,
4018    block_id: YogStr,
4019) -> bool {
4020    unsafe {
4021        host!(
4022            "dev/yog/YogCallbackChunkGenerator",
4023            "setBlockInGeneratingChunk",
4024            [I(x), I(y), I(z), S(block_id)]
4025        )
4026        .unwrap_or(false)
4027    }
4028}
4029
4030#[unsafe(no_mangle)]
4031pub extern "system" fn Java_dev_yog_NativeBridge_nativeGenerateChunk<'l>(
4032    mut env: JNIEnv<'l>,
4033    _class: JClass<'l>,
4034    generator_type_id: JString<'l>,
4035    chunk_x: jint,
4036    chunk_z: jint,
4037    min_y: jint,
4038    height: jint,
4039) {
4040    let id: String = match env.get_string(&generator_type_id) {
4041        Ok(s) => String::from(s),
4042        Err(_) => return,
4043    };
4044    let Some((module, ud, cb)) = handlers().chunk_generators.get(&id).cloned() else {
4045        return;
4046    };
4047    if module.is_retiring() {
4048        return;
4049    }
4050    let Some(_call) = module.enter() else { return };
4051    let writer = yog_abi::YogChunkWriterApi {
4052        chunk_x,
4053        chunk_z,
4054        min_y,
4055        height,
4056        ctx: std::ptr::null_mut(),
4057        set_block: chunk_writer_set_block,
4058    };
4059    guard("chunk_generator", || unsafe {
4060        cb(ud, &writer as *const _);
4061    });
4062}
4063
4064#[unsafe(no_mangle)]
4065pub extern "system" fn Java_dev_yog_NativeBridge_nativeTypedCommandSchemas<'l>(
4066    env: JNIEnv<'l>,
4067    _class: JClass<'l>,
4068) -> jstring {
4069    // One line per DISTINCT (name, schema) pair — a name can have several
4070    // typed registrations (different arg shapes); Brigadier merges repeated
4071    // `dispatcher.register()` calls for the same literal name into one tree,
4072    // so the Java side just needs every distinct schema once each.
4073    let mut lines = Vec::new();
4074    for (name, regs) in handlers().commands.iter() {
4075        let mut seen = std::collections::HashSet::new();
4076        for (_module, schema, _, _) in regs {
4077            if let Some(schema) = schema
4078                && seen.insert(schema.as_str())
4079            {
4080                lines.push(format!("{name}\t{schema}"));
4081            }
4082        }
4083    }
4084    env.new_string(lines.join("\n"))
4085        .map(jni::objects::JString::into_raw)
4086        .unwrap_or(std::ptr::null_mut())
4087}
4088
4089#[unsafe(no_mangle)]
4090pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnCommand<'l>(
4091    mut env: JNIEnv<'l>,
4092    _class: JClass<'l>,
4093    name: JString<'l>,
4094    args: JString<'l>,
4095    source: JString<'l>,
4096    uuid: JString<'l>,
4097    dimension: JString<'l>,
4098) -> jstring {
4099    let (n, a, s, u, d) = (
4100        env.get_string(&name).map(String::from).unwrap_or_default(),
4101        env.get_string(&args).map(String::from).unwrap_or_default(),
4102        env.get_string(&source)
4103            .map(String::from)
4104            .unwrap_or_default(),
4105        env.get_string(&uuid).map(String::from).unwrap_or_default(),
4106        env.get_string(&dimension)
4107            .map(String::from)
4108            .unwrap_or_default(),
4109    );
4110    let h = handlers();
4111    let srv = srv_ptr();
4112    let args: Vec<&str> = if a.is_empty() { Vec::new() } else { a.split('\t').collect() };
4113    // One handler with `args`; its reply, or `None` when it passed.
4114    let call = |ud: &*mut c_void, f: &YogCommandFn, args: &str| -> Option<String> {
4115        let ev = yog_abi::YogCommandEvent {
4116            name: YogStr::from_str(&n),
4117            args: YogStr::from_str(args),
4118            source: YogStr::from_str(&s),
4119            uuid: YogStr::from_str(&u),
4120            dimension: YogStr::from_str(&d),
4121        };
4122        let mut buf = [0u8; 4096];
4123        let mut reply_len: u32 = 0;
4124        unsafe { f(*ud, srv, &ev, buf.as_mut_ptr(), buf.len() as u32, &mut reply_len) };
4125        (reply_len > 0).then(|| String::from_utf8_lossy(&buf[..reply_len as usize]).into_owned())
4126    };
4127    let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4128        let Some(regs) = h.commands.get(&n) else {
4129            return String::new();
4130        };
4131        let live = regs.iter().filter(|(module, ..)| !module.is_retiring());
4132        // 1. Typed handlers whose schema has exactly these many args.
4133        // 2. Ones ending in a `string`, with the extra words joined into it
4134        //    (the game may have parsed them as separate words).
4135        // 3. Plain (schema-less) handlers, with whatever was typed.
4136        for pass in 0..3 {
4137            for (module, schema, ud, f) in live.clone() {
4138                let joined;
4139                let these = match (pass, schema) {
4140                    (0, Some(schema)) if schema.split_whitespace().count() == args.len() => a.as_str(),
4141                    (1, Some(schema)) => {
4142                        let types: Vec<&str> = schema.split_whitespace().collect();
4143                        let k = types.len();
4144                        if types.last() != Some(&"string") || k == 0 || args.len() <= k {
4145                            continue;
4146                        }
4147                        let mut parts: Vec<String> = args[..k - 1].iter().map(|x| x.to_string()).collect();
4148                        parts.push(args[k - 1..].join(" "));
4149                        joined = parts.join("\t");
4150                        joined.as_str()
4151                    }
4152                    (2, None) => a.as_str(),
4153                    _ => continue,
4154                };
4155                let Some(_call) = module.enter() else {
4156                    continue;
4157                };
4158                if let Some(r) = call(ud, f, these) {
4159                    return r;
4160                }
4161            }
4162        }
4163        String::new()
4164    }))
4165    .unwrap_or_else(|_| {
4166        yog_logging::error!("a mod panicked handling command `{}`", n);
4167        String::new()
4168    });
4169
4170    env.new_string(reply)
4171        .map(jni::objects::JString::into_raw)
4172        .unwrap_or(std::ptr::null_mut())
4173}
4174
4175#[unsafe(no_mangle)]
4176pub extern "system" fn Java_dev_yog_NativeBridge_nativeItemDefs<'l>(
4177    env: JNIEnv<'l>,
4178    _class: JClass<'l>,
4179) -> jstring {
4180    let s = handlers()
4181        .items
4182        .iter()
4183        .map(|d| {
4184            let mut parts = vec![d.id.clone()];
4185            parts.push(format!("max_stack={}", d.max_stack));
4186            if let Some(n) = &d.name {
4187                parts.push(format!("name={n}"));
4188            }
4189            if let Some(t) = &d.tooltip {
4190                parts.push(format!("tooltip={t}"));
4191            }
4192            if d.max_damage > 0 {
4193                parts.push(format!("max_damage={}", d.max_damage));
4194            }
4195            if d.fire_resistant {
4196                parts.push("fire_resistant=1".into());
4197            }
4198            if d.fuel_ticks > 0 {
4199                parts.push(format!("fuel_ticks={}", d.fuel_ticks));
4200            }
4201            if let Some(f) = &d.food {
4202                parts.push(format!(
4203                    "food={}:{}:{}",
4204                    f.nutrition,
4205                    f.saturation,
4206                    if f.can_always_eat { 1 } else { 0 }
4207                ));
4208            }
4209            parts.join("\t")
4210        })
4211        .collect::<Vec<_>>()
4212        .join("\n");
4213    env.new_string(s)
4214        .map(jni::objects::JString::into_raw)
4215        .unwrap_or(std::ptr::null_mut())
4216}
4217
4218#[unsafe(no_mangle)]
4219pub extern "system" fn Java_dev_yog_NativeBridge_nativeBlockDefs<'l>(
4220    env: JNIEnv<'l>,
4221    _class: JClass<'l>,
4222) -> jstring {
4223    let s = handlers()
4224        .blocks
4225        .iter()
4226        .map(|d| {
4227            let mut parts = vec![d.id.clone()];
4228            parts.push(format!("hardness={}", d.hardness));
4229            parts.push(format!("resistance={}", d.resistance));
4230            if let Some(n) = &d.name {
4231                parts.push(format!("name={n}"));
4232            }
4233            if let Some(sh) = d.shape {
4234                parts.push(format!(
4235                    "shape={}:{}:{}:{}:{}:{}",
4236                    sh[0], sh[1], sh[2], sh[3], sh[4], sh[5]
4237                ));
4238            }
4239            if d.light_level > 0 {
4240                parts.push(format!("light={}", d.light_level));
4241            }
4242            if let Some(snd) = &d.sound {
4243                parts.push(format!("sound={snd}"));
4244            }
4245            if d.requires_tool {
4246                parts.push("requires_tool=1".into());
4247            }
4248            if d.no_collision {
4249                parts.push("no_collision=1".into());
4250            }
4251            if d.slipperiness > 0.0 {
4252                parts.push(format!("slipperiness={}", d.slipperiness));
4253            }
4254            if d.connects {
4255                parts.push("connects=1".into());
4256            }
4257            if !d.connect_groups.is_empty() {
4258                parts.push(format!("connect_groups={}", d.connect_groups.join(",")));
4259            }
4260            if let Some(inv) = &d.inventory_id {
4261                parts.push(format!("inventory_id={inv}"));
4262            }
4263            if d.redstone_source {
4264                parts.push("redstone=1".into());
4265            }
4266            parts.join("\t")
4267        })
4268        .collect::<Vec<_>>()
4269        .join("\n");
4270    env.new_string(s)
4271        .map(jni::objects::JString::into_raw)
4272        .unwrap_or(std::ptr::null_mut())
4273}
4274
4275/// Tab/newline-encoded inventory defs — one line per registered
4276/// `InventoryDef` (see `yog_inventory`): `id\tslot_count=N\tlayout=x:y,...
4277/// \tinclude_player_inventory=0|1\tplayer_inv=x:y\tbackground_texture=...\ttitle=...`
4278#[unsafe(no_mangle)]
4279pub extern "system" fn Java_dev_yog_NativeBridge_nativeInventoryDefs<'l>(
4280    env: JNIEnv<'l>,
4281    _class: JClass<'l>,
4282) -> jstring {
4283    let s = handlers()
4284        .inventories
4285        .iter()
4286        .map(|d| {
4287            let mut parts = vec![d.id.clone()];
4288            parts.push(format!("slot_count={}", d.slot_count));
4289            parts.push(format!(
4290                "layout={}",
4291                yog_inventory::encode_layout(&d.resolved_layout())
4292            ));
4293            parts.push(format!(
4294                "include_player_inventory={}",
4295                d.include_player_inventory as u8
4296            ));
4297            parts.push(format!(
4298                "player_inv={}:{}",
4299                d.player_inv_offset.0, d.player_inv_offset.1
4300            ));
4301            if let Some(tex) = &d.background_texture {
4302                parts.push(format!("background_texture={tex}"));
4303            }
4304            if !d.title.is_empty() {
4305                parts.push(format!("title={}", d.title));
4306            }
4307            if let Some((w, h)) = d.background_size {
4308                parts.push(format!("background_size={w}:{h}"));
4309            }
4310            parts.join("\t")
4311        })
4312        .collect::<Vec<_>>()
4313        .join("\n");
4314    env.new_string(s)
4315        .map(jni::objects::JString::into_raw)
4316        .unwrap_or(std::ptr::null_mut())
4317}
4318
4319#[unsafe(no_mangle)]
4320pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPacket<'l>(
4321    mut env: JNIEnv<'l>,
4322    _class: JClass<'l>,
4323    channel: JString<'l>,
4324    player: JString<'l>,
4325    payload: JByteArray<'l>,
4326) {
4327    let ch = env
4328        .get_string(&channel)
4329        .map(String::from)
4330        .unwrap_or_default();
4331    let pl = env
4332        .get_string(&player)
4333        .map(String::from)
4334        .unwrap_or_default();
4335    let data = env.convert_byte_array(&payload).unwrap_or_default();
4336    let ev = yog_abi::YogPacketEvent {
4337        channel: YogStr::from_str(&ch),
4338        player: YogStr::from_str(&pl),
4339        payload: data.as_ptr(),
4340        payload_len: data.len() as u32,
4341    };
4342    let h = handlers();
4343    let srv = srv_ptr();
4344    guard("on_packet", || {
4345        if let Some((module, ud, f)) = h.packets.get(&ch) {
4346            if module.is_retiring() {
4347                return;
4348            }
4349            let Some(_call) = module.enter() else { return };
4350            unsafe { f(*ud, srv, &ev) };
4351        }
4352    });
4353}
4354
4355#[unsafe(no_mangle)]
4356pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnClientPacket<'l>(
4357    mut env: JNIEnv<'l>,
4358    _class: JClass<'l>,
4359    channel: JString<'l>,
4360    payload: JByteArray<'l>,
4361) {
4362    let ch = env
4363        .get_string(&channel)
4364        .map(String::from)
4365        .unwrap_or_default();
4366    let data = env.convert_byte_array(&payload).unwrap_or_default();
4367    let ev = yog_abi::YogPacketEvent {
4368        channel: YogStr::from_str(&ch),
4369        player: YogStr::EMPTY,
4370        payload: data.as_ptr(),
4371        payload_len: data.len() as u32,
4372    };
4373    let h = handlers();
4374    let srv = srv_ptr();
4375    guard("on_client_packet", || {
4376        if let Some((module, ud, f)) = h.client_packets.get(&ch) {
4377            if module.is_retiring() {
4378                return;
4379            }
4380            let Some(_call) = module.enter() else { return };
4381            unsafe { f(*ud, srv, &ev) };
4382        }
4383    });
4384}
4385
4386#[unsafe(no_mangle)]
4387pub extern "system" fn Java_dev_yog_NativeBridge_nativePacketChannels<'l>(
4388    env: JNIEnv<'l>,
4389    _class: JClass<'l>,
4390) -> jstring {
4391    let s = handlers()
4392        .packets
4393        .keys()
4394        .cloned()
4395        .collect::<Vec<_>>()
4396        .join("\n");
4397    env.new_string(s)
4398        .map(jni::objects::JString::into_raw)
4399        .unwrap_or(std::ptr::null_mut())
4400}
4401
4402#[unsafe(no_mangle)]
4403pub extern "system" fn Java_dev_yog_NativeBridge_nativeClientPacketChannels<'l>(
4404    env: JNIEnv<'l>,
4405    _class: JClass<'l>,
4406) -> jstring {
4407    let s = handlers()
4408        .client_packets
4409        .keys()
4410        .cloned()
4411        .collect::<Vec<_>>()
4412        .join("\n");
4413    env.new_string(s)
4414        .map(jni::objects::JString::into_raw)
4415        .unwrap_or(std::ptr::null_mut())
4416}
4417
4418#[unsafe(no_mangle)]
4419pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntitySpawn<'l>(
4420    mut env: JNIEnv<'l>,
4421    _class: JClass<'l>,
4422    entity_type: JString<'l>,
4423    uuid: JString<'l>,
4424    dimension: JString<'l>,
4425) {
4426    let h = handlers();
4427    if h.entity_spawn.is_empty() {
4428        return;
4429    }
4430    let (et, u, d) = (
4431        jstr!(env, entity_type),
4432        jstr!(env, uuid),
4433        jstr!(env, dimension),
4434    );
4435    let ev = yog_abi::YogEntitySpawnEvent {
4436        entity_type: YogStr::from_str(&et),
4437        uuid: YogStr::from_str(&u),
4438        dimension: YogStr::from_str(&d),
4439    };
4440    let srv = srv_ptr();
4441    guard("on_entity_spawn", || {
4442        each(&h.entity_spawn, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4443    });
4444}
4445
4446#[unsafe(no_mangle)]
4447pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntitySpawnPre<'l>(
4448    mut env: JNIEnv<'l>,
4449    _class: JClass<'l>,
4450    entity_type: JString<'l>,
4451    uuid: JString<'l>,
4452    dimension: JString<'l>,
4453) -> jni::sys::jboolean {
4454    let h = handlers();
4455    if h.entity_spawn.is_empty() {
4456        return 1;
4457    }
4458    let et = jstr!(env, entity_type, 1);
4459    let u = jstr!(env, uuid, 1);
4460    let d = jstr!(env, dimension, 1);
4461    let ev = yog_abi::YogEntitySpawnEvent {
4462        entity_type: YogStr::from_str(&et),
4463        uuid: YogStr::from_str(&u),
4464        dimension: YogStr::from_str(&d),
4465    };
4466    let srv = srv_ptr();
4467    allowed(&h.entity_spawn, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4468}
4469
4470#[unsafe(no_mangle)]
4471pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDamagePre<'l>(
4472    mut env: JNIEnv<'l>,
4473    _class: JClass<'l>,
4474    entity_type: JString<'l>,
4475    uuid: JString<'l>,
4476    amount: jfloat,
4477    source: JString<'l>,
4478    dimension: JString<'l>,
4479) -> jni::sys::jboolean {
4480    let h = handlers();
4481    if h.entity_damage.is_empty() {
4482        return 1;
4483    }
4484    let et = jstr!(env, entity_type, 1);
4485    let u = jstr!(env, uuid, 1);
4486    let s = jstr!(env, source, 1);
4487    let d = jstr!(env, dimension, 1);
4488    let ev = yog_abi::YogEntityDamageEvent {
4489        entity_type: YogStr::from_str(&et),
4490        uuid: YogStr::from_str(&u),
4491        amount,
4492        source: YogStr::from_str(&s),
4493        dimension: YogStr::from_str(&d),
4494    };
4495    let srv = srv_ptr();
4496    allowed(&h.entity_damage, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4497}
4498
4499#[unsafe(no_mangle)]
4500pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlaceBlockPre<'l>(
4501    mut env: JNIEnv<'l>,
4502    _class: JClass<'l>,
4503    player: JString<'l>,
4504    block: JString<'l>,
4505    x: jint,
4506    y: jint,
4507    z: jint,
4508    dimension: JString<'l>,
4509) -> jni::sys::jboolean {
4510    let h = handlers();
4511    if h.player_place_block.is_empty() {
4512        return 1;
4513    }
4514    let p = jstr!(env, player, 1);
4515    let b = jstr!(env, block, 1);
4516    let d = jstr!(env, dimension, 1);
4517    let ev = YogPlaceBlockEvent {
4518        player: YogStr::from_str(&p),
4519        block: YogStr::from_str(&b),
4520        pos: YogBlockPos { x, y, z },
4521        dimension: YogStr::from_str(&d),
4522    };
4523    let srv = srv_ptr();
4524    allowed(&h.player_place_block, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4525}
4526
4527#[unsafe(no_mangle)]
4528pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlaceBlock<'l>(
4529    mut env: JNIEnv<'l>,
4530    _class: JClass<'l>,
4531    player: JString<'l>,
4532    block: JString<'l>,
4533    x: jint,
4534    y: jint,
4535    z: jint,
4536    dimension: JString<'l>,
4537) {
4538    let h = handlers();
4539    if h.player_place_block.is_empty() {
4540        return;
4541    }
4542    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
4543    let ev = YogPlaceBlockEvent {
4544        player: YogStr::from_str(&p),
4545        block: YogStr::from_str(&b),
4546        pos: YogBlockPos { x, y, z },
4547        dimension: YogStr::from_str(&d),
4548    };
4549    let srv = srv_ptr();
4550    guard("on_player_place_block", || {
4551        each(&h.player_place_block, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4552    });
4553}
4554
4555#[unsafe(no_mangle)]
4556pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerDeathPre<'l>(
4557    mut env: JNIEnv<'l>,
4558    _class: JClass<'l>,
4559    player: JString<'l>,
4560    uuid: JString<'l>,
4561    source: JString<'l>,
4562    dimension: JString<'l>,
4563) -> jni::sys::jboolean {
4564    let h = handlers();
4565    if h.player_death.is_empty() {
4566        return 1;
4567    }
4568    let p = jstr!(env, player, 1);
4569    let u = jstr!(env, uuid, 1);
4570    let s = jstr!(env, source, 1);
4571    let d = jstr!(env, dimension, 1);
4572    let ev = YogPlayerDeathEvent {
4573        player: YogStr::from_str(&p),
4574        uuid: YogStr::from_str(&u),
4575        source: YogStr::from_str(&s),
4576        dimension: YogStr::from_str(&d),
4577    };
4578    let srv = srv_ptr();
4579    allowed(&h.player_death, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4580}
4581
4582#[unsafe(no_mangle)]
4583pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerDeath<'l>(
4584    mut env: JNIEnv<'l>,
4585    _class: JClass<'l>,
4586    player: JString<'l>,
4587    uuid: JString<'l>,
4588    source: JString<'l>,
4589    dimension: JString<'l>,
4590) {
4591    let h = handlers();
4592    if h.player_death.is_empty() {
4593        return;
4594    }
4595    let (p, u, s, d) = (
4596        jstr!(env, player),
4597        jstr!(env, uuid),
4598        jstr!(env, source),
4599        jstr!(env, dimension),
4600    );
4601    let ev = YogPlayerDeathEvent {
4602        player: YogStr::from_str(&p),
4603        uuid: YogStr::from_str(&u),
4604        source: YogStr::from_str(&s),
4605        dimension: YogStr::from_str(&d),
4606    };
4607    let srv = srv_ptr();
4608    guard("on_player_death", || {
4609        each(&h.player_death, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4610    });
4611}
4612
4613#[unsafe(no_mangle)]
4614pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerRespawn<'l>(
4615    mut env: JNIEnv<'l>,
4616    _class: JClass<'l>,
4617    player: JString<'l>,
4618    uuid: JString<'l>,
4619    at_anchor: jni::sys::jboolean,
4620    dimension: JString<'l>,
4621) {
4622    let h = handlers();
4623    if h.player_respawn.is_empty() {
4624        return;
4625    }
4626    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
4627    let ev = YogPlayerRespawnEvent {
4628        player: YogStr::from_str(&p),
4629        uuid: YogStr::from_str(&u),
4630        at_anchor: at_anchor != 0,
4631        dimension: YogStr::from_str(&d),
4632    };
4633    let srv = srv_ptr();
4634    guard("on_player_respawn", || {
4635        each(&h.player_respawn, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4636    });
4637}
4638
4639#[unsafe(no_mangle)]
4640pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnAdvancement<'l>(
4641    mut env: JNIEnv<'l>,
4642    _class: JClass<'l>,
4643    player: JString<'l>,
4644    uuid: JString<'l>,
4645    advancement: JString<'l>,
4646    dimension: JString<'l>,
4647) {
4648    let h = handlers();
4649    if h.advancement.is_empty() {
4650        return;
4651    }
4652    let (p, u, a, d) = (
4653        jstr!(env, player),
4654        jstr!(env, uuid),
4655        jstr!(env, advancement),
4656        jstr!(env, dimension),
4657    );
4658    let ev = YogAdvancementEvent {
4659        player: YogStr::from_str(&p),
4660        uuid: YogStr::from_str(&u),
4661        advancement: YogStr::from_str(&a),
4662        dimension: YogStr::from_str(&d),
4663    };
4664    let srv = srv_ptr();
4665    guard("on_advancement", || {
4666        each(&h.advancement, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4667    });
4668}
4669
4670#[unsafe(no_mangle)]
4671pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityInteractPre<'l>(
4672    mut env: JNIEnv<'l>,
4673    _class: JClass<'l>,
4674    player: JString<'l>,
4675    player_uuid: JString<'l>,
4676    entity_type: JString<'l>,
4677    entity_uuid: JString<'l>,
4678    hand: JString<'l>,
4679    dimension: JString<'l>,
4680) -> jni::sys::jboolean {
4681    let h = handlers();
4682    if h.entity_interact.is_empty() {
4683        return 1;
4684    }
4685    let p = jstr!(env, player, 1);
4686    let pu = jstr!(env, player_uuid, 1);
4687    let et = jstr!(env, entity_type, 1);
4688    let eu = jstr!(env, entity_uuid, 1);
4689    let ha = jstr!(env, hand, 1);
4690    let d = jstr!(env, dimension, 1);
4691    let ev = YogEntityInteractEvent {
4692        player: YogStr::from_str(&p),
4693        player_uuid: YogStr::from_str(&pu),
4694        entity_type: YogStr::from_str(&et),
4695        entity_uuid: YogStr::from_str(&eu),
4696        hand: YogStr::from_str(&ha),
4697        dimension: YogStr::from_str(&d),
4698    };
4699    let srv = srv_ptr();
4700    allowed(&h.entity_interact, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4701}
4702
4703#[unsafe(no_mangle)]
4704pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityInteract<'l>(
4705    mut env: JNIEnv<'l>,
4706    _class: JClass<'l>,
4707    player: JString<'l>,
4708    player_uuid: JString<'l>,
4709    entity_type: JString<'l>,
4710    entity_uuid: JString<'l>,
4711    hand: JString<'l>,
4712    dimension: JString<'l>,
4713) {
4714    let h = handlers();
4715    if h.entity_interact.is_empty() {
4716        return;
4717    }
4718    let (p, pu) = (jstr!(env, player), jstr!(env, player_uuid));
4719    let (et, eu, ha, d) = (
4720        jstr!(env, entity_type),
4721        jstr!(env, entity_uuid),
4722        jstr!(env, hand),
4723        jstr!(env, dimension),
4724    );
4725    let ev = YogEntityInteractEvent {
4726        player: YogStr::from_str(&p),
4727        player_uuid: YogStr::from_str(&pu),
4728        entity_type: YogStr::from_str(&et),
4729        entity_uuid: YogStr::from_str(&eu),
4730        hand: YogStr::from_str(&ha),
4731        dimension: YogStr::from_str(&d),
4732    };
4733    let srv = srv_ptr();
4734    guard("on_entity_interact", || {
4735        each(&h.entity_interact, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4736    });
4737}
4738
4739#[unsafe(no_mangle)]
4740pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemCraft<'l>(
4741    mut env: JNIEnv<'l>,
4742    _class: JClass<'l>,
4743    player: JString<'l>,
4744    player_uuid: JString<'l>,
4745    result_item: JString<'l>,
4746    result_count: jint,
4747    dimension: JString<'l>,
4748) {
4749    let h = handlers();
4750    if h.item_craft.is_empty() {
4751        return;
4752    }
4753    let (p, pu, ri, d) = (
4754        jstr!(env, player),
4755        jstr!(env, player_uuid),
4756        jstr!(env, result_item),
4757        jstr!(env, dimension),
4758    );
4759    let ev = YogCraftEvent {
4760        player: YogStr::from_str(&p),
4761        player_uuid: YogStr::from_str(&pu),
4762        result_item: YogStr::from_str(&ri),
4763        result_count: result_count as u32,
4764        dimension: YogStr::from_str(&d),
4765    };
4766    let srv = srv_ptr();
4767    guard("on_item_craft", || {
4768        each(&h.item_craft, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4769    });
4770}
4771
4772#[unsafe(no_mangle)]
4773pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnExplosionPre<'l>(
4774    mut env: JNIEnv<'l>,
4775    _class: JClass<'l>,
4776    dimension: JString<'l>,
4777    x: jdouble,
4778    y: jdouble,
4779    z: jdouble,
4780    power: jfloat,
4781    cause_uuid: JString<'l>,
4782) -> jni::sys::jboolean {
4783    let h = handlers();
4784    if h.explosion.is_empty() {
4785        return 1;
4786    }
4787    let d = jstr!(env, dimension, 1);
4788    let cu = jstr!(env, cause_uuid, 1);
4789    let ev = YogExplosionEvent {
4790        dimension: YogStr::from_str(&d),
4791        x,
4792        y,
4793        z,
4794        power,
4795        cause_uuid: YogStr::from_str(&cu),
4796    };
4797    let srv = srv_ptr();
4798    allowed(&h.explosion, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4799}
4800
4801#[unsafe(no_mangle)]
4802pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnExplosion<'l>(
4803    mut env: JNIEnv<'l>,
4804    _class: JClass<'l>,
4805    dimension: JString<'l>,
4806    x: jdouble,
4807    y: jdouble,
4808    z: jdouble,
4809    power: jfloat,
4810    cause_uuid: JString<'l>,
4811) {
4812    let h = handlers();
4813    if h.explosion.is_empty() {
4814        return;
4815    }
4816    let (d, cu) = (jstr!(env, dimension), jstr!(env, cause_uuid));
4817    let ev = YogExplosionEvent {
4818        dimension: YogStr::from_str(&d),
4819        x,
4820        y,
4821        z,
4822        power,
4823        cause_uuid: YogStr::from_str(&cu),
4824    };
4825    let srv = srv_ptr();
4826    guard("on_explosion", || {
4827        each(&h.explosion, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4828    });
4829}
4830
4831// ── ABI minor 9 JNI entry points ─────────────────────────────────────────────
4832
4833#[unsafe(no_mangle)]
4834pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemPickupPre<'l>(
4835    mut env: JNIEnv<'l>,
4836    _class: JClass<'l>,
4837    player: JString<'l>,
4838    player_uuid: JString<'l>,
4839    item_id: JString<'l>,
4840    item_count: jint,
4841    entity_uuid: JString<'l>,
4842    dimension: JString<'l>,
4843) -> jni::sys::jboolean {
4844    let h = handlers();
4845    if h.item_pickup.is_empty() {
4846        return 1;
4847    }
4848    let p = jstr!(env, player, 1);
4849    let pu = jstr!(env, player_uuid, 1);
4850    let ii = jstr!(env, item_id, 1);
4851    let eu = jstr!(env, entity_uuid, 1);
4852    let d = jstr!(env, dimension, 1);
4853    let ev = YogItemPickupEvent {
4854        player: YogStr::from_str(&p),
4855        player_uuid: YogStr::from_str(&pu),
4856        item_id: YogStr::from_str(&ii),
4857        item_count: item_count as u32,
4858        entity_uuid: YogStr::from_str(&eu),
4859        dimension: YogStr::from_str(&d),
4860    };
4861    let srv = srv_ptr();
4862    allowed(&h.item_pickup, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4863}
4864
4865#[unsafe(no_mangle)]
4866pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemPickup<'l>(
4867    mut env: JNIEnv<'l>,
4868    _class: JClass<'l>,
4869    player: JString<'l>,
4870    player_uuid: JString<'l>,
4871    item_id: JString<'l>,
4872    item_count: jint,
4873    entity_uuid: JString<'l>,
4874    dimension: JString<'l>,
4875) {
4876    let h = handlers();
4877    if h.item_pickup.is_empty() {
4878        return;
4879    }
4880    let (p, pu) = (jstr!(env, player), jstr!(env, player_uuid));
4881    let (ii, eu, d) = (
4882        jstr!(env, item_id),
4883        jstr!(env, entity_uuid),
4884        jstr!(env, dimension),
4885    );
4886    let ev = YogItemPickupEvent {
4887        player: YogStr::from_str(&p),
4888        player_uuid: YogStr::from_str(&pu),
4889        item_id: YogStr::from_str(&ii),
4890        item_count: item_count as u32,
4891        entity_uuid: YogStr::from_str(&eu),
4892        dimension: YogStr::from_str(&d),
4893    };
4894    let srv = srv_ptr();
4895    guard("on_item_pickup", || {
4896        each(&h.item_pickup, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4897    });
4898}
4899
4900#[unsafe(no_mangle)]
4901pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerMove<'l>(
4902    mut env: JNIEnv<'l>,
4903    _class: JClass<'l>,
4904    player: JString<'l>,
4905    player_uuid: JString<'l>,
4906    x: jdouble,
4907    y: jdouble,
4908    z: jdouble,
4909    yaw: jfloat,
4910    pitch: jfloat,
4911    dimension: JString<'l>,
4912) {
4913    let h = handlers();
4914    if h.player_move.is_empty() {
4915        return;
4916    }
4917    let (p, pu, d) = (
4918        jstr!(env, player),
4919        jstr!(env, player_uuid),
4920        jstr!(env, dimension),
4921    );
4922    let ev = YogPlayerMoveEvent {
4923        player: YogStr::from_str(&p),
4924        player_uuid: YogStr::from_str(&pu),
4925        x,
4926        y,
4927        z,
4928        yaw,
4929        pitch,
4930        dimension: YogStr::from_str(&d),
4931    };
4932    let srv = srv_ptr();
4933    guard("on_player_move", || {
4934        each(&h.player_move, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4935    });
4936}
4937
4938#[unsafe(no_mangle)]
4939pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerOpenPre<'l>(
4940    mut env: JNIEnv<'l>,
4941    _class: JClass<'l>,
4942    player: JString<'l>,
4943    player_uuid: JString<'l>,
4944    dimension: JString<'l>,
4945) -> jni::sys::jboolean {
4946    let h = handlers();
4947    if h.container_open.is_empty() {
4948        return 1;
4949    }
4950    let p = jstr!(env, player, 1);
4951    let pu = jstr!(env, player_uuid, 1);
4952    let d = jstr!(env, dimension, 1);
4953    let ev = YogContainerOpenEvent {
4954        player: YogStr::from_str(&p),
4955        player_uuid: YogStr::from_str(&pu),
4956        container_type: YogStr::EMPTY,
4957        dimension: YogStr::from_str(&d),
4958    };
4959    let srv = srv_ptr();
4960    allowed(&h.container_open, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4961}
4962
4963#[unsafe(no_mangle)]
4964pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerOpen<'l>(
4965    mut env: JNIEnv<'l>,
4966    _class: JClass<'l>,
4967    player: JString<'l>,
4968    player_uuid: JString<'l>,
4969    container_type: JString<'l>,
4970    dimension: JString<'l>,
4971) {
4972    let h = handlers();
4973    if h.container_open.is_empty() {
4974        return;
4975    }
4976    let (p, pu, ct, d) = (
4977        jstr!(env, player),
4978        jstr!(env, player_uuid),
4979        jstr!(env, container_type),
4980        jstr!(env, dimension),
4981    );
4982    let ev = YogContainerOpenEvent {
4983        player: YogStr::from_str(&p),
4984        player_uuid: YogStr::from_str(&pu),
4985        container_type: YogStr::from_str(&ct),
4986        dimension: YogStr::from_str(&d),
4987    };
4988    let srv = srv_ptr();
4989    guard("on_container_open", || {
4990        each(&h.container_open, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4991    });
4992}
4993
4994#[unsafe(no_mangle)]
4995pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerClose<'l>(
4996    mut env: JNIEnv<'l>,
4997    _class: JClass<'l>,
4998    player: JString<'l>,
4999    player_uuid: JString<'l>,
5000    dimension: JString<'l>,
5001) {
5002    let h = handlers();
5003    if h.container_close.is_empty() {
5004        return;
5005    }
5006    let (p, pu, d) = (
5007        jstr!(env, player),
5008        jstr!(env, player_uuid),
5009        jstr!(env, dimension),
5010    );
5011    let ev = YogContainerCloseEvent {
5012        player: YogStr::from_str(&p),
5013        player_uuid: YogStr::from_str(&pu),
5014        dimension: YogStr::from_str(&d),
5015    };
5016    let srv = srv_ptr();
5017    guard("on_container_close", || {
5018        each(&h.container_close, |ud, f| unsafe { f(ud, srv, &ev, 1) });
5019    });
5020}
5021
5022#[unsafe(no_mangle)]
5023pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnProjectileHitPre<'l>(
5024    mut env: JNIEnv<'l>,
5025    _class: JClass<'l>,
5026    projectile_type: JString<'l>,
5027    projectile_uuid: JString<'l>,
5028    shooter_uuid: JString<'l>,
5029    hit_type: JString<'l>,
5030    hit_entity_uuid: JString<'l>,
5031    x: jdouble,
5032    y: jdouble,
5033    z: jdouble,
5034    dimension: JString<'l>,
5035) -> jni::sys::jboolean {
5036    let h = handlers();
5037    if h.projectile_hit.is_empty() {
5038        return 1;
5039    }
5040    let pt = jstr!(env, projectile_type, 1);
5041    let pu = jstr!(env, projectile_uuid, 1);
5042    let su = jstr!(env, shooter_uuid, 1);
5043    let ht = jstr!(env, hit_type, 1);
5044    let heu = jstr!(env, hit_entity_uuid, 1);
5045    let dim = jstr!(env, dimension, 1);
5046    let ev = YogProjectileHitEvent {
5047        projectile_type: YogStr::from_str(&pt),
5048        projectile_uuid: YogStr::from_str(&pu),
5049        shooter_uuid: YogStr::from_str(&su),
5050        hit_type: YogStr::from_str(&ht),
5051        hit_entity_uuid: YogStr::from_str(&heu),
5052        x,
5053        y,
5054        z,
5055        dimension: YogStr::from_str(&dim),
5056    };
5057    let srv = srv_ptr();
5058    allowed(&h.projectile_hit, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
5059}
5060
5061#[unsafe(no_mangle)]
5062pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnProjectileHit<'l>(
5063    mut env: JNIEnv<'l>,
5064    _class: JClass<'l>,
5065    projectile_type: JString<'l>,
5066    projectile_uuid: JString<'l>,
5067    shooter_uuid: JString<'l>,
5068    hit_type: JString<'l>,
5069    hit_entity_uuid: JString<'l>,
5070    x: jdouble,
5071    y: jdouble,
5072    z: jdouble,
5073    dimension: JString<'l>,
5074) {
5075    let h = handlers();
5076    if h.projectile_hit.is_empty() {
5077        return;
5078    }
5079    let (pt, pu) = (jstr!(env, projectile_type), jstr!(env, projectile_uuid));
5080    let (su, ht) = (jstr!(env, shooter_uuid), jstr!(env, hit_type));
5081    let (heu, dim) = (jstr!(env, hit_entity_uuid), jstr!(env, dimension));
5082    let ev = YogProjectileHitEvent {
5083        projectile_type: YogStr::from_str(&pt),
5084        projectile_uuid: YogStr::from_str(&pu),
5085        shooter_uuid: YogStr::from_str(&su),
5086        hit_type: YogStr::from_str(&ht),
5087        hit_entity_uuid: YogStr::from_str(&heu),
5088        x,
5089        y,
5090        z,
5091        dimension: YogStr::from_str(&dim),
5092    };
5093    let srv = srv_ptr();
5094    guard("on_projectile_hit", || {
5095        each(&h.projectile_hit, |ud, f| unsafe { f(ud, srv, &ev, 1) });
5096    });
5097}
5098
5099// ── ABI minor 10 — client-side JNI entry points ───────────────────────────────
5100
5101#[unsafe(no_mangle)]
5102pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnClientTick<'l>(
5103    _env: JNIEnv<'l>,
5104    _class: JClass<'l>,
5105) {
5106    let h = handlers();
5107    if h.client_tick.is_empty() {
5108        return;
5109    }
5110    guard("on_client_tick", || {
5111        each(&h.client_tick, |ud, f| unsafe { f(ud) });
5112    });
5113}
5114
5115/// The host's GL function pointer for `sym` (0 if it has none).
5116fn gl_proc_address(sym: &str) -> usize {
5117    let ptr: Option<i64> = unsafe { bridge!("glProcAddress", [S(YogStr::from_str(sym))]) };
5118    ptr.unwrap_or(0) as usize
5119}
5120
5121#[unsafe(no_mangle)]
5122pub extern "system" fn Java_dev_yog_NativeBridge_nativeGlInit<'l>(
5123    _env: JNIEnv<'l>,
5124    _class: JClass<'l>,
5125) {
5126    if GL.get().is_some() {
5127        return;
5128    }
5129    let mut raw_get_binary: usize = 0;
5130    let mut raw_prog_binary: usize = 0;
5131    let gl = unsafe {
5132        glow::Context::from_loader_function(|sym| {
5133            let ptr = gl_proc_address(sym);
5134            // Capture extension pointers while the loader runs.
5135            match sym {
5136                "glGetProgramBinary" => raw_get_binary = ptr,
5137                "glProgramBinary" => raw_prog_binary = ptr,
5138                _ => {}
5139            }
5140            ptr as *const _
5141        })
5142    };
5143    let _ = GL.set(GlCtx(gl));
5144    let _ = GL_GET_PROGRAM_BINARY.set((raw_get_binary != 0).then_some(raw_get_binary));
5145    let _ = GL_PROGRAM_BINARY.set((raw_prog_binary != 0).then_some(raw_prog_binary));
5146    // `glGetProgramiv` is a core function; always available.  We look it up once here
5147    // to avoid depending on glow internals for the PROGRAM_BINARY_LENGTH query.
5148    let get_program_iv = gl_proc_address("glGetProgramiv");
5149    let _ = GL_GET_PROGRAM_IV.set((get_program_iv != 0).then_some(get_program_iv));
5150}
5151
5152#[unsafe(no_mangle)]
5153pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnHudRender<'l>(
5154    _env: JNIEnv<'l>,
5155    _class: JClass<'l>,
5156    delta_tick: jfloat,
5157    screen_w: jint,
5158    screen_h: jint,
5159    scale_factor: jfloat,
5160    player_x: jfloat,
5161    player_y: jfloat,
5162    player_z: jfloat,
5163) {
5164    let h = handlers();
5165    let mut gfx = GFX_FN_TABLE;
5166    gfx.screen_w = screen_w;
5167    gfx.screen_h = screen_h;
5168    gfx.delta_tick = delta_tick;
5169    gfx.scale_factor = scale_factor;
5170    gfx.player_pos = [player_x, player_y, player_z];
5171
5172    // Mod hud_render callbacks.
5173    if !h.hud_render.is_empty() {
5174        guard("on_hud_render", || {
5175            each(&h.hud_render, |ud, f| unsafe { f(ud, &gfx) });
5176        });
5177    }
5178
5179    // Render active book UIs.
5180    let active_layers: Vec<UiLayer> = h.active_uis.lock().unwrap().clone();
5181    let sw = screen_w as f32;
5182    let sh = screen_h as f32;
5183    let ctx = unsafe { yog_gfx::GfxContext::from_raw(&gfx as *const _) };
5184    let mut renderers = h.book_renderers.lock().unwrap();
5185    for layer in &active_layers {
5186        if !layer.visible {
5187            continue;
5188        }
5189        if let Some(renderer) = renderers.get_mut(&layer.id) {
5190            renderer.render(&ctx, sw, sh);
5191        }
5192    }
5193}
5194
5195#[unsafe(no_mangle)]
5196pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnWorldRender<'l>(
5197    env: JNIEnv<'l>,
5198    _class: JClass<'l>,
5199    delta_tick: jfloat,
5200    screen_w: jint,
5201    screen_h: jint,
5202    scale_factor: jfloat,
5203    view_proj_arr: JFloatArray<'l>,
5204    cam_x: jfloat,
5205    cam_y: jfloat,
5206    cam_z: jfloat,
5207    player_x: jfloat,
5208    player_y: jfloat,
5209    player_z: jfloat,
5210) {
5211    let h = handlers();
5212    if h.world_render.is_empty() {
5213        return;
5214    }
5215    let mut view_proj = [0f32; 16];
5216    if env
5217        .get_float_array_region(&view_proj_arr, 0, &mut view_proj)
5218        .is_err()
5219    {
5220        return;
5221    }
5222    let mut gfx = GFX_FN_TABLE;
5223    gfx.screen_w = screen_w;
5224    gfx.screen_h = screen_h;
5225    gfx.delta_tick = delta_tick;
5226    gfx.scale_factor = scale_factor;
5227    gfx.view_proj = view_proj;
5228    gfx.camera_pos = [cam_x, cam_y, cam_z];
5229    gfx.player_pos = [player_x, player_y, player_z];
5230    guard("on_world_render", || {
5231        each(&h.world_render, |ud, f| unsafe { f(ud, &gfx) });
5232    });
5233}
5234
5235#[unsafe(no_mangle)]
5236pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnKeyPress<'l>(
5237    _env: JNIEnv<'l>,
5238    _class: JClass<'l>,
5239    key_code: jint,
5240    scan_code: jint,
5241    action: jint,
5242    modifiers: jint,
5243) -> jni::sys::jboolean {
5244    let h = handlers();
5245    if h.key_press.is_empty() {
5246        return 1;
5247    }
5248    let ev = YogKeyPressEvent {
5249        key_code,
5250        scan_code,
5251        action,
5252        modifiers,
5253    };
5254    allowed(&h.key_press, |ud, f| unsafe { f(ud, &ev) }) as jni::sys::jboolean
5255}
5256
5257#[unsafe(no_mangle)]
5258pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnScreenOpen<'l>(
5259    mut env: JNIEnv<'l>,
5260    _class: JClass<'l>,
5261    screen_class: JString<'l>,
5262) {
5263    let h = handlers();
5264    if h.screen_open.is_empty() {
5265        return;
5266    }
5267    let sc = match env.get_string(&screen_class) {
5268        Ok(s) => String::from(s),
5269        Err(_) => return,
5270    };
5271    guard("on_screen_open", || {
5272        each(&h.screen_open, |ud, f| unsafe {
5273            f(ud, YogStr::from_str(&sc))
5274        });
5275    });
5276}
5277
5278#[unsafe(no_mangle)]
5279pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnScreenClose<'l>(
5280    mut env: JNIEnv<'l>,
5281    _class: JClass<'l>,
5282    screen_class: JString<'l>,
5283) {
5284    let h = handlers();
5285    if h.screen_close.is_empty() {
5286        return;
5287    }
5288    let sc = match env.get_string(&screen_class) {
5289        Ok(s) => String::from(s),
5290        Err(_) => return,
5291    };
5292    guard("on_screen_close", || {
5293        each(&h.screen_close, |ud, f| unsafe {
5294            f(ud, YogStr::from_str(&sc))
5295        });
5296    });
5297}
5298
5299mod client_api;
5300mod control_socket;
5301mod images;
5302mod svg;
5303mod jcall;
5304mod ui_jni;