From 097df865e52643f8301836f3d8c0b6c90693160d Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sun, 18 Oct 2026 18:28:37 +0000 Subject: [PATCH] Resend replay player tab entry when skin arrives RPlayer requests the skin asynchronously, so the first tab list packet is usually sent without textures and viewers already in range kept the default skin. Re-send despawn/delist/list/spawn to the current viewers once the skin response is received. --- .../src/de/steamwar/entity/REntityServer.java | 16 ++++++++++++++++ .../src/de/steamwar/entity/RPlayer.java | 10 ++++++++++ .../steamwar/network/CoreNetworkHandler.java | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java index 7a48f553..bff64bec 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/REntityServer.java @@ -141,6 +141,22 @@ public class REntityServer implements Listener { } } + /** + * Re-sends list and spawn packets of an already added entity to all current viewers, + * e.g. when information that is part of the tab list entry changed after the entity was added. + */ + void refreshEntity(REntity entity) { + if (!entityMap.containsKey(entity.entityId)) return; + + for (Player player : players.getOrDefault(entityToId(entity), emptyPlayers)) { + Consumer sink = packet -> TinyProtocol.instance.sendPacket(player, packet); + if (!entity.isHidden()) entity.despawn(sink); + entity.delist(sink); + entity.list(sink); + if (!entity.isHidden()) entity.spawn(sink); + } + } + void removeEntity(REntity entity) { entity.despawn(packet -> updateEntity(entity, packet)); removeEntityFromChunk(entity); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java index cfb6304d..b57e1ee3 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RPlayer.java @@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity { } private GameProfile saved; + private boolean awaitingSkin = false; @Override void list(Consumer packetSink) { saved = getGameProfile(); + if (saved.getProperties().isEmpty() && !awaitingSkin) { + // The skin is requested asynchronously, so it is usually not available yet when the entity is created. + // Without a refresh, players that already see this entity would keep the default skin. + awaitingSkin = true; + CoreNetworkHandler.onSkinReceived(actualUUID, () -> { + awaitingSkin = false; + server.refreshEntity(this); + }); + } packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE)); } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java index bc5d471e..a5fe8cf5 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/network/CoreNetworkHandler.java @@ -21,6 +21,7 @@ package de.steamwar.network; import com.mojang.authlib.properties.Property; import de.steamwar.core.BountifulWrapper; +import de.steamwar.core.Core; import de.steamwar.network.handlers.InventoryHandler; import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.common.PlayerSkinResponsePacket; @@ -31,8 +32,11 @@ import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; public class CoreNetworkHandler extends PacketHandler { @@ -85,9 +89,23 @@ public class CoreNetworkHandler extends PacketHandler { } }; + private static final Map> SKIN_LISTENERS = new ConcurrentHashMap<>(); + + /** + * Runs the given callback on the main thread once the skin of the given player has been received. + */ + public static void onSkinReceived(UUID uuid, Runnable callback) { + SKIN_LISTENERS.computeIfAbsent(uuid, __ -> new CopyOnWriteArrayList<>()).add(callback); + } + @Handler public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) { if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return; SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature())); + + Bukkit.getScheduler().runTask(Core.getInstance(), () -> { + List listeners = SKIN_LISTENERS.remove(packet.getUuid()); + if (listeners != null) listeners.forEach(Runnable::run); + }); } }