Sign in Sign up
kretrod/lodestone Public
Branches
master
1068 lines (992 loc) · 37.6 KB Raw
//! Applying the switches, once per frame, through JNI.
//!
//! Every module here changes only the client's own state — the kind of thing a
//! client can do anywhere, whether or not it owns the server. Nothing reaches
//! for the integrated server: no editing world time, no server-side health, no
//! teleports the server is told to accept, because none of that exists to a
//! client connected to someone else's world.
//!
//! A server can of course disagree with what we do, and for flight or noclip it
//! will. Where a module has variants, they differ in exactly that: how far the
//! position stream we produce is from something a vanilla client could have
//! sent.

use std::time::Instant;

use jni_sys::jobject;

use crate::jni::Jni;
use crate::mc::{Mc, TargetKind, World};
use crate::state::{AimMode, AuraTarget, Config, FlyMode, GameState, SpeedMode, Target};

/// Values a module replaced, so switching it off restores the game's own.
pub struct Saved {
    may_fly: Option<bool>,
    fly_speed: Option<f32>,
    walk_speed: Option<f32>,
    no_physics: Option<bool>,
    prev_on_ground: bool,
    prev_hurt_time: i32,
    prev_tick: i32,
    last_frame: Instant,
    /// Criticals wait for the fall: once we have hopped, hold the swing until
    /// the player is actually descending.
    crit_jumped: bool,
    died_at: Option<Instant>,
    last_step: Instant,
    last_dip: Instant,
    blink_origin: Option<(f64, f64, f64)>,
    last_swing: Instant,
    last_aura: Instant,
    last_trigger: Instant,
    freecam_origin: Option<(f64, f64, f64)>,
    /// The detached camera: a global reference, so it survives between frames.
    camera: Option<jobject>,
    cam_pos: (f64, f64, f64),
    cam_yaw: f32,
    cam_pitch: f32,
    /// Where the body was pointing when the camera detached, held there so the
    /// mouse turns the camera instead.
    body_yaw: f32,
    body_pitch: f32,
    cam_eye_offset: f64,
    entity_reach: Option<f64>,
    block_reach: Option<f64>,
    step_height: Option<f64>,
    gamma: Option<f64>,
    smart_cull: Option<bool>,
    view_distance: Option<i32>,
    view_distance_sent: i32,
    fov: Option<i32>,
    bob_view: Option<bool>,
}

impl Default for Saved {
    fn default() -> Self {
        Self {
            may_fly: None,
            fly_speed: None,
            walk_speed: None,
            no_physics: None,
            prev_on_ground: false,
            prev_hurt_time: 0,
            prev_tick: 0,
            last_frame: Instant::now(),
            crit_jumped: false,
            died_at: None,
            last_step: Instant::now(),
            last_dip: Instant::now(),
            blink_origin: None,
            last_swing: Instant::now(),
            last_aura: Instant::now(),
            last_trigger: Instant::now(),
            freecam_origin: None,
            camera: None,
            cam_pos: (0.0, 0.0, 0.0),
            cam_yaw: 0.0,
            cam_pitch: 0.0,
            body_yaw: 0.0,
            body_pitch: 0.0,
            cam_eye_offset: 1.62,
            entity_reach: None,
            block_reach: None,
            step_height: None,
            gamma: None,
            smart_cull: None,
            view_distance: None,
            view_distance_sent: 0,
            fov: None,
            bob_view: None,
        }
    }
}

