Sign in Sign up
kretrod/lodestone Public
Branches
master
2682 lines (2488 loc) · 103.7 KB Raw
//! Minecraft bindings.
//!
//! 26.2 ships unobfuscated, so every name below is the real one from the game's
//! source. Field and method IDs stay valid for the life of the class, so they
//! are resolved once; object references never are, and get re-read each frame.
//!
//! Anything that fails to resolve is recorded in `missing` rather than being
//! treated as fatal — one renamed field should cost you that feature, not the
//! whole menu.

use jni_sys::{jclass, jfieldID, jmethodID, jobject, jvalue};

use crate::jni::Jni;

const MINECRAFT: &str = "net/minecraft/client/Minecraft";
const LOCAL_PLAYER: &str = "net/minecraft/client/player/LocalPlayer";
const ENTITY: &str = "net/minecraft/world/entity/Entity";
const LIVING: &str = "net/minecraft/world/entity/LivingEntity";
const ABILITIES: &str = "net/minecraft/world/entity/player/Abilities";
const VEC3: &str = "net/minecraft/world/phys/Vec3";
const GAME_MODE: &str = "net/minecraft/client/multiplayer/MultiPlayerGameMode";
const WINDOW: &str = "com/mojang/blaze3d/platform/Window";

/// Everything resolved once at startup.
pub struct Mc {
    pub minecraft: jclass,
    pub local_player: jclass,
    pub living: jclass,

    f_instance: jfieldID,
    f_player: jfieldID,
    f_level: jfieldID,
    f_game_mode: jfieldID,
    f_window: jfieldID,
    f_mouse_handler: Option<jfieldID>,
    f_mouse_grabbed: Option<jfieldID>,
    m_set_ignore_first_move: Option<jmethodID>,
    f_accum_dx: Option<jfieldID>,
    f_accum_dy: Option<jfieldID>,
    f_singleplayer: Option<jfieldID>,

    f_abilities: jfieldID,
    f_position: jfieldID,
    f_x_rot: jfieldID,
    f_y_rot: jfieldID,
    f_y_rot_o: Option<jfieldID>,
    f_x_rot_o: Option<jfieldID>,
    f_y_head_rot: Option<jfieldID>,
    f_y_head_rot_o: Option<jfieldID>,
    f_y_body_rot: Option<jfieldID>,
    f_y_body_rot_o: Option<jfieldID>,
    f_smart_cull: Option<jfieldID>,
    f_right_click_delay: Option<jfieldID>,
    f_miss_time: Option<jfieldID>,
    m_set_shift: Option<jmethodID>,
    f_delta_tracker: Option<jfieldID>,
    f_ms_per_tick: Option<jfieldID>,
    f_gui: Option<jfieldID>,
    f_hud: Option<jfieldID>,
    f_hud_hidden: Option<jfieldID>,
    f_xo: Option<jfieldID>,
    f_yo: Option<jfieldID>,
    f_zo: Option<jfieldID>,
    f_x_old: Option<jfieldID>,
    f_y_old: Option<jfieldID>,
    f_z_old: Option<jfieldID>,
    m_partial_tick: Option<jmethodID>,
    f_on_ground: Option<jfieldID>,
    f_no_physics: Option<jfieldID>,
    f_fall_distance: Option<jfieldID>,
    f_hurt_time: Option<jfieldID>,

    f_flying: jfieldID,
    f_may_fly: jfieldID,
    f_fly_speed: jfieldID,
    f_walk_speed: jfieldID,
    f_instabuild: Option<jfieldID>,

    pub f_vx: jfieldID,
    pub f_vy: jfieldID,
    pub f_vz: jfieldID,

    f_destroy_delay: Option<jfieldID>,

    m_set_delta: Option<jmethodID>,
    m_get_delta: Option<jmethodID>,
    m_set_sprinting: Option<jmethodID>,
    m_is_sprinting: Option<jmethodID>,
    m_get_health: Option<jmethodID>,
    m_set_pos: Option<jmethodID>,
    m_start_attack: Option<jmethodID>,
    m_respawn: Option<jmethodID>,
    m_attack_strength: Option<jmethodID>,
    m_line_of_sight: Option<jmethodID>,
    f_tick_count: Option<jfieldID>,
    f_x_last: Option<jfieldID>,
    f_y_last: Option<jfieldID>,
    f_z_last: Option<jfieldID>,
    f_yrot_last: Option<jfieldID>,
    f_xrot_last: Option<jfieldID>,
    f_position_reminder: Option<jfieldID>,
    f_jumping: Option<jfieldID>,
    m_window_width: Option<jmethodID>,
    m_window_height: Option<jmethodID>,

    pub missing: Vec<String>,
}

/// Resolve a required id, or bail out of construction.
macro_rules! need {
    ($miss:expr, $what:expr, $expr:expr) => {
        match $expr {
            Some(v) => v,
            None => {
                $miss.push($what.to_string());
                return Err($miss.join(", "));
            }
        }
    };
}

/// Resolve an optional id, recording a miss and carrying on.
macro_rules! want {
    ($miss:expr, $what:expr, $expr:expr) => {
        match $expr {
            Some(v) => Some(v),
            None => {
                $miss.push($what.to_string());
                None
            }
        }
    };
}

impl Mc {
    pub fn resolve(j: &Jni) -> Result<Mc, String> {
        let mut missing: Vec<String> = Vec::new();

        let minecraft = need!(missing, "class Minecraft", class(j, MINECRAFT));
        let local_player = need!(missing, "class LocalPlayer", class(j, LOCAL_PLAYER));
        let entity = need!(missing, "class Entity", class(j, ENTITY));
        let living = need!(missing, "class LivingEntity", class(j, LIVING));
        let abilities = need!(missing, "class Abilities", class(j, ABILITIES));
        let vec3 = need!(missing, "class Vec3", class(j, VEC3));
        let game_mode = need!(missing, "class MultiPlayerGameMode", class(j, GAME_MODE));
        let window = class(j, WINDOW);

        let f_instance = need!(
            missing,
            "Minecraft.instance",
            j.static_field(minecraft, "instance", "Lnet/minecraft/client/Minecraft;")
        );
        let f_player = need!(
            missing,
            "Minecraft.player",
            j.field(minecraft, "player", "Lnet/minecraft/client/player/LocalPlayer;")
        );
        let f_level = need!(
            missing,
            "Minecraft.level",
            j.field(minecraft, "level", "Lnet/minecraft/client/multiplayer/ClientLevel;")
        );
        let f_game_mode = need!(
            missing,
            "Minecraft.gameMode",
            j.field(
                minecraft,
                "gameMode",
                "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;"
            )
        );
        let f_window = need!(
            missing,
            "Minecraft.window",
            j.field(minecraft, "window", "Lcom/mojang/blaze3d/platform/Window;")
        );
        // 26.2 has no Minecraft.screen field; whether the mouse is grabbed is
        // the same question in practice — grabbed means you are in the world
        // and not in a GUI.
        let f_mouse_handler = want!(
            missing,
            "Minecraft.mouseHandler",
            j.field(minecraft, "mouseHandler", "Lnet/minecraft/client/MouseHandler;")
        );
        let (f_mouse_grabbed, m_set_ignore_first_move, f_accum_dx, f_accum_dy) =
            match class(j, "net/minecraft/client/MouseHandler") {
                Some(c) => (
                    want!(missing, "MouseHandler.mouseGrabbed", j.field(c, "mouseGrabbed", "Z")),
                    j.method(c, "setIgnoreFirstMove", "()V"),
                    j.field(c, "accumulatedDX", "D"),
                    j.field(c, "accumulatedDY", "D"),
                ),
                None => (None, None, None, None),
            };
        let f_singleplayer = want!(
            missing,
            "Minecraft.singleplayerServer",
            j.field(
                minecraft,
                "singleplayerServer",
                "Lnet/minecraft/client/server/IntegratedServer;"
            )
        );

        let f_abilities = need!(
            missing,
            "Player.abilities",
            j.field(
                local_player,
                "abilities",
                "Lnet/minecraft/world/entity/player/Abilities;"
            )
        );
        let f_position = need!(
            missing,
            "Entity.position",
            j.field(entity, "position", "Lnet/minecraft/world/phys/Vec3;")
        );
        let f_x_rot = need!(missing, "Entity.xRot", j.field(entity, "xRot", "F"));
        let f_y_rot = need!(missing, "Entity.yRot", j.field(entity, "yRot", "F"));
        let f_y_head_rot = j.field(living, "yHeadRot", "F");
        let f_y_head_rot_o = j.field(living, "yHeadRotO", "F");
        let f_y_body_rot = j.field(living, "yBodyRot", "F");
        let f_y_body_rot_o = j.field(living, "yBodyRotO", "F");
        let f_smart_cull = j.field(minecraft, "smartCull", "Z");
        // Client-only use/attack cooldowns the server does not enforce.
        let f_right_click_delay = j.field(minecraft, "rightClickDelay", "I");
        let f_miss_time = j.field(minecraft, "missTime", "I");
        let m_set_shift = j.method(entity, "setShiftKeyDown", "(Z)V");
        let f_delta_tracker = j.field(minecraft, "deltaTracker", "Lnet/minecraft/client/DeltaTracker$Timer;");
        let f_ms_per_tick = class(j, "net/minecraft/client/DeltaTracker$Timer")
            .and_then(|c| j.field(c, "msPerTick", "F"));
        let m_partial_tick = class(j, "net/minecraft/client/DeltaTracker$Timer")
            .and_then(|c| j.method(c, "getGameTimeDeltaPartialTick", "(Z)F"));
        // 26.2 moved the HUD out of Gui into its own class; isHidden is what F1
        // toggles.
        let f_gui = j.field(minecraft, "gui", "Lnet/minecraft/client/gui/Gui;");
        let f_hud = class(j, "net/minecraft/client/gui/Gui")
            .and_then(|c| j.field(c, "hud", "Lnet/minecraft/client/gui/Hud;"));
        let f_hud_hidden = class(j, "net/minecraft/client/gui/Hud")
            .and_then(|c| j.field(c, "isHidden", "Z"));
        let f_y_rot_o = j.field(entity, "yRotO", "F");
        let f_x_rot_o = j.field(entity, "xRotO", "F");
        let f_xo = j.field(entity, "xo", "D");
        let f_yo = j.field(entity, "yo", "D");
        let f_zo = j.field(entity, "zo", "D");
        let f_x_old = j.field(entity, "xOld", "D");
        let f_y_old = j.field(entity, "yOld", "D");
        let f_z_old = j.field(entity, "zOld", "D");
        let f_on_ground = want!(missing, "Entity.onGround", j.field(entity, "onGround", "Z"));
        let f_no_physics = want!(missing, "Entity.noPhysics", j.field(entity, "noPhysics", "Z"));
        let f_fall_distance = want!(
            missing,
            "Entity.fallDistance",
            j.field(entity, "fallDistance", "D")
        );
        let f_hurt_time = want!(missing, "LivingEntity.hurtTime", j.field(living, "hurtTime", "I"));

        let f_flying = need!(missing, "Abilities.flying", j.field(abilities, "flying", "Z"));
        let f_may_fly = need!(missing, "Abilities.mayfly", j.field(abilities, "mayfly", "Z"));
        let f_fly_speed = need!(
            missing,
            "Abilities.flyingSpeed",
            j.field(abilities, "flyingSpeed", "F")
        );
        let f_walk_speed = need!(
            missing,
            "Abilities.walkingSpeed",
            j.field(abilities, "walkingSpeed", "F")
        );
        let f_instabuild = want!(
            missing,
            "Abilities.instabuild",
            j.field(abilities, "instabuild", "Z")
        );

        let f_vx = need!(missing, "Vec3.x", j.field(vec3, "x", "D"));
        let f_vy = need!(missing, "Vec3.y", j.field(vec3, "y", "D"));
        let f_vz = need!(missing, "Vec3.z", j.field(vec3, "z", "D"));

        let f_destroy_delay = want!(
            missing,
            "MultiPlayerGameMode.destroyDelay",
            j.field(game_mode, "destroyDelay", "I")
        );

        let m_set_delta = want!(
            missing,
            "Entity.setDeltaMovement(DDD)",
            j.method(entity, "setDeltaMovement", "(DDD)V")
        );
        let m_get_delta = want!(
            missing,
            "Entity.getDeltaMovement()",
            j.method(entity, "getDeltaMovement", "()Lnet/minecraft/world/phys/Vec3;")
        );
        let m_set_sprinting = want!(
            missing,
            "Entity.setSprinting(Z)",
            j.method(entity, "setSprinting", "(Z)V")
        );
        let m_is_sprinting = want!(
            missing,
            "Entity.isSprinting()",
            j.method(entity, "isSprinting", "()Z")
        );
        let m_set_pos = want!(
            missing,
            "Entity.setPos(DDD)",
            j.method(entity, "setPos", "(DDD)V")
        );
        let m_start_attack = want!(
            missing,
            "Minecraft.startAttack()",
            j.method(minecraft, "startAttack", "()Z")
        );
        let f_jumping = want!(missing, "LivingEntity.jumping", j.field(living, "jumping", "Z"));
        let m_respawn = want!(
            missing,
            "LocalPlayer.respawn()",
            j.method(local_player, "respawn", "()V")
        );
        let m_attack_strength = match class(j, "net/minecraft/world/entity/player/Player") {
            Some(c) => want!(
                missing,
                "Player.getAttackStrengthScale(F)",
                j.method(c, "getAttackStrengthScale", "(F)F")
            ),
            None => None,
        };
        let m_line_of_sight = want!(
            missing,
            "LivingEntity.hasLineOfSight(Entity)",
            j.method(
                living,
                "hasLineOfSight",
                "(Lnet/minecraft/world/entity/Entity;)Z"
            )
        );
        let f_tick_count = want!(missing, "Entity.tickCount", j.field(entity, "tickCount", "I"));
        // What LocalPlayer.sendPosition() compares against to decide whether a
        // movement packet is needed at all.
        let f_x_last = want!(missing, "LocalPlayer.xLast", j.field(local_player, "xLast", "D"));
        let f_y_last = want!(missing, "LocalPlayer.yLast", j.field(local_player, "yLast", "D"));
        let f_z_last = want!(missing, "LocalPlayer.zLast", j.field(local_player, "zLast", "D"));
        let f_yrot_last = j.field(local_player, "yRotLast", "F");
        let f_xrot_last = j.field(local_player, "xRotLast", "F");
        let f_position_reminder = want!(
            missing,
            "LocalPlayer.positionReminder",
            j.field(local_player, "positionReminder", "I")
        );

        let m_get_health = want!(
            missing,
            "LivingEntity.getHealth()",
            j.method(living, "getHealth", "()F")
        );
        let (m_window_width, m_window_height) = match window {
            Some(w) => (
                want!(missing, "Window.getWidth()", j.method(w, "getWidth", "()I")),
                want!(missing, "Window.getHeight()", j.method(w, "getHeight", "()I")),
            ),
            None => {
                missing.push("class Window".into());
                (None, None)
            }
        };

        Ok(Mc {
            minecraft,
            local_player,
            living,
            f_instance,
            f_player,
            f_level,
            f_game_mode,
            f_window,
            f_mouse_handler,
            f_mouse_grabbed,
            m_set_ignore_first_move,
            f_accum_dx,
            f_accum_dy,
            f_singleplayer,
            f_abilities,
            f_position,
            f_x_rot,
            f_y_rot,
            f_y_rot_o,
            f_x_rot_o,
            f_y_head_rot,
            f_y_head_rot_o,
            f_y_body_rot,
            f_y_body_rot_o,
            f_smart_cull,
            f_right_click_delay,
            f_miss_time,
            m_set_shift,
            f_delta_tracker,
            f_ms_per_tick,
            f_gui,
            f_hud,
            f_hud_hidden,
            f_xo,
            f_yo,
            f_zo,
            f_x_old,
            f_y_old,
            f_z_old,
            m_partial_tick,
            f_on_ground,
            f_no_physics,
            f_fall_distance,
            f_hurt_time,
            f_flying,
            f_may_fly,
            f_fly_speed,
            f_walk_speed,
            f_instabuild,
            f_vx,
            f_vy,
            f_vz,
            f_destroy_delay,
            m_set_delta,
            m_get_delta,
            m_set_sprinting,
            m_is_sprinting,
            m_get_health,
            m_set_pos,
            m_start_attack,
            m_respawn,
            m_attack_strength,
            m_line_of_sight,
            f_tick_count,
            f_x_last,
            f_y_last,
            f_z_last,
            f_yrot_last,
            f_xrot_last,
            f_position_reminder,
            f_jumping,
            m_window_width,
            m_window_height,
            missing,
        })
    }

