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.
This commit is contained in:
2026-10-19 08:21:52 +02:00
parent 230f8c521a
commit 097df865e5
3 changed files with 44 additions and 0 deletions
@@ -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<Object> 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) { void removeEntity(REntity entity) {
entity.despawn(packet -> updateEntity(entity, packet)); entity.despawn(packet -> updateEntity(entity, packet));
removeEntityFromChunk(entity); removeEntityFromChunk(entity);
@@ -76,10 +76,20 @@ public class RPlayer extends REntity implements RInteractableEntity<RPlayer> {
} }
private GameProfile saved; private GameProfile saved;
private boolean awaitingSkin = false;
@Override @Override
void list(Consumer<Object> packetSink) { void list(Consumer<Object> packetSink) {
saved = getGameProfile(); 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)); packetSink.accept(ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, saved, GameMode.CREATIVE));
} }
@@ -21,6 +21,7 @@ package de.steamwar.network;
import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.Property;
import de.steamwar.core.BountifulWrapper; import de.steamwar.core.BountifulWrapper;
import de.steamwar.core.Core;
import de.steamwar.network.handlers.InventoryHandler; import de.steamwar.network.handlers.InventoryHandler;
import de.steamwar.network.packets.PacketHandler; import de.steamwar.network.packets.PacketHandler;
import de.steamwar.network.packets.common.PlayerSkinResponsePacket; import de.steamwar.network.packets.common.PlayerSkinResponsePacket;
@@ -31,8 +32,11 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class CoreNetworkHandler extends PacketHandler { public class CoreNetworkHandler extends PacketHandler {
@@ -85,9 +89,23 @@ public class CoreNetworkHandler extends PacketHandler {
} }
}; };
private static final Map<UUID, List<Runnable>> 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 @Handler
public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) { public void handlePlayerSkinResponse(PlayerSkinResponsePacket packet) {
if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return; if (!SKIN_DATA_PROMISES.containsKey(packet.getUuid())) return;
SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature())); SKIN_DATA_PROMISES.put(packet.getUuid(), new Property("textures", packet.getSkin(), packet.getSignature()));
Bukkit.getScheduler().runTask(Core.getInstance(), () -> {
List<Runnable> listeners = SKIN_LISTENERS.remove(packet.getUuid());
if (listeners != null) listeners.forEach(Runnable::run);
});
} }
} }