Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
879 lines (799 loc) · 39.0 KB Raw
// What the modules actually do.
//
// Everything here is client-side only: nothing depends on owning the server, so
// it behaves the same on a real one. Anything that changes game state also
// remembers what it changed, so switching a module off puts the game back
// rather than leaving it altered.
//
// The rule that shapes most of this: the server re-simulates your position and
// takes your aim from the movement stream. So moving is a matter of nudging
// velocity within what it will accept, and aiming silently means telling it a
// rotation the camera never took.
#pragma once

#include <cmath>

#include "actions.hpp"
#include "interact.hpp"
#include "jni.hpp"
#include "mc.hpp"
#include "state.hpp"
#include "world.hpp"

namespace lodestone::cheats {

/// What we changed and must put back.
struct Saved {
    bool fly_applied = false;
    bool had_may_fly = false;
    float had_fly_speed = 0.05f;
    unsigned long long last_attack_ms = 0;
    unsigned long long last_crystal_ms = 0;
    /// Criticals wait for the fall: once we have hopped, hold the swing until
    /// the player is actually descending.
    bool crit_jumped = false;
    bool sprint_reset_pending = false;
    /// The slot the mace was swapped in from, so it can go back on landing.
    int mace_prev_slot = -1;

    /// Aimbot turn rate, carried between frames so the turn accelerates and
    /// settles rather than snapping on and off.
    float aim_vel_yaw = 0.0f, aim_vel_pitch = 0.0f;
    double last_frame_s = 0.0;

    unsigned long long last_trigger_ms = 0;
    unsigned long long last_totem_ms = 0;
    /// Damage is detected as an edge, so the previous frame's values matter.
    float prev_health = 20.0f;
    int prev_hurt_time = 0;

    /// Attribute values as they were before a module overwrote them.
    bool reach_saved = false;
    double had_entity_reach = 0.0, had_block_reach = 0.0;
    bool step_saved = false;
    double had_step_height = 0.0;
    bool no_physics_applied = false;

    /// Visual options as they were found, so switching a module off puts the
    /// game back instead of leaving it altered.
    bool gamma_saved = false;
    double had_gamma = 0.0;
    bool tilt_saved = false;
    double had_tilt = 0.0;
    bool bob_saved = false;
    bool had_bob = true;
    bool cull_saved = false;
    bool had_cull = true;
    bool fov_saved = false;
    int had_fov = 70;
    bool view_saved = false;
    int had_view = 12;
    /// The last view distance actually asked for: the request is a packet, so
    /// it only goes when the number changes.
    int view_sent = 0;
    bool timer_applied = false;

    unsigned long long last_click_ms = 0;
    unsigned long long last_respawn_ms = 0;

