From 85a98c6a097b4bc069ccd870433622c5a3f5df53 Mon Sep 17 00:00:00 2001
From: RaphiMC <50594595+RaphiMC@users.noreply.github.com>
Date: Mon, 16 Sep 2024 00:31:51 +0200
Subject: [PATCH] Implemented entity interaction translation
---
README.md | 4 +-
.../container/player/InventoryContainer.java | 10 +++-
.../data/enums/java/InteractActionType.java | 26 +++++++++
.../protocol/packet/ClientPlayerPackets.java | 53 ++++++++++++++++++-
.../protocol/packet/EntityPackets.java | 39 +++++++++-----
.../protocol/packet/JoinPackets.java | 2 +-
.../protocol/storage/EntityTracker.java | 21 +++++---
7 files changed, 132 insertions(+), 23 deletions(-)
create mode 100644 src/main/java/net/raphimc/viabedrock/protocol/data/enums/java/InteractActionType.java
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;
}