Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
459 lines (406 loc) · 21.2 KB Raw
// World bindings: finding entities, reading them, and hitting them.
//
// Everything optional is reported through `missing` rather than failing, so a
// rename in a future version costs the one feature that used it. Signatures
// were checked against the decompiled 26.2 classes — 26.2 moved a fair amount,
// so assuming older shapes would quietly resolve nothing.
#pragma once

#include <string>
#include <vector>

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

namespace lodestone::mc {

class World {
  public:
    bool resolve(const Env& j, const Mc& core, std::vector<std::string>& missing) {
        auto need = [&](jmethodID id, const char* what) {
            if (!id) missing.emplace_back(what);
            return id;
        };

        c_client_level_ = j.find_class("net/minecraft/client/multiplayer/ClientLevel");
        c_iterable_ = j.find_class("java/lang/Iterable");
        c_iterator_ = j.find_class("java/util/Iterator");
        c_aabb_ = j.find_class("net/minecraft/world/phys/AABB");
        c_component_ = j.find_class("net/minecraft/network/chat/Component");
        c_game_mode_ = j.find_class("net/minecraft/client/multiplayer/MultiPlayerGameMode");
        c_listener_ = j.find_class("net/minecraft/client/multiplayer/ClientPacketListener");
        c_player_info_ = j.find_class("net/minecraft/client/multiplayer/PlayerInfo");
        c_local_player_ = j.find_class("net/minecraft/client/player/LocalPlayer");
        // Classification. Each is optional: losing one costs that category of
        // ESP rather than the scan.
        c_item_entity_ = j.find_class("net/minecraft/world/entity/item/ItemEntity");
        c_monster_ = j.find_class("net/minecraft/world/entity/monster/Monster");
        c_animal_ = j.find_class("net/minecraft/world/entity/animal/Animal");

        m_entities_ = need(j.method(c_client_level_, "entitiesForRendering", "()Ljava/lang/Iterable;"),
                           "ClientLevel.entitiesForRendering()");
        m_iterator_ = need(j.method(c_iterable_, "iterator", "()Ljava/util/Iterator;"),
                           "Iterable.iterator()");
        m_has_next_ = need(j.method(c_iterator_, "hasNext", "()Z"), "Iterator.hasNext()");
        m_next_ = need(j.method(c_iterator_, "next", "()Ljava/lang/Object;"), "Iterator.next()");

        if (c_aabb_) {
            f_min_x_ = j.field(c_aabb_, "minX", "D");
            f_min_y_ = j.field(c_aabb_, "minY", "D");
            f_min_z_ = j.field(c_aabb_, "minZ", "D");
            f_max_x_ = j.field(c_aabb_, "maxX", "D");
            f_max_y_ = j.field(c_aabb_, "maxY", "D");
            f_max_z_ = j.field(c_aabb_, "maxZ", "D");
        }
        m_bounding_box_ = core.bounding_box_method();

        jclass entity = core.entity_class();
        m_get_name_ = j.method(entity, "getName", "()Lnet/minecraft/network/chat/Component;");
        m_get_string_ = j.method(c_component_, "getString", "()Ljava/lang/String;");
        m_get_uuid_ = j.method(entity, "getUUID", "()Ljava/util/UUID;");
        m_is_invisible_ = j.method(entity, "isInvisible", "()Z");

        m_attack_ = need(
            j.method(c_game_mode_, "attack",
                     "(Lnet/minecraft/world/entity/player/Player;Lnet/minecraft/world/entity/"
                     "Entity;)V"),
            "MultiPlayerGameMode.attack(Player,Entity)");

        f_connection_ = j.field(c_local_player_, "connection",
                                "Lnet/minecraft/client/multiplayer/ClientPacketListener;");

        // Weather is four interpolated floats on the level. Zeroing all four —
        // including the previous-tick copies, or it flickers — clears the sky.
        if (c_client_level_) {
            f_rain_ = j.field(c_client_level_, "rainLevel", "F");
            f_thunder_ = j.field(c_client_level_, "thunderLevel", "F");
            f_o_rain_ = j.field(c_client_level_, "oRainLevel", "F");
            f_o_thunder_ = j.field(c_client_level_, "oThunderLevel", "F");
        }
        m_get_player_info_ =
            j.method(c_listener_, "getPlayerInfo",
                     "(Ljava/util/UUID;)Lnet/minecraft/client/multiplayer/PlayerInfo;");
        m_get_latency_ = j.method(c_player_info_, "getLatency", "()I");

        c_living_ = core.living_class();
        if (c_living_) {
            m_attribute_value_ =
                j.method(c_living_, "getAttributeValue", "(Lnet/minecraft/core/Holder;)D");
            m_attr_instance_ = j.method(
                c_living_, "getAttribute",
                "(Lnet/minecraft/core/Holder;)Lnet/minecraft/world/entity/ai/attributes/"
                "AttributeInstance;");
            m_is_using_item_ = j.method(c_living_, "isUsingItem", "()Z");
            m_ticks_using_ = j.method(c_living_, "getTicksUsingItem", "()I");
            f_hurt_time_ = j.field(c_living_, "hurtTime", "I");
            // What someone is carrying and wearing.
            m_main_hand_ =
                j.method(c_living_, "getMainHandItem", "()Lnet/minecraft/world/item/ItemStack;");
            m_armor_value_ = j.method(c_living_, "getArmorValue", "()I");
            m_active_effects_ = j.method(c_living_, "getActiveEffects", "()Ljava/util/Collection;");
        }
        if (jclass stack = j.find_class("net/minecraft/world/item/ItemStack")) {
            m_hover_name_ =
                j.method(stack, "getHoverName", "()Lnet/minecraft/network/chat/Component;");
            m_stack_empty_ = j.method(stack, "isEmpty", "()Z");
        }
        if (jclass effect = j.find_class("net/minecraft/world/effect/MobEffectInstance")) {
            m_effect_desc_ = j.method(effect, "getDescriptionId", "()Ljava/lang/String;");
        }
        if (jclass collection = j.find_class("java/util/Collection")) {
            m_coll_iter_ = j.method(collection, "iterator", "()Ljava/util/Iterator;");
        }

        // Reach and step height are attributes rather than plain fields, so they
        // are read and written through the AttributeInstance a holder names.
        c_attribute_instance_ =
            j.find_class("net/minecraft/world/entity/ai/attributes/AttributeInstance");
        if (c_attribute_instance_) {
            m_attr_set_base_ = j.method(c_attribute_instance_, "setBaseValue", "(D)V");
            m_attr_get_base_ = j.method(c_attribute_instance_, "getBaseValue", "()D");
        }
        jclass attributes = j.find_class("net/minecraft/world/entity/ai/attributes/Attributes");
        auto holder = [&](const char* name) -> jobject {
            if (!attributes) return nullptr;
            jfieldID f = j.static_field(attributes, name, "Lnet/minecraft/core/Holder;");
            jobject local = j.static_obj_field(attributes, f);
            return local ? j.global(local) : nullptr;  // pinned: used every frame
        };
        holder_entity_reach_ = holder("ENTITY_INTERACTION_RANGE");
        holder_block_reach_ = holder("BLOCK_INTERACTION_RANGE");
        holder_step_height_ = holder("STEP_HEIGHT");

        // What the crosshair is resting on, which is all the trigger bot needs.
        f_hit_result_ =
            j.field(core.minecraft_class(), "hitResult", "Lnet/minecraft/world/phys/HitResult;");
        c_entity_hit_ = j.find_class("net/minecraft/world/phys/EntityHitResult");
        m_hit_get_entity_ =
            j.method(c_entity_hit_, "getEntity", "()Lnet/minecraft/world/entity/Entity;");

        // Private on Player, so calling it needs a non-virtual call — but it is
        // the exact predicate the server will evaluate, which beats
        // reimplementing it and disagreeing at the edges.
        c_player_ = core.player_class();
        m_can_critical_ =
            j.method(c_player_, "canCriticalAttack", "(Lnet/minecraft/world/entity/Entity;)Z");

        if (entity) {
            f_no_physics_ = j.field(entity, "noPhysics", "Z");
            f_horizontal_collision_ = j.field(entity, "horizontalCollision", "Z");
        }

        return m_entities_ && m_iterator_ && m_next_;
    }

