Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,24 @@ public String getDisplayName() {
return username;
}

protected boolean handlesDisplayName() {
return false;
}

@Override
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// Doesn't do anything for players
// TODO test mannequins
if (!handlesDisplayName()) {
return;
}
super.setDisplayName(entityMetadata);
}

@Override
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
// Doesn't do anything for players
// TODO test mannequins
if (!handlesDisplayName()) {
return;
}
super.setDisplayNameVisible(entityMetadata);
}

public void setBelowNameText(String text) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,112 @@
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;

import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

public class MannequinEntity extends AvatarEntity {

private int profileVersion;

public MannequinEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw, "Mannequin"); // TODO from translation
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw,
EntityUtils.translatedEntityName(definition.entityType(), session));
}

@Override
protected boolean handlesDisplayName() {
return true;
}

public void setProfile(EntityMetadata<ResolvableProfile, ?> entityMetadata) {
setSkin(entityMetadata.getValue(), true, () -> {});
ResolvableProfile resolvableProfile = entityMetadata.getValue();
if (resolvableProfile == null) {
resolvableProfile = SkinManager.EMPTY_RESOLVABLE_PROFILE;
}

int version = ++this.profileVersion;
ResolvableProfile finalResolvableProfile = resolvableProfile;
SkinManager.resolveProfile(resolvableProfile).whenCompleteAsync((resolvedProfile, throwable) -> {
GameProfile profile = resolvedProfile;
if (throwable != null || profile == null) {
profile = finalResolvableProfile.getProfile();
}
if (profile == null) {
profile = SkinManager.EMPTY_PROFILE;
}

GameProfile finalProfile = profile;
session.ensureInEventLoop(() -> {
if (version != this.profileVersion) {
return;
}
applyProfile(finalResolvableProfile, finalProfile);
});
});
}

public void setDescription(EntityMetadata<Optional<Component>, ?> entityMetadata) {
Optional<Component> description = entityMetadata.getValue();
if (description != null && description.isPresent()) {
setBelowNameText(MessageTranslator.convertMessage(description.get(), session.locale()));
} else {
setBelowNameText(null);
}
}

private void applyProfile(ResolvableProfile resolvableProfile, GameProfile profile) {
setSkin(profile, true, () -> {});
updateUsername(resolvableProfile, profile);
}

private void updateUsername(ResolvableProfile resolvableProfile, GameProfile profile) {
String previousName = this.username;
String newName = resolveName(resolvableProfile, profile);
if (Objects.equals(previousName, newName)) {
return;
}

this.username = newName;
boolean updatedNametag = false;
if (Objects.equals(this.nametag, previousName) || this.nametag.isEmpty()) {
setNametag(newName, false);
updatedNametag = true;
}

if (updatedNametag) {
var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier());
if (team != null) {
updateNametag(team);
}
}
}

private String resolveName(ResolvableProfile resolvableProfile, GameProfile profile) {
if (profile != null) {
String name = profile.getName();
if (name != null && !name.isBlank()) {
return name;
}
}

if (resolvableProfile != null) {
GameProfile partial = resolvableProfile.getProfile();
if (partial != null) {
String name = partial.getName();
if (name != null && !name.isBlank()) {
return name;
}
}
}

return EntityUtils.translatedEntityName(definition.entityType(), session);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package org.geysermc.geyser.entity.type.player;

import net.kyori.adventure.text.Component;
import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.mcprotocollib.auth.GameProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataTypes;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ObjectEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.ResolvableProfile;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class MannequinEntityTest {

private static final EntityDefinition<MannequinEntity> MANNEQUIN_DEFINITION = new EntityDefinition<>(
null,
EntityType.MANNEQUIN,
"minecraft:mannequin",
0.6f,
1.8f,
0.0f,
null,
List.of()
);

private static final EntityDefinition<TestAvatarEntity> AVATAR_DEFINITION = new EntityDefinition<>(
null,
EntityType.PLAYER,
"minecraft:player",
0.6f,
1.8f,
0.0f,
null,
List.of()
);

private GeyserSession session;

@BeforeEach
void setUp() {
session = mock(GeyserSession.class);
when(session.locale()).thenReturn("en_us");

WorldCache worldCache = mock(WorldCache.class);
Scoreboard scoreboard = mock(Scoreboard.class);
when(worldCache.getScoreboard()).thenReturn(scoreboard);
when(session.getWorldCache()).thenReturn(worldCache);
when(scoreboard.getTeamFor(any())).thenReturn(null);

doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(0);
runnable.run();
return null;
}).when(session).ensureInEventLoop(any());
}