pub fn apply(
    j: &Jni,
    mc: &Mc,
    world: Option<&World>,
    cfg: &Config,
    saved: &mut Saved,
) -> GameState {
    let mut st = GameState::default();

    let Some(instance) = mc.instance(j) else {
        return st;
    };
    st.screen_open = mc.screen_open(j, instance);
    st.single_player = mc.single_player(j, instance);

    let Some(player) = mc.player(j, instance) else {
        return st;
    };
    st.in_world = true;

    // ---- read ------------------------------------------------------------
    if let Some(p) = mc.position(j, player) {
        st.pos = p;
        st.eye = (p.0, p.1 + 1.62, p.2);
    }
    let (yaw, pitch) = mc.rotation(j, player);
    st.yaw = yaw;
    st.pitch = pitch;
    st.on_ground = mc.on_ground(j, player);
    st.health = mc.health(j, player).unwrap_or(0.0);
    st.sprinting = mc.is_sprinting(j, player);
    let hurt_time = mc.hurt_time(j, player);
    let abilities = mc.abilities(j, player);

    // The game's physics runs per tick; our hook runs per frame. Anything that
    // adds to velocity has to act once a tick or it scales with the frame rate.
    let tick = mc.tick_count(j, player);
    let new_tick = tick != saved.prev_tick;
    saved.prev_tick = tick;
    let dt = saved.last_frame.elapsed().as_secs_f32().clamp(0.0, 0.1);
    saved.last_frame = Instant::now();

    // ---- auto respawn ----------------------------------------------------
    // A short wait, so the death screen is actually up before we answer it.
    if st.health <= 0.0 {
        let since = *saved.died_at.get_or_insert_with(Instant::now);
        if cfg.misc.auto_respawn && since.elapsed().as_secs_f32() > 0.5 {
            mc.respawn(j, player);
            saved.died_at = None;
        }
    } else {
        saved.died_at = None;
    }

    let m = &cfg.movement;

    // ---- flight ----------------------------------------------------------
    let creative_fly = m.fly && m.fly_mode == FlyMode::Creative;
    if let Some(ab) = abilities {
        if creative_fly {
            if saved.may_fly.is_none() {
                saved.may_fly = Some(mc.may_fly(j, ab));
            }
            if !mc.may_fly(j, ab) {
                mc.set_may_fly(j, ab, true);
            }
            if !mc.flying(j, ab) {
                mc.set_flying(j, ab, true);
            }
            if saved.fly_speed.is_none() {
                saved.fly_speed = Some(mc.fly_speed(j, ab));
            }
            if (mc.fly_speed(j, ab) - m.fly_speed).abs() > f32::EPSILON {
                mc.set_fly_speed(j, ab, m.fly_speed);
            }
        } else {
            if let Some(prev) = saved.may_fly.take() {
                mc.set_flying(j, ab, false);
                mc.set_may_fly(j, ab, prev);
            }
            if let Some(prev) = saved.fly_speed.take() {
                mc.set_fly_speed(j, ab, prev);
            }
        }

        // ---- speed -------------------------------------------------------
        if m.speed && m.speed_mode == SpeedMode::Abilities {
            if saved.walk_speed.is_none() {
                saved.walk_speed = Some(mc.walk_speed(j, ab));
            }
            if (mc.walk_speed(j, ab) - m.speed_value).abs() > f32::EPSILON {
                mc.set_walk_speed(j, ab, m.speed_value);
            }
        } else if let Some(prev) = saved.walk_speed.take() {
            mc.set_walk_speed(j, ab, prev);
        }
    }

    if m.fly && !st.screen_open {
        // A vanilla server flags you as "floating" on any tick where your
        // vertical delta is >= -0.03125 and you are not allowed to fly; eighty
        // of those in a row is a disconnect. Descending properly, even for a
        // single tick, puts the counter back to zero — so dip on a timer and
        // the kick never arrives.
        let dipping = m.fly_anti_kick
            && new_tick
            && saved.last_dip.elapsed().as_secs_f32() >= m.fly_dip_interval.max(0.2);

        if dipping {
            saved.last_dip = Instant::now();
            if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
                mc.set_delta_movement(j, player, dx, -0.05, dz);
            }
        } else {
            match m.fly_mode {
                FlyMode::Creative => {}
                FlyMode::Glide => {
                    // Cancel gravity only: horizontal control stays vanilla,
                    // and the position stream keeps looking like walking.
                    if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
                        mc.set_delta_movement(j, player, dx, 0.0, dz);
                    }
                }
                FlyMode::Motion => {
                    // Velocity only, no ability flag. Accelerate rather than
                    // snap, so every step stays a plausible size.
                    if let Some((dx, dy, dz)) = mc.delta_movement(j, player) {
                        let speed = limit_speed(m, m.fly_speed as f64);
                        let (sy, cy) = ((yaw.to_radians()) as f64).sin_cos();
                        let want_x = -sy * speed;
                        let want_z = cy * speed;
                        let blend = 0.35;
                        mc.set_delta_movement(
                            j,
                            player,
                            dx + (want_x - dx) * blend,
                            dy * 0.6,
                            dz + (want_z - dz) * blend,
                        );
                    }
                }
                FlyMode::Teleport => {
                    if saved.last_step.elapsed().as_secs_f32() >= m.fly_step_interval {
                        saved.last_step = Instant::now();
                        // The server re-simulates the move you claim with
                        // collision; a step that passes through a block ends up
                        // somewhere else on its side and you get pulled back.
                        // Small steps through open air are what survive.
                        let step = limit_step(m, m.fly_step as f64);
                        let (sy, cy) = ((yaw.to_radians()) as f64).sin_cos();
                        let (sp, cp) = ((pitch.to_radians()) as f64).sin_cos();
                        let (x, y, z) = st.pos;
                        mc.set_pos(
                            j,
                            player,
                            x - sy * cp * step,
                            y - sp * step,
                            z + cy * cp * step,
                        );
                    }
                }
            }
        }
    }

    // ---- bhop ------------------------------------------------------------
    // Vanilla gives sprint-jumping a real speed bonus, and it is movement the
    // server expects to see, so this is quick without being a lie.
    if m.bhop && !st.screen_open && new_tick && st.on_ground && st.sprinting {
        mc.set_jumping(j, player, true);
    }

    // ---- blink -----------------------------------------------------------
    // Hold the position updates back entirely: you keep moving, the server
    // keeps seeing you where you stopped.
    if m.blink && !m.freecam {
        if saved.blink_origin.is_none() {
            saved.blink_origin = Some(st.pos);
        }
        if let Some(origin) = saved.blink_origin {
            mc.freeze_sent_position(j, player, origin);
        }
    } else if !m.freecam {
        saved.blink_origin = None;
    }

    // ---- velocity speed --------------------------------------------------
    // Set an absolute speed rather than scaling what is already there: scaling
    // every frame compounds into a slingshot.
    if m.speed && m.speed_mode == SpeedMode::Velocity && !st.screen_open && new_tick {
        if let Some((dx, dy, dz)) = mc.delta_movement(j, player) {
            let horizontal = (dx * dx + dz * dz).sqrt();
            // Only steer while you are already moving, so standing still does
            // not slide you across the ground.
            if horizontal > 0.02 {
                let scale = m.speed_value as f64 / horizontal;
                mc.set_delta_movement(j, player, dx * scale, dy, dz * scale);
            }
        }
    }

    // ---- jetpack ---------------------------------------------------------
    if m.jetpack && mc.jumping(j, player) && !st.screen_open && new_tick {
        if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
            mc.set_delta_movement(j, player, dx, m.jetpack_power as f64, dz);
        }
    }

    // ---- auto sprint -----------------------------------------------------
    // Held every frame: the game clears the flag whenever you stop, so a
    // one-shot write would last a single tick.
    if m.sprint && !st.screen_open && !st.sprinting {
        mc.set_sprinting(j, player, true);
    }

    // ---- noclip ----------------------------------------------------------
    if m.noclip || m.freecam {
        if saved.no_physics.is_none() {
            saved.no_physics = Some(false);
        }
        mc.set_no_physics(j, player, true);
    } else if let Some(prev) = saved.no_physics.take() {
        mc.set_no_physics(j, player, prev);
    }



    // ---- jesus -----------------------------------------------------------
    // Buoyancy pulls you under; holding a small upward velocity while touching
    // water keeps you on the surface instead.
    if m.jesus && !st.screen_open && new_tick
        && world.map(|w| w.in_water(j, player)).unwrap_or(false)
    {
        if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
            mc.set_delta_movement(j, player, dx, 0.08, dz);
        }
    }

    // ---- spider ----------------------------------------------------------
    // Walking into a wall normally stops you flat; a steady climb turns it
    // into a ladder.
    if m.spider && !st.screen_open && new_tick
        && world.map(|w| w.hitting_wall(j, player)).unwrap_or(false)
    {
        if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
            mc.set_delta_movement(j, player, dx, m.spider_power as f64, dz);
        }
    }

    // ---- jump power ------------------------------------------------------
    // The frame you leave the ground is the one carrying the jump impulse.
    if m.jump_power && saved.prev_on_ground && !st.on_ground {
        if let Some((dx, dy, dz)) = mc.delta_movement(j, player) {
            if dy > 0.05 {
                mc.set_delta_movement(j, player, dx, dy * m.jump_multiplier as f64, dz);
            }
        }
    }

    // ---- no fall ---------------------------------------------------------
    if m.no_fall {
        if mc.fall_distance(j, player) > 0.0 {
            mc.set_fall_distance(j, player, 0.0);
        }
    }

    // ---- anti knockback --------------------------------------------------
    // A hit shows up as hurtTime jumping to its maximum; that same tick the
    // server's velocity has already been applied, so scale it back down.
    let c = &cfg.combat;
    if c.anti_knockback && hurt_time > saved.prev_hurt_time {
        if let Some((dx, dy, dz)) = mc.delta_movement(j, player) {
            mc.set_delta_movement(
                j,
                player,
                dx * c.kb_horizontal as f64,
                dy * c.kb_vertical as f64,
                dz * c.kb_horizontal as f64,
            );
        }
    }

    // ---- no hurt camera --------------------------------------------------
    // After the knockback check, which needs the real value.
    if cfg.visuals.no_hurt_cam && hurt_time > 0 {
        mc.set_hurt_time(j, player, 0);
    }

    // ---- auto clicker ----------------------------------------------------
    if c.auto_clicker && !st.screen_open {
        let interval = 1.0 / c.click_cps.max(0.5);
        // Jitter the gap so the rhythm is not machine-perfect.
        let jitter = 1.0 + c.click_jitter * pseudo_random(saved.last_swing);
        if saved.last_swing.elapsed().as_secs_f32() >= interval * jitter {
            saved.last_swing = Instant::now();
            mc.start_attack(j, instance);
        }
    }

    // ---- everything that needs the world ---------------------------------
    if let Some(w) = world {
        freecam(j, mc, w, instance, player, cfg, saved, &mut st, dt);
        // The camera, not the player: in third person they are not the same
        // place, and ESP has to project from where the view actually is.
        if let Some((pos, yaw, pitch)) = w.camera(j, mc, instance) {
            st.camera = pos;
            st.camera_yaw = yaw;
            st.camera_pitch = pitch;
        } else {
            st.camera = st.eye;
            st.camera_yaw = st.yaw;
            st.camera_pitch = st.pitch;
        }
        st.fov = w.fov(j, instance).unwrap_or(70.0);
        if let Some(level) = mc.level(j, instance) {
            st.loaded_chunks = w.loaded_chunks(j, level);
        }

        attributes(j, w, player, cfg, saved);
        visuals(j, mc, w, instance, cfg, saved);

        let aura = &cfg.combat;
        let want_scan = aura.kill_aura || aura.aimbot || esp_wanted(cfg);
        if want_scan {
            let best = scan(j, mc, w, instance, player, cfg, &mut st);
            combat(j, mc, w, instance, player, cfg, saved, &mut st, best, dt);
        }
        trigger_bot(j, mc, w, instance, player, cfg, saved);
    }

    saved.prev_on_ground = st.on_ground;
    saved.prev_hurt_time = hurt_time;
    st
}

