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