@Test
void avatarEntityIgnoresDisplayNameMetadata() {
TestAvatarEntity avatar = new TestAvatarEntity(session);
EntityMetadata<Optional<Component>, MetadataType<Optional<Component>>> metadata =
new ObjectEntityMetadata<>(0, MetadataTypes.OPTIONAL_COMPONENT, Optional.of(Component.text("Custom")));

avatar.setDisplayName(metadata);

assertEquals("TestUser", avatar.getUsername());
assertEquals("TestUser", avatar.getNametag());
}

@Test
void mannequinAppliesDisplayNameMetadata() {
TestMannequinEntity mannequin = new TestMannequinEntity(session);
EntityMetadata<Optional<Component>, MetadataType<Optional<Component>>> metadata =
new ObjectEntityMetadata<>(0, MetadataTypes.OPTIONAL_COMPONENT, Optional.of(Component.text("Fancy")));

mannequin.setDisplayName(metadata);

assertEquals("Fancy", mannequin.getNametag());
}

@Test
void mannequinUsesResolvedProfileNameWhenAvailable() throws Exception {
TestMannequinEntity mannequin = new TestMannequinEntity(session);
GameProfile resolvedProfile = new GameProfile(UUID.randomUUID(), "ResolvedName");
ResolvableProfile resolvableProfile = new ResolvableProfile(
new GameProfile(UUID.randomUUID(), "Partial"),
null,
null,
null,
null,
false
);

invokeApplyProfile(mannequin, resolvableProfile, resolvedProfile);

assertEquals("ResolvedName", mannequin.getUsername());
assertEquals("ResolvedName", mannequin.getNametag());
assertSame(resolvedProfile, mannequin.lastSkinProfile);
}

@Test
void mannequinFallsBackToResolvableProfileName() throws Exception {
TestMannequinEntity mannequin = new TestMannequinEntity(session);
GameProfile unresolvedProfile = new GameProfile((UUID) null, null);
ResolvableProfile resolvableProfile = new ResolvableProfile(
new GameProfile(UUID.randomUUID(), "Partial"),
null,
null,
null,
null,
false
);

invokeApplyProfile(mannequin, resolvableProfile, unresolvedProfile);

assertEquals("Partial", mannequin.getUsername());
assertEquals("Partial", mannequin.getNametag());
}

@Test
void mannequinFallsBackToTranslatedNameWhenNoNamesProvided() throws Exception {
TestMannequinEntity mannequin = new TestMannequinEntity(session);
String originalName = mannequin.getUsername();

ResolvableProfile emptyResolvableProfile = new ResolvableProfile(
new GameProfile((UUID) null, null),
null,
null,
null,
null,
false
);
GameProfile emptyProfile = new GameProfile((UUID) null, null);

invokeApplyProfile(mannequin, emptyResolvableProfile, emptyProfile);

assertEquals(originalName, mannequin.getUsername());
assertEquals(originalName, mannequin.getNametag());
}

private static void invokeApplyProfile(MannequinEntity entity, ResolvableProfile resolvableProfile, GameProfile gameProfile) throws Exception {
Method method = MannequinEntity.class.getDeclaredMethod("applyProfile", ResolvableProfile.class, GameProfile.class);
method.setAccessible(true);
method.invoke(entity, resolvableProfile, gameProfile);
}

private static class TestAvatarEntity extends AvatarEntity {
TestAvatarEntity(GeyserSession session) {
super(session, 1, 1L, UUID.randomUUID(), AVATAR_DEFINITION, Vector3f.from(0, 0, 0), Vector3f.from(0, 0, 0), 0f, 0f, 0f, "TestUser");
}

@Override
public void setSkin(GameProfile profile, boolean cape, Runnable after) {
if (after != null) {
after.run();
}
}

@Override
public void setSkin(String texturesProperty, boolean cape, Runnable after) {
if (after != null) {
after.run();
}
}
}

private static class TestMannequinEntity extends MannequinEntity {
private GameProfile lastSkinProfile;

TestMannequinEntity(GeyserSession session) {
super(session, 2, 2L, UUID.randomUUID(), MANNEQUIN_DEFINITION, Vector3f.from(0, 0, 0), Vector3f.from(0, 0, 0), 0f, 0f, 0f);
}

@Override
public void setSkin(GameProfile profile, boolean cape, Runnable after) {
this.lastSkinProfile = profile;
if (after != null) {
after.run();
}
}

@Override
public void setSkin(String texturesProperty, boolean cape, Runnable after) {
if (after != null) {
after.run();
}
}
}
}