fn esp_wanted(cfg: &Config) -> bool {
    let e = &cfg.esp;
    e.players || e.mobs || e.animals || e.items || e.containers
}

/// Walk the level's entities once, filling in ESP targets and picking the best
/// thing to hit. Returns that candidate, still a live reference for this frame.
fn scan(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    st: &mut GameState,
) -> Option<jobject> {
    let Some(level) = mc.level(j, instance) else {
        return None;
    };
    let Some(iterator) = w.entity_iterator(j, level) else {
        return None;
    };

    let eye = st.eye;
    let esp_range = cfg.esp.distance as f64;
    let aura_range = cfg.combat.aura_range as f64;
    let mut best: Option<jobject> = None;
    let mut best_score = f64::MAX;

    // A hard cap: a busy world can hold thousands of entities and this runs
    // every frame.
    for _ in 0..4096 {
        let Some(entity) = w.iter_next(j, iterator) else {
            break;
        };
        if j.same_object(entity, player) || !w.is_alive(j, entity) {
            j.delete_local(entity);
            continue;
        }
        let kind = w.classify(j, entity);
        let Some((min, max)) = w.bounding_box(j, entity) else {
            j.delete_local(entity);
            continue;
        };
        let centre = (
            (min.0 + max.0) / 2.0,
            (min.1 + max.1) / 2.0,
            (min.2 + max.2) / 2.0,
        );
        let distance = distance_between(eye, centre);

        if aura_wanted(cfg, kind) && distance <= aura_range {
            // Walls: the game already knows how to answer this, so ask it
            // rather than casting our own ray.
            let visible = cfg.combat.aura_through_walls
                || mc.has_line_of_sight(j, player, entity);
            if visible && attackable(j, w, player, entity) {
                let score = match cfg.combat.aura_target {
                    AuraTarget::Nearest => distance,
                    AuraTarget::Weakest => {
                        if w.is_living(j, entity) {
                            mc.health(j, entity).unwrap_or(f32::MAX) as f64
                        } else {
                            f64::MAX
                        }
                    }
                    // Smallest turn from where you are already looking.
                    AuraTarget::Angle => {
                        let (yaw, _) = look_at(eye, centre);
                        angle_delta(st.yaw, yaw).abs() as f64
                    }
                };
                if score < best_score {
                    if let Some(previous) = best.replace(entity) {
                        j.delete_local(previous);
                    }
                    best_score = score;
                    // Kept alive for the attack below; do not delete it here.
                }
            }
        }

        if esp_shows(cfg, kind) && distance <= esp_range {
            let living = w.is_living(j, entity);
            st.targets.push(Target {
                min,
                max,
                health: if living { mc.health(j, entity).unwrap_or(0.0) } else { 0.0 },
                max_health: if living { w.max_health(j, entity) } else { 0.0 },
                distance: distance as f32,
                kind,
                name: if cfg.esp.nametags {
                    w.name(j, entity).unwrap_or_default()
                } else {
                    String::new()
                },
            });
        }

        if best.map(|b| !j.same_object(b, entity)).unwrap_or(true) {
            j.delete_local(entity);
        }
    }
    j.delete_local(iterator);
    best
}

