Skip to content
Merged
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 @@ -7,8 +7,13 @@
import org.bukkit.event.Listener;
import org.bukkit.plugin.Plugin;

import com.destroystokyo.paper.profile.PlayerProfile;
import com.destroystokyo.paper.profile.ProfileProperty;

import dev.lone.itemsadder.api.CustomEntity;
import dev.lone.itemsadder.api.CustomPlayer;
import dev.lone.itemsadder.api.Events.PlayerEmoteEndEvent;
import dev.lone.itemsadder.api.Events.PlayerEmotePlayEvent;

/** Loaded only when ItemsAdder is enabled. */
public final class ItemsAdderHandsUpAnimation implements HandsUpAnimation, Listener {
Expand All @@ -26,7 +31,11 @@ public boolean start(Player player) {
try {
CustomPlayer.playEmote(player, EMOTE);
CustomPlayer animated = CustomPlayer.byAlreadySpawned(player);
return animated != null && animated.isPlayingAnimation(EMOTE);
if (animated == null || !animated.isPlayingAnimation(EMOTE)) {
return false;
}
applyActiveSkin(animated, player);
return true;
} catch (RuntimeException exception) {
plugin.getLogger().log(Level.WARNING, "Could not start the hands-up emote.", exception);
return false;
Expand All @@ -45,6 +54,55 @@ public void stop(Player player) {
}
}

// ItemsAdder 4.0.18 skins emote renderers from the player name (its Mojang skin),
// so push the profile textures active now through its internal changePlayerSkin.
private void applyActiveSkin(CustomPlayer animated, Player player) {
try {
PlayerProfile profile = player.getPlayerProfile();
if (profile == null) {
return;
}
String textures = null;
for (ProfileProperty property : profile.getProperties()) {
if ("textures".equals(property.getName())) {
textures = property.getValue();
break;
}
}
if (textures == null || textures.isEmpty()) {
return;
}
java.lang.reflect.Field rendererField = CustomEntity.class.getDeclaredField("renderer");
rendererField.setAccessible(true);
Object renderer = rendererField.get(animated);
if (renderer != null) {
renderer.getClass().getMethod("changePlayerSkin", String.class).invoke(renderer, textures);
}
} catch (RuntimeException | ReflectiveOperationException exception) {
plugin.getLogger().log(Level.WARNING, "Could not apply the active skin to the hands-up emote.", exception);
}
}

// ItemsAdder creates the renderer after this event, so skin it on the next tick.
// This covers emotes started outside /handsup as well as the pose itself.
@EventHandler(ignoreCancelled = true)
public void onEmotePlay(PlayerEmotePlayEvent event) {
Player player = event.getPlayer();
String emote = event.getEmoteName();
plugin.getServer().getScheduler().runTask(plugin, () -> skinPlayingEmote(player, emote));
}

private void skinPlayingEmote(Player player, String emote) {
try {
CustomPlayer animated = CustomPlayer.byAlreadySpawned(player);
if (animated != null && animated.isPlayingAnimation(emote)) {
applyActiveSkin(animated, player);
}
} catch (RuntimeException exception) {
plugin.getLogger().log(Level.WARNING, "Could not apply the active skin to an emote.", exception);
}
}

@EventHandler
public void onEmoteEnd(PlayerEmoteEndEvent event) {
if (EMOTE.equals(event.getEmoteName())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import dev.lone.itemsadder.api.CustomPlayer;
import dev.lone.itemsadder.api.Events.PlayerEmoteEndEvent;
import dev.lone.itemsadder.api.Events.PlayerEmotePlayEvent;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.Test;
Expand All @@ -30,6 +31,100 @@ void startRequiresTheRequestedAnimationToActuallyBePlaying() {
}
}

@Test
void startPushesActiveProfileTexturesToRenderer() throws Exception {
CustomPlayer rendered = mock(CustomPlayer.class);
when(rendered.isPlayingAnimation(ItemsAdderHandsUpAnimation.EMOTE)).thenReturn(true);
itemsadder.m.dn renderer = mock(itemsadder.m.dn.class);
java.lang.reflect.Field field = dev.lone.itemsadder.api.CustomEntity.class.getDeclaredField("renderer");
field.setAccessible(true);
field.set(rendered, renderer);
com.destroystokyo.paper.profile.PlayerProfile profile =
mock(com.destroystokyo.paper.profile.PlayerProfile.class);
when(player.getPlayerProfile()).thenReturn(profile);
when(profile.getProperties()).thenReturn(java.util.Set.of(
new com.destroystokyo.paper.profile.ProfileProperty("textures", "active-textures-value", "sig")));
try (var api = mockStatic(CustomPlayer.class)) {
api.when(() -> CustomPlayer.byAlreadySpawned(player)).thenReturn(rendered);
assertTrue(animation.start(player));
verify(renderer).changePlayerSkin("active-textures-value");
}
}

@Test
void anyEmoteIsSkinnedFromTheActiveProfileOnTheNextTick() throws Exception {
org.bukkit.Server server = mock(org.bukkit.Server.class);
org.bukkit.scheduler.BukkitScheduler scheduler = mock(org.bukkit.scheduler.BukkitScheduler.class);
when(plugin.getServer()).thenReturn(server);
when(server.getScheduler()).thenReturn(scheduler);
var task = org.mockito.ArgumentCaptor.forClass(Runnable.class);
PlayerEmotePlayEvent event = mock(PlayerEmotePlayEvent.class);
when(event.getPlayer()).thenReturn(player);
when(event.getEmoteName()).thenReturn("wave");

CustomPlayer rendered = mock(CustomPlayer.class);
itemsadder.m.dn renderer = mock(itemsadder.m.dn.class);
java.lang.reflect.Field field = dev.lone.itemsadder.api.CustomEntity.class.getDeclaredField("renderer");
field.setAccessible(true);
field.set(rendered, renderer);
com.destroystokyo.paper.profile.PlayerProfile profile =
mock(com.destroystokyo.paper.profile.PlayerProfile.class);
when(player.getPlayerProfile()).thenReturn(profile);
when(profile.getProperties()).thenReturn(java.util.Set.of(
new com.destroystokyo.paper.profile.ProfileProperty("textures", "wave-textures", "sig")));

try (var api = mockStatic(CustomPlayer.class)) {
animation.onEmotePlay(event);
verify(scheduler).runTask(eq(plugin), task.capture());

api.when(() -> CustomPlayer.byAlreadySpawned(player)).thenReturn(null);
task.getValue().run();
api.when(() -> CustomPlayer.byAlreadySpawned(player)).thenReturn(rendered);
task.getValue().run();
verify(renderer, never()).changePlayerSkin(anyString());

when(rendered.isPlayingAnimation("wave")).thenReturn(true);
task.getValue().run();
verify(renderer).changePlayerSkin("wave-textures");

when(plugin.getLogger()).thenReturn(mock(Logger.class));
api.when(() -> CustomPlayer.byAlreadySpawned(player))
.thenThrow(new IllegalStateException("disabled"));
assertDoesNotThrow(task.getValue()::run);
verify(plugin.getLogger()).log(any(java.util.logging.Level.class), anyString(), any(Throwable.class));
}
}

@Test
void startSkinPushSkipsMissingDataAndSurvivesRendererFailures() throws Exception {
CustomPlayer rendered = mock(CustomPlayer.class);
when(rendered.isPlayingAnimation(ItemsAdderHandsUpAnimation.EMOTE)).thenReturn(true);
com.destroystokyo.paper.profile.PlayerProfile profile =
mock(com.destroystokyo.paper.profile.PlayerProfile.class);
when(player.getPlayerProfile()).thenReturn(profile);
var other = new com.destroystokyo.paper.profile.ProfileProperty("other", "x", "sig");
var emptyTextures = new com.destroystokyo.paper.profile.ProfileProperty("textures", "", "sig");
var textures = new com.destroystokyo.paper.profile.ProfileProperty("textures", "value", "sig");
try (var api = mockStatic(CustomPlayer.class)) {
api.when(() -> CustomPlayer.byAlreadySpawned(player)).thenReturn(rendered);
when(profile.getProperties())
.thenReturn(new java.util.LinkedHashSet<>(java.util.List.of(other, emptyTextures)));
assertTrue(animation.start(player));
when(profile.getProperties()).thenReturn(java.util.Set.of(other));
assertTrue(animation.start(player));
when(profile.getProperties()).thenReturn(java.util.Set.of(textures));
assertTrue(animation.start(player));
itemsadder.m.dn renderer = mock(itemsadder.m.dn.class);
doThrow(new IllegalStateException("gone")).when(renderer).changePlayerSkin("value");
java.lang.reflect.Field field = dev.lone.itemsadder.api.CustomEntity.class.getDeclaredField("renderer");
field.setAccessible(true);
field.set(rendered, renderer);
when(plugin.getLogger()).thenReturn(mock(Logger.class));
assertTrue(animation.start(player));
verify(plugin.getLogger()).log(any(java.util.logging.Level.class), anyString(), any(Throwable.class));
}
}

@Test
void stopLeavesOtherEmotesAlone() {
CustomPlayer rendered = mock(CustomPlayer.class);
Expand Down
Loading