    // ---- iteration -------------------------------------------------------

    /// An iterator over everything the client is rendering.
    jobject entity_iterator(const Env& j, jobject level) const {
        jobject iterable = j.call_obj(level, m_entities_);
        if (!iterable) return nullptr;
        jobject it = j.call_obj(iterable, m_iterator_);
        j.delete_local(iterable);
        return it;
    }

    /// The next entity, or null when the iterator is spent.
    jobject iter_next(const Env& j, jobject iterator) const {
        if (!j.call_bool(iterator, m_has_next_)) return nullptr;
        return j.call_obj(iterator, m_next_);
    }

    // ---- reads -----------------------------------------------------------

    bool bounding_box(const Env& j, jobject entity, state::Vec3* min, state::Vec3* max) const {
        jobject box = j.call_obj(entity, m_bounding_box_);
        if (!box) return false;
        min->x = j.double_field(box, f_min_x_);
        min->y = j.double_field(box, f_min_y_);
        min->z = j.double_field(box, f_min_z_);
        max->x = j.double_field(box, f_max_x_);
        max->y = j.double_field(box, f_max_y_);
        max->z = j.double_field(box, f_max_z_);
        j.delete_local(box);
        return true;
    }

    bool is_invisible(const Env& j, jobject entity) const {
        return j.call_bool(entity, m_is_invisible_);
    }