fn aura_wanted(cfg: &Config, kind: TargetKind) -> bool {
    let c = &cfg.combat;
    if !c.kill_aura && !c.aimbot {
        return false;
    }
    match kind {
        TargetKind::Player => c.aura_players,
        TargetKind::Mob => c.aura_mobs,
        TargetKind::Animal => c.aura_animals,
        _ => false,
    }
}

fn esp_shows(cfg: &Config, kind: TargetKind) -> bool {
    let e = &cfg.esp;
    match kind {
        TargetKind::Player => e.players,
        TargetKind::Mob => e.mobs,
        TargetKind::Animal => e.animals,
        TargetKind::Item => e.items,
        TargetKind::Other => false,
    }
}

fn distance_between(a: (f64, f64, f64), b: (f64, f64, f64)) -> f64 {
    let (dx, dy, dz) = (b.0 - a.0, b.1 - a.1, b.2 - a.2);
    (dx * dx + dy * dy + dz * dz).sqrt()
}

/// Yaw and pitch that point from `from` at `to`, in Minecraft's convention.
fn look_at(from: (f64, f64, f64), to: (f64, f64, f64)) -> (f32, f32) {
    let (dx, dy, dz) = (to.0 - from.0, to.1 - from.1, to.2 - from.2);
    let horizontal = (dx * dx + dz * dz).sqrt();
    let yaw = dz.atan2(dx).to_degrees() - 90.0;
    let pitch = -dy.atan2(horizontal).to_degrees();
    (yaw as f32, pitch as f32)
}