    // ---- navigation --------------------------------------------------------

    pub fn instance(&self, j: &Jni) -> Option<jobject> {
        j.static_obj_field(self.minecraft, self.f_instance)
    }

    pub fn player(&self, j: &Jni, mc: jobject) -> Option<jobject> {
        j.obj_field(mc, self.f_player)
    }

    pub fn level(&self, j: &Jni, mc: jobject) -> Option<jobject> {
        j.obj_field(mc, self.f_level)
    }

    pub fn game_mode(&self, j: &Jni, mc: jobject) -> Option<jobject> {
        j.obj_field(mc, self.f_game_mode)
    }

    pub fn abilities(&self, j: &Jni, player: jobject) -> Option<jobject> {
        j.obj_field(player, self.f_abilities)
    }

    /// True while a GUI screen (inventory, pause menu, chat) is open — the
    /// game releases the mouse for exactly those.
    pub fn screen_open(&self, j: &Jni, mc: jobject) -> bool {
        let (Some(fh), Some(fg)) = (self.f_mouse_handler, self.f_mouse_grabbed) else {
            return false;
        };
        match j.obj_field(mc, fh) {
            Some(h) => !j.bool_field(h, fg).unwrap_or(true),
            None => false,
        }
    }

    pub fn single_player(&self, j: &Jni, mc: jobject) -> bool {
        match self.f_singleplayer {
            Some(f) => j.obj_field(mc, f).is_some(),
            None => false,
        }
    }

    pub fn window_size(&self, j: &Jni, mc: jobject) -> Option<(i32, i32)> {
        let w = j.obj_field(mc, self.f_window)?;
        let width = j.call_int(w, self.m_window_width?, &[])?;
        let height = j.call_int(w, self.m_window_height?, &[])?;
        Some((width, height))
    }

    // ---- entity state ------------------------------------------------------

    pub fn position(&self, j: &Jni, entity: jobject) -> Option<(f64, f64, f64)> {
        let v = j.obj_field(entity, self.f_position)?;
        Some((
            j.double_field(v, self.f_vx)?,
            j.double_field(v, self.f_vy)?,
            j.double_field(v, self.f_vz)?,
        ))
    }

    /// The previous-tick position (xOld/yOld/zOld) — the start of the
    /// interpolation the renderer lerps from towards `position`.
    pub fn old_position(&self, j: &Jni, entity: jobject) -> Option<(f64, f64, f64)> {
        Some((
            j.double_field(entity, self.f_x_old?)?,
            j.double_field(entity, self.f_y_old?)?,
            j.double_field(entity, self.f_z_old?)?,
        ))
    }

    /// The current frame's partial tick in [0,1]: how far the render is between
    /// the last game tick and the next. Entities are drawn at
    /// `lerp(old, current, partialTick)`, so anything projected from their
    /// live position must apply the same interpolation or it trails the model.
    pub fn partial_tick(&self, j: &Jni, instance: jobject) -> f32 {
        let (Some(ft), Some(m)) = (self.f_delta_tracker, self.m_partial_tick) else {
            return 1.0;
        };
        let Some(timer) = j.obj_field(instance, ft) else {
            return 1.0;
        };
        j.call_float(timer, m, &[jvalue { z: 1 }]).unwrap_or(1.0)
    }

    pub fn rotation(&self, j: &Jni, entity: jobject) -> (f32, f32) {
        (
            j.float_field(entity, self.f_y_rot).unwrap_or(0.0),
            j.float_field(entity, self.f_x_rot).unwrap_or(0.0),
        )
    }

    pub fn on_ground(&self, j: &Jni, entity: jobject) -> bool {
        self.f_on_ground
            .and_then(|f| j.bool_field(entity, f))
            .unwrap_or(false)
    }

    pub fn health(&self, j: &Jni, entity: jobject) -> Option<f32> {
        j.call_float(entity, self.m_get_health?, &[])
    }

    pub fn is_sprinting(&self, j: &Jni, entity: jobject) -> bool {
        self.m_is_sprinting
            .and_then(|m| j.call_bool(entity, m, &[]))
            .unwrap_or(false)
    }

    // ---- writes ------------------------------------------------------------

    /// Point an entity somewhere. yHeadRot matters too, or the body and head
    /// disagree and the result looks wrong to everyone else.
    pub fn set_rotation(&self, j: &Jni, entity: jobject, yaw: f32, pitch: f32) {
        j.set_float(entity, self.f_y_rot, yaw);
        j.set_float(entity, self.f_x_rot, pitch.clamp(-90.0, 90.0));
        if let Some(f) = self.f_y_head_rot {
            j.set_float(entity, f, yaw);
        }
    }

    pub fn set_sprinting(&self, j: &Jni, entity: jobject, on: bool) {
        if let Some(m) = self.m_set_sprinting {
            j.call_void(entity, m, &[jvalue { z: on as u8 }]);
        }
    }

    pub fn set_delta_movement(&self, j: &Jni, entity: jobject, x: f64, y: f64, z: f64) {
        if let Some(m) = self.m_set_delta {
            j.call_void(entity, m, &[jvalue { d: x }, jvalue { d: y }, jvalue { d: z }]);
        }
    }

    pub fn delta_movement(&self, j: &Jni, entity: jobject) -> Option<(f64, f64, f64)> {
        let v = j.call_obj(entity, self.m_get_delta?, &[])?;
        Some((
            j.double_field(v, self.f_vx)?,
            j.double_field(v, self.f_vy)?,
            j.double_field(v, self.f_vz)?,
        ))
    }

    /// Force the sneak (shift) state. The client's own physics backs you off a
    /// ledge while shift is held (maybeBackOffFromEdge), and player position is
    /// client-authoritative, so this genuinely keeps you on the block — that is
    /// Safewalk.
    pub fn set_shift(&self, j: &Jni, entity: jobject, on: bool) {
        if let Some(m) = self.m_set_shift {
            j.call_void(entity, m, &[jvalue { z: on as u8 }]);
        }
    }

    /// Scale the game clock. advanceGameTime divides elapsed real time by the
    /// timer's msPerTick to decide how many ticks to run, so a smaller value
    /// runs the whole game — movement, mining, eating, and the packets they
    /// send — proportionally faster. 50 ms/tick is normal (20 TPS).
    pub fn set_timer(&self, j: &Jni, instance: jobject, multiplier: f32) {
        let (Some(ft), Some(fm)) = (self.f_delta_tracker, self.f_ms_per_tick) else {
            return;
        };
        let Some(timer) = j.obj_field(instance, ft) else {
            return;
        };
        let want = 50.0 / multiplier.clamp(0.1, 10.0);
        j.set_float(timer, fm, want);
    }

    /// Zero the client-side use/attack cooldowns. The server has no rate limit
    /// on use, place, interact, attack or break packets — these three fields
    /// are the client throttling itself, so clearing them each frame lifts the
    /// cap to one action per tick (the input loop's own ceiling). destroyDelay
    /// lives on the game mode and is handled where mining is.
    pub fn clear_use_cooldowns(&self, j: &Jni, instance: jobject) {
        if let Some(f) = self.f_right_click_delay {
            if j.int_field(instance, f).unwrap_or(0) != 0 {
                j.set_int(instance, f, 0);
            }
        }
        if let Some(f) = self.f_miss_time {
            if j.int_field(instance, f).unwrap_or(0) != 0 {
                j.set_int(instance, f, 0);
            }
        }
    }

    /// Hide the whole HUD — hotbar, hearts, hunger, experience bar — the way
    /// F1 does. Freecam wants this: you are not looking through your own eyes,
    /// so your own status bars have no business being on screen.
    pub fn set_hud_hidden(&self, j: &Jni, instance: jobject, hidden: bool) {
        let (Some(fg), Some(fh), Some(fi)) = (self.f_gui, self.f_hud, self.f_hud_hidden) else {
            return;
        };
        let Some(gui) = j.obj_field(instance, fg) else {
            return;
        };
        let Some(hud) = j.obj_field(gui, fh) else {
            return;
        };
        j.set_bool(hud, fi, hidden);
    }

    pub fn hud_hidden(&self, j: &Jni, instance: jobject) -> bool {
        let (Some(fg), Some(fh), Some(fi)) = (self.f_gui, self.f_hud, self.f_hud_hidden) else {
            return false;
        };
        j.obj_field(instance, fg)
            .and_then(|gui| j.obj_field(gui, fh))
            .and_then(|hud| j.bool_field(hud, fi))
            .unwrap_or(false)
    }

    /// Discard the mouse movement that piled up while the menu held the cursor.
    ///
    /// The menu frees the cursor with a raw GLFW call, which the game's own
    /// MouseHandler knows nothing about — so on close it applies the leftover
    /// delta to the camera in one jump. Zeroing its accumulator and telling it
    /// to ignore the next move (the same thing the game does when it grabs the
    /// mouse itself) removes the jerk.
    pub fn reset_mouse_delta(&self, j: &Jni, instance: jobject) {
        let Some(handler) = self.f_mouse_handler.and_then(|f| j.obj_field(instance, f)) else {
            return;
        };
        if let Some(f) = self.f_accum_dx {
            j.set_double(handler, f, 0.0);
        }
        if let Some(f) = self.f_accum_dy {
            j.set_double(handler, f, 0.0);
        }
        if let Some(m) = self.m_set_ignore_first_move {
            j.call_void(handler, m, &[]);
        }
    }

    /// Occlusion culling. With it off, the renderer stops skipping chunk
    /// sections it thinks are hidden behind others — which is what lets you see
    /// a cave from inside the rock instead of a wall of black.
    pub fn set_smart_cull(&self, j: &Jni, instance: jobject, on: bool) {
        if let Some(f) = self.f_smart_cull {
            j.set_bool(instance, f, on);
        }
    }

    pub fn smart_cull(&self, j: &Jni, instance: jobject) -> bool {
        self.f_smart_cull
            .and_then(|f| j.bool_field(instance, f))
            .unwrap_or(true)
    }