    /// Block Reach writes the same attribute as combat Reach, so it keeps its
    /// own remembered value rather than sharing one.
    bool build_reach_saved = false;
    double had_build_reach = 0.0;
};

namespace {

double now_seconds() { return static_cast<double>(GetTickCount64()) / 1000.0; }

/// Yaw/pitch that looks from `from` at `to`, in Minecraft's convention.
void look_at(const state::Vec3& from, const state::Vec3& to, float* yaw, float* pitch) {
    double dx = to.x - from.x, dy = to.y - from.y, dz = to.z - from.z;
    double flat = std::sqrt(dx * dx + dz * dz);
    *yaw = static_cast<float>(std::atan2(-dx, dz) * 180.0 / 3.14159265358979323846);
    *pitch = static_cast<float>(-std::atan2(dy, flat) * 180.0 / 3.14159265358979323846);
}

float clampf(float v, float lo, float hi) { return v < lo ? lo : (v > hi ? hi : v); }

/// Shortest signed turn from one yaw to another, in degrees. Going the long way
/// round a wrap is the difference between a flick and a spin.
float angle_delta(float from, float to) {
    float d = std::fmod(to - from, 360.0f);
    if (d > 180.0f) d -= 360.0f;
    if (d < -180.0f) d += 360.0f;
    return d;
}

/// A cheap -0.5..0.5 wobble that moves over time. Deliberately not a good
/// random source — nothing here needs one, it only needs to not be still.
float pseudo_random() {
    unsigned long long t = GetTickCount64() / 50;
    t = t * 6364136223846793005ULL + 1442695040888963407ULL;
    return static_cast<float>((t >> 33) & 0xFFFF) / 65535.0f - 0.5f;
}

/// Launch pitch (degrees, negative is up) and flight time (ticks) that land a
/// projectile `horizontal` away and `dy` up at launch `speed`.
///
/// An arrow leaves at power*3 blocks/tick and then falls under gravity
/// (0.05/tick) with air drag (0.99/tick). There is no closed form once drag is
/// in, so this simulates a virtual arrow at each candidate angle and keeps the
/// one that lands nearest.
bool solve_pitch(double speed, double horizontal, double dy, double* out_pitch, double* out_time) {
    if (speed <= 1e-6 || horizontal <= 1e-6) return false;
    double best_angle = 0.0, best_err = 1e18, best_t = 0.0;
    // Sweeping upward from -85 takes the low arc first, which is the shot a
    // person would take when both arcs reach.
    for (double angle = -85.0; angle <= 85.0; angle += 0.5) {
        double rad = angle * 3.14159265358979323846 / 180.0;
        double vx = std::cos(rad) * speed;
        double vy = -std::sin(rad) * speed;  // pitch up is negative in Minecraft
        double x = 0.0, y = 0.0, t = 0.0;
        while (x < horizontal && t < 200.0) {
            x += vx;
            y += vy;
            vy -= 0.05;
            vx *= 0.99;
            vy *= 0.99;
            t += 1.0;
        }
        double err = std::fabs(y - dy);
        if (err < best_err) {
            best_err = err;
            best_angle = angle;
            best_t = t;
        }
    }
    if (best_err >= 1.5) return false;  // out of range whatever the angle
    *out_pitch = best_angle;
    *out_time = best_t;
    return true;
}

bool aura_wants(const state::Combat& c, state::TargetKind kind) {
    switch (kind) {
        case state::TargetKind::Player: return c.aura_players;
        case state::TargetKind::Mob: return c.aura_mobs;
        case state::TargetKind::Animal: return c.aura_animals;
        default: return false;
    }
}

// ---------------------------------------------------------------------------

/// Flight, by telling the client it is allowed to. The server grants this
/// normally in creative; elsewhere it will disagree and pull you back, which is
/// why the speed matters more than the permission.
void flight(const jni::Env& j, const mc::Actions& act, jobject player,
            const state::Config& cfg, Saved& saved) {
    jobject abilities = act.abilities(j, player);
    if (!abilities) return;

    if (cfg.movement.fly) {
        if (!saved.fly_applied) {
            saved.had_may_fly = act.flying(j, abilities);
            saved.had_fly_speed = act.fly_speed(j, abilities);
            saved.fly_applied = true;
        }
        act.set_may_fly(j, abilities, true);
        act.set_flying(j, abilities, true);
        act.set_fly_speed(j, abilities, cfg.movement.fly_speed);
    } else if (saved.fly_applied) {
        // Put back exactly what was there, so turning it off is invisible.
        act.set_flying(j, abilities, saved.had_may_fly);
        act.set_fly_speed(j, abilities, saved.had_fly_speed);
        saved.fly_applied = false;
    }
    j.delete_local(abilities);
}

/// Horizontal speed, applied to the velocity the server will re-simulate.
void speed(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
           const state::GameState& game) {
    if (!cfg.movement.speed || game.screen_open) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    double flat = std::sqrt(v.x * v.x + v.z * v.z);
    if (flat < 0.02) return;  // standing still: nothing to scale

    // Keeping inside what the server re-simulates without snapping you back is
    // the whole difference between "fast" and "rubber-banded".
    double want = cfg.movement.speed_limit ? 0.42 : 2.0;
    double target = flat + static_cast<double>(cfg.movement.speed_value) * 0.2;
    if (target > want) target = want;
    double scale = target / flat;
    act.set_delta_movement(j, player, v.x * scale, v.y, v.z * scale);
}

/// Sprint, held on rather than tapped.
void sprint(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
            const state::GameState& game) {
    if (!cfg.movement.sprint || game.screen_open || !game.in_world) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    if (std::sqrt(v.x * v.x + v.z * v.z) > 0.05 && !act.sprinting(j, player)) {
        act.set_sprinting(j, player, true);
    }
}

/// No Fall: the server decides fall damage from the on-ground flag you send, so
/// keep saying you are on the ground while descending.
void no_fall(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
             const state::GameState& game) {
    if (!cfg.movement.no_fall || game.on_ground || !game.in_world) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v) || v.y > -0.5) return;  // only once actually falling
    act.flush_position(j, player, game.pos, game.yaw, game.pitch, true);
}

/// Bhop: jump the instant you land, so momentum is never lost to a step.
void bhop(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
          const state::GameState& game) {
    if (!cfg.movement.bhop || !game.on_ground || game.screen_open) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    if (std::sqrt(v.x * v.x + v.z * v.z) < 0.05) return;  // only while actually moving
    act.set_delta_movement(j, player, v.x, 0.42, v.z);    // a vanilla jump
}

/// High Jump: amplify the jump itself, not free flight — so it still reads as a
/// jump to anything watching the position stream.
void high_jump(const jni::Env& j, const mc::Actions& act, jobject player,
               const state::Config& cfg, const state::GameState& game) {
    if (!cfg.movement.jump_power || game.screen_open || game.on_ground) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    // Only the frame you leave the ground: a vanilla jump is 0.42.
    if (v.y > 0.40 && v.y < 0.45) {
        act.set_delta_movement(j, player, v.x, v.y * cfg.movement.jump_multiplier, v.z);
    }
}

