Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
372 lines (338 loc) · 17.4 KB Raw
// Minecraft bindings: the handles the client needs, resolved by name once.
//
// Every id is looked up through `jni::Env`, which clears exceptions, so a rename
// in a future version costs the one feature that used it rather than the whole
// client. Anything optional is reported through `missing` so the menu can say
// what is unavailable instead of silently doing nothing.
//
// Signatures here were checked against the decompiled 26.2 classes rather than
// guessed — 26.2 renamed a good deal (ResourceLocation became Identifier, and
// textures went behind Blaze3D's GpuTexture), so assuming 1.20 shapes would
// quietly resolve nothing.
#pragma once

#include <string>
#include <vector>

#include "jni.hpp"
#include "state.hpp"

namespace lodestone::mc {

using jni::Args;
using jni::Env;
using jni::jv;

/// Core handles: the client singleton, the local player, the world, and the
/// per-entity reads that every feature starts from.
class Mc {
  public:
    /// Resolve everything. Returns false only when the client class itself is
    /// unrecognisable, which means the version is too different to continue.
    bool resolve(const Env& j, std::vector<std::string>& missing) {
        c_minecraft_ = j.find_class("net/minecraft/client/Minecraft");
        c_entity_ = j.find_class("net/minecraft/world/entity/Entity");
        c_living_ = j.find_class("net/minecraft/world/entity/LivingEntity");
        c_vec3_ = j.find_class("net/minecraft/world/phys/Vec3");
        c_player_ = j.find_class("net/minecraft/world/entity/player/Player");
        if (!c_minecraft_ || !c_entity_ || !c_vec3_) {
            missing.emplace_back("Minecraft/Entity/Vec3 classes");
            return false;
        }

        m_get_instance_ = j.static_method(c_minecraft_, "getInstance",
                                          "()Lnet/minecraft/client/Minecraft;");
        f_player_ = j.field(c_minecraft_, "player", "Lnet/minecraft/client/player/LocalPlayer;");
        f_level_ = j.field(c_minecraft_, "level", "Lnet/minecraft/client/multiplayer/ClientLevel;");
        f_screen_ = j.field(c_minecraft_, "screen", "Lnet/minecraft/client/gui/screens/Screen;");
        f_game_mode_ = j.field(c_minecraft_, "gameMode",
                               "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;");

        // `position` exists as both a private field and a public accessor; the
        // accessor is the stable one.
        m_position_ = j.method(c_entity_, "position", "()Lnet/minecraft/world/phys/Vec3;");
        f_x_ = j.field(c_vec3_, "x", "D");
        f_y_ = j.field(c_vec3_, "y", "D");
        f_z_ = j.field(c_vec3_, "z", "D");
        f_y_rot_ = j.field(c_entity_, "yRot", "F");
        f_x_rot_ = j.field(c_entity_, "xRot", "F");
        m_on_ground_ = j.method(c_entity_, "onGround", "()Z");
        m_is_alive_ = j.method(c_entity_, "isAlive", "()Z");
        m_get_id_ = j.method(c_entity_, "getId", "()I");
        m_bounding_box_ = j.method(c_entity_, "getBoundingBox", "()Lnet/minecraft/world/phys/AABB;");
        if (c_living_) {
            m_get_health_ = j.method(c_living_, "getHealth", "()F");
            m_get_max_health_ = j.method(c_living_, "getMaxHealth", "()F");
        }

        // The field of view the world is actually rendered with, which the ESP
        // projection has to match. It arrives as a boxed Integer because the
        // option type erases.
        f_options_ = j.field(c_minecraft_, "options", "Lnet/minecraft/client/Options;");
        jclass options = j.find_class("net/minecraft/client/Options");
        jclass option_instance = j.find_class("net/minecraft/client/OptionInstance");
        jclass integer = j.find_class("java/lang/Integer");
        m_fov_ = j.method(options, "fov", "()Lnet/minecraft/client/OptionInstance;");
        m_option_get_ = j.method(option_instance, "get", "()Ljava/lang/Object;");
        m_int_value_ = j.method(integer, "intValue", "()I");

        // The settings a visual module changes are OptionInstance objects on
        // Options, so reading or writing one means boxing through java.lang.
        m_option_set_ = j.method(option_instance, "set", "(Ljava/lang/Object;)V");
        f_opt_fov_ = j.field(options, "fov", "Lnet/minecraft/client/OptionInstance;");
        f_opt_gamma_ = j.field(options, "gamma", "Lnet/minecraft/client/OptionInstance;");
        f_opt_bob_view_ = j.field(options, "bobView", "Lnet/minecraft/client/OptionInstance;");
        f_opt_damage_tilt_ =
            j.field(options, "damageTiltStrength", "Lnet/minecraft/client/OptionInstance;");
        f_opt_render_distance_ =
            j.field(options, "renderDistance", "Lnet/minecraft/client/OptionInstance;");
        c_integer_ = integer;
        c_double_ = j.find_class("java/lang/Double");
        c_boolean_ = j.find_class("java/lang/Boolean");
        m_int_value_of_ = j.static_method(integer, "valueOf", "(I)Ljava/lang/Integer;");
        m_double_value_ = j.method(c_double_, "doubleValue", "()D");
        m_double_value_of_ = j.static_method(c_double_, "valueOf", "(D)Ljava/lang/Double;");
        m_bool_value_ = j.method(c_boolean_, "booleanValue", "()Z");
        m_bool_value_of_ = j.static_method(c_boolean_, "valueOf", "(Z)Ljava/lang/Boolean;");

        // Occlusion culling, the frame timer, and two one-shot actions.
        f_smart_cull_ = j.field(c_minecraft_, "smartCull", "Z");
        f_delta_tracker_ =
            j.field(c_minecraft_, "deltaTracker", "Lnet/minecraft/client/DeltaTracker$Timer;");
        if (jclass timer = j.find_class("net/minecraft/client/DeltaTracker$Timer")) {
            f_ms_per_tick_ = j.field(timer, "msPerTick", "F");
        }
        // Client-side use/attack cooldowns the server does not enforce.
        f_right_click_delay_ = j.field(c_minecraft_, "rightClickDelay", "I");
        f_miss_time_ = j.field(c_minecraft_, "missTime", "I");
        if (jclass game_mode = j.find_class("net/minecraft/client/multiplayer/MultiPlayerGameMode")) {
            f_destroy_delay_ = j.field(game_mode, "destroyDelay", "I");
        }
        m_start_attack_ = j.method(c_minecraft_, "startAttack", "()Z");
        c_local_player_ = j.find_class("net/minecraft/client/player/LocalPlayer");
        m_respawn_ = j.method(c_local_player_, "respawn", "()V");

        for (auto& [id, what] : {std::pair{m_get_instance_ ? 1 : 0, "Minecraft.getInstance()"},
                                 std::pair{f_player_ ? 1 : 0, "Minecraft.player"},
                                 std::pair{f_level_ ? 1 : 0, "Minecraft.level"},
                                 std::pair{m_position_ ? 1 : 0, "Entity.position()"}}) {
            if (!id) missing.emplace_back(what);
        }
        return true;
    }

