Skip to content

Commit b281a2d

Browse files
committed
add support for attribute swapping to and from items with attack range components
1 parent 7d487a4 commit b281a2d

File tree

2 files changed

+64
-39
lines changed

2 files changed

+64
-39
lines changed

common/src/main/java/ac/grim/grimac/checks/impl/combat/Reach.java

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
4545
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
4646
import org.jetbrains.annotations.NotNull;
47+
import org.jetbrains.annotations.Nullable;
4748

4849
import java.util.ArrayList;
4950
import java.util.Arrays;
@@ -112,14 +113,46 @@ public void onPacketReceive(final PacketReceiveEvent event) {
112113

113114
InteractionHand hand = action.getAction() == WrapperPlayClientInteractEntity.InteractAction.ATTACK ?
114115
InteractionHand.MAIN_HAND : action.getHand(); // attacks can be only performed with the main hand
115-
ItemStack itemInHand = player.inventory.getItemInHand(hand);
116+
117+
ItemStack currentStack = player.inventory.getItemInHand(hand);
118+
ItemStack startStack = player.inventory.getStartOfTickStack();
119+
120+
boolean hasRange = false;
121+
float maxReach = 0f;
122+
float hitboxMargin = 0f;
123+
124+
if (ATTACK_RANGE_COMPONENT_EXISTS) {
125+
ItemAttackRange startRange = startStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
126+
127+
// If the start stack has no range component, the client defaults to vanilla reach behavior,
128+
// regardless of what the current stack is (No Range -> X = No Range used).
129+
if (startRange != null) {
130+
ItemAttackRange currentRange = currentStack.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
131+
if (currentRange == null) {
132+
// Range (Start) -> No Range (Current)
133+
// Client logic uses Start Range
134+
hasRange = true;
135+
maxReach = startRange.getMaxRange();
136+
hitboxMargin = startRange.getHitboxMargin();
137+
} else {
138+
// Range (Start) -> Range (Current)
139+
// Client logic requires satisfying BOTH constraints
140+
hasRange = true;
141+
maxReach = Math.min(startRange.getMaxRange(), currentRange.getMaxRange());
142+
hitboxMargin = Math.min(startRange.getHitboxMargin(), currentRange.getHitboxMargin());
143+
}
144+
}
145+
}
116146

117147
boolean tooManyAttacks = playerAttackQueue.size() > 10;
118148
if (!tooManyAttacks) {
119-
playerAttackQueue.put(action.getEntityId(), new InteractionData(new Vector3d(player.x, player.y, player.z), itemInHand)); // Queue for next tick for very precise check
149+
playerAttackQueue.put(action.getEntityId(), new InteractionData(
150+
player.x, player.y, player.z,
151+
hasRange, maxReach, hitboxMargin
152+
)); // Queue for next tick for very precise check
120153
}
121154

122-
boolean knownInvalid = isKnownInvalid(entity, itemInHand);
155+
boolean knownInvalid = isKnownInvalid(entity, hasRange, maxReach, hitboxMargin);
123156

124157
if ((shouldModifyPackets() && cancelImpossibleHits && knownInvalid) || tooManyAttacks) {
125158
event.setCancelled(true);
@@ -141,7 +174,7 @@ public void onPacketReceive(final PacketReceiveEvent event) {
141174
// than this method. If this method flags, the other method WILL flag.
142175
//
143176
// Meaning that the other check should be the only one that flags.
144-
private boolean isKnownInvalid(PacketEntity reachEntity, ItemStack itemInHand) {
177+
private boolean isKnownInvalid(PacketEntity reachEntity, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin) {
145178
// If the entity doesn't exist, or if it is exempt, or if it is dead
146179
if ((blacklisted.contains(reachEntity.type) || !reachEntity.isLivingEntity) && reachEntity.type != EntityTypes.END_CRYSTAL)
147180
return false; // exempt
@@ -152,12 +185,12 @@ private boolean isKnownInvalid(PacketEntity reachEntity, ItemStack itemInHand) {
152185

153186
// Filter out what we assume to be cheats
154187
if (cancelBuffer != 0) {
155-
CheckResult result = checkReach(reachEntity, new Vector3d(player.x, player.y, player.z), itemInHand, true);
188+
CheckResult result = checkReach(reachEntity, player.x, player.y, player.z, hasAttackRange, itemMaxReach, itemHitboxMargin, true);
156189
return result.isFlag(); // If they flagged
157190
} else {
158191
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
159192

160-
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastMovementIncludePosition);
193+
double maxReach = applyReachModifiers(targetBox, hasAttackRange, itemMaxReach, itemHitboxMargin, !player.packetStateData.didLastMovementIncludePosition);
161194

162195
return ReachUtils.getMinReachToBox(player, targetBox) > maxReach;
163196
}
@@ -169,7 +202,7 @@ private void tickBetterReachCheckWithAngle() {
169202
if (reachEntity == null) continue;
170203

171204
InteractionData interactionData = attack.getValue();
172-
CheckResult result = checkReach(reachEntity, interactionData.vector(), interactionData.itemInHand(), false);
205+
CheckResult result = checkReach(reachEntity, interactionData.x, interactionData.y, interactionData.z, interactionData.hasAttackRange, interactionData.maxReach, interactionData.hitboxMargin, false);
173206
switch (result.type()) {
174207
case REACH -> {
175208
String added = ", type=" + reachEntity.type.getName().getKey();
@@ -192,10 +225,10 @@ private void tickBetterReachCheckWithAngle() {
192225
}
193226

194227
@NotNull
195-
private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, ItemStack itemInHand, boolean isPrediction) {
228+
private CheckResult checkReach(PacketEntity reachEntity, double x, double y, double z, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin, boolean isPrediction) {
196229
SimpleCollisionBox targetBox = getTargetBox(reachEntity);
197230

198-
double maxReach = applyReachModifiers(targetBox, itemInHand, !player.packetStateData.didLastLastMovementIncludePosition);
231+
double maxReach = applyReachModifiers(targetBox, hasAttackRange, itemMaxReach, itemHitboxMargin, !player.packetStateData.didLastLastMovementIncludePosition);
199232
double minDistance = Double.MAX_VALUE;
200233

201234
// https://bugs.mojang.com/browse/MC-67665
@@ -221,10 +254,10 @@ private CheckResult checkReach(PacketEntity reachEntity, Vector3d from, ItemStac
221254

222255

223256
final double[] possibleEyeHeights = player.getPossibleEyeHeights();
224-
final Vector3dm eyePos = new Vector3dm(from.getX(), 0, from.getZ());
257+
final Vector3dm eyePos = new Vector3dm(x, 0, z);
225258
for (Vector3dm lookVec : possibleLookDirs) {
226259
for (double eye : possibleEyeHeights) {
227-
eyePos.setY(from.getY() + eye);
260+
eyePos.setY(y + eye);
228261
Vector3dm endReachPos = eyePos.clone().add(lookVec.getX() * distance, lookVec.getY() * distance, lookVec.getZ() * distance);
229262

230263
Vector3dm intercept = ReachUtils.calculateIntercept(targetBox, eyePos, endReachPos).first();
@@ -263,20 +296,13 @@ private SimpleCollisionBox getTargetBox(PacketEntity reachEntity) {
263296
return reachEntity.getPossibleCollisionBoxes();
264297
}
265298

266-
private double applyReachModifiers(SimpleCollisionBox targetBox, ItemStack itemInHand, boolean giveMovementThreshold) {
299+
private double applyReachModifiers(SimpleCollisionBox targetBox, boolean hasAttackRange, float itemMaxReach, float itemHitboxMargin, boolean giveMovementThreshold) {
267300
double maxReach;
268301
double hitboxMargin = threshold;
269302

270-
ItemAttackRange attackRange = null;
271-
272-
if (player.getClientVersion().isNewerThanOrEquals(ClientVersion.V_1_21_11) && ATTACK_RANGE_COMPONENT_EXISTS) {
273-
// TODO: ViaVersion support https://github.com/ViaVersion/ViaVersion/pull/4733
274-
attackRange = itemInHand.getComponentOr(ComponentTypes.ATTACK_RANGE, null);
275-
}
276-
277-
if (attackRange != null) {
278-
maxReach = attackRange.getMaxRange();
279-
hitboxMargin += attackRange.getHitboxMargin();
303+
if (hasAttackRange) {
304+
maxReach = itemMaxReach;
305+
hitboxMargin += itemHitboxMargin;
280306
} else {
281307
maxReach = player.compensatedEntities.self.getAttributeValue(Attributes.ENTITY_INTERACTION_RANGE);
282308
// 1.7 and 1.8 players get a bit of extra hitbox (this is why you should use 1.8 on cross version servers)
@@ -318,7 +344,6 @@ public boolean isFlag() {
318344
}
319345
}
320346

321-
private record InteractionData(Vector3d vector, ItemStack itemInHand) {
347+
private record InteractionData(double x, double y, double z, boolean hasAttackRange, float maxReach, float hitboxMargin) {
322348
}
323-
324349
}

common/src/main/java/ac/grim/grimac/utils/latency/CompensatedInventory.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ public class CompensatedInventory extends Check implements PacketCheck {
5656
// Unsupported inventory is -2
5757
private int packetSendingInventorySize = PLAYER_INVENTORY_CASE;
5858

59+
// The item held at the start of the current client tick (processed at the end of the previous tick)
60+
// Currently only used by 1.21.11+ players to handle attribute swapping items with the ATTACK_RANGE Component
61+
private ItemStack startOfTickStack = ItemStack.EMPTY;
62+
63+
public ItemStack getStartOfTickStack() {
64+
return startOfTickStack;
65+
}
66+
5967
public CompensatedInventory(GrimPlayer playerData) {
6068
super(playerData);
6169

@@ -234,9 +242,7 @@ public void onPacketReceive(final PacketReceiveEvent event) {
234242
inventory.getInventoryStorage().handleClientClaimedSlotSet(slot);
235243
inventory.getInventoryStorage().setItem(slot, use);
236244
}
237-
}
238-
239-
if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
245+
} else if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) {
240246
WrapperPlayClientPlayerDigging dig = new WrapperPlayClientPlayerDigging(event);
241247

242248
// 1.8 clients don't predict dropping items
@@ -258,18 +264,14 @@ public void onPacketReceive(final PacketReceiveEvent event) {
258264
inventory.setHeldItem(null);
259265
inventory.getInventoryStorage().handleClientClaimedSlotSet(Inventory.HOTBAR_OFFSET + player.packetStateData.lastSlotSelected);
260266
}
261-
}
262-
263-
if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
267+
} else if (event.getPacketType() == PacketType.Play.Client.HELD_ITEM_CHANGE) {
264268
final int slot = new WrapperPlayClientHeldItemChange(event).getSlot();
265269

266270
// Stop people from spamming the server with an out-of-bounds exception
267271
if (slot > 8 || slot < 0) return;
268272

269273
inventory.selected = slot;
270-
}
271-
272-
if (event.getPacketType() == PacketType.Play.Client.CREATIVE_INVENTORY_ACTION) {
274+
} else if (event.getPacketType() == PacketType.Play.Client.CREATIVE_INVENTORY_ACTION) {
273275
WrapperPlayClientCreativeInventoryAction action = new WrapperPlayClientCreativeInventoryAction(event);
274276
if (player.gamemode != GameMode.CREATIVE) return;
275277

@@ -281,9 +283,7 @@ public void onPacketReceive(final PacketReceiveEvent event) {
281283
inventory.getSlot(action.getSlot()).set(action.getItemStack());
282284
inventory.getInventoryStorage().handleClientClaimedSlotSet(action.getSlot());
283285
}
284-
}
285-
286-
if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW && !event.isCancelled()) {
286+
} else if (event.getPacketType() == PacketType.Play.Client.CLICK_WINDOW && !event.isCancelled()) {
287287
WrapperPlayClientClickWindow click = new WrapperPlayClientClickWindow(event);
288288

289289
// How is this possible? Maybe transaction splitting.
@@ -312,10 +312,10 @@ public void onPacketReceive(final PacketReceiveEvent event) {
312312
if (slot == -1 || slot == -999 || slot < menu.getSlots().size()) {
313313
menu.doClick(button, slot, clickType);
314314
}
315-
}
316-
317-
if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
315+
} else if (event.getPacketType() == PacketType.Play.Client.CLOSE_WINDOW) {
318316
this.closeActiveInventory();
317+
} else if (event.getPacketType() == PacketType.Play.Client.CLIENT_TICK_END) {
318+
this.startOfTickStack = getHeldItem();
319319
}
320320
}
321321

0 commit comments

Comments
 (0)