Sign in Sign up
kretrod/lodestone-cpp Public
Branches
main
205 lines (181 loc) · 9.4 KB Raw
// Inventory and item bindings: what is in your hands, what is in the bag, and
// how to get one into the other.
//
// Placing blocks is deliberately not here yet — it needs a BlockHitResult built
// from a BlockPos and a Direction, and getting that wrong is worse than not
// placing at all, so it waits until those are confirmed too.
#pragma once

#include <string>
#include <vector>

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

namespace lodestone::mc {

class Interact {
  public:
    bool resolve(const Env& j, const Mc& core, std::vector<std::string>& missing) {
        jclass items = j.find_class("net/minecraft/world/item/Items");
        jclass hand = j.find_class("net/minecraft/world/InteractionHand");
        c_inventory_ = j.find_class("net/minecraft/world/entity/player/Inventory");
        c_stack_ = j.find_class("net/minecraft/world/item/ItemStack");
        c_end_crystal_ = j.find_class("net/minecraft/world/entity/boss/enderdragon/EndCrystal");
        jclass player = j.find_class("net/minecraft/world/entity/player/Player");
        jclass living = core.living_class();

        auto item_constant = [&](const char* name) -> jobject {
            if (!items) return nullptr;
            jfieldID f = j.static_field(items, name, "Lnet/minecraft/world/item/Item;");
            jobject local = j.static_obj_field(items, f);
            return local ? j.global(local) : nullptr;
        };
        auto hand_constant = [&](const char* name) -> jobject {
            if (!hand) return nullptr;
            jfieldID f = j.static_field(hand, name, "Lnet/minecraft/world/InteractionHand;");
            jobject local = j.static_obj_field(hand, f);
            return local ? j.global(local) : nullptr;
        };

        totem_ = item_constant("TOTEM_OF_UNDYING");
        shield_ = item_constant("SHIELD");
        end_crystal_ = item_constant("END_CRYSTAL");
        mace_ = item_constant("MACE");
        main_hand_ = hand_constant("MAIN_HAND");
        off_hand_ = hand_constant("OFF_HAND");
        if (!totem_ || !end_crystal_ || !mace_) missing.emplace_back("Items constants");

        m_get_inventory_ =
            j.method(player, "getInventory", "()Lnet/minecraft/world/entity/player/Inventory;");
        m_inv_get_item_ =
            j.method(c_inventory_, "getItem", "(I)Lnet/minecraft/world/item/ItemStack;");
        m_get_selected_ = j.method(c_inventory_, "getSelectedSlot", "()I");
        m_set_selected_ = j.method(c_inventory_, "setSelectedSlot", "(I)V");
        m_get_item_in_hand_ =
            j.method(living, "getItemInHand",
                     "(Lnet/minecraft/world/InteractionHand;)Lnet/minecraft/world/item/ItemStack;");
        m_stack_get_item_ = j.method(c_stack_, "getItem", "()Lnet/minecraft/world/item/Item;");
        m_stack_is_empty_ = j.method(c_stack_, "isEmpty", "()Z");
        // Raising a shield and swapping into the offhand both go through the
        // game's own paths, so what reaches the server is an ordinary action.
        c_game_mode_ = j.find_class("net/minecraft/client/multiplayer/MultiPlayerGameMode");
        m_start_use_item_ = j.method(core.minecraft_class(), "startUseItem", "()V");
        // 26.2 renamed ClickType to ContainerInput.
        m_container_input_ = j.method(c_game_mode_, "handleContainerInput",
                                      "(IIILnet/minecraft/world/inventory/ContainerInput;"
                                      "Lnet/minecraft/world/entity/player/Player;)V");
        if (jclass container_input = j.find_class("net/minecraft/world/inventory/ContainerInput")) {
            jfieldID f = j.static_field(container_input, "SWAP",
                                        "Lnet/minecraft/world/inventory/ContainerInput;");
            jobject local = j.static_obj_field(container_input, f);
            swap_input_ = local ? j.global(local) : nullptr;
        }
        if (!m_start_use_item_) missing.emplace_back("Minecraft.startUseItem()");
        if (!m_container_input_ || !swap_input_) {
            missing.emplace_back("MultiPlayerGameMode.handleContainerInput");
        }

        if (!m_get_inventory_ || !m_inv_get_item_) missing.emplace_back("Inventory access");
        return true;
    }

    // ---- what is held ----------------------------------------------------

    bool holding_totem(const Env& j, jobject player) const {
        return hand_is(j, player, off_hand_, totem_) || hand_is(j, player, main_hand_, totem_);
    }
    bool holding_shield(const Env& j, jobject player) const {
        return hand_is(j, player, off_hand_, shield_) || hand_is(j, player, main_hand_, shield_);
    }
    bool holding_mace(const Env& j, jobject player) const {
        return hand_is(j, player, main_hand_, mace_);
    }