    // ---- handles ---------------------------------------------------------

    jobject instance(const Env& j) const {
        return j.call_static_obj(c_minecraft_, m_get_instance_);
    }
    jobject player(const Env& j, jobject mc) const { return j.obj_field(mc, f_player_); }
    jobject level(const Env& j, jobject mc) const { return j.obj_field(mc, f_level_); }
    jobject game_mode(const Env& j, jobject mc) const { return j.obj_field(mc, f_game_mode_); }
    /// A screen is open (inventory, chat, pause) — most features should hold off.
    bool screen_open(const Env& j, jobject mc) const {
        jobject s = j.obj_field(mc, f_screen_);
        if (!s) return false;
        j.delete_local(s);
        return true;
    }

    // ---- per-entity reads ------------------------------------------------

    bool position(const Env& j, jobject entity, state::Vec3* out) const {
        jobject v = j.call_obj(entity, m_position_);
        if (!v) return false;
        out->x = j.double_field(v, f_x_);
        out->y = j.double_field(v, f_y_);
        out->z = j.double_field(v, f_z_);
        j.delete_local(v);
        return true;
    }
    float yaw(const Env& j, jobject entity) const { return j.float_field(entity, f_y_rot_); }
    float pitch(const Env& j, jobject entity) const { return j.float_field(entity, f_x_rot_); }
    bool on_ground(const Env& j, jobject entity) const { return j.call_bool(entity, m_on_ground_); }
    bool is_alive(const Env& j, jobject entity) const { return j.call_bool(entity, m_is_alive_); }
    int entity_id(const Env& j, jobject entity) const { return j.call_int(entity, m_get_id_); }
    float health(const Env& j, jobject entity) const { return j.call_float(entity, m_get_health_); }
    float max_health(const Env& j, jobject entity) const {
        return j.call_float(entity, m_get_max_health_);
    }
    bool is_player(const Env& j, jobject entity) const { return j.is_instance(entity, c_player_); }

    /// The rendered field of view, or 0 when it could not be read.
    float fov(const Env& j, jobject mc) const {
        jobject options = j.obj_field(mc, f_options_);
        if (!options) return 0.0f;
        jobject instance = j.call_obj(options, m_fov_);
        if (!instance) return 0.0f;
        jobject boxed = j.call_obj(instance, m_option_get_);
        float out = boxed ? static_cast<float>(j.call_int(boxed, m_int_value_)) : 0.0f;
        j.delete_local(boxed);
        j.delete_local(instance);
        j.delete_local(options);
        return out;
    }