    /// Point a camera entity, with every rotation the renderer might read.
    ///
    /// `LivingEntity.getViewYRot` does not use `yRot` at all — it interpolates
    /// `yHeadRotO` to `yHeadRot`. Setting only `yRot` leaves the view lerping
    /// between a stale head angle and the current one every frame, which reads
    /// as the camera swinging wildly on its own.
    pub fn aim_camera(&self, j: &Jni, entity: jobject, yaw: f32, pitch: f32) {
        let pitch = pitch.clamp(-90.0, 90.0);
        for (f, v) in [
            (self.f_y_rot, yaw),
            (self.f_x_rot, pitch),
        ] {
            j.set_float(entity, f, v);
        }
        for (f, v) in [
            (self.f_y_rot_o, yaw),
            (self.f_x_rot_o, pitch),
            (self.f_y_head_rot, yaw),
            (self.f_y_head_rot_o, yaw),
            (self.f_y_body_rot, yaw),
            (self.f_y_body_rot_o, yaw),
        ] {
            if let Some(f) = f {
                j.set_float(entity, f, v);
            }
        }
    }

    /// Move an entity's previous-tick and render-previous positions with it,
    /// so nothing interpolates from where it used to be.
    pub fn set_old_position(&self, j: &Jni, entity: jobject, pos: (f64, f64, f64)) {
        for (f, v) in [
            (self.f_xo, pos.0),
            (self.f_yo, pos.1),
            (self.f_zo, pos.2),
            (self.f_x_old, pos.0),
            (self.f_y_old, pos.1),
            (self.f_z_old, pos.2),
        ] {
            if let Some(f) = f {
                j.set_double(entity, f, v);
            }
        }
    }

    pub fn set_pos(&self, j: &Jni, entity: jobject, x: f64, y: f64, z: f64) -> bool {
        match self.m_set_pos {
            Some(m) => {
                j.call_void(entity, m, &[jvalue { d: x }, jvalue { d: y }, jvalue { d: z }]);
                true
            }
            None => false,
        }
    }

    pub fn jumping(&self, j: &Jni, entity: jobject) -> bool {
        self.f_jumping
            .and_then(|f| j.bool_field(entity, f))
            .unwrap_or(false)
    }

    /// The game's own "swing at whatever is under the crosshair", so the
    /// attack goes through exactly the path a real click takes.
    pub fn start_attack(&self, j: &Jni, instance: jobject) {
        if let Some(m) = self.m_start_attack {
            let _ = j.call_bool(instance, m, &[]);
        }
    }

    /// The game's tick counter. Physics runs per tick, not per frame, so any
    /// module that *adds* to velocity has to act once a tick or it compounds
    /// with the frame rate.
    pub fn tick_count(&self, j: &Jni, entity: jobject) -> i32 {
        self.f_tick_count
            .and_then(|f| j.int_field(entity, f))
            .unwrap_or(0)
    }

    /// How far the attack cooldown has recharged, 0..1. Swinging below 1.0
    /// deals a fraction of the damage, which is why a fast aura hits for
    /// almost nothing in modern Minecraft.
    pub fn attack_strength(&self, j: &Jni, player: jobject) -> f32 {
        self.m_attack_strength
            .and_then(|m| j.call_float(player, m, &[jvalue { f: 0.0 }]))
            .unwrap_or(1.0)
    }

    pub fn has_line_of_sight(&self, j: &Jni, from: jobject, to: jobject) -> bool {
        self.m_line_of_sight
            .and_then(|m| j.call_bool(from, m, &[jvalue { l: to }]))
            .unwrap_or(true)
    }

    pub fn respawn(&self, j: &Jni, player: jobject) {
        if let Some(m) = self.m_respawn {
            j.call_void(player, m, &[]);
        }
    }

    pub fn set_jumping(&self, j: &Jni, entity: jobject, on: bool) {
        if let Some(f) = self.f_jumping {
            j.set_bool(entity, f, on);
        }
    }

    /// Stop the client telling the server where it is.
    ///
    /// `sendPosition()` only builds a packet when the current position differs
    /// from `xLast/yLast/zLast`, or when `positionReminder` reaches 20. Writing
    /// the current position into those every frame makes both tests fail, so no
    /// movement packet goes out and the server keeps the last position it saw.
    /// This is what makes freecam actually free rather than a rubber-band.
    pub fn freeze_sent_position(&self, j: &Jni, player: jobject, pos: (f64, f64, f64)) {
        if let (Some(fx), Some(fy), Some(fz)) = (self.f_x_last, self.f_y_last, self.f_z_last) {
            j.set_double(player, fx, pos.0);
            j.set_double(player, fy, pos.1);
            j.set_double(player, fz, pos.2);
        }
        if let Some(f) = self.f_position_reminder {
            j.set_int(player, f, 0);
        }
    }

    /// The last position this client told the server about — which *is* the
    /// server's view of you until the next movement packet lands.
    pub fn sent_position(&self, j: &Jni, player: jobject) -> Option<(f64, f64, f64)> {
        Some((
            j.double_field(player, self.f_x_last?)?,
            j.double_field(player, self.f_y_last?)?,
            j.double_field(player, self.f_z_last?)?,
        ))
    }

    /// Freeze the rotation the client reports, the same way.
    pub fn freeze_sent_rotation(&self, j: &Jni, player: jobject, yaw: f32, pitch: f32) {
        if let Some(f) = self.f_yrot_last {
            j.set_float(player, f, yaw);
        }
        if let Some(f) = self.f_xrot_last {
            j.set_float(player, f, pitch);
        }
    }

    pub fn set_no_physics(&self, j: &Jni, entity: jobject, on: bool) {
        if let Some(f) = self.f_no_physics {
            j.set_bool(entity, f, on);
        }
    }

    pub fn set_fall_distance(&self, j: &Jni, entity: jobject, v: f64) {
        if let Some(f) = self.f_fall_distance {
            j.set_double(entity, f, v);
        }
    }

    pub fn fall_distance(&self, j: &Jni, entity: jobject) -> f64 {
        self.f_fall_distance
            .and_then(|f| j.double_field(entity, f))
            .unwrap_or(0.0)
    }

    pub fn hurt_time(&self, j: &Jni, entity: jobject) -> i32 {
        self.f_hurt_time
            .and_then(|f| j.int_field(entity, f))
            .unwrap_or(0)
    }

    pub fn set_hurt_time(&self, j: &Jni, entity: jobject, v: i32) {
        if let Some(f) = self.f_hurt_time {
            j.set_int(entity, f, v);
        }
    }

    pub fn set_destroy_delay(&self, j: &Jni, game_mode: jobject, v: i32) {
        if let Some(f) = self.f_destroy_delay {
            j.set_int(game_mode, f, v);
        }
    }

    // ---- abilities ---------------------------------------------------------

    pub fn flying(&self, j: &Jni, ab: jobject) -> bool {
        j.bool_field(ab, self.f_flying).unwrap_or(false)
    }
    pub fn may_fly(&self, j: &Jni, ab: jobject) -> bool {
        j.bool_field(ab, self.f_may_fly).unwrap_or(false)
    }
    pub fn set_flying(&self, j: &Jni, ab: jobject, v: bool) {
        j.set_bool(ab, self.f_flying, v);
    }
    pub fn set_may_fly(&self, j: &Jni, ab: jobject, v: bool) {
        j.set_bool(ab, self.f_may_fly, v);
    }
    pub fn fly_speed(&self, j: &Jni, ab: jobject) -> f32 {
        j.float_field(ab, self.f_fly_speed).unwrap_or(0.05)
    }
    pub fn set_fly_speed(&self, j: &Jni, ab: jobject, v: f32) {
        j.set_float(ab, self.f_fly_speed, v);
    }
    pub fn walk_speed(&self, j: &Jni, ab: jobject) -> f32 {
        j.float_field(ab, self.f_walk_speed).unwrap_or(0.1)
    }
    pub fn set_walk_speed(&self, j: &Jni, ab: jobject, v: f32) {
        j.set_float(ab, self.f_walk_speed, v);
    }
    pub fn set_instabuild(&self, j: &Jni, ab: jobject, v: bool) {
        if let Some(f) = self.f_instabuild {
            j.set_bool(ab, f, v);
        }
    }
    pub fn instabuild(&self, j: &Jni, ab: jobject) -> bool {
        self.f_instabuild
            .and_then(|f| j.bool_field(ab, f))
            .unwrap_or(false)
    }
}

/// Find a class and pin it with a global reference: field and method IDs are
/// only valid while their class stays loaded.
fn class(j: &Jni, name: &str) -> Option<jclass> {
    let local = j.find_class(name)?;
    j.global(local).map(|g| g as jclass)
}

// ---------------------------------------------------------------------------
// Player skin
//
// The ESP preview draws the player's real skin, which means getting at the GL
// texture the game already has resident rather than decoding a PNG ourselves.
// 26.2 routes that through Blaze3D: the skin is a `ClientAsset.Texture` naming
// an `Identifier`, the `TextureManager` turns that into an `AbstractTexture`,
// and the OpenGL backend's `GlTexture` hands back the raw texture name.
// ---------------------------------------------------------------------------

pub struct Skin {
    m_get_skin: jmethodID,
    m_body: jmethodID,
    m_texture_path: jmethodID,
    m_model: jmethodID,
    m_texture_manager: jmethodID,
    m_get_texture: jmethodID,
    m_gpu_texture: jmethodID,
    m_gl_id: jmethodID,
    /// `PlayerModelType.SLIM`, compared by identity so no string is decoded.
    slim_value: jobject,
    // Out of a world there is no player entity to ask, but the client still
    // knows who you are: the session user and the skin manager answer both
    // questions from the main menu.
    m_get_user: jmethodID,
    m_user_name: jmethodID,
    m_game_profile: jmethodID,
    m_skin_manager: jmethodID,
    m_create_lookup: jmethodID,
    m_supplier_get: jmethodID,
}

impl Skin {
    pub fn resolve(j: &Jni, missing: &mut Vec<String>) -> Option<Skin> {
        let out = Self::bind(j);
        if out.is_none() {
            missing.push("player skin (ESP preview)".to_string());
        }
        out
    }

    fn bind(j: &Jni) -> Option<Skin> {
        let player = class(j, "net/minecraft/client/player/AbstractClientPlayer")?;
        let skin = class(j, "net/minecraft/world/entity/player/PlayerSkin")?;
        let asset = class(j, "net/minecraft/core/ClientAsset$Texture")?;
        let model_type = class(j, "net/minecraft/world/entity/player/PlayerModelType")?;
        let minecraft = class(j, "net/minecraft/client/Minecraft")?;
        let manager = class(j, "net/minecraft/client/renderer/texture/TextureManager")?;
        let texture = class(j, "net/minecraft/client/renderer/texture/AbstractTexture")?;
        let gl_texture = class(j, "com/mojang/blaze3d/opengl/GlTexture")?;
        let user = class(j, "net/minecraft/client/User")?;
        let skin_manager = class(j, "net/minecraft/client/resources/SkinManager")?;
        let supplier = class(j, "java/util/function/Supplier")?;

        let slim_field = j.static_field(
            model_type,
            "SLIM",
            "Lnet/minecraft/world/entity/player/PlayerModelType;",
        )?;
        let slim_local = j.static_obj_field(model_type, slim_field)?;
        Some(Skin {
            m_get_skin: j.method(
                player,
                "getSkin",
                "()Lnet/minecraft/world/entity/player/PlayerSkin;",
            )?,
            m_body: j.method(skin, "body", "()Lnet/minecraft/core/ClientAsset$Texture;")?,
            m_texture_path: j.method(asset, "texturePath", "()Lnet/minecraft/resources/Identifier;")?,
            m_model: j.method(
                skin,
                "model",
                "()Lnet/minecraft/world/entity/player/PlayerModelType;",
            )?,
            m_texture_manager: j.method(
                minecraft,
                "getTextureManager",
                "()Lnet/minecraft/client/renderer/texture/TextureManager;",
            )?,
            m_get_texture: j.method(
                manager,
                "getTexture",
                "(Lnet/minecraft/resources/Identifier;)Lnet/minecraft/client/renderer/texture/AbstractTexture;",
            )?,
            m_gpu_texture: j.method(
                texture,
                "getTexture",
                "()Lcom/mojang/blaze3d/textures/GpuTexture;",
            )?,
            // Declared on the OpenGL implementation rather than the abstract
            // GpuTexture, which is fine: the object really is a GlTexture.
            m_gl_id: j.method(gl_texture, "glId", "()I")?,
            slim_value: j.global(slim_local)?,
            m_get_user: j.method(minecraft, "getUser", "()Lnet/minecraft/client/User;")?,
            m_user_name: j.method(user, "getName", "()Ljava/lang/String;")?,
            m_game_profile: j.method(
                minecraft,
                "getGameProfile",
                "()Lcom/mojang/authlib/GameProfile;",
            )?,
            m_skin_manager: j.method(
                minecraft,
                "getSkinManager",
                "()Lnet/minecraft/client/resources/SkinManager;",
            )?,
            // The Supplier form resolves straight away — it yields the default
            // skin while the real one is still downloading — where the
            // CompletableFuture form would need polling.
            m_create_lookup: j.method(
                skin_manager,
                "createLookup",
                "(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/function/Supplier;",
            )?,
            m_supplier_get: j.method(supplier, "get", "()Ljava/lang/Object;")?,
        })
    }

