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