/// Shortest signed way round from one angle to another.
fn angle_delta(from: f32, to: f32) -> f32 {
    let mut d = (to - from) % 360.0;
    if d > 180.0 {
        d -= 360.0;
    }
    if d < -180.0 {
        d += 360.0;
    }
    d
}

#[allow(clippy::too_many_arguments)]
fn combat(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    saved: &mut Saved,
    st: &mut GameState,
    best: Option<jobject>,
    dt: f32,
) {
    let c = &cfg.combat;
    let Some(target) = best else {
        saved.crit_jumped = false;
        return;
    };
    let Some((min, max)) = w.bounding_box(j, target) else {
        j.delete_local(target);
        return;
    };
    // Aim at the middle of the body rather than the feet or the hat.
    let centre = (
        (min.0 + max.0) / 2.0,
        (min.1 + max.1) / 2.0,
        (min.2 + max.2) / 2.0,
    );
    let (want_yaw, want_pitch) = look_at(st.eye, centre);

    // ---- aimbot ----------------------------------------------------------
    if c.aimbot && angle_delta(st.yaw, want_yaw).abs() <= c.aim_fov / 2.0 {
        // Time-based, so the turn takes the same wall-clock time at 30 fps as
        // at 300. A camera that teleports onto a target is the single most
        // obvious thing a cheat can do.
        let blend = (c.aim_speed * dt * 20.0).clamp(0.02, 1.0);
        let yaw = st.yaw + angle_delta(st.yaw, want_yaw) * blend;
        let pitch = st.pitch + (want_pitch - st.pitch) * blend;
        if c.aim_mode == AimMode::Camera {
            mc.set_rotation(j, player, yaw, pitch);
            st.yaw = yaw;
            st.pitch = pitch;
        }
        // Silent aim is applied at the swing below and put straight back.
    }

    // ---- kill aura -------------------------------------------------------
    if c.kill_aura {
        // Modern Minecraft scales damage by how far the attack cooldown has
        // recharged: swinging at 20 cps lands twenty hits for a fraction of
        // the damage of one. Waiting for a full bar is both stronger and far
        // less conspicuous than spamming.
        let ready = if c.aura_cooldown {
            mc.attack_strength(j, player) >= 0.95
        } else {
            let interval = 1.0 / c.aura_cps.max(0.5);
            saved.last_aura.elapsed().as_secs_f32() >= interval
        };

        if ready {
            // Criticals: a hit only counts while you are falling, so hop and
            // hold the swing until the descent starts.
            let mut may_swing = true;
            if c.criticals {
                let dy = mc.delta_movement(j, player).map(|d| d.1).unwrap_or(0.0);
                if st.on_ground {
                    if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
                        mc.set_delta_movement(j, player, dx, 0.42, dz);
                    }
                    saved.crit_jumped = true;
                    may_swing = false;
                } else if saved.crit_jumped && dy >= 0.0 {
                    // Still on the way up.
                    may_swing = false;
                } else {
                    saved.crit_jumped = false;
                }
            }

            if may_swing {
                let restore = if c.aura_rotate
                    || (c.aimbot && c.aim_mode == AimMode::Silent)
                {
                    let previous = (st.yaw, st.pitch);
                    mc.set_rotation(j, player, want_yaw, want_pitch);
                    Some(previous)
                } else {
                    None
                };
                if let Some(game_mode) = mc.game_mode(j, instance) {
                    w.attack(j, game_mode, player, target);
                }
                if let Some((yaw, pitch)) = restore {
                    if c.aim_mode == AimMode::Silent && !c.aura_rotate {
                        mc.set_rotation(j, player, yaw, pitch);
                    }
                }
                saved.last_aura = Instant::now();
            }
        }
    }
    j.delete_local(target);
}