    /// The GL texture name of this player's skin, and whether the model is the
    /// three-pixel-arm "slim" variant.
    /// The skin of a player *entity*, which only exists inside a world.
    pub fn of(&self, j: &Jni, instance: jobject, player: jobject) -> Option<(u32, bool)> {
        let skin = j.call_obj(player, self.m_get_skin, &[])?;
        self.from_skin(j, instance, skin)
    }

    /// The skin of the logged-in profile, which works at the main menu too.
    pub fn of_profile(&self, j: &Jni, instance: jobject) -> Option<(u32, bool)> {
        let manager = j.call_obj(instance, self.m_skin_manager, &[])?;
        let profile = j.call_obj(instance, self.m_game_profile, &[])?;
        // `false`: do not insist on a signed skin, so this still answers when
        // offline or before the signature arrives.
        let lookup = j.call_obj(
            manager,
            self.m_create_lookup,
            &[jvalue { l: profile }, jvalue { z: 0 }],
        )?;
        let skin = j.call_obj(lookup, self.m_supplier_get, &[])?;
        self.from_skin(j, instance, skin)
    }

    /// The name on the session, which is known before any world is loaded.
    pub fn name(&self, j: &Jni, instance: jobject) -> Option<String> {
        let user = j.call_obj(instance, self.m_get_user, &[])?;
        let s = j.call_obj(user, self.m_user_name, &[])?;
        let out = j.rust_string(s);
        j.delete_local(s);
        j.delete_local(user);
        out
    }

    /// Turn a `PlayerSkin` into the GL texture name behind it, and whether the
    /// model is the three-pixel-arm "slim" variant.
    fn from_skin(&self, j: &Jni, instance: jobject, skin: jobject) -> Option<(u32, bool)> {
        let body = j.call_obj(skin, self.m_body, &[])?;
        let id = j.call_obj(body, self.m_texture_path, &[])?;
        let manager = j.call_obj(instance, self.m_texture_manager, &[])?;
        let texture = j.call_obj(manager, self.m_get_texture, &[jvalue { l: id }])?;
        let gpu = j.call_obj(texture, self.m_gpu_texture, &[])?;
        let gl = j.call_int(gpu, self.m_gl_id, &[])?;
        if gl <= 0 {
            return None;
        }
        let slim = j
            .call_obj(skin, self.m_model, &[])
            .map(|m| j.same_object(m, self.slim_value))
            .unwrap_or(false);
        Some((gl as u32, slim))
    }
}

// ---------------------------------------------------------------------------
// World scanning and combat
//
// Everything below is resolved the same way as the core bindings: optional, so
// a rename costs one feature rather than the whole client.
// ---------------------------------------------------------------------------

/// The entity classes we sort targets into, plus the calls needed to walk the
/// level's entity list and to hit something.
pub struct World {
    pub player_class: jclass,
    monster_class: Option<jclass>,
    animal_class: Option<jclass>,
    item_class: Option<jclass>,
    living_class: jclass,
    arrow_class: Option<jclass>,

    m_entities_for_rendering: Option<jmethodID>,
    m_iterator: Option<jmethodID>,
    m_has_next: Option<jmethodID>,
    m_next: Option<jmethodID>,

    m_is_alive: Option<jmethodID>,
    m_get_id: Option<jmethodID>,
    m_get_bounding_box: Option<jmethodID>,
    m_get_eye_position: Option<jmethodID>,
    m_get_name: Option<jmethodID>,
    m_component_string: Option<jmethodID>,
    m_get_max_health: Option<jmethodID>,
    m_main_hand: Option<jmethodID>,
    m_hover_name: Option<jmethodID>,
    m_stack_empty: Option<jmethodID>,
    m_armor_value: Option<jmethodID>,
    m_active_effects: Option<jmethodID>,
    m_effect_desc: Option<jmethodID>,
    m_effect_ampl: Option<jmethodID>,
    m_effect_dur: Option<jmethodID>,
    m_coll_iter: Option<jmethodID>,
    m_iter_has: Option<jmethodID>,
    m_iter_next: Option<jmethodID>,
    m_is_invisible: Option<jmethodID>,
    m_can_critical: Option<jmethodID>,
    m_is_using_item: Option<jmethodID>,
    m_ticks_using: Option<jmethodID>,
    posrot_class: Option<jclass>,
    m_posrot_init: Option<jmethodID>,
    status_class: Option<jclass>,
    m_status_init: Option<jmethodID>,
    m_listener_send: Option<jmethodID>,
    m_get_uuid: Option<jmethodID>,
    m_attr_value: Option<jmethodID>,
    f_player_connection: Option<jfieldID>,
    m_server_brand: Option<jmethodID>,
    m_get_player_info: Option<jmethodID>,
    m_get_latency: Option<jmethodID>,

    f_min_x: Option<jfieldID>,
    f_min_y: Option<jfieldID>,
    f_min_z: Option<jfieldID>,
    f_max_x: Option<jfieldID>,
    f_max_y: Option<jfieldID>,
    f_max_z: Option<jfieldID>,

    m_attack: Option<jmethodID>,
    m_swing: Option<jmethodID>,
    main_hand: Option<jobject>,

    f_game_renderer: Option<jfieldID>,
    f_main_camera: Option<jfieldID>,
    f_cam_position: Option<jfieldID>,
    f_cam_x_rot: Option<jfieldID>,
    f_cam_y_rot: Option<jfieldID>,

    f_options: Option<jfieldID>,
    f_opt_fov: Option<jfieldID>,
    f_opt_gamma: Option<jfieldID>,
    f_opt_bob_view: Option<jfieldID>,
    f_opt_damage_tilt: Option<jfieldID>,
    f_opt_render_distance: Option<jfieldID>,
    f_server_render_distance: Option<jfieldID>,
    m_broadcast_options: Option<jmethodID>,
    m_set_server_render_distance: Option<jmethodID>,
    f_chunk_source: Option<jfieldID>,
    m_update_view_radius: Option<jmethodID>,
    m_loaded_chunks: Option<jmethodID>,
    boolean_class: Option<jclass>,
    m_bool_value_of: Option<jmethodID>,
    m_bool_value: Option<jmethodID>,
    m_opt_get: Option<jmethodID>,
    m_opt_set: Option<jmethodID>,
    double_class: Option<jclass>,
    m_double_value_of: Option<jmethodID>,
    m_double_value: Option<jmethodID>,
    m_int_value_of: Option<jmethodID>,
    integer_class: Option<jclass>,
    m_int_value: Option<jmethodID>,

    f_attributes: Option<jfieldID>,
    m_attr_instance: Option<jmethodID>,
    m_attr_set_base: Option<jmethodID>,
    m_attr_get_base: Option<jmethodID>,
    pub holder_entity_reach: Option<jobject>,
    pub holder_block_reach: Option<jobject>,
    pub holder_step_height: Option<jobject>,

    f_rain_level: Option<jfieldID>,
    f_thunder_level: Option<jfieldID>,
    f_o_rain_level: Option<jfieldID>,
    f_o_thunder_level: Option<jfieldID>,
    f_touching_water: Option<jfieldID>,
    f_horizontal_collision: Option<jfieldID>,

    // Freecam: a detached camera entity, and the input that must stop driving
    // the body while the camera is the thing moving.
    m_set_camera_entity: Option<jmethodID>,
    armor_stand_class: Option<jclass>,
    m_armor_stand_init: Option<jmethodID>,
    f_client_input: Option<jfieldID>,
    f_key_presses: Option<jfieldID>,
    f_move_vector: Option<jfieldID>,
    input_class: Option<jclass>,
    input_empty: Option<jobject>,
    vec2_zero: Option<jobject>,
    f_in_forward: Option<jfieldID>,
    f_in_back: Option<jfieldID>,
    f_in_left: Option<jfieldID>,
    f_in_right: Option<jfieldID>,
    f_in_jump: Option<jfieldID>,
    f_in_shift: Option<jfieldID>,
    f_hit_result: Option<jfieldID>,
    entity_hit_class: Option<jclass>,
    m_hit_get_entity: Option<jmethodID>,
}