/// Jetpack: rise while the jump key is held.
void jetpack(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
             const state::GameState& game) {
    if (!cfg.movement.jetpack || game.screen_open) return;
    if ((GetAsyncKeyState(VK_SPACE) & 0x8000) == 0) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    act.set_delta_movement(j, player, v.x, cfg.movement.jetpack_power, v.z);
}

/// Auto Mace: get the mace into your hand on the way down, and give the slot
/// back on landing. The swing is left to the aura or to you.
void auto_mace(const jni::Env& j, const mc::Interact& it, const mc::Actions& act, jobject player,
               const state::Config& cfg, const state::GameState& game, Saved& saved) {
    if (!cfg.combat.auto_mace || game.screen_open) return;
    state::Vec3 v;
    bool descending = !game.on_ground && act.delta_movement(j, player, &v) && v.y < -0.25;
    if (descending) {
        if (!it.holding_mace(j, player)) {
            int slot = it.find_mace(j, player);
            if (slot >= 0) {
                if (saved.mace_prev_slot < 0) saved.mace_prev_slot = it.selected_slot(j, player);
                it.select_slot(j, player, slot);
            }
        }
    } else if (saved.mace_prev_slot >= 0) {
        it.select_slot(j, player, saved.mace_prev_slot);
        saved.mace_prev_slot = -1;
    }
}

/// Auto Crystal: detonate the end crystals near you. A crystal is an entity, so
/// breaking one is just an attack; placing needs a face to build on beside the
/// target and is a separate problem.
void auto_crystal(const jni::Env& j, const mc::Mc& core, const mc::World& world,
                  const mc::Interact& it, jobject instance, jobject player,
                  const state::Config& cfg, const state::GameState& game, Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!c.auto_crystal || game.screen_open || !game.in_world) return;
    double delay = c.crystal_delay > 0.02f ? static_cast<double>(c.crystal_delay) : 0.02;
    if (now_seconds() - static_cast<double>(saved.last_crystal_ms) / 1000.0 < delay) return;

    jobject level = core.level(j, instance);
    jobject game_mode = core.game_mode(j, instance);
    if (!level || !game_mode) return;
    jobject iterator = world.entity_iterator(j, level);
    if (!iterator) return;

    // Nearest within reach: the closest crystal is both the one most likely
    // aimed at you and the one the server will accept the hit on.
    jobject best = nullptr;
    double best_distance = static_cast<double>(c.crystal_range);
    for (int n = 0; n < 4096; ++n) {
        jobject entity = world.iter_next(j, iterator);
        if (!entity) break;
        bool keep = false;
        if (it.is_end_crystal(j, entity)) {
            state::Vec3 lo, hi;
            if (world.bounding_box(j, entity, &lo, &hi)) {
                double cx = (lo.x + hi.x) * 0.5, cy = (lo.y + hi.y) * 0.5, cz = (lo.z + hi.z) * 0.5;
                double dx = cx - game.eye.x, dy = cy - game.eye.y, dz = cz - game.eye.z;
                double d = std::sqrt(dx * dx + dy * dy + dz * dz);
                if (d < best_distance) {
                    best_distance = d;
                    if (best) j.delete_local(best);
                    best = entity;
                    keep = true;
                }
            }
        }
        if (!keep) j.delete_local(entity);
    }
    j.delete_local(iterator);
    if (!best) return;

    world.attack(j, game_mode, player, best);
    saved.last_crystal_ms = GetTickCount64();
    j.delete_local(best);
}