    // ---- what is in the bag ----------------------------------------------

    /// The first slot holding `item`, searching `slots` of them.
    ///
    /// Anything that has to be *held* searches only the nine hotbar slots,
    /// because selecting is all we can do without opening the inventory; the
    /// totem goes to the offhand through a container click, so it may come from
    /// anywhere.
    int find_item(const Env& j, jobject player, jobject item, int slots) const {
        if (!item || !m_get_inventory_ || !m_inv_get_item_) return -1;
        jobject inventory = j.call_obj(player, m_get_inventory_);
        if (!inventory) return -1;
        int found = -1;
        for (int slot = 0; slot < slots; ++slot) {
            jobject stack = j.call_obj(inventory, m_inv_get_item_, {jv(static_cast<jint>(slot))});
            if (!stack) continue;
            bool is = stack_is(j, stack, item);
            j.delete_local(stack);
            if (is) {
                found = slot;
                break;
            }
        }
        j.delete_local(inventory);
        return found;
    }

    int find_totem(const Env& j, jobject player) const { return find_item(j, player, totem_, 36); }
    int find_crystal(const Env& j, jobject player) const {
        return find_item(j, player, end_crystal_, 9);
    }
    int find_mace(const Env& j, jobject player) const { return find_item(j, player, mace_, 9); }

    /// Which hotbar slot is in hand, or -1.
    int selected_slot(const Env& j, jobject player) const {
        if (!m_get_inventory_ || !m_get_selected_) return -1;
        jobject inventory = j.call_obj(player, m_get_inventory_);
        if (!inventory) return -1;
        int slot = j.call_int(inventory, m_get_selected_);
        j.delete_local(inventory);
        return slot;
    }

    /// Put a hotbar slot in hand. The client tells the server with its own
    /// held-item packet on the next tick, so this is indistinguishable from
    /// scrolling.
    void select_slot(const Env& j, jobject player, int slot) const {
        if (slot < 0 || slot > 8 || !m_get_inventory_ || !m_set_selected_) return;
        jobject inventory = j.call_obj(player, m_get_inventory_);
        if (!inventory) return;
        j.call_void(inventory, m_set_selected_, {jv(static_cast<jint>(slot))});
        j.delete_local(inventory);
    }

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

    /// Start using whatever is in hand — which is how a shield goes up.
    void start_use(const Env& j, jobject instance) const {
        j.call_void(instance, m_start_use_item_);
    }

    /// Move an inventory slot into the offhand through the container input the
    /// game itself uses, so the server sees an ordinary inventory action rather
    /// than an item appearing in a hand it never travelled to.
    void swap_to_offhand(const Env& j, jobject game_mode, jobject player, int slot) const {
        if (!m_container_input_ || !swap_input_ || slot < 0) return;
        // The player's own container numbers the hotbar 36..44 and the rest
        // 9..35; the offhand is button 40.
        jint container_slot = slot < 9 ? slot + 36 : slot;
        j.call_void(game_mode, m_container_input_,
                    {jv(static_cast<jint>(0)), jv(container_slot), jv(static_cast<jint>(40)),
                     jv(swap_input_), jv(player)});
    }

  private:
    bool stack_is(const Env& j, jobject stack, jobject item) const {
        if (!item || !m_stack_get_item_ || !m_stack_is_empty_) return false;
        if (j.call_bool(stack, m_stack_is_empty_)) return false;
        jobject held = j.call_obj(stack, m_stack_get_item_);
        if (!held) return false;
        bool same = j.same_object(held, item);
        j.delete_local(held);
        return same;
    }

    bool hand_is(const Env& j, jobject player, jobject hand, jobject item) const {
        if (!hand || !m_get_item_in_hand_) return false;
        jobject stack = j.call_obj(player, m_get_item_in_hand_, {jv(hand)});
        if (!stack) return false;
        bool is = stack_is(j, stack, item);
        j.delete_local(stack);
        return is;
    }

    jclass c_inventory_ = nullptr, c_stack_ = nullptr, c_end_crystal_ = nullptr;
    jobject totem_ = nullptr, shield_ = nullptr, end_crystal_ = nullptr, mace_ = nullptr;
    jobject main_hand_ = nullptr, off_hand_ = nullptr;

    jmethodID m_get_inventory_ = nullptr, m_inv_get_item_ = nullptr;
    jmethodID m_get_selected_ = nullptr, m_set_selected_ = nullptr;
    jmethodID m_get_item_in_hand_ = nullptr, m_stack_get_item_ = nullptr;
    jmethodID m_stack_is_empty_ = nullptr;

    jclass c_game_mode_ = nullptr;
    jmethodID m_start_use_item_ = nullptr, m_container_input_ = nullptr;
    jobject swap_input_ = nullptr;  ///< the SWAP constant, pinned
};

}  // namespace lodestone::mc