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