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