    /// The display name, flattened to plain text.
    std::string name(const Env& j, jobject entity) const {
        jobject component = j.call_obj(entity, m_get_name_);
        if (!component) return {};
        jobject s = j.call_obj(component, m_get_string_);
        std::string out = j.to_string(s);
        j.delete_local(s);
        j.delete_local(component);
        return out;
    }

    state::TargetKind classify(const Env& j, const Mc& core, jobject entity) const {
        if (core.is_player(j, entity)) return state::TargetKind::Player;
        if (c_item_entity_ && j.is_instance(entity, c_item_entity_)) return state::TargetKind::Item;
        if (c_monster_ && j.is_instance(entity, c_monster_)) return state::TargetKind::Mob;
        if (c_animal_ && j.is_instance(entity, c_animal_)) return state::TargetKind::Animal;
        return state::TargetKind::Other;
    }

    /// Does the server actually list this entity as a person?
    ///
    /// Everything extending `Player` classifies as one, and server-side NPCs,
    /// holograms and duplicate "clone" entities are all built from that class —
    /// so they show up in the ESP wearing a player's colour. A real player has
    /// an entry in the client's player-info (tab) map, keyed by UUID; those
    /// fakes do not. `known` comes back false when we could not tell, and the
    /// caller then leaves the classification alone rather than hide someone
    /// real.
    bool tab_player(const Env& j, jobject local_player, jobject entity, bool* known) const {
        *known = false;
        if (!m_get_uuid_ || !f_connection_ || !m_get_player_info_) return true;
        jobject uuid = j.call_obj(entity, m_get_uuid_);
        if (!uuid) return true;
        jobject connection = j.obj_field(local_player, f_connection_);
        if (!connection) {
            j.delete_local(uuid);
            return true;
        }
        jobject info = j.call_obj(connection, m_get_player_info_, {jv(uuid)});
        j.delete_local(uuid);
        *known = true;
        if (!info) return false;  // listed nowhere: not a person
        j.delete_local(info);
        return true;
    }

    /// Round-trip time the server reports for this player, or -1.
    int ping_of(const Env& j, jobject local_player, jobject entity) const {
        if (!m_get_uuid_ || !f_connection_ || !m_get_player_info_ || !m_get_latency_) return -1;
        jobject uuid = j.call_obj(entity, m_get_uuid_);
        if (!uuid) return -1;
        jobject connection = j.obj_field(local_player, f_connection_);
        if (!connection) {
            j.delete_local(uuid);
            return -1;
        }
        jobject info = j.call_obj(connection, m_get_player_info_, {jv(uuid)});
        j.delete_local(uuid);
        if (!info) return -1;
        int ping = j.call_int(info, m_get_latency_);
        j.delete_local(info);
        return ping;
    }

    /// An attribute's current value — what actually decides reach, rather than
    /// a guess at vanilla's three blocks.
    double attribute(const Env& j, jobject entity, jobject holder) const {
        if (!m_attribute_value_ || !holder) return 0.0;
        return j.call_double(entity, m_attribute_value_, {jv(holder)});
    }

    /// An attribute's *base* value, before modifiers. Unlike the computed value
    /// above this is the one that can be written back.
    bool attribute_base(const Env& j, jobject entity, jobject holder, double* out) const {
        if (!m_attr_instance_ || !m_attr_get_base_ || !holder) return false;
        jobject inst = j.call_obj(entity, m_attr_instance_, {jv(holder)});
        if (!inst) return false;
        *out = j.call_double(inst, m_attr_get_base_);
        j.delete_local(inst);
        return true;
    }