/// Kill Aura. Iterates entities itself rather than reusing the render scan,
/// because attacking needs the live object and the scan keeps only the numbers.
void kill_aura(const jni::Env& j, const mc::Mc& core, const mc::World& world,
               const mc::Actions& act, jobject instance, jobject player,
               const state::Config& cfg, const state::GameState& game, Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!c.kill_aura || game.screen_open || !game.in_world) return;

    double interval = 1.0 / (c.aura_cps > 0.5f ? static_cast<double>(c.aura_cps) : 0.5);
    double now = now_seconds();
    if (now - static_cast<double>(saved.last_attack_ms) / 1000.0 < interval) return;

    jobject level = core.level(j, instance);
    jobject game_mode = core.game_mode(j, instance);
    if (!level || !game_mode) return;
    jobject iterator = world.entity_iterator(j, level);
    if (!iterator) return;

    jobject best = nullptr;
    double best_distance = static_cast<double>(c.aura_range);
    state::Vec3 best_centre{};
    for (int n = 0; n < 4096; ++n) {
        jobject entity = world.iter_next(j, iterator);
        if (!entity) break;
        bool keep = false;
        if (!j.same_object(entity, player) && core.is_alive(j, entity)) {
            state::TargetKind kind = world.classify(j, core, entity);
            if (aura_wants(c, kind)) {
                state::Vec3 lo, hi;
                if (world.bounding_box(j, entity, &lo, &hi)) {
                    // Hitbox: consider a box larger than the one being drawn.
                    // The server still checks the real one, so this widens what
                    // we will *try* rather than making a miss land.
                    if (c.hitbox) {
                        double e = static_cast<double>(c.hitbox_expand);
                        lo = {lo.x - e, lo.y - e, lo.z - e};
                        hi = {hi.x + e, hi.y + e, hi.z + e};
                    }
                    state::Vec3 centre{(lo.x + hi.x) * 0.5, (lo.y + hi.y) * 0.5,
                                       (lo.z + hi.z) * 0.5};
                    double dx = centre.x - game.eye.x, dy = centre.y - game.eye.y,
                           dz = centre.z - game.eye.z;
                    double d = std::sqrt(dx * dx + dy * dy + dz * dz);
                    // Walls: the game already knows how to answer this, so ask
                    // it rather than casting our own ray.
                    bool visible =
                        c.aura_through_walls || act.has_line_of_sight(j, player, entity);
                    if (d < best_distance && visible) {
                        best_distance = d;
                        best_centre = centre;
                        if (best) j.delete_local(best);
                        best = entity;
                        keep = true;
                    }
                }
            }
        }
        if (!keep) j.delete_local(entity);
    }
    j.delete_local(iterator);
    if (!best) return;

    // Criticals: a hit only crits while you are falling and not sprinting, so
    // hop first and hold the swing until the game would agree. A nudge is
    // enough — a few centimetres, visible in the position stream, barely
    // visible to anyone watching.
    bool may_swing = true;
    if (c.criticals) {
        state::Vec3 v;
        bool falling = act.delta_movement(j, player, &v) && v.y < 0.0;
        if (saved.crit_jumped && falling) {
            saved.crit_jumped = false;  // descending: this one counts
        } else {
            if (act.sprinting(j, player)) act.set_sprinting(j, player, false);
            if (game.on_ground) {
                state::Vec3 d;
                if (act.delta_movement(j, player, &d)) {
                    act.set_delta_movement(j, player, d.x, static_cast<double>(c.crit_hop), d.z);
                }
                // Push the leaving-the-ground position now rather than waiting
                // for the tick's own send, so the server agrees we are airborne.
                state::Vec3 lifted{game.pos.x, game.pos.y + 0.001, game.pos.z};
                act.flush_position(j, player, lifted, game.yaw, game.pitch, false);
                saved.crit_jumped = true;
            }
            may_swing = false;
        }
    }
    if (!may_swing) {
        j.delete_local(best);
        return;
    }

    // Sprint reset: a hit landing on the tick sprint restarts gets the sprint
    // knockback bonus. It fights criticals, which need no sprint, so it only
    // runs when criticals is off.
    if (c.sprint_reset && !c.criticals && act.sprinting(j, player)) {
        act.set_sprinting(j, player, false);
        saved.sprint_reset_pending = true;
    }

    // Aim at it. Silent means the server is told a rotation the camera never
    // took, so the look has to go out in the movement stream — the attack
    // packet carries none.
    if (c.aura_rotate || c.aim_mode == state::AimMode::Silent) {
        float yaw = 0, pitch = 0;
        look_at(game.eye, best_centre, &yaw, &pitch);
        if (c.aim_mode == state::AimMode::Silent && !c.aura_rotate) {
            act.flush_position(j, player, game.pos, yaw, pitch, game.on_ground);
        } else {
            act.set_rotation(j, player, yaw, pitch);
        }
    }
    world.attack(j, game_mode, player, best);
    saved.last_attack_ms = GetTickCount64();
    if (saved.sprint_reset_pending) {
        act.set_sprinting(j, player, true);  // restart, completing the reset
        saved.sprint_reset_pending = false;
    }
    j.delete_local(best);
}

