diff --git a/README.md b/README.md index 6e4457a..d7a4c7e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you want to talk about ViaBedrock or learn more about it you can join my [Dis - [x] Biomes - [x] Player spawning - [x] Entity spawning -- [ ] Entity interactions +- [x] Entity interactions - [ ] Entity metadata - [x] Entity attributes - [ ] Entity mounting @@ -43,7 +43,7 @@ If you want to talk about ViaBedrock or learn more about it you can join my [Dis - [x] Bossbar - [x] Player list - [x] Command suggestions -- [x] Sounds (No mob ambient sounds yet) +- [x] Sounds (No mob sounds yet) - [x] Particles - [x] Player Skins (Requires [BedrockSkinUtility](https://github.com/Camotoy/BedrockSkinUtility) mod) - [x] Basic resource pack conversion (Contributions welcome) diff --git a/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java b/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java index 264bc32..88d7da7 100644 --- a/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java +++ b/src/main/java/net/raphimc/viabedrock/api/model/container/player/InventoryContainer.java @@ -94,6 +94,14 @@ public byte javaWindowId() { return (byte) ContainerID.CONTAINER_ID_INVENTORY.getValue(); } + public byte getSelectedHotbarSlot() { + return this.selectedHotbarSlot; + } + + public BedrockItem getSelectedHotbarItem() { + return this.getItem(this.selectedHotbarSlot); + } + public void sendSelectedHotbarSlotToClient() { final PacketWrapper setCarriedItem = PacketWrapper.create(ClientboundPackets1_21.SET_CARRIED_ITEM, this.user); setCarriedItem.write(Types.BYTE, this.selectedHotbarSlot); @@ -122,7 +130,7 @@ private void onSelectedHotbarSlotChanged(final BedrockItem oldItem, final Bedroc final PacketWrapper interact = PacketWrapper.create(ServerboundBedrockPackets.INTERACT, this.user); interact.write(Types.BYTE, (byte) InteractPacket_Action.InteractUpdate.getValue()); // action interact.write(BedrockTypes.UNSIGNED_VAR_LONG, 0L); // target runtime entity id - interact.write(BedrockTypes.POSITION_3F, new Position3f(0F, 0F, 0F)); // mouse position + interact.write(BedrockTypes.POSITION_3F, Position3f.ZERO); // mouse position interact.sendToServer(BedrockProtocol.class); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/data/enums/java/InteractActionType.java b/src/main/java/net/raphimc/viabedrock/protocol/data/enums/java/InteractActionType.java new file mode 100644 index 0000000..96622d6 --- /dev/null +++ b/src/main/java/net/raphimc/viabedrock/protocol/data/enums/java/InteractActionType.java @@ -0,0 +1,26 @@ +/* + * This file is part of ViaBedrock - https://github.com/RaphiMC/ViaBedrock + * Copyright (C) 2023-2024 RK_01/RaphiMC and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.raphimc.viabedrock.protocol.data.enums.java; + +public enum InteractActionType { + + INTERACT, + ATTACK, + INTERACT_AT, + +} diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/ClientPlayerPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/ClientPlayerPackets.java index d09fb62..c11d326 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/ClientPlayerPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/ClientPlayerPackets.java @@ -26,7 +26,9 @@ import com.viaversion.viaversion.protocols.v1_20_5to1_21.packet.ClientboundPackets1_21; import com.viaversion.viaversion.util.Pair; import net.raphimc.viabedrock.ViaBedrock; +import net.raphimc.viabedrock.api.model.container.player.InventoryContainer; import net.raphimc.viabedrock.api.model.entity.ClientPlayerEntity; +import net.raphimc.viabedrock.api.model.entity.Entity; import net.raphimc.viabedrock.api.util.BitSets; import net.raphimc.viabedrock.api.util.PacketFactory; import net.raphimc.viabedrock.protocol.BedrockProtocol; @@ -37,6 +39,7 @@ import net.raphimc.viabedrock.protocol.data.enums.java.*; import net.raphimc.viabedrock.protocol.model.Position3f; import net.raphimc.viabedrock.protocol.rewriter.GameTypeRewriter; +import net.raphimc.viabedrock.protocol.rewriter.ItemRewriter; import net.raphimc.viabedrock.protocol.storage.*; import net.raphimc.viabedrock.protocol.types.BedrockTypes; @@ -215,7 +218,7 @@ protected void register() { switch (action) { case PERFORM_RESPAWN -> { - wrapper.write(BedrockTypes.POSITION_3F, new Position3f(0F, 0F, 0F)); // position + wrapper.write(BedrockTypes.POSITION_3F, Position3f.ZERO); // position wrapper.write(Types.BYTE, (byte) PlayerRespawnState.ClientReadyToSpawn.getValue()); // state wrapper.write(BedrockTypes.UNSIGNED_VAR_LONG, clientPlayer.runtimeId()); // runtime entity id } @@ -355,6 +358,54 @@ protected void register() { default -> throw new IllegalStateException("Unhandled PlayerActionAction: " + action); } }); + protocol.registerServerbound(ServerboundPackets1_20_5.INTERACT, ServerboundBedrockPackets.INVENTORY_TRANSACTION, wrapper -> { + final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); + final InventoryContainer inventoryContainer = wrapper.user().get(InventoryTracker.class).getInventoryContainer(); + final int entityId = wrapper.read(Types.VAR_INT); // entity id + final InteractActionType action = InteractActionType.values()[wrapper.read(Types.VAR_INT)]; // action + final Entity entity = entityTracker.getEntityByJid(entityId); + if (entity == null) { + wrapper.cancel(); + return; + } + + wrapper.write(BedrockTypes.VAR_INT, 0); // legacy request id + wrapper.write(BedrockTypes.UNSIGNED_VAR_INT, ComplexInventoryTransaction_Type.ItemUseOnEntityTransaction.getValue()); // transaction type + wrapper.write(BedrockTypes.UNSIGNED_VAR_INT, 0); // actions count + wrapper.write(BedrockTypes.UNSIGNED_VAR_LONG, entity.runtimeId()); // runtime entity id + wrapper.write(BedrockTypes.UNSIGNED_VAR_INT, (switch (action) { + case INTERACT, INTERACT_AT -> ItemUseOnActorInventoryTransaction_ActionType.Interact; + case ATTACK -> ItemUseOnActorInventoryTransaction_ActionType.Attack; + default -> throw new IllegalStateException("Unhandled InteractActionType: " + action); + }).getValue()); // action type + wrapper.write(BedrockTypes.VAR_INT, (int) inventoryContainer.getSelectedHotbarSlot()); // hotbar slot + wrapper.write(wrapper.user().get(ItemRewriter.class).itemType(), inventoryContainer.getSelectedHotbarItem()); // hand item + wrapper.write(BedrockTypes.POSITION_3F, entityTracker.getClientPlayer().position()); // player position + + switch (action) { + case INTERACT -> wrapper.cancel(); + case ATTACK -> { + wrapper.read(Types.BOOLEAN); // secondary action + wrapper.write(BedrockTypes.POSITION_3F, Position3f.ZERO); // click position + + entityTracker.getClientPlayer().sendSwingPacketToServer(); + entityTracker.getClientPlayer().cancelNextSwingPacket(); + } + case INTERACT_AT -> { + final float x = wrapper.read(Types.FLOAT); // x + final float y = wrapper.read(Types.FLOAT); // y + final float z = wrapper.read(Types.FLOAT); // z + final InteractionHand hand = InteractionHand.values()[wrapper.read(Types.VAR_INT)]; // hand + if (hand != InteractionHand.MAIN_HAND) { + wrapper.cancel(); + return; + } + wrapper.read(Types.BOOLEAN); // secondary action + wrapper.write(BedrockTypes.POSITION_3F, entity.position().add(x, y, z)); // click position + } + default -> throw new IllegalStateException("Unhandled InteractActionType: " + action); + } + }); protocol.registerServerbound(ServerboundPackets1_20_5.MOVE_PLAYER_STATUS_ONLY, ServerboundBedrockPackets.MOVE_PLAYER, wrapper -> { final ClientPlayerEntity clientPlayer = wrapper.user().get(EntityTracker.class).getClientPlayer(); clientPlayer.updatePlayerPosition(wrapper, wrapper.read(Types.BOOLEAN)); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java index 10f2295..768e271 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/EntityPackets.java @@ -394,8 +394,7 @@ public static void register(final BedrockProtocol protocol) { setEntityData.write(Types1_21.ENTITY_DATA_LIST, Lists.newArrayList(new EntityData(entity.getJavaEntityDataIndex("PAINTING_VARIANT"), Types1_21.ENTITY_DATA_TYPES.paintingVariantType, paintingHolder))); // entity data setEntityData.send(BedrockProtocol.class); }); - protocol.registerClientbound(ClientboundBedrockPackets.ENTITY_EVENT, null, wrapper -> { - wrapper.cancel(); + protocol.registerClientbound(ClientboundBedrockPackets.ENTITY_EVENT, ClientboundPackets1_21.ENTITY_EVENT, wrapper -> { final EntityTracker entityTracker = wrapper.user().get(EntityTracker.class); final GameSessionStorage gameSession = wrapper.user().get(GameSessionStorage.class); @@ -403,26 +402,42 @@ public static void register(final BedrockProtocol protocol) { final byte rawEvent = wrapper.read(Types.BYTE); // event final ActorEvent event = ActorEvent.getByValue(rawEvent); // event if (event == null) { + wrapper.cancel(); ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Unknown ActorEvent: " + rawEvent); return; } final int data = wrapper.read(BedrockTypes.VAR_INT); // data + final Entity entity = entityTracker.getEntityByRid(runtimeEntityId); + if (entity == null) { + wrapper.cancel(); + return; + } switch (event) { + case HURT -> { + final CompoundTag damageTypeRegistry = gameSession.getJavaRegistries().getCompoundTag("minecraft:damage_type"); + wrapper.setPacketType(ClientboundPackets1_21.DAMAGE_EVENT); + wrapper.write(Types.VAR_INT, entity.javaId()); // entity id + wrapper.write(Types.VAR_INT, RegistryUtil.getRegistryIndex(damageTypeRegistry, damageTypeRegistry.getCompoundTag("minecraft:generic"))); // source type + wrapper.write(Types.VAR_INT, 0); // source cause id + wrapper.write(Types.VAR_INT, 0); // source direct id + wrapper.write(Types.BOOLEAN, false); // has source position + } case DEATH -> { - if (runtimeEntityId == entityTracker.getClientPlayer().runtimeId()) { - entityTracker.getClientPlayer().setHealth(0F); - entityTracker.getClientPlayer().sendAttribute("minecraft:health"); - - if (gameSession.getDeathMessage() != null && entityTracker.getClientPlayer().isDead()) { - final PacketWrapper playerCombatKill = PacketWrapper.create(ClientboundPackets1_21.PLAYER_COMBAT_KILL, wrapper.user()); - playerCombatKill.write(Types.VAR_INT, entityTracker.getClientPlayer().javaId()); // entity id - playerCombatKill.write(Types.TAG, TextUtil.textComponentToNbt(gameSession.getDeathMessage())); // message - playerCombatKill.send(BedrockProtocol.class); - } + wrapper.cancel(); + if (entity instanceof LivingEntity livingEntity) { + livingEntity.setHealth(0F); + livingEntity.sendAttribute("minecraft:health"); + } + if (entity == entityTracker.getClientPlayer() && entityTracker.getClientPlayer().isDead() && gameSession.getDeathMessage() != null) { + final PacketWrapper playerCombatKill = PacketWrapper.create(ClientboundPackets1_21.PLAYER_COMBAT_KILL, wrapper.user()); + playerCombatKill.write(Types.VAR_INT, entityTracker.getClientPlayer().javaId()); // entity id + playerCombatKill.write(Types.TAG, TextUtil.textComponentToNbt(gameSession.getDeathMessage())); // message + playerCombatKill.send(BedrockProtocol.class); } } default -> { + wrapper.cancel(); // TODO: Handle remaining events // throw new IllegalStateException("Unhandled ActorEvent: " + event); } diff --git a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java index 46f5d98..ac6ea91 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/packet/JoinPackets.java @@ -154,7 +154,7 @@ public static void register(final BedrockProtocol protocol) { final PacketWrapper interact = PacketWrapper.create(ServerboundBedrockPackets.INTERACT, wrapper.user()); interact.write(Types.BYTE, (byte) InteractPacket_Action.InteractUpdate.getValue()); // action interact.write(BedrockTypes.UNSIGNED_VAR_LONG, 0L); // target runtime entity id - interact.write(BedrockTypes.POSITION_3F, new Position3f(0F, 0F, 0F)); // mouse position + interact.write(BedrockTypes.POSITION_3F, Position3f.ZERO); // mouse position interact.sendToServer(BedrockProtocol.class); final PacketWrapper emoteList = PacketWrapper.create(ServerboundBedrockPackets.EMOTE_LIST, wrapper.user()); diff --git a/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java b/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java index 76dcfcf..4a9eebf 100644 --- a/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java +++ b/src/main/java/net/raphimc/viabedrock/protocol/storage/EntityTracker.java @@ -39,8 +39,9 @@ public class EntityTracker extends StoredObject { private final AtomicInteger ID_COUNTER = new AtomicInteger(1); private ClientPlayerEntity clientPlayerEntity = null; - private final Map runtimeIdToUniqueId = new HashMap<>(); private final Map entities = new HashMap<>(); + private final Map runtimeIdToUniqueId = new HashMap<>(); + private final Map javaIdToUniqueId = new HashMap<>(); private final Map itemFrames = new HashMap<>(); public EntityTracker(final UserConnection user) { @@ -69,16 +70,19 @@ public T addEntity(final T entity, final boolean updateTeam) this.clientPlayerEntity = (ClientPlayerEntity) entity; } - if (this.runtimeIdToUniqueId.putIfAbsent(entity.runtimeId(), entity.uniqueId()) != null) { - ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Duplicate runtime entity ID: " + entity.runtimeId()); - } final Entity prevEntity = this.entities.put(entity.uniqueId(), entity); if (prevEntity != null) { ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Duplicate unique entity ID: " + entity.uniqueId()); + this.removeEntity(prevEntity); final PacketWrapper removeEntities = PacketWrapper.create(ClientboundPackets1_21.REMOVE_ENTITIES, this.user()); removeEntities.write(Types.VAR_INT_ARRAY_PRIMITIVE, new int[]{prevEntity.javaId()}); // entity ids removeEntities.send(BedrockProtocol.class); - prevEntity.remove(); + } + if (this.javaIdToUniqueId.put(entity.javaId(), entity.uniqueId()) != null) { + ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Duplicate Java entity ID: " + entity.javaId()); + } + if (this.runtimeIdToUniqueId.putIfAbsent(entity.runtimeId(), entity.uniqueId()) != null) { + ViaBedrock.getPlatform().getLogger().log(Level.WARNING, "Duplicate runtime entity ID: " + entity.runtimeId()); } if (updateTeam && entity instanceof PlayerEntity player) { @@ -93,8 +97,9 @@ public void removeEntity(final Entity entity) { throw new IllegalArgumentException("Cannot remove client player entity"); } - this.runtimeIdToUniqueId.remove(entity.runtimeId()); this.entities.remove(entity.uniqueId()); + this.runtimeIdToUniqueId.remove(entity.runtimeId()); + this.javaIdToUniqueId.remove(entity.javaId()); entity.remove(); } @@ -174,6 +179,10 @@ public Entity getEntityByUid(final long uniqueId) { return this.entities.get(uniqueId); } + public Entity getEntityByJid(final int javaId) { + return this.entities.get(this.javaIdToUniqueId.get(javaId)); + } + public ClientPlayerEntity getClientPlayer() { return this.clientPlayerEntity; }