Sign in Sign up
kretrod/lodestone Public
Branches
master
1185 lines (1074 loc) · 42.4 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>,
    f_singleplayer: Option<jfieldID>,

    f_abilities: jfieldID,
    f_position: jfieldID,
    f_x_rot: jfieldID,
    f_y_rot: jfieldID,
    f_y_head_rot: Option<jfieldID>,
    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 = match class(j, "net/minecraft/client/MouseHandler") {
            Some(c) => want!(
                missing,
                "MouseHandler.mouseGrabbed",
                j.field(c, "mouseGrabbed", "Z")
            ),
            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_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,
            f_singleplayer,
            f_abilities,
            f_position,
            f_x_rot,
            f_y_rot,
            f_y_head_rot,
            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)?,
        ))
    }

    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)?,
        ))
    }

    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);
        }
    }

    /// 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)
}

// ---------------------------------------------------------------------------
// 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,

    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_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>,

    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>,
    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>,

    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");

        Some(World {
            player_class,
            monster_class: class(j, "net/minecraft/world/entity/monster/Monster"),
            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_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_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;")),
            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"),

            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
    }

    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 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);
        }
    }

    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);
        }
    }

    /// 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)
}

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