Scoreboard rework (#4947)

* Initial version of the great scoreboard rework

* Fixed some issues and added some initial tests

* Addressed review

* Added CubeCraft's scoreboard as a test, and fixed a discovered bug

* Removed var usage for primitives and String, removed star imports
This commit is contained in:
Tim203 2024-10-08 19:26:46 +02:00 committed by GitHub
parent c656e415f3
commit ef4acb121f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 4838 additions and 1110 deletions

View file

@ -61,6 +61,7 @@ dependencies {
// Test
testImplementation(libs.junit)
testImplementation(libs.mockito)
// Annotation Processors
compileOnly(projects.ap)

View file

@ -25,6 +25,12 @@
package org.geysermc.geyser.entity.type;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@ -35,12 +41,18 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.*;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.EntityUtils;
@ -55,12 +67,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEnt
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import java.util.*;
@Getter
@Setter
public class Entity implements GeyserEntity {
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
protected final GeyserSession session;
@ -68,6 +77,12 @@ public class Entity implements GeyserEntity {
protected int entityId;
protected final long geyserId;
protected UUID uuid;
/**
* Do not call this setter directly!
* This will bypass the scoreboard and setting the metadata
*/
@Setter(AccessLevel.NONE)
protected String nametag = "";
protected Vector3f position;
protected Vector3f motion;
@ -97,7 +112,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE)
private float boundingBoxWidth;
@Setter(AccessLevel.NONE)
protected String nametag = "";
private String displayName;
@Setter(AccessLevel.NONE)
protected boolean silent = false;
/* Metadata end */
@ -126,11 +141,12 @@ public class Entity implements GeyserEntity {
public Entity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
this.session = session;
this.definition = definition;
this.displayName = standardDisplayName();
this.entityId = entityId;
this.geyserId = geyserId;
this.uuid = uuid;
this.definition = definition;
this.motion = motion;
this.yaw = yaw;
this.pitch = pitch;
@ -341,7 +357,7 @@ public class Entity implements GeyserEntity {
* Sends the Bedrock metadata to the client
*/
public void updateBedrockMetadata() {
if (!valid) {
if (!isValid()) {
return;
}
@ -410,17 +426,81 @@ public class Entity implements GeyserEntity {
return 300;
}
public String teamIdentifier() {
return uuid.toString();
}
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// displayName is shown when always display name is enabled. Either with or without team.
// That's why there are both a displayName and a nametag variable.
// Displayname is ignored for players, and is always their username.
Optional<Component> name = entityMetadata.getValue();
if (name.isPresent()) {
nametag = MessageTranslator.convertMessage(name.get(), session.locale());
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
} else if (!nametag.isEmpty()) {
// Clear nametag
dirtyMetadata.put(EntityDataTypes.NAME, "");
String displayName = MessageTranslator.convertMessage(name.get(), session.locale());
this.displayName = displayName;
setNametag(displayName, true);
return;
}
// if no displayName is set, use entity name (ENDER_DRAGON -> Ender Dragon)
// maybe we can/should use a translatable here instead?
this.displayName = standardDisplayName();
setNametag(null, true);
}
protected String standardDisplayName() {
return EntityUtils.translatedEntityName(definition.entityType(), session);
}
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// ensure that the team format is used when nametag changes
if (nametag != null && fromDisplayName) {
var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier());
if (team != null) {
updateNametag(team);
return;
}
}
if (nametag == null) {
nametag = "";
}
boolean changed = !Objects.equals(this.nametag, nametag);
this.nametag = nametag;
// we only update metadata if the value has changed
if (!changed) {
return;
}
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
// if nametag (player with team) is hidden for player, so should the score (belowname)
scoreVisibility(!nametag.isEmpty());
}
public void updateNametag(@Nullable Team team) {
// allow LivingEntity+ to have a different visibility check
updateNametag(team, true);
}
protected void updateNametag(@Nullable Team team, boolean visible) {
if (team != null) {
String newNametag;
// (team) visibility is LivingEntity+, team displayName is Entity+
if (visible) {
newNametag = team.displayName(getDisplayName());
} else {
// The name is not visible to the session player; clear name
newNametag = "";
}
setNametag(newNametag, false);
return;
}
// The name has reset, if it was previously something else
setNametag(null, false);
}
protected void scoreVisibility(boolean show) {}
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
}

View file

@ -25,6 +25,11 @@
package org.geysermc.geyser.entity.type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@ -45,6 +50,7 @@ import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.util.AttributeUtils;
@ -65,12 +71,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.level.particle.EntityEffect
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle;
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
import java.util.*;
@Getter
@Setter
public class LivingEntity extends Entity {
protected ItemData helmet = ItemData.AIR;
protected ItemData chestplate = ItemData.AIR;
protected ItemData leggings = ItemData.AIR;
@ -150,6 +153,16 @@ public class LivingEntity extends Entity {
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
}
@Override
public void updateNametag(@Nullable Team team) {
// if name not visible, don't mark it as visible
updateNametag(team, team == null || team.isVisibleFor(session.getPlayerEntity().getUsername()));
}
public void hideNametag() {
setNametag("", false);
}
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
byte xd = entityMetadata.getPrimitiveValue();

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.entity.type.living.animal;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@ -32,11 +33,13 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import java.util.UUID;
public class RabbitEntity extends AnimalEntity {
private boolean isKillerBunny;
public RabbitEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@ -46,7 +49,7 @@ public class RabbitEntity extends AnimalEntity {
int variant = entityMetadata.getPrimitiveValue();
// Change the killer bunny to display as white since it only exists on Java Edition
boolean isKillerBunny = variant == 99;
isKillerBunny = variant == 99;
if (isKillerBunny) {
variant = 1;
}
@ -56,6 +59,14 @@ public class RabbitEntity extends AnimalEntity {
dirtyMetadata.put(EntityDataTypes.VARIANT, variant);
}
@Override
protected String standardDisplayName() {
if (isKillerBunny) {
return EntityUtils.translatedEntityName(Key.key("killer_bunny"), session);
}
return super.standardDisplayName();
}
@Override
protected float getAdultSize() {
return 0.55f;
@ -71,4 +82,4 @@ public class RabbitEntity extends AnimalEntity {
protected ItemTag getFoodTag() {
return ItemTag.RABBIT_FOOD;
}
}
}

View file

@ -25,6 +25,12 @@
package org.geysermc.geyser.entity.type.player;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
@ -32,53 +38,35 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.Ability;
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
import org.cloudburstmc.protocol.bedrock.data.GameType;
import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Score;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
@Getter @Setter
public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@ -96,6 +84,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
private String username;
private String cachedScore = "";
private boolean scoreVisible = true;
/**
* The textures property from the GameProfile.
*/
@ -123,6 +114,31 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.texturesProperty = texturesProperty;
}
/**
* Do not use! For testing purposes only
*/
public PlayerEntity(GeyserSession session, long geyserId, UUID uuid, String username) {
super(
session,
-1,
geyserId,
uuid,
EntityDefinition.builder(null).type(EntityType.PLAYER).build(false),
Vector3f.ZERO,
Vector3f.ZERO,
0,
0,
0
);
this.username = username;
this.nametag = username;
this.texturesProperty = null;
// clear initial metadata
dirtyMetadata.apply(new EntityDataMap());
setFlagsDirty(false);
}
@Override
protected void initializeMetadata() {
super.initializeMetadata();
@ -132,17 +148,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override
public void spawnEntity() {
// Check to see if the player should have a belowname counterpart added
Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (objective != null) {
setBelowNameText(objective);
}
// Update in case this entity has been despawned, then respawned
this.nametag = this.username;
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
addPlayerPacket.setUuid(uuid);
addPlayerPacket.setUsername(username);
@ -177,6 +182,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Since we re-use player entities: Clear flags, held item, etc
this.resetMetadata();
this.nametag = username;
this.hand = ItemData.AIR;
this.offhand = ItemData.AIR;
this.boots = ItemData.AIR;
@ -386,38 +392,30 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
}
}
@Override
public String getDisplayName() {
return username;
}
@Override
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// Doesn't do anything for players
}
//todo this will become common entity logic once UUID support is implemented for them
public void updateDisplayName(@Nullable Team team) {
boolean needsUpdate;
if (team != null) {
String newDisplayName;
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.getColor();
String chatColor = MessageTranslator.toChatColor(color);
// We have to emulate what modern Java text already does for us and add the color to each section
String prefix = team.getCurrentData().getPrefix();
String suffix = team.getCurrentData().getSuffix();
newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix;
} else {
// The name is not visible to the session player; clear name
newDisplayName = "";
}
needsUpdate = !newDisplayName.equals(this.nametag);
this.nametag = newDisplayName;
} else {
// The name has reset, if it was previously something else
needsUpdate = !this.nametag.equals(this.username);
this.nametag = this.username;
}
@Override
public String teamIdentifier() {
return username;
}
if (needsUpdate) {
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag);
@Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that
// setNametag is called again with fromDisplayName on false
if (nametag == null && !fromDisplayName) {
// nametag = null means reset, so reset it back to username
nametag = username;
}
super.setNametag(nametag, fromDisplayName);
}
@Override
@ -425,6 +423,33 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Doesn't do anything for players
}
public void setBelowNameText(String text) {
if (text == null) {
text = "";
}
boolean changed = !Objects.equals(cachedScore, text);
cachedScore = text;
if (isScoreVisible() && changed) {
dirtyMetadata.put(EntityDataTypes.SCORE, text);
}
}
@Override
protected void scoreVisibility(boolean show) {
boolean visibilityChanged = scoreVisible != show;
scoreVisible = show;
if (!visibilityChanged) {
return;
}
// if the player has no cachedScore, we never have to change the score.
// hide = set to "" (does nothing), show = change from "" (does nothing)
if (cachedScore.isEmpty()) {
return;
}
dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
}
@Override
protected void setDimensions(Pose pose) {
float height;
@ -451,64 +476,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
setBoundingBoxHeight(height);
}
public void setBelowNameText(Objective objective) {
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
Score score = objective.getScores().get(username);
String numberString;
NumberFormat numberFormat;
int amount;
if (score != null) {
amount = score.getScore();
numberFormat = score.getNumberFormat();
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
} else {
amount = 0;
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof BlankFormat) {
numberString = "";
} else if (numberFormat instanceof FixedFormat fixedFormat) {
numberString = MessageTranslator.convertMessage(fixedFormat.getValue());
} else if (numberFormat instanceof StyledFormat styledFormat) {
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
styledAmount.putString("text", String.valueOf(amount));
numberString = MessageTranslator.convertJsonMessage(
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
} else {
numberString = String.valueOf(amount);
}
String displayString = numberString + " " + ChatColor.RESET + objective.getDisplayName();
if (valid) {
// Already spawned - we still need to run the rest of this code because the spawn packet will be
// providing the information
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, displayString);
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, displayString);
}
} else {
if (valid) {
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, "");
}
}
}
/**
* @return the UUID that should be used when dealing with Bedrock's tab list.
*/

View file