    bool set_attribute_base(const Env& j, jobject entity, jobject holder, double value) const {
        if (!m_attr_instance_ || !m_attr_set_base_ || !holder) return false;
        jobject inst = j.call_obj(entity, m_attr_instance_, {jv(holder)});
        if (!inst) return false;
        j.call_void(inst, m_attr_set_base_, {jv(value)});
        j.delete_local(inst);
        return true;
    }

    jobject holder_entity_reach() const { return holder_entity_reach_; }
    jobject holder_block_reach() const { return holder_block_reach_; }
    jobject holder_step_height() const { return holder_step_height_; }

    bool is_living(const Env& j, jobject entity) const {
        return c_living_ && j.is_instance(entity, c_living_);
    }

    int hurt_time(const Env& j, jobject entity) const {
        return f_hurt_time_ ? j.int_field(entity, f_hurt_time_) : 0;
    }

    /// Walking into something — what Spider climbs on.
    bool hitting_wall(const Env& j, jobject entity) const {
        return f_horizontal_collision_ && j.bool_field(entity, f_horizontal_collision_);
    }

    void set_no_physics(const Env& j, jobject entity, bool on) const {
        if (f_no_physics_) j.set_bool(entity, f_no_physics_, on);
    }

    void set_weather(const Env& j, jobject level, float value) const {
        for (jfieldID f : {f_rain_, f_thunder_, f_o_rain_, f_o_thunder_}) {
            if (f) j.set_float(level, f, value);
        }
    }

    /// What someone is carrying, wearing, and running on: the held item's name,
    /// the armour value, and the active effects. Every part is an optional
    /// lookup, so a rename costs that line rather than the whole scan.
    void entity_intel(const Env& j, jobject entity, std::string* held, int* armor,
                      std::vector<std::string>* effects) const {
        if (m_main_hand_) {
            if (jobject stack = j.call_obj(entity, m_main_hand_)) {
                bool empty = m_stack_empty_ ? j.call_bool(stack, m_stack_empty_) : true;
                if (!empty && m_hover_name_) {
                    if (jobject component = j.call_obj(stack, m_hover_name_)) {
                        jobject s = j.call_obj(component, m_get_string_);
                        *held = j.to_string(s);
                        j.delete_local(s);
                        j.delete_local(component);
                    }
                }
                j.delete_local(stack);
            }
        }
        if (m_armor_value_) *armor = j.call_int(entity, m_armor_value_);

        if (!m_active_effects_ || !m_coll_iter_ || !m_effect_desc_) return;
        jobject coll = j.call_obj(entity, m_active_effects_);
        if (!coll) return;
        if (jobject it = j.call_obj(coll, m_coll_iter_)) {
            // Bounded: a long stack of effects is not worth a frame.
            for (int n = 0; n < 16 && j.call_bool(it, m_has_next_); ++n) {
                jobject effect = j.call_obj(it, m_next_);
                if (!effect) break;
                jobject s = j.call_obj(effect, m_effect_desc_);
                std::string id = j.to_string(s);
                j.delete_local(s);
                // "effect.minecraft.speed" reads better as just "speed".
                size_t dot = id.find_last_of('.');
                if (dot != std::string::npos) id = id.substr(dot + 1);
                if (!id.empty()) effects->push_back(id);
                j.delete_local(effect);
            }
            j.delete_local(it);
        }
        j.delete_local(coll);
    }

    /// The entity under the crosshair, or null. The caller owns the reference.
    jobject crosshair_entity(const Env& j, jobject instance) const {
        if (!f_hit_result_ || !c_entity_hit_ || !m_hit_get_entity_) return nullptr;
        jobject hit = j.obj_field(instance, f_hit_result_);
        if (!hit) return nullptr;
        if (!j.is_instance(hit, c_entity_hit_)) {  // a block, or nothing at all
            j.delete_local(hit);
            return nullptr;
        }
        jobject e = j.call_obj(hit, m_hit_get_entity_);
        j.delete_local(hit);
        return e;
    }

    /// Whether the game agrees this swing would land as a critical. Asking it
    /// the question the server will ask beats reimplementing the rule.
    bool can_critical(const Env& j, jobject player, jobject target) const {
        return j.call_nonvirtual_bool(player, c_player_, m_can_critical_, {jv(target)});
    }