    // ---- options ---------------------------------------------------------
    //
    // Each getter reports whether it could actually read, so a module only ever
    // remembers a previous value it really saw — and therefore only restores
    // one it is sure about.

    /// The OptionInstance a field names, or null. The caller owns the ref.
    jobject option(const Env& j, jobject mc, jfieldID field) const {
        jobject options = j.obj_field(mc, f_options_);
        if (!options) return nullptr;
        jobject opt = j.obj_field(options, field);
        j.delete_local(options);
        return opt;
    }

    bool option_double(const Env& j, jobject mc, jfieldID field, double* out) const {
        jobject opt = option(j, mc, field);
        if (!opt) return false;
        jobject boxed = j.call_obj(opt, m_option_get_);
        bool ok = boxed != nullptr;
        if (ok) *out = j.call_double(boxed, m_double_value_);
        j.delete_local(boxed);
        j.delete_local(opt);
        return ok;
    }
    void set_option_double(const Env& j, jobject mc, jfieldID field, double value) const {
        jobject opt = option(j, mc, field);
        if (!opt) return;
        if (m_option_set_ && m_double_value_of_) {
            if (jobject boxed = j.call_static_obj(c_double_, m_double_value_of_, {jv(value)})) {
                j.call_void(opt, m_option_set_, {jv(boxed)});
                j.delete_local(boxed);
            }
        }
        j.delete_local(opt);
    }

    bool option_int(const Env& j, jobject mc, jfieldID field, int* out) const {
        jobject opt = option(j, mc, field);
        if (!opt) return false;
        jobject boxed = j.call_obj(opt, m_option_get_);
        bool ok = boxed != nullptr;
        if (ok) *out = j.call_int(boxed, m_int_value_);
        j.delete_local(boxed);
        j.delete_local(opt);
        return ok;
    }
    void set_option_int(const Env& j, jobject mc, jfieldID field, int value) const {
        jobject opt = option(j, mc, field);
        if (!opt) return;
        if (m_option_set_ && m_int_value_of_) {
            jobject boxed =
                j.call_static_obj(c_integer_, m_int_value_of_, {jv(static_cast<jint>(value))});
            if (boxed) {
                j.call_void(opt, m_option_set_, {jv(boxed)});
                j.delete_local(boxed);
            }
        }
        j.delete_local(opt);
    }

    bool option_bool(const Env& j, jobject mc, jfieldID field, bool* out) const {
        jobject opt = option(j, mc, field);
        if (!opt) return false;
        jobject boxed = j.call_obj(opt, m_option_get_);
        bool ok = boxed != nullptr;
        if (ok) *out = j.call_bool(boxed, m_bool_value_);
        j.delete_local(boxed);
        j.delete_local(opt);
        return ok;
    }
    void set_option_bool(const Env& j, jobject mc, jfieldID field, bool value) const {
        jobject opt = option(j, mc, field);
        if (!opt) return;
        if (m_option_set_ && m_bool_value_of_) {
            if (jobject boxed = j.call_static_obj(c_boolean_, m_bool_value_of_, {jv(value)})) {
                j.call_void(opt, m_option_set_, {jv(boxed)});
                j.delete_local(boxed);
            }
        }
        j.delete_local(opt);
    }

    bool gamma(const Env& j, jobject mc, double* out) const {
        return option_double(j, mc, f_opt_gamma_, out);
    }
    /// The option is clamped in the settings screen but not on the way in.
    void set_gamma(const Env& j, jobject mc, double v) const {
        set_option_double(j, mc, f_opt_gamma_, v);
    }
    bool damage_tilt(const Env& j, jobject mc, double* out) const {
        return option_double(j, mc, f_opt_damage_tilt_, out);
    }
    void set_damage_tilt(const Env& j, jobject mc, double v) const {
        set_option_double(j, mc, f_opt_damage_tilt_, v);
    }
    bool bob_view(const Env& j, jobject mc, bool* out) const {
        return option_bool(j, mc, f_opt_bob_view_, out);
    }
    void set_bob_view(const Env& j, jobject mc, bool v) const {
        set_option_bool(j, mc, f_opt_bob_view_, v);
    }
    bool fov_option(const Env& j, jobject mc, int* out) const {
        return option_int(j, mc, f_opt_fov_, out);
    }
    void set_fov_option(const Env& j, jobject mc, int v) const {
        set_option_int(j, mc, f_opt_fov_, v);
    }
    bool render_distance(const Env& j, jobject mc, int* out) const {
        return option_int(j, mc, f_opt_render_distance_, out);
    }
    /// Beyond vanilla's own maximum the renderer is off its designed range, so
    /// this asks for no more than that however the slider is set.
    void set_render_distance(const Env& j, jobject mc, int chunks) const {
        int want = chunks < 2 ? 2 : (chunks > 32 ? 32 : chunks);
        set_option_int(j, mc, f_opt_render_distance_, want);
    }

