Skip to content

Commit b5939eb

Browse files
committed
Made claude get it to work kind of?
Signed-off-by: BT (calcastor/mame) <43831917+calcastor@users.noreply.github.com>
1 parent bd3058d commit b5939eb

2 files changed

Lines changed: 273 additions & 40 deletions

File tree

platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/ModernPlayerUtils.java

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
import com.comphenix.protocol.events.PacketEvent;
66
import com.destroystokyo.paper.profile.CraftPlayerProfile;
77
import com.destroystokyo.paper.profile.PlayerProfile;
8-
import com.google.common.collect.ImmutableMultimap;
9-
import com.mojang.authlib.GameProfile;
10-
import com.mojang.authlib.properties.Property;
11-
import com.mojang.authlib.properties.PropertyMap;
128
import dev.pgm.community.util.PlayerUtils;
139
import dev.pgm.community.util.Supports;
1410
import dev.pgm.community.utils.MessageUtils;
@@ -25,7 +21,6 @@
2521
import net.minecraft.server.level.ServerPlayer;
2622
import net.minecraft.world.scores.PlayerTeam;
2723
import net.minecraft.world.scores.Scoreboard;
28-
import org.apache.commons.lang3.StringUtils;
2924
import org.bukkit.Material;
3025
import org.bukkit.craftbukkit.entity.CraftPlayer;
3126
import org.bukkit.entity.Player;
@@ -89,7 +84,7 @@ public Skin getPlayerSkin(Player player, Player viewer) {
8984
if (playerSkins.containsKey(player.getUniqueId())) {
9085
Map<UUID, Skin> uuidSkinMap = playerSkins.get(player.getUniqueId());
9186
Skin skin = uuidSkinMap.get(viewer.getUniqueId());
92-
if (skin != null) return skin;
87+
if (skin != null && !skin.isEmpty()) return skin;
9388
}
9489
return getPlayerSkin(player);
9590
}
@@ -122,41 +117,19 @@ public ItemStack customSkull(String url, String displayName, String... lore) {
122117

123118
@Override
124119
public void refreshPlayer(@Nullable PacketEvent event, Player player, Player viewer) {
125-
String playerDisplayName = PLAYER_UTILS.getPlayerDisplayName(player, viewer);
126-
String playerName = PLAYER_UTILS.getPlayerName(player, viewer);
127-
128-
if (StringUtils.isBlank(playerName) || StringUtils.isBlank(playerDisplayName)) {
129-
return;
130-
}
131-
132-
if (event != null) event.setCancelled(true);
133-
134120
ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle();
135121
ServerPlayer viewerNms = ((CraftPlayer) viewer).getHandle();
136-
GameProfile realProfile = nmsPlayer.gameProfile;
137122

138-
Skin skin = PLAYER_UTILS.getPlayerSkin(player, viewer);
139-
ImmutableMultimap.Builder<String, Property> builder = ImmutableMultimap.builder();
140-
if (skin != null && skin.getData() != null) {
141-
builder.put("textures", new Property("textures", skin.getData(), skin.getSignature()));
142-
} else {
143-
realProfile.properties().get("textures").forEach(p -> builder.put("textures", p));
144-
}
145-
146-
nmsPlayer.gameProfile =
147-
new GameProfile(realProfile.id(), playerName, new PropertyMap(builder.build()));
123+
// Send with real profile; the PLAYER_INFO packet handler transforms name+skin if nicked
148124
viewerNms.connection.send(
149125
ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(nmsPlayer), viewerNms));
150-
nmsPlayer.gameProfile = realProfile;
151126

127+
// Send team JOIN with real name; the SCOREBOARD_TEAM handler transforms it if nicked
152128
Scoreboard scoreboard = nmsPlayer.getBukkitEntity().getScoreboard().getHandle();
153129
PlayerTeam team = scoreboard.getPlayersTeam(player.getName());
154-
155130
if (team != null) {
156-
var teamPacket = ClientboundSetPlayerTeamPacket.createPlayerPacket(
157-
team, playerName, ClientboundSetPlayerTeamPacket.Action.ADD);
158-
159-
viewerNms.connection.send(teamPacket);
131+
viewerNms.connection.send(ClientboundSetPlayerTeamPacket.createPlayerPacket(
132+
team, player.getName(), ClientboundSetPlayerTeamPacket.Action.ADD));
160133
}
161134
}
162135
}

platform/platform-modern/src/main/java/dev/pgm/community/platform/modern/PacketManipulations.java

Lines changed: 268 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,298 @@
33
import static dev.pgm.community.util.PlayerUtils.PLAYER_UTILS;
44

