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_draw2d_text_scaled(
1826    text: YogStr,
1827    font: YogStr,
1828    x: f32,
1829    y: f32,
1830    scale: f32,
1831    color: u32,
1832    shadow: bool,
1833) {
1834    unsafe {
1835        let _: Option<()> = host!(
1836            "dev/yog/NativeDraw",
1837            "drawTextScaled",
1838            [S(text), S(font), F(x), F(y), F(scale), I(color as i32), Z(shadow)]
1839        );
1840    }
1841}
1842
1843unsafe extern "C" fn gfx_draw2d_blocks(blocks: YogStr, x: f32, y: f32, w: f32, h: f32, yaw: f32, pitch: f32) {
1844    if w <= 0.0 || h <= 0.0 {
1845        return;
1846    }
1847    unsafe {
1848        let _: Option<()> = host!(
1849            "dev/yog/NativeDraw",
1850            "drawBlocks",
1851            [S(blocks), F(x), F(y), F(w), F(h), F(yaw), F(pitch)]
1852        );
1853    }
1854}
1855
1856unsafe extern "C" fn gfx_text_width(text: YogStr, font: YogStr) -> f32 {
1857    unsafe { host!("dev/yog/NativeDraw", "textWidth", [S(text), S(font)]).unwrap_or(0.0) }
1858}
1859
1860unsafe extern "C" fn gfx_draw2d_item(id: YogStr, x: f32, y: f32, size: f32) {
1861    unsafe {
1862        let _: Option<()> = host!(
1863            "dev/yog/NativeDraw",
1864            "drawItem",
1865            [S(id), F(x), F(y), F(size)]
1866        );
1867    }
1868}
1869
1870// ── Static GFX function table (function pointers only — per-frame data filled at call time) ──
1871
1872static GFX_FN_TABLE: YogGfxApi = YogGfxApi {
1873    // Per-frame fields zeroed in the static; actual values are set on the stack per render call.
1874    screen_w: 0,
1875    screen_h: 0,
1876    delta_tick: 0.0,
1877    scale_factor: 1.0,
1878    view_proj: [0.0; 16],
1879    camera_pos: [0.0; 3],
1880    player_pos: [0.0; 3],
1881    _pad1: 0.0,
1882    buf_create: gfx_buf_create,
1883    buf_delete: gfx_buf_delete,
1884    buf_data: gfx_buf_data,
1885    buf_subdata: gfx_buf_subdata,
1886    vao_create: gfx_vao_create,
1887    vao_delete: gfx_vao_delete,
1888    vao_attrib: gfx_vao_attrib,
1889    vao_set_ebo: gfx_vao_set_ebo,
1890    prog_create: gfx_prog_create,
1891    prog_delete: gfx_prog_delete,
1892    prog_uniform_1i: gfx_prog_uniform_1i,
1893    prog_uniform_1f: gfx_prog_uniform_1f,
1894    prog_uniform_2f: gfx_prog_uniform_2f,
1895    prog_uniform_3f: gfx_prog_uniform_3f,
1896    prog_uniform_4f: gfx_prog_uniform_4f,
1897    prog_uniform_mat4: gfx_prog_uniform_mat4,
1898    tex_create: gfx_tex_create,
1899    tex_delete: gfx_tex_delete,
1900    tex_bind: gfx_tex_bind,
1901    tex_from_mc: gfx_tex_from_mc,
1902    draw_arrays: gfx_draw_arrays,
1903    draw_elements: gfx_draw_elements,
1904    set_blend: gfx_set_blend,
1905    set_depth: gfx_set_depth,
1906    set_scissor: gfx_set_scissor,
1907    clear_scissor: gfx_clear_scissor,
1908    set_viewport: gfx_set_viewport,
1909    draw2d_rect: gfx_draw2d_rect,
1910    draw2d_gradient: gfx_draw2d_gradient,
1911    draw2d_text: gfx_draw2d_text,
1912    draw2d_item: gfx_draw2d_item,
1913    draw2d_text_font: gfx_draw2d_text_font,
1914    text_width: gfx_text_width,
1915    draw2d_image: images::gfx_draw2d_image,
1916    image_size: images::gfx_image_size,
1917    draw2d_svg: svg::gfx_draw2d_svg,
1918    draw2d_entity: svg::gfx_draw2d_entity,
1919    draw2d_text_scaled: gfx_draw2d_text_scaled,
1920    draw2d_blocks: gfx_draw2d_blocks,
1921};
1922
1923// ── YogApi registration functions ─────────────────────────────────────────────
1924//
1925// ctx is *const RwLock<RuntimeHandlers> (cast from *mut c_void) — see
1926// `current_registration()` for how each entry gets tagged with the
1927// `LoadedModule` that's registering it.
1928
1929macro_rules! api_event {
1930    ($name:ident, $field:ident, $fn_ty:ty) => {
1931        unsafe extern "C" fn $name(ctx: *mut c_void, ud: *mut c_void, h: $fn_ty) {
1932            unsafe {
1933                let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1934                let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1935                handlers.$field.push((current_registration(), ud, h));
1936            }
1937        }
1938    };
1939}
1940
1941api_event!(api_on_block_break, block_break, YogBlockBreakFn);
1942api_event!(api_on_chat, chat, YogChatFn);
1943api_event!(api_on_player_join, player_join, YogPlayerFn);
1944api_event!(api_on_player_leave, player_leave, YogPlayerFn);
1945api_event!(api_on_use_item, use_item, YogUseItemFn);
1946api_event!(api_on_use_block, use_block, YogUseBlockFn);
1947api_event!(api_on_attack_entity, attack_entity, YogAttackEntityFn);
1948api_event!(api_on_entity_damage, entity_damage, YogEntityDamageFn);
1949api_event!(api_on_entity_death, entity_death, YogEntityDeathFn);
1950api_event!(api_on_entity_spawn, entity_spawn, YogEntitySpawnFn);
1951api_event!(
1952    api_on_player_place_block,
1953    player_place_block,
1954    YogPlaceBlockFn
1955);
1956api_event!(api_on_player_death, player_death, YogPlayerDeathFn);
1957api_event!(api_on_player_respawn, player_respawn, YogPlayerRespawnFn);
1958api_event!(api_on_advancement, advancement, YogAdvancementFn);
1959api_event!(api_on_entity_interact, entity_interact, YogEntityInteractFn);
1960api_event!(api_on_item_craft, item_craft, YogCraftFn);
1961api_event!(api_on_explosion, explosion, YogExplosionFn);
1962api_event!(api_on_item_pickup, item_pickup, YogItemPickupFn);
1963api_event!(api_on_player_move, player_move, YogPlayerMoveFn);
1964api_event!(api_on_container_open, container_open, YogContainerOpenFn);
1965api_event!(api_on_container_close, container_close, YogContainerCloseFn);
1966api_event!(api_on_projectile_hit, projectile_hit, YogProjectileHitFn);
1967api_event!(api_on_client_tick, client_tick, YogClientFn);
1968api_event!(api_on_hud_render, hud_render, YogHudRenderFn);
1969api_event!(api_on_world_render, world_render, YogWorldRenderFn);
1970api_event!(api_on_key_press, key_press, YogKeyPressFn);
1971api_event!(api_on_screen_open, screen_open, YogScreenFn);
1972api_event!(api_on_screen_close, screen_close, YogScreenFn);
1973api_event!(api_on_server_tick, server_tick, YogServerFn);
1974api_event!(api_on_server_started, server_started, YogServerFn);
1975api_event!(api_on_server_stopping, server_stopping, YogServerFn);
1976
1977unsafe extern "C" fn api_on_packet(
1978    ctx: *mut c_void,
1979    channel: YogStr,
1980    ud: *mut c_void,
1981    h: YogPacketFn,
1982) {
1983    unsafe {
1984        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
1985        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
1986        handlers
1987            .packets
1988            .insert(channel.as_str().to_owned(), (current_registration(), ud, h));
1989    }
1990}
1991
1992unsafe extern "C" fn api_on_client_packet(
1993    ctx: *mut c_void,
1994    channel: YogStr,
1995    ud: *mut c_void,
1996    h: YogPacketFn,
1997) {
1998    unsafe {
1999        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2000        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2001        handlers
2002            .client_packets
2003            .insert(channel.as_str().to_owned(), (current_registration(), ud, h));
2004    }
2005}
2006
2007unsafe extern "C" fn api_register_command(
2008    ctx: *mut c_void,
2009    name: YogStr,
2010    ud: *mut c_void,
2011    h: YogCommandFn,
2012) {
2013    unsafe {
2014        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2015        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2016        handlers
2017            .commands
2018            .entry(name.as_str().to_owned())
2019            .or_default()
2020            .push((current_registration(), None, ud, h));
2021    }
2022}
2023
2024unsafe extern "C" fn api_register_typed_command(
2025    ctx: *mut c_void,
2026    name: YogStr,
2027    schema: YogStr,
2028    ud: *mut c_void,
2029    h: YogCommandFn,
2030) {
2031    unsafe {
2032        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2033        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2034        let n = name.as_str().to_owned();
2035        handlers.commands.entry(n).or_default().push((
2036            current_registration(),
2037            Some(schema.as_str().to_owned()),
2038            ud,
2039            h,
2040        ));
2041    }
2042}
2043
2044unsafe extern "C" fn api_register_recipe_json(
2045    ctx: *mut c_void,
2046    namespace: YogStr,
2047    name: YogStr,
2048    json: YogStr,
2049) {
2050    unsafe {
2051        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2052        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2053        let ns = namespace.as_str().to_owned();
2054        let n = name.as_str().to_owned();
2055        let j = json.as_str().to_owned();
2056        // Feed the recipe to already-registered book renderers (crafting pages).
2057        let full_id = format!("{ns}:{n}");
2058        for r in handlers.book_renderers.lock().unwrap().values_mut() {
2059            r.add_recipe(full_id.clone(), &j);
2060        }
2061        handlers.recipes.push((ns, n, j));
2062    }
2063}
2064
2065unsafe extern "C" fn api_register_item(ctx: *mut c_void, def: *const YogItemDef) {
2066    unsafe {
2067        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2068        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2069        let d = &*def;
2070        let food = if d.food_nutrition > 0 {
2071            Some(FoodDef {
2072                nutrition: d.food_nutrition,
2073                saturation: d.food_saturation,
2074                can_always_eat: d.food_always_eat,
2075            })
2076        } else {
2077            None
2078        };
2079        handlers.items.push(ItemDef {
2080            id: d.id.as_str().to_owned(),
2081            max_stack: d.max_stack as u8,
2082            name: if d.name.is_empty() {
2083                None
2084            } else {
2085                Some(d.name.as_str().to_owned())
2086            },
2087            tooltip: if d.tooltip.is_empty() {
2088                None
2089            } else {
2090                Some(d.tooltip.as_str().to_owned())
2091            },
2092            max_damage: d.max_damage,
2093            fire_resistant: d.fire_resistant,
2094            fuel_ticks: d.fuel_ticks,
2095            food,
2096        });
2097    }
2098}
2099
2100unsafe extern "C" fn api_register_block(ctx: *mut c_void, def: *const YogBlockDef) {
2101    unsafe {
2102        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2103        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2104        let d = &*def;
2105        handlers.blocks.push(BlockDef {
2106            id: d.id.as_str().to_owned(),
2107            hardness: d.hardness,
2108            resistance: d.resistance,
2109            name: if d.name.is_empty() {
2110                None
2111            } else {
2112                Some(d.name.as_str().to_owned())
2113            },
2114            light_level: d.light_level,
2115            sound: if d.sound.is_empty() {
2116                None
2117            } else {
2118                Some(d.sound.as_str().to_owned())
2119            },
2120            requires_tool: d.requires_tool,
2121            no_collision: d.no_collision,
2122            slipperiness: d.slipperiness,
2123            shape: if d.shape == [0.0f32; 6] {
2124                None
2125            } else {
2126                Some(d.shape)
2127            },
2128            connects: d.connects,
2129            connect_groups: if d.connect_groups.is_empty() {
2130                Vec::new()
2131            } else {
2132                d.connect_groups
2133                    .as_str()
2134                    .split(',')
2135                    .map(str::to_owned)
2136                    .collect()
2137            },
2138            inventory_id: if d.inventory_id.is_empty() {
2139                None
2140            } else {
2141                Some(d.inventory_id.as_str().to_owned())
2142            },
2143            redstone_source: d.redstone_source,
2144        });
2145    }
2146}
2147
2148unsafe extern "C" fn api_register_inventory(
2149    ctx: *mut c_void,
2150    def: *const yog_abi::YogInventoryDef,
2151) {
2152    unsafe {
2153        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2154        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2155        let d = &*def;
2156        handlers.inventories.push(yog_inventory::InventoryDef {
2157            id: d.id.as_str().to_owned(),
2158            slot_count: d.slot_count as usize,
2159            layout: yog_inventory::decode_layout(d.layout.as_str()),
2160            include_player_inventory: d.include_player_inventory,
2161            player_inv_offset: (d.player_inv_x, d.player_inv_y),
2162            background_texture: if d.background_texture.is_empty() {
2163                None
2164            } else {
2165                Some(d.background_texture.as_str().to_owned())
2166            },
2167            title: d.title.as_str().to_owned(),
2168            background_size: if d.background_w > 0.0 && d.background_h > 0.0 {
2169                Some((d.background_w, d.background_h))
2170            } else {
2171                None
2172            },
2173        });
2174    }
2175}
2176
2177unsafe extern "C" fn api_schedule_once(
2178    ctx: *mut c_void,
2179    delay_ticks: u64,
2180    ud: *mut c_void,
2181    h: YogScheduledFn,
2182) {
2183    unsafe {
2184        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2185        let handlers = handlers_lock.write().expect("handlers lock poisoned");
2186        handlers
2187            .scheduler
2188            .lock()
2189            .expect("scheduler poisoned")
2190            .once_tasks
2191            .push(OnceTask {
2192                module: current_registration(),
2193                delay_remaining: delay_ticks,
2194                ud,
2195                f: h,
2196            });
2197    }
2198}
2199
2200unsafe extern "C" fn api_schedule_repeating(
2201    ctx: *mut c_void,
2202    period_ticks: u64,
2203    ud: *mut c_void,
2204    h: YogScheduledFn,
2205) {
2206    unsafe {
2207        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2208        let handlers = handlers_lock.write().expect("handlers lock poisoned");
2209        handlers
2210            .scheduler
2211            .lock()
2212            .expect("scheduler poisoned")
2213            .repeating_tasks
2214            .push(RepeatingTask {
2215                module: current_registration(),
2216                period: period_ticks,
2217                ticks_left: period_ticks,
2218                ud,
2219                f: h,
2220            });
2221    }
2222}
2223
2224unsafe extern "C" fn api_register_startup_grant(
2225    ctx: *mut c_void,
2226    grant: *const YogStartupGrantDef,
2227) {
2228    unsafe {
2229        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2230        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2231        let g = &*grant;
2232        let items: Vec<String> = if g.items.is_empty() {
2233            Vec::new()
2234        } else {
2235            {
2236                g.items
2237                    .as_str()
2238                    .split('|')
2239                    .map(|s: &str| s.to_owned())
2240                    .collect()
2241            }
2242        };
2243        let book = if g.book.is_empty() {
2244            None
2245        } else {
2246            Some(g.book.as_str().to_owned())
2247        };
2248        let command = if g.command.is_empty() {
2249            None
2250        } else {
2251            Some(g.command.as_str().to_owned())
2252        };
2253        handlers.startup_grants.push(yog_registry::StartupGrant {
2254            id: g.id.as_str().to_owned(),
2255            items,
2256            book,
2257            command,
2258        });
2259    }
2260}
2261
2262unsafe extern "C" fn api_register_book(ctx: *mut c_void, book_id: YogStr, book_json: YogStr) {
2263    unsafe {
2264        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2265        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2266        let id = book_id.as_str().to_owned();
2267        let json = book_json.as_str().to_owned();
2268        handlers.books.insert(id.clone(), json.clone());
2269        // Parse JSON → Book → BookRenderer so rendering works without Java round-trip.
2270        match serde_json::from_str::<yog_book::Book>(&json) {
2271            Ok(book) => {
2272                let mut renderer = yog_book::BookRenderer::new(book);
2273                // i18n books carry translation keys; shown in the game language.
2274                renderer.set_translator(translate);
2275                // Recipes may register before the book — replay them into the renderer.
2276                for (ns, name, j) in &handlers.recipes {
2277                    renderer.add_recipe(format!("{ns}:{name}"), j);
2278                }
2279                handlers.book_renderers.lock().unwrap().insert(id, renderer);
2280            }
2281            Err(e) => {
2282                yog_logging::warn!(
2283                    "book '{}': failed to parse JSON for Rust renderer: {}",
2284                    id,
2285                    e
2286                );
2287            }
2288        }
2289    }
2290}
2291
2292unsafe extern "C" fn api_register_dimension_json(ctx: *mut c_void, id: YogStr, json: YogStr) {
2293    unsafe {
2294        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2295        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2296        let id = id.as_str().to_owned();
2297        let json = json.as_str().to_owned();
2298        yog_logging::info!("registered dimension type: {}", id);
2299        handlers.dimensions.insert(id, json);
2300    }
2301}
2302
2303unsafe extern "C" fn api_register_chunk_generator(
2304    ctx: *mut c_void,
2305    id: YogStr,
2306    ud: *mut c_void,
2307    h: yog_abi::YogChunkGeneratorFn,
2308) {
2309    unsafe {
2310        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2311        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2312        let id = id.as_str().to_owned();
2313        yog_logging::info!("registered chunk generator for dimension: {}", id);
2314        handlers
2315            .chunk_generators
2316            .insert(id, (current_registration(), ud, h));
2317    }
2318}
2319
2320unsafe extern "C" fn api_register_ui(
2321    ctx: *mut c_void,
2322    ui_id: YogStr,
2323    _layout_json: YogStr,
2324    ud: *mut c_void,
2325    h: yog_abi::YogUIEventFn,
2326) {
2327    unsafe {
2328        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2329        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2330        let id = ui_id.as_str().to_owned();
2331        handlers
2332            .ui_handlers
2333            .insert(id.clone(), (current_registration(), ud, h));
2334        yog_logging::info!("registered UI handler: {}", id);
2335    }
2336}
2337
2338unsafe extern "C" fn api_on_ui_render(
2339    ctx: *mut c_void,
2340    ui_id: YogStr,
2341    ud: *mut c_void,
2342    h: YogHudRenderFn,
2343) {
2344    unsafe {
2345        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2346        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2347        let id = ui_id.as_str().to_owned();
2348        handlers
2349            .ui_render_handlers
2350            .entry(id)
2351            .or_default()
2352            .push((current_registration(), ud, h));
2353    }
2354}
2355
2356/// Strip TSV separators so one mod can't corrupt the line format.
2357fn tsv_clean(v: &str) -> String {
2358    v.replace(['\t', '\n', '\r'], " ")
2359}
2360
2361unsafe extern "C" fn api_ui_open(_ctx: *mut c_void, ui_id: YogStr, modal: bool, pause: bool) {
2362    unsafe {
2363        if ui_id.as_str().is_empty() {
2364            return;
2365        }
2366        // Java side schedules onto the render thread; no-op on dedicated servers.
2367        let _: Option<()> = bridge!("openUI", [S(ui_id), Z(modal), Z(pause)]);
2368    }
2369}
2370
2371unsafe extern "C" fn api_ui_close(_ctx: *mut c_void, ui_id: YogStr) {
2372    unsafe {
2373        // Java side defers onto the render thread (after the current event) and
2374        // checks the open screen is that Yog UI; no-op on dedicated servers.
2375        let _: Option<()> = bridge!("closeUI", [S(ui_id)]);
2376    }
2377}
2378
2379unsafe extern "C" fn api_ui_consume_event(_ctx: *mut c_void) {
2380    ui_jni::consume_event();
2381}
2382
2383unsafe extern "C" fn api_translate(_ctx: *mut c_void, key: YogStr) -> YogOwnedStr {
2384    unsafe { translate(key.as_str()).map_or(YogOwnedStr::NONE, YogOwnedStr::from_string) }
2385}
2386
2387/// The game's text for translation `key` in the current language — `None`
2388/// when no lang file has it (or there's no JVM).
2389fn translate(key: &str) -> Option<String> {
2390    if key.is_empty() {
2391        return None;
2392    }
2393    unsafe { bridge!("translate", [S(YogStr::from_str(key))]) }
2394}
2395
2396unsafe extern "C" fn api_mods_list(_ctx: *mut c_void) -> YogOwnedStr {
2397    unsafe {
2398        let mut lines: Vec<String> = Vec::new();
2399        let infos = MOD_INFOS.lock().expect("mod infos lock poisoned").clone();
2400        for m in &infos {
2401            // A `.yres` name/description shows in the player's language.
2402            let localized = |text: &str, key: &str| {
2403                translate(key).map_or_else(|| text.to_string(), |t| tsv_clean(&t))
2404            };
2405            lines.push(format!(
2406                "yog\t{}\t{}\t{}\t{}\t{}",
2407                m[0],
2408                localized(&m[1], &m[5]),
2409                m[2],
2410                m[3],
2411                localized(&m[4], &m[6])
2412            ));
2413        }
2414        // Loader-side (Java) mods, if the host provides them.
2415        let platform: Option<String> = bridge!("listPlatformMods", []);
2416        for line in platform.iter().flat_map(|s| s.lines()) {
2417            if !line.trim().is_empty() {
2418                lines.push(format!("platform\t{line}"));
2419            }
2420        }
2421        YogOwnedStr::from_string(lines.join("\n"))
2422    }
2423}
2424
2425unsafe extern "C" fn api_register_menu_entry(ctx: *mut c_void, label: YogStr, ui_id: YogStr) {
2426    unsafe {
2427        let handlers_lock = &*(ctx as *const RwLock<RuntimeHandlers>);
2428        let mut handlers = handlers_lock.write().expect("handlers lock poisoned");
2429        let l = label.as_str().to_owned();
2430        let u = ui_id.as_str().to_owned();
2431        handlers.menu_entries.push((l, u));
2432    }
2433}
2434
2435#[unsafe(no_mangle)]
2436pub extern "system" fn Java_dev_yog_NativeBridge_nativeBookJson<'l>(
2437    mut env: JNIEnv<'l>,
2438    _class: JClass<'l>,
2439    book_id: JString<'l>,
2440) -> jstring {
2441    let id = jstr!(env, book_id, std::ptr::null_mut());
2442    let json = handlers()
2443        .books
2444        .get(&id)
2445        .cloned()
2446        .unwrap_or_else(|| "null".to_string());
2447    env.new_string(json)
2448        .map(jni::objects::JString::into_raw)
2449        .unwrap_or(std::ptr::null_mut())
2450}
2451
2452// ── World queries & effects (ABI minor 1) ────────────────────────────────────
2453//
2454// Each of these forwards to a `NativeBridge` static of the same shape; the JNI
2455// signature is derived from the argument list so the two cannot drift apart.
2456
2457fn vec_args(v: YogVec3) -> [JArg<'static>; 3] {
2458    [JArg::D(v.x), JArg::D(v.y), JArg::D(v.z)]
2459}
2460
2461fn pos_args(p: YogBlockPos) -> [JArg<'static>; 3] {
2462    [JArg::I(p.x), JArg::I(p.y), JArg::I(p.z)]
2463}
2464
2465unsafe extern "C" fn srv_entity_info(_ctx: *mut c_void, uuid: YogStr) -> YogOwnedStr {
2466    unsafe { owned(bridge!("entityInfo", [JArg::S(uuid)])) }
2467}
2468
2469unsafe extern "C" fn srv_entities_in_box(
2470    _ctx: *mut c_void,
2471    dim: YogStr,
2472    min: YogVec3,
2473    max: YogVec3,
2474) -> YogOwnedStr {
2475    unsafe {
2476        let [a, b, c] = vec_args(min);
2477        let [d, e, f] = vec_args(max);
2478        owned(bridge!("entitiesInBox", [JArg::S(dim), a, b, c, d, e, f]))
2479    }
2480}
2481
2482unsafe extern "C" fn srv_raycast_block(
2483    _ctx: *mut c_void,
2484    dim: YogStr,
2485    from: YogVec3,
2486    to: YogVec3,
2487) -> YogOwnedStr {
2488    unsafe {
2489        let [a, b, c] = vec_args(from);
2490        let [d, e, f] = vec_args(to);
2491        owned(bridge!("raycastBlock", [JArg::S(dim), a, b, c, d, e, f]))
2492    }
2493}
2494
2495unsafe extern "C" fn srv_raycast_entity(
2496    _ctx: *mut c_void,
2497    dim: YogStr,
2498    from: YogVec3,
2499    to: YogVec3,
2500    exclude: YogStr,
2501) -> YogOwnedStr {
2502    unsafe {
2503        let [a, b, c] = vec_args(from);
2504        let [d, e, f] = vec_args(to);
2505        owned(bridge!(
2506            "raycastEntity",
2507            [JArg::S(dim), a, b, c, d, e, f, JArg::S(exclude)]
2508        ))
2509    }
2510}
2511
2512unsafe extern "C" fn srv_block_info(
2513    _ctx: *mut c_void,
2514    dim: YogStr,
2515    pos: YogBlockPos,
2516) -> YogOwnedStr {
2517    unsafe {
2518        let [x, y, z] = pos_args(pos);
2519        owned(bridge!("blockInfo", [JArg::S(dim), x, y, z]))
2520    }
2521}
2522
2523unsafe extern "C" fn srv_break_block(
2524    _ctx: *mut c_void,
2525    dim: YogStr,
2526    pos: YogBlockPos,
2527    drop: bool,
2528    breaker: YogStr,
2529) -> bool {
2530    unsafe {
2531        let [x, y, z] = pos_args(pos);
2532        bridge!(
2533            "breakBlock",
2534            [JArg::S(dim), x, y, z, JArg::Z(drop), JArg::S(breaker)]
2535        )
2536        .unwrap_or(false)
2537    }
2538}
2539
2540unsafe extern "C" fn srv_place_block_from_slot(
2541    _ctx: *mut c_void,
2542    player: YogStr,
2543    slot: u32,
2544    dim: YogStr,
2545    pos: YogBlockPos,
2546) -> bool {
2547    unsafe {
2548        let [x, y, z] = pos_args(pos);
2549        bridge!(
2550            "placeBlockFromSlot",
2551            [JArg::S(player), JArg::I(slot as i32), JArg::S(dim), x, y, z]
2552        )
2553        .unwrap_or(false)
2554    }
2555}
2556
2557unsafe extern "C" fn srv_bonemeal(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> bool {
2558    unsafe {
2559        let [x, y, z] = pos_args(pos);
2560        bridge!("bonemeal", [JArg::S(dim), x, y, z]).unwrap_or(false)
2561    }
2562}
2563
2564unsafe extern "C" fn srv_explode(
2565    _ctx: *mut c_void,
2566    dim: YogStr,
2567    pos: YogVec3,
2568    power: f32,
2569    fire: bool,
2570    source: YogStr,
2571) -> bool {
2572    unsafe {
2573        let [x, y, z] = vec_args(pos);
2574        bridge!(
2575            "explode",
2576            [
2577                JArg::S(dim),
2578                x,
2579                y,
2580                z,
2581                JArg::F(power),
2582                JArg::Z(fire),
2583                JArg::S(source),
2584            ]
2585        )
2586        .unwrap_or(false)
2587    }
2588}
2589
2590unsafe extern "C" fn srv_tellraw(_ctx: *mut c_void, player: YogStr, json: YogStr) -> bool {
2591    unsafe { bridge!("tellraw", [JArg::S(player), JArg::S(json)]).unwrap_or(false) }
2592}
2593
2594unsafe extern "C" fn srv_entity_damage(
2595    _ctx: *mut c_void,
2596    uuid: YogStr,
2597    amount: f32,
2598    damage_type: YogStr,
2599    attacker: YogStr,
2600) -> bool {
2601    unsafe {
2602        bridge!(
2603            "entityDamage",
2604            [
2605                JArg::S(uuid),
2606                JArg::F(amount),
2607                JArg::S(damage_type),
2608                JArg::S(attacker),
2609            ]
2610        )
2611        .unwrap_or(false)
2612    }
2613}
2614
2615unsafe extern "C" fn srv_entity_set_fire_ticks(
2616    _ctx: *mut c_void,
2617    uuid: YogStr,
2618    ticks: i32,
2619) -> bool {
2620    unsafe { bridge!("entitySetFireTicks", [JArg::S(uuid), JArg::I(ticks)]).unwrap_or(false) }
2621}
2622
2623unsafe extern "C" fn srv_player_add_xp(_ctx: *mut c_void, player: YogStr, amount: i32) -> bool {
2624    unsafe { bridge!("playerAddXp", [JArg::S(player), JArg::I(amount)]).unwrap_or(false) }
2625}
2626
2627unsafe extern "C" fn srv_player_set_may_fly(
2628    _ctx: *mut c_void,
2629    player: YogStr,
2630    may_fly: bool,
2631) -> bool {
2632    unsafe { bridge!("playerSetMayFly", [JArg::S(player), JArg::Z(may_fly)]).unwrap_or(false) }
2633}
2634
2635unsafe extern "C" fn srv_player_info(_ctx: *mut c_void, player: YogStr) -> YogOwnedStr {
2636    unsafe { owned(bridge!("playerInfo", [JArg::S(player)])) }
2637}
2638
2639unsafe extern "C" fn srv_spawn_item(
2640    _ctx: *mut c_void,
2641    dim: YogStr,
2642    pos: YogVec3,
2643    vel: YogVec3,
2644    item_id: YogStr,
2645    count: u32,
2646    snbt: YogStr,
2647) -> YogOwnedStr {
2648    unsafe {
2649        let [a, b, c] = vec_args(pos);
2650        let [d, e, f] = vec_args(vel);
2651        owned(bridge!(
2652            "spawnItem",
2653            [
2654                JArg::S(dim),
2655                a,
2656                b,
2657                c,
2658                d,
2659                e,
2660                f,
2661                JArg::S(item_id),
2662                JArg::I(count as i32),
2663                JArg::S(snbt),
2664            ]
2665        ))
2666    }
2667}
2668
2669unsafe extern "C" fn srv_item_entity_stack(_ctx: *mut c_void, uuid: YogStr) -> YogOwnedStr {
2670    unsafe { owned(bridge!("itemEntityStack", [JArg::S(uuid)])) }
2671}
2672
2673unsafe extern "C" fn srv_actionbar_json(_ctx: *mut c_void, player: YogStr, json: YogStr) -> bool {
2674    unsafe { bridge!("actionbarJson", [JArg::S(player), JArg::S(json)]).unwrap_or(false) }
2675}
2676
2677unsafe extern "C" fn srv_use_item_on_block(
2678    _ctx: *mut c_void,
2679    dim: YogStr,
2680    pos: YogBlockPos,
2681    face: u8,
2682    item_id: YogStr,
2683    player: YogStr,
2684) -> bool {
2685    unsafe {
2686        let [x, y, z] = pos_args(pos);
2687        bridge!(
2688            "useItemOnBlock",
2689            [
2690                JArg::S(dim),
2691                x,
2692                y,
2693                z,
2694                JArg::I(face as i32),
2695                JArg::S(item_id),
2696                JArg::S(player),
2697            ]
2698        )
2699        .unwrap_or(false)
2700    }
2701}
2702
2703unsafe extern "C" fn srv_pour_fluid(
2704    _ctx: *mut c_void,
2705    dim: YogStr,
2706    pos: YogBlockPos,
2707    bucket_item_id: YogStr,
2708) -> bool {
2709    unsafe {
2710        let [x, y, z] = pos_args(pos);
2711        bridge!(
2712            "pourFluid",
2713            [JArg::S(dim), x, y, z, JArg::S(bucket_item_id)]
2714        )
2715        .unwrap_or(false)
2716    }
2717}
2718
2719unsafe extern "C" fn srv_has_entity_tag(
2720    _ctx: *mut c_void,
2721    entity_type: YogStr,
2722    tag: YogStr,
2723) -> bool {
2724    unsafe { bridge!("hasEntityTag", [JArg::S(entity_type), JArg::S(tag)]).unwrap_or(false) }
2725}
2726
2727unsafe extern "C" fn srv_item_block(_ctx: *mut c_void, item_id: YogStr) -> YogOwnedStr {
2728    unsafe { owned(bridge!("itemBlock", [JArg::S(item_id)])) }
2729}
2730
2731unsafe extern "C" fn srv_place_feature(
2732    _ctx: *mut c_void,
2733    dim: YogStr,
2734    pos: YogBlockPos,
2735    feature_id: YogStr,
2736) -> bool {
2737    unsafe {
2738        let [x, y, z] = pos_args(pos);
2739        bridge!("placeFeature", [JArg::S(dim), x, y, z, JArg::S(feature_id)]).unwrap_or(false)
2740    }
2741}
2742
2743unsafe extern "C" fn srv_item_entity_set_stack(
2744    _ctx: *mut c_void,
2745    uuid: YogStr,
2746    item_id: YogStr,
2747    count: u32,
2748    snbt: YogStr,
2749) -> bool {
2750    unsafe {
2751        bridge!(
2752            "itemEntitySetStack",
2753            [
2754                JArg::S(uuid),
2755                JArg::S(item_id),
2756                JArg::I(count as i32),
2757                JArg::S(snbt),
2758            ]
2759        )
2760        .unwrap_or(false)
2761    }
2762}
2763
2764// ── Table constructors ────────────────────────────────────────────────────────
2765
2766fn build_server_table() -> YogServer {
2767    YogServer {
2768        ctx: std::ptr::null_mut(),
2769        abi_version: ABI_VERSION,
2770        size: std::mem::size_of::<YogServer>() as u32,
2771        free_str: yog_free_str,
2772        broadcast: srv_broadcast,
2773        get_block: srv_get_block,
2774        set_block: srv_set_block,
2775        world_time: srv_world_time,
2776        set_time: srv_set_time,
2777        is_raining: srv_is_raining,
2778        set_weather: srv_set_weather,
2779        give_item: srv_give_item,
2780        player_teleport: srv_player_teleport,
2781        send_to_player: srv_send_to_player,
2782        send_to_server: srv_send_to_server,
2783        kick_player: srv_kick_player,
2784        set_gamemode: srv_set_gamemode,
2785        send_title: srv_send_title,
2786        send_actionbar: srv_send_actionbar,
2787        play_sound: srv_play_sound,
2788        play_sound_player: srv_play_sound_player,
2789        entity_teleport: srv_entity_teleport,
2790        entity_position: srv_entity_position,
2791        entity_rotation: srv_entity_rotation,
2792        entity_health: srv_entity_health,
2793        entity_set_health: srv_entity_set_health,
2794        entity_kill: srv_entity_kill,
2795        spawn_entity: srv_spawn_entity,
2796        entity_add_effect: srv_entity_add_effect,
2797        entity_remove_effect: srv_entity_remove_effect,
2798        entity_clear_effects: srv_entity_clear_effects,
2799        entity_velocity: srv_entity_velocity,
2800        entity_set_velocity: srv_entity_set_velocity,
2801        entity_add_velocity: srv_entity_add_velocity,
2802        has_item_tag: srv_has_item_tag,
2803        has_block_tag: srv_has_block_tag,
2804        drop_loot: srv_drop_loot,
2805        scoreboard_get: srv_scoreboard_get,
2806        scoreboard_set: srv_scoreboard_set,
2807        scoreboard_add: srv_scoreboard_add,
2808        bossbar_create: srv_bossbar_create,
2809        bossbar_remove: srv_bossbar_remove,
2810        bossbar_set_title: srv_bossbar_set_title,
2811        bossbar_set_progress: srv_bossbar_set_progress,
2812        bossbar_set_color: srv_bossbar_set_color,
2813        bossbar_add_player: srv_bossbar_add_player,
2814        bossbar_remove_player: srv_bossbar_remove_player,
2815        bossbar_set_visible: srv_bossbar_set_visible,
2816        game_dir: srv_game_dir,
2817        get_block_nbt: srv_get_block_nbt,
2818        set_block_nbt: srv_set_block_nbt,
2819        get_inventory_slot: srv_get_inventory_slot,
2820        set_inventory_slot: srv_set_inventory_slot,
2821        player_inventory: srv_player_inventory,
2822        player_set_slot: srv_player_set_slot,
2823        player_teleport_dim: srv_player_teleport_dim,
2824        entity_teleport_dim: srv_entity_teleport_dim,
2825        online_players: srv_online_players,
2826        world_entity_count: srv_world_entity_count,
2827        entity_get_nbt: srv_entity_get_nbt,
2828        entity_set_nbt: srv_entity_set_nbt,
2829        spawn_particles: srv_spawn_particles,
2830        entity_attribute_get: srv_entity_attribute_get,
2831        entity_attribute_set: srv_entity_attribute_set,
2832        get_held_item_nbt: srv_get_held_item_nbt,
2833        set_held_item_nbt: srv_set_held_item_nbt,
2834        get_offhand_item_nbt: srv_get_offhand_item_nbt,
2835        set_offhand_item_nbt: srv_set_offhand_item_nbt,
2836        get_slot_item: srv_get_slot_item,
2837        set_slot_item: srv_set_slot_item,
2838        entity_info: srv_entity_info,
2839        entities_in_box: srv_entities_in_box,
2840        raycast_block: srv_raycast_block,
2841        raycast_entity: srv_raycast_entity,
2842        block_info: srv_block_info,
2843        break_block: srv_break_block,
2844        place_block_from_slot: srv_place_block_from_slot,
2845        bonemeal: srv_bonemeal,
2846        explode: srv_explode,
2847        tellraw: srv_tellraw,
2848        entity_damage: srv_entity_damage,
2849        entity_set_fire_ticks: srv_entity_set_fire_ticks,
2850        player_add_xp: srv_player_add_xp,
2851        player_set_may_fly: srv_player_set_may_fly,
2852        player_info: srv_player_info,
2853        spawn_item: srv_spawn_item,
2854        item_entity_stack: srv_item_entity_stack,
2855        item_entity_set_stack: srv_item_entity_set_stack,
2856        actionbar_json: srv_actionbar_json,
2857        use_item_on_block: srv_use_item_on_block,
2858        pour_fluid: srv_pour_fluid,
2859        has_entity_tag: srv_has_entity_tag,
2860        item_block: srv_item_block,
2861        place_feature: srv_place_feature,
2862        set_item_cooldown: srv_set_item_cooldown,
2863        entity_teleport_with_passengers: srv_entity_teleport_with_passengers,
2864        redstone_power: srv_redstone_power,
2865        set_redstone_output: srv_set_redstone_output,
2866        get_block_state: srv_get_block_state,
2867        set_block_state: srv_set_block_state,
2868        world_is_thundering: srv_world_is_thundering,
2869        world_set_thunder: srv_world_set_thunder,
2870        fill_region: srv_fill_region,
2871        scan_region: srv_scan_region,
2872    }
2873}
2874
2875// ── ABI minor 3 ──────────────────────────────────────────────────────────────
2876
2877unsafe extern "C" fn srv_set_item_cooldown(_ctx: *mut c_void, player: YogStr, item_id: YogStr, ticks: i32) -> bool {
2878    unsafe { bridge!("setItemCooldown", [S(player), S(item_id), I(ticks)]).unwrap_or(false) }
2879}
2880
2881unsafe extern "C" fn srv_entity_teleport_with_passengers(_ctx: *mut c_void, uuid: YogStr, dim: YogStr, pos: YogVec3) -> bool {
2882    unsafe {
2883        bridge!("entityTeleportWithPassengers", [S(uuid), S(dim), D(pos.x), D(pos.y), D(pos.z)]).unwrap_or(false)
2884    }
2885}
2886
2887unsafe extern "C" fn srv_redstone_power(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> i32 {
2888    unsafe { bridge!("redstonePower", [S(dim), I(pos.x), I(pos.y), I(pos.z)]).unwrap_or(-1) }
2889}
2890
2891unsafe extern "C" fn srv_set_redstone_output(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos, level: i32) -> bool {
2892    unsafe { bridge!("setRedstoneOutput", [S(dim), I(pos.x), I(pos.y), I(pos.z), I(level)]).unwrap_or(false) }
2893}
2894
2895unsafe extern "C" fn srv_get_block_state(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos) -> YogOwnedStr {
2896    unsafe { owned(bridge!("getBlockState", [S(dim), I(pos.x), I(pos.y), I(pos.z)])) }
2897}
2898
2899unsafe extern "C" fn srv_set_block_state(_ctx: *mut c_void, dim: YogStr, pos: YogBlockPos, state: YogStr) -> bool {
2900    unsafe { bridge!("setBlockState", [S(dim), I(pos.x), I(pos.y), I(pos.z), S(state)]).unwrap_or(false) }
2901}
2902
2903unsafe extern "C" fn srv_world_is_thundering(_ctx: *mut c_void, dim: YogStr) -> bool {
2904    unsafe { bridge!("worldIsThundering", [S(dim)]).unwrap_or(false) }
2905}
2906
2907unsafe extern "C" fn srv_world_set_thunder(_ctx: *mut c_void, dim: YogStr, thundering: bool, duration_ticks: i32) -> bool {
2908    unsafe { bridge!("worldSetThunder", [S(dim), Z(thundering), I(duration_ticks)]).unwrap_or(false) }
2909}
2910
2911unsafe extern "C" fn srv_fill_region(
2912    _ctx: *mut c_void,
2913    dim: YogStr,
2914    from: YogBlockPos,
2915    to: YogBlockPos,
2916    state: YogStr,
2917) -> i32 {
2918    unsafe {
2919        bridge!(
2920            "fillRegion",
2921            [S(dim), I(from.x), I(from.y), I(from.z), I(to.x), I(to.y), I(to.z), S(state)]
2922        )
2923        .unwrap_or(-1)
2924    }
2925}
2926
2927unsafe extern "C" fn srv_scan_region(_ctx: *mut c_void, dim: YogStr, from: YogBlockPos, to: YogBlockPos) -> YogOwnedStr {
2928    unsafe { owned(bridge!("scanRegion", [S(dim), I(from.x), I(from.y), I(from.z), I(to.x), I(to.y), I(to.z)])) }
2929}
2930
2931// ── Inter-mod communication ──────────────────────────────────────────────────
2932
2933unsafe extern "C" fn interop_export_impl(
2934    _ctx: *mut c_void,
2935    mod_id: YogStr,
2936    symbol: YogStr,
2937    ptr: *const c_void,
2938) {
2939    unsafe {
2940        let mod_id = mod_id.as_str();
2941        let sym = symbol.as_str();
2942        let registry = INTEROP_SYMBOLS.get_or_init(|| Mutex::new(HashMap::new()));
2943        registry
2944            .lock()
2945            .expect("interop symbols lock poisoned")
2946            .entry(mod_id.to_string())
2947            .or_default()
2948            .insert(sym.to_string(), ptr as usize);
2949    }
2950}
2951
2952unsafe extern "C" fn interop_import_impl(
2953    _ctx: *mut c_void,
2954    mod_id: YogStr,
2955    symbol: YogStr,
2956) -> *const c_void {
2957    unsafe {
2958        let mod_id = mod_id.as_str();
2959        let sym = symbol.as_str();
2960        let registry = INTEROP_SYMBOLS.get_or_init(|| Mutex::new(HashMap::new()));
2961        registry
2962            .lock()
2963            .expect("interop symbols lock poisoned")
2964            .get(mod_id)
2965            .and_then(|m: &HashMap<String, usize>| m.get(sym))
2966            .map(|&ptr| ptr as *const c_void)
2967            .unwrap_or(std::ptr::null())
2968    }
2969}
2970
2971fn build_api_table(ctx: *const RwLock<RuntimeHandlers>, server: *const YogServer) -> YogApi {
2972    YogApi {
2973        abi_version: ABI_VERSION,
2974        size: std::mem::size_of::<YogApi>() as u32,
2975        ctx: ctx as *mut c_void,
2976        server,
2977        on_block_break: api_on_block_break,
2978        on_chat: api_on_chat,
2979        on_player_join: api_on_player_join,
2980        on_player_leave: api_on_player_leave,
2981        on_use_item: api_on_use_item,
2982        on_use_block: api_on_use_block,
2983        on_attack_entity: api_on_attack_entity,
2984        on_entity_damage: api_on_entity_damage,
2985        on_entity_death: api_on_entity_death,
2986        on_entity_spawn: api_on_entity_spawn,
2987        on_player_place_block: api_on_player_place_block,
2988        on_player_death: api_on_player_death,
2989        on_player_respawn: api_on_player_respawn,
2990        on_advancement: api_on_advancement,
2991        on_entity_interact: api_on_entity_interact,
2992        on_item_craft: api_on_item_craft,
2993        on_explosion: api_on_explosion,
2994        on_item_pickup: api_on_item_pickup,
2995        on_player_move: api_on_player_move,
2996        on_container_open: api_on_container_open,
2997        on_container_close: api_on_container_close,
2998        on_projectile_hit: api_on_projectile_hit,
2999        on_server_tick: api_on_server_tick,
3000        on_server_started: api_on_server_started,
3001        on_server_stopping: api_on_server_stopping,
3002        on_packet: api_on_packet,
3003        on_client_packet: api_on_client_packet,
3004        register_command: api_register_command,
3005        register_typed_command: api_register_typed_command,
3006        register_recipe_json: api_register_recipe_json,
3007        register_item: api_register_item,
3008        register_block: api_register_block,
3009        schedule_once: api_schedule_once,
3010        schedule_repeating: api_schedule_repeating,
3011        on_client_tick: api_on_client_tick,
3012        on_hud_render: api_on_hud_render,
3013        on_key_press: api_on_key_press,
3014        on_screen_open: api_on_screen_open,
3015        on_screen_close: api_on_screen_close,
3016        on_world_render: api_on_world_render,
3017        register_startup_grant: api_register_startup_grant,
3018        register_book: api_register_book,
3019        register_ui: api_register_ui,
3020        on_ui_render: api_on_ui_render,
3021        register_menu_entry: api_register_menu_entry,
3022        mods_list: api_mods_list,
3023        free_str: yog_free_str,
3024        ui_open: api_ui_open,
3025        register_inventory: api_register_inventory,
3026        interop_export: interop_export_impl,
3027        interop_import: interop_import_impl,
3028        register_dimension_json: api_register_dimension_json,
3029        register_chunk_generator: api_register_chunk_generator,
3030        ui_close: api_ui_close,
3031        ui_consume_event: api_ui_consume_event,
3032        translate: api_translate,
3033        ui_open_flags: client_api::api_ui_open_flags,
3034        client_play_sound: client_api::api_client_play_sound,
3035        client_stop_sound: client_api::api_client_stop_sound,
3036        client_held_item: client_api::api_client_held_item,
3037        client_key_down: client_api::api_client_key_down,
3038    }
3039}
3040
3041// ── Mod loading ───────────────────────────────────────────────────────────────
3042
3043fn platform_tag() -> String {
3044    format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
3045}
3046
3047type AbiVersionFn = unsafe extern "C" fn() -> u32;
3048/// Second arg is a null-terminated C string: the mod's `id` from its manifest.
3049type RegisterFn = unsafe extern "C" fn(*const YogApi, *const std::os::raw::c_char);
3050
3051fn load_mods(dir: &Path, api: &YogApi) {
3052    let entries = match std::fs::read_dir(dir) {
3053        Ok(e) => e,
3054        Err(_) => {
3055            yog_logging::info!("no mods directory at {} — none loaded", dir.display());
3056            return;
3057        }
3058    };
3059
3060    // First pass: collect (mod_id, dependencies, archive_path)
3061    struct ModMeta {
3062        id: String,
3063        deps: Vec<String>,
3064        yog_path: PathBuf,
3065    }
3066    let mut metas: Vec<ModMeta> = Vec::new();
3067    for entry in entries.flatten() {
3068        let path = entry.path();
3069        if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3070            continue;
3071        }
3072        let info = read_mod_info(&path);
3073        let deps = read_mod_deps(&path);
3074        metas.push(ModMeta {
3075            id: info[0].clone(),
3076            deps,
3077            yog_path: path,
3078        });
3079    }
3080
3081    // Topological sort by dependencies (Kahn's algorithm)
3082    let ids: Vec<String> = metas.iter().map(|m| m.id.clone()).collect();
3083    let deps_list: Vec<Vec<String>> = metas.iter().map(|m| m.deps.clone()).collect();
3084    let sorted = match topo_sort_mods(&ids, &deps_list) {
3085        Some(v) => v,
3086        None => {
3087            yog_logging::error!("circular dependency detected — loading in file order");
3088            (0..metas.len()).collect()
3089        }
3090    };
3091
3092    let mut loaded: Vec<LoadedModule> = Vec::new();
3093    for idx in sorted {
3094        let meta = &metas[idx];
3095        let lib_path = match extract_yog(&meta.yog_path) {
3096            Some(p) => p,
3097            None => {
3098                yog_logging::error!(
3099                    "no native for {} in {}",
3100                    platform_tag(),
3101                    meta.yog_path.display()
3102                );
3103                continue;
3104            }
3105        };
3106        if let Some(module) = load_mod_lib(&lib_path, api, &meta.id) {
3107            loaded.push(module);
3108            MOD_INFOS
3109                .lock()
3110                .expect("mod infos lock poisoned")
3111                .push(read_mod_info(&meta.yog_path));
3112        }
3113    }
3114    yog_logging::info!("loaded {} mod(s) from {}", loaded.len(), dir.display());
3115
3116    // Final import resolution pass — after all mods loaded, each mod's pending
3117    // imports can be resolved against the now-complete interop table.
3118    resolve_all_imports(api, &loaded);
3119}
3120
3121/// After all mods loaded, call `__yog_resolve_imports_final` on each library
3122/// to resolve any imports that weren't satisfied during initial registration.
3123fn resolve_all_imports(api: &YogApi, loaded: &[LoadedModule]) {
3124    for module in loaded {
3125        module.with_library(|lib| unsafe {
3126            type ResolveFn = unsafe extern "C" fn(*const YogApi);
3127            if let Ok(resolve) = lib.get::<ResolveFn>(b"__yog_resolve_imports_final") {
3128                resolve(api as *const YogApi);
3129            }
3130        });
3131    }
3132}
3133
3134/// Kahn's algorithm — returns indices in dependency order, or None on cycle.
3135fn topo_sort_mods(ids: &[String], deps_list: &[Vec<String>]) -> Option<Vec<usize>> {
3136    let n = ids.len();
3137    let name_to_idx: HashMap<&str, usize> = ids
3138        .iter()
3139        .enumerate()
3140        .map(|(i, id)| (id.as_str(), i))
3141        .collect();
3142    let mut in_degree = vec![0u32; n];
3143    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
3144    for (i, deps) in deps_list.iter().enumerate() {
3145        for dep in deps {
3146            if let Some(&j) = name_to_idx.get(dep.as_str()) {
3147                adj[j].push(i);
3148                in_degree[i] += 1;
3149            }
3150        }
3151    }
3152    let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
3153    let mut order = Vec::with_capacity(n);
3154    while let Some(u) = queue.pop() {
3155        order.push(u);
3156        for &v in &adj[u] {
3157            in_degree[v] -= 1;
3158            if in_degree[v] == 0 {
3159                queue.push(v);
3160            }
3161        }
3162    }
3163    if order.len() == n { Some(order) } else { None }
3164}
3165
3166/// Loads `path` as a fresh generation of `mod_id` via the shared
3167/// `HotReloader`, validates its ABI, and calls its `yog_mod_register` —
3168/// tagging every handler it registers with the returned `LoadedModule` (see
3169/// `current_registration()`). On any failure the module (if it got as far
3170/// as being tracked by the `HotReloader`) is retired immediately: nothing
3171/// can be executing inside a library that never successfully registered,
3172/// so this always hard-unloads right away rather than lingering.
3173fn load_mod_lib(path: &Path, api: &YogApi, mod_id: &str) -> Option<LoadedModule> {
3174    let module = match unsafe { hot_reloader().load_initial(mod_id, path) } {
3175        Ok(m) => m,
3176        Err(e) => {
3177            yog_logging::error!("failed to load {}: {}", path.display(), e);
3178            return None;
3179        }
3180    };
3181
3182    let abi = module.with_library(|lib| unsafe {
3183        let abi: Symbol<AbiVersionFn> = lib.get(b"yog_abi_version").ok()?;
3184        Some(abi())
3185    });
3186    match abi.flatten() {
3187        Some(v) if v == ABI_VERSION => {}
3188        Some(v) => {
3189            yog_logging::error!(
3190                "{}: ABI {} incompatible with runtime ABI {}",
3191                path.display(),
3192                v,
3193                ABI_VERSION
3194            );
3195            module.retire(|| {});
3196            return None;
3197        }
3198        None => {
3199            yog_logging::error!("{} is not a Yog mod (no yog_abi_version)", path.display());
3200            module.retire(|| {});
3201            return None;
3202        }
3203    }
3204
3205    if let Err(e) = invoke_mod_register(&module, api, mod_id) {
3206        yog_logging::error!("{}: {}", path.display(), e);
3207        module.retire(|| {});
3208        return None;
3209    }
3210
3211    Some(module)
3212}
3213
3214/// Calls a loaded module's `yog_mod_register`, tagging everything it
3215/// registers with `module` via `CURRENT_REGISTRATION` for the duration of
3216/// the call — used for both a mod's initial load and a later hot-reload.
3217fn invoke_mod_register(module: &LoadedModule, api: &YogApi, mod_id: &str) -> Result<(), String> {
3218    module
3219        .with_library(|lib| unsafe {
3220            let register: Symbol<RegisterFn> = lib
3221                .get(b"yog_mod_register")
3222                .map_err(|_| "missing yog_mod_register".to_string())?;
3223            let mod_id_c = std::ffi::CString::new(mod_id).unwrap();
3224            *CURRENT_REGISTRATION.lock().unwrap() = Some(module.clone());
3225            register(api as *const YogApi, mod_id_c.as_ptr());
3226            *CURRENT_REGISTRATION.lock().unwrap() = None;
3227            Ok(())
3228        })
3229        .unwrap_or_else(|| Err("module was unloaded before registration could run".to_string()))
3230}
3231
3232/// Metadata for a mod file: [id, name, version, authors, description,
3233/// name_key, description_key].
3234/// For .yog archives this comes from the bundled yog.toml; bare native libs
3235/// fall back to the file stem.
3236fn read_mod_info(path: &Path) -> [String; 7] {
3237    let stem = path
3238        .file_stem()
3239        .map(|s| s.to_string_lossy().into_owned())
3240        .unwrap_or_default();
3241    let mut info = [
3242        stem.clone(),
3243        stem,
3244        String::new(),
3245        String::new(),
3246        String::new(),
3247        String::new(),
3248        String::new(),
3249    ];
3250    if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3251        return info;
3252    }
3253    let Some(text) = (|| {
3254        let file = std::fs::File::open(path).ok()?;
3255        let mut archive = zip::ZipArchive::new(file).ok()?;
3256        let mut entry = archive.by_name("yog.toml").ok()?;
3257        let mut text = String::new();
3258        std::io::Read::read_to_string(&mut entry, &mut text).ok()?;
3259        Some(text)
3260    })() else {
3261        return info;
3262    };
3263    // Minimal parse — enough for id/name/version/description/authors without
3264    // pulling in a TOML dependency. Source manifests keep these under [mod];
3265    // the packaged manifest (written by yog-cli) has them at top level, so
3266    // keys before any section header count too.
3267    let mut in_mod = true;
3268    for line in text.lines() {
3269        let line = line.trim();
3270        if line.starts_with('[') {
3271            in_mod = line == "[mod]";
3272            continue;
3273        }
3274        if !in_mod {
3275            continue;
3276        }
3277        let Some((key, value)) = line.split_once('=') else {
3278            continue;
3279        };
3280        let key = key.trim();
3281        let value = value.trim();
3282        let unquote = |v: &str| v.trim().trim_matches('"').to_string();
3283        match key {
3284            "id" => info[0] = tsv_clean(&unquote(value)),
3285            "name" => info[1] = tsv_clean(&unquote(value)),
3286            "version" => info[2] = tsv_clean(&unquote(value)),
3287            "description" => info[4] = tsv_clean(&unquote(value)),
3288            "name_key" => info[5] = unquote(value),
3289            "description_key" => info[6] = unquote(value),
3290            "authors" => {
3291                let list = value.trim_start_matches('[').trim_end_matches(']');
3292                let authors: Vec<String> = list
3293                    .split(',')
3294                    .map(unquote)
3295                    .filter(|a| !a.is_empty())
3296                    .collect();
3297                info[3] = tsv_clean(&authors.join(", "));
3298            }
3299            _ => {}
3300        }
3301    }
3302    info
3303}
3304
3305/// Parse `[dependencies]` section from yog.toml inside a .yog archive.
3306fn read_mod_deps(path: &Path) -> Vec<String> {
3307    if path.extension().and_then(|e| e.to_str()) != Some("yog") {
3308        return Vec::new();
3309    }
3310    let text = (|| {
3311        let file = std::fs::File::open(path).ok()?;
3312        let mut archive = zip::ZipArchive::new(file).ok()?;
3313        let mut entry = archive.by_name("yog.toml").ok()?;
3314        let mut text = String::new();
3315        std::io::Read::read_to_string(&mut entry, &mut text).ok()?;
3316        Some(text)
3317    })();
3318    let Some(text) = text else { return Vec::new() };
3319    let mut deps = Vec::new();
3320    let mut in_deps = false;
3321    for line in text.lines() {
3322        let line = line.trim();
3323        if line.starts_with('[') {
3324            in_deps = line == "[dependencies]";
3325            continue;
3326        }
3327        if !in_deps {
3328            continue;
3329        }
3330        // `mod_id = "1.0"` or `mod_id = "*"` — we only care about the key (mod_id)
3331        if let Some((key, _)) = line.split_once('=') {
3332            deps.push(key.trim().to_string());
3333        }
3334    }
3335    deps
3336}
3337
3338fn extract_yog(path: &Path) -> Option<PathBuf> {
3339    let file = std::fs::File::open(path).ok()?;
3340    let mut archive = zip::ZipArchive::new(file).ok()?;
3341    let prefix = format!("natives/{}/", platform_tag());
3342    let mut entry_name = None;
3343    for i in 0..archive.len() {
3344        let f = archive.by_index(i).ok()?;
3345        let name = f.name().ok()?;
3346        if name.starts_with(&prefix) && !name.ends_with('/') {
3347            entry_name = Some(name.to_string());
3348            break;
3349        }
3350    }
3351    let entry_name = entry_name?;
3352    let ext = Path::new(&entry_name)
3353        .extension()
3354        .and_then(|e| e.to_str())
3355        .unwrap_or("bin");
3356    let stem = path.file_stem()?.to_string_lossy().into_owned();
3357    // A fresh name per extraction: a hot reload must never truncate the
3358    // library the game still has mapped, and `dlopen` of an already-loaded
3359    // path would just hand back the old module.
3360    static EXTRACTIONS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
3361    let n = EXTRACTIONS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3362    let out =
3363        std::env::temp_dir().join(format!("yog-{}-{}-{}.{}", stem, std::process::id(), n, ext));
3364    let mut entry = archive.by_name(&entry_name).ok()?;
3365    let mut out_file = std::fs::File::create(&out).ok()?;
3366    std::io::copy(&mut entry, &mut out_file).ok()?;
3367    Some(out)
3368}
3369
3370// ── Dispatcher helpers ────────────────────────────────────────────────────────
3371
3372fn srv_ptr() -> *const YogServer {
3373    SERVER.get().expect("yog: SERVER not initialised") as *const YogServer
3374}
3375
3376// ── JNI entry points ──────────────────────────────────────────────────────────
3377
3378#[unsafe(no_mangle)]
3379pub extern "system" fn Java_dev_yog_NativeBridge_nativeInit<'l>(
3380    mut env: JNIEnv<'l>,
3381    _class: JClass<'l>,
3382    mods_dir: JString<'l>,
3383) {
3384    if let Ok(vm) = env.get_java_vm() {
3385        let _ = JAVA_VM.set(vm);
3386    }
3387
3388    let dir = env
3389        .get_string(&mods_dir)
3390        .map(String::from)
3391        .unwrap_or_default();
3392
3393    // Build YogServer and store in static (gets a stable address).
3394    let _ = SERVER.set(build_server_table());
3395    let server_ptr = SERVER.get().unwrap() as *const YogServer;
3396
3397    // Publish HANDLERS *before* any mod registers — its address is then
3398    // `'static`-stable for the runtime's entire lifetime, unlike the old
3399    // design (a temporary heap `Box` moved into `HANDLERS` only after
3400    // `nativeInit` returned, silently invalidating the `ctx` pointer any
3401    // later re-registration — e.g. a hot-reload — would have needed).
3402    let handlers_lock = HANDLERS.get_or_init(|| RwLock::new(RuntimeHandlers::default()));
3403    let handlers_ptr = handlers_lock as *const RwLock<RuntimeHandlers>;
3404
3405    let api: &'static YogApi = API_TABLE.get_or_init(|| build_api_table(handlers_ptr, server_ptr));
3406
3407    let _ = HOT_RELOADER.get_or_init(|| HotReloader::new(RuntimeModuleRegistry));
3408
3409    guard("mod loading", || {
3410        load_mods(Path::new(&dir), api);
3411    });
3412
3413    control_socket::start_if_requested();
3414
3415    yog_logging::info!("runtime initialised — the gate is open.");
3416}
3417
3418#[unsafe(no_mangle)]
3419pub extern "system" fn Java_dev_yog_NativeBridge_nativeHotReload<'l>(
3420    mut env: JNIEnv<'l>,
3421    _class: JClass<'l>,
3422    mod_id: JString<'l>,
3423    new_yog_path: JString<'l>,
3424) -> jni::sys::jboolean {
3425    let Ok(mod_id) = env.get_string(&mod_id).map(String::from) else {
3426        return 0;
3427    };
3428    let Ok(yog_path) = env.get_string(&new_yog_path).map(String::from) else {
3429        return 0;
3430    };
3431    let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3432        trigger_hot_reload(&mod_id, Path::new(&yog_path))
3433    }))
3434    .unwrap_or(false);
3435    ok as jni::sys::jboolean
3436}
3437
3438/// Rebuilds and re-registers `mod_id` from a freshly built `.yog` at
3439/// `new_yog_path`: extracts the current platform's native, loads it as a
3440/// new generation via the shared `HotReloader` (which retires the previous
3441/// generation per its hybrid soft/hard-unload policy — hard-unloaded right
3442/// away if nothing's mid-call, deferred otherwise), then calls its
3443/// `yog_mod_register` again so its handlers get tagged under the new
3444/// generation. How this actually gets triggered — a keybind, a file-watch,
3445/// an IDE-driven socket — is a separate, later concern; this only needs to
3446/// work correctly once called.
3447fn trigger_hot_reload(mod_id: &str, new_yog_path: &Path) -> bool {
3448    let Some(native_path) = extract_yog(new_yog_path) else {
3449        yog_logging::error!(
3450            "hot-reload {}: no native for {} in {}",
3451            mod_id,
3452            platform_tag(),
3453            new_yog_path.display()
3454        );
3455        return false;
3456    };
3457
3458    let api = API_TABLE.get().expect("yog: nativeInit not called yet");
3459
3460    let module = match unsafe { hot_reloader().reload(mod_id, &native_path) } {
3461        Ok(m) => m,
3462        Err(e) => {
3463            yog_logging::error!(
3464                "hot-reload {}: failed to load {}: {}",
3465                mod_id,
3466                native_path.display(),
3467                e
3468            );
3469            return false;
3470        }
3471    };
3472
3473    let abi = module.with_library(|lib| unsafe {
3474        let abi: Symbol<AbiVersionFn> = lib.get(b"yog_abi_version").ok()?;
3475        Some(abi())
3476    });
3477    match abi.flatten() {
3478        Some(v) if v == ABI_VERSION => {}
3479        other => {
3480            yog_logging::error!(
3481                "hot-reload {}: ABI {:?} incompatible with runtime ABI {} — mod has no active handlers until a compatible reload succeeds",
3482                mod_id,
3483                other,
3484                ABI_VERSION
3485            );
3486            return false;
3487        }
3488    }
3489
3490    if let Err(e) = invoke_mod_register(&module, api, mod_id) {
3491        yog_logging::error!("hot-reload {}: {}", mod_id, e);
3492        return false;
3493    }
3494
3495    yog_logging::info!("hot-reloaded mod {}", mod_id);
3496    true
3497}
3498
3499#[unsafe(no_mangle)]
3500pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnBlockBreak<'l>(
3501    mut env: JNIEnv<'l>,
3502    _class: JClass<'l>,
3503    player: JString<'l>,
3504    block: JString<'l>,
3505    x: jint,
3506    y: jint,
3507    z: jint,
3508    dimension: JString<'l>,
3509) {
3510    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
3511    let ev = yog_abi::YogBlockBreakEvent {
3512        player: YogStr::from_str(&p),
3513        block: YogStr::from_str(&b),
3514        pos: YogBlockPos { x, y, z },
3515        dimension: YogStr::from_str(&d),
3516    };
3517    let srv = srv_ptr();
3518    guard("on_block_break", || {
3519        each(&handlers().block_break, |ud, f| unsafe {
3520            f(ud, srv, &ev, 1)
3521        });
3522    });
3523}
3524
3525#[unsafe(no_mangle)]
3526pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnChat<'l>(
3527    mut env: JNIEnv<'l>,
3528    _class: JClass<'l>,
3529    player: JString<'l>,
3530    message: JString<'l>,
3531    dimension: JString<'l>,
3532) {
3533    let (p, m, d) = (
3534        jstr!(env, player),
3535        jstr!(env, message),
3536        jstr!(env, dimension),
3537    );
3538    let ev = yog_abi::YogChatEvent {
3539        player: YogStr::from_str(&p),
3540        message: YogStr::from_str(&m),
3541        dimension: YogStr::from_str(&d),
3542    };
3543    let srv = srv_ptr();
3544    guard("on_chat", || {
3545        each(&handlers().chat, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3546    });
3547}
3548
3549#[unsafe(no_mangle)]
3550pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerJoin<'l>(
3551    mut env: JNIEnv<'l>,
3552    _class: JClass<'l>,
3553    player: JString<'l>,
3554    uuid: JString<'l>,
3555    dimension: JString<'l>,
3556) {
3557    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
3558    let ev = yog_abi::YogPlayerEvent {
3559        player: YogStr::from_str(&p),
3560        uuid: YogStr::from_str(&u),
3561        dimension: YogStr::from_str(&d),
3562    };
3563    let srv = srv_ptr();
3564    guard("on_player_join", || {
3565        each(&handlers().player_join, |ud, f| unsafe {
3566            f(ud, srv, &ev, 1)
3567        });
3568    });
3569    // Queue startup grants: giving items directly inside the join callback
3570    // fails for the host of a freshly created world (player not fully in the
3571    // world yet), so grants are processed a few server ticks later.
3572    let h = handlers();
3573    if !h.startup_grants.is_empty() {
3574        h.pending_grants
3575            .lock()
3576            .expect("pending_grants poisoned")
3577            .push((p, u, 0));
3578    }
3579}
3580
3581/// The command tag a player carries once startup grant `id` was given. It's
3582/// saved with the player's own data, so it follows the player — not their
3583/// name or uuid (a dev client gets a new name each launch, while a
3584/// singleplayer world's owner stays the same player).
3585fn grant_tag(id: &str) -> String {
3586    format!("yog.grant.{id}")
3587}
3588
3589/// Try to fulfil pending startup grants. Called each server tick; a grant is
3590/// only marked as given (tagged on the player) after `give_item` succeeds.
3591fn process_pending_grants() {
3592    let h = handlers();
3593    let mut pending = match h.pending_grants.try_lock() {
3594        Ok(g) => g,
3595        Err(_) => return,
3596    };
3597    if pending.is_empty() {
3598        return;
3599    }
3600    // Wait a couple of ticks after join before the first attempt.
3601    const FIRST_TRY_TICK: u32 = 2;
3602    // Give up after 30 seconds — the player probably left.
3603    const MAX_TICKS: u32 = 600;
3604
3605    let mut still_pending = Vec::new();
3606    for (p, u, ticks) in pending.drain(..) {
3607        if ticks < FIRST_TRY_TICK {
3608            still_pending.push((p, u, ticks + 1));
3609            continue;
3610        }
3611        let player = YogStr::from_str(&p);
3612        let mut all_ok = true;
3613        for sg in &h.startup_grants {
3614            let tag = grant_tag(&sg.id);
3615            let tag = YogStr::from_str(&tag);
3616            if unsafe { bridge!("playerHasTag", [S(player), S(tag)]) }.unwrap_or(false) {
3617                continue;
3618            }
3619            let book = sg.book.as_ref().map(|_| "minecraft:written_book");
3620            let mut ok = true;
3621            for item_id in sg.items.iter().map(String::as_str).chain(book) {
3622                ok &= unsafe { srv_give_item(std::ptr::null_mut(), player, YogStr::from_str(item_id), 1) };
3623            }
3624            if ok && unsafe { bridge!("addPlayerTag", [S(player), S(tag)]) }.unwrap_or(false) {
3625                yog_logging::info!("startup grant {} given to {}", sg.id, p);
3626            } else {
3627                all_ok = false;
3628            }
3629        }
3630        if !all_ok && ticks < MAX_TICKS {
3631            still_pending.push((p, u, ticks + 1));
3632        }
3633    }
3634    *pending = still_pending;
3635}
3636
3637#[unsafe(no_mangle)]
3638pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerLeave<'l>(
3639    mut env: JNIEnv<'l>,
3640    _class: JClass<'l>,
3641    player: JString<'l>,
3642    uuid: JString<'l>,
3643    dimension: JString<'l>,
3644) {
3645    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
3646    let ev = yog_abi::YogPlayerEvent {
3647        player: YogStr::from_str(&p),
3648        uuid: YogStr::from_str(&u),
3649        dimension: YogStr::from_str(&d),
3650    };
3651    let srv = srv_ptr();
3652    guard("on_player_leave", || {
3653        each(&handlers().player_leave, |ud, f| unsafe {
3654            f(ud, srv, &ev, 1)
3655        });
3656    });
3657}
3658
3659#[unsafe(no_mangle)]
3660pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseItem<'l>(
3661    mut env: JNIEnv<'l>,
3662    _class: JClass<'l>,
3663    player: JString<'l>,
3664    item: JString<'l>,
3665    sneaking: jni::sys::jboolean,
3666    dimension: JString<'l>,
3667) {
3668    let (p, i, d) = (jstr!(env, player), jstr!(env, item), jstr!(env, dimension));
3669    let ev = yog_abi::YogUseItemEvent {
3670        player: YogStr::from_str(&p),
3671        item: YogStr::from_str(&i),
3672        sneaking: sneaking != 0,
3673        dimension: YogStr::from_str(&d),
3674    };
3675    let srv = srv_ptr();
3676    guard("on_use_item", || {
3677        each(&handlers().use_item, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3678    });
3679}
3680
3681#[unsafe(no_mangle)]
3682pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseItemPre<'l>(
3683    mut env: JNIEnv<'l>,
3684    _class: JClass<'l>,
3685    player: JString<'l>,
3686    item: JString<'l>,
3687    sneaking: jni::sys::jboolean,
3688    dimension: JString<'l>,
3689) -> jni::sys::jboolean {
3690    let h = handlers();
3691    if h.use_item.is_empty() {
3692        return 1;
3693    }
3694    let p = jstr!(env, player, 1);
3695    let i = jstr!(env, item, 1);
3696    let d = jstr!(env, dimension, 1);
3697    let ev = yog_abi::YogUseItemEvent {
3698        player: YogStr::from_str(&p),
3699        item: YogStr::from_str(&i),
3700        sneaking: sneaking != 0,
3701        dimension: YogStr::from_str(&d),
3702    };
3703    let srv = srv_ptr();
3704    allowed(&h.use_item, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3705}
3706
3707#[unsafe(no_mangle)]
3708pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseBlock<'l>(
3709    mut env: JNIEnv<'l>,
3710    _class: JClass<'l>,
3711    player: JString<'l>,
3712    block: JString<'l>,
3713    x: jint,
3714    y: jint,
3715    z: jint,
3716    dimension: JString<'l>,
3717) {
3718    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
3719    let ev = yog_abi::YogUseBlockEvent {
3720        player: YogStr::from_str(&p),
3721        block: YogStr::from_str(&b),
3722        pos: YogBlockPos { x, y, z },
3723        dimension: YogStr::from_str(&d),
3724    };
3725    let srv = srv_ptr();
3726    guard("on_use_block", || {
3727        each(&handlers().use_block, |ud, f| unsafe { f(ud, srv, &ev, 1) });
3728    });
3729}
3730
3731#[unsafe(no_mangle)]
3732pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnUseBlockPre<'l>(
3733    mut env: JNIEnv<'l>,
3734    _class: JClass<'l>,
3735    player: JString<'l>,
3736    block: JString<'l>,
3737    x: jint,
3738    y: jint,
3739    z: jint,
3740    dimension: JString<'l>,
3741) -> jni::sys::jboolean {
3742    let h = handlers();
3743    if h.use_block.is_empty() {
3744        return 1;
3745    }
3746    let p = jstr!(env, player, 1);
3747    let b = jstr!(env, block, 1);
3748    let d = jstr!(env, dimension, 1);
3749    let ev = yog_abi::YogUseBlockEvent {
3750        player: YogStr::from_str(&p),
3751        block: YogStr::from_str(&b),
3752        pos: YogBlockPos { x, y, z },
3753        dimension: YogStr::from_str(&d),
3754    };
3755    let srv = srv_ptr();
3756    allowed(&h.use_block, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3757}
3758
3759#[unsafe(no_mangle)]
3760pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnAttackEntity<'l>(
3761    mut env: JNIEnv<'l>,
3762    _class: JClass<'l>,
3763    player: JString<'l>,
3764    target_type: JString<'l>,
3765    target_uuid: JString<'l>,
3766    dimension: JString<'l>,
3767) {
3768    let (p, tt, tu, d) = (
3769        jstr!(env, player),
3770        jstr!(env, target_type),
3771        jstr!(env, target_uuid),
3772        jstr!(env, dimension),
3773    );
3774    let ev = yog_abi::YogAttackEntityEvent {
3775        player: YogStr::from_str(&p),
3776        target_type: YogStr::from_str(&tt),
3777        target_uuid: YogStr::from_str(&tu),
3778        dimension: YogStr::from_str(&d),
3779    };
3780    let srv = srv_ptr();
3781    guard("on_attack_entity", || {
3782        each(&handlers().attack_entity, |ud, f| unsafe {
3783            f(ud, srv, &ev, 1)
3784        });
3785    });
3786}
3787
3788#[unsafe(no_mangle)]
3789pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDamage<'l>(
3790    mut env: JNIEnv<'l>,
3791    _class: JClass<'l>,
3792    entity_type: JString<'l>,
3793    uuid: JString<'l>,
3794    amount: jfloat,
3795    source: JString<'l>,
3796    dimension: JString<'l>,
3797) {
3798    let (et, u, s, d) = (
3799        jstr!(env, entity_type),
3800        jstr!(env, uuid),
3801        jstr!(env, source),
3802        jstr!(env, dimension),
3803    );
3804    let ev = yog_abi::YogEntityDamageEvent {
3805        entity_type: YogStr::from_str(&et),
3806        uuid: YogStr::from_str(&u),
3807        amount,
3808        source: YogStr::from_str(&s),
3809        dimension: YogStr::from_str(&d),
3810    };
3811    let srv = srv_ptr();
3812    guard("on_entity_damage", || {
3813        each(&handlers().entity_damage, |ud, f| unsafe {
3814            f(ud, srv, &ev, 1)
3815        });
3816    });
3817}
3818
3819#[unsafe(no_mangle)]
3820pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDeath<'l>(
3821    mut env: JNIEnv<'l>,
3822    _class: JClass<'l>,
3823    entity_type: JString<'l>,
3824    uuid: JString<'l>,
3825    source: JString<'l>,
3826    dimension: JString<'l>,
3827) {
3828    let (et, u, s, d) = (
3829        jstr!(env, entity_type),
3830        jstr!(env, uuid),
3831        jstr!(env, source),
3832        jstr!(env, dimension),
3833    );
3834    let ev = yog_abi::YogEntityDeathEvent {
3835        entity_type: YogStr::from_str(&et),
3836        uuid: YogStr::from_str(&u),
3837        source: YogStr::from_str(&s),
3838        dimension: YogStr::from_str(&d),
3839    };
3840    let srv = srv_ptr();
3841    guard("on_entity_death", || {
3842        each(&handlers().entity_death, |ud, f| unsafe {
3843            f(ud, srv, &ev, 1)
3844        });
3845    });
3846}
3847
3848#[unsafe(no_mangle)]
3849pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnTick<'l>(
3850    _env: JNIEnv<'l>,
3851    _class: JClass<'l>,
3852) {
3853    let h = handlers();
3854    let srv = srv_ptr();
3855    guard("on_tick", || {
3856        each(&h.server_tick, |ud, f| unsafe { f(ud, srv) });
3857    });
3858
3859    guard("startup_grants", process_pending_grants);
3860
3861    // Scheduler — once tasks
3862    {
3863        let mut sched = h.scheduler.lock().expect("scheduler poisoned");
3864        let mut to_fire: Vec<(LoadedModule, *mut c_void, YogScheduledFn)> = Vec::new();
3865        let mut remaining = Vec::new();
3866        for task in sched.once_tasks.drain(..) {
3867            if task.delay_remaining == 0 {
3868                to_fire.push((task.module.clone(), task.ud, task.f));
3869            } else {
3870                remaining.push(OnceTask {
3871                    delay_remaining: task.delay_remaining - 1,
3872                    ..task
3873                });
3874            }
3875        }
3876        sched.once_tasks = remaining;
3877        drop(sched);
3878        for (module, ud, f) in to_fire {
3879            if module.is_retiring() {
3880                continue;
3881            }
3882            let Some(_call) = module.enter() else {
3883                continue;
3884            };
3885            guard("schedule_once", || unsafe { f(ud, srv) });
3886        }
3887    }
3888
3889    // Scheduler — repeating tasks
3890    {
3891        let mut sched = h.scheduler.lock().expect("scheduler poisoned");
3892        let mut to_fire: Vec<(LoadedModule, *mut c_void, YogScheduledFn)> = Vec::new();
3893        for task in &mut sched.repeating_tasks {
3894            if task.ticks_left == 0 {
3895                to_fire.push((task.module.clone(), task.ud, task.f));
3896                task.ticks_left = task.period;
3897            } else {
3898                task.ticks_left -= 1;
3899            }
3900        }
3901        drop(sched);
3902        for (module, ud, f) in to_fire {
3903            if module.is_retiring() {
3904                continue;
3905            }
3906            let Some(_call) = module.enter() else {
3907                continue;
3908            };
3909            guard("schedule_repeating", || unsafe { f(ud, srv) });
3910        }
3911    }
3912}
3913
3914#[unsafe(no_mangle)]
3915pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnServerStarted<'l>(
3916    _env: JNIEnv<'l>,
3917    _class: JClass<'l>,
3918    _world_dir: JString<'l>,
3919) {
3920    // Grants pending for a previous world's players are void.
3921    handlers().pending_grants.lock().expect("pending_grants poisoned").clear();
3922    let srv = srv_ptr();
3923    guard("on_server_started", || {
3924        each(&handlers().server_started, |ud, f| unsafe { f(ud, srv) });
3925    });
3926}
3927
3928#[unsafe(no_mangle)]
3929pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnServerStopping<'l>(
3930    _env: JNIEnv<'l>,
3931    _class: JClass<'l>,
3932) {
3933    let srv = srv_ptr();
3934    guard("on_server_stopping", || {
3935        each(&handlers().server_stopping, |ud, f| unsafe { f(ud, srv) });
3936    });
3937}
3938
3939#[unsafe(no_mangle)]
3940pub extern "system" fn Java_dev_yog_NativeBridge_nativeCommandNames<'l>(
3941    env: JNIEnv<'l>,
3942    _class: JClass<'l>,
3943) -> jstring {
3944    let names = handlers()
3945        .commands
3946        .keys()
3947        .cloned()
3948        .collect::<Vec<_>>()
3949        .join("\n");
3950    env.new_string(names)
3951        .map(jni::objects::JString::into_raw)
3952        .unwrap_or(std::ptr::null_mut())
3953}
3954
3955#[unsafe(no_mangle)]
3956pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnBlockBreakPre<'l>(
3957    mut env: JNIEnv<'l>,
3958    _class: JClass<'l>,
3959    player: JString<'l>,
3960    block: JString<'l>,
3961    x: jint,
3962    y: jint,
3963    z: jint,
3964    dimension: JString<'l>,
3965) -> jni::sys::jboolean {
3966    let h = handlers();
3967    if h.block_break.is_empty() {
3968        return 1;
3969    }
3970    let p = jstr!(env, player, 1);
3971    let b = jstr!(env, block, 1);
3972    let d = jstr!(env, dimension, 1);
3973    let ev = yog_abi::YogBlockBreakEvent {
3974        player: YogStr::from_str(&p),
3975        block: YogStr::from_str(&b),
3976        pos: YogBlockPos { x, y, z },
3977        dimension: YogStr::from_str(&d),
3978    };
3979    let srv = srv_ptr();
3980    allowed(&h.block_break, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
3981}
3982
3983#[unsafe(no_mangle)]
3984pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnChatPre<'l>(
3985    mut env: JNIEnv<'l>,
3986    _class: JClass<'l>,
3987    player: JString<'l>,
3988    message: JString<'l>,
3989    dimension: JString<'l>,
3990) -> jni::sys::jboolean {
3991    let h = handlers();
3992    if h.chat.is_empty() {
3993        return 1;
3994    }
3995    let p = jstr!(env, player, 1);
3996    let m = jstr!(env, message, 1);
3997    let d = jstr!(env, dimension, 1);
3998    let ev = yog_abi::YogChatEvent {
3999        player: YogStr::from_str(&p),
4000        message: YogStr::from_str(&m),
4001        dimension: YogStr::from_str(&d),
4002    };
4003    let srv = srv_ptr();
4004    allowed(&h.chat, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4005}
4006
4007#[unsafe(no_mangle)]
4008pub extern "system" fn Java_dev_yog_NativeBridge_nativeRecipeJsons<'l>(
4009    env: JNIEnv<'l>,
4010    _class: JClass<'l>,
4011) -> jstring {
4012    let s = handlers()
4013        .recipes
4014        .iter()
4015        .map(|(ns, name, json)| format!("{ns}\t{name}\t{json}"))
4016        .collect::<Vec<_>>()
4017        .join("\n");
4018    env.new_string(s)
4019        .map(jni::objects::JString::into_raw)
4020        .unwrap_or(std::ptr::null_mut())
4021}
4022
4023#[unsafe(no_mangle)]
4024pub extern "system" fn Java_dev_yog_NativeBridge_nativeDimensionJsons<'l>(
4025    env: JNIEnv<'l>,
4026    _class: JClass<'l>,
4027) -> jstring {
4028    // One line per declared dimension: `id \t def_json`. The host converts
4029    // `def_json` (our YogDimensionDef shape) into vanilla dimension_type /
4030    // dimension datapack JSON — see `yog-dimensions` crate docs.
4031    let s = handlers()
4032        .dimensions
4033        .iter()
4034        .map(|(id, json)| format!("{id}\t{json}"))
4035        .collect::<Vec<_>>()
4036        .join("\n");
4037    env.new_string(s)
4038        .map(jni::objects::JString::into_raw)
4039        .unwrap_or(std::ptr::null_mut())
4040}
4041
4042/// Called by a registered chunk-generator closure (via `ChunkWriter::set_block`)
4043/// to place one block in the chunk currently being generated. Calls back
4044/// into `YogCallbackChunkGenerator.setBlockInGeneratingChunk` on the Java
4045/// side — same thread, since the whole round trip is synchronous.
4046unsafe extern "C" fn chunk_writer_set_block(
4047    _ctx: *mut c_void,
4048    x: i32,
4049    y: i32,
4050    z: i32,
4051    block_id: YogStr,
4052) -> bool {
4053    unsafe {
4054        host!(
4055            "dev/yog/YogCallbackChunkGenerator",
4056            "setBlockInGeneratingChunk",
4057            [I(x), I(y), I(z), S(block_id)]
4058        )
4059        .unwrap_or(false)
4060    }
4061}
4062
4063#[unsafe(no_mangle)]
4064pub extern "system" fn Java_dev_yog_NativeBridge_nativeGenerateChunk<'l>(
4065    mut env: JNIEnv<'l>,
4066    _class: JClass<'l>,
4067    generator_type_id: JString<'l>,
4068    chunk_x: jint,
4069    chunk_z: jint,
4070    min_y: jint,
4071    height: jint,
4072) {
4073    let id: String = match env.get_string(&generator_type_id) {
4074        Ok(s) => String::from(s),
4075        Err(_) => return,
4076    };
4077    let Some((module, ud, cb)) = handlers().chunk_generators.get(&id).cloned() else {
4078        return;
4079    };
4080    if module.is_retiring() {
4081        return;
4082    }
4083    let Some(_call) = module.enter() else { return };
4084    let writer = yog_abi::YogChunkWriterApi {
4085        chunk_x,
4086        chunk_z,
4087        min_y,
4088        height,
4089        ctx: std::ptr::null_mut(),
4090        set_block: chunk_writer_set_block,
4091    };
4092    guard("chunk_generator", || unsafe {
4093        cb(ud, &writer as *const _);
4094    });
4095}
4096
4097#[unsafe(no_mangle)]
4098pub extern "system" fn Java_dev_yog_NativeBridge_nativeTypedCommandSchemas<'l>(
4099    env: JNIEnv<'l>,
4100    _class: JClass<'l>,
4101) -> jstring {
4102    // One line per DISTINCT (name, schema) pair — a name can have several
4103    // typed registrations (different arg shapes); Brigadier merges repeated
4104    // `dispatcher.register()` calls for the same literal name into one tree,
4105    // so the Java side just needs every distinct schema once each.
4106    let mut lines = Vec::new();
4107    for (name, regs) in handlers().commands.iter() {
4108        let mut seen = std::collections::HashSet::new();
4109        for (_module, schema, _, _) in regs {
4110            if let Some(schema) = schema
4111                && seen.insert(schema.as_str())
4112            {
4113                lines.push(format!("{name}\t{schema}"));
4114            }
4115        }
4116    }
4117    env.new_string(lines.join("\n"))
4118        .map(jni::objects::JString::into_raw)
4119        .unwrap_or(std::ptr::null_mut())
4120}
4121
4122#[unsafe(no_mangle)]
4123pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnCommand<'l>(
4124    mut env: JNIEnv<'l>,
4125    _class: JClass<'l>,
4126    name: JString<'l>,
4127    args: JString<'l>,
4128    source: JString<'l>,
4129    uuid: JString<'l>,
4130    dimension: JString<'l>,
4131) -> jstring {
4132    let (n, a, s, u, d) = (
4133        env.get_string(&name).map(String::from).unwrap_or_default(),
4134        env.get_string(&args).map(String::from).unwrap_or_default(),
4135        env.get_string(&source)
4136            .map(String::from)
4137            .unwrap_or_default(),
4138        env.get_string(&uuid).map(String::from).unwrap_or_default(),
4139        env.get_string(&dimension)
4140            .map(String::from)
4141            .unwrap_or_default(),
4142    );
4143    let h = handlers();
4144    let srv = srv_ptr();
4145    let args: Vec<&str> = if a.is_empty() { Vec::new() } else { a.split('\t').collect() };
4146    // One handler with `args`; its reply, or `None` when it passed.
4147    let call = |ud: &*mut c_void, f: &YogCommandFn, args: &str| -> Option<String> {
4148        let ev = yog_abi::YogCommandEvent {
4149            name: YogStr::from_str(&n),
4150            args: YogStr::from_str(args),
4151            source: YogStr::from_str(&s),
4152            uuid: YogStr::from_str(&u),
4153            dimension: YogStr::from_str(&d),
4154        };
4155        let mut buf = [0u8; 4096];
4156        let mut reply_len: u32 = 0;
4157        unsafe { f(*ud, srv, &ev, buf.as_mut_ptr(), buf.len() as u32, &mut reply_len) };
4158        (reply_len > 0).then(|| String::from_utf8_lossy(&buf[..reply_len as usize]).into_owned())
4159    };
4160    let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
4161        let Some(regs) = h.commands.get(&n) else {
4162            return String::new();
4163        };
4164        let live = regs.iter().filter(|(module, ..)| !module.is_retiring());
4165        // 1. Typed handlers whose schema has exactly these many args.
4166        // 2. Ones ending in a `string`, with the extra words joined into it
4167        //    (the game may have parsed them as separate words).
4168        // 3. Plain (schema-less) handlers, with whatever was typed.
4169        for pass in 0..3 {
4170            for (module, schema, ud, f) in live.clone() {
4171                let joined;
4172                let these = match (pass, schema) {
4173                    (0, Some(schema)) if schema.split_whitespace().count() == args.len() => a.as_str(),
4174                    (1, Some(schema)) => {
4175                        let types: Vec<&str> = schema.split_whitespace().collect();
4176                        let k = types.len();
4177                        if types.last() != Some(&"string") || k == 0 || args.len() <= k {
4178                            continue;
4179                        }
4180                        let mut parts: Vec<String> = args[..k - 1].iter().map(|x| x.to_string()).collect();
4181                        parts.push(args[k - 1..].join(" "));
4182                        joined = parts.join("\t");
4183                        joined.as_str()
4184                    }
4185                    (2, None) => a.as_str(),
4186                    _ => continue,
4187                };
4188                let Some(_call) = module.enter() else {
4189                    continue;
4190                };
4191                if let Some(r) = call(ud, f, these) {
4192                    return r;
4193                }
4194            }
4195        }
4196        String::new()
4197    }))
4198    .unwrap_or_else(|_| {
4199        yog_logging::error!("a mod panicked handling command `{}`", n);
4200        String::new()
4201    });
4202
4203    env.new_string(reply)
4204        .map(jni::objects::JString::into_raw)
4205        .unwrap_or(std::ptr::null_mut())
4206}
4207
4208#[unsafe(no_mangle)]
4209pub extern "system" fn Java_dev_yog_NativeBridge_nativeItemDefs<'l>(
4210    env: JNIEnv<'l>,
4211    _class: JClass<'l>,
4212) -> jstring {
4213    let s = handlers()
4214        .items
4215        .iter()
4216        .map(|d| {
4217            let mut parts = vec![d.id.clone()];
4218            parts.push(format!("max_stack={}", d.max_stack));
4219            if let Some(n) = &d.name {
4220                parts.push(format!("name={n}"));
4221            }
4222            if let Some(t) = &d.tooltip {
4223                parts.push(format!("tooltip={t}"));
4224            }
4225            if d.max_damage > 0 {
4226                parts.push(format!("max_damage={}", d.max_damage));
4227            }
4228            if d.fire_resistant {
4229                parts.push("fire_resistant=1".into());
4230            }
4231            if d.fuel_ticks > 0 {
4232                parts.push(format!("fuel_ticks={}", d.fuel_ticks));
4233            }
4234            if let Some(f) = &d.food {
4235                parts.push(format!(
4236                    "food={}:{}:{}",
4237                    f.nutrition,
4238                    f.saturation,
4239                    if f.can_always_eat { 1 } else { 0 }
4240                ));
4241            }
4242            parts.join("\t")
4243        })
4244        .collect::<Vec<_>>()
4245        .join("\n");
4246    env.new_string(s)
4247        .map(jni::objects::JString::into_raw)
4248        .unwrap_or(std::ptr::null_mut())
4249}
4250
4251#[unsafe(no_mangle)]
4252pub extern "system" fn Java_dev_yog_NativeBridge_nativeBlockDefs<'l>(
4253    env: JNIEnv<'l>,
4254    _class: JClass<'l>,
4255) -> jstring {
4256    let s = handlers()
4257        .blocks
4258        .iter()
4259        .map(|d| {
4260            let mut parts = vec![d.id.clone()];
4261            parts.push(format!("hardness={}", d.hardness));
4262            parts.push(format!("resistance={}", d.resistance));
4263            if let Some(n) = &d.name {
4264                parts.push(format!("name={n}"));
4265            }
4266            if let Some(sh) = d.shape {
4267                parts.push(format!(
4268                    "shape={}:{}:{}:{}:{}:{}",
4269                    sh[0], sh[1], sh[2], sh[3], sh[4], sh[5]
4270                ));
4271            }
4272            if d.light_level > 0 {
4273                parts.push(format!("light={}", d.light_level));
4274            }
4275            if let Some(snd) = &d.sound {
4276                parts.push(format!("sound={snd}"));
4277            }
4278            if d.requires_tool {
4279                parts.push("requires_tool=1".into());
4280            }
4281            if d.no_collision {
4282                parts.push("no_collision=1".into());
4283            }
4284            if d.slipperiness > 0.0 {
4285                parts.push(format!("slipperiness={}", d.slipperiness));
4286            }
4287            if d.connects {
4288                parts.push("connects=1".into());
4289            }
4290            if !d.connect_groups.is_empty() {
4291                parts.push(format!("connect_groups={}", d.connect_groups.join(",")));
4292            }
4293            if let Some(inv) = &d.inventory_id {
4294                parts.push(format!("inventory_id={inv}"));
4295            }
4296            if d.redstone_source {
4297                parts.push("redstone=1".into());
4298            }
4299            parts.join("\t")
4300        })
4301        .collect::<Vec<_>>()
4302        .join("\n");
4303    env.new_string(s)
4304        .map(jni::objects::JString::into_raw)
4305        .unwrap_or(std::ptr::null_mut())
4306}
4307
4308/// Tab/newline-encoded inventory defs — one line per registered
4309/// `InventoryDef` (see `yog_inventory`): `id\tslot_count=N\tlayout=x:y,...
4310/// \tinclude_player_inventory=0|1\tplayer_inv=x:y\tbackground_texture=...\ttitle=...`
4311#[unsafe(no_mangle)]
4312pub extern "system" fn Java_dev_yog_NativeBridge_nativeInventoryDefs<'l>(
4313    env: JNIEnv<'l>,
4314    _class: JClass<'l>,
4315) -> jstring {
4316    let s = handlers()
4317        .inventories
4318        .iter()
4319        .map(|d| {
4320            let mut parts = vec![d.id.clone()];
4321            parts.push(format!("slot_count={}", d.slot_count));
4322            parts.push(format!(
4323                "layout={}",
4324                yog_inventory::encode_layout(&d.resolved_layout())
4325            ));
4326            parts.push(format!(
4327                "include_player_inventory={}",
4328                d.include_player_inventory as u8
4329            ));
4330            parts.push(format!(
4331                "player_inv={}:{}",
4332                d.player_inv_offset.0, d.player_inv_offset.1
4333            ));
4334            if let Some(tex) = &d.background_texture {
4335                parts.push(format!("background_texture={tex}"));
4336            }
4337            if !d.title.is_empty() {
4338                parts.push(format!("title={}", d.title));
4339            }
4340            if let Some((w, h)) = d.background_size {
4341                parts.push(format!("background_size={w}:{h}"));
4342            }
4343            parts.join("\t")
4344        })
4345        .collect::<Vec<_>>()
4346        .join("\n");
4347    env.new_string(s)
4348        .map(jni::objects::JString::into_raw)
4349        .unwrap_or(std::ptr::null_mut())
4350}
4351
4352#[unsafe(no_mangle)]
4353pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPacket<'l>(
4354    mut env: JNIEnv<'l>,
4355    _class: JClass<'l>,
4356    channel: JString<'l>,
4357    player: JString<'l>,
4358    payload: JByteArray<'l>,
4359) {
4360    let ch = env
4361        .get_string(&channel)
4362        .map(String::from)
4363        .unwrap_or_default();
4364    let pl = env
4365        .get_string(&player)
4366        .map(String::from)
4367        .unwrap_or_default();
4368    let data = env.convert_byte_array(&payload).unwrap_or_default();
4369    let ev = yog_abi::YogPacketEvent {
4370        channel: YogStr::from_str(&ch),
4371        player: YogStr::from_str(&pl),
4372        payload: data.as_ptr(),
4373        payload_len: data.len() as u32,
4374    };
4375    let h = handlers();
4376    let srv = srv_ptr();
4377    guard("on_packet", || {
4378        if let Some((module, ud, f)) = h.packets.get(&ch) {
4379            if module.is_retiring() {
4380                return;
4381            }
4382            let Some(_call) = module.enter() else { return };
4383            unsafe { f(*ud, srv, &ev) };
4384        }
4385    });
4386}
4387
4388#[unsafe(no_mangle)]
4389pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnClientPacket<'l>(
4390    mut env: JNIEnv<'l>,
4391    _class: JClass<'l>,
4392    channel: JString<'l>,
4393    payload: JByteArray<'l>,
4394) {
4395    let ch = env
4396        .get_string(&channel)
4397        .map(String::from)
4398        .unwrap_or_default();
4399    let data = env.convert_byte_array(&payload).unwrap_or_default();
4400    let ev = yog_abi::YogPacketEvent {
4401        channel: YogStr::from_str(&ch),
4402        player: YogStr::EMPTY,
4403        payload: data.as_ptr(),
4404        payload_len: data.len() as u32,
4405    };
4406    let h = handlers();
4407    let srv = srv_ptr();
4408    guard("on_client_packet", || {
4409        if let Some((module, ud, f)) = h.client_packets.get(&ch) {
4410            if module.is_retiring() {
4411                return;
4412            }
4413            let Some(_call) = module.enter() else { return };
4414            unsafe { f(*ud, srv, &ev) };
4415        }
4416    });
4417}
4418
4419#[unsafe(no_mangle)]
4420pub extern "system" fn Java_dev_yog_NativeBridge_nativePacketChannels<'l>(
4421    env: JNIEnv<'l>,
4422    _class: JClass<'l>,
4423) -> jstring {
4424    let s = handlers()
4425        .packets
4426        .keys()
4427        .cloned()
4428        .collect::<Vec<_>>()
4429        .join("\n");
4430    env.new_string(s)
4431        .map(jni::objects::JString::into_raw)
4432        .unwrap_or(std::ptr::null_mut())
4433}
4434
4435#[unsafe(no_mangle)]
4436pub extern "system" fn Java_dev_yog_NativeBridge_nativeClientPacketChannels<'l>(
4437    env: JNIEnv<'l>,
4438    _class: JClass<'l>,
4439) -> jstring {
4440    let s = handlers()
4441        .client_packets
4442        .keys()
4443        .cloned()
4444        .collect::<Vec<_>>()
4445        .join("\n");
4446    env.new_string(s)
4447        .map(jni::objects::JString::into_raw)
4448        .unwrap_or(std::ptr::null_mut())
4449}
4450
4451#[unsafe(no_mangle)]
4452pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntitySpawn<'l>(
4453    mut env: JNIEnv<'l>,
4454    _class: JClass<'l>,
4455    entity_type: JString<'l>,
4456    uuid: JString<'l>,
4457    dimension: JString<'l>,
4458) {
4459    let h = handlers();
4460    if h.entity_spawn.is_empty() {
4461        return;
4462    }
4463    let (et, u, d) = (
4464        jstr!(env, entity_type),
4465        jstr!(env, uuid),
4466        jstr!(env, dimension),
4467    );
4468    let ev = yog_abi::YogEntitySpawnEvent {
4469        entity_type: YogStr::from_str(&et),
4470        uuid: YogStr::from_str(&u),
4471        dimension: YogStr::from_str(&d),
4472    };
4473    let srv = srv_ptr();
4474    guard("on_entity_spawn", || {
4475        each(&h.entity_spawn, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4476    });
4477}
4478
4479#[unsafe(no_mangle)]
4480pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntitySpawnPre<'l>(
4481    mut env: JNIEnv<'l>,
4482    _class: JClass<'l>,
4483    entity_type: JString<'l>,
4484    uuid: JString<'l>,
4485    dimension: JString<'l>,
4486) -> jni::sys::jboolean {
4487    let h = handlers();
4488    if h.entity_spawn.is_empty() {
4489        return 1;
4490    }
4491    let et = jstr!(env, entity_type, 1);
4492    let u = jstr!(env, uuid, 1);
4493    let d = jstr!(env, dimension, 1);
4494    let ev = yog_abi::YogEntitySpawnEvent {
4495        entity_type: YogStr::from_str(&et),
4496        uuid: YogStr::from_str(&u),
4497        dimension: YogStr::from_str(&d),
4498    };
4499    let srv = srv_ptr();
4500    allowed(&h.entity_spawn, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4501}
4502
4503#[unsafe(no_mangle)]
4504pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityDamagePre<'l>(
4505    mut env: JNIEnv<'l>,
4506    _class: JClass<'l>,
4507    entity_type: JString<'l>,
4508    uuid: JString<'l>,
4509    amount: jfloat,
4510    source: JString<'l>,
4511    dimension: JString<'l>,
4512) -> jni::sys::jboolean {
4513    let h = handlers();
4514    if h.entity_damage.is_empty() {
4515        return 1;
4516    }
4517    let et = jstr!(env, entity_type, 1);
4518    let u = jstr!(env, uuid, 1);
4519    let s = jstr!(env, source, 1);
4520    let d = jstr!(env, dimension, 1);
4521    let ev = yog_abi::YogEntityDamageEvent {
4522        entity_type: YogStr::from_str(&et),
4523        uuid: YogStr::from_str(&u),
4524        amount,
4525        source: YogStr::from_str(&s),
4526        dimension: YogStr::from_str(&d),
4527    };
4528    let srv = srv_ptr();
4529    allowed(&h.entity_damage, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4530}
4531
4532#[unsafe(no_mangle)]
4533pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlaceBlockPre<'l>(
4534    mut env: JNIEnv<'l>,
4535    _class: JClass<'l>,
4536    player: JString<'l>,
4537    block: JString<'l>,
4538    x: jint,
4539    y: jint,
4540    z: jint,
4541    dimension: JString<'l>,
4542) -> jni::sys::jboolean {
4543    let h = handlers();
4544    if h.player_place_block.is_empty() {
4545        return 1;
4546    }
4547    let p = jstr!(env, player, 1);
4548    let b = jstr!(env, block, 1);
4549    let d = jstr!(env, dimension, 1);
4550    let ev = YogPlaceBlockEvent {
4551        player: YogStr::from_str(&p),
4552        block: YogStr::from_str(&b),
4553        pos: YogBlockPos { x, y, z },
4554        dimension: YogStr::from_str(&d),
4555    };
4556    let srv = srv_ptr();
4557    allowed(&h.player_place_block, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4558}
4559
4560#[unsafe(no_mangle)]
4561pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlaceBlock<'l>(
4562    mut env: JNIEnv<'l>,
4563    _class: JClass<'l>,
4564    player: JString<'l>,
4565    block: JString<'l>,
4566    x: jint,
4567    y: jint,
4568    z: jint,
4569    dimension: JString<'l>,
4570) {
4571    let h = handlers();
4572    if h.player_place_block.is_empty() {
4573        return;
4574    }
4575    let (p, b, d) = (jstr!(env, player), jstr!(env, block), jstr!(env, dimension));
4576    let ev = YogPlaceBlockEvent {
4577        player: YogStr::from_str(&p),
4578        block: YogStr::from_str(&b),
4579        pos: YogBlockPos { x, y, z },
4580        dimension: YogStr::from_str(&d),
4581    };
4582    let srv = srv_ptr();
4583    guard("on_player_place_block", || {
4584        each(&h.player_place_block, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4585    });
4586}
4587
4588#[unsafe(no_mangle)]
4589pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerDeathPre<'l>(
4590    mut env: JNIEnv<'l>,
4591    _class: JClass<'l>,
4592    player: JString<'l>,
4593    uuid: JString<'l>,
4594    source: JString<'l>,
4595    dimension: JString<'l>,
4596) -> jni::sys::jboolean {
4597    let h = handlers();
4598    if h.player_death.is_empty() {
4599        return 1;
4600    }
4601    let p = jstr!(env, player, 1);
4602    let u = jstr!(env, uuid, 1);
4603    let s = jstr!(env, source, 1);
4604    let d = jstr!(env, dimension, 1);
4605    let ev = YogPlayerDeathEvent {
4606        player: YogStr::from_str(&p),
4607        uuid: YogStr::from_str(&u),
4608        source: YogStr::from_str(&s),
4609        dimension: YogStr::from_str(&d),
4610    };
4611    let srv = srv_ptr();
4612    allowed(&h.player_death, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4613}
4614
4615#[unsafe(no_mangle)]
4616pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerDeath<'l>(
4617    mut env: JNIEnv<'l>,
4618    _class: JClass<'l>,
4619    player: JString<'l>,
4620    uuid: JString<'l>,
4621    source: JString<'l>,
4622    dimension: JString<'l>,
4623) {
4624    let h = handlers();
4625    if h.player_death.is_empty() {
4626        return;
4627    }
4628    let (p, u, s, d) = (
4629        jstr!(env, player),
4630        jstr!(env, uuid),
4631        jstr!(env, source),
4632        jstr!(env, dimension),
4633    );
4634    let ev = YogPlayerDeathEvent {
4635        player: YogStr::from_str(&p),
4636        uuid: YogStr::from_str(&u),
4637        source: YogStr::from_str(&s),
4638        dimension: YogStr::from_str(&d),
4639    };
4640    let srv = srv_ptr();
4641    guard("on_player_death", || {
4642        each(&h.player_death, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4643    });
4644}
4645
4646#[unsafe(no_mangle)]
4647pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerRespawn<'l>(
4648    mut env: JNIEnv<'l>,
4649    _class: JClass<'l>,
4650    player: JString<'l>,
4651    uuid: JString<'l>,
4652    at_anchor: jni::sys::jboolean,
4653    dimension: JString<'l>,
4654) {
4655    let h = handlers();
4656    if h.player_respawn.is_empty() {
4657        return;
4658    }
4659    let (p, u, d) = (jstr!(env, player), jstr!(env, uuid), jstr!(env, dimension));
4660    let ev = YogPlayerRespawnEvent {
4661        player: YogStr::from_str(&p),
4662        uuid: YogStr::from_str(&u),
4663        at_anchor: at_anchor != 0,
4664        dimension: YogStr::from_str(&d),
4665    };
4666    let srv = srv_ptr();
4667    guard("on_player_respawn", || {
4668        each(&h.player_respawn, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4669    });
4670}
4671
4672#[unsafe(no_mangle)]
4673pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnAdvancement<'l>(
4674    mut env: JNIEnv<'l>,
4675    _class: JClass<'l>,
4676    player: JString<'l>,
4677    uuid: JString<'l>,
4678    advancement: JString<'l>,
4679    dimension: JString<'l>,
4680) {
4681    let h = handlers();
4682    if h.advancement.is_empty() {
4683        return;
4684    }
4685    let (p, u, a, d) = (
4686        jstr!(env, player),
4687        jstr!(env, uuid),
4688        jstr!(env, advancement),
4689        jstr!(env, dimension),
4690    );
4691    let ev = YogAdvancementEvent {
4692        player: YogStr::from_str(&p),
4693        uuid: YogStr::from_str(&u),
4694        advancement: YogStr::from_str(&a),
4695        dimension: YogStr::from_str(&d),
4696    };
4697    let srv = srv_ptr();
4698    guard("on_advancement", || {
4699        each(&h.advancement, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4700    });
4701}
4702
4703#[unsafe(no_mangle)]
4704pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityInteractPre<'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) -> jni::sys::jboolean {
4714    let h = handlers();
4715    if h.entity_interact.is_empty() {
4716        return 1;
4717    }
4718    let p = jstr!(env, player, 1);
4719    let pu = jstr!(env, player_uuid, 1);
4720    let et = jstr!(env, entity_type, 1);
4721    let eu = jstr!(env, entity_uuid, 1);
4722    let ha = jstr!(env, hand, 1);
4723    let d = jstr!(env, dimension, 1);
4724    let ev = YogEntityInteractEvent {
4725        player: YogStr::from_str(&p),
4726        player_uuid: YogStr::from_str(&pu),
4727        entity_type: YogStr::from_str(&et),
4728        entity_uuid: YogStr::from_str(&eu),
4729        hand: YogStr::from_str(&ha),
4730        dimension: YogStr::from_str(&d),
4731    };
4732    let srv = srv_ptr();
4733    allowed(&h.entity_interact, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4734}
4735
4736#[unsafe(no_mangle)]
4737pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnEntityInteract<'l>(
4738    mut env: JNIEnv<'l>,
4739    _class: JClass<'l>,
4740    player: JString<'l>,
4741    player_uuid: JString<'l>,
4742    entity_type: JString<'l>,
4743    entity_uuid: JString<'l>,
4744    hand: JString<'l>,
4745    dimension: JString<'l>,
4746) {
4747    let h = handlers();
4748    if h.entity_interact.is_empty() {
4749        return;
4750    }
4751    let (p, pu) = (jstr!(env, player), jstr!(env, player_uuid));
4752    let (et, eu, ha, d) = (
4753        jstr!(env, entity_type),
4754        jstr!(env, entity_uuid),
4755        jstr!(env, hand),
4756        jstr!(env, dimension),
4757    );
4758    let ev = YogEntityInteractEvent {
4759        player: YogStr::from_str(&p),
4760        player_uuid: YogStr::from_str(&pu),
4761        entity_type: YogStr::from_str(&et),
4762        entity_uuid: YogStr::from_str(&eu),
4763        hand: YogStr::from_str(&ha),
4764        dimension: YogStr::from_str(&d),
4765    };
4766    let srv = srv_ptr();
4767    guard("on_entity_interact", || {
4768        each(&h.entity_interact, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4769    });
4770}
4771
4772#[unsafe(no_mangle)]
4773pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemCraft<'l>(
4774    mut env: JNIEnv<'l>,
4775    _class: JClass<'l>,
4776    player: JString<'l>,
4777    player_uuid: JString<'l>,
4778    result_item: JString<'l>,
4779    result_count: jint,
4780    dimension: JString<'l>,
4781) {
4782    let h = handlers();
4783    if h.item_craft.is_empty() {
4784        return;
4785    }
4786    let (p, pu, ri, d) = (
4787        jstr!(env, player),
4788        jstr!(env, player_uuid),
4789        jstr!(env, result_item),
4790        jstr!(env, dimension),
4791    );
4792    let ev = YogCraftEvent {
4793        player: YogStr::from_str(&p),
4794        player_uuid: YogStr::from_str(&pu),
4795        result_item: YogStr::from_str(&ri),
4796        result_count: result_count as u32,
4797        dimension: YogStr::from_str(&d),
4798    };
4799    let srv = srv_ptr();
4800    guard("on_item_craft", || {
4801        each(&h.item_craft, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4802    });
4803}
4804
4805#[unsafe(no_mangle)]
4806pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnExplosionPre<'l>(
4807    mut env: JNIEnv<'l>,
4808    _class: JClass<'l>,
4809    dimension: JString<'l>,
4810    x: jdouble,
4811    y: jdouble,
4812    z: jdouble,
4813    power: jfloat,
4814    cause_uuid: JString<'l>,
4815) -> jni::sys::jboolean {
4816    let h = handlers();
4817    if h.explosion.is_empty() {
4818        return 1;
4819    }
4820    let d = jstr!(env, dimension, 1);
4821    let cu = jstr!(env, cause_uuid, 1);
4822    let ev = YogExplosionEvent {
4823        dimension: YogStr::from_str(&d),
4824        x,
4825        y,
4826        z,
4827        power,
4828        cause_uuid: YogStr::from_str(&cu),
4829    };
4830    let srv = srv_ptr();
4831    allowed(&h.explosion, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4832}
4833
4834#[unsafe(no_mangle)]
4835pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnExplosion<'l>(
4836    mut env: JNIEnv<'l>,
4837    _class: JClass<'l>,
4838    dimension: JString<'l>,
4839    x: jdouble,
4840    y: jdouble,
4841    z: jdouble,
4842    power: jfloat,
4843    cause_uuid: JString<'l>,
4844) {
4845    let h = handlers();
4846    if h.explosion.is_empty() {
4847        return;
4848    }
4849    let (d, cu) = (jstr!(env, dimension), jstr!(env, cause_uuid));
4850    let ev = YogExplosionEvent {
4851        dimension: YogStr::from_str(&d),
4852        x,
4853        y,
4854        z,
4855        power,
4856        cause_uuid: YogStr::from_str(&cu),
4857    };
4858    let srv = srv_ptr();
4859    guard("on_explosion", || {
4860        each(&h.explosion, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4861    });
4862}
4863
4864// ── ABI minor 9 JNI entry points ─────────────────────────────────────────────
4865
4866#[unsafe(no_mangle)]
4867pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemPickupPre<'l>(
4868    mut env: JNIEnv<'l>,
4869    _class: JClass<'l>,
4870    player: JString<'l>,
4871    player_uuid: JString<'l>,
4872    item_id: JString<'l>,
4873    item_count: jint,
4874    entity_uuid: JString<'l>,
4875    dimension: JString<'l>,
4876) -> jni::sys::jboolean {
4877    let h = handlers();
4878    if h.item_pickup.is_empty() {
4879        return 1;
4880    }
4881    let p = jstr!(env, player, 1);
4882    let pu = jstr!(env, player_uuid, 1);
4883    let ii = jstr!(env, item_id, 1);
4884    let eu = jstr!(env, entity_uuid, 1);
4885    let d = jstr!(env, dimension, 1);
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    allowed(&h.item_pickup, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4896}
4897
4898#[unsafe(no_mangle)]
4899pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnItemPickup<'l>(
4900    mut env: JNIEnv<'l>,
4901    _class: JClass<'l>,
4902    player: JString<'l>,
4903    player_uuid: JString<'l>,
4904    item_id: JString<'l>,
4905    item_count: jint,
4906    entity_uuid: JString<'l>,
4907    dimension: JString<'l>,
4908) {
4909    let h = handlers();
4910    if h.item_pickup.is_empty() {
4911        return;
4912    }
4913    let (p, pu) = (jstr!(env, player), jstr!(env, player_uuid));
4914    let (ii, eu, d) = (
4915        jstr!(env, item_id),
4916        jstr!(env, entity_uuid),
4917        jstr!(env, dimension),
4918    );
4919    let ev = YogItemPickupEvent {
4920        player: YogStr::from_str(&p),
4921        player_uuid: YogStr::from_str(&pu),
4922        item_id: YogStr::from_str(&ii),
4923        item_count: item_count as u32,
4924        entity_uuid: YogStr::from_str(&eu),
4925        dimension: YogStr::from_str(&d),
4926    };
4927    let srv = srv_ptr();
4928    guard("on_item_pickup", || {
4929        each(&h.item_pickup, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4930    });
4931}
4932
4933#[unsafe(no_mangle)]
4934pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnPlayerMove<'l>(
4935    mut env: JNIEnv<'l>,
4936    _class: JClass<'l>,
4937    player: JString<'l>,
4938    player_uuid: JString<'l>,
4939    x: jdouble,
4940    y: jdouble,
4941    z: jdouble,
4942    yaw: jfloat,
4943    pitch: jfloat,
4944    dimension: JString<'l>,
4945) {
4946    let h = handlers();
4947    if h.player_move.is_empty() {
4948        return;
4949    }
4950    let (p, pu, d) = (
4951        jstr!(env, player),
4952        jstr!(env, player_uuid),
4953        jstr!(env, dimension),
4954    );
4955    let ev = YogPlayerMoveEvent {
4956        player: YogStr::from_str(&p),
4957        player_uuid: YogStr::from_str(&pu),
4958        x,
4959        y,
4960        z,
4961        yaw,
4962        pitch,
4963        dimension: YogStr::from_str(&d),
4964    };
4965    let srv = srv_ptr();
4966    guard("on_player_move", || {
4967        each(&h.player_move, |ud, f| unsafe { f(ud, srv, &ev, 1) });
4968    });
4969}
4970
4971#[unsafe(no_mangle)]
4972pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerOpenPre<'l>(
4973    mut env: JNIEnv<'l>,
4974    _class: JClass<'l>,
4975    player: JString<'l>,
4976    player_uuid: JString<'l>,
4977    dimension: JString<'l>,
4978) -> jni::sys::jboolean {
4979    let h = handlers();
4980    if h.container_open.is_empty() {
4981        return 1;
4982    }
4983    let p = jstr!(env, player, 1);
4984    let pu = jstr!(env, player_uuid, 1);
4985    let d = jstr!(env, dimension, 1);
4986    let ev = YogContainerOpenEvent {
4987        player: YogStr::from_str(&p),
4988        player_uuid: YogStr::from_str(&pu),
4989        container_type: YogStr::EMPTY,
4990        dimension: YogStr::from_str(&d),
4991    };
4992    let srv = srv_ptr();
4993    allowed(&h.container_open, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
4994}
4995
4996#[unsafe(no_mangle)]
4997pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerOpen<'l>(
4998    mut env: JNIEnv<'l>,
4999    _class: JClass<'l>,
5000    player: JString<'l>,
5001    player_uuid: JString<'l>,
5002    container_type: JString<'l>,
5003    dimension: JString<'l>,
5004) {
5005    let h = handlers();
5006    if h.container_open.is_empty() {
5007        return;
5008    }
5009    let (p, pu, ct, d) = (
5010        jstr!(env, player),
5011        jstr!(env, player_uuid),
5012        jstr!(env, container_type),
5013        jstr!(env, dimension),
5014    );
5015    let ev = YogContainerOpenEvent {
5016        player: YogStr::from_str(&p),
5017        player_uuid: YogStr::from_str(&pu),
5018        container_type: YogStr::from_str(&ct),
5019        dimension: YogStr::from_str(&d),
5020    };
5021    let srv = srv_ptr();
5022    guard("on_container_open", || {
5023        each(&h.container_open, |ud, f| unsafe { f(ud, srv, &ev, 1) });
5024    });
5025}
5026
5027#[unsafe(no_mangle)]
5028pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnContainerClose<'l>(
5029    mut env: JNIEnv<'l>,
5030    _class: JClass<'l>,
5031    player: JString<'l>,
5032    player_uuid: JString<'l>,
5033    dimension: JString<'l>,
5034) {
5035    let h = handlers();
5036    if h.container_close.is_empty() {
5037        return;
5038    }
5039    let (p, pu, d) = (
5040        jstr!(env, player),
5041        jstr!(env, player_uuid),
5042        jstr!(env, dimension),
5043    );
5044    let ev = YogContainerCloseEvent {
5045        player: YogStr::from_str(&p),
5046        player_uuid: YogStr::from_str(&pu),
5047        dimension: YogStr::from_str(&d),
5048    };
5049    let srv = srv_ptr();
5050    guard("on_container_close", || {
5051        each(&h.container_close, |ud, f| unsafe { f(ud, srv, &ev, 1) });
5052    });
5053}
5054
5055#[unsafe(no_mangle)]
5056pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnProjectileHitPre<'l>(
5057    mut env: JNIEnv<'l>,
5058    _class: JClass<'l>,
5059    projectile_type: JString<'l>,
5060    projectile_uuid: JString<'l>,
5061    shooter_uuid: JString<'l>,
5062    hit_type: JString<'l>,
5063    hit_entity_uuid: JString<'l>,
5064    x: jdouble,
5065    y: jdouble,
5066    z: jdouble,
5067    dimension: JString<'l>,
5068) -> jni::sys::jboolean {
5069    let h = handlers();
5070    if h.projectile_hit.is_empty() {
5071        return 1;
5072    }
5073    let pt = jstr!(env, projectile_type, 1);
5074    let pu = jstr!(env, projectile_uuid, 1);
5075    let su = jstr!(env, shooter_uuid, 1);
5076    let ht = jstr!(env, hit_type, 1);
5077    let heu = jstr!(env, hit_entity_uuid, 1);
5078    let dim = jstr!(env, dimension, 1);
5079    let ev = YogProjectileHitEvent {
5080        projectile_type: YogStr::from_str(&pt),
5081        projectile_uuid: YogStr::from_str(&pu),
5082        shooter_uuid: YogStr::from_str(&su),
5083        hit_type: YogStr::from_str(&ht),
5084        hit_entity_uuid: YogStr::from_str(&heu),
5085        x,
5086        y,
5087        z,
5088        dimension: YogStr::from_str(&dim),
5089    };
5090    let srv = srv_ptr();
5091    allowed(&h.projectile_hit, |ud, f| unsafe { f(ud, srv, &ev, 0) }) as jni::sys::jboolean
5092}
5093
5094#[unsafe(no_mangle)]
5095pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnProjectileHit<'l>(
5096    mut env: JNIEnv<'l>,
5097    _class: JClass<'l>,
5098    projectile_type: JString<'l>,
5099    projectile_uuid: JString<'l>,
5100    shooter_uuid: JString<'l>,
5101    hit_type: JString<'l>,
5102    hit_entity_uuid: JString<'l>,
5103    x: jdouble,
5104    y: jdouble,
5105    z: jdouble,
5106    dimension: JString<'l>,
5107) {
5108    let h = handlers();
5109    if h.projectile_hit.is_empty() {
5110        return;
5111    }
5112    let (pt, pu) = (jstr!(env, projectile_type), jstr!(env, projectile_uuid));
5113    let (su, ht) = (jstr!(env, shooter_uuid), jstr!(env, hit_type));
5114    let (heu, dim) = (jstr!(env, hit_entity_uuid), jstr!(env, dimension));
5115    let ev = YogProjectileHitEvent {
5116        projectile_type: YogStr::from_str(&pt),
5117        projectile_uuid: YogStr::from_str(&pu),
5118        shooter_uuid: YogStr::from_str(&su),
5119        hit_type: YogStr::from_str(&ht),
5120        hit_entity_uuid: YogStr::from_str(&heu),
5121        x,
5122        y,
5123        z,
5124        dimension: YogStr::from_str(&dim),
5125    };
5126    let srv = srv_ptr();
5127    guard("on_projectile_hit", || {
5128        each(&h.projectile_hit, |ud, f| unsafe { f(ud, srv, &ev, 1) });
5129    });
5130}
5131
5132// ── ABI minor 10 — client-side JNI entry points ───────────────────────────────
5133
5134#[unsafe(no_mangle)]
5135pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnClientTick<'l>(
5136    _env: JNIEnv<'l>,
5137    _class: JClass<'l>,
5138) {
5139    let h = handlers();
5140    if h.client_tick.is_empty() {
5141        return;
5142    }
5143    guard("on_client_tick", || {
5144        each(&h.client_tick, |ud, f| unsafe { f(ud) });
5145    });
5146}
5147
5148/// The host's GL function pointer for `sym` (0 if it has none).
5149fn gl_proc_address(sym: &str) -> usize {
5150    let ptr: Option<i64> = unsafe { bridge!("glProcAddress", [S(YogStr::from_str(sym))]) };
5151    ptr.unwrap_or(0) as usize
5152}
5153
5154#[unsafe(no_mangle)]
5155pub extern "system" fn Java_dev_yog_NativeBridge_nativeGlInit<'l>(
5156    _env: JNIEnv<'l>,
5157    _class: JClass<'l>,
5158) {
5159    if GL.get().is_some() {
5160        return;
5161    }
5162    let mut raw_get_binary: usize = 0;
5163    let mut raw_prog_binary: usize = 0;
5164    let gl = unsafe {
5165        glow::Context::from_loader_function(|sym| {
5166            let ptr = gl_proc_address(sym);
5167            // Capture extension pointers while the loader runs.
5168            match sym {
5169                "glGetProgramBinary" => raw_get_binary = ptr,
5170                "glProgramBinary" => raw_prog_binary = ptr,
5171                _ => {}
5172            }
5173            ptr as *const _
5174        })
5175    };
5176    let _ = GL.set(GlCtx(gl));
5177    let _ = GL_GET_PROGRAM_BINARY.set((raw_get_binary != 0).then_some(raw_get_binary));
5178    let _ = GL_PROGRAM_BINARY.set((raw_prog_binary != 0).then_some(raw_prog_binary));
5179    // `glGetProgramiv` is a core function; always available.  We look it up once here
5180    // to avoid depending on glow internals for the PROGRAM_BINARY_LENGTH query.
5181    let get_program_iv = gl_proc_address("glGetProgramiv");
5182    let _ = GL_GET_PROGRAM_IV.set((get_program_iv != 0).then_some(get_program_iv));
5183}
5184
5185#[unsafe(no_mangle)]
5186pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnHudRender<'l>(
5187    _env: JNIEnv<'l>,
5188    _class: JClass<'l>,
5189    delta_tick: jfloat,
5190    screen_w: jint,
5191    screen_h: jint,
5192    scale_factor: jfloat,
5193    player_x: jfloat,
5194    player_y: jfloat,
5195    player_z: jfloat,
5196) {
5197    let h = handlers();
5198    let mut gfx = GFX_FN_TABLE;
5199    gfx.screen_w = screen_w;
5200    gfx.screen_h = screen_h;
5201    gfx.delta_tick = delta_tick;
5202    gfx.scale_factor = scale_factor;
5203    gfx.player_pos = [player_x, player_y, player_z];
5204
5205    // Mod hud_render callbacks.
5206    if !h.hud_render.is_empty() {
5207        guard("on_hud_render", || {
5208            each(&h.hud_render, |ud, f| unsafe { f(ud, &gfx) });
5209        });
5210    }
5211
5212    // Render active book UIs.
5213    let active_layers: Vec<UiLayer> = h.active_uis.lock().unwrap().clone();
5214    let sw = screen_w as f32;
5215    let sh = screen_h as f32;
5216    let ctx = unsafe { yog_gfx::GfxContext::from_raw(&gfx as *const _) };
5217    let mut renderers = h.book_renderers.lock().unwrap();
5218    for layer in &active_layers {
5219        if !layer.visible {
5220            continue;
5221        }
5222        if let Some(renderer) = renderers.get_mut(&layer.id) {
5223            renderer.render(&ctx, sw, sh);
5224        }
5225    }
5226}
5227
5228#[unsafe(no_mangle)]
5229pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnWorldRender<'l>(
5230    env: JNIEnv<'l>,
5231    _class: JClass<'l>,
5232    delta_tick: jfloat,
5233    screen_w: jint,
5234    screen_h: jint,
5235    scale_factor: jfloat,
5236    view_proj_arr: JFloatArray<'l>,
5237    cam_x: jfloat,
5238    cam_y: jfloat,
5239    cam_z: jfloat,
5240    player_x: jfloat,
5241    player_y: jfloat,
5242    player_z: jfloat,
5243) {
5244    let h = handlers();
5245    if h.world_render.is_empty() {
5246        return;
5247    }
5248    let mut view_proj = [0f32; 16];
5249    if env
5250        .get_float_array_region(&view_proj_arr, 0, &mut view_proj)
5251        .is_err()
5252    {
5253        return;
5254    }
5255    let mut gfx = GFX_FN_TABLE;
5256    gfx.screen_w = screen_w;
5257    gfx.screen_h = screen_h;
5258    gfx.delta_tick = delta_tick;
5259    gfx.scale_factor = scale_factor;
5260    gfx.view_proj = view_proj;
5261    gfx.camera_pos = [cam_x, cam_y, cam_z];
5262    gfx.player_pos = [player_x, player_y, player_z];
5263    guard("on_world_render", || {
5264        each(&h.world_render, |ud, f| unsafe { f(ud, &gfx) });
5265    });
5266}
5267
5268#[unsafe(no_mangle)]
5269pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnKeyPress<'l>(
5270    _env: JNIEnv<'l>,
5271    _class: JClass<'l>,
5272    key_code: jint,
5273    scan_code: jint,
5274    action: jint,
5275    modifiers: jint,
5276) -> jni::sys::jboolean {
5277    let h = handlers();
5278    if h.key_press.is_empty() {
5279        return 1;
5280    }
5281    let ev = YogKeyPressEvent {
5282        key_code,
5283        scan_code,
5284        action,
5285        modifiers,
5286    };
5287    allowed(&h.key_press, |ud, f| unsafe { f(ud, &ev) }) as jni::sys::jboolean
5288}
5289
5290#[unsafe(no_mangle)]
5291pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnScreenOpen<'l>(
5292    mut env: JNIEnv<'l>,
5293    _class: JClass<'l>,
5294    screen_class: JString<'l>,
5295) {
5296    let h = handlers();
5297    if h.screen_open.is_empty() {
5298        return;
5299    }
5300    let sc = match env.get_string(&screen_class) {
5301        Ok(s) => String::from(s),
5302        Err(_) => return,
5303    };
5304    guard("on_screen_open", || {
5305        each(&h.screen_open, |ud, f| unsafe {
5306            f(ud, YogStr::from_str(&sc))
5307        });
5308    });
5309}
5310
5311#[unsafe(no_mangle)]
5312pub extern "system" fn Java_dev_yog_NativeBridge_nativeOnScreenClose<'l>(
5313    mut env: JNIEnv<'l>,
5314    _class: JClass<'l>,
5315    screen_class: JString<'l>,
5316) {
5317    let h = handlers();
5318    if h.screen_close.is_empty() {
5319        return;
5320    }
5321    let sc = match env.get_string(&screen_class) {
5322        Ok(s) => String::from(s),
5323        Err(_) => return,
5324    };
5325    guard("on_screen_close", || {
5326        each(&h.screen_close, |ud, f| unsafe {
5327            f(ud, YogStr::from_str(&sc))
5328        });
5329    });
5330}
5331
5332mod client_api;
5333mod control_socket;
5334mod images;
5335mod svg;
5336mod jcall;
5337mod ui_jni;