From d4ae31998185f9e10b3ebdb13d029211f7a98553 Mon Sep 17 00:00:00 2001 From: RoccoDev Date: Mon, 12 Oct 2026 03:50:47 +0200 Subject: [PATCH] Allow mannequins to inherit server teams --- .../rewriter/EntityPacketRewriter1_21_9.java | 56 +++++-- .../storage/MannequinTeamStorage.java | 145 ++++++++++++++++++ .../storage/ProtocolStorables1_21_9.java | 5 + .../tracker/EntityTracker1_21_9.java | 4 + 4 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/MannequinTeamStorage.java diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/rewriter/EntityPacketRewriter1_21_9.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/rewriter/EntityPacketRewriter1_21_9.java index fa8de5788..fd70147bf 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/rewriter/EntityPacketRewriter1_21_9.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/rewriter/EntityPacketRewriter1_21_9.java @@ -23,6 +23,7 @@ import com.viaversion.viabackwards.api.rewriters.EntityRewriter; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.Protocol1_21_9To1_21_7; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.MannequinData; +import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.MannequinTeamStorage; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.PlayerRotationStorage; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.ProtocolStorables1_21_9; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.tracker.EntityTracker1_21_9; @@ -50,10 +51,8 @@ import com.viaversion.viaversion.rewriter.entitydata.EntityDataHandler; import com.viaversion.viaversion.util.ChatColorUtil; import com.viaversion.viaversion.util.Copyable; -import java.util.BitSet; -import java.util.List; -import java.util.Map; -import java.util.UUID; + +import java.util.*; import java.util.concurrent.ThreadLocalRandom; import org.checkerframework.checker.nullness.qual.Nullable; @@ -157,6 +156,32 @@ public void registerPackets() { } while (slot < 0); }); + protocol.appendClientbound(ClientboundPackets1_21_9.SET_PLAYER_TEAM, wrapper -> { + final String teamName = wrapper.get(Types.STRING, 0); + final byte action = wrapper.get(Types.BYTE, 0); + final ProtocolStorables1_21_9 storables = wrapper.user().storables(protocol); + final MannequinTeamStorage storage = storables.mannequinTeamStorage(); + if (action == 1) { + final List mannequins = storables.mannequinTeamStorage().removeTeam(teamName); + for (String name : mannequins) { + storage.setFakeTeam(wrapper.user(), name); + } + } else if (action != 2) { + final String[] entries = wrapper.passthrough(Types.STRING_ARRAY); + final String newTeam = action != 4 ? teamName : null; + for (int i = 0; i < entries.length; i++) { + final String rewritten = storage.changeTeamAndRewrite(newTeam, entries[i]); + if (rewritten == null) { + continue; + } + entries[i] = rewritten; + if (newTeam == null) { + storage.setFakeTeam(wrapper.user(), rewritten); + } + } + } + }); + // Back to more non-mannequin things protocol.registerClientbound(ClientboundPackets1_21_9.SET_ENTITY_MOTION, wrapper -> { wrapper.passthrough(Types.VAR_INT); // Entity ID @@ -276,7 +301,20 @@ private void sendInitialPlayerInfoUpdate(final UserConnection connection, final playerInfo.write(Types.BOOLEAN, true); // Show hat playerInfo.send(Protocol1_21_9To1_21_7.class); - sendPlayerTeamDisplayName(connection, mannequinData, mannequinData.displayName()); + final ProtocolStorables1_21_9 storables = connection.storables(protocol); + final String serverTeam = storables.mannequinTeamStorage().registerMannequin(mannequinData, mannequinData.name()); + if (serverTeam == null || !mannequinData.hasTeam()) { + // Still create the mannequin team even if server has team, so we can set it later if needed + sendPlayerTeamDisplayName(connection, mannequinData, mannequinData.displayName()); + } + if (serverTeam != null) { + // Server had a team for this entity already, but players want names + final PacketWrapper setTeam = PacketWrapper.create(ClientboundPackets1_21_6.SET_PLAYER_TEAM, connection); + setTeam.write(Types.STRING, serverTeam); + setTeam.write(Types.BYTE, (byte) 3); // Add entities + setTeam.write(Types.STRING_ARRAY, new String[] { mannequinData.name() }); + setTeam.send(Protocol1_21_9To1_21_7.class); + } } private void sendPlayerInfoDisplayNameUpdate(final UserConnection connection, final MannequinData mannequinData, @Nullable final Tag displayName) { @@ -294,16 +332,16 @@ private void sendPlayerInfoDisplayNameUpdate(final UserConnection connection, fi private void sendPlayerTeamDisplayName(final UserConnection connection, final MannequinData mannequinData, final Tag displayName) { // Send the display name as a team prefix - final Tag nonNullDisplayName = displayName != null ? displayName : new StringTag("Mannequin"); + boolean showName = displayName != null && (!(displayName instanceof StringTag str) || !str.getValue().isEmpty()); final PacketWrapper addTeam = PacketWrapper.create(ClientboundPackets1_21_6.SET_PLAYER_TEAM, connection); addTeam.write(Types.STRING, mannequinData.name()); addTeam.write(Types.BYTE, mannequinData.hasTeam() ? (byte) 2 : 0); // Mode - addTeam.write(Types.TRUSTED_TAG, nonNullDisplayName); // Display Name + addTeam.write(Types.TRUSTED_TAG, displayName != null ? displayName : new StringTag("Mannequin")); // Display Name addTeam.write(Types.BYTE, (byte) 0); // Flags - addTeam.write(Types.VAR_INT, 0); // Nametag visibility + addTeam.write(Types.VAR_INT, showName ? 0 : 1); // Nametag visibility addTeam.write(Types.VAR_INT, 0); // Collision rule addTeam.write(Types.VAR_INT, 15); // Color - addTeam.write(Types.TRUSTED_TAG, nonNullDisplayName); // Prefix + addTeam.write(Types.TRUSTED_TAG, displayName != null ? displayName : new StringTag("")); // Prefix addTeam.write(Types.TRUSTED_TAG, new StringTag("")); // Suffix if (!mannequinData.hasTeam()) { addTeam.write(Types.STRING_ARRAY, new String[]{mannequinData.name()}); diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/MannequinTeamStorage.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/MannequinTeamStorage.java new file mode 100644 index 000000000..c67339307 --- /dev/null +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/MannequinTeamStorage.java @@ -0,0 +1,145 @@ +/* + * This file is part of ViaBackwards - https://github.com/ViaVersion/ViaBackwards + * Copyright (C) 2016-2026 ViaVersion and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage; + +import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.Protocol1_21_9To1_21_7; +import com.viaversion.viaversion.api.connection.UserConnection; +import com.viaversion.viaversion.api.protocol.packet.PacketWrapper; +import com.viaversion.viaversion.api.type.Types; +import com.viaversion.viaversion.protocols.v1_21_5to1_21_6.packet.ClientboundPackets1_21_6; + +import java.util.*; + +public class MannequinTeamStorage { + private final Map scoreboardEntities = new HashMap<>(); + private final Map> entitiesByTeam = new HashMap<>(); + + /** + * Called when the server changes the team for an entity. + * + *

