Sign in Sign up
kretrod/lodestone Public
Branches
master
1272 lines (1185 loc) · 45.2 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::blocks::Blocks;
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,
    hud_hidden: Option<bool>,
    last_dodge: Instant,
    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,
            hud_hidden: None,
            last_dodge: Instant::now(),
            fov: None,
            bob_view: None,
        }
    }
}

pub fn apply(
    j: &Jni,
    mc: &Mc,
    world: Option<&World>,
    blocks: Option<&Blocks>,
    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 || aura.auto_dodge || esp_wanted(cfg);
        if want_scan {
            let mut danger = Danger::default();
            let best = scan(j, mc, w, instance, player, cfg, &mut st, &mut danger);
            combat(j, mc, w, instance, player, cfg, saved, &mut st, best, dt);
            if cfg.combat.auto_dodge && new_tick && !st.screen_open {
                auto_dodge(j, mc, blocks, instance, player, cfg, saved, &st, &danger);
            }
        }
        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.
/// Things in the world that are about to hurt you.
#[derive(Default)]
struct Danger {
    /// Hostile mobs and where they are.
    threats: Vec<(f64, f64, f64)>,
    /// Arrows in flight, with the velocity they are carrying.
    arrows: Vec<((f64, f64, f64), (f64, f64, f64))>,
}

#[allow(clippy::too_many_arguments)]
fn scan(
    j: &Jni,
    mc: &Mc,
    w: &World,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    st: &mut GameState,
    danger: &mut Danger,
) -> 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.
                }
            }
        }

        // Anything that might be about to hit us, for Auto Dodge.
        if cfg.combat.auto_dodge {
            if kind == TargetKind::Mob && distance <= cfg.combat.dodge_range as f64 {
                danger.threats.push(centre);
            } else if cfg.combat.dodge_arrows && w.is_arrow(j, entity) {
                if let (Some(pos), Some(vel)) =
                    (mc.position(j, entity), mc.delta_movement(j, entity))
                {
                    // Only ones actually moving; a spent arrow in the ground is
                    // not a threat.
                    if vel.0 * vel.0 + vel.1 * vel.1 + vel.2 * vel.2 > 0.01 {
                        danger.arrows.push((pos, vel));
                    }
                }
            }
        }

        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);
        }
    } 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);
            }
            if let Some(prev) = saved.hud_hidden.take() {
                mc.set_hud_hidden(j, instance, prev);
            }
        }
        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);
        // You are not looking through your own eyes, so your own hotbar,
        // hearts and experience bar have no business being on screen.
        saved.hud_hidden = Some(mc.hud_hidden(j, instance));
        mc.set_hud_hidden(j, instance, true);
        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;
}