55
import com.comphenix.protocol.PacketType;
6+
import com.comphenix.protocol.ProtocolLibrary;
67
import com.comphenix.protocol.events.ListenerPriority;
8+
import com.comphenix.protocol.events.PacketContainer;
79
import com.comphenix.protocol.events.PacketEvent;
8-
import com.comphenix.protocol.wrappers.PlayerInfoData;
10+
import com.mojang.authlib.GameProfile;
11+
import io.netty.channel.Channel;
12+
import io.netty.channel.ChannelHandlerContext;
13+
import io.netty.channel.ChannelOutboundHandlerAdapter;
14+
import io.netty.channel.ChannelPromise;
15+
import io.papermc.paper.profile.MutablePropertyMap;
16+
import java.lang.reflect.Field;
17+
import java.util.ArrayList;
18+
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.HashMap;
921
import java.util.List;
1022
import java.util.Map;
23+
import java.util.Set;
1124
import java.util.UUID;
25+
import java.util.WeakHashMap;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket;
28+
import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket;
1229
import org.bukkit.Bukkit;
30+
import org.bukkit.craftbukkit.entity.CraftPlayer;
1331
import org.bukkit.entity.Player;
32+
import org.bukkit.event.EventHandler;
33+
import org.bukkit.event.EventPriority;
34+
import org.bukkit.event.Listener;
35+
import org.bukkit.event.player.PlayerJoinEvent;
36+
import org.bukkit.event.player.PlayerQuitEvent;
1437
import org.bukkit.plugin.Plugin;
1538
import tc.oc.pgm.platform.modern.packets.PacketSender;
1639
import tc.oc.pgm.platform.modern.util.Packets;
40+
import tc.oc.pgm.platform.modern.util.Skins;
1741