/// Aimbot, camera mode: turn the view towards the nearest target.
///
/// Reads the entity scan the frame already did rather than walking the world
/// again — aiming only needs a position, which the scan already has.
void aimbot(const jni::Env& j, const mc::Actions& act, jobject player, const state::Config& cfg,
            const state::GameState& game, Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!c.aimbot || game.screen_open || !game.in_world) return;
    // Silent aim is applied at the swing in kill_aura and put straight back, so
    // only Camera mode moves the view itself.
    if (c.aim_mode != state::AimMode::Camera) return;

    double now = now_seconds();
    float dt = saved.last_frame_s > 0.0 ? static_cast<float>(now - saved.last_frame_s) : 1.0f / 60.0f;
    saved.last_frame_s = now;
    if (dt <= 0.0f || dt > 0.25f) dt = 1.0f / 60.0f;  // a stall is not a turn

    const state::Target* best = nullptr;
    float best_distance = 1e9f;
    for (const auto& t : game.targets) {
        if (!aura_wants(c, t.kind)) continue;
        // Worth aiming at things further off than you can actually hit, so the
        // turn is already finished by the time they close.
        if (t.distance > c.aura_range * 3.0f) continue;
        if (t.distance < best_distance) {
            best_distance = t.distance;
            best = &t;
        }
    }

    // Let a built-up turn rate decay rather than snapping to a stop.
    auto settle = [&] {
        saved.aim_vel_yaw *= 0.85f;
        saved.aim_vel_pitch *= 0.85f;
    };
    if (!best) return settle();

    state::Vec3 centre{(best->min.x + best->max.x) * 0.5, (best->min.y + best->max.y) * 0.5,
                       (best->min.z + best->max.z) * 0.5};
    float want_yaw = 0.0f, want_pitch = 0.0f;
    look_at(game.eye, centre, &want_yaw, &want_pitch);
    if (std::fabs(angle_delta(game.yaw, want_yaw)) > c.aim_fov * 0.5f) return settle();

    // A lerp towards the target is what makes an aimbot look like one: fastest
    // at the instant it acquires and then crawling, which is the opposite of
    // how a hand moves. This is a critically damped spring, so the turn
    // accelerates, carries and settles without overshooting — and it is capped
    // at a maximum rate, so no acquisition is instant however far off it starts.
    float stiffness = c.aim_speed * 20.0f;
    if (stiffness < 0.8f) stiffness = 0.8f;
    float damping = 2.0f * std::sqrt(stiffness);
    float max_rate = 45.0f + c.aim_speed * 320.0f;

    // A little noise on the aim point: a head held perfectly still on a moving
    // target is not something a hand does.
    float wobble = pseudo_random() * 0.6f;
    float dy = angle_delta(game.yaw, want_yaw + wobble);
    float dp = (want_pitch + wobble * 0.4f) - game.pitch;
    saved.aim_vel_yaw += (dy * stiffness - saved.aim_vel_yaw * damping) * dt;
    saved.aim_vel_pitch += (dp * stiffness - saved.aim_vel_pitch * damping) * dt;
    saved.aim_vel_yaw = clampf(saved.aim_vel_yaw, -max_rate, max_rate);
    saved.aim_vel_pitch = clampf(saved.aim_vel_pitch, -max_rate, max_rate);

    act.set_rotation(j, player, game.yaw + saved.aim_vel_yaw * dt,
                     clampf(game.pitch + saved.aim_vel_pitch * dt, -90.0f, 90.0f));
}

/// Swing when the crosshair is already on something — no aiming and no target
/// selection, which is why it is the least conspicuous of the three.
void trigger_bot(const jni::Env& j, const mc::Mc& core, const mc::World& world, jobject instance,
                 jobject player, const state::Config& cfg, const state::GameState& game,
                 Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!c.trigger_bot || game.screen_open || !game.in_world) return;
    double delay = c.trigger_delay > 0.05f ? static_cast<double>(c.trigger_delay) : 0.05;
    if (now_seconds() - static_cast<double>(saved.last_trigger_ms) / 1000.0 < delay) return;

    jobject target = world.crosshair_entity(j, instance);
    if (!target) return;
    // The server disconnects you outright for attacking a dropped item, an
    // experience orb or yourself — handleInteract treats those as a protocol
    // violation rather than a miss. The crosshair lands on them all the time,
    // so this filter is not optional. Everything it must exclude is excluded by
    // the entity simply being a LivingEntity.
    if (j.same_object(target, player) || !world.is_living(j, target)) {
        j.delete_local(target);
        return;
    }
    if (jobject game_mode = core.game_mode(j, instance)) {
        world.attack(j, game_mode, player, target);
        saved.last_trigger_ms = GetTickCount64();
    }
    j.delete_local(target);
}

