1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// 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