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); + }); + }); + } }