@ -25,185 +25,100 @@
package org.geysermc.geyser.scoreboard;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
@Getter
public final class Objective {
private final Scoreboard scoreboard;
private final long id;
private boolean active = true;
private final List<DisplaySlot> activeSlots = new ArrayList<>();
@Setter
private UpdateType updateType = UpdateType.ADD;
private final String objectiveName;
private final Map<String, ScoreReference> scores = new ConcurrentHashMap<>();
private String objectiveName;
private ScoreboardPosition displaySlot;
private String displaySlotName;
private String displayName = "unknown";
private String displayName;
private NumberFormat numberFormat;
private int type = 0; // 0 = integer, 1 = heart
private ScoreType type;
private Map<String, Score> scores = new ConcurrentHashMap<>();
private Objective(Scoreboard scoreboard) {
this.id = scoreboard.getNextId().getAndIncrement();
this.scoreboard = scoreboard;
}
/**
* /!\ This method is made for temporary objectives until the real objective is received
*
* @param scoreboard the scoreboard
* @param objectiveName the name of the objective
*/
public Objective(Scoreboard scoreboard, String objectiveName) {
this(scoreboard);
this.scoreboard = scoreboard;
this.objectiveName = objectiveName;
this.active = false;
}
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
this(scoreboard);
this.objectiveName = objectiveName;
this.displaySlot = displaySlot;
this.displaySlotName = translateDisplaySlot(displaySlot);
this.displayName = displayName;
this.type = type;
}
private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
return switch (displaySlot) {
case BELOW_NAME -> "belowname";
case PLAYER_LIST -> "list";
default -> "sidebar";
};
}
public void registerScore(String id, int score, Component displayName, NumberFormat numberFormat) {
if (!scores.containsKey(id)) {
long scoreId = scoreboard.getNextId().getAndIncrement();
Score scoreObject = new Score(scoreId, id)
.setScore(score)
.setTeam(scoreboard.getTeamFor(id))
.setDisplayName(displayName)
.setNumberFormat(numberFormat)
.setUpdateType(UpdateType.ADD);
scores.put(id, scoreObject);
if (scores.containsKey(id)) {
return;
}
var reference = new ScoreReference(scoreboard, id, score, displayName, numberFormat);
scores.put(id, reference);
for (var slot : activeSlots) {
slot.addScore(reference);
}
}
public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) {
Score stored = scores.get(id);
ScoreReference stored = scores.get(id);
if (stored != null) {
stored.setScore(score)
.setDisplayName(displayName)
.setNumberFormat(numberFormat)
.setUpdateType(UpdateType.UPDATE);
stored.updateProperties(scoreboard, score, displayName, numberFormat);
return;
}
registerScore(id, score, displayName, numberFormat);
}
public void removeScore(String id) {
Score stored = scores.get(id);
ScoreReference stored = scores.remove(id);
if (stored != null) {
stored.setUpdateType(UpdateType.REMOVE);
stored.markDeleted();
}
}
/**
* Used internally to remove a score from the score map
*/
public void removeScore0(String id) {
scores.remove(id);
}
public void updateProperties(Component displayNameComponent, ScoreType type, NumberFormat format) {
String displayName = MessageTranslator.convertMessageRaw(displayNameComponent, scoreboard.session().locale());
boolean changed = !Objects.equals(this.displayName, displayName) || this.type != type;
public Objective setDisplayName(String displayName) {
this.displayName = displayName;
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
return this;
}
this.type = type;
public Objective setNumberFormat(NumberFormat numberFormat) {
if (Objects.equals(this.numberFormat, numberFormat)) {
return this;
}
this.numberFormat = numberFormat;
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
// Update the number format for scores that are following this objective's number format
for (Score score : scores.values()) {
if (score.getNumberFormat() == null) {
score.setUpdateType(UpdateType.UPDATE);
if (!Objects.equals(this.numberFormat, format)) {
this.numberFormat = format;
// update the number format for scores that are following this objective's number format,
// but only if the objective itself doesn't need to be updated.
// When the objective itself has to update all scores are updated anyway
if (!changed) {
for (ScoreReference score : scores.values()) {
if (score.numberFormat() == null) {
score.markChanged();
}
}
}
}
return this;
}
public Objective setType(int type) {
this.type = type;
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
return this;
}
public void setActive(ScoreboardPosition displaySlot) {
if (!active) {
active = true;
this.displaySlot = displaySlot;
displaySlotName = translateDisplaySlot(displaySlot);
if (changed) {
for (DisplaySlot slot : activeSlots) {
slot.markNeedsUpdate();
}
}
}
/**
* The objective will be removed on the next update
*/
public void pendingRemove() {
updateType = UpdateType.REMOVE;
public boolean hasDisplaySlot() {
return !activeSlots.isEmpty();
}
public @Nullable TeamColor getTeamColor() {
return switch (displaySlot) {
case SIDEBAR_TEAM_RED -> TeamColor.RED;
case SIDEBAR_TEAM_AQUA -> TeamColor.AQUA;
case SIDEBAR_TEAM_BLUE -> TeamColor.BLUE;
case SIDEBAR_TEAM_GOLD -> TeamColor.GOLD;
case SIDEBAR_TEAM_GRAY -> TeamColor.GRAY;
case SIDEBAR_TEAM_BLACK -> TeamColor.BLACK;
case SIDEBAR_TEAM_GREEN -> TeamColor.GREEN;
case SIDEBAR_TEAM_WHITE -> TeamColor.WHITE;
case SIDEBAR_TEAM_YELLOW -> TeamColor.YELLOW;
case SIDEBAR_TEAM_DARK_RED -> TeamColor.DARK_RED;
case SIDEBAR_TEAM_DARK_AQUA -> TeamColor.DARK_AQUA;
case SIDEBAR_TEAM_DARK_BLUE -> TeamColor.DARK_BLUE;
case SIDEBAR_TEAM_DARK_GRAY -> TeamColor.DARK_GRAY;
case SIDEBAR_TEAM_DARK_GREEN -> TeamColor.DARK_GREEN;
case SIDEBAR_TEAM_DARK_PURPLE -> TeamColor.DARK_PURPLE;
case SIDEBAR_TEAM_LIGHT_PURPLE -> TeamColor.LIGHT_PURPLE;
default -> null;
};
public void addDisplaySlot(DisplaySlot slot) {
activeSlots.add(slot);
}
public void removed() {
active = false;
updateType = UpdateType.REMOVE;
scores = null;
public void removeDisplaySlot(DisplaySlot slot) {
activeSlots.remove(slot);
}
}

View file

@ -1,199 +0,0 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.Objects;
@Getter
@Accessors(chain = true)
public final class Score {
private final long id;
private final String name;
private ScoreInfo cachedInfo;
/**
* Changes that have been made since the last cached data.
*/
private final Score.ScoreData currentData;
/**
* The data that is currently displayed to the Bedrock client.
*/
private Score.ScoreData cachedData;
public Score(long id, String name) {
this.id = id;
this.name = name;
this.currentData = new ScoreData();
}
public String getDisplayName() {
String displayName = cachedData.displayName;
if (displayName != null) {
return displayName;
}
Team team = cachedData.team;
if (team != null) {
return team.getDisplayName(name);
}
return name;
}
public int getScore() {
return currentData.getScore();
}
public Score setScore(int score) {
currentData.score = score;
return this;
}
public Team getTeam() {
return currentData.team;
}
public Score setTeam(Team team) {
if (currentData.team != null && team != null) {
if (!currentData.team.equals(team)) {
currentData.team = team;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
// simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (currentData.team != null || team != null) {
currentData.team = team;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public Score setDisplayName(Component displayName) {
if (currentData.displayName != null && displayName != null) {
String convertedDisplayName = MessageTranslator.convertMessage(displayName);
if (!currentData.displayName.equals(convertedDisplayName)) {
currentData.displayName = convertedDisplayName;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
// simplified from (this.displayName != null && displayName == null) || (this.displayName == null && displayName != null)
if (currentData.displayName != null || displayName != null) {
currentData.displayName = MessageTranslator.convertMessage(displayName);
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public NumberFormat getNumberFormat() {
return currentData.numberFormat;
}
public Score setNumberFormat(NumberFormat numberFormat) {
if (!Objects.equals(currentData.numberFormat, numberFormat)) {
currentData.numberFormat = numberFormat;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public UpdateType getUpdateType() {
return currentData.updateType;
}
public Score setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.changed = true;
}
currentData.updateType = updateType;
return this;
}
public boolean shouldUpdate() {
return cachedData == null || currentData.changed ||
(currentData.team != null && currentData.team.shouldUpdate());
}
public void update(Objective objective) {
if (cachedData == null) {
cachedData = new ScoreData();
cachedData.updateType = UpdateType.ADD;
if (currentData.updateType == UpdateType.REMOVE) {
cachedData.updateType = UpdateType.REMOVE;
}
} else {
cachedData.updateType = currentData.updateType;
}
currentData.changed = false;
cachedData.team = currentData.team;
cachedData.score = currentData.score;
cachedData.displayName = currentData.displayName;
cachedData.numberFormat = currentData.numberFormat;
String name = this.name;
if (cachedData.displayName != null) {
name = cachedData.displayName;
} else if (cachedData.team != null) {
cachedData.team.prepareUpdate();
name = cachedData.team.getDisplayName(name);
}
NumberFormat numberFormat = cachedData.numberFormat;
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof FixedFormat fixedFormat) {
name += " " + ChatColor.RESET + MessageTranslator.convertMessage(fixedFormat.getValue());
}
cachedInfo = new ScoreInfo(id, objective.getObjectiveName(), cachedData.score, name);
}
@Getter
public static final class ScoreData {
private UpdateType updateType;
private boolean changed;
private Team team;
private int score;
private String displayName;
private NumberFormat numberFormat;
private ScoreData() {
updateType = UpdateType.ADD;
}
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard;
import java.util.Objects;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
public final class ScoreReference {
public static final long LAST_UPDATE_DEFAULT = -1;
private static final long LAST_UPDATE_REMOVE = -2;
private final String name;
private final boolean hidden;
private String displayName;
private int score;
private NumberFormat numberFormat;
private long lastUpdate;
public ScoreReference(
Scoreboard scoreboard, String name, int score, Component displayName, NumberFormat format) {
this.name = name;
// hidden is a sidebar exclusive feature
this.hidden = name.startsWith("#");
updateProperties(scoreboard, score, displayName, format);
this.lastUpdate = LAST_UPDATE_DEFAULT;
}
public String name() {
return name;
}
public boolean hidden() {
return hidden;
}
public String displayName() {
return displayName;
}
public void displayName(Component displayName, Scoreboard scoreboard) {
if (this.displayName != null && displayName != null) {
String convertedDisplayName = MessageTranslator.convertMessage(displayName, scoreboard.session().locale());
if (!this.displayName.equals(convertedDisplayName)) {
this.displayName = convertedDisplayName;
markChanged();
}
return;
}
// simplified from (this.displayName != null && displayName == null) || (this.displayName == null && displayName != null)
if (this.displayName != null || displayName != null) {
this.displayName = MessageTranslator.convertMessage(displayName, scoreboard.session().locale());
markChanged();
}
}
public int score() {
return score;
}
private void score(int score) {
boolean changed = this.score != score;
this.score = score;
if (changed) {
markChanged();
}
}
public NumberFormat numberFormat() {
return numberFormat;
}
private void numberFormat(NumberFormat numberFormat) {
if (Objects.equals(numberFormat(), numberFormat)) {
return;
}
this.numberFormat = numberFormat;
markChanged();
}
public void updateProperties(Scoreboard scoreboard, int score, Component displayName, NumberFormat numberFormat) {
score(score);
displayName(displayName, scoreboard);
numberFormat(numberFormat);
}
public long lastUpdate() {
return lastUpdate;
}
public boolean isRemoved() {
return lastUpdate == LAST_UPDATE_REMOVE;
}
public void markChanged() {
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
lastUpdate = System.currentTimeMillis();
}
public void markDeleted() {
lastUpdate = -1;
}
}

View file

@ -25,43 +25,72 @@
package org.geysermc.geyser.scoreboard;
import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.display.slot.BelownameDisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.PlayerlistDisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.SidebarDisplaySlot;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.jetbrains.annotations.Contract;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.geysermc.geyser.scoreboard.UpdateType.*;
/**
* Here follows some information about how scoreboards work in Java Edition, that is related to the workings of this
* class:
* <p>
* Objectives can be divided in two states: inactive and active.
* Inactive objectives is the default state for objectives that have been created using the SetObjective packet.
* Scores can be added, updated and removed, but as long as they're inactive they aren't shown to the player.
* An objective becomes active when a SetDisplayObjective packet is received, which contains the slot that
* the objective should be displayed at.
* <p>
* While Bedrock can handle showing one objective on multiple slots at the same time, we have to help Bedrock a bit
* for example by limiting the amount of sidebar scores to the amount of lines that can be shown
* (otherwise Bedrock may lag) and only showing online players in the playerlist (otherwise it's too cluttered.)
* This fact is the biggest contributor for the class being structured like it is.
*/
public final class Scoreboard {
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
private final GeyserSession session;
private final GeyserLogger logger;
@Getter
private final AtomicLong nextId = new AtomicLong(0);
private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
@Getter
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class);
private final Map<ScoreboardPosition, DisplaySlot> objectiveSlots = Collections.synchronizedMap(new EnumMap<>(ScoreboardPosition.class));
private final List<DisplaySlot> removedSlots = Collections.synchronizedList(new ArrayList<>());
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
/**
* Required to preserve vanilla behavior, which also uses a map.
@ -71,6 +100,7 @@ public final class Scoreboard {
@Getter
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
private final AtomicBoolean updateLockActive = new AtomicBoolean(false);
private int lastAddScoreCount = 0;
private int lastRemoveScoreCount = 0;
@ -80,24 +110,22 @@ public final class Scoreboard {
}
public void removeScoreboard() {
Iterator<Objective> iterator = objectives.values().iterator();
while (iterator.hasNext()) {
Objective objective = iterator.next();
iterator.remove();
var copy = new HashMap<>(objectiveSlots);
objectiveSlots.clear();
deleteObjective(objective, false);
for (DisplaySlot slot : copy.values()) {
slot.remove();
}
}
public @Nullable Objective registerNewObjective(String objectiveId) {
Objective objective = objectives.get(objectiveId);
if (objective != null) {
// we have no other choice, or we have to make a new map?
// if the objective hasn't been deleted, we have to force it
if (objective.getUpdateType() != REMOVE) {
return null;
// matches vanilla behaviour
if (SHOW_SCOREBOARD_LOGS) {
logger.warning("An objective with the same name '" + objectiveId + "' already exists! Ignoring new objective!");
}
deleteObjective(objective, true);
return null;
}
objective = new Objective(this, objectiveId);
@ -105,273 +133,162 @@ public final class Scoreboard {
return objective;
}
public void displayObjective(String objectiveId, ScoreboardPosition displaySlot) {
public void displayObjective(String objectiveId, ScoreboardPosition slot) {
if (objectiveId.isEmpty()) {
// matches vanilla behaviour
var display = objectiveSlots.get(slot);
if (display != null) {
removedSlots.add(display);
objectiveSlots.remove(slot, display);
var objective = display.objective();
objective.removeDisplaySlot(display);
}
return;
}
Objective objective = objectives.get(objectiveId);
if (objective == null) {
return;
}
if (!objective.isActive()) {
objective.setActive(displaySlot);
// for reactivated objectives
objective.setUpdateType(ADD);
var display = objectiveSlots.get(slot);
if (display != null && display.objective() != objective) {
removedSlots.add(display);
}
Objective storedObjective = objectiveSlots.get(displaySlot);
if (storedObjective != null && storedObjective != objective) {
storedObjective.pendingRemove();
}
objectiveSlots.put(displaySlot, objective);
if (displaySlot == ScoreboardPosition.BELOW_NAME) {
// Display the below name score option to all players
// Of note: unlike Bedrock, if there is an objective in the below name slot, everyone has a display
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (!entity.isValid()) {
// Player hasn't spawned yet - don't bother, it'll be done then
continue;
}
entity.setBelowNameText(objective);
}
}
display = switch (DisplaySlot.slotCategory(slot)) {
case SIDEBAR -> new SidebarDisplaySlot(session, objective, slot);
case BELOW_NAME -> new BelownameDisplaySlot(session, objective);
case PLAYER_LIST -> new PlayerlistDisplaySlot(session, objective);
default -> throw new IllegalStateException("Unexpected value: " + slot);
};
objectiveSlots.put(slot, display);
objective.addDisplaySlot(display);
}
public Team registerNewTeam(String teamName, String[] players) {
public void registerNewTeam(
String teamName,
String[] players,
Component name,
Component prefix,
Component suffix,
NameTagVisibility visibility,
TeamColor color
) {
Team team = teams.get(teamName);
if (team != null) {
if (SHOW_SCOREBOARD_LOGS) {
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
}
return team;
return;
}
team = new Team(this, teamName);
team.addEntities(players);
team = new Team(this, teamName, players, name, prefix, suffix, visibility, color);
teams.put(teamName, team);
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
if (ADD_TEAM_SUGGESTIONS) {
session.addCommandEnum("Geyser_Teams", team.getId());
session.addCommandEnum("Geyser_Teams", team.id());
}
return team;
}
public void onUpdate() {
// if an update is already running, let it finish
if (updateLockActive.getAndSet(true)) {
return;
}
List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount);
List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount);
List<Objective> removedObjectives = new ArrayList<>();
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
Objective correctSidebar = null;
DisplaySlot correctSidebarSlot = null;
for (Objective objective : objectives.values()) {
// objective has been deleted
if (objective.getUpdateType() == REMOVE) {
removedObjectives.add(objective);
for (DisplaySlot slot : objectiveSlots.values()) {
// slot has been removed
if (slot.updateType() == REMOVE) {
continue;
}
// there's nothing we can do with inactive objectives
// after checking if the objective has been deleted,
// except waiting for the objective to become activated (:
if (!objective.isActive()) {
continue;
}
if (playerTeam != null && playerTeam.getColor() == objective.getTeamColor()) {
correctSidebar = objective;
if (playerTeam != null && playerTeam.color() == slot.teamColor()) {
correctSidebarSlot = slot;
}
}
if (correctSidebar == null) {
correctSidebar = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
if (correctSidebarSlot == null) {
correctSidebarSlot = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
}
for (Objective objective : removedObjectives) {
var actualRemovedSlots = new ArrayList<>(removedSlots);
for (var slot : actualRemovedSlots) {
// Deletion must be handled before the active objectives are handled - otherwise if a scoreboard display is changed before the current
// scoreboard is removed, the client can crash
deleteObjective(objective, true);
slot.remove();
}
removedSlots.removeAll(actualRemovedSlots);
handleObjective(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
handleObjective(correctSidebar, addScores, removeScores);
handleObjective(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
Iterator<Team> teamIterator = teams.values().iterator();
while (teamIterator.hasNext()) {
Team current = teamIterator.next();
switch (current.getCachedUpdateType()) {
case ADD, UPDATE -> current.markUpdated();
case REMOVE -> teamIterator.remove();
}
}
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
handleDisplaySlot(correctSidebarSlot, addScores, removeScores);
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
if (!removeScores.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.REMOVE);
setScorePacket.setInfos(removeScores);
session.sendUpstreamPacket(setScorePacket);
SetScorePacket packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(removeScores);
session.sendUpstreamPacket(packet);
}
if (!addScores.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.SET);
setScorePacket.setInfos(addScores);
session.sendUpstreamPacket(setScorePacket);
SetScorePacket packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(addScores);
session.sendUpstreamPacket(packet);
}
lastAddScoreCount = addScores.size();
lastRemoveScoreCount = removeScores.size();
updateLockActive.set(false);
}
private void handleObjective(Objective objective, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
if (objective == null || objective.getUpdateType() == REMOVE) {
return;
private void handleDisplaySlot(DisplaySlot slot, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
if (slot != null) {
slot.render(addScores, removeScores);
}
// hearts can't hold teams, so we treat them differently
if (objective.getType() == 1) {
for (Score score : objective.getScores().values()) {
boolean update = score.shouldUpdate();
if (update) {
score.update(objective);
}
if (score.getUpdateType() != REMOVE && update) {
addScores.add(score.getCachedInfo());
}
if (score.getUpdateType() != ADD && update) {
removeScores.add(score.getCachedInfo());
}
}
return;
}
boolean objectiveAdd = objective.getUpdateType() == ADD;
boolean objectiveUpdate = objective.getUpdateType() == UPDATE;
for (Score score : objective.getScores().values()) {
if (score.getUpdateType() == REMOVE) {
ScoreInfo cachedInfo = score.getCachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
}
// score is pending to be removed, so we can remove it from the objective
objective.removeScore0(score.getName());
break;
}
Team team = score.getTeam();
boolean add = objectiveAdd || objectiveUpdate;
if (team != null) {
if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
score.setTeam(null);
add = true;
}
}
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.getCachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && score.getUpdateType() != ADD && !(objectiveUpdate || objectiveAdd)) {
removeScores.add(score.getCachedInfo());
}
score.setUpdateType(NOTHING);
}
if (objectiveUpdate) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
}
if (objectiveAdd || objectiveUpdate) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());
displayObjectivePacket.setCriteria("dummy");
displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName());
displayObjectivePacket.setSortOrder(1); // 0 = ascending, 1 = descending
session.sendUpstreamPacket(displayObjectivePacket);
}
objective.setUpdateType(NOTHING);
}
/**
* @param remove if we should remove the objective from the objectives map.
*/
public void deleteObjective(Objective objective, boolean remove) {
if (remove) {
objectives.remove(objective.getObjectiveName());
}
objectiveSlots.remove(objective.getDisplaySlot(), objective);
objective.removed();
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
}
public Objective getObjective(String objectiveName) {
return objectives.get(objectiveName);
}
public Collection<Objective> getObjectives() {
return objectives.values();
}
public void unregisterObjective(String objectiveName) {
Objective objective = getObjective(objectiveName);
if (objective != null) {
objective.pendingRemove();
public void removeObjective(Objective objective) {
objectives.remove(objective.getObjectiveName());
for (DisplaySlot slot : objective.getActiveSlots()) {
objectiveSlots.remove(slot.position(), slot);
removedSlots.add(slot);
}
}
public Objective getSlot(ScoreboardPosition slot) {
return objectiveSlots.get(slot);
public void resetPlayerScores(String playerNameOrEntityUuid) {
for (Objective objective : objectives.values()) {
objective.removeScore(playerNameOrEntityUuid);
}
}
public Team getTeam(String teamName) {
return teams.get(teamName);
}
public Team getTeamFor(String entity) {
return playerToTeam.get(entity);
public Team getTeamFor(String playerNameOrEntityUuid) {
return playerToTeam.get(playerNameOrEntityUuid);
}
public void removeTeam(String teamName) {
Team remove = teams.remove(teamName);
if (remove != null) {
remove.setUpdateType(REMOVE);
// We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly
// With the player's lack of a team in visibility checks
updateEntityNames(remove, remove.getEntities(), true);
for (String name : remove.getEntities()) {
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
playerToTeam.remove(name);
}
session.removeCommandEnum("Geyser_Teams", remove.getId());
if (remove == null) {
return;
}
remove.remove();
session.removeCommandEnum("Geyser_Teams", remove.id());
}
@Contract("-> new")
@ -381,48 +298,46 @@ public final class Scoreboard {
(o1, o2) -> o1, LinkedHashMap::new));
}
/**
* Updates the display names of all entities in a given team.
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
*/
public void updateEntityNames(Team team, boolean teamChange) {
Set<String> names = new HashSet<>(team.getEntities());
updateEntityNames(team, names, teamChange);
public void playerRegistered(PlayerEntity player) {
for (DisplaySlot slot : objectiveSlots.values()) {
slot.playerRegistered(player);
}
}
/**
* Updates the display name of a set of entities within a given team. The team may also be null if the set is being removed
* from a team.
*/
public void updateEntityNames(@Nullable Team team, Set<String> names, boolean teamChange) {
if (names.remove(session.getPlayerEntity().getUsername()) && teamChange) {
// If the player's team changed, then other entities' teams may modify their visibility based on team status
refreshSessionPlayerDisplays();
public void playerRemoved(PlayerEntity player) {
for (DisplaySlot slot : objectiveSlots.values()) {
slot.playerRemoved(player);
}
if (!names.isEmpty()) {
for (Entity entity : session.getEntityCache().getEntities().values()) {
// This more complex logic is for the future to iterate over all entities, not just players
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
player.updateDisplayName(team);
player.updateBedrockMetadata();
if (names.isEmpty()) {
break;
}
}
}
public void entityRegistered(Entity entity) {
var team = getTeamFor(entity.teamIdentifier());
if (team != null) {
team.onEntitySpawn(entity);
}
}
public void entityRemoved(Entity entity) {
var team = getTeamFor(entity.teamIdentifier());
if (team != null) {
team.onEntityRemove(entity);
}
}
public void setTeamFor(Team team, Set<String> entities) {
for (DisplaySlot slot : objectiveSlots.values()) {
// only sidebar slots use teams
if (slot instanceof SidebarDisplaySlot sidebar) {
sidebar.setTeamFor(team, entities);
}
}
}
/**
* If the team's player was refreshed, then we need to go through every entity and check...
*/
private void refreshSessionPlayerDisplays() {
for (Entity entity : session.getEntityCache().getEntities().values()) {
if (entity instanceof PlayerEntity player) {
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
player.updateDisplayName(playerTeam);
player.updateBedrockMetadata();
}
}
public long nextId() {
return nextId.getAndIncrement();
}
public GeyserSession session() {
return session;
}
}

View file

@ -173,7 +173,6 @@ public final class ScoreboardUpdater extends Thread {
@Getter
public static final class ScoreboardSession {
private final GeyserSession session;
@SuppressWarnings("WriteOnlyObject")
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
private int packetsPerSecond;
private long lastUpdate;

View file

@ -25,48 +25,66 @@
package org.geysermc.geyser.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.HashSet;
import java.util.Set;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
@Getter
@Accessors(chain = true)
public final class Team {
public static final long LAST_UPDATE_DEFAULT = -1;
private static final long LAST_UPDATE_REMOVE = -2;
private final Scoreboard scoreboard;
private final String id;
@Getter(AccessLevel.PACKAGE)
private final Set<String> entities;
private final Set<Entity> managedEntities;
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
@Setter private TeamColor color;
private TeamColor color;
private final TeamData currentData;
private TeamData cachedData;
private String name;
private String prefix;
private String suffix;
private long lastUpdate;
private boolean updating;
public Team(Scoreboard scoreboard, String id) {
public Team(
Scoreboard scoreboard,
String id,
String[] players,
Component name,
Component prefix,
Component suffix,
NameTagVisibility visibility,
TeamColor color
) {
this.scoreboard = scoreboard;
this.id = id;
currentData = new TeamData();
entities = new ObjectOpenHashSet<>();
this.entities = new ObjectOpenHashSet<>();
this.managedEntities = new ObjectOpenHashSet<>();
this.lastUpdate = LAST_UPDATE_DEFAULT;
// doesn't call entity update
updateProperties(name, prefix, suffix, visibility, color);
// calls entity update
addEntities(players);
lastUpdate = LAST_UPDATE_DEFAULT;
}
public Set<String> addEntities(String... names) {
public void addEntities(String... names) {
Set<String> added = new HashSet<>();
for (String name : names) {
if (entities.add(name)) {
added.add(name);
// go to next score if score is already present
if (!entities.add(name)) {
continue;
}
added.add(name);
scoreboard.getPlayerToTeam().compute(name, (player, oldTeam) -> {
if (oldTeam != null) {
// Remove old team from this map, and from the set of players of the old team.
@ -78,26 +96,15 @@ public final class Team {
}
if (added.isEmpty()) {
return added;
return;
}
// we don't have to change the updateType,
// because the scores itself need updating, not the team
for (Objective objective : scoreboard.getObjectives()) {
for (String addedEntity : added) {
Score score = objective.getScores().get(addedEntity);
if (score != null) {
score.setTeam(this);
}
}
}
return added;
// we don't have to change our updateType,
// because the scores themselves need updating, not the team
scoreboard.setTeamFor(this, added);
addAddedEntities(added);
}
/**
* @return all removed entities from this team
*/
public Set<String> removeEntities(String... names) {
public void removeEntities(String... names) {
Set<String> removed = new HashSet<>();
for (String name : names) {
if (entities.remove(name)) {
@ -105,87 +112,22 @@ public final class Team {
}
scoreboard.getPlayerToTeam().remove(name, this);
}
return removed;
removeRemovedEntities(removed);
}
public boolean hasEntity(String name) {
return entities.contains(name);
}
public Team setName(String name) {
currentData.name = name;
return this;
}
public Team setPrefix(String prefix) {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (prefix.length() == 4 && "null".equals(prefix)) {
currentData.prefix = "";
return this;
public String displayName(String score) {
String chatColor = ChatColor.chatColorFor(color);
// most sidebar plugins will use the reset color, because they don't want color
// skip the unneeded double reset color in that case
if (ChatColor.RESET.equals(chatColor)) {
chatColor = "";
}
currentData.prefix = prefix;
return this;
}
public Team setSuffix(String suffix) {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (suffix.length() == 4 && "null".equals(suffix)) {
currentData.suffix = "";
return this;
}
currentData.suffix = suffix;
return this;
}
public String getDisplayName(String score) {
return cachedData != null ?
cachedData.getDisplayName(score) :
currentData.getDisplayName(score);
}
public void markUpdated() {
updating = false;
}
public boolean shouldUpdate() {
return updating || cachedData == null || currentData.changed;
}
public void prepareUpdate() {
if (updating) {
return;
}
updating = true;
if (cachedData == null) {
cachedData = new TeamData();
cachedData.updateType = currentData.updateType != UpdateType.REMOVE ? UpdateType.ADD : UpdateType.REMOVE;
} else {
cachedData.updateType = currentData.updateType;
}
currentData.changed = false;
cachedData.name = currentData.name;
cachedData.prefix = currentData.prefix;
cachedData.suffix = currentData.suffix;
}
public UpdateType getUpdateType() {
return currentData.updateType;
}
public UpdateType getCachedUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType;
}
public Team setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.changed = true;
}
currentData.updateType = updateType;
return this;
// also add reset because setting the color does not reset the formatting, unlike Java
return chatColor + prefix + ChatColor.RESET + chatColor + score + ChatColor.RESET + chatColor + suffix;
}
public boolean isVisibleFor(String entity) {
@ -201,34 +143,178 @@ public final class Team {
};
}
public Team setNameTagVisibility(@Nullable NameTagVisibility nameTagVisibility) {
if (nameTagVisibility != null) {
// Null check like this (and this.nameTagVisibility defaults to ALWAYS) as of Java 1.19.4
this.nameTagVisibility = nameTagVisibility;
public void updateProperties(Component name, Component prefix, Component suffix, NameTagVisibility visibility, TeamColor color) {
// this shouldn't happen but hey!
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
return this;
String oldName = this.name;
String oldPrefix = this.prefix;
String oldSuffix = this.suffix;
boolean oldVisible = isVisibleFor(playerName());
var oldColor = this.color;
this.name = MessageTranslator.convertMessageRaw(name, session().locale());
this.prefix = MessageTranslator.convertMessageRaw(prefix, session().locale());
this.suffix = MessageTranslator.convertMessageRaw(suffix, session().locale());
// matches vanilla behaviour, the visibility is not reset (to ALWAYS) if it is null.
// instead the visibility is not altered
if (visibility != null) {
this.nameTagVisibility = visibility;
}
this.color = color;
if (lastUpdate == LAST_UPDATE_DEFAULT) {
// addEntities is called after the initial updateProperties, so no need to do any entity updates here
if (this.color != TeamColor.RESET || !this.prefix.isEmpty() || !this.suffix.isEmpty()) {
markChanged();
}
return;
}
if (!this.name.equals(oldName)
|| !this.prefix.equals(oldPrefix)
|| !this.suffix.equals(oldSuffix)
|| color != oldColor) {
markChanged();
updateEntities();
return;
}
if (isVisibleFor(playerName()) != oldVisible) {
// if just the visibility changed, we only have to update the entities.
// We don't have to mark it as changed
updateEntities();
}
}
public boolean shouldRemove() {
return lastUpdate == LAST_UPDATE_REMOVE;
}
public void markChanged() {
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
lastUpdate = System.currentTimeMillis();
}
public void remove() {
lastUpdate = LAST_UPDATE_REMOVE;
for (String name : entities()) {
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
scoreboard.getPlayerToTeam().remove(name);
}
if (entities().contains(playerName())) {
refreshAllEntities();
return;
}
for (Entity entity : managedEntities) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
private void updateEntities() {
for (Entity entity : managedEntities) {
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
public void onEntitySpawn(Entity entity) {
// I've basically ported addAddedEntities
if (entities.contains(entity.teamIdentifier())) {
managedEntities.add(entity);
// onEntitySpawn includes all entities but players, so it cannot contain self
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
public void onEntityRemove(Entity entity) {
// we don't have to update anything, since the player is removed.
managedEntities.remove(entity);
}
private void addAddedEntities(Set<String> names) {
// can't contain self if none are added
if (names.isEmpty()) {
return;
}
boolean containsSelf = names.contains(playerName());
for (Entity entity : session().getEntityCache().getEntities().values()) {
if (names.contains(entity.teamIdentifier())) {
managedEntities.add(entity);
if (!containsSelf) {
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void removeRemovedEntities(Set<String> names) {
boolean containsSelf = names.contains(playerName());
var iterator = managedEntities.iterator();
while (iterator.hasNext()) {
var entity = iterator.next();
if (names.contains(entity.teamIdentifier())) {
iterator.remove();
if (!containsSelf) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void refreshAllEntities() {
for (Entity entity : session().getEntityCache().getEntities().values()) {
entity.updateNametag(scoreboard.getTeamFor(entity.teamIdentifier()));
entity.updateBedrockMetadata();
}
}
private GeyserSession session() {
return scoreboard.session();
}
private String playerName() {
return session().getPlayerEntity().getUsername();
}
public String id() {
return id;
}
public TeamColor color() {
return color;
}
public long lastUpdate() {
return lastUpdate;
}
public Set<String> entities() {
return entities;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Getter
public static final class TeamData {
private UpdateType updateType;
private boolean changed;
private String name;
private String prefix;
private String suffix;
private TeamData() {
updateType = UpdateType.ADD;
}
public String getDisplayName(String score) {
return prefix + score + suffix;
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public class BelownameDisplayScore extends DisplayScore {
private final PlayerEntity player;
public BelownameDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference, PlayerEntity player) {
super(slot, scoreId, reference);
this.player = player;
}
@Override
public void update(Objective objective) {}
public PlayerEntity player() {
return player;
}
@Override
public void markUpdated() {
super.markUpdated();
}
public ScoreReference reference() {
return reference;
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public abstract class DisplayScore {
protected final DisplaySlot slot;
protected final long id;
protected final ScoreReference reference;
protected long lastTeamUpdate;
protected long lastUpdate;
public DisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference) {
this.slot = slot;
this.id = scoreId;
this.reference = reference;
}
public boolean shouldUpdate() {
return reference.lastUpdate() != lastUpdate;
}
public abstract void update(Objective objective);
public String name() {
return reference.name();
}
public int score() {
return reference.score();
}
public boolean referenceRemoved() {
return reference.isRemoved();
}
protected void markUpdated() {
// with the last update (also for team) we rather have an old lastUpdate
// (and have to update again the next cycle) than potentially losing information
// by fetching the lastUpdate after update was performed
this.lastUpdate = reference.lastUpdate();
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public final class PlayerlistDisplayScore extends DisplayScore {
private final long playerId;
private ScoreInfo cachedInfo;
public PlayerlistDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference, long playerId) {
super(slot, scoreId, reference);
this.playerId = playerId;
}
@Override
public boolean shouldUpdate() {
// for player references the player's name is shown,
// so we only have to update when the score has changed
return cachedInfo == null || cachedInfo.getScore() != reference.score();
}
@Override
public void update(Objective objective) {
cachedInfo = new ScoreInfo(id, slot.objectiveId(), reference.score(), ScoreInfo.ScorerType.PLAYER, playerId);
}
public ScoreInfo cachedInfo() {
return cachedInfo;
}
public boolean exists() {
return cachedInfo != null;
}
}

View file

@ -0,0 +1,139 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import java.util.Objects;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
public final class SidebarDisplayScore extends DisplayScore {
private ScoreInfo cachedInfo;
private Team team;
private String order;
private boolean onlyScoreValueChanged;
public SidebarDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference) {
super(slot, scoreId, reference);
team(slot.objective().getScoreboard().getTeamFor(reference.name()));
}
@Override
public boolean shouldUpdate() {
return super.shouldUpdate() || shouldTeamUpdate();
}
private boolean shouldTeamUpdate() {
return team != null && team.lastUpdate() != lastTeamUpdate;
}
@Override
public void update(Objective objective) {
markUpdated();
String finalName = reference.name();
String displayName = reference.displayName();
if (displayName != null) {
finalName = displayName;
} else if (team != null) {
this.lastTeamUpdate = team.lastUpdate();
finalName = team.displayName(reference.name());
}
NumberFormat numberFormat = reference.numberFormat();
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof FixedFormat fixedFormat) {
finalName += " " + ChatColor.RESET + MessageTranslator.convertMessage(fixedFormat.getValue(), objective.getScoreboard().session().locale());
}
if (order != null) {
finalName = order + ChatColor.RESET + finalName;
}
if (cachedInfo != null) {
onlyScoreValueChanged = finalName.equals(cachedInfo.getName());
}
cachedInfo = new ScoreInfo(id, slot.objectiveId(), reference.score(), finalName);
}
public String order() {
return order;
}
public DisplayScore order(String order) {
if (Objects.equals(this.order, order)) {
return this;
}
this.order = order;
// this guarantees an update
requestUpdate();
return this;
}
public Team team() {
return team;
}
public void team(Team team) {
if (this.team != null && team != null) {
if (!this.team.equals(team)) {
this.team = team;
requestUpdate();
}
return;
}
// simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (this.team != null || team != null) {
this.team = team;
requestUpdate();
}
}
private void requestUpdate() {
this.lastUpdate = 0;
}
public ScoreInfo cachedInfo() {
return cachedInfo;
}
public boolean exists() {
return cachedInfo != null;
}
public boolean onlyScoreValueChanged() {
return onlyScoreValueChanged;
}
}

View file

@ -0,0 +1,182 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.List;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.BelownameDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public class BelownameDisplaySlot extends DisplaySlot {
private final Long2ObjectMap<BelownameDisplayScore> displayScores = new Long2ObjectOpenHashMap<>();
public BelownameDisplaySlot(GeyserSession session, Objective objective) {
super(session, objective, ScoreboardPosition.BELOW_NAME);
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
// how belowname works is that if the player itself has belowname as a display slot,
// every player entity will show a score below their name.
// when the objective is added, updated or removed we thus have to update the belowname for every player
// when an individual score is updated (score or number format) we have to update the individual player
// remove is handled in #remove()
if (updateType == UpdateType.ADD) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
playerRegistered(player);
}
return;
}
if (updateType == UpdateType.UPDATE) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
setBelowNameText(player, scoreFor(player.getUsername()));
}
updateType = UpdateType.NOTHING;
return;
}
for (var score : displayScores.values()) {
// we don't have to worry about a score not existing, because that's handled by both
// this method when an objective is added and addScore/playerRegistered.
// we only have to update them, if they have changed
// (or delete them, if the score no longer exists)
if (!score.shouldUpdate()) {
continue;
}
if (score.referenceRemoved()) {
clearBelowNameText(score.player());
continue;
}
score.markUpdated();
setBelowNameText(score.player(), score.reference());
}
}
@Override
public void remove() {
updateType = UpdateType.REMOVE;
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
clearBelowNameText(player);
}
}
@Override
public void addScore(ScoreReference reference) {
addDisplayScore(reference);
}
@Override
public void playerRegistered(PlayerEntity player) {
var reference = scoreFor(player.getUsername());
setBelowNameText(player, reference);
// keep track of score when the player is active
if (reference != null) {
// we already set the text, so we only have to update once the score does
addDisplayScore(player, reference).markUpdated();
}
}
@Override
public void playerRemoved(PlayerEntity player) {
displayScores.remove(player.getGeyserId());
}
private void addDisplayScore(ScoreReference reference) {
var players = session.getEntityCache().getPlayersByName(reference.name());
for (PlayerEntity player : players) {
addDisplayScore(player, reference);
}
}
private BelownameDisplayScore addDisplayScore(PlayerEntity player, ScoreReference reference) {
var score = new BelownameDisplayScore(this, objective.getScoreboard().nextId(), reference, player);
displayScores.put(player.getGeyserId(), score);
return score;
}
private void setBelowNameText(PlayerEntity player, ScoreReference reference) {
player.setBelowNameText(calculateBelowNameText(reference));
player.updateBedrockMetadata();
}
private void clearBelowNameText(PlayerEntity player) {
player.setBelowNameText(null);
player.updateBedrockMetadata();
}
private String calculateBelowNameText(ScoreReference reference) {
String numberString;
NumberFormat numberFormat = null;
// even if the player doesn't have a score, as long as belowname is on the client Java behaviour is
// to show them with a score of 0
int score = 0;
if (reference != null) {
score = reference.score();
numberFormat = reference.numberFormat();
}
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof BlankFormat) {
numberString = "";
} else if (numberFormat instanceof FixedFormat fixedFormat) {
numberString = MessageTranslator.convertMessage(fixedFormat.getValue(), session.locale());
} else if (numberFormat instanceof StyledFormat styledFormat) {
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
styledAmount.putString("text", String.valueOf(score));
numberString = MessageTranslator.convertJsonMessage(
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
} else {
numberString = String.valueOf(score);
}
return numberString + " " + ChatColor.RESET + objective.getDisplayName();
}
private ScoreReference scoreFor(String username) {
return objective.getScores().get(username);
}
}

View file

@ -0,0 +1,162 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
public abstract class DisplaySlot {
protected final GeyserSession session;
protected final Objective objective;
/**
* Use this instead of objective name because one objective can be shared in multiple slots,
* but each slot has its own logic and might not contain all scores
*/
protected final String objectiveId;
protected final ScoreboardPosition slot;
protected final TeamColor teamColor;
protected final String positionName;
protected UpdateType updateType = UpdateType.ADD;
public DisplaySlot(GeyserSession session, Objective objective, ScoreboardPosition slot) {
this.session = session;
this.objective = objective;
this.objectiveId = String.valueOf(objective.getScoreboard().nextId());
this.slot = slot;
this.teamColor = teamColor(slot);
this.positionName = positionName(slot);
}
public final void render(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
if (updateType == UpdateType.REMOVE) {
return;
}
render0(addScores, removeScores);
}
protected abstract void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores);
public abstract void addScore(ScoreReference reference);
public abstract void playerRegistered(PlayerEntity player);
public abstract void playerRemoved(PlayerEntity player);
public void remove() {
updateType = UpdateType.REMOVE;
sendRemoveObjective();
}
public void markNeedsUpdate() {
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
}
protected void sendDisplayObjective() {
SetDisplayObjectivePacket packet = new SetDisplayObjectivePacket();
packet.setObjectiveId(objectiveId());
packet.setDisplayName(objective.getDisplayName());
packet.setCriteria("dummy");
packet.setDisplaySlot(positionName);
packet.setSortOrder(1); // 0 = ascending, 1 = descending
session.sendUpstreamPacket(packet);
}
protected void sendRemoveObjective() {
RemoveObjectivePacket packet = new RemoveObjectivePacket();
packet.setObjectiveId(objectiveId());
session.sendUpstreamPacket(packet);
}
public Objective objective() {
return objective;
}
public String objectiveId() {
return objectiveId;
}
public ScoreboardPosition position() {
return slot;
}
public @Nullable TeamColor teamColor() {
return teamColor;
}
public UpdateType updateType() {
return updateType;
}
public static ScoreboardPosition slotCategory(ScoreboardPosition slot) {
return switch (slot) {
case BELOW_NAME -> ScoreboardPosition.BELOW_NAME;
case PLAYER_LIST -> ScoreboardPosition.PLAYER_LIST;
default -> ScoreboardPosition.SIDEBAR;
};
}
private static String positionName(ScoreboardPosition slot) {
return switch (slot) {
case BELOW_NAME -> "belowname";
case PLAYER_LIST -> "list";
default -> "sidebar";
};
}
private static @Nullable TeamColor teamColor(ScoreboardPosition slot) {
return switch (slot) {
case SIDEBAR_TEAM_RED -> TeamColor.RED;
case SIDEBAR_TEAM_AQUA -> TeamColor.AQUA;
case SIDEBAR_TEAM_BLUE -> TeamColor.BLUE;
case SIDEBAR_TEAM_GOLD -> TeamColor.GOLD;
case SIDEBAR_TEAM_GRAY -> TeamColor.GRAY;
case SIDEBAR_TEAM_BLACK -> TeamColor.BLACK;
case SIDEBAR_TEAM_GREEN -> TeamColor.GREEN;
case SIDEBAR_TEAM_WHITE -> TeamColor.WHITE;
case SIDEBAR_TEAM_YELLOW -> TeamColor.YELLOW;
case SIDEBAR_TEAM_DARK_RED -> TeamColor.DARK_RED;
case SIDEBAR_TEAM_DARK_AQUA -> TeamColor.DARK_AQUA;
case SIDEBAR_TEAM_DARK_BLUE -> TeamColor.DARK_BLUE;
case SIDEBAR_TEAM_DARK_GRAY -> TeamColor.DARK_GRAY;
case SIDEBAR_TEAM_DARK_GREEN -> TeamColor.DARK_GREEN;
case SIDEBAR_TEAM_DARK_PURPLE -> TeamColor.DARK_PURPLE;
case SIDEBAR_TEAM_LIGHT_PURPLE -> TeamColor.LIGHT_PURPLE;
default -> null;
};
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.PlayerlistDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public class PlayerlistDisplaySlot extends DisplaySlot {
private final Long2ObjectMap<PlayerlistDisplayScore> displayScores =
Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
private final List<PlayerlistDisplayScore> removedScores = Collections.synchronizedList(new ArrayList<>());
public PlayerlistDisplaySlot(GeyserSession session, Objective objective) {
super(session, objective, ScoreboardPosition.PLAYER_LIST);
registerExisting();
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
boolean objectiveAdd = updateType == UpdateType.ADD;
boolean objectiveUpdate = updateType == UpdateType.UPDATE;
boolean objectiveNothing = updateType == UpdateType.NOTHING;
// if 'add' the scores aren't present, if 'update' the objective is re-added so the scores don't have to be
// manually removed, if 'remove' the scores are removed anyway
if (objectiveNothing) {
var removedScoresCopy = new ArrayList<>(removedScores);
for (var removedScore : removedScoresCopy) {
//todo idk if this if-statement is needed
if (removedScore.cachedInfo() != null) {
removeScores.add(removedScore.cachedInfo());
}
}
removedScores.removeAll(removedScoresCopy);
} else {
removedScores.clear();
}
for (var score : displayScores.values()) {
if (score.referenceRemoved()) {
ScoreInfo cachedInfo = score.cachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
}
continue;
}
//todo does an animated title exist on tab?
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.cachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && objectiveNothing) {
removeScores.add(score.cachedInfo());
}
}
if (objectiveUpdate) {
sendRemoveObjective();
}
if (objectiveAdd || objectiveUpdate) {
sendDisplayObjective();
}
updateType = UpdateType.NOTHING;
}
@Override
public void addScore(ScoreReference reference) {
// while it breaks a lot of stuff in Java, scoreboard do work fine with multiple players having
// the same username
var players = session.getEntityCache().getPlayersByName(reference.name());
var selfPlayer = session.getPlayerEntity();
if (reference.name().equals(selfPlayer.getUsername())) {
players.add(selfPlayer);
}
for (PlayerEntity player : players) {
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
}
}
private void registerExisting() {
playerRegistered(session.getPlayerEntity());
session.getEntityCache().getAllPlayerEntities().forEach(this::playerRegistered);
}
@Override
public void playerRegistered(PlayerEntity player) {
var reference = objective.getScores().get(player.getUsername());
if (reference == null) {
return;
}
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
}
@Override
public void playerRemoved(PlayerEntity player) {
var score = displayScores.remove(player.getGeyserId());
if (score == null) {
return;
}
removedScores.add(score);
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.SidebarDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public final class SidebarDisplaySlot extends DisplaySlot {
private static final int SCORE_DISPLAY_LIMIT = 15;
private static final Comparator<ScoreReference> SCORE_DISPLAY_ORDER =
Comparator.comparing(ScoreReference::score)
.reversed()
.thenComparing(ScoreReference::name, String.CASE_INSENSITIVE_ORDER);
private List<SidebarDisplayScore> displayScores = new ArrayList<>(SCORE_DISPLAY_LIMIT);
public SidebarDisplaySlot(GeyserSession session, Objective objective, ScoreboardPosition position) {
super(session, objective, position);
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
// while one could argue that we may not have to do this fancy Java filter when there are fewer scores than the
// line limit, we would lose the correct order of the scores if we don't
var newDisplayScores =
objective.getScores().values().stream()
.filter(score -> !score.hidden())
.sorted(SCORE_DISPLAY_ORDER)
.limit(SCORE_DISPLAY_LIMIT)
.map(reference -> {
// pretty much an ArrayList#remove
var iterator = this.displayScores.iterator();
while (iterator.hasNext()) {
var score = iterator.next();
if (score.name().equals(reference.name())) {
iterator.remove();
return score;
}
}
// new score, so it should be added
return new SidebarDisplayScore(this, objective.getScoreboard().nextId(), reference);
}).collect(Collectors.toList());
// in newDisplayScores we removed the items that were already present from displayScores,
// meaning that the items that remain are items that are no longer displayed
for (var score : this.displayScores) {
removeScores.add(score.cachedInfo());
}
// preserves the new order
this.displayScores = newDisplayScores;
// fixes ordering issues with multiple entries with same score
if (!this.displayScores.isEmpty()) {
SidebarDisplayScore lastScore = null;
int count = 0;
for (var score : this.displayScores) {
if (lastScore == null) {
lastScore = score;
continue;
}
if (score.score() == lastScore.score()) {
// something to keep in mind is that Bedrock doesn't support some legacy color codes and adds some
// codes as well, so if the line limit is every increased keep that in mind
if (count == 0) {
lastScore.order(ChatColor.styleOrder(count++));
}
score.order(ChatColor.styleOrder(count++));
} else {
if (count == 0) {
lastScore.order(null);
}
count = 0;
}
lastScore = score;
}
if (count == 0 && lastScore != null) {
lastScore.order(null);
}
}
boolean objectiveAdd = updateType == UpdateType.ADD;
boolean objectiveUpdate = updateType == UpdateType.UPDATE;
for (var score : this.displayScores) {
Team team = score.team();
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
if (team != null) {
// entities are mostly removed from teams without notifying the scores.
if (team.shouldRemove() || !team.hasEntity(score.name())) {
score.team(null);
add = true;
}
}
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.cachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && !(objectiveUpdate || objectiveAdd) && !score.onlyScoreValueChanged()) {
removeScores.add(score.cachedInfo());
}
}
if (objectiveUpdate) {
sendRemoveObjective();
}
if (objectiveAdd || objectiveUpdate) {
sendDisplayObjective();
}
updateType = UpdateType.NOTHING;
}
@Override
public void addScore(ScoreReference reference) {
// we handle them a bit different: we sort the scores, and we add them ourselves
}
@Override
public void playerRegistered(PlayerEntity player) {
}
@Override
public void playerRemoved(PlayerEntity player) {
}
public void setTeamFor(Team team, Set<String> entities) {
// we only have to worry about scores that are currently displayed,
// because the constructor of the display score fetches the team
for (var score : displayScores) {
if (entities.contains(score.name())) {
score.team(team);
}
}
}
}

View file

@ -31,15 +31,18 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import lombok.Getter;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.Tickable;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Each session has its own EntityCache in the occasion that an entity packet is sent specifically
* for that player (e.g. seeing vanished players from /vanish)
@ -68,6 +71,10 @@ public class EntityCache {
if (cacheEntity(entity)) {
entity.spawnEntity();
// start tracking newly spawned entities.
// This is however not called for players, that's done in addPlayerEntity
session.getWorldCache().getScoreboard().entityRegistered(entity);
if (entity instanceof Tickable) {
// Start ticking it
tickableEntities.add((Tickable) entity);
@ -86,21 +93,24 @@ public class EntityCache {
}
public void removeEntity(Entity entity) {
if (entity == null) {
return;
}
if (entity instanceof PlayerEntity player) {
session.getPlayerWithCustomHeads().remove(player.getUuid());
}
if (entity != null) {
if (entity.isValid()) {
entity.despawnEntity();
}
if (entity.isValid()) {
entity.despawnEntity();
}
entities.remove(entityIdTranslations.remove(entity.getEntityId()));
long geyserId = entityIdTranslations.remove(entity.getEntityId());
entities.remove(geyserId);
// don't track the entity anymore, now that it's removed
session.getWorldCache().getScoreboard().entityRemoved(entity);
if (entity instanceof Tickable) {
tickableEntities.remove(entity);
}
if (entity instanceof Tickable) {
tickableEntities.remove(entity);
}
}
@ -126,15 +136,39 @@ public class EntityCache {
public void addPlayerEntity(PlayerEntity entity) {
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
playerEntities.putIfAbsent(entity.getUuid(), entity);
boolean exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
if (exists) {
return;
}
// notify scoreboard for new entity
var scoreboard = session.getWorldCache().getScoreboard();
scoreboard.playerRegistered(entity);
// spawnPlayer's entityRegistered is not called for players
scoreboard.entityRegistered(entity);
}
public PlayerEntity getPlayerEntity(UUID uuid) {
return playerEntities.get(uuid);
}
public List<PlayerEntity> getPlayersByName(String name) {
var list = new ArrayList<PlayerEntity>();
for (PlayerEntity player : playerEntities.values()) {
if (name.equals(player.getUsername())) {
list.add(player);
}
}
return list;
}
public PlayerEntity removePlayerEntity(UUID uuid) {
return playerEntities.remove(uuid);
var player = playerEntities.remove(uuid);
if (player != null) {
// notify scoreboard
session.getWorldCache().getScoreboard().playerRemoved(player);
}
return player;
}
public Collection<PlayerEntity> getAllPlayerEntities() {

View file

@ -31,6 +31,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
@ -49,7 +50,7 @@ public final class WorldCache {
@Getter
private final ScoreboardSession scoreboardSession;
@Getter
private Scoreboard scoreboard;
private @NonNull Scoreboard scoreboard;
@Getter
@Setter
private Difficulty difficulty = Difficulty.EASY;
@ -81,10 +82,8 @@ public final class WorldCache {
}
public void removeScoreboard() {
if (scoreboard != null) {
scoreboard.removeScoreboard();
scoreboard = new Scoreboard(session);
}
scoreboard.removeScoreboard();
scoreboard = new Scoreboard(session);
}
public int increaseAndGetScoreboardPacketsPerSecond() {

View file

@ -25,6 +25,8 @@
package org.geysermc.geyser.text;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
public class ChatColor {
public static final String ANSI_RESET = (char) 0x1b + "[0m";
@ -84,4 +86,58 @@ public class ChatColor {
string = string.replace(WHITE, (char) 0x1b + "[37;1m");
return string;
}
}
public static String styleOrder(int index) {
// https://bugs.mojang.com/browse/MCPE-41729
// strikethrough and underlined do not exist on Bedrock
return switch (index) {
case 0 -> BLACK;
case 1 -> DARK_BLUE;
case 2 -> DARK_GREEN;
case 3 -> DARK_AQUA;
case 4 -> DARK_RED;
case 5 -> DARK_PURPLE;
case 6 -> GOLD;
case 7 -> GRAY;
case 8 -> DARK_GRAY;
case 9 -> BLUE;
case 10 -> GREEN;
case 11 -> AQUA;
case 12 -> RED;
case 13 -> LIGHT_PURPLE;
case 14 -> YELLOW;
case 15 -> WHITE;
case 16 -> OBFUSCATED;
case 17 -> BOLD;
default -> ITALIC;
};
}
public static String chatColorFor(TeamColor teamColor) {
// https://bugs.mojang.com/browse/MCPE-41729
// strikethrough and underlined do not exist on Bedrock
return switch (teamColor) {
case BLACK -> BLACK;
case DARK_BLUE -> DARK_BLUE;
case DARK_GREEN -> DARK_GREEN;
case DARK_AQUA -> DARK_AQUA;
case DARK_RED -> DARK_RED;
case DARK_PURPLE -> DARK_PURPLE;
case GOLD -> GOLD;
case GRAY -> GRAY;
case DARK_GRAY -> DARK_GRAY;
case BLUE -> BLUE;
case GREEN -> GREEN;
case AQUA -> AQUA;
case RED -> RED;
case LIGHT_PURPLE -> LIGHT_PURPLE;
case YELLOW -> YELLOW;
case WHITE -> WHITE;
case OBFUSCATED -> OBFUSCATED;
case BOLD -> BOLD;
case STRIKETHROUGH, UNDERLINED -> "";
case ITALIC -> ITALIC;
default -> RESET;
};
}
}

View file

@ -32,40 +32,22 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
@Translator(packet = ClientboundResetScorePacket.class)
public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> {
@Override
public void translate(GeyserSession session, ClientboundResetScorePacket packet) {
WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard();
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
Objective belowName = scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (packet.getObjective() == null) {
// No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME)
for (Objective otherObjective : scoreboard.getObjectives()) {
otherObjective.removeScore(packet.getOwner());
}
// as described below
if (belowName != null) {
JavaSetScoreTranslator.setBelowName(session, belowName, packet.getOwner());
}
scoreboard.resetPlayerScores(packet.getOwner());
} else {
Objective objective = scoreboard.getObjective(packet.getObjective());
objective.removeScore(packet.getOwner());
// If this is the objective that is in use to show the below name text, we need to update the player
// attached to this score.
if (objective == belowName) {
// Update the score on this player to now reflect 0
JavaSetScoreTranslator.setBelowName(session, objective, packet.getOwner());
}
}
// ScoreboardUpdater will handle it for us if the packets per second

View file

@ -25,72 +25,45 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
@Translator(packet = ClientboundSetObjectivePacket.class)
public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> {
private final GeyserLogger logger = GeyserImpl.getInstance().getLogger();
@Override
public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) {
WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard();
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
Objective objective = scoreboard.getObjective(packet.getName());
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE && packet.getAction() == ObjectiveAction.ADD) {
// matches vanilla behaviour
logger.warning("An objective with the same name '" + packet.getName() + "' already exists! Ignoring packet");
Objective objective;
if (packet.getAction() == ObjectiveAction.ADD) {
objective = scoreboard.registerNewObjective(packet.getName());
} else {
objective = scoreboard.getObjective(packet.getName());
}
// matches vanilla
if (objective == null) {
return;
}
if ((objective == null || objective.getUpdateType() == UpdateType.REMOVE) && packet.getAction() != ObjectiveAction.REMOVE) {
objective = scoreboard.registerNewObjective(packet.getName());
}
switch (packet.getAction()) {
case ADD, UPDATE -> {
objective.setDisplayName(MessageTranslator.convertMessage(packet.getDisplayName()))
.setNumberFormat(packet.getNumberFormat())
.setType(packet.getType().ordinal());
if (objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
// Update the score tag of all players
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (entity.isValid()) {
entity.setBelowNameText(objective);
}
}
}
}
case REMOVE -> {
scoreboard.unregisterObjective(packet.getName());
if (objective != null && objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
// Clear the score tag from all players
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
// Other places we check for the entity being valid,
// but we must set the below name text as null for all players
// or else PlayerEntity#spawnEntity will find a null objective and not touch EntityData#SCORE_TAG
entity.setBelowNameText(null);
}
}
}
case ADD, UPDATE ->
objective.updateProperties(packet.getDisplayName(), packet.getType(), packet.getNumberFormat());
case REMOVE -> scoreboard.removeObjective(objective);
}
if (objective == null || !objective.isActive()) {
// Scoreboard#removeObjective doesn't touch the display slot(s) that were attached to it.
// So Objective#hasDisplaySlot will be true as long as it's currently present on the Bedrock client
if (!objective.hasDisplaySlot()) {
return;
}

View file

@ -25,23 +25,17 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import java.util.Arrays;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.Arrays;
import java.util.Set;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
@Translator(packet = ClientboundSetPlayerTeamPacket.class)
public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> {
@ -60,83 +54,45 @@ public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSet
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
Scoreboard scoreboard = session.getWorldCache().getScoreboard();
Team team = scoreboard.getTeam(packet.getTeamName());
switch (packet.getAction()) {
case CREATE -> {
team = scoreboard.registerNewTeam(packet.getTeamName(), packet.getPlayers())
.setName(MessageTranslator.convertMessage(packet.getDisplayName()))
.setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility())
.setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.locale()))
.setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.locale()));
if (packet.getPlayers().length != 0) {
if ((team.getNameTagVisibility() != NameTagVisibility.ALWAYS && !team.isVisibleFor(session.getPlayerEntity().getUsername()))
|| team.getColor() != TeamColor.RESET
|| !team.getCurrentData().getPrefix().isEmpty()
|| !team.getCurrentData().getSuffix().isEmpty()) {
// Something is here that would modify entity names
scoreboard.updateEntityNames(team, true);
}
if (packet.getAction() == TeamAction.CREATE) {
scoreboard.registerNewTeam(
packet.getTeamName(),
packet.getPlayers(),
packet.getDisplayName(),
packet.getPrefix(),
packet.getSuffix(),
packet.getNameTagVisibility(),
packet.getColor()
);
} else {
Team team = scoreboard.getTeam(packet.getTeamName());
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
case UPDATE -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
TeamColor oldColor = team.getColor();
NameTagVisibility oldVisibility = team.getNameTagVisibility();
String oldPrefix = team.getCurrentData().getPrefix();
String oldSuffix = team.getCurrentData().getSuffix();
team.setName(MessageTranslator.convertMessage(packet.getDisplayName()))
.setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility())
.setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.locale()))
.setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.locale()))
.setUpdateType(UpdateType.UPDATE);
if (oldVisibility != team.getNameTagVisibility()
|| oldColor != team.getColor()
|| !oldPrefix.equals(team.getCurrentData().getPrefix())
|| !oldSuffix.equals(team.getCurrentData().getSuffix())) {
// Update entities attached to this team as something about their nameplates have changed
scoreboard.updateEntityNames(team, false);
switch (packet.getAction()) {
case UPDATE -> {
team.updateProperties(
packet.getDisplayName(),
packet.getPrefix(),
packet.getSuffix(),
packet.getNameTagVisibility(),
packet.getColor()
);
}
case ADD_PLAYER -> team.addEntities(packet.getPlayers());
case REMOVE_PLAYER -> team.removeEntities(packet.getPlayers());
case REMOVE -> scoreboard.removeTeam(packet.getTeamName());
}
case ADD_PLAYER -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
Set<String> added = team.addEntities(packet.getPlayers());
scoreboard.updateEntityNames(team, added, true);
}
case REMOVE_PLAYER -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
Set<String> removed = team.removeEntities(packet.getPlayers());
scoreboard.updateEntityNames(null, removed, true);
}
case REMOVE -> scoreboard.removeTeam(packet.getTeamName());
}
// ScoreboardUpdater will handle it for us if the packets per second
// (for score and team packets) is higher than the first threshold
if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {

View file

@ -25,12 +25,8 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
@ -39,6 +35,7 @@ import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
@Translator(packet = ClientboundSetScorePacket.class)
public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> {
@ -63,16 +60,7 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
}
return;
}
// If this is the objective that is in use to show the below name text, we need to update the player
// attached to this score.
boolean isBelowName = objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
objective.setScore(packet.getOwner(), packet.getValue(), packet.getDisplay(), packet.getNumberFormat());
if (isBelowName) {
// Update the below name score on this player
setBelowName(session, objective, packet.getOwner());
}
// ScoreboardUpdater will handle it for us if the packets per second
// (for score and team packets) is higher than the first threshold
@ -80,36 +68,4 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
scoreboard.onUpdate();
}
}
/**
* @param objective the objective that currently resides on the below name display slot
*/
static void setBelowName(GeyserSession session, Objective objective, String username) {
PlayerEntity entity = getOtherPlayerEntity(session, username);
if (entity == null) {
return;
}
entity.setBelowNameText(objective);
}
private static @Nullable PlayerEntity getOtherPlayerEntity(GeyserSession session, String username) {
// We don't care about the session player, because... they're not going to be seeing their own score
if (session.getPlayerEntity().getUsername().equals(username)) {
return null;
}
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (entity.getUsername().equals(username)) {
if (entity.isValid()) {
return entity;
} else {
// The below name text will be applied on spawn
return null;
}
}
}
return null;
}
}

View file

@ -25,6 +25,8 @@
package org.geysermc.geyser.translator.text;
import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.ScoreComponent;
@ -53,12 +55,6 @@ import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class MessageTranslator {
// These are used for handling the translations of the messages
@ -71,9 +67,6 @@ public class MessageTranslator {
private static final LegacyComponentSerializer BEDROCK_SERIALIZER;
private static final String BEDROCK_COLORS;
// Store team colors for player names
private static final Map<TeamColor, String> TEAM_COLORS = new EnumMap<>(TeamColor.class);
// Legacy formatting character
private static final String BASE = "\u00a7";
@ -81,31 +74,6 @@ public class MessageTranslator {
private static final String RESET = BASE + "r";
static {
TEAM_COLORS.put(TeamColor.RESET, RESET);
TEAM_COLORS.put(TeamColor.BLACK, BASE + "0");
TEAM_COLORS.put(TeamColor.DARK_BLUE, BASE + "1");
TEAM_COLORS.put(TeamColor.DARK_GREEN, BASE + "2");
TEAM_COLORS.put(TeamColor.DARK_AQUA, BASE + "3");
TEAM_COLORS.put(TeamColor.DARK_RED, BASE + "4");
TEAM_COLORS.put(TeamColor.DARK_PURPLE, BASE + "5");
TEAM_COLORS.put(TeamColor.GOLD, BASE + "6");
TEAM_COLORS.put(TeamColor.GRAY, BASE + "7");
TEAM_COLORS.put(TeamColor.DARK_GRAY, BASE + "8");
TEAM_COLORS.put(TeamColor.BLUE, BASE + "9");
TEAM_COLORS.put(TeamColor.GREEN, BASE + "a");
TEAM_COLORS.put(TeamColor.AQUA, BASE + "b");
TEAM_COLORS.put(TeamColor.RED, BASE + "c");
TEAM_COLORS.put(TeamColor.LIGHT_PURPLE, BASE + "d");
TEAM_COLORS.put(TeamColor.YELLOW, BASE + "e");
TEAM_COLORS.put(TeamColor.WHITE, BASE + "f");
// Formats, not colors
TEAM_COLORS.put(TeamColor.OBFUSCATED, BASE + "k");
TEAM_COLORS.put(TeamColor.BOLD, BASE + "l");
TEAM_COLORS.put(TeamColor.STRIKETHROUGH, BASE + "m");
TEAM_COLORS.put(TeamColor.ITALIC, BASE + "o");
// Temporary fix for https://github.com/KyoriPowered/adventure/issues/447 - TODO resolve properly
GsonComponentSerializer source = DefaultComponentSerializer.get()
.toBuilder()
@ -157,13 +125,31 @@ public class MessageTranslator {
}
/**
* Convert a Java message to the legacy format ready for bedrock
* Convert a Java message to the legacy format ready for bedrock. Unlike
* {@link #convertMessageRaw(Component, String)} this adds a leading color reset. In Bedrock
* some places have build-in colors.
*
* @param message Java message
* @param locale Locale to use for translation strings
* @return Parsed and formatted message for bedrock
*/
public static String convertMessage(Component message, String locale) {
return convertMessage(message, locale, true);
}
/**
* Convert a Java message to the legacy format ready for bedrock. Unlike {@link #convertMessage(Component, String)}
* this version does not add a leading color reset. In Bedrock some places have build-in colors.
*
* @param message Java message
* @param locale Locale to use for translation strings
* @return Parsed and formatted message for bedrock
*/
public static String convertMessageRaw(Component message, String locale) {
return convertMessage(message, locale, false);
}
private static String convertMessage(Component message, String locale, boolean addLeadingResetFormat) {
try {
// Translate any components that require it
message = RENDERER.render(message, locale);
@ -172,7 +158,7 @@ public class MessageTranslator {
StringBuilder finalLegacy = new StringBuilder();
char[] legacyChars = legacy.toCharArray();
boolean lastFormatReset = false;
boolean lastFormatReset = !addLeadingResetFormat;
for (int i = 0; i < legacyChars.length; i++) {
char legacyChar = legacyChars[i];
if (legacyChar != ChatColor.ESCAPE || i >= legacyChars.length - 1) {
@ -185,7 +171,7 @@ public class MessageTranslator {
char next = legacyChars[++i];
if (BEDROCK_COLORS.indexOf(next) != -1) {
// Append this color code, as well as a necessary reset code
// Unlike Java Edition, the ChatFormatting is not reset when a ChatColor is added
if (!lastFormatReset) {
finalLegacy.append(RESET);
}
@ -378,16 +364,6 @@ public class MessageTranslator {
session.sendUpstreamPacket(textPacket);
}
/**
* Convert a team color to a chat color
*
* @param teamColor Color or format to convert
* @return The chat color character
*/
public static String toChatColor(TeamColor teamColor) {
return TEAM_COLORS.getOrDefault(teamColor, "");
}
/**
* Checks if the given message is over 256 characters (Java edition server chat limit) and sends a message to the user if it is
*

View file

@ -25,6 +25,7 @@
package org.geysermc.geyser.util;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.GameType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@ -38,6 +39,8 @@ import org.geysermc.geyser.entity.type.living.animal.AnimalEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
@ -290,6 +293,23 @@ public final class EntityUtils {
};
}
private static String translatedEntityName(String namespace, String name, GeyserSession session) {
return MinecraftLocale.getLocaleString("entity." + namespace + "." + name, session.locale());
}
public static String translatedEntityName(Key type, GeyserSession session) {
return translatedEntityName(type.namespace(), type.value(), session);
}
public static String translatedEntityName(EntityType type, GeyserSession session) {
if (type == EntityType.PLAYER) {
return "Player"; // the player's name is always shown instead
}
// this works at least with all 1.20.5 entities, except the killer bunny since that's not an entity type.
String typeName = type.name().toLowerCase(Locale.ROOT);
return translatedEntityName("minecraft", typeName, session);
}
private EntityUtils() {
}
}

View file

@ -0,0 +1,270 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.junit.jupiter.api.Test;
public class NameVisibilityScoreboardTest {
@Test
void playerVisibilityNever() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
});
}
@Test
void playerVisibilityHideForOtherTeam() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.HIDE_FOR_OTHER_TEAMS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
// only hidden if session player (Tim203) is in a team as well
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
assertNoNextPacket(context);
// create another team and add Tim203 to it
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
// Tim203 is now in another team, so it should be hidden
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
assertNoNextPacket(context);
// add Tim203 to same team as player1, score should be visible again
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
});
}
@Test
void playerVisibilityHideForOwnTeam() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.HIDE_FOR_OWN_TEAM,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
// Tim203 is not in a team (let alone the same team), so should be visible
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
assertNoNextPacket(context);
// Tim203 is now in the same team as player1, so should be hidden
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
assertNoNextPacket(context);
// create another team and add Tim203 to there, score should be visible again
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
});
}
@Test
void playerVisibilityAlways() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
// adding self to another team shouldn't make a difference
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
assertNoNextPacket(context);
// adding self to player1 team shouldn't matter
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNoNextPacket(context);
});
}
}

View file

@ -0,0 +1,227 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.belowname;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.junit.jupiter.api.Test;
public class BasicBelownameScoreboardTests {
@Test
void displayWithNoPlayersAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNoNextPacket(context);
});
}
@Test
void displayColorWithOnePlayer() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", NamedTextColor.BLUE),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §r§9objective");
return packet;
}, context);
});
}
@Test
void displayWithOnePlayerAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
});
}
@Test
void overrideAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective2")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective2");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective1")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective1");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
});
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.playerlist;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
/*
Identical to sidebar
*/
public class BasicPlayerlistScoreboardTests {
@Test
void display() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void displayNameColors() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", Style.style(NamedTextColor.AQUA, TextDecoration.BOLD)),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§b§lobjective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void overrideWithOneScore() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective1", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective2", 2));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective2")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective2");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
// session player name is Tim203
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, ScoreInfo.ScorerType.PLAYER, 1)));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective1")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("2");
packet.setDisplayName("objective1");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
// session player name is Tim203
packet.setInfos(List.of(new ScoreInfo(3, "2", 1, ScoreInfo.ScorerType.PLAYER, 1)));
return packet;
}, context);
});
}
}

View file

@ -0,0 +1,756 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.server;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class CubecraftScoreboardTest {
@Test
void test() {
mockContextScoreboard(context -> {
var setTeamTranslator = new JavaSetPlayerTeamTranslator();
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
// unused
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("SB_NoName", Component.text("SB_NoName"), Component.empty(), Component.empty(), true, true, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.RESET, new String[0]));
assertNoNextPacket(context);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"sidebar",
ObjectiveAction.ADD,
Component.text("sidebar"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "sidebar")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("sidebar");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
// Now they're going to create a bunch of teams and add players to those teams in a very inefficient way.
// Presumably this is a leftover from an old system, as these don't seem to do anything but hide their nametags.
// For which you could just use a single team.
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "A_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", TeamAction.ADD_PLAYER, new String[] { "B_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "C_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "D_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", TeamAction.ADD_PLAYER, new String[] { "E_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "F_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "G_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", TeamAction.ADD_PLAYER, new String[] { "H_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "I_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", TeamAction.ADD_PLAYER, new String[] { "J_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "K_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", TeamAction.ADD_PLAYER, new String[] { "L_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", TeamAction.ADD_PLAYER, new String[] { "M_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "N_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", TeamAction.ADD_PLAYER, new String[] { "O_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "P_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "Q_Player" }));
assertNoNextPacket(context);
// Now that those teams are created and people added to it, they set the final sidebar name and add the lines to it.
// They're also not doing this efficiently, because they don't add the players when the team is created.
// Instead, they send an additional packet.
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"sidebar",
ObjectiveAction.UPDATE,
Component.empty()
.append(Component.text(
"CubeCraft", Style.style(NamedTextColor.WHITE, TextDecoration.BOLD))),
ScoreType.INTEGER,
null));
assertNextPacket(
() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
},
context);
assertNextPacket(
() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§f§lCubeCraft");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-0",
Component.text("SB_l-0"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-0", TeamAction.ADD_PLAYER, new String[] {"§0§0"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-0",
Component.text("SB_l-0"),
Component.empty().append(Component.text("", Style.style(NamedTextColor.BLACK))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§0", "sidebar", 10));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 10, "§r§0§0§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-1",
Component.text("SB_l-1"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-1", TeamAction.ADD_PLAYER, new String[] {"§0§1"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-1",
Component.text("SB_l-1"),
Component.empty()
.append(Component.textOfChildren(
Component.text("User: ", TextColor.color(0x3aa9ff)),
Component.text("Tim203", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§1", "sidebar", 9));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(2, "0", 9, "§bUser: §r§fTim203§r§0§1§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-2",
Component.text("SB_l-2"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-2", TeamAction.ADD_PLAYER, new String[] {"§0§2"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-2",
Component.text("SB_l-2"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Rank: ", TextColor.color(0x3aa9ff)),
Component.text("\uE1AB ", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§2", "sidebar", 8));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(3, "0", 8, "§bRank: §r§f\uE1AB §r§0§2§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-3",
Component.text("SB_l-3"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-3", TeamAction.ADD_PLAYER, new String[] {"§0§3"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-3",
Component.text("SB_l-3"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§3", "sidebar", 7));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(4, "0", 7, "§r§0§3§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-4",
Component.text("SB_l-4"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-4", TeamAction.ADD_PLAYER, new String[] {"§0§4"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-4",
Component.text("SB_l-4"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§4", "sidebar", 6));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(5, "0", 6, "§r§0§4§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-5",
Component.text("SB_l-5"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-5", TeamAction.ADD_PLAYER, new String[] {"§0§5"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-5",
Component.text("SB_l-5"),
Component.empty().append(Component.text("", NamedTextColor.DARK_BLUE)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§5", "sidebar", 5));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(6, "0", 5, "§r§0§5§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-6",
Component.text("SB_l-6"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-6", TeamAction.ADD_PLAYER, new String[] {"§0§6"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-6",
Component.text("SB_l-6"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Lobby: ", TextColor.color(0x3aa9ff)),
Component.text("EU #10", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§6", "sidebar", 4));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(7, "0", 4, "§bLobby: §r§fEU #10§r§0§6§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-7",
Component.text("SB_l-7"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-7", TeamAction.ADD_PLAYER, new String[] {"§0§7"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-7",
Component.text("SB_l-7"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Players: ", TextColor.color(0x3aa9ff)),
Component.text("783", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§7", "sidebar", 3));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(8, "0", 3, "§bPlayers: §r§f783§r§0§7§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-8",
Component.text("SB_l-8"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-8", TeamAction.ADD_PLAYER, new String[] {"§0§8"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-8",
Component.text("SB_l-8"),
Component.empty().append(Component.text("", NamedTextColor.DARK_GREEN)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§8", "sidebar", 2));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(9, "0", 2, "§r§0§8§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-9",
Component.text("SB_l-9"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-9", TeamAction.ADD_PLAYER, new String[] {"§0§9"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-9",
Component.text("SB_l-9"),
Component.empty().append(Component.text("24/09/24 (g2208)", TextColor.color(0x777777))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§9", "sidebar", 1));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(10, "0", 1, "§824/09/24 (g2208)§r§0§9§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-10",
Component.text("SB_l-10"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-10", TeamAction.ADD_PLAYER, new String[] {"§0§a"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-10",
Component.text("SB_l-10"),
Component.empty().append(Component.text("play.cubecraft.net", NamedTextColor.GOLD)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§a", "sidebar", 0));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(11, "0", 0, "§6play.cubecraft.net§r§0§a§r")));
return packet;
},
context);
// after this we get a ClientboundPlayerInfoUpdatePacket with the action UPDATE_DISPLAY_NAME,
// but that one is only shown in the tablist so we don't have to handle that.
// And after that we get each player's ClientboundPlayerInfoUpdatePacket with also a UPDATE_DISPLAY_NAME,
// which is also not interesting for us.
// CubeCraft seems to use two armor stands per player: 1 for the rank badge and 1 for the player name.
// So the only thing we have to verify is that the nametag is hidden
mockAndAddPlayerEntity(context, "A_Player", 2);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "B_Player", 3);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(3);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "E_Player", 4);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(4);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "H_Player", 5);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(5);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "J_Player", 6);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(6);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "K_Player", 7);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(7);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "L_Player", 8);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(8);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "O_Player", 9);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(9);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
});
}
}

View file

@ -0,0 +1,218 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
/*
Identical to playerlist
*/
public class BasicSidebarScoreboardTests {
@Test
void displayAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
});
}
@Test
void displayNameColors() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", Style.style(NamedTextColor.AQUA, TextDecoration.BOLD)),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§b§lobjective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void override() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective1", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective2", 2));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective2")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective2");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "Tim203")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective1")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("2");
packet.setDisplayName("objective1");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(3, "2", 1, "Tim203")));
return packet;
}, context);
});
}
}

View file

@ -0,0 +1,533 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaResetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class OrderAndLimitSidebarScoreboardTests {
@Test
void aboveDisplayLimit() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
var resetScoreTranslator = new JavaResetScorePacket();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
// some are in an odd order to make sure that there is no bias for which score is send first,
// and to make sure that the score value also doesn't influence the order
context.translate(setScoreTranslator, new ClientboundSetScorePacket("a", "objective", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("b", "objective", 2));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("c", "objective", 3));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("d", "objective", 5));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("e", "objective", 4));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("f", "objective", 6));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("g", "objective", 9));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("h", "objective", 8));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("i", "objective", 7));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("p", "objective", 10));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("o", "objective", 11));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("n", "objective", 12));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("m", "objective", 13));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("k", "objective", 14));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("l", "objective", 15));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("j", "objective", 16));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("q", "objective", 17));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(1, "0", 17, "q"),
new ScoreInfo(2, "0", 16, "j"),
new ScoreInfo(3, "0", 15, "l"),
new ScoreInfo(4, "0", 14, "k"),
new ScoreInfo(5, "0", 13, "m"),
new ScoreInfo(6, "0", 12, "n"),
new ScoreInfo(7, "0", 11, "o"),
new ScoreInfo(8, "0", 10, "p"),
new ScoreInfo(9, "0", 9, "g"),
new ScoreInfo(10, "0", 8, "h"),
new ScoreInfo(11, "0", 7, "i"),
new ScoreInfo(12, "0", 6, "f"),
new ScoreInfo(13, "0", 5, "d"),
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(15, "0", 3, "c")
));
return packet;
}, context);
assertNoNextPacket(context);
// remove a score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("m", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(5, "0", 13, "m")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
// add a score
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("aa", "objective", 13)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(17, "0", 13, "aa")));
return packet;
}, context);
// add score with same score value (after)
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ga", "objective", 9)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(15, "0", 3, "c"),
new ScoreInfo(9, "0", 9, "§0§rg")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(9, "0", 9, "§0§rg"),
new ScoreInfo(18, "0", 9, "§1§rga")
));
return packet;
}, context);
// add another score with same score value (before all)
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ag", "objective", 9)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§2§rga")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 9, "§0§rag"),
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§2§rga")
));
return packet;
}, context);
// remove score with same value
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("g", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§1§rga")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 9, "§1§rga"),
new ScoreInfo(20, "0", 4, "e")
));
return packet;
}, context);
// remove the other score with the same value
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("ga", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 9, "§1§rga"),
new ScoreInfo(19, "0", 9, "ag")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 9, "ag"),
new ScoreInfo(21, "0", 3, "c")
));
return packet;
}, context);
});
}
@Test
void aboveDisplayLimitWithTeam() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
var resetScoreTranslator = new JavaResetScorePacket();
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
// some are in an odd order to make sure that there is no bias for which score is send first,
// and to make sure that the score value also doesn't influence the order
context.translate(setScoreTranslator, new ClientboundSetScorePacket("a", "objective", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("b", "objective", 2));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("c", "objective", 3));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("d", "objective", 5));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("e", "objective", 4));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("f", "objective", 6));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("g", "objective", 9));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("h", "objective", 8));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("i", "objective", 7));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("p", "objective", 10));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("o", "objective", 11));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("n", "objective", 12));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("m", "objective", 13));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("k", "objective", 14));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("l", "objective", 15));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("j", "objective", 16));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("q", "objective", 17));
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{ "f", "o" }
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(1, "0", 17, "q"),
new ScoreInfo(2, "0", 16, "j"),
new ScoreInfo(3, "0", 15, "l"),
new ScoreInfo(4, "0", 14, "k"),
new ScoreInfo(5, "0", 13, "m"),
new ScoreInfo(6, "0", 12, "n"),
new ScoreInfo(7, "0", 11, "§4prefix§r§4o§r§4suffix"),
new ScoreInfo(8, "0", 10, "p"),
new ScoreInfo(9, "0", 9, "g"),
new ScoreInfo(10, "0", 8, "h"),
new ScoreInfo(11, "0", 7, "i"),
new ScoreInfo(12, "0", 6, "§4prefix§r§4f§r§4suffix"),
new ScoreInfo(13, "0", 5, "d"),
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(15, "0", 3, "c")
));
return packet;
}, context);
assertNoNextPacket(context);
// remove a score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("m", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(5, "0", 13, "m")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
// add a score
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("aa", "objective", 13)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(17, "0", 13, "aa")));
return packet;
}, context);
// add some teams for the upcoming score adds
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_AQUA,
new String[]{ "oa" }
)
);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team3",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_PURPLE,
new String[]{ "ao" }
)
);
assertNoNextPacket(context);
// add a score that on Java should be after 'o', but would be before on Bedrock without manual order
// due to the team color
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("oa", "objective", 11)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(15, "0", 3, "c"),
new ScoreInfo(7, "0", 11, "§0§r§4prefix§r§4o§r§4suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(7, "0", 11, "§0§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
// add a score that on Java should be before 'o', but would be after on Bedrock without manual order
// due to the team color
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ao", "objective", 11)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§2§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 11, "§0§r§5prefix§r§5ao§r§5suffix"),
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§2§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
// remove original 'o' score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("o", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix"),
new ScoreInfo(20, "0", 4, "e")
));
return packet;
}, context);
// remove the other score with the same value as 'o'
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("oa", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix"),
new ScoreInfo(19, "0", 11, "§5prefix§r§5ao§r§5suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 11, "§5prefix§r§5ao§r§5suffix"),
new ScoreInfo(21, "0", 3, "c")
));
return packet;
}, context);
});
}
}