/// Whether the server will accept an attack on this entity at all.
///
/// Vanilla's handleInteract disconnects the client for attacking an ItemEntity,
/// an ExperienceOrb, itself, or a non-attackable arrow. Every one of those is
/// excluded by the entity simply being a LivingEntity, so that is the test.
fn attackable(j: &Jni, w: &World, player: jobject, target: jobject) -> bool {
    !j.same_object(target, player) && w.is_living(j, target)
}

/// Swing when the crosshair is already on something — no aiming, no target
/// selection, which is why it is the least conspicuous of the three.
fn trigger_bot(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    saved: &mut Saved,
) {
    let c = &cfg.combat;
    if !c.trigger_bot {
        return;
    }
    if saved.last_trigger.elapsed().as_secs_f32() < c.trigger_delay.max(0.05) {
        return;
    }
    let Some(target) = w.crosshair_entity(j, instance) else {
        return;
    };
    // The server disconnects you outright for attacking a dropped item, an
    // experience orb or yourself — handleInteract treats those as a protocol
    // violation, not a miss. The crosshair lands on them all the time, so this
    // filter is not optional.
    if !attackable(j, w, player, target) {
        j.delete_local(target);
        return;
    }
    saved.last_trigger = Instant::now();
    if let Some(game_mode) = mc.game_mode(j, instance) {
        w.attack(j, game_mode, player, target);
    }
    j.delete_local(target);
}

/// Reach and step height are attributes, so they are set once and restored
/// when the module goes off.
fn attributes(j: &Jni, w: &World, player: jobject, cfg: &Config, saved: &mut Saved) {
    let c = &cfg.combat;
    if c.reach {
        if saved.entity_reach.is_none() {
            saved.entity_reach = w.attribute_base(j, player, w.holder_entity_reach);
            saved.block_reach = w.attribute_base(j, player, w.holder_block_reach);
        }
        let want = c.reach_distance as f64;
        w.set_attribute_base(j, player, w.holder_entity_reach, want);
        w.set_attribute_base(j, player, w.holder_block_reach, want);
    } else {
        if let Some(v) = saved.entity_reach.take() {
            w.set_attribute_base(j, player, w.holder_entity_reach, v);
        }
        if let Some(v) = saved.block_reach.take() {
            w.set_attribute_base(j, player, w.holder_block_reach, v);
        }
    }

    let m = &cfg.movement;
    if m.step {
        if saved.step_height.is_none() {
            saved.step_height = w.attribute_base(j, player, w.holder_step_height);
        }
        w.set_attribute_base(j, player, w.holder_step_height, m.step_height as f64);
    } else if let Some(v) = saved.step_height.take() {
        w.set_attribute_base(j, player, w.holder_step_height, v);
    }
}