/// Get out of the way.
///
/// Two problems that want different answers. A skeleton's arrow is already in
/// flight and will arrive on a known path, so the fix is to step sideways out
/// of that path — and the sooner the smaller the step needs to be. A zombie is
/// a slow thing that follows you, so the fix is to keep distance from it while
/// not walking into a wall or off a ledge.
///
/// Both produce a direction to want; the last step is choosing the nearest
/// direction to that which you can actually walk in, which is the part a
/// villager's navigator does properly and a naive "run away" does not.
#[allow(clippy::too_many_arguments)]
fn auto_dodge(
    j: &Jni,
    mc: &Mc,
    blocks: Option<&Blocks>,
    instance: jobject,
    player: jobject,
    cfg: &Config,
    saved: &mut Saved,
    st: &GameState,
    danger: &Danger,
) {
    let c = &cfg.combat;
    let eye = st.eye;
    let mut want = (0.0f64, 0.0f64);
    let mut urgent = false;

    // ---- arrows ----------------------------------------------------------
    for (pos, vel) in &danger.arrows {
        let rel = (eye.0 - pos.0, eye.1 - pos.1, eye.2 - pos.2);
        let speed_sq = vel.0 * vel.0 + vel.1 * vel.1 + vel.2 * vel.2;
        if speed_sq < 1e-6 {
            continue;
        }
        // Closest approach, assuming we stand still: the time at which the
        // separation stops shrinking.
        let t = -(rel.0 * vel.0 + rel.1 * vel.1 + rel.2 * vel.2) / speed_sq;
        if !(0.0..40.0).contains(&t) {
            continue;
        }
        let miss = (
            rel.0 + vel.0 * t,
            rel.1 + vel.1 * t,
            rel.2 + vel.2 * t,
        );
        let distance = (miss.0 * miss.0 + miss.1 * miss.1 + miss.2 * miss.2).sqrt();
        // A player is about 0.6 wide; give it margin for the arrow's own size
        // and for our own movement in the meantime.
        if distance > 1.4 {
            continue;
        }
        // Sidestep across the arrow's path, on whichever side we are already
        // drifting towards — that is the shorter move.
        let perp = (-vel.2, vel.0);
        let length = (perp.0 * perp.0 + perp.1 * perp.1).sqrt().max(1e-9);
        let perp = (perp.0 / length, perp.1 / length);
        let sign = if rel.0 * perp.0 + rel.2 * perp.1 >= 0.0 { 1.0 } else { -1.0 };
        // The closer it is to arriving, the harder we commit.
        let weight = (2.0 - t / 20.0).clamp(1.0, 2.0);
        want.0 += perp.0 * sign * weight;
        want.1 += perp.1 * sign * weight;
        urgent = true;
    }

    // ---- mobs ------------------------------------------------------------
    if !urgent {
        for threat in &danger.threats {
            let away = (eye.0 - threat.0, eye.2 - threat.2);
            let distance = (away.0 * away.0 + away.1 * away.1).sqrt().max(0.001);
            // Inverse square: the one breathing on you matters far more than
            // the one across the room.
            let weight = 1.0 / (distance * distance);
            want.0 += away.0 / distance * weight;
            want.1 += away.1 / distance * weight;
        }
    }

    let length = (want.0 * want.0 + want.1 * want.1).sqrt();
    if length < 1e-6 {
        return;
    }
    let want = (want.0 / length, want.1 / length);

    // ---- pick a direction we can actually take ---------------------------
    let level = mc.level(j, instance);
    let base = want.1.atan2(want.0);
    let mut chosen = None;
    // Straight on first, then progressively wider deviations either side.
    for offset in [0.0f64, 0.39, -0.39, 0.79, -0.79, 1.18, -1.18, 1.57, -1.57] {
        let angle = base + offset;
        let dir = (angle.cos(), angle.sin());
        if walkable(j, blocks, level, st.pos, dir, c.dodge_cliffs) {
            chosen = Some(dir);
            break;
        }
    }
    let Some(dir) = chosen else {
        return;
    };

    // ---- move ------------------------------------------------------------
    let speed = c.dodge_speed as f64;
    if let Some((_, dy, _)) = mc.delta_movement(j, player) {
        mc.set_delta_movement(j, player, dir.0 * speed, dy, dir.1 * speed);
    }

    // A one-block step up is a jump, not a wall — take it rather than stalling.
    if let (Some(b), Some(level)) = (blocks, level) {
        let ahead = (
            (st.pos.0 + dir.0).floor() as i32,
            st.pos.1.floor() as i32,
            (st.pos.2 + dir.1).floor() as i32,
        );
        let blocked_feet = b.blocks_motion(j, level, ahead.0, ahead.1, ahead.2);
        let clear_head = !b.blocks_motion(j, level, ahead.0, ahead.1 + 2, ahead.2);
        if blocked_feet && clear_head && st.on_ground {
            if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
                mc.set_delta_movement(j, player, dx, 0.42, dz);
            }
        }
    }
    saved.last_dodge = Instant::now();
}

/// Can we step this way? Head and feet clear, and — if asked — something to
/// land on rather than a drop.
fn walkable(
    j: &Jni,
    blocks: Option<&Blocks>,
    level: Option<jobject>,
    from: (f64, f64, f64),
    dir: (f64, f64),
    avoid_cliffs: bool,
) -> bool {
    let (Some(b), Some(level)) = (blocks, level) else {
        // With no way to read the world, moving is still better than standing
        // in front of a skeleton.
        return true;
    };
    let x = (from.0 + dir.0 * 1.2).floor() as i32;
    let z = (from.2 + dir.1 * 1.2).floor() as i32;
    let y = from.1.floor() as i32;

    // Head height must be clear even if we end up stepping up.
    if b.blocks_motion(j, level, x, y + 1, z) {
        return false;
    }
    if avoid_cliffs {
        // Something solid within a short drop; otherwise this is a ledge.
        let footing = (0..=3).any(|d| b.blocks_motion(j, level, x, y - d, z));
        if !footing {
            return false;
        }
    }
    true
}

/// 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);
                if let Some(prev) = saved.hud_hidden.take() {
                    mc.set_hud_hidden(j, instance, prev);
                }
            }
            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);
    }
}