/// Staying alive: the totem and the shield, both driven by damage landing.
void survival(const jni::Env& j, const mc::Mc& core, const mc::Interact& it, jobject instance,
              jobject player, const state::Config& cfg, const state::GameState& game,
              Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!game.in_world) return;

    // No health threshold of its own. A threshold either holds the offhand
    // hostage for nothing or arrives after the hit that mattered; what actually
    // decides it is damage landing — the instant health drops, or the instant
    // the totem you were holding pops and leaves the hand empty.
    bool took_damage = game.health < saved.prev_health || game.hurt_time > saved.prev_hurt_time;
    saved.prev_health = game.health;
    saved.prev_hurt_time = game.hurt_time;

    if (c.auto_totem && game.health > 0.0f && took_damage && !it.holding_totem(j, player)) {
        double since = now_seconds() - static_cast<double>(saved.last_totem_ms) / 1000.0;
        bool ready = c.totem_mode == state::TotemMode::Legit
                         ? (game.health <= 10.0f && since > 0.18)
                         : since > 0.1;
        if (ready) {
            int slot = it.find_totem(j, player);
            if (slot >= 0) {
                if (jobject game_mode = core.game_mode(j, instance)) {
                    it.swap_to_offhand(j, game_mode, player, slot);
                    saved.last_totem_ms = GetTickCount64();
                }
            }
        }
    }

    // Raised on the tick a hit lands and held while the damage flash lasts.
    // Nothing here predicts the swing: a shield that comes up before the
    // attacker moves is a tell, one that comes up on the hit is just fast.
    if (c.auto_shield && game.hurt_time > 0 && !game.screen_open && it.holding_shield(j, player)) {
        it.start_use(j, instance);
    }
}

/// Reach and step height are attributes, so they are set once and put back when
/// the module goes off rather than rewritten every frame.
void attributes(const jni::Env& j, const mc::World& world, jobject player,
                const state::Config& cfg, Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (c.reach) {
        if (!saved.reach_saved) {
            saved.reach_saved = world.attribute_base(j, player, world.holder_entity_reach(),
                                                     &saved.had_entity_reach);
            world.attribute_base(j, player, world.holder_block_reach(), &saved.had_block_reach);
        }
        double want = static_cast<double>(c.reach_distance);
        world.set_attribute_base(j, player, world.holder_entity_reach(), want);
        world.set_attribute_base(j, player, world.holder_block_reach(), want);
    } else if (saved.reach_saved) {
        world.set_attribute_base(j, player, world.holder_entity_reach(), saved.had_entity_reach);
        world.set_attribute_base(j, player, world.holder_block_reach(), saved.had_block_reach);
        saved.reach_saved = false;
    }

    const state::Movement& m = cfg.movement;
    if (m.step) {
        if (!saved.step_saved) {
            saved.step_saved =
                world.attribute_base(j, player, world.holder_step_height(), &saved.had_step_height);
        }
        world.set_attribute_base(j, player, world.holder_step_height(),
                                 static_cast<double>(m.step_height));
    } else if (saved.step_saved) {
        world.set_attribute_base(j, player, world.holder_step_height(), saved.had_step_height);
        saved.step_saved = false;
    }

    // Block Reach is a module of its own and writes the same attribute combat
    // Reach does, so it keeps its own remembered value: whichever is switched
    // on last wins, and each still restores what it actually found.
    if (cfg.building.block_reach) {
        if (!saved.build_reach_saved) {
            saved.build_reach_saved = world.attribute_base(j, player, world.holder_block_reach(),
                                                           &saved.had_build_reach);
        }
        world.set_attribute_base(j, player, world.holder_block_reach(),
                                 static_cast<double>(cfg.building.block_reach_dist));
    } else if (saved.build_reach_saved) {
        world.set_attribute_base(j, player, world.holder_block_reach(), saved.had_build_reach);
        saved.build_reach_saved = false;
    }
}

/// The client-side cooldowns that gate how fast you can place and break.
/// Neither is enforced by the server and both refill on their own, so this just
/// keeps knocking them back to zero — there is nothing to put back.
void building(const jni::Env& j, const mc::Mc& core, jobject instance,
              const state::Config& cfg) {
    if (!cfg.building.no_cooldown) return;
    core.clear_use_cooldowns(j, instance);
    if (jobject game_mode = core.game_mode(j, instance)) {
        core.set_destroy_delay(j, game_mode, 0);
        j.delete_local(game_mode);
    }
}

/// Noclip, by clearing the physics flag the game already uses for spectators.
/// The server still re-simulates your position, so this is what lets you move
/// through something — not what convinces the server you did.
void noclip(const jni::Env& j, const mc::World& world, jobject player, const state::Config& cfg,
            Saved& saved) {
    if (cfg.movement.noclip) {
        world.set_no_physics(j, player, true);
        saved.no_physics_applied = true;
    } else if (saved.no_physics_applied) {
        world.set_no_physics(j, player, false);
        saved.no_physics_applied = false;
    }
}

/// Spider: climb whatever you walk into, by pushing up while a wall is being
/// hit. The game already tracks that collision, so there is nothing to trace.
void spider(const jni::Env& j, const mc::World& world, const mc::Actions& act, jobject player,
            const state::Config& cfg, const state::GameState& game) {
    if (!cfg.movement.spider || game.screen_open || !game.in_world) return;
    if (!world.hitting_wall(j, player)) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    act.set_delta_movement(j, player, v.x, static_cast<double>(cfg.movement.spider_power), v.z);
}