fn visuals(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    cfg: &Config,
    saved: &mut Saved,
) {
    let v = &cfg.visuals;
    if v.no_weather {
        if let Some(level) = mc.level(j, instance) {
            w.set_weather(j, level, 0.0);
        }
    }

    // Occlusion culling: with it off the renderer stops skipping sections it
    // believes are hidden, so terrain you are inside of draws instead of
    // reading as a black wall. It cannot conjure chunks the server never sent —
    // only stop hiding the ones you already have.
    if v.no_culling {
        if saved.smart_cull.is_none() {
            saved.smart_cull = Some(mc.smart_cull(j, instance));
        }
        mc.set_smart_cull(j, instance, false);
    } else if let Some(prev) = saved.smart_cull.take() {
        mc.set_smart_cull(j, instance, prev);
    }

    // Asking for a wider view. Only acted on when the number changes: the
    // request is a packet, and one per frame would be a flood.
    if v.view_distance {
        let want = v.view_distance_chunks as i32;
        if saved.view_distance.is_none() {
            saved.view_distance = w.render_distance(j, instance);
        }
        if saved.view_distance_sent != want {
            saved.view_distance_sent = want;
            w.request_view_distance(j, instance, want);
            if let Some(level) = mc.level(j, instance) {
                w.update_view_radius(j, level, want);
            }
        }
    } else if let Some(prev) = saved.view_distance.take() {
        saved.view_distance_sent = 0;
        w.request_view_distance(j, instance, prev);
    }

    if v.no_bob {
        if saved.bob_view.is_none() {
            saved.bob_view = w.bob_view(j, instance);
        }
        w.set_bob_view(j, instance, false);
    } else if let Some(b) = saved.bob_view.take() {
        w.set_bob_view(j, instance, b);
    }

    if v.fullbright {
        if saved.gamma.is_none() {
            saved.gamma = w.gamma(j, instance);
        }
        // The option is clamped in the UI but not on the way in.
        w.set_gamma(j, instance, 15.0);
    } else if let Some(g) = saved.gamma.take() {
        w.set_gamma(j, instance, g);
    }

    if v.fov {
        if saved.fov.is_none() {
            saved.fov = w.fov(j, instance).map(|f| f as i32);
        }
        w.set_fov(j, instance, v.fov_value as i32);
    } else if let Some(f) = saved.fov.take() {
        w.set_fov(j, instance, f);
    }
}

/// Client-side spectator.
///
/// Nothing here moves the player. The camera is a separate entity that is never
/// added to the level — nothing ticks it, nothing renders it, nothing about it
/// is sent anywhere — and `Minecraft.setCameraEntity` points the view at it.
/// Your body stands exactly where it was, doing exactly what the server expects
/// of someone standing still, which is why there is nothing to correct.
///
/// The mouse still turns the *player*, so each frame the turn it applied is
/// taken off the body and added to the camera instead.
#[allow(clippy::too_many_arguments)]
fn freecam(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    saved: &mut Saved,
    st: &mut GameState,
    dt: f32,
) {
    let m = &cfg.movement;

    if !m.freecam {
        if let Some(camera) = saved.camera.take() {
            w.set_camera_entity(j, instance, player);
            j.delete_global(camera);
            if let Some(origin) = saved.freecam_origin.take() {
                mc.set_pos(j, player, origin.0, origin.1, origin.2);
                mc.set_old_position(j, player, origin);
            }
        }
        return;
    }

    // ---- attach ----------------------------------------------------------
    if saved.camera.is_none() {
        let Some(level) = mc.level(j, instance) else {
            return;
        };
        saved.freecam_origin = Some(st.pos);
        saved.cam_pos = st.eye;
        let Some(camera) = w.new_camera_entity(j, level, st.pos) else {
            return;
        };
        // An armour stand's eyes are not at its feet; place it so the eyes land
        // where we want to be looking from.
        saved.cam_eye_offset = w.eye_offset(j, mc, camera).max(0.1);
        mc.set_no_physics(j, camera, true);
        w.set_camera_entity(j, instance, camera);
        saved.camera = Some(camera);
    }
    let (Some(camera), Some(origin)) = (saved.camera, saved.freecam_origin) else {
        return;
    };

    // ---- keep the body exactly where it was ------------------------------
    //
    // The mouse and the keys still drive the player — trying to hold its
    // rotation still fought the game for it and wound up spinning. Far simpler
    // to let the player turn freely and just pin its *position* every frame,
    // then make sure none of it is ever sent.
    mc.set_pos(j, player, origin.0, origin.1, origin.2);
    mc.set_old_position(j, player, origin);
    mc.set_delta_movement(j, player, 0.0, 0.0, 0.0);
    mc.set_fall_distance(j, player, 0.0);
    mc.freeze_sent_position(j, player, origin);
    mc.freeze_sent_rotation(j, player, st.yaw, st.pitch);

    // ---- the camera follows your view, and flies on your keys ------------
    let yaw = st.yaw;
    let pitch = st.pitch;
    let keys = w.movement_keys(j, player);

    let (sy, cy) = (yaw as f64).to_radians().sin_cos();
    let (sp, cp) = (pitch as f64).to_radians().sin_cos();
    let forward = (-sy * cp, -sp, cy * cp);
    let right = (-forward.2, 0.0, forward.0);
    let right_len = (right.0 * right.0 + right.2 * right.2).sqrt().max(1e-9);
    let right = (right.0 / right_len, 0.0, right.2 / right_len);

    let mut step = (0.0f64, 0.0f64, 0.0f64);
    let mut add = |v: (f64, f64, f64), sign: f64| {
        step.0 += v.0 * sign;
        step.1 += v.1 * sign;
        step.2 += v.2 * sign;
    };
    if keys.forward {
        add(forward, 1.0);
    }
    if keys.backward {
        add(forward, -1.0);
    }
    if keys.right {
        add(right, 1.0);
    }
    if keys.left {
        add(right, -1.0);
    }
    if keys.up {
        add((0.0, 1.0, 0.0), 1.0);
    }
    if keys.down {
        add((0.0, 1.0, 0.0), -1.0);
    }

    let length = (step.0 * step.0 + step.1 * step.1 + step.2 * step.2).sqrt();
    if length > 1e-6 {
        // Time-based, so the camera moves at one speed whatever the frame rate.
        let distance = m.freecam_speed as f64 * (dt as f64) * 20.0;
        saved.cam_pos.0 += step.0 / length * distance;
        saved.cam_pos.1 += step.1 / length * distance;
        saved.cam_pos.2 += step.2 / length * distance;
    }

    w.place(
        j,
        mc,
        camera,
        (
            saved.cam_pos.0,
            saved.cam_pos.1 - saved.cam_eye_offset,
            saved.cam_pos.2,
        ),
    );
    w.aim(j, mc, camera, yaw, pitch);

    // The overlay projects from here, so ESP keeps working while detached.
    st.camera = saved.cam_pos;
    st.camera_yaw = yaw;
    st.camera_pitch = pitch;
}