impl World {
    pub fn resolve(j: &Jni, mc: &Mc, missing: &mut Vec<String>) -> Option<World> {
        let client_level = class(j, "net/minecraft/client/multiplayer/ClientLevel")?;
        let iterable = class(j, "java/lang/Iterable")?;
        let iterator = class(j, "java/util/Iterator")?;
        let entity = class(j, "net/minecraft/world/entity/Entity")?;
        let living = class(j, "net/minecraft/world/entity/LivingEntity")?;
        let player_class = class(j, "net/minecraft/world/entity/player/Player")?;
        let game_mode = class(j, "net/minecraft/client/multiplayer/MultiPlayerGameMode")?;
        let aabb = class(j, "net/minecraft/world/phys/AABB")?;
        let component = class(j, "net/minecraft/network/chat/Component");
        let camera = class(j, "net/minecraft/client/Camera");
        let renderer = class(j, "net/minecraft/client/renderer/GameRenderer");
        let options = class(j, "net/minecraft/client/Options");
        let option_instance = class(j, "net/minecraft/client/OptionInstance");

        let main_hand = class(j, "net/minecraft/world/InteractionHand").and_then(|c| {
            let f = j.static_field(c, "MAIN_HAND", "Lnet/minecraft/world/InteractionHand;")?;
            let v = j.static_obj_field(c, f)?;
            j.global(v)
        });
        if main_hand.is_none() {
            missing.push("InteractionHand.MAIN_HAND".into());
        }

        let double_class = class(j, "java/lang/Double");
        let integer_class = class(j, "java/lang/Integer");
        let attribute_instance =
            class(j, "net/minecraft/world/entity/ai/attributes/AttributeInstance");
        let attributes = class(j, "net/minecraft/world/entity/ai/attributes/Attributes");
        let entity_hit = class(j, "net/minecraft/world/phys/EntityHitResult");
        let level_class = class(j, "net/minecraft/world/level/Level");
        let armor_stand = class(j, "net/minecraft/world/entity/decoration/ArmorStand");
        let client_input = class(j, "net/minecraft/client/player/ClientInput");
        let input_cls = class(j, "net/minecraft/world/entity/player/Input");

        Some(World {
            player_class,
            monster_class: class(j, "net/minecraft/world/entity/monster/Monster"),
            // Not loaded until something shoots; FindClass loads it for us.
            arrow_class: class(j, "net/minecraft/world/entity/projectile/AbstractArrow"),
            animal_class: class(j, "net/minecraft/world/entity/animal/Animal"),
            item_class: class(j, "net/minecraft/world/entity/item/ItemEntity"),
            living_class: living,

            m_entities_for_rendering: want!(
                missing,
                "ClientLevel.entitiesForRendering()",
                j.method(client_level, "entitiesForRendering", "()Ljava/lang/Iterable;")
            ),
            m_iterator: want!(
                missing,
                "Iterable.iterator()",
                j.method(iterable, "iterator", "()Ljava/util/Iterator;")
            ),
            m_has_next: want!(missing, "Iterator.hasNext()", j.method(iterator, "hasNext", "()Z")),
            m_next: want!(
                missing,
                "Iterator.next()",
                j.method(iterator, "next", "()Ljava/lang/Object;")
            ),

            m_is_alive: want!(missing, "Entity.isAlive()", j.method(entity, "isAlive", "()Z")),
            m_get_id: j.method(entity, "getId", "()I"),
            m_get_bounding_box: want!(
                missing,
                "Entity.getBoundingBox()",
                j.method(entity, "getBoundingBox", "()Lnet/minecraft/world/phys/AABB;")
            ),
            m_get_eye_position: want!(
                missing,
                "Entity.getEyePosition()",
                j.method(entity, "getEyePosition", "()Lnet/minecraft/world/phys/Vec3;")
            ),
            m_get_name: want!(
                missing,
                "Entity.getName()",
                j.method(entity, "getName", "()Lnet/minecraft/network/chat/Component;")
            ),
            m_component_string: component
                .and_then(|c| j.method(c, "getString", "()Ljava/lang/String;")),
            m_is_invisible: j.method(entity, "isInvisible", "()Z"),
            // Private, so it needs a non-virtual call — but it is the exact
            // predicate the server will evaluate, which beats reimplementing it.
            m_can_critical: Some(player_class).and_then(|c| {
                j.method(
                    c,
                    "canCriticalAttack",
                    "(Lnet/minecraft/world/entity/Entity;)Z",
                )
            }),
            m_is_using_item: j.method(living, "isUsingItem", "()Z"),
            m_ticks_using: j.method(living, "getTicksUsingItem", "()I"),
            posrot_class: class(j, "net/minecraft/network/protocol/game/ServerboundMovePlayerPacket$PosRot"),
            m_posrot_init: class(j, "net/minecraft/network/protocol/game/ServerboundMovePlayerPacket$PosRot")
                .and_then(|c| j.method(c, "<init>", "(DDDFFZZ)V")),
            status_class: class(j, "net/minecraft/network/protocol/game/ServerboundMovePlayerPacket$StatusOnly"),
            m_status_init: class(j, "net/minecraft/network/protocol/game/ServerboundMovePlayerPacket$StatusOnly")
                .and_then(|c| j.method(c, "<init>", "(ZZ)V")),
            m_listener_send: class(j, "net/minecraft/client/multiplayer/ClientPacketListener")
                .and_then(|c| {
                    j.method(c, "send", "(Lnet/minecraft/network/protocol/Packet;)V")
                }),
            m_get_uuid: j.method(entity, "getUUID", "()Ljava/util/UUID;"),
            m_attr_value: j.method(living, "getAttributeValue", "(Lnet/minecraft/core/Holder;)D"),
            f_player_connection: j.field(
                mc.local_player,
                "connection",
                "Lnet/minecraft/client/multiplayer/ClientPacketListener;",
            ),
            m_server_brand: class(j, "net/minecraft/client/multiplayer/ClientCommonPacketListenerImpl")
                .and_then(|c| j.method(c, "serverBrand", "()Ljava/lang/String;")),
            m_get_player_info: class(j, "net/minecraft/client/multiplayer/ClientPacketListener")
                .and_then(|c| {
                    j.method(
                        c,
                        "getPlayerInfo",
                        "(Ljava/util/UUID;)Lnet/minecraft/client/multiplayer/PlayerInfo;",
                    )
                }),
            m_get_latency: class(j, "net/minecraft/client/multiplayer/PlayerInfo")
                .and_then(|c| j.method(c, "getLatency", "()I")),
            m_main_hand: j.method(living, "getMainHandItem", "()Lnet/minecraft/world/item/ItemStack;"),
            m_hover_name: class(j, "net/minecraft/world/item/ItemStack")
                .and_then(|c| j.method(c, "getHoverName", "()Lnet/minecraft/network/chat/Component;")),
            m_stack_empty: class(j, "net/minecraft/world/item/ItemStack")
                .and_then(|c| j.method(c, "isEmpty", "()Z")),
            m_armor_value: j.method(living, "getArmorValue", "()I"),
            m_active_effects: j.method(living, "getActiveEffects", "()Ljava/util/Collection;"),
            m_effect_desc: class(j, "net/minecraft/world/effect/MobEffectInstance")
                .and_then(|c| j.method(c, "getDescriptionId", "()Ljava/lang/String;")),
            m_effect_ampl: class(j, "net/minecraft/world/effect/MobEffectInstance")
                .and_then(|c| j.method(c, "getAmplifier", "()I")),
            m_effect_dur: class(j, "net/minecraft/world/effect/MobEffectInstance")
                .and_then(|c| j.method(c, "getDuration", "()I")),
            m_coll_iter: class(j, "java/util/Collection")
                .and_then(|c| j.method(c, "iterator", "()Ljava/util/Iterator;")),
            m_iter_has: class(j, "java/util/Iterator").and_then(|c| j.method(c, "hasNext", "()Z")),
            m_iter_next: class(j, "java/util/Iterator")
                .and_then(|c| j.method(c, "next", "()Ljava/lang/Object;")),
            m_get_max_health: want!(
                missing,
                "LivingEntity.getMaxHealth()",
                j.method(living, "getMaxHealth", "()F")
            ),

            f_min_x: j.field(aabb, "minX", "D"),
            f_min_y: j.field(aabb, "minY", "D"),
            f_min_z: j.field(aabb, "minZ", "D"),
            f_max_x: j.field(aabb, "maxX", "D"),
            f_max_y: j.field(aabb, "maxY", "D"),
            f_max_z: j.field(aabb, "maxZ", "D"),

            m_attack: want!(
                missing,
                "MultiPlayerGameMode.attack(Player,Entity)",
                j.method(
                    game_mode,
                    "attack",
                    "(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/Entity;)V"
                )
            ),
            m_swing: want!(
                missing,
                "LivingEntity.swing(InteractionHand)",
                j.method(living, "swing", "(Lnet/minecraft/world/InteractionHand;)V")
            ),
            main_hand,

            f_game_renderer: j.field(
                mc.minecraft,
                "gameRenderer",
                "Lnet/minecraft/client/renderer/GameRenderer;",
            ),
            f_main_camera: renderer
                .and_then(|c| j.field(c, "mainCamera", "Lnet/minecraft/client/Camera;")),
            f_cam_position: camera
                .and_then(|c| j.field(c, "position", "Lnet/minecraft/world/phys/Vec3;")),
            f_cam_x_rot: camera.and_then(|c| j.field(c, "xRot", "F")),
            f_cam_y_rot: camera.and_then(|c| j.field(c, "yRot", "F")),

            f_options: j.field(mc.minecraft, "options", "Lnet/minecraft/client/Options;"),
            f_opt_fov: options
                .and_then(|c| j.field(c, "fov", "Lnet/minecraft/client/OptionInstance;")),
            f_opt_gamma: options
                .and_then(|c| j.field(c, "gamma", "Lnet/minecraft/client/OptionInstance;")),
            f_opt_bob_view: options
                .and_then(|c| j.field(c, "bobView", "Lnet/minecraft/client/OptionInstance;")),
            f_opt_damage_tilt: options.and_then(|c| {
                j.field(c, "damageTiltStrength", "Lnet/minecraft/client/OptionInstance;")
            }),
            f_opt_render_distance: options.and_then(|c| {
                j.field(c, "renderDistance", "Lnet/minecraft/client/OptionInstance;")
            }),
            f_server_render_distance: options
                .and_then(|c| j.field(c, "serverRenderDistance", "I")),
            m_broadcast_options: options.and_then(|c| j.method(c, "broadcastOptions", "()V")),
            m_set_server_render_distance: options
                .and_then(|c| j.method(c, "setServerRenderDistance", "(I)V")),
            f_chunk_source: class(j, "net/minecraft/client/multiplayer/ClientLevel").and_then(|c| {
                j.field(
                    c,
                    "chunkSource",
                    "Lnet/minecraft/client/multiplayer/ClientChunkCache;",
                )
            }),
            m_update_view_radius: class(j, "net/minecraft/client/multiplayer/ClientChunkCache")
                .and_then(|c| j.method(c, "updateViewRadius", "(I)V")),
            m_loaded_chunks: class(j, "net/minecraft/client/multiplayer/ClientChunkCache")
                .and_then(|c| j.method(c, "getLoadedChunksCount", "()I")),
            boolean_class: class(j, "java/lang/Boolean"),
            m_bool_value_of: class(j, "java/lang/Boolean")
                .and_then(|c| j.static_method(c, "valueOf", "(Z)Ljava/lang/Boolean;")),
            m_bool_value: class(j, "java/lang/Boolean")
                .and_then(|c| j.method(c, "booleanValue", "()Z")),
            m_opt_get: option_instance
                .and_then(|c| j.method(c, "get", "()Ljava/lang/Object;")),
            m_opt_set: option_instance
                .and_then(|c| j.method(c, "set", "(Ljava/lang/Object;)V")),
            double_class,
            m_double_value_of: double_class
                .and_then(|c| j.static_method(c, "valueOf", "(D)Ljava/lang/Double;")),
            m_double_value: double_class.and_then(|c| j.method(c, "doubleValue", "()D")),
            integer_class,
            m_int_value_of: integer_class
                .and_then(|c| j.static_method(c, "valueOf", "(I)Ljava/lang/Integer;")),
            m_int_value: integer_class.and_then(|c| j.method(c, "intValue", "()I")),

            f_attributes: j.field(
                living,
                "attributes",
                "Lnet/minecraft/world/entity/ai/attributes/AttributeMap;",
            ),
            m_attr_instance: class(j, "net/minecraft/world/entity/ai/attributes/AttributeMap")
                .and_then(|c| {
                    j.method(
                        c,
                        "getInstance",
                        "(Lnet/minecraft/core/Holder;)Lnet/minecraft/world/entity/ai/attributes/AttributeInstance;",
                    )
                }),
            m_attr_set_base: attribute_instance
                .and_then(|c| j.method(c, "setBaseValue", "(D)V")),
            m_attr_get_base: attribute_instance
                .and_then(|c| j.method(c, "getBaseValue", "()D")),
            holder_entity_reach: holder(j, attributes, "ENTITY_INTERACTION_RANGE"),
            holder_block_reach: holder(j, attributes, "BLOCK_INTERACTION_RANGE"),
            holder_step_height: holder(j, attributes, "STEP_HEIGHT"),

            f_rain_level: level_class.and_then(|c| j.field(c, "rainLevel", "F")),
            f_thunder_level: level_class.and_then(|c| j.field(c, "thunderLevel", "F")),
            f_o_rain_level: level_class.and_then(|c| j.field(c, "oRainLevel", "F")),
            f_o_thunder_level: level_class.and_then(|c| j.field(c, "oThunderLevel", "F")),
            f_touching_water: j.field(entity, "wasTouchingWater", "Z"),
            f_horizontal_collision: j.field(entity, "horizontalCollision", "Z"),

            m_set_camera_entity: j.method(
                mc.minecraft,
                "setCameraEntity",
                "(Lnet/minecraft/world/entity/Entity;)V",
            ),
            armor_stand_class: armor_stand,
            m_armor_stand_init: armor_stand
                .and_then(|c| j.method(c, "<init>", "(Lnet/minecraft/world/level/Level;DDD)V")),
            f_client_input: j.field(
                mc.local_player,
                "input",
                "Lnet/minecraft/client/player/ClientInput;",
            ),
            f_key_presses: client_input.and_then(|c| {
                j.field(c, "keyPresses", "Lnet/minecraft/world/entity/player/Input;")
            }),
            f_move_vector: client_input
                .and_then(|c| j.field(c, "moveVector", "Lnet/minecraft/world/phys/Vec2;")),
            input_class: input_cls,
            input_empty: input_cls.and_then(|c| {
                let f = j.static_field(c, "EMPTY", "Lnet/minecraft/world/entity/player/Input;")?;
                let v = j.static_obj_field(c, f)?;
                j.global(v)
            }),
            vec2_zero: class(j, "net/minecraft/world/phys/Vec2").and_then(|c| {
                let f = j.static_field(c, "ZERO", "Lnet/minecraft/world/phys/Vec2;")?;
                let v = j.static_obj_field(c, f)?;
                j.global(v)
            }),
            f_in_forward: input_cls.and_then(|c| j.field(c, "forward", "Z")),
            f_in_back: input_cls.and_then(|c| j.field(c, "backward", "Z")),
            f_in_left: input_cls.and_then(|c| j.field(c, "left", "Z")),
            f_in_right: input_cls.and_then(|c| j.field(c, "right", "Z")),
            f_in_jump: input_cls.and_then(|c| j.field(c, "jump", "Z")),
            f_in_shift: input_cls.and_then(|c| j.field(c, "shift", "Z")),
            f_hit_result: j.field(
                mc.minecraft,
                "hitResult",
                "Lnet/minecraft/world/phys/HitResult;",
            ),
            entity_hit_class: entity_hit,
            m_hit_get_entity: entity_hit.and_then(|c| {
                j.method(c, "getEntity", "()Lnet/minecraft/world/entity/Entity;")
            }),
        })
    }

    /// The level's entity iterator, ready to walk.
    pub fn entity_iterator(&self, j: &Jni, level: jobject) -> Option<jobject> {
        let iterable = j.call_obj(level, self.m_entities_for_rendering?, &[])?;
        let it = j.call_obj(iterable, self.m_iterator?, &[]);
        j.delete_local(iterable);
        it
    }

    pub fn iter_next(&self, j: &Jni, iterator: jobject) -> Option<jobject> {
        if !j.call_bool(iterator, self.m_has_next?, &[])? {
            return None;
        }
        j.call_obj(iterator, self.m_next?, &[])
    }

    pub fn is_alive(&self, j: &Jni, entity: jobject) -> bool {
        self.m_is_alive
            .and_then(|m| j.call_bool(entity, m, &[]))
            .unwrap_or(false)
    }

    pub fn classify(&self, j: &Jni, entity: jobject) -> TargetKind {
        if j.is_instance(entity, self.player_class) {
            return TargetKind::Player;
        }
        if let Some(c) = self.item_class {
            if j.is_instance(entity, c) {
                return TargetKind::Item;
            }
        }
        if let Some(c) = self.monster_class {
            if j.is_instance(entity, c) {
                return TargetKind::Mob;
            }
        }
        if let Some(c) = self.animal_class {
            if j.is_instance(entity, c) {
                return TargetKind::Animal;
            }
        }
        TargetKind::Other
    }

    /// The network id of an entity — what backtrack keys on.
    pub fn entity_id(&self, j: &Jni, entity: jobject) -> Option<i32> {
        j.call_int(entity, self.m_get_id?, &[])
    }

    /// Nearest living entity to a point, within range — for the bow, which
    /// picks its own target independently of the melee aura.
    pub fn nearest_living(
        &self,
        j: &Jni,
        level: jobject,
        player: jobject,
        eye: (f64, f64, f64),
        range: f64,
    ) -> Option<jobject> {
        let iterator = self.entity_iterator(j, level)?;
        let mut best: Option<jobject> = None;
        let mut best_d = range * range;
        let mut guard = 0;
        while guard < 4096 {
            guard += 1;
            let Some(e) = self.iter_next(j, iterator) else { break };
            if j.same_object(e, player) || !self.is_living(j, e) || !self.is_alive(j, e) {
                j.delete_local(e);
                continue;
            }
            if let Some((min, max)) = self.bounding_box(j, e) {
                let c = ((min.0 + max.0) / 2.0, (min.1 + max.1) / 2.0, (min.2 + max.2) / 2.0);
                let d = (c.0 - eye.0).powi(2) + (c.1 - eye.1).powi(2) + (c.2 - eye.2).powi(2);
                if d < best_d {
                    best_d = d;
                    if let Some(prev) = best.replace(e) {
                        j.delete_local(prev);
                    }
                    continue;
                }
            }
            j.delete_local(e);
        }
        j.delete_local(iterator);
        best
    }