View file

@ -0,0 +1,265 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class VanillaSidebarScoreboardTests {
@Test
void displayAndAddScore() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreValue() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 2));
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "owner")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreDisplayName() {
// this ensures that MCPE-143063 is properly handled
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("owner", "objective", 1).withDisplay(Component.text("hi"))
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "hi")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "hi")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreDisplayNameAndValue() {
// this ensures that MCPE-143063 is properly handled
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("owner", "objective", 2).withDisplay(Component.text("hi"))
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "hi")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "hi")));
return packet;
}, context);
});
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import java.util.Collections;
import java.util.function.Supplier;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.junit.jupiter.api.Assertions;
public class AssertUtils {
public static <T> void assertContextEquals(Supplier<? extends T> expected, T actual) {
if (actual == null) {
Assertions.fail("Expected another packet! " + expected.get());
}
Assertions.assertEquals(expected.get(), actual);
}
public static void assertNextPacket(Supplier<BedrockPacket> expected, GeyserMockContext context) {
assertContextEquals(expected, context.nextPacket());
}
public static void assertNoNextPacket(GeyserMockContext context) {
Assertions.assertEquals(
Collections.emptyList(),
context.packets(),
"Expected no remaining packets, got " + context.packetCount()
);
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import org.geysermc.geyser.GeyserLogger;
public class EmptyGeyserLogger implements GeyserLogger {
@Override
public void severe(String message) {
}
@Override
public void severe(String message, Throwable error) {
}
@Override
public void error(String message) {
}
@Override
public void error(String message, Throwable error) {
}
@Override
public void warning(String message) {
}
@Override
public void info(String message) {
}
@Override
public void debug(String message) {
}
@Override
public void setDebug(boolean debug) {
}
@Override
public boolean isDebug() {
return false;
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class GeyserMockContext {
private final List<Object> mocksAndSpies = new ArrayList<>();
private final List<Object> storedObjects = new ArrayList<>();
private final List<BedrockPacket> packets = Collections.synchronizedList(new ArrayList<>());
private MockedStatic<GeyserImpl> geyserImplMock;
public static void mockContext(Consumer<GeyserMockContext> geyserContext) {
var context = new GeyserMockContext();
var geyserImpl = context.mock(GeyserImpl.class);
var config = context.mock(GeyserConfiguration.class);
when(config.getScoreboardPacketThreshold()).thenReturn(1_000);
when(geyserImpl.getConfig()).thenReturn(config);
var logger = context.storeObject(new EmptyGeyserLogger());
when(geyserImpl.getLogger()).thenReturn(logger);
try (var mocked = mockStatic(GeyserImpl.class)) {
mocked.when(GeyserImpl::getInstance).thenReturn(geyserImpl);
context.geyserImplMock = mocked;
geyserContext.accept(context);
}
}
public static void mockContext(Runnable runnable) {
mockContext(context -> runnable.run());
}
public <T> T mock(Class<T> type) {
return addMockOrSpy(Mockito.mock(type));
}
public <T> T spy(T object) {
return addMockOrSpy(Mockito.spy(object));
}
private <T> T addMockOrSpy(T mockOrSpy) {
mocksAndSpies.add(mockOrSpy);
return mockOrSpy;
}
public <T> T storeObject(T object) {
storedObjects.add(object);
return object;
}
/**
* Retries the mock or spy that is an instance of the specified type.
* This is only really intended for classes where you only need a single instance of.
*/
public <T> T mockOrSpy(Class<T> type) {
for (Object mock : mocksAndSpies) {
if (type.isInstance(mock)) {
return type.cast(mock);
}
}
return null;
}
public <T> T storedObject(Class<T> type) {
for (Object storedObject : storedObjects) {
if (type.isInstance(storedObject)) {
return type.cast(storedObject);
}
}
return null;
}
public GeyserSession session() {
return mockOrSpy(GeyserSession.class);
}
void addPacket(BedrockPacket packet) {
packets.add(packet);
}
public int packetCount() {
return packets.size();
}
public BedrockPacket nextPacket() {
if (packets.isEmpty()) {
return null;
}
return packets.remove(0);
}
public List<BedrockPacket> packets() {
return Collections.unmodifiableList(packets);
}
public <T> void translate(PacketTranslator<T> translator, T packet) {
translator.translate(session(), packet);
}
public MockedStatic<GeyserImpl> geyserImplMock() {
return geyserImplMock;
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContext.mockContext;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import java.util.UUID;
import java.util.function.Consumer;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.session.cache.WorldCache;
import org.mockito.stubbing.Answer;
public class GeyserMockContextScoreboard {
public static void mockContextScoreboard(Consumer<GeyserMockContext> geyserContext) {
mockContext(context -> {
createSessionSpy(context);
geyserContext.accept(context);
assertNoNextPacket(context);
});
}
private static void createSessionSpy(GeyserMockContext context) {
// GeyserSession has so many dependencies, it's easier to just mock it
var session = context.mock(GeyserSession.class);
when(session.locale()).thenReturn("en_US");
doAnswer((Answer<Void>) invocation -> {
context.addPacket(invocation.getArgument(0, BedrockPacket.class));
return null;
}).when(session).sendUpstreamPacket(any());
// SessionPlayerEntity loads stuff in like blocks, which is not what we want
var playerEntity = context.mock(SessionPlayerEntity.class);
when(playerEntity.getGeyserId()).thenReturn(1L);
when(playerEntity.getUsername()).thenReturn("Tim203");
when(session.getPlayerEntity()).thenReturn(playerEntity);
var entityCache = context.spy(new EntityCache(session));
when(session.getEntityCache()).thenReturn(entityCache);
var worldCache = context.spy(new WorldCache(session));
when(session.getWorldCache()).thenReturn(worldCache);
// disable global scoreboard updater
when(worldCache.increaseAndGetScoreboardPacketsPerSecond()).thenReturn(0);
}
public static PlayerEntity mockAndAddPlayerEntity(GeyserMockContext context, String username, long geyserId) {
var playerEntity = spy(new PlayerEntity(context.session(), geyserId, UUID.randomUUID(), username));
// fake the player being spawned
when(playerEntity.isValid()).thenReturn(true);
var entityCache = context.mockOrSpy(EntityCache.class);
entityCache.addPlayerEntity(playerEntity);
// called when the player spawns
entityCache.cacheEntity(playerEntity);
return playerEntity;
}
}

View file

@ -39,6 +39,7 @@ neoforge-minecraft = "21.1.1"
mixin = "0.8.5"
mixinextras = "0.3.5"
minecraft = "1.21.1"
mockito = "5.+"
# plugin versions
indra = "3.1.3"
@ -133,6 +134,8 @@ protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-con
math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" }
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
# plugins
lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" }
indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" }