/// Hold velocity inside what a server accepts without correcting you. Vanilla
/// only complains past roughly ten blocks in a tick, but staying well under
/// that is also what keeps you from outrunning chunk loading.
fn limit_speed(m: &crate::state::Movement, want: f64) -> f64 {
    if m.speed_limit {
        want.min(0.55)
    } else {
        want
    }
}

fn limit_step(m: &crate::state::Movement, want: f64) -> f64 {
    if m.speed_limit {
        want.min(2.0)
    } else {
        want
    }
}

/// Cheap deterministic jitter in [-0.5, 0.5] — enough to break up a perfectly
/// even click interval without pulling in a random number generator.
fn pseudo_random(seed: Instant) -> f32 {
    let n = seed.elapsed().subsec_nanos();
    ((n.wrapping_mul(2654435761) >> 8) as f32 / u32::MAX as f32) - 0.5
}

/// Put everything back — used when the client unloads.
pub fn restore(j: &Jni, mc: &Mc, world: Option<&World>, saved: &mut Saved) {
    let Some(instance) = mc.instance(j) else {
        return;
    };
    let Some(player) = mc.player(j, instance) else {
        return;
    };
    if let Some(ab) = mc.abilities(j, player) {
        if let Some(prev) = saved.may_fly.take() {
            mc.set_flying(j, ab, false);
            mc.set_may_fly(j, ab, prev);
        }
        if let Some(prev) = saved.fly_speed.take() {
            mc.set_fly_speed(j, ab, prev);
        }
        if let Some(prev) = saved.walk_speed.take() {
            mc.set_walk_speed(j, ab, prev);
        }
    }
    if let Some(prev) = saved.no_physics.take() {
        mc.set_no_physics(j, player, prev);
    }
    if let Some(w) = world {
        if let Some(camera) = saved.camera.take() {
            if let Some(instance) = mc.instance(j) {
                w.set_camera_entity(j, instance, player);
            }
            j.delete_global(camera);
            if let Some(origin) = saved.freecam_origin.take() {
                mc.set_pos(j, player, origin.0, origin.1, origin.2);
            }
        }
        if let Some(v) = saved.entity_reach.take() {
            w.set_attribute_base(j, player, w.holder_entity_reach, v);
        }
        if let Some(v) = saved.block_reach.take() {
            w.set_attribute_base(j, player, w.holder_block_reach, v);
        }
        if let Some(v) = saved.step_height.take() {
            w.set_attribute_base(j, player, w.holder_step_height, v);
        }
        if let Some(g) = saved.gamma.take() {
            w.set_gamma(j, instance, g);
        }
        if let Some(f) = saved.fov.take() {
            w.set_fov(j, instance, f);
        }
        if let Some(b) = saved.bob_view.take() {
            w.set_bob_view(j, instance, b);
        }
    }
    if let Some(prev) = saved.smart_cull.take() {
        mc.set_smart_cull(j, instance, prev);
    }
}