    pub fn is_arrow(&self, j: &Jni, entity: jobject) -> bool {
        match self.arrow_class {
            Some(c) => j.is_instance(entity, c),
            None => false,
        }
    }

    pub fn is_living(&self, j: &Jni, entity: jobject) -> bool {
        j.is_instance(entity, self.living_class)
    }

    /// World-space bounding box, which is what an ESP box is drawn from.
    pub fn bounding_box(&self, j: &Jni, entity: jobject) -> Option<((f64, f64, f64), (f64, f64, f64))> {
        let bb = j.call_obj(entity, self.m_get_bounding_box?, &[])?;
        let out = (
            (
                j.double_field(bb, self.f_min_x?)?,
                j.double_field(bb, self.f_min_y?)?,
                j.double_field(bb, self.f_min_z?)?,
            ),
            (
                j.double_field(bb, self.f_max_x?)?,
                j.double_field(bb, self.f_max_y?)?,
                j.double_field(bb, self.f_max_z?)?,
            ),
        );
        j.delete_local(bb);
        Some(out)
    }

    pub fn name(&self, j: &Jni, entity: jobject) -> Option<String> {
        let component = j.call_obj(entity, self.m_get_name?, &[])?;
        let s = j.call_obj(component, self.m_component_string?, &[])?;
        let out = j.rust_string(s);
        j.delete_local(s);
        j.delete_local(component);
        out
    }

    pub fn holder_entity_reach(&self) -> Option<jobject> {
        self.holder_entity_reach
    }

    pub fn holder_block_reach(&self) -> Option<jobject> {
        self.holder_block_reach
    }

    /// Push the player's real position to the server now, without waiting for
    /// the tick's own send — and independently of anything (Blink) that is
    /// holding the normal sends back.
    ///
    /// This is what makes a movement the server has to witness — a critical's
    /// hop, a jump boost — actually reach it at the moment it matters, and it
    /// also updates xLast/yLast/zLast so the client's own bookkeeping agrees a
    /// packet went out.
    pub fn flush_position(
        &self,
        j: &Jni,
        mc: &Mc,
        player: jobject,
        pos: (f64, f64, f64),
        yaw: f32,
        pitch: f32,
        on_ground: bool,
    ) {
        let (Some(cls), Some(init), Some(send)) =
            (self.posrot_class, self.m_posrot_init, self.m_listener_send)
        else {
            return;
        };
        let Some(conn_field) = self.f_player_connection else {
            return;
        };
        let Some(listener) = j.obj_field(player, conn_field) else {
            return;
        };
        let packet = j.new_object(
            cls,
            init,
            &[
                jvalue { d: pos.0 },
                jvalue { d: pos.1 },
                jvalue { d: pos.2 },
                jvalue { f: yaw },
                jvalue { f: pitch },
                jvalue { z: on_ground as u8 },
                jvalue { z: 0 },
            ],
        );
        let Some(packet) = packet else {
            return;
        };
        j.call_void(listener, send, &[jvalue { l: packet }]);
        j.delete_local(packet);
        // Keep the client's own "last sent" in step, so it neither sends a
        // duplicate nor, under Blink, believes nothing was sent.
        mc.freeze_sent_position(j, player, pos);
    }

    /// Tell the server our on-ground state without moving — the clean packet
    /// NoFall. The server runs its fall-damage check against the onGround flag
    /// we send (doCheckFallDamage takes it as an argument), so a StatusOnly
    /// carrying onGround=true resets the server's fall accumulation and no
    /// damage is applied, all without touching position.
    pub fn send_ground_status(&self, j: &Jni, player: jobject, on_ground: bool) {
        let (Some(cls), Some(init), Some(send)) =
            (self.status_class, self.m_status_init, self.m_listener_send)
        else {
            return;
        };
        let Some(conn_field) = self.f_player_connection else {
            return;
        };
        let Some(listener) = j.obj_field(player, conn_field) else {
            return;
        };
        let Some(packet) = j.new_object(
            cls,
            init,
            &[jvalue { z: on_ground as u8 }, jvalue { z: 0 }],
        ) else {
            return;
        };
        j.call_void(listener, send, &[jvalue { l: packet }]);
        j.delete_local(packet);
    }

    /// Would this swing be a critical? Asked of the game itself.
    ///
    /// `ServerPlayer.canCriticalAttack` is what actually decides, and it is
    /// this same method on this same class — so asking the client's copy gives
    /// the same answer the server will reach, as long as the movement that
    /// produced it has been sent.
    pub fn can_critical(&self, j: &Jni, player: jobject, target: jobject) -> Option<bool> {
        j.call_nonvirtual_bool(
            player,
            self.player_class,
            self.m_can_critical?,
            &[jvalue { l: target }],
        )
    }

    /// Charge, 0..1, of a bow being drawn — or None if nothing is being used.
    /// Vanilla: t = ticksUsing/20; power = (t*t + 2t)/3, capped at 1.
    pub fn bow_charge(&self, j: &Jni, player: jobject) -> Option<f32> {
        if !j.call_bool(player, self.m_is_using_item?, &[]).unwrap_or(false) {
            return None;
        }
        let ticks = j.call_int(player, self.m_ticks_using?, &[])? as f32;
        let t = ticks / 20.0;
        Some(((t * t + t * 2.0) / 3.0).min(1.0))
    }

    /// The server's advertised brand — anti-cheats and forks set a custom one
    /// (e.g. their product name), so it is a fingerprint on its own.
    pub fn server_brand(&self, j: &Jni, player: jobject) -> Option<String> {
        let conn = j.obj_field(player, self.f_player_connection?)?;
        let s = j.call_obj(conn, self.m_server_brand?, &[])?;
        let out = j.rust_string(s);
        j.delete_local(s);
        out
    }

    pub fn is_invisible(&self, j: &Jni, entity: jobject) -> bool {
        self.m_is_invisible
            .and_then(|m| j.call_bool(entity, m, &[]))
            .unwrap_or(false)
    }

    /// An attribute including its modifiers, unlike the base value — which is
    /// what matters for "how far can this player actually reach".
    pub fn attribute_value(&self, j: &Jni, entity: jobject, holder: Option<jobject>) -> Option<f64> {
        j.call_double(entity, self.m_attr_value?, &[jvalue { l: holder? }])
    }

    /// Round-trip time the server reports for a player, straight out of the
    /// tab-list entry it already sends.
    pub fn ping_of(&self, j: &Jni, local_player: jobject, entity: jobject) -> Option<i32> {
        let uuid = j.call_obj(entity, self.m_get_uuid?, &[])?;
        let connection = j.obj_field(local_player, self.f_player_connection?)?;
        let info = j.call_obj(connection, self.m_get_player_info?, &[jvalue { l: uuid }]);
        j.delete_local(uuid);
        let info = info?;
        let ping = j.call_int(info, self.m_get_latency?, &[]);
        j.delete_local(info);
        ping
    }

    /// Does the server actually list this entity as a person?
    ///
    /// `classify` calls anything extending `Player` a player, and server-side
    /// NPCs, holograms and duplicate "clone" entities are all built from that
    /// class — so they show up in the ESP wearing a player's colour. A real
    /// player has an entry in the client's player-info (tab) map, keyed by
    /// UUID; those fakes do not.
    ///
    /// `None` means we could not tell — the binding is missing, or there is no
    /// connection — and the caller then leaves the classification alone rather
    /// than risk hiding someone real.
    pub fn tab_player(&self, j: &Jni, local_player: jobject, entity: jobject) -> Option<bool> {
        let uuid = j.call_obj(entity, self.m_get_uuid?, &[])?;
        let connection = j.obj_field(local_player, self.f_player_connection?)?;
        let info = j.call_obj(connection, self.m_get_player_info?, &[jvalue { l: uuid }]);
        j.delete_local(uuid);
        match info {
            Some(i) => {
                j.delete_local(i);
                Some(true)
            }
            // Listed nowhere: something wearing a player's class, not a person.
            None => Some(false),
        }
    }

    /// Gear + potion intel the server already synced onto the entity: the held
    /// weapon's name, armour points, and each active effect as a short tag
    /// with its level. The same data the packet stream carries, read straight
    /// off the object.
    pub fn entity_intel(&self, j: &Jni, entity: jobject) -> (String, i32, Vec<String>) {
        let held = self
            .m_main_hand
            .and_then(|m| j.call_obj(entity, m, &[]))
            .map(|stack| {
                let empty = self
                    .m_stack_empty
                    .and_then(|m| j.call_bool(stack, m, &[]))
                    .unwrap_or(true);
                let name = if empty {
                    String::new()
                } else {
                    self.m_hover_name
                        .and_then(|m| j.call_obj(stack, m, &[]))
                        .and_then(|c| {
                            let s = self.m_component_string.and_then(|m| j.call_obj(c, m, &[]));
                            let out = s.and_then(|s| {
                                let r = j.rust_string(s);
                                j.delete_local(s);
                                r
                            });
                            j.delete_local(c);
                            out
                        })
                        .unwrap_or_default()
                };
                j.delete_local(stack);
                name
            })
            .unwrap_or_default();

        let armor = self.m_armor_value.and_then(|m| j.call_int(entity, m, &[])).unwrap_or(0);

        let mut effects = Vec::new();
        if let (Some(ae), Some(it_m), Some(has), Some(next), Some(desc)) = (
            self.m_active_effects,
            self.m_coll_iter,
            self.m_iter_has,
            self.m_iter_next,
            self.m_effect_desc,
        ) {
            if let Some(coll) = j.call_obj(entity, ae, &[]) {
                if let Some(it) = j.call_obj(coll, it_m, &[]) {
                    let mut guard = 0;
                    while guard < 32 && j.call_bool(it, has, &[]).unwrap_or(false) {
                        guard += 1;
                        let Some(inst) = j.call_obj(it, next, &[]) else { break };
                        if let Some(id_s) = j.call_obj(inst, desc, &[]) {
                            if let Some(id) = j.rust_string(id_s) {
                                // "effect.minecraft.invisibility" -> "invisibility"
                                let short = id.rsplit('.').next().unwrap_or(&id).to_string();
                                let lvl = self
                                    .m_effect_ampl
                                    .and_then(|m| j.call_int(inst, m, &[]))
                                    .unwrap_or(0)
                                    + 1;
                                effects.push(if lvl > 1 {
                                    format!("{short} {lvl}")
                                } else {
                                    short
                                });
                            }
                            j.delete_local(id_s);
                        }
                        j.delete_local(inst);
                    }
                    j.delete_local(it);
                }
                j.delete_local(coll);
            }
        }
        (held, armor, effects)
    }

    pub fn max_health(&self, j: &Jni, entity: jobject) -> f32 {
        self.m_get_max_health
            .and_then(|m| j.call_float(entity, m, &[]))
            .unwrap_or(0.0)
    }

    /// Hit something, through the game's own attack path.
    pub fn attack(&self, j: &Jni, game_mode: jobject, player: jobject, target: jobject) {
        let (Some(attack), Some(swing), Some(hand)) = (self.m_attack, self.m_swing, self.main_hand)
        else {
            return;
        };
        j.call_void(
            game_mode,
            attack,
            &[jvalue { l: player }, jvalue { l: target }],
        );
        j.call_void(player, swing, &[jvalue { l: hand }]);
    }

    /// Where the camera actually is, which is not the player when the view is
    /// in third person.
    pub fn camera(&self, j: &Jni, mc: &Mc, instance: jobject) -> Option<((f64, f64, f64), f32, f32)> {
        let renderer = j.obj_field(instance, self.f_game_renderer?)?;
        let camera = j.obj_field(renderer, self.f_main_camera?)?;
        let pos = j.obj_field(camera, self.f_cam_position?)?;
        let out = (
            (
                j.double_field(pos, mc.f_vx)?,
                j.double_field(pos, mc.f_vy)?,
                j.double_field(pos, mc.f_vz)?,
            ),
            j.float_field(camera, self.f_cam_y_rot?)?,
            j.float_field(camera, self.f_cam_x_rot?)?,
        );
        Some(out)
    }

    fn option(&self, j: &Jni, instance: jobject, which: Option<jfieldID>) -> Option<jobject> {
        let options = j.obj_field(instance, self.f_options?)?;
        j.obj_field(options, which?)
    }

    pub fn fov(&self, j: &Jni, instance: jobject) -> Option<f32> {
        let opt = self.option(j, instance, self.f_opt_fov)?;
        let boxed = j.call_obj(opt, self.m_opt_get?, &[])?;
        let v = j.call_int(boxed, self.m_int_value?, &[])?;
        j.delete_local(boxed);
        Some(v as f32)
    }

    pub fn set_fov(&self, j: &Jni, instance: jobject, value: i32) {
        let (Some(opt), Some(set), Some(value_of), Some(cls)) = (
            self.option(j, instance, self.f_opt_fov),
            self.m_opt_set,
            self.m_int_value_of,
            self.integer_class,
        ) else {
            return;
        };
        // SAFETY: a static call with a matching descriptor.
        if let Some(boxed) = j.call_static_obj(cls, value_of, &[jvalue { i: value }]) {
            j.call_void(opt, set, &[jvalue { l: boxed }]);
            j.delete_local(boxed);
        }
    }

