diff --git a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
index 9e365ab67..8e5a57fae 100644
--- a/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
+++ b/core/src/main/java/org/geysermc/geyser/util/EntityUtils.java
@@ -27,6 +27,8 @@ package org.geysermc.geyser.util;
import java.util.Locale;
import net.kyori.adventure.key.Key;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.GameType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@@ -292,22 +294,27 @@ public final class EntityUtils {
};
}
- private static String translatedEntityName(String namespace, String name, GeyserSession session) {
- // MinecraftLocale would otherwise invoke getBootstrap (which doesn't exist) and create some folders
+ private static String translatedEntityName(@NonNull String namespace, @NonNull String name, @NonNull GeyserSession session) {
+ // MinecraftLocale would otherwise invoke getBootstrap (which doesn't exist) and create some folders,
+ // so use the default fallback value as used in Minecraft Java
if (EnvironmentUtils.isUnitTesting) {
return "entity." + namespace + "." + name;
}
return MinecraftLocale.getLocaleString("entity." + namespace + "." + name, session.locale());
}
- public static String translatedEntityName(Key type, GeyserSession session) {
+ public static String translatedEntityName(@NonNull Key type, @NonNull GeyserSession session) {
return translatedEntityName(type.namespace(), type.value(), session);
}
- public static String translatedEntityName(EntityType type, GeyserSession session) {
+ public static String translatedEntityName(@Nullable EntityType type, @NonNull GeyserSession session) {
if (type == EntityType.PLAYER) {
return "Player"; // the player's name is always shown instead
}
+ // default fallback value as used in Minecraft Java
+ if (type == null) {
+ return "entity.unregistered_sadface";
+ }
// this works at least with all 1.20.5 entities, except the killer bunny since that's not an entity type.
String typeName = type.name().toLowerCase(Locale.ROOT);
return translatedEntityName("minecraft", typeName, session);
diff --git a/core/src/test/java/org/geysermc/geyser/scoreboard/network/TeamIdentifierTest.java b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java
similarity index 69%
rename from core/src/test/java/org/geysermc/geyser/scoreboard/network/TeamIdentifierTest.java
rename to core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java
index c7fa866e1..1ec245007 100644
--- a/core/src/test/java/org/geysermc/geyser/scoreboard/network/TeamIdentifierTest.java
+++ b/core/src/test/java/org/geysermc/geyser/scoreboard/network/ScoreboardIssueTests.java
@@ -28,16 +28,25 @@ package org.geysermc.geyser.scoreboard.network;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketType;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
+import org.geysermc.geyser.entity.type.living.monster.EnderDragonPartEntity;
+import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.translator.protocol.java.entity.JavaRemoveEntitiesTranslator;
import org.geysermc.geyser.translator.protocol.java.entity.spawn.JavaAddExperienceOrbTranslator;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundRemoveEntitiesPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddExperienceOrbPacket;
import org.junit.jupiter.api.Test;
-public class TeamIdentifierTest {
+/**
+ * Tests that don't fit in a larger system (e.g. sidebar objective) that were reported on GitHub
+ */
+public class ScoreboardIssueTests {
+ /**
+ * Test for #5075
+ */
@Test
void entityWithoutUuid() {
// experience orbs are the only known entities without an uuid, see Entity#teamIdentifier for more info
@@ -50,6 +59,10 @@ public class TeamIdentifierTest {
// because the entity would be registered and deregistered to the scoreboard.
assertDoesNotThrow(() -> {
context.translate(addExperienceOrbTranslator, new ClientboundAddExperienceOrbPacket(2, 0, 0, 0, 1));
+
+ String displayName = context.mockOrSpy(EntityCache.class).getEntityByJavaId(2).getDisplayName();
+ assertEquals("entity.minecraft.experience_orb", displayName);
+
context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[] { 2 }));
});
@@ -58,4 +71,23 @@ public class TeamIdentifierTest {
assertNextPacketType(context, RemoveEntityPacket.class);
});
}
+
+ /**
+ * Test for #5078
+ */
+ @Test
+ void entityWithoutType() {
+ // dragon entity parts are an entity in Geyser, but do not have an entity type
+ mockContextScoreboard(context -> {
+ // EntityUtils#translatedEntityName used to not take null EntityType's into account,
+ // so it used to throw an exception
+ assertDoesNotThrow(() -> {
+ // dragon entity parts are not spawned using a packet, so we manually create an instance
+ var dragonHeadPart = new EnderDragonPartEntity(context.session(), 2, 2, 1, 1);
+
+ String displayName = dragonHeadPart.getDisplayName();
+ assertEquals("entity.unregistered_sadface", displayName);
+ });
+ });
+ }
}