Skip to main content

yog_runtime/
lib.rs

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