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