Skip to main content

yog_runtime/
lib.rs

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