18-
public class PacketManipulations implements PacketSender {
42+
public class PacketManipulations implements PacketSender, Listener {
43+
44+
private static final String HANDLER_NAME = "community_nickname_team";
45+
46+
private static final Field PLAYER_INFO_ENTRIES_FIELD;
47+
private static final Field TEAM_PLAYERS_FIELD;
48+
private static final ThreadLocal<Boolean> SENDING = ThreadLocal.withInitial(() -> false);
49+
50+
static {
51+
try {
52+
PLAYER_INFO_ENTRIES_FIELD =
53+
ClientboundPlayerInfoUpdatePacket.class.getDeclaredField("entries");
54+
PLAYER_INFO_ENTRIES_FIELD.setAccessible(true);
55+
TEAM_PLAYERS_FIELD = ClientboundSetPlayerTeamPacket.class.getDeclaredField("players");
56+
TEAM_PLAYERS_FIELD.setAccessible(true);
57+
} catch (NoSuchFieldException e) {
58+
throw new ExceptionInInitializerError(e);
59+
}
60+
}
61+
62+
private final Plugin plugin;
63+
64+
// Cache of per-viewer fake names for players who have just quit, keyed by real name.
65+
// Populated at LOWEST priority in onPlayerQuit (before other plugins like PGM may send
66+
// LEAVE packets), so we can substitute the fake name in SCOREBOARD_TEAM LEAVE packets
67+
// that arrive after the player has been removed from the server player list.
68+
// ConcurrentHashMap because the Netty IO thread reads this while the main thread writes.
69+
// Entries are evicted after one tick.
70+
private final Map<String, Map<UUID, String>> offlineFakeNames = new ConcurrentHashMap<>();
1971

2072
public PacketManipulations(Plugin plugin) {
73+
this.plugin = plugin;
2174
Packets.register(
2275
plugin,
2376
ListenerPriority.LOWEST,
2477
Map.of(PacketType.Play.Server.PLAYER_INFO, this::handlePlayerInfo));
78+
plugin.getServer().getPluginManager().registerEvents(this, plugin);
79+
}
80+
81+
@EventHandler(priority = EventPriority.MONITOR)
82+
public void onPlayerJoin(PlayerJoinEvent event) {
83+
injectHandler(event.getPlayer());
84+
}
85+
86+
// LOWEST priority so the cache is populated before any plugin (including PGM) that may
87+
// call team.removeEntry() inside its own quit handler, which sends LEAVE packets synchronously.
88+
@EventHandler(priority = EventPriority.LOWEST)
89+
public void onPlayerQuit(PlayerQuitEvent event) {
90+
Player quitter = event.getPlayer();
91+
removeHandler(quitter);
92+
93+
// Cache per-viewer fake names while the player is still in the player list.
94+
// If any LEAVE packet is sent after they are removed (player == null from getPlayerExact),
95+
// we can still substitute the correct fake name for each viewer.
96+
Map<UUID, String> fakeNames = null;
97+
for (Player viewer : Bukkit.getOnlinePlayers()) {
98+
if (viewer.equals(quitter)) continue;
99+
String fakeName = PLAYER_UTILS.getPlayerName(quitter, viewer);
100+
if (!fakeName.equals(quitter.getName())) {
101+
if (fakeNames == null) fakeNames = new HashMap<>();
102+
fakeNames.put(viewer.getUniqueId(), fakeName);
103+
}
104+
}
105+
106+
if (fakeNames != null) {
107+
String realName = quitter.getName();
108+
offlineFakeNames.put(realName, fakeNames);
109+
// One tick is enough: LEAVE packets are sent synchronously during disconnect cleanup.
110+
plugin.getServer().getScheduler().runTask(plugin, () -> offlineFakeNames.remove(realName));
111+
}
112+
}
113+
114+
private void injectHandler(Player player) {
115+
Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel;
116+
if (channel.pipeline().get(HANDLER_NAME) == null) {
117+
channel
118+
.pipeline()
119+
.addAfter(
120+
"packet_handler", HANDLER_NAME, new ScoreboardTeamHandler(player.getUniqueId()));
121+
}
122+
}
123+
124+
private void removeHandler(Player player) {
125+
try {
126+
Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel;
127+
if (channel.pipeline().get(HANDLER_NAME) != null) {
128+
channel.pipeline().remove(HANDLER_NAME);
129+
}
130+
} catch (Exception ignored) {
131+
}
25132
}
26133

27134
private void handlePlayerInfo(PacketEvent event) {
135+
if (SENDING.get()) return;
136+
ClientboundPlayerInfoUpdatePacket nms =
137+
(ClientboundPlayerInfoUpdatePacket) event.getPacket().getHandle();
138+
139+
Set<ClientboundPlayerInfoUpdatePacket.Action> actions = nms.actions();
140+
boolean hasAddPlayer = actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER);
141+
boolean hasUpdateDisplayName =
142+
actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME);
143+
144+
// ADD_PLAYER carries the profile (name + skin) that needs substitution.
145+
// UPDATE_DISPLAY_NAME carries a tab-list display name that may contain the real name.
146+
// All other actions carry no player-identifying strings, so nothing to transform.
147+
if (!hasAddPlayer && !hasUpdateDisplayName) return;
148+
28149
Player viewer = event.getPlayer();
150+
List<ClientboundPlayerInfoUpdatePacket.Entry> entries = nms.entries();
151+
List<ClientboundPlayerInfoUpdatePacket.Entry> modified = null;
152+
153+
for (int i = 0; i < entries.size(); i++) {
154+
ClientboundPlayerInfoUpdatePacket.Entry entry = entries.get(i);
29155

30-
List<PlayerInfoData> dataList = event.getPacket().getPlayerInfoDataLists().read(0);
31-
for (PlayerInfoData playerInfoData : dataList) {
32-
if (playerInfoData == null) continue;
33-
UUID playerId = playerInfoData.getProfileId();
34-
Player player = Bukkit.getPlayer(playerId);
156+
Player player = Bukkit.getPlayer(entry.profileId());
35157
if (player == null || player.equals(viewer) || !player.isOnline()) continue;
36158

37-
PLAYER_UTILS.refreshPlayer(event, player, viewer);
159+
String fakeName = PLAYER_UTILS.getPlayerName(player, viewer);
160+
if (fakeName.equals(player.getName())) continue;
161+
162+
// Build the fake profile if this packet initialises the player.
163+
GameProfile profile = entry.profile();
164+
if (hasAddPlayer && profile != null) {
165+
profile = new GameProfile(entry.profileId(), fakeName, new MutablePropertyMap());
166+
Skins.toProfile(profile, PLAYER_UTILS.getPlayerSkin(player, viewer));
167+
}
168+
169+
if (modified == null) modified = new ArrayList<>(entries);
170+
modified.set(
171+
i,
172+
new ClientboundPlayerInfoUpdatePacket.Entry(
173+
entry.profileId(),
174+
profile,
175+
entry.listed(),
176+
entry.latency(),
177+
entry.gameMode(),
178+
// Null out the display name so the client falls back to the profile name
179+
// (the fake name). This prevents any real-name-based decoration that PGM
180+
// may have put in the display name from leaking through the tab list.
181+
null,
182+
entry.showHat(),
183+
entry.listOrder(),
184+
entry.chatSession()));
185+
}
186+
187+
if (modified != null) {
188+
// The same NMS packet object is broadcast to all online players. Mutating it in-place
189+
// would corrupt the data seen by viewers processed after this one. Clone and resend.
190+
PacketContainer clone = event.getPacket().deepClone();
191+
try {
192+
PLAYER_INFO_ENTRIES_FIELD.set(clone.getHandle(), List.copyOf(modified));
193+
SENDING.set(true);
194+
ProtocolLibrary.getProtocolManager().sendServerPacket(viewer, clone, false);
195+
} catch (Exception e) {
196+
e.printStackTrace();
197+
return;
198+
} finally {
199+
SENDING.set(false);
200+
}
201+
event.setCancelled(true);
202+
}
203+
}
204+
205+
/**
206+
* Per-player Netty outbound handler that intercepts ClientboundSetPlayerTeamPacket before it
207+
* reaches the encoder. Runs on the player's Netty IO thread — one instance per connected player,
208+
* so all state here is naturally per-viewer with no cross-player sharing.
209+
*
210+
* <p>Handles two cases:
211+
*
212+
* <ul>
213+
* <li>Name substitution: replaces a nicked player's real name with their fake name.
214+
* <li>Duplicate suppression: PGM broadcasts to all party scoreboards (25+), so the same NMS
215+
* packet object is queued to this player's channel multiple times. Identity deduplication
216+
* via a WeakHashMap ensures only the first delivery is forwarded; duplicates are dropped.
217+
* The NMS object remains GC-alive for all duplicate deliveries because Netty holds a
218+
* reference in the pending write queue, so WeakHashMap entries are stable for the entire
219+
* duplicate run and are naturally reclaimed afterwards.
220+
* </ul>
221+
*/
222+
private class ScoreboardTeamHandler extends ChannelOutboundHandlerAdapter {
223+
224+
private final UUID viewerUUID;
225+
226+
// Identity set: the first time we see a given NMS packet object, we forward it (possibly
227+
// with name substitution). Any subsequent write() call with the same object is a duplicate
228+
// from the 25× scoreboard broadcast and is dropped. WeakHashMap ensures entries are
229+
// reclaimed once the packet is no longer referenced by Netty's pending write queue.
230+
private final Set<Object> seen = Collections.newSetFromMap(new WeakHashMap<>());
231+
232+
ScoreboardTeamHandler(UUID viewerUUID) {
233+
this.viewerUUID = viewerUUID;
234+
}
235+
236+
@Override
237+
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
238+
throws Exception {
239+
if (!(msg instanceof ClientboundSetPlayerTeamPacket packet)
240+
|| packet.getPlayerAction() == null) {
241+
super.write(ctx, msg, promise);
242+
return;
243+
}
244+
245+
// Drop duplicate deliveries of the same NMS object.
246+
if (!seen.add(packet)) {
247+
promise.setSuccess();
248+
return;
249+
}
250+
251+
Player viewer = Bukkit.getPlayer(viewerUUID);
252+
if (viewer == null) {
253+
super.write(ctx, msg, promise);
254+
return;
255+
}
256+
257+
Collection<String> players = packet.getPlayers();
258+
List<String> playerList = players instanceof List<String> l ? l : new ArrayList<>(players);
259+
List<String> modified = null;
260+
boolean isLeave = packet.getPlayerAction() == ClientboundSetPlayerTeamPacket.Action.REMOVE;
261+
262+
for (int i = 0; i < playerList.size(); i++) {
263+
String name = playerList.get(i);
264+
Player player = Bukkit.getPlayerExact(name);
265+
String fakeName = null;
266+
267+
if (player != null && player.isOnline()) {
268+
fakeName = PLAYER_UTILS.getPlayerName(player, viewer);
269+
if (fakeName.equals(name)) fakeName = null;
270+
} else if (isLeave) {
271+
// Player is offline. Use the fake name cached in onPlayerQuit if available.
272+
Map<UUID, String> offlineFakes = offlineFakeNames.get(name);
273+
if (offlineFakes != null) {
274+
fakeName = offlineFakes.get(viewerUUID);
275+
}
276+
}
277+
278+
if (fakeName != null) {
279+
if (modified == null) modified = new ArrayList<>(playerList);
280+
modified.set(i, fakeName);
281+
}
282+
}
283+
284+
if (modified == null) {
285+
super.write(ctx, msg, promise);
286+
return;
287+
}
288+
289+
// Temporarily replace the players list in-place, encode, then restore.
290+
// Synchronized on the packet because the same NMS object is queued to multiple players'
291+
// channels, which may be on different Netty IO threads.
292+
synchronized (packet) {
293+
Collection<String> original = packet.getPlayers();
294+
TEAM_PLAYERS_FIELD.set(packet, modified);
295+
super.write(ctx, msg, promise);
296+
TEAM_PLAYERS_FIELD.set(packet, original);
297+
}
38298
}
39299
}
40300
}

0 commit comments

Comments
 (0)