Attempts to find known mannequins by the UUID in the entry. If not found, + * it stores the team association in case a mannequin with that UUID does spawn. + * + * @param newTeam the new team, or {@code null} to leave the team + * @param entry the raw entry from the server + * @return the mannequin/player name for this entry if it should be rewritten, {@code null} otherwise + */ + public String changeTeamAndRewrite(String newTeam, String entry) { + UUID uuid; + // Only consider entity UUIDs, the server uses those to assign teams to mannequins, + // but we need player names for scoreboard teams + if (entry.length() != 36) { + return null; + } + try { + uuid = UUID.fromString(entry); + } catch (IllegalArgumentException ignored) { + return null; + } + + ScoreboardEntity entity = scoreboardEntities.get(uuid); + if (entity == null) { + if (newTeam == null) { + // Untracked entity removed from team, we don't care + return null; + } + entity = new ScoreboardEntity(uuid); + scoreboardEntities.put(uuid, entity); + } + + changeServerTeam(entity, newTeam); + + if (newTeam == null && !entity.isMannequin()) { + // Remove the team registration but only forget the entity + // now if not a mannequin. Keep entry until the mannequin is + // despawned, in case server sends another team + scoreboardEntities.remove(uuid); + } + + return entity.mannequinName; + } + + private void changeServerTeam(ScoreboardEntity entity, String newTeam) { + if (entity.serverTeam != null) { + final List oldTeam = entitiesByTeam.get(entity.serverTeam); + if (oldTeam != null) { + oldTeam.remove(entity); + } + } + entity.serverTeam = newTeam; + if (newTeam != null) { + entitiesByTeam.computeIfAbsent(newTeam, $ -> new ArrayList<>()).add(entity); + } + } + + public List removeTeam(String teamName) { + final List entities = entitiesByTeam.remove(teamName); + if (entities == null) { + return List.of(); + } + final List result = new ArrayList<>(); + for (ScoreboardEntity entity : entities) { + scoreboardEntities.remove(entity.uuid); + if (entity.isMannequin()) { + result.add(entity.mannequinName); + } + } + return result; + } + + public String registerMannequin(MannequinData data, String playerName) { + // If server set team before mannequin spawned, scoreboardEntities has it. Otherwise, + // we still register the entity so that future team changes recognize the entity as a mannequin + final ScoreboardEntity entity = scoreboardEntities.computeIfAbsent(data.uuid(), ScoreboardEntity::new); + entity.mannequinName = playerName; + return entity.serverTeam; + } + + public void unregisterMannequin(MannequinData data) { + final ScoreboardEntity entity = scoreboardEntities.get(data.uuid()); + if (entity != null && entity.serverTeam == null) { + // Remove lingering mannequin entry with no server team that was waiting + // for the mannequin to despawn + scoreboardEntities.remove(data.uuid()); + } + } + + public void setFakeTeam(UserConnection connection, String mannequinName) { + // Mannequin was removed from server team, apply our fake team again + final PacketWrapper setTeam = PacketWrapper.create(ClientboundPackets1_21_6.SET_PLAYER_TEAM, connection); + setTeam.write(Types.STRING, mannequinName); + setTeam.write(Types.BYTE, (byte) 3); // Add entities + setTeam.write(Types.STRING_ARRAY, new String[]{mannequinName}); + setTeam.send(Protocol1_21_9To1_21_7.class); + } + + private static class ScoreboardEntity { + private final UUID uuid; + private String serverTeam; + // Set to VB's mannequin team name (= mannequin profile name) if entity is a known mannequin, null otherwise + private String mannequinName; + + private ScoreboardEntity(UUID uuid) { + this.uuid = uuid; + } + + private boolean isMannequin() { + return mannequinName != null; + } + } +} diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/ProtocolStorables1_21_9.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/ProtocolStorables1_21_9.java index efdcc19c3..b084ca6a6 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/ProtocolStorables1_21_9.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/storage/ProtocolStorables1_21_9.java @@ -25,6 +25,7 @@ public final class ProtocolStorables1_21_9 extends ProtocolStorablesBase { private final PlayerRotationStorage playerRotationStorage = new PlayerRotationStorage(); private final DimensionScaleStorage dimensionScaleStorage = new DimensionScaleStorage(); private final BundleStateTracker bundleStateTracker = new BundleStateTracker(); + private final MannequinTeamStorage mannequinTeamStorage = new MannequinTeamStorage(); public PlayerRotationStorage playerRotationStorage() { return playerRotationStorage; @@ -37,4 +38,8 @@ public DimensionScaleStorage dimensionScaleStorage() { public BundleStateTracker bundleStateTracker() { return bundleStateTracker; } + + public MannequinTeamStorage mannequinTeamStorage() { + return mannequinTeamStorage; + } } diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/tracker/EntityTracker1_21_9.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/tracker/EntityTracker1_21_9.java index a73bce729..c40157e76 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/tracker/EntityTracker1_21_9.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_21_9to1_21_7/tracker/EntityTracker1_21_9.java @@ -19,6 +19,7 @@ import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.Protocol1_21_9To1_21_7; import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.MannequinData; +import com.viaversion.viabackwards.protocol.v1_21_9to1_21_7.storage.ProtocolStorables1_21_9; import com.viaversion.viaversion.api.connection.UserConnection; import com.viaversion.viaversion.api.data.entity.TrackedEntity; import com.viaversion.viaversion.api.minecraft.entities.EntityType; @@ -47,6 +48,9 @@ public EntityTracker1_21_9(final UserConnection connection, @Nullable final Enti public void sendRemovePacket(final TrackedEntity entity) { final MannequinData trackedEntityId = entity.get(MannequinData.class); if (trackedEntityId != null) { + final ProtocolStorables1_21_9 storables = user().storables(Protocol1_21_9To1_21_7.class); + storables.mannequinTeamStorage().unregisterMannequin(trackedEntityId); + final PacketWrapper playerInfoRemove = PacketWrapper.create(ClientboundPackets1_21_6.PLAYER_INFO_REMOVE, user()); playerInfoRemove.write(Types.UUID_ARRAY, new UUID[]{trackedEntityId.uuid()}); playerInfoRemove.send(Protocol1_21_9To1_21_7.class);