Skip to content
Open
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 @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<String> 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
Expand Down Expand Up @@ -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) {
Expand All @@ -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()});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<UUID, ScoreboardEntity> scoreboardEntities = new HashMap<>();
private final Map<String, List<ScoreboardEntity>> entitiesByTeam = new HashMap<>();

/**
* Called when the server changes the team for an entity.
*
* <p>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<ScoreboardEntity> 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<String> removeTeam(String teamName) {
final List<ScoreboardEntity> entities = entitiesByTeam.remove(teamName);
if (entities == null) {
return List.of();
}
final List<String> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,4 +38,8 @@ public DimensionScaleStorage dimensionScaleStorage() {
public BundleStateTracker bundleStateTracker() {
return bundleStateTracker;
}

public MannequinTeamStorage mannequinTeamStorage() {
return mannequinTeamStorage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down