/// Aim a drawn bow at the ballistic solution for a moving target.
void bow_aimbot(const jni::Env& j, const mc::Mc& core, const mc::World& world,
                const mc::Actions& act, jobject instance, jobject player,
                const state::Config& cfg, const state::GameState& game) {
    if (!cfg.combat.bow_aimbot || game.screen_open || !game.in_world) return;
    float charge = world.bow_charge(j, player);
    if (charge < 0.1f) return;  // not drawn enough to be worth aiming

    jobject level = core.level(j, instance);
    if (!level) return;
    jobject target = world.nearest_living(j, core, level, player, game.eye, 80.0);
    if (!target) return;
    state::Vec3 lo, hi, vel{0, 0, 0};
    bool have_box = world.bounding_box(j, target, &lo, &hi);
    act.delta_movement(j, target, &vel);
    j.delete_local(target);
    if (!have_box) return;

    double launch_speed = static_cast<double>(charge) * 3.0;
    state::Vec3 base{(lo.x + hi.x) * 0.5, lo.y + (hi.y - lo.y) * 0.5, (lo.z + hi.z) * 0.5};

    // Converge the flight time and the lead together: where the target will be
    // depends on how long the arrow takes, which depends on where it is going.
    double flight = 0.0;
    state::Vec3 aim = base;
    double pitch = 0.0, t = 0.0;
    for (int i = 0; i < 4; ++i) {
        aim = {base.x + vel.x * flight, base.y + vel.y * flight, base.z + vel.z * flight};
        double ax = aim.x - game.eye.x, az = aim.z - game.eye.z;
        if (!solve_pitch(launch_speed, std::sqrt(ax * ax + az * az), aim.y - game.eye.y, &pitch,
                         &t)) {
            return;
        }
        flight = t;
    }

    double ax = aim.x - game.eye.x, az = aim.z - game.eye.z;
    float yaw = static_cast<float>(std::atan2(az, ax) * 180.0 / 3.14159265358979323846 - 90.0);
    act.set_rotation(j, player, yaw, static_cast<float>(pitch));
}

/// The visual options. Each remembers what it found before changing anything,
/// so turning a module off puts the setting back rather than leaving it.
void visuals(const jni::Env& j, const mc::Mc& core, const mc::World& world, jobject instance,
             const state::Config& cfg, Saved& saved) {
    const state::Visuals& v = cfg.visuals;

    if (v.no_weather) {
        if (jobject level = core.level(j, instance)) {
            world.set_weather(j, level, 0.0f);
            j.delete_local(level);
        }
    }

    // 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 already here.
    if (v.no_culling) {
        if (!saved.cull_saved) saved.cull_saved = core.smart_cull(j, instance, &saved.had_cull);
        core.set_smart_cull(j, instance, false);
    } else if (saved.cull_saved) {
        core.set_smart_cull(j, instance, saved.had_cull);
        saved.cull_saved = false;
    }

    // The damage tilt is scaled by an accessibility option the game reads while
    // it renders, so zeroing that removes the lurch before it is ever drawn.
    // Clearing hurtTime afterwards only takes effect for the *next* frame,
    // which still leaves one frame of it on every hit.
    if (v.no_hurt_cam) {
        if (!saved.tilt_saved) saved.tilt_saved = core.damage_tilt(j, instance, &saved.had_tilt);
        core.set_damage_tilt(j, instance, 0.0);
    } else if (saved.tilt_saved) {
        core.set_damage_tilt(j, instance, saved.had_tilt);
        saved.tilt_saved = false;
    }

    if (v.no_bob) {
        if (!saved.bob_saved) saved.bob_saved = core.bob_view(j, instance, &saved.had_bob);
        core.set_bob_view(j, instance, false);
    } else if (saved.bob_saved) {
        core.set_bob_view(j, instance, saved.had_bob);
        saved.bob_saved = false;
    }

    if (v.fullbright) {
        if (!saved.gamma_saved) saved.gamma_saved = core.gamma(j, instance, &saved.had_gamma);
        core.set_gamma(j, instance, 15.0);
    } else if (saved.gamma_saved) {
        core.set_gamma(j, instance, saved.had_gamma);
        saved.gamma_saved = false;
    }

    if (v.fov) {
        if (!saved.fov_saved) saved.fov_saved = core.fov_option(j, instance, &saved.had_fov);
        core.set_fov_option(j, instance, static_cast<int>(v.fov_value));
    } else if (saved.fov_saved) {
        core.set_fov_option(j, instance, saved.had_fov);
        saved.fov_saved = false;
    }

    // Asking for a wider view, and only when the number changes: the request
    // travels as a packet, and one per frame would be a flood.
    if (v.view_distance) {
        int want = static_cast<int>(v.view_distance_chunks);
        if (!saved.view_saved) saved.view_saved = core.render_distance(j, instance, &saved.had_view);
        if (saved.view_sent != want) {
            saved.view_sent = want;
            core.set_render_distance(j, instance, want);
        }
    } else if (saved.view_saved) {
        core.set_render_distance(j, instance, saved.had_view);
        saved.view_saved = false;
        saved.view_sent = 0;
    }

    if (cfg.movement.timer) {
        core.set_timer(j, instance, cfg.movement.timer_speed);
        saved.timer_applied = true;
    } else if (saved.timer_applied) {
        core.set_timer(j, instance, 1.0f);  // back to 20 TPS
        saved.timer_applied = false;
    }
}

