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