    bool smart_cull(const Env& j, jobject mc, bool* out) const {
        if (!f_smart_cull_) return false;
        *out = j.bool_field(mc, f_smart_cull_);
        return true;
    }
    void set_smart_cull(const Env& j, jobject mc, bool on) const {
        if (f_smart_cull_) j.set_bool(mc, f_smart_cull_, on);
    }

    /// The game runs physics from the timer's msPerTick, so a smaller value is
    /// more ticks per second — everything, including what you send, runs
    /// proportionally faster. 50 ms/tick is normal, i.e. 20 TPS.
    void set_timer(const Env& j, jobject mc, float multiplier) const {
        if (!f_delta_tracker_ || !f_ms_per_tick_) return;
        float m = multiplier < 0.1f ? 0.1f : (multiplier > 10.0f ? 10.0f : multiplier);
        jobject timer = j.obj_field(mc, f_delta_tracker_);
        if (!timer) return;
        j.set_float(timer, f_ms_per_tick_, 50.0f / m);
        j.delete_local(timer);
    }

    /// Clear the client-side gaps between using and between missing. Neither is
    /// enforced by the server, and both refill on their own, so there is
    /// nothing to restore — only a value to keep knocking back down.
    void clear_use_cooldowns(const Env& j, jobject mc) const {
        if (f_right_click_delay_ && j.int_field(mc, f_right_click_delay_) != 0) {
            j.set_int(mc, f_right_click_delay_, 0);
        }
        if (f_miss_time_ && j.int_field(mc, f_miss_time_) != 0) {
            j.set_int(mc, f_miss_time_, 0);
        }
    }

    void set_destroy_delay(const Env& j, jobject game_mode, int v) const {
        if (f_destroy_delay_) j.set_int(game_mode, f_destroy_delay_, v);
    }

    /// One swing, through the game's own path — the same call the mouse makes.
    void start_attack(const Env& j, jobject mc) const {
        if (m_start_attack_) j.call_bool(mc, m_start_attack_);
    }
    void respawn(const Env& j, jobject player) const {
        if (m_respawn_) j.call_void(player, m_respawn_);
    }

    jclass entity_class() const { return c_entity_; }
    jclass living_class() const { return c_living_; }
    jclass minecraft_class() const { return c_minecraft_; }
    jclass player_class() const { return c_player_; }
    jmethodID bounding_box_method() const { return m_bounding_box_; }

  private:
    jclass c_minecraft_ = nullptr, c_entity_ = nullptr, c_living_ = nullptr;
    jclass c_vec3_ = nullptr, c_player_ = nullptr;

    jmethodID m_get_instance_ = nullptr, m_position_ = nullptr, m_on_ground_ = nullptr;
    jmethodID m_is_alive_ = nullptr, m_get_id_ = nullptr, m_bounding_box_ = nullptr;
    jmethodID m_get_health_ = nullptr, m_get_max_health_ = nullptr;

    jmethodID m_fov_ = nullptr, m_option_get_ = nullptr, m_int_value_ = nullptr;

    jfieldID f_player_ = nullptr, f_level_ = nullptr, f_screen_ = nullptr, f_game_mode_ = nullptr;
    jfieldID f_options_ = nullptr;
    jfieldID f_x_ = nullptr, f_y_ = nullptr, f_z_ = nullptr;
    jfieldID f_y_rot_ = nullptr, f_x_rot_ = nullptr;

    jclass c_integer_ = nullptr, c_double_ = nullptr, c_boolean_ = nullptr;
    jclass c_local_player_ = nullptr;
    jmethodID m_option_set_ = nullptr, m_int_value_of_ = nullptr;
    jmethodID m_double_value_ = nullptr, m_double_value_of_ = nullptr;
    jmethodID m_bool_value_ = nullptr, m_bool_value_of_ = nullptr;
    jmethodID m_start_attack_ = nullptr, m_respawn_ = nullptr;
    jfieldID f_opt_fov_ = nullptr, f_opt_gamma_ = nullptr, f_opt_bob_view_ = nullptr;
    jfieldID f_opt_damage_tilt_ = nullptr, f_opt_render_distance_ = nullptr;
    jfieldID f_smart_cull_ = nullptr, f_delta_tracker_ = nullptr, f_ms_per_tick_ = nullptr;
    jfieldID f_right_click_delay_ = nullptr, f_miss_time_ = nullptr, f_destroy_delay_ = nullptr;
};

}  // namespace lodestone::mc