    /// Charge of a bow being drawn, 0..1, or -1 if nothing is being used.
    /// Vanilla: t = ticksUsing/20, power = (t² + 2t)/3, capped at one.
    float bow_charge(const Env& j, jobject player) const {
        if (!m_is_using_item_ || !m_ticks_using_) return -1.0f;
        if (!j.call_bool(player, m_is_using_item_)) return -1.0f;
        float t = static_cast<float>(j.call_int(player, m_ticks_using_)) / 20.0f;
        float power = (t * t + t * 2.0f) / 3.0f;
        return power > 1.0f ? 1.0f : power;
    }

    /// Nearest living thing to the eye within range, or null. The caller owns
    /// the reference.
    jobject nearest_living(const Env& j, const Mc& core, jobject level, jobject player,
                           const state::Vec3& eye, double range) const {
        jobject iterator = entity_iterator(j, level);
        if (!iterator) return nullptr;
        jobject best = nullptr;
        double best_d = range * range;  // compared squared, so no square roots
        for (int n = 0; n < 4096; ++n) {
            jobject e = iter_next(j, iterator);
            if (!e) break;
            bool keep = false;
            if (!j.same_object(e, player) && is_living(j, e) && core.is_alive(j, e)) {
                state::Vec3 lo, hi;
                if (bounding_box(j, e, &lo, &hi)) {
                    double dx = (lo.x + hi.x) * 0.5 - eye.x;
                    double dy = (lo.y + hi.y) * 0.5 - eye.y;
                    double dz = (lo.z + hi.z) * 0.5 - eye.z;
                    double d = dx * dx + dy * dy + dz * dz;
                    if (d < best_d) {
                        best_d = d;
                        if (best) j.delete_local(best);
                        best = e;
                        keep = true;
                    }
                }
            }
            if (!keep) j.delete_local(e);
        }
        j.delete_local(iterator);
        return best;
    }

    // ---- combat ----------------------------------------------------------

    void attack(const Env& j, jobject game_mode, jobject player, jobject target) const {
        j.call_void(game_mode, m_attack_, {jv(player), jv(target)});
    }

  private:
    jclass c_client_level_ = nullptr, c_iterable_ = nullptr, c_iterator_ = nullptr;
    jclass c_aabb_ = nullptr, c_component_ = nullptr, c_game_mode_ = nullptr;
    jclass c_listener_ = nullptr, c_player_info_ = nullptr, c_local_player_ = nullptr;
    jclass c_item_entity_ = nullptr, c_monster_ = nullptr, c_animal_ = nullptr;

    jmethodID m_entities_ = nullptr, m_iterator_ = nullptr, m_has_next_ = nullptr;
    jmethodID m_next_ = nullptr, m_bounding_box_ = nullptr, m_get_name_ = nullptr;
    jmethodID m_get_string_ = nullptr, m_get_uuid_ = nullptr, m_is_invisible_ = nullptr;
    jmethodID m_attack_ = nullptr, m_get_player_info_ = nullptr, m_get_latency_ = nullptr;
    jmethodID m_attribute_value_ = nullptr;

    jfieldID f_min_x_ = nullptr, f_min_y_ = nullptr, f_min_z_ = nullptr;
    jfieldID f_max_x_ = nullptr, f_max_y_ = nullptr, f_max_z_ = nullptr;
    jfieldID f_connection_ = nullptr;

    jclass c_attribute_instance_ = nullptr, c_entity_hit_ = nullptr;
    jclass c_player_ = nullptr, c_living_ = nullptr;

    jmethodID m_attr_instance_ = nullptr, m_attr_set_base_ = nullptr, m_attr_get_base_ = nullptr;
    jmethodID m_hit_get_entity_ = nullptr, m_can_critical_ = nullptr;
    jmethodID m_is_using_item_ = nullptr, m_ticks_using_ = nullptr;

    /// Pinned globals: looked up once, read every frame.
    jobject holder_entity_reach_ = nullptr, holder_block_reach_ = nullptr;
    jobject holder_step_height_ = nullptr;

    jfieldID f_hit_result_ = nullptr, f_hurt_time_ = nullptr;
    jfieldID f_no_physics_ = nullptr, f_horizontal_collision_ = nullptr;
    jfieldID f_rain_ = nullptr, f_thunder_ = nullptr;
    jfieldID f_o_rain_ = nullptr, f_o_thunder_ = nullptr;

    jmethodID m_main_hand_ = nullptr, m_hover_name_ = nullptr, m_stack_empty_ = nullptr;
    jmethodID m_armor_value_ = nullptr, m_active_effects_ = nullptr;
    jmethodID m_effect_desc_ = nullptr, m_coll_iter_ = nullptr;
};

}  // namespace lodestone::mc