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