Sign in Sign up
kretrod/lodestone Public
Branches
master
795 lines (738 loc) · 27.5 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_swing: Instant,
    last_aura: Instant,
    last_trigger: Instant,
    freecam_origin: Option<(f64, f64, f64)>,
    entity_reach: Option<f64>,
    block_reach: Option<f64>,
    step_height: Option<f64>,
    gamma: Option<f64>,
    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_swing: Instant::now(),
            last_aura: Instant::now(),
            last_trigger: Instant::now(),
            freecam_origin: None,
            entity_reach: None,
            block_reach: None,
            step_height: None,
            gamma: None,
            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 vanilla_fly = m.fly && m.fly_mode == FlyMode::Vanilla;
    if let Some(ab) = abilities {
        if vanilla_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 {
        match m.fly_mode {
            FlyMode::Vanilla => {}
            FlyMode::Glide => {
                // Cancel gravity only: horizontal control stays vanilla, and
                // the position stream keeps looking like ordinary movement.
                if let Some((dx, _, dz)) = mc.delta_movement(j, player) {
                    mc.set_delta_movement(j, player, dx, 0.0, dz);
                }
            }
            FlyMode::Smooth => {
                // Drive velocity along the way you are looking, accelerating
                // rather than snapping, so every step stays a plausible size.
                if let Some((dx, dy, dz)) = mc.delta_movement(j, player) {
                    let speed = 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();
                    let step = 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,
                    );
                }
            }
        }
    }

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

    // ---- freecam ---------------------------------------------------------
    // Without control of the packets we send, the honest version of this is a
    // ghost: collision off, and your body put back where it started when you
    // switch it off.
    if m.freecam {
        if saved.freecam_origin.is_none() {
            saved.freecam_origin = Some(st.pos);
        }
    } else if let Some((x, y, z)) = saved.freecam_origin.take() {
        mc.set_pos(j, player, x, y, z);
    }

    // ---- 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 {
        // 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);

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

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

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

/// 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((x, y, z)) = saved.freecam_origin.take() {
        mc.set_pos(j, player, x, y, z);
    }
    if let Some(w) = world {
        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);
        }
    }
}