/// Respawn as soon as you are dead. The client can do this the instant the
/// death screen would appear, which is well before a person could click.
void misc(const jni::Env& j, const mc::Mc& core, jobject player, const state::Config& cfg,
          const state::GameState& game, Saved& saved) {
    if (!cfg.misc.auto_respawn || game.health > 0.0f) return;
    // A short gap between attempts: the first one has to reach the server and
    // come back before a second would mean anything.
    if (now_seconds() - static_cast<double>(saved.last_respawn_ms) / 1000.0 < 0.25) return;
    saved.last_respawn_ms = GetTickCount64();
    core.respawn(j, player);
}

/// Anti-knockback: scale down the velocity a hit just imparted.
void anti_knockback(const jni::Env& j, const mc::Actions& act, jobject player,
                    const state::Config& cfg, const state::GameState& game, Saved& saved) {
    const state::Combat& c = cfg.combat;
    // Only on the frame a hit lands. Scaling velocity every frame would fight
    // ordinary movement rather than the knockback.
    if (!c.anti_knockback || game.hurt_time <= saved.prev_hurt_time) return;
    state::Vec3 v;
    if (!act.delta_movement(j, player, &v)) return;
    act.set_delta_movement(j, player, v.x * static_cast<double>(c.kb_horizontal),
                           v.y * static_cast<double>(c.kb_vertical),
                           v.z * static_cast<double>(c.kb_horizontal));
}

/// Auto clicker: swing while the button is held, at a rate whose gap is
/// jittered — a machine-perfect rhythm is the easiest thing there is to spot.
void auto_clicker(const jni::Env& j, const mc::Mc& core, jobject instance,
                  const state::Config& cfg, const state::GameState& game, Saved& saved) {
    const state::Combat& c = cfg.combat;
    if (!c.auto_clicker || game.screen_open || !game.in_world) return;
    if ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) == 0) return;  // only while held
    double interval = 1.0 / (c.click_cps > 0.5f ? static_cast<double>(c.click_cps) : 0.5);
    double jitter = 1.0 + static_cast<double>(c.click_jitter) * static_cast<double>(pseudo_random());
    if (now_seconds() - static_cast<double>(saved.last_click_ms) / 1000.0 < interval * jitter) {
        return;
    }
    saved.last_click_ms = GetTickCount64();
    core.start_attack(j, instance);
}

}  // namespace

/// Run every enabled module for this frame. `cfg` is a copy taken under the
/// lock, so nothing here blocks the window procedure.
inline void apply(const jni::Env& j, const mc::Mc& core, const mc::World& world,
                  const mc::Interact& it, const mc::Actions& act, jobject instance,
                  jobject player, const state::Config& cfg, const state::GameState& game,
                  Saved& saved) {
    if (!player) return;
    flight(j, act, player, cfg, saved);
    speed(j, act, player, cfg, game);
    sprint(j, act, player, cfg, game);
    bhop(j, act, player, cfg, game);
    high_jump(j, act, player, cfg, game);
    jetpack(j, act, player, cfg, game);
    no_fall(j, act, player, cfg, game);
    noclip(j, world, player, cfg, saved);
    spider(j, world, act, player, cfg, game);
    attributes(j, world, player, cfg, saved);
    visuals(j, core, world, instance, cfg, saved);
    building(j, core, instance, cfg);
    misc(j, core, player, cfg, game, saved);
    aimbot(j, act, player, cfg, game, saved);
    bow_aimbot(j, core, world, act, instance, player, cfg, game);
    auto_clicker(j, core, instance, cfg, game, saved);
    // Before survival: both read the same hurt-time edge, and survival is what
    // consumes it by advancing prev_hurt_time.
    anti_knockback(j, act, player, cfg, game, saved);
    survival(j, core, it, instance, player, cfg, game, saved);
    auto_mace(j, it, act, player, cfg, game, saved);
    auto_crystal(j, core, world, it, instance, player, cfg, game, saved);
    trigger_bot(j, core, world, instance, player, cfg, game, saved);
    kill_aura(j, core, world, act, instance, player, cfg, game, saved);
}

}  // namespace lodestone::cheats