diff --git a/api/src/main/java/us/myles/ViaVersion/api/ViaVersionConfig.java b/api/src/main/java/us/myles/ViaVersion/api/ViaVersionConfig.java index 776c58097..165adefdf 100644 --- a/api/src/main/java/us/myles/ViaVersion/api/ViaVersionConfig.java +++ b/api/src/main/java/us/myles/ViaVersion/api/ViaVersionConfig.java @@ -79,10 +79,22 @@ public interface ViaVersionConfig { /** * Whether the player can block with the shield without a delay. * + * This option requires {@link #isShowShieldWhenSwordInHand()} to be disabled + * * @return {@code true} if non delayed shield blocking is enabled. */ boolean isNoDelayShieldBlocking(); + /** + * Puts the shield into the second hand when holding a sword. + * The shield will disappear when switching to another item. + * + * This option requires {@link #isShieldBlocking()} to be enabled + * + * @return {@code true} if the shield should appear when holding a sword + */ + boolean isShowShieldWhenSwordInHand(); + /** * Get if armor stand positions are fixed so holograms show up at the correct height in 1.9 & 1.10 * diff --git a/common/src/main/java/us/myles/ViaVersion/AbstractViaConfig.java b/common/src/main/java/us/myles/ViaVersion/AbstractViaConfig.java index 816bd1f6c..7fa1ef24e 100644 --- a/common/src/main/java/us/myles/ViaVersion/AbstractViaConfig.java +++ b/common/src/main/java/us/myles/ViaVersion/AbstractViaConfig.java @@ -33,6 +33,7 @@ public abstract class AbstractViaConfig extends Config implements ViaVersionConf private boolean suppressMetadataErrors; private boolean shieldBlocking; private boolean noDelayShieldBlocking; + private boolean showShieldWhenSwordInHand; private boolean hologramPatch; private boolean pistonAnimationPatch; private boolean bossbarPatch; @@ -93,6 +94,7 @@ public abstract class AbstractViaConfig extends Config implements ViaVersionConf suppressMetadataErrors = getBoolean("suppress-metadata-errors", false); shieldBlocking = getBoolean("shield-blocking", true); noDelayShieldBlocking = getBoolean("no-delay-shield-blocking", false); + showShieldWhenSwordInHand = getBoolean("show-shield-when-sword-in-hand", false); hologramPatch = getBoolean("hologram-patch", false); pistonAnimationPatch = getBoolean("piston-animation-patch", false); bossbarPatch = getBoolean("bossbar-patch", true); @@ -177,6 +179,11 @@ public abstract class AbstractViaConfig extends Config implements ViaVersionConf return noDelayShieldBlocking; } + @Override + public boolean isShowShieldWhenSwordInHand() { + return showShieldWhenSwordInHand; + } + @Override public boolean isHologramPatch() { return hologramPatch; diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/InventoryPackets.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/InventoryPackets.java index 93e30fb3c..206e63abe 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/InventoryPackets.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/InventoryPackets.java @@ -18,6 +18,7 @@ package us.myles.ViaVersion.protocols.protocol1_9to1_8.packets; import us.myles.ViaVersion.api.PacketWrapper; +import us.myles.ViaVersion.api.Via; import us.myles.ViaVersion.api.minecraft.item.Item; import us.myles.ViaVersion.api.protocol.Protocol; import us.myles.ViaVersion.api.remapper.PacketHandler; @@ -113,6 +114,25 @@ public class InventoryPackets { @Override public void handle(PacketWrapper wrapper) throws Exception { Item stack = wrapper.get(Type.ITEM, 0); + + boolean showShieldWhenSwordInHand = Via.getConfig().isShowShieldWhenSwordInHand() + && Via.getConfig().isShieldBlocking(); + + // Check if it is the inventory of the player + if (showShieldWhenSwordInHand) { + InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); + EntityTracker1_9 entityTracker = wrapper.user().get(EntityTracker1_9.class); + + short slotID = wrapper.get(Type.SHORT, 0); + short windowId = wrapper.get(Type.BYTE, 0); + + // Store item in slot + inventoryTracker.setItemId(windowId, slotID, stack == null ? 0 : stack.getIdentifier()); + + // Sync shield item in offhand with main hand + entityTracker.syncShieldWithSword(); + } + ItemRewriter.toClient(stack); } }); @@ -145,8 +165,29 @@ public class InventoryPackets { @Override public void handle(PacketWrapper wrapper) throws Exception { Item[] stacks = wrapper.get(Type.ITEM_ARRAY, 0); - for (Item stack : stacks) + Short windowId = wrapper.get(Type.UNSIGNED_BYTE, 0); + + InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); + EntityTracker1_9 entityTracker = wrapper.user().get(EntityTracker1_9.class); + + boolean showShieldWhenSwordInHand = Via.getConfig().isShowShieldWhenSwordInHand() + && Via.getConfig().isShieldBlocking(); + + for (short i = 0; i < stacks.length; i++) { + Item stack = stacks[i]; + + // Store items in slots + if (showShieldWhenSwordInHand) { + inventoryTracker.setItemId(windowId, i, stack == null ? 0 : stack.getIdentifier()); + } + ItemRewriter.toClient(stack); + } + + // Sync shield item in offhand with main hand + if (showShieldWhenSwordInHand) { + entityTracker.syncShieldWithSword(); + } } }); // Brewing Patch @@ -185,6 +226,7 @@ public class InventoryPackets { public void handle(PacketWrapper wrapper) throws Exception { InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); inventoryTracker.setInventory(null); + inventoryTracker.resetInventory(wrapper.get(Type.UNSIGNED_BYTE, 0)); } }); } @@ -218,6 +260,22 @@ public class InventoryPackets { @Override public void handle(PacketWrapper wrapper) throws Exception { Item stack = wrapper.get(Type.ITEM, 0); + + boolean showShieldWhenSwordInHand = Via.getConfig().isShowShieldWhenSwordInHand() + && Via.getConfig().isShieldBlocking(); + + if (showShieldWhenSwordInHand) { + InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); + EntityTracker1_9 entityTracker = wrapper.user().get(EntityTracker1_9.class); + short slotID = wrapper.get(Type.SHORT, 0); + + // Update item in slot + inventoryTracker.setItemId((short) 0, slotID, stack == null ? 0 : stack.getIdentifier()); + + // Sync shield item in offhand with main hand + entityTracker.syncShieldWithSword(); + } + ItemRewriter.toServer(stack); } }); @@ -259,6 +317,18 @@ public class InventoryPackets { @Override public void handle(PacketWrapper wrapper) throws Exception { Item stack = wrapper.get(Type.ITEM, 0); + + if (Via.getConfig().isShowShieldWhenSwordInHand()) { + Short windowId = wrapper.get(Type.UNSIGNED_BYTE, 0); + byte mode = wrapper.get(Type.BYTE, 1); + short hoverSlot = wrapper.get(Type.SHORT, 0); + byte button = wrapper.get(Type.BYTE, 0); + + // Move items in inventory to track the sword location + InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); + inventoryTracker.handleWindowClick(windowId, mode, hoverSlot, button); + } + ItemRewriter.toServer(stack); } }); @@ -305,12 +375,15 @@ public class InventoryPackets { @Override public void registerMap() { + map(Type.UNSIGNED_BYTE); // 0 - Window ID + // Inventory tracking handler(new PacketHandler() { @Override public void handle(PacketWrapper wrapper) throws Exception { InventoryTracker inventoryTracker = wrapper.user().get(InventoryTracker.class); inventoryTracker.setInventory(null); + inventoryTracker.resetInventory(wrapper.get(Type.UNSIGNED_BYTE, 0)); } }); } @@ -319,14 +392,30 @@ public class InventoryPackets { protocol.registerIncoming(ServerboundPackets1_9.HELD_ITEM_CHANGE, new PacketRemapper() { @Override public void registerMap() { + map(Type.SHORT); // 0 - Slot id + // Blocking patch handler(new PacketHandler() { @Override public void handle(PacketWrapper wrapper) throws Exception { + boolean showShieldWhenSwordInHand = Via.getConfig().isShowShieldWhenSwordInHand() + && Via.getConfig().isShieldBlocking(); + EntityTracker1_9 entityTracker = wrapper.user().get(EntityTracker1_9.class); if (entityTracker.isBlocking()) { entityTracker.setBlocking(false); - entityTracker.setSecondHand(null); + + if (!showShieldWhenSwordInHand) { + entityTracker.setSecondHand(null); + } + } + + if (showShieldWhenSwordInHand) { + // Update current held item slot index + entityTracker.setHeldItemSlot(wrapper.get(Type.SHORT, 0)); + + // Sync shield item in offhand with main hand + entityTracker.syncShieldWithSword(); } } }); diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/PlayerPackets.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/PlayerPackets.java index be276950d..79cba7c42 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/PlayerPackets.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/PlayerPackets.java @@ -498,7 +498,9 @@ public class PlayerPackets { // cancel any blocking >.> EntityTracker1_9 tracker = wrapper.user().get(EntityTracker1_9.class); if (tracker.isBlocking()) { - tracker.setSecondHand(null); + if(!Via.getConfig().isShowShieldWhenSwordInHand()) { + tracker.setSecondHand(null); + } tracker.setBlocking(false); } } diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/WorldPackets.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/WorldPackets.java index 29cd075eb..0d09a9a16 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/WorldPackets.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/packets/WorldPackets.java @@ -269,7 +269,9 @@ public class WorldPackets { EntityTracker1_9 entityTracker = wrapper.user().get(EntityTracker1_9.class); if (entityTracker.isBlocking()) { entityTracker.setBlocking(false); - entityTracker.setSecondHand(null); + if (!Via.getConfig().isShowShieldWhenSwordInHand()) { + entityTracker.setSecondHand(null); + } } } } @@ -296,23 +298,36 @@ public class WorldPackets { if (Via.getConfig().isShieldBlocking()) { EntityTracker1_9 tracker = wrapper.user().get(EntityTracker1_9.class); + // Check if the shield is already there or if we have to give it here + boolean showShieldWhenSwordInHand = Via.getConfig().isShowShieldWhenSwordInHand(); + if (item != null && Protocol1_9To1_8.isSword(item.getIdentifier())) { if (hand == 0) { if (!tracker.isBlocking()) { tracker.setBlocking(true); - Item shield = new Item(442, (byte) 1, (short) 0, null); - tracker.setSecondHand(shield); + + // Check if the shield is already in the offhand + if (!showShieldWhenSwordInHand || tracker.getItemInSecondHand() == null) { + + // Set shield in offhand when interacting with main hand + Item shield = new Item(442, (byte) 1, (short) 0, null); + tracker.setSecondHand(shield); + } } } - // Uses left or right hand to start blocking depending on the no delay setting - boolean noDelayBlocking = Via.getConfig().isNoDelayShieldBlocking(); + // Use the main hand to trigger the blocking + boolean blockUsingMainHand = Via.getConfig().isNoDelayShieldBlocking() + && !showShieldWhenSwordInHand; - if (noDelayBlocking && hand == 1 || !noDelayBlocking && hand == 0) { + if (blockUsingMainHand && hand == 1 || !blockUsingMainHand && hand == 0) { wrapper.cancel(); } } else { - tracker.setSecondHand(null); + if (!showShieldWhenSwordInHand) { + // Remove the shield from the offhand + tracker.setSecondHand(null); + } tracker.setBlocking(false); } } diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/EntityTracker1_9.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/EntityTracker1_9.java index 41b808869..41ebb4d33 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/EntityTracker1_9.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/EntityTracker1_9.java @@ -66,6 +66,8 @@ public class EntityTracker1_9 extends EntityTracker { private boolean teamExists = false; private GameMode gameMode; private String currentTeam; + private int heldItemSlot; + private Item itemInSecondHand = null; public EntityTracker1_9(UserConnection user) { super(user, EntityType.PLAYER); @@ -89,7 +91,7 @@ public class EntityTracker1_9 extends EntityTracker { PacketWrapper wrapper = new PacketWrapper(0x3C, null, getUser()); wrapper.write(Type.VAR_INT, entityID); wrapper.write(Type.VAR_INT, 1); // slot - wrapper.write(Type.ITEM, item); + wrapper.write(Type.ITEM, this.itemInSecondHand = item); try { wrapper.send(Protocol1_9To1_8.class); } catch (Exception e) { @@ -97,6 +99,31 @@ public class EntityTracker1_9 extends EntityTracker { } } + public Item getItemInSecondHand() { + return itemInSecondHand; + } + + /** + * It will set a shield to the offhand if a sword is in the main hand. + * The item in the offhand will be cleared if there is no sword in the main hand. + */ + public void syncShieldWithSword() { + InventoryTracker inventoryTracker = getUser().get(InventoryTracker.class); + + // Get item in new selected slot + int inventorySlot = this.heldItemSlot + 36; // Hotbar slot index to inventory slot + int itemIdentifier = inventoryTracker.getItemId((short) 0, (short) inventorySlot); + + boolean isSword = Protocol1_9To1_8.isSword(itemIdentifier); + + // Update if the state changed + if (isSword == (this.itemInSecondHand == null)) { + + // Update shield in off hand depending if a sword is in the main hand + setSecondHand(isSword ? new Item(442, (byte) 1, (short) 0, null) : null); + } + } + @Override public void removeEntity(int entityId) { super.removeEntity(entityId); @@ -395,4 +422,8 @@ public class EntityTracker1_9 extends EntityTracker { public void setCurrentTeam(String currentTeam) { this.currentTeam = currentTeam; } + + public void setHeldItemSlot(int heldItemSlot) { + this.heldItemSlot = heldItemSlot; + } } diff --git a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/InventoryTracker.java b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/InventoryTracker.java index 0d5566e79..fb47155f5 100644 --- a/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/InventoryTracker.java +++ b/common/src/main/java/us/myles/ViaVersion/protocols/protocol1_9to1_8/storage/InventoryTracker.java @@ -20,9 +20,16 @@ package us.myles.ViaVersion.protocols.protocol1_9to1_8.storage; import us.myles.ViaVersion.api.data.StoredObject; import us.myles.ViaVersion.api.data.UserConnection; +import java.util.HashMap; +import java.util.Map; + public class InventoryTracker extends StoredObject { private String inventory; + private final Map> windowItemCache = new HashMap<>(); + private int itemIdInCursor = 0; + private boolean dragging = false; + public InventoryTracker(UserConnection user) { super(user); } @@ -34,4 +41,142 @@ public class InventoryTracker extends StoredObject { public void setInventory(String inventory) { this.inventory = inventory; } + + public void resetInventory(short windowId) { + // Reset the cursor state of the inventory + if (inventory == null) { + this.itemIdInCursor = 0; + this.dragging = false; + + // Remove window from cache (Except players window) + if (windowId != 0) { + this.windowItemCache.remove(windowId); + } + } + } + + public int getItemId(short windowId, short slot) { + Map itemMap = this.windowItemCache.get(windowId); + if (itemMap == null) { + return 0; + } + + return itemMap.getOrDefault(slot, 0); + } + + public void setItemId(short windowId, short slot, int itemId) { + if (windowId == -1 && slot == -1) { + // Set the cursor item + this.itemIdInCursor = itemId; + } else { + // Set item in normal inventory + this.windowItemCache.computeIfAbsent(windowId, k -> new HashMap<>()).put(slot, itemId); + } + } + + /** + * Handle the window click to track the position of the sword + * + * @param windowId Id of the current inventory + * @param mode Inventory operation mode + * @param hoverSlot The slot number of the current mouse position + * @param button The button to use in the click + */ + public void handleWindowClick(short windowId, byte mode, short hoverSlot, byte button) { + EntityTracker1_9 entityTracker = getUser().get(EntityTracker1_9.class); + + // Skip inventory background clicks + if (hoverSlot == -1) { + return; + } + + // Interaction with the offhand slot + if (hoverSlot == 45) { + entityTracker.setSecondHand(null); // Remove it so we know that we can update it on ITEM_USE + return; + } + + // It is not possible to put a sword into the armor or crafting result slot + boolean isArmorOrResultSlot = hoverSlot >= 5 && hoverSlot <= 8 || hoverSlot == 0; + + switch (mode) { + case 0: // Click on slot + + // The cursor is empty, so we can put an item to it + if (this.itemIdInCursor == 0) { + // Move item to cursor + this.itemIdInCursor = getItemId(windowId, hoverSlot); + + // Remove item in slot + setItemId(windowId, hoverSlot, 0); + } else { + // Dropping item + if (hoverSlot == -999) { + this.itemIdInCursor = 0; + } else if (!isArmorOrResultSlot) { + int previousItem = getItemId(windowId, hoverSlot); + + // Place item in inventory + setItemId(windowId, hoverSlot, this.itemIdInCursor); + + // Pick up the other item + this.itemIdInCursor = previousItem; + } + } + break; + case 2: // Move item using number keys + if (!isArmorOrResultSlot) { + short hotkeySlot = (short) (button + 36); + + // Get items to swap + int sourceItem = getItemId(windowId, hoverSlot); + int destinationItem = getItemId(windowId, hotkeySlot); + + // Swap + setItemId(windowId, hotkeySlot, sourceItem); + setItemId(windowId, hoverSlot, destinationItem); + } + + break; + case 4: // Drop item + int hoverItem = getItemId(windowId, hoverSlot); + + if (hoverItem != 0) { + setItemId(windowId, hoverSlot, 0); + } + + break; + case 5: // Mouse dragging + switch (button) { + case 0: // Start left dragging + case 4: // Start right dragging + this.dragging = true; + break; + case 1: // Place item during left dragging + case 5: // Place item during right dragging + // Check dragging mode and item on cursor + if (this.dragging && this.itemIdInCursor != 0 && !isArmorOrResultSlot) { + int previousItem = getItemId(windowId, hoverSlot); + + // Place item on cursor in hovering slot + setItemId(windowId, hoverSlot, this.itemIdInCursor); + + // Pick up the other item + this.itemIdInCursor = previousItem; + } + break; + case 2: // Stop left dragging + case 6: // Stop right dragging + this.dragging = false; + break; + } + + break; + default: + break; + } + + // Update shield state in offhand + entityTracker.syncShieldWithSword(); + } } diff --git a/common/src/main/resources/assets/viaversion/config.yml b/common/src/main/resources/assets/viaversion/config.yml index 9ee7068ab..c57b06b06 100644 --- a/common/src/main/resources/assets/viaversion/config.yml +++ b/common/src/main/resources/assets/viaversion/config.yml @@ -169,7 +169,12 @@ suppress-metadata-errors: false shield-blocking: true # If this setting is active, the main hand is used instead of the off hand to trigger the blocking of the player. # With the main hand the blocking starts way faster. +# (Requires "show-shield-when-sword-in-hand" to be disabled) no-delay-shield-blocking: false +# If this setting is active, the shield will appear immediately for 1.9+ when you hold a sword in your main hand. +# The shield disappears when you switch to another item. +# (Requires "shield-blocking" to be enabled) +show-shield-when-sword-in-hand: false # Enable player tick simulation, this fixes eating, drinking, nether portals. simulate-pt: true # Should we use nms player to simulate packets, (may fix anti-cheat issues)