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