    pub fn bob_view(&self, j: &Jni, instance: jobject) -> Option<bool> {
        let opt = self.option(j, instance, self.f_opt_bob_view)?;
        let boxed = j.call_obj(opt, self.m_opt_get?, &[])?;
        let v = j.call_bool(boxed, self.m_bool_value?, &[])?;
        j.delete_local(boxed);
        Some(v)
    }

    pub fn set_bob_view(&self, j: &Jni, instance: jobject, value: bool) {
        let (Some(opt), Some(set), Some(value_of), Some(cls)) = (
            self.option(j, instance, self.f_opt_bob_view),
            self.m_opt_set,
            self.m_bool_value_of,
            self.boolean_class,
        ) else {
            return;
        };
        if let Some(boxed) = j.call_static_obj(cls, value_of, &[jvalue { z: value as u8 }]) {
            j.call_void(opt, set, &[jvalue { l: boxed }]);
            j.delete_local(boxed);
        }
    }

    // ---- view distance ---------------------------------------------------
    //
    // A server sends chunks out to the smaller of its own view distance and the
    // one the client asked for in its settings packet. So asking for more is
    // not a trick: it is the documented way to get more, and it works whenever
    // the server's limit is above what you were requesting.

    pub fn render_distance(&self, j: &Jni, instance: jobject) -> Option<i32> {
        let opt = self.option(j, instance, self.f_opt_render_distance)?;
        let boxed = j.call_obj(opt, self.m_opt_get?, &[])?;
        let v = j.call_int(boxed, self.m_int_value?, &[])?;
        j.delete_local(boxed);
        Some(v)
    }

    /// Ask for `chunks` of view distance.
    ///
    /// Only the asking. An earlier version also resized the client's chunk
    /// store with `updateViewRadius` and raised the announced server distance
    /// by hand, reasoning that the extra chunks would otherwise be held back
    /// locally. That crashed the game: the renderer sizes its buffers from the
    /// view distance it was told about, and quietly growing the world behind it
    /// leaves more chunk sections in existence than those buffers can index.
    /// With Sodium the symptom is "Overflowed the mesh time buffer".
    ///
    /// None of it was needed. Setting the option and sending the settings
    /// packet is the whole request; the server answers with a chunk-cache-radius
    /// packet, and the game's own handler resizes the store and notifies the
    /// renderer properly. Ask, then let it do that.
    pub fn request_view_distance(&self, j: &Jni, instance: jobject, chunks: i32) {
        // Beyond vanilla's own maximum the renderer is off its designed range.
        let chunks = chunks.clamp(2, 32);
        let Some(options) = self.f_options.and_then(|f| j.obj_field(instance, f)) else {
            return;
        };
        if let (Some(opt), Some(set), Some(value_of), Some(cls)) = (
            self.option(j, instance, self.f_opt_render_distance),
            self.m_opt_set,
            self.m_int_value_of,
            self.integer_class,
        ) {
            if let Some(boxed) = j.call_static_obj(cls, value_of, &[jvalue { i: chunks }]) {
                j.call_void(opt, set, &[jvalue { l: boxed }]);
                j.delete_local(boxed);
            }
        }
        if let Some(m) = self.m_broadcast_options {
            j.call_void(options, m, &[]);
        }
    }

    pub fn loaded_chunks(&self, j: &Jni, level: jobject) -> i32 {
        let (Some(f), Some(m)) = (self.f_chunk_source, self.m_loaded_chunks) else {
            return 0;
        };
        let Some(source) = j.obj_field(level, f) else {
            return 0;
        };
        j.call_int(source, m, &[]).unwrap_or(0)
    }

    /// The accessibility slider that scales the damage tilt. Zero removes it
    /// before it is ever drawn — unlike clearing hurtTime, which only takes
    /// effect after the frame that already showed the tilt.
    pub fn damage_tilt(&self, j: &Jni, instance: jobject) -> Option<f64> {
        let opt = self.option(j, instance, self.f_opt_damage_tilt)?;
        let boxed = j.call_obj(opt, self.m_opt_get?, &[])?;
        let v = j.call_double(boxed, self.m_double_value?, &[])?;
        j.delete_local(boxed);
        Some(v)
    }

    pub fn set_damage_tilt(&self, j: &Jni, instance: jobject, value: f64) {
        let (Some(opt), Some(set), Some(value_of), Some(cls)) = (
            self.option(j, instance, self.f_opt_damage_tilt),
            self.m_opt_set,
            self.m_double_value_of,
            self.double_class,
        ) else {
            return;
        };
        if let Some(boxed) = j.call_static_obj(cls, value_of, &[jvalue { d: value }]) {
            j.call_void(opt, set, &[jvalue { l: boxed }]);
            j.delete_local(boxed);
        }
    }

    pub fn gamma(&self, j: &Jni, instance: jobject) -> Option<f64> {
        let opt = self.option(j, instance, self.f_opt_gamma)?;
        let boxed = j.call_obj(opt, self.m_opt_get?, &[])?;
        let v = j.call_double(boxed, self.m_double_value?, &[])?;
        j.delete_local(boxed);
        Some(v)
    }

    pub fn set_gamma(&self, j: &Jni, instance: jobject, value: f64) {
        let (Some(opt), Some(set), Some(value_of), Some(cls)) = (
            self.option(j, instance, self.f_opt_gamma),
            self.m_opt_set,
            self.m_double_value_of,
            self.double_class,
        ) else {
            return;
        };
        if let Some(boxed) = j.call_static_obj(cls, value_of, &[jvalue { d: value }]) {
            j.call_void(opt, set, &[jvalue { l: boxed }]);
            j.delete_local(boxed);
        }
    }
}

impl World {
    /// One attribute of an entity, e.g. how far it can reach.
    fn attribute(&self, j: &Jni, entity: jobject, holder: Option<jobject>) -> Option<jobject> {
        let map = j.obj_field(entity, self.f_attributes?)?;
        j.call_obj(map, self.m_attr_instance?, &[jvalue { l: holder? }])
    }

    pub fn attribute_base(&self, j: &Jni, entity: jobject, holder: Option<jobject>) -> Option<f64> {
        let inst = self.attribute(j, entity, holder)?;
        j.call_double(inst, self.m_attr_get_base?, &[])
    }

    pub fn set_attribute_base(
        &self,
        j: &Jni,
        entity: jobject,
        holder: Option<jobject>,
        value: f64,
    ) -> bool {
        let (Some(inst), Some(set)) = (self.attribute(j, entity, holder), self.m_attr_set_base)
        else {
            return false;
        };
        j.call_void(inst, set, &[jvalue { d: value }]);
        true
    }

    pub fn in_water(&self, j: &Jni, entity: jobject) -> bool {
        self.f_touching_water
            .and_then(|f| j.bool_field(entity, f))
            .unwrap_or(false)
    }

    pub fn hitting_wall(&self, j: &Jni, entity: jobject) -> bool {
        self.f_horizontal_collision
            .and_then(|f| j.bool_field(entity, f))
            .unwrap_or(false)
    }

    /// Rain and thunder are plain interpolated floats on the level; zeroing
    /// both (and their previous-tick copies, or it flickers) clears the sky.
    pub fn set_weather(&self, j: &Jni, level: jobject, value: f32) {
        for f in [
            self.f_rain_level,
            self.f_thunder_level,
            self.f_o_rain_level,
            self.f_o_thunder_level,
        ]
        .into_iter()
        .flatten()
        {
            j.set_float(level, f, value);
        }
    }

    // ---- freecam ---------------------------------------------------------

    /// Build a camera to fly around with. It is never added to the level, so
    /// nothing ticks it, nothing renders it, and nothing about it reaches the
    /// server — it exists only for the camera to sit on.
    pub fn new_camera_entity(
        &self,
        j: &Jni,
        level: jobject,
        pos: (f64, f64, f64),
    ) -> Option<jobject> {
        let local = j.new_object(
            self.armor_stand_class?,
            self.m_armor_stand_init?,
            &[
                jvalue { l: level },
                jvalue { d: pos.0 },
                jvalue { d: pos.1 },
                jvalue { d: pos.2 },
            ],
        )?;
        let global = j.global(local);
        j.delete_local(local);
        global
    }

    pub fn set_camera_entity(&self, j: &Jni, instance: jobject, entity: jobject) {
        if let Some(m) = self.m_set_camera_entity {
            j.call_void(instance, m, &[jvalue { l: entity }]);
        }
    }

    /// How far above an entity's feet its eyes sit, so the camera can be placed
    /// by where you want to be looking from.
    pub fn eye_offset(&self, j: &Jni, mc: &Mc, entity: jobject) -> f64 {
        let Some(m) = self.m_get_eye_position else {
            return 0.0;
        };
        let Some(eye) = j.call_obj(entity, m, &[]) else {
            return 0.0;
        };
        let y = j.double_field(eye, mc.f_vy).unwrap_or(0.0);
        j.delete_local(eye);
        let feet = mc.position(j, entity).map(|p| p.1).unwrap_or(y);
        y - feet
    }

    /// What the player is pressing this frame.
    pub fn movement_keys(&self, j: &Jni, player: jobject) -> Keys {
        let mut keys = Keys::default();
        let Some(input) = self.f_client_input.and_then(|f| j.obj_field(player, f)) else {
            return keys;
        };
        let Some(presses) = self.f_key_presses.and_then(|f| j.obj_field(input, f)) else {
            return keys;
        };
        let read = |field: Option<jfieldID>| {
            field.and_then(|f| j.bool_field(presses, f)).unwrap_or(false)
        };
        keys.forward = read(self.f_in_forward);
        keys.backward = read(self.f_in_back);
        keys.left = read(self.f_in_left);
        keys.right = read(self.f_in_right);
        keys.up = read(self.f_in_jump);
        keys.down = read(self.f_in_shift);
        keys
    }

    /// Take the controls away from the body, so it stands still while the
    /// camera flies.
    pub fn clear_movement(&self, j: &Jni, player: jobject) {
        let Some(input) = self.f_client_input.and_then(|f| j.obj_field(player, f)) else {
            return;
        };
        if let (Some(f), Some(empty)) = (self.f_key_presses, self.input_empty) {
            j.set_obj(input, f, empty);
        }
        if let (Some(f), Some(zero)) = (self.f_move_vector, self.vec2_zero) {
            j.set_obj(input, f, zero);
        }
    }

    /// Place an entity with no interpolation smear: the previous-tick and
    /// render-previous copies are moved with it.
    pub fn place(&self, j: &Jni, mc: &Mc, entity: jobject, pos: (f64, f64, f64)) {
        mc.set_pos(j, entity, pos.0, pos.1, pos.2);
        for (name, v) in [("xo", pos.0), ("yo", pos.1), ("zo", pos.2)] {
            let _ = name;
            let _ = v;
        }
        mc.set_old_position(j, entity, pos);
    }

    /// Point an entity, with no interpolation smear.
    pub fn aim(&self, j: &Jni, mc: &Mc, entity: jobject, yaw: f32, pitch: f32) {
        mc.aim_camera(j, entity, yaw, pitch);
    }

    /// Whatever the crosshair is on, if it is an entity.
    pub fn crosshair_entity(&self, j: &Jni, instance: jobject) -> Option<jobject> {
        let hit = j.obj_field(instance, self.f_hit_result?)?;
        if !j.is_instance(hit, self.entity_hit_class?) {
            return None;
        }
        j.call_obj(hit, self.m_hit_get_entity?, &[])
    }
}

/// A static `Holder` constant off the Attributes class, pinned for reuse.
fn holder(j: &Jni, attributes: Option<jclass>, name: &str) -> Option<jobject> {
    let c = attributes?;
    let f = j.static_field(c, name, "Lnet/minecraft/core/Holder;")?;
    let v = j.static_obj_field(c, f)?;
    j.global(v)
}

/// Which way the player is asking to move.
#[derive(Default, Clone, Copy)]
pub struct Keys {
    pub forward: bool,
    pub backward: bool,
    pub left: bool,
    pub right: bool,
    pub up: bool,
    pub down: bool,
}

/// What a scanned entity is, for filtering and colouring.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TargetKind {
    Player,
    Mob,
    Animal,
    Item,
    Other,
}

// ---------------------------------------------------------------------------
// Placing blocks, using items, moving things between slots.
// ---------------------------------------------------------------------------

/// The calls behind Air Place, Auto Build, Auto Totem and Auto Shield.
pub struct Interact {
    vec3_class: jclass,
    m_vec3_init: jmethodID,
    block_pos_class: jclass,
    m_block_pos_init: jmethodID,
    hit_class: jclass,
    m_hit_init: jmethodID,
    /// UP, DOWN, NORTH, SOUTH, WEST, EAST — in that order, matching `FACES`.
    directions: Vec<jobject>,

    m_use_item_on: Option<jmethodID>,
    m_start_use_item: Option<jmethodID>,
    m_container_input: Option<jmethodID>,
    swap_input: Option<jobject>,

    m_get_inventory: Option<jmethodID>,
    m_inv_get_item: Option<jmethodID>,
    m_stack_get_item: Option<jmethodID>,
    m_stack_is_empty: Option<jmethodID>,
    m_get_item_in_hand: Option<jmethodID>,
    main_hand: Option<jobject>,
    off_hand: Option<jobject>,
    totem: Option<jobject>,
    shield: Option<jobject>,
    end_crystal: Option<jobject>,
    mace: Option<jobject>,
    m_set_selected: Option<jmethodID>,
    m_get_selected: Option<jmethodID>,
    /// The end-crystal entity class. It sits here rather than on `World`
    /// because this is already the struct that knows about crystals, and it is
    /// only ever asked alongside the item.
    c_end_crystal: Option<jclass>,

}

/// Offsets for the six faces, in the order `directions` holds them.
pub const FACES: [(i32, i32, i32); 6] = [
    (0, 1, 0),  // UP
    (0, -1, 0), // DOWN
    (0, 0, -1), // NORTH
    (0, 0, 1),  // SOUTH
    (-1, 0, 0), // WEST
    (1, 0, 0),  // EAST
];

impl Interact {
    pub fn resolve(j: &Jni, mc: &Mc, missing: &mut Vec<String>) -> Option<Interact> {
        let vec3_class = class(j, "net/minecraft/world/phys/Vec3")?;
        let block_pos_class = class(j, "net/minecraft/core/BlockPos")?;
        let hit_class = class(j, "net/minecraft/world/phys/BlockHitResult")?;
        let direction_class = class(j, "net/minecraft/core/Direction")?;
        let game_mode = class(j, "net/minecraft/client/multiplayer/MultiPlayerGameMode")?;
        let inventory = class(j, "net/minecraft/world/entity/player/Inventory");
        let stack = class(j, "net/minecraft/world/item/ItemStack");
        let items = class(j, "net/minecraft/world/item/Items");
        let player = class(j, "net/minecraft/world/entity/player/Player");
        let living = class(j, "net/minecraft/world/entity/LivingEntity")?;
        let hand = class(j, "net/minecraft/world/InteractionHand");
        let container_input = class(j, "net/minecraft/world/inventory/ContainerInput");
        let directions: Vec<jobject> = ["UP", "DOWN", "NORTH", "SOUTH", "WEST", "EAST"]
            .iter()
            .filter_map(|name| {
                let f = j.static_field(direction_class, name, "Lnet/minecraft/core/Direction;")?;
                let v = j.static_obj_field(direction_class, f)?;
                j.global(v)
            })
            .collect();
        if directions.len() != 6 {
            missing.push("Direction constants".into());
            return None;
        }

        let global_static = |cls: Option<jclass>, name: &str, sig: &str| -> Option<jobject> {
            let c = cls?;
            let f = j.static_field(c, name, sig)?;
            let v = j.static_obj_field(c, f)?;
            j.global(v)
        };

        Some(Interact {
            m_vec3_init: want!(missing, "Vec3.<init>(DDD)", j.method(vec3_class, "<init>", "(DDD)V"))?,
            m_block_pos_init: want!(
                missing,
                "BlockPos.<init>(III)",
                j.method(block_pos_class, "<init>", "(III)V")
            )?,
            m_hit_init: want!(
                missing,
                "BlockHitResult.<init>",
                j.method(
                    hit_class,
                    "<init>",
                    "(Lnet/minecraft/world/phys/Vec3;Lnet/minecraft/core/Direction;Lnet/minecraft/core/BlockPos;Z)V"
                )
            )?,
            vec3_class,
            block_pos_class,
            hit_class,
            directions,

            m_use_item_on: want!(
                missing,
                "MultiPlayerGameMode.useItemOn",
                j.method(
                    game_mode,
                    "useItemOn",
                    "(Lnet/minecraft/client/player/LocalPlayer;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;"
                )
            ),
            m_start_use_item: want!(
                missing,
                "Minecraft.startUseItem()",
                j.method(mc.minecraft, "startUseItem", "()V")
            ),
            // 26.2 renamed ClickType to ContainerInput.
            m_container_input: want!(
                missing,
                "MultiPlayerGameMode.handleContainerInput",
                j.method(
                    game_mode,
                    "handleContainerInput",
                    "(IIILnet/minecraft/world/inventory/ContainerInput;Lnet/minecraft/world/entity/player/Player;)V"
                )
            ),
            swap_input: global_static(
                container_input,
                "SWAP",
                "Lnet/minecraft/world/inventory/ContainerInput;",
            ),

            m_get_inventory: player.and_then(|c| {
                j.method(
                    c,
                    "getInventory",
                    "()Lnet/minecraft/world/entity/player/Inventory;",
                )
            }),
            m_inv_get_item: inventory
                .and_then(|c| j.method(c, "getItem", "(I)Lnet/minecraft/world/item/ItemStack;")),
            m_stack_get_item: stack
                .and_then(|c| j.method(c, "getItem", "()Lnet/minecraft/world/item/Item;")),
            m_stack_is_empty: stack.and_then(|c| j.method(c, "isEmpty", "()Z")),
            m_get_item_in_hand: j.method(
                living,
                "getItemInHand",
                "(Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/item/ItemStack;",
            ),
            main_hand: global_static(hand, "MAIN_HAND", "Lnet/minecraft/world/InteractionHand;"),
            off_hand: global_static(hand, "OFF_HAND", "Lnet/minecraft/world/InteractionHand;"),
            totem: global_static(items, "TOTEM_OF_UNDYING", "Lnet/minecraft/world/item/Item;"),
            shield: global_static(items, "SHIELD", "Lnet/minecraft/world/item/Item;"),
            end_crystal: global_static(items, "END_CRYSTAL", "Lnet/minecraft/world/item/Item;"),
            mace: global_static(items, "MACE", "Lnet/minecraft/world/item/Item;"),
            m_set_selected: inventory.and_then(|c| j.method(c, "setSelectedSlot", "(I)V")),
            m_get_selected: inventory.and_then(|c| j.method(c, "getSelectedSlot", "()I")),
            c_end_crystal: class(j, "net/minecraft/world/entity/boss/enderdragon/EndCrystal"),

        })
    }

    /// Place whatever is held against `face` of the block at `pos`.
    ///
    /// The game's own path: build the hit result a real click would have
    /// produced and hand it to useItemOn, so the packet, the cooldown and the
    /// swing are all the ones vanilla would send.
    pub fn place_against(
        &self,
        j: &Jni,
        game_mode: jobject,
        player: jobject,
        pos: (i32, i32, i32),
        face: usize,
    ) -> bool {
        let (Some(use_on), Some(hand)) = (self.m_use_item_on, self.main_hand) else {
            return false;
        };
        let (dx, dy, dz) = FACES[face.min(5)];
        // Aim at the middle of the face being clicked.
        let hit = (
            pos.0 as f64 + 0.5 + dx as f64 * 0.5,
            pos.1 as f64 + 0.5 + dy as f64 * 0.5,
            pos.2 as f64 + 0.5 + dz as f64 * 0.5,
        );
        let Some(vec) = j.new_object(
            self.vec3_class,
            self.m_vec3_init,
            &[jvalue { d: hit.0 }, jvalue { d: hit.1 }, jvalue { d: hit.2 }],
        ) else {
            return false;
        };
        let block_pos = j.new_object(
            self.block_pos_class,
            self.m_block_pos_init,
            &[jvalue { i: pos.0 }, jvalue { i: pos.1 }, jvalue { i: pos.2 }],
        );
        let Some(block_pos) = block_pos else {
            j.delete_local(vec);
            return false;
        };
        let result = j.new_object(
            self.hit_class,
            self.m_hit_init,
            &[
                jvalue { l: vec },
                jvalue { l: self.directions[face.min(5)] },
                jvalue { l: block_pos },
                jvalue { z: 0 },
            ],
        );
        j.delete_local(vec);
        j.delete_local(block_pos);
        let Some(result) = result else {
            return false;
        };
        let ok = j
            .call_obj(
                game_mode,
                use_on,
                &[jvalue { l: player }, jvalue { l: hand }, jvalue { l: result }],
            )
            .is_some();
        j.delete_local(result);
        ok
    }

    /// Start using whatever is held — right-click, in effect.
    pub fn start_use(&self, j: &Jni, instance: jobject) {
        if let Some(m) = self.m_start_use_item {
            j.call_void(instance, m, &[]);
        }
    }

    fn stack_is(&self, j: &Jni, stack: jobject, item: Option<jobject>) -> bool {
        let (Some(get), Some(empty), Some(item)) =
            (self.m_stack_get_item, self.m_stack_is_empty, item)
        else {
            return false;
        };
        if j.call_bool(stack, empty, &[]).unwrap_or(true) {
            return false;
        }
        match j.call_obj(stack, get, &[]) {
            Some(held) => {
                let same = j.same_object(held, item);
                j.delete_local(held);
                same
            }
            None => false,
        }
    }

    pub fn holding_totem(&self, j: &Jni, player: jobject) -> bool {
        self.hand_is(j, player, self.off_hand, self.totem)
            || self.hand_is(j, player, self.main_hand, self.totem)
    }

    pub fn holding_shield(&self, j: &Jni, player: jobject) -> bool {
        self.hand_is(j, player, self.off_hand, self.shield)
            || self.hand_is(j, player, self.main_hand, self.shield)
    }

    fn hand_is(&self, j: &Jni, player: jobject, hand: Option<jobject>, item: Option<jobject>) -> bool {
        let (Some(m), Some(hand)) = (self.m_get_item_in_hand, hand) else {
            return false;
        };
        match j.call_obj(player, m, &[jvalue { l: hand }]) {
            Some(stack) => {
                let is = self.stack_is(j, stack, item);
                j.delete_local(stack);
                is
            }
            None => false,
        }
    }

    /// The first inventory slot holding `item`, searching `slots` of it.
    ///
    /// Anything that has to be *held* searches only the nine hotbar slots,
    /// because selecting is all we can do without opening the inventory; the
    /// totem goes to the offhand through a container click, so it may come from
    /// anywhere.
    pub fn find_item(
        &self,
        j: &Jni,
        player: jobject,
        item: Option<jobject>,
        slots: i32,
    ) -> Option<usize> {
        item?;
        let (Some(get_inv), Some(get_item)) = (self.m_get_inventory, self.m_inv_get_item) else {
            return None;
        };
        let inventory = j.call_obj(player, get_inv, &[])?;
        let mut found = None;
        for slot in 0..slots {
            let Some(stack) = j.call_obj(inventory, get_item, &[jvalue { i: slot }]) else {
                continue;
            };
            let is = self.stack_is(j, stack, item);
            j.delete_local(stack);
            if is {
                found = Some(slot as usize);
                break;
            }
        }
        j.delete_local(inventory);
        found
    }

    /// Inventory slot holding a totem, or None.
    pub fn find_totem(&self, j: &Jni, player: jobject) -> Option<usize> {
        self.find_item(j, player, self.totem, 36)
    }

    /// Hotbar slot holding an end crystal, or None.
    pub fn find_crystal(&self, j: &Jni, player: jobject) -> Option<usize> {
        self.find_item(j, player, self.end_crystal, 9)
    }

    /// Hotbar slot holding a mace, or None.
    pub fn find_mace(&self, j: &Jni, player: jobject) -> Option<usize> {
        self.find_item(j, player, self.mace, 9)
    }

    pub fn holding_mace(&self, j: &Jni, player: jobject) -> bool {
        self.hand_is(j, player, self.main_hand, self.mace)
    }

    /// Which hotbar slot is in hand.
    pub fn selected_slot(&self, j: &Jni, player: jobject) -> Option<usize> {
        let inventory = j.call_obj(player, self.m_get_inventory?, &[])?;
        let slot = j.call_int(inventory, self.m_get_selected?, &[]);
        j.delete_local(inventory);
        slot.map(|s| s as usize)
    }

    /// Put a hotbar slot in hand. The client tells the server with its own
    /// held-item packet on the next tick, so this is an ordinary swap and not
    /// something an anti-cheat can distinguish from scrolling.
    pub fn select_slot(&self, j: &Jni, player: jobject, slot: usize) {
        if slot > 8 {
            return;
        }
        let (Some(get_inv), Some(set)) = (self.m_get_inventory, self.m_set_selected) else {
            return;
        };
        let Some(inventory) = j.call_obj(player, get_inv, &[]) else {
            return;
        };
        j.call_void(inventory, set, &[jvalue { i: slot as i32 }]);
        j.delete_local(inventory);
    }

    /// Is this entity an end crystal?
    pub fn is_end_crystal(&self, j: &Jni, entity: jobject) -> bool {
        match self.c_end_crystal {
            Some(c) => j.is_instance(entity, c),
            None => false,
        }
    }

    /// Swap an inventory slot with the offhand, through the game's own path so
    /// the server sees an ordinary inventory action.
    pub fn swap_to_offhand(&self, j: &Jni, game_mode: jobject, player: jobject, slot: usize) {
        let (Some(m), Some(swap)) = (self.m_container_input, self.swap_input) else {
            return;
        };
        // The player's own container numbers the hotbar 36..44 and the rest
        // 9..35; the offhand is button 40.
        let container_slot = if slot < 9 { slot + 36 } else { slot } as i32;
        j.call_void(
            game_mode,
            m,
            &[
                jvalue { i: 0 },
                jvalue { i: container_slot },
                jvalue { i: 40 },
                jvalue { l: swap },
                jvalue { l: player },
            ],
        );
    }

}