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