3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-11-20 15:00:11 +01:00

Fixed some issues and added some initial tests

Dieser Commit ist enthalten in:
Tim203 2024-08-13 20:02:09 +02:00
Ursprung a5345b37fb
Commit 859d8e281f
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
23 geänderte Dateien mit 2322 neuen und 164 gelöschten Zeilen

Datei anzeigen

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

Datei anzeigen

@ -41,6 +41,7 @@ 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;
@ -102,7 +103,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE)
private float boundingBoxWidth;
@Setter(AccessLevel.NONE)
private String displayName = "";
private String displayName;
@Setter(AccessLevel.NONE)
protected boolean silent = false;
/* Metadata end */
@ -131,6 +132,7 @@ 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.displayName = EntityUtils.entityTypeName(definition.entityType());
this.entityId = entityId;
this.geyserId = geyserId;
@ -346,7 +348,7 @@ public class Entity implements GeyserEntity {
* Sends the Bedrock metadata to the client
*/
public void updateBedrockMetadata() {
if (!valid) {
if (!isValid()) {
return;
}
@ -415,27 +417,42 @@ public class Entity implements GeyserEntity {
return 300;
}
public String teamIdentifier() {
return uuid.toString();
}
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// the difference between displayName and nametag aren't important for non-living entities and for players,
// but the displayName is needed for living entities that are part of a scoreboard team.
// For them the nametag is prefix + displayName + suffix
// 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()) {
var displayName = MessageTranslator.convertMessage(name.get(), session.locale());
this.displayName = displayName;
setNametag(displayName, true);
} else {
this.displayName = "";
setNametag(null, 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 = EntityUtils.entityTypeName(definition.entityType());
setNametag(null, true);
}
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
var hide = nametag == null;
if (hide) {
// 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 = "";
}
var changed = Objects.equals(this.nametag, nametag);
var changed = !Objects.equals(this.nametag, nametag);
this.nametag = nametag;
// we only update metadata if the value has changed
if (!changed) {
@ -444,7 +461,29 @@ public class Entity implements GeyserEntity {
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
// if nametag (player with team) is hidden for player, so should the score (belowname)
scoreVisibility(!hide);
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) {}

Datei anzeigen

@ -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;
@ -47,7 +52,6 @@ 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.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.MathUtils;
@ -66,9 +70,6 @@ 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.*;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
@Getter
@Setter
public class LivingEntity extends Entity {
@ -146,45 +147,16 @@ public class LivingEntity extends Entity {
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
}
@Override
public void updateNametag(@Nullable Team team) {
if (team != null) {
String newNametag;
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.color();
String chatColor = MessageTranslator.toChatColor(color);
// We have to emulate what modern Java text already does for us and add the color to each section
newNametag = chatColor + team.prefix() + chatColor + getDisplayName() + chatColor + team.suffix();
} 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);
// 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 String teamIdentifier() {
return uuid.toString();
}
@Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
if (nametag != null && fromDisplayName) {
var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier());
if (team != null) {
updateNametag(team);
return;
}
}
super.setNametag(nametag, fromDisplayName);
}
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
byte xd = entityMetadata.getPrimitiveValue();

Datei anzeigen

@ -25,6 +25,7 @@
package org.geysermc.geyser.entity.type.player;
import java.util.Objects;
import lombok.Getter;
import lombok.Setter;
import net.kyori.adventure.text.Component;
@ -37,12 +38,14 @@ 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.*;
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;
@ -61,6 +64,7 @@ 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 {
@ -78,7 +82,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
private String username;
private String cachedScore;
private String cachedScore = "";
private boolean scoreVisible = true;
/**
@ -108,6 +112,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();
@ -391,19 +420,26 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
text = "";
}
var changed = !Objects.equals(cachedScore, text);
cachedScore = text;
if (scoreVisible) {
if (isScoreVisible() && changed) {
dirtyMetadata.put(EntityDataTypes.SCORE, text);
}
}
@Override
protected void scoreVisibility(boolean show) {
var changed = scoreVisible != show;
var visibilityChanged = scoreVisible != show;
scoreVisible = show;
if (changed) {
dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
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

Datei anzeigen

@ -83,7 +83,7 @@ public final class Objective {
}
public void updateProperties(Component displayNameComponent, ScoreType type, NumberFormat format) {
var displayName = MessageTranslator.convertMessage(displayNameComponent, scoreboard.session().locale());
var displayName = MessageTranslator.convertMessageRaw(displayNameComponent, scoreboard.session().locale());
var changed = !Objects.equals(this.displayName, displayName) || this.type != type;
this.displayName = displayName;

Datei anzeigen

@ -31,8 +31,8 @@ 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.entity.type.LivingEntity;
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;
@ -45,7 +45,7 @@ public final class Team {
private final String id;
private final Set<String> entities;
private final Set<LivingEntity> managedEntities;
private final Set<Entity> managedEntities;
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
private TeamColor color;
@ -68,23 +68,23 @@ public final class Team {
this.id = id;
this.entities = new ObjectOpenHashSet<>();
this.managedEntities = new ObjectOpenHashSet<>();
this.lastUpdate = LAST_UPDATE_DEFAULT;
addEntitiesNoUpdate(players);
// this calls the update
// doesn't call entity update
updateProperties(name, prefix, suffix, visibility, color);
// calls entitity update
addEntities(players);
lastUpdate = LAST_UPDATE_DEFAULT;
}
public void addEntities(String... names) {
addAddedEntities(addEntitiesNoUpdate(names));
}
private Set<String> addEntitiesNoUpdate(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.
@ -96,12 +96,12 @@ public final class Team {
}
if (added.isEmpty()) {
return added;
return;
}
// we don't have to change our updateType,
// because the scores itself need updating, not the team
// because the scores themselves need updating, not the team
scoreboard.setTeamFor(this, added);
return added;
addAddedEntities(added);
}
public void removeEntities(String... names) {
@ -120,7 +120,14 @@ public final class Team {
}
public String displayName(String score) {
return prefix + score + suffix;
var 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 = "";
}
// 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) {
@ -148,9 +155,9 @@ public final class Team {
var oldVisible = isVisibleFor(playerName());
var oldColor = this.color;
this.name = MessageTranslator.convertMessage(name, session().locale());
this.prefix = MessageTranslator.convertMessage(prefix, session().locale());
this.suffix = MessageTranslator.convertMessage(suffix, session().locale());
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) {
@ -159,24 +166,9 @@ public final class Team {
this.color = color;
if (lastUpdate == LAST_UPDATE_DEFAULT) {
if (entities.isEmpty()) {
return;
}
var hidden = false;
if (nameTagVisibility != NameTagVisibility.ALWAYS && !isVisibleFor(playerName())) {
// while the team has technically changed, we don't mark it as changed because the visibility
// doesn't influence any of the display slots
hideEntities();
hidden = true;
}
// 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();
// we've already hidden the entities, so we don't have to update them again
if (!hidden) {
updateEntities();
}
}
return;
}
@ -220,36 +212,32 @@ public final class Team {
refreshAllEntities();
return;
}
for (LivingEntity entity : managedEntities) {
for (Entity entity : managedEntities) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
private void hideEntities() {
for (LivingEntity entity : managedEntities) {
entity.hideNametag();
}
}
private void updateEntities() {
for (LivingEntity entity : managedEntities) {
for (Entity entity : managedEntities) {
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
private void addAddedEntities(Set<String> names) {
// can't contain self if none are added
if (names.isEmpty()) {
return;
}
var containsSelf = names.contains(playerName());
for (Entity entity : session().getEntityCache().getEntities().values()) {
if (!(entity instanceof LivingEntity living)) {
continue;
}
if (names.contains(living.teamIdentifier())) {
managedEntities.add(living);
if (names.contains(entity.teamIdentifier())) {
managedEntities.add(entity);
if (!containsSelf) {
living.updateNametag(this);
living.updateBedrockMetadata();
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
}
@ -281,11 +269,8 @@ public final class Team {
private void refreshAllEntities() {
for (Entity entity : session().getEntityCache().getEntities().values()) {
if (!(entity instanceof LivingEntity living)) {
continue;
}
living.updateNametag(scoreboard.getTeamFor(living.teamIdentifier()));
living.updateBedrockMetadata();
entity.updateNametag(scoreboard.getTeamFor(entity.teamIdentifier()));
entity.updateBedrockMetadata();
}
}

Datei anzeigen

@ -40,6 +40,7 @@ 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);
@ -81,6 +82,9 @@ public final class SidebarDisplayScore extends DisplayScore {
finalName = order + ChatColor.RESET + finalName;
}
if (cachedInfo != null) {
onlyScoreValueChanged = finalName.equals(cachedInfo.getName());
}
cachedInfo = new ScoreInfo(id, slot.objectiveId(), reference.score(), finalName);
}
@ -128,4 +132,8 @@ public final class SidebarDisplayScore extends DisplayScore {
public boolean exists() {
return cachedInfo != null;
}
public boolean onlyScoreValueChanged() {
return onlyScoreValueChanged;
}
}

Datei anzeigen

@ -50,8 +50,6 @@ public class BelownameDisplaySlot extends DisplaySlot {
public BelownameDisplaySlot(GeyserSession session, Objective objective) {
super(session, objective, ScoreboardPosition.BELOW_NAME);
setAndAddBelownameForExisting();
updateType = UpdateType.NOTHING;
}
@Override
@ -61,7 +59,13 @@ public class BelownameDisplaySlot extends DisplaySlot {
// 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
// add is handled in the constructor and remove is handled in #remove()
// 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()));
@ -118,12 +122,6 @@ public class BelownameDisplaySlot extends DisplaySlot {
displayScores.remove(player.getGeyserId());
}
private void setAndAddBelownameForExisting() {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
playerRegistered(player);
}
}
private void addDisplayScore(ScoreReference reference) {
var players = session.getEntityCache().getPlayersByName(reference.name());
for (PlayerEntity player : players) {

Datei anzeigen

@ -147,7 +147,7 @@ public final class SidebarDisplaySlot extends DisplaySlot {
// 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)) {
if (add && exists && !(objectiveUpdate || objectiveAdd) && !score.onlyScoreValueChanged()) {
removeScores.add(score.cachedInfo());
}
}

Datei anzeigen

@ -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";
@ -86,6 +88,8 @@ public class ChatColor {
}
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;
@ -105,9 +109,35 @@ public class ChatColor {
case 15 -> WHITE;
case 16 -> OBFUSCATED;
case 17 -> BOLD;
case 18 -> STRIKETHROUGH;
case 19 -> UNDERLINE;
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;
};
}
}

Datei anzeigen

@ -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.ScoreComponent;
import net.kyori.adventure.text.TranslatableComponent;
@ -39,14 +41,16 @@ import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.*;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.DummyLegacyHoverEventSerializer;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.GsonComponentSerializerWrapper;
import org.geysermc.geyser.text.MinecraftTranslationRegistry;
import org.geysermc.geyser.text.TextDecoration;
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.*;
public class MessageTranslator {
// These are used for handling the translations of the messages
@ -59,9 +63,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";
@ -69,31 +70,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()
@ -145,13 +121,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);
@ -160,7 +154,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) {
@ -173,7 +167,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);
}
@ -366,16 +360,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
*

Datei anzeigen

@ -290,6 +290,28 @@ public final class EntityUtils {
};
}
public static String entityTypeName(EntityType type) {
var typeName = type.name();
var builder = new StringBuilder();
boolean upNext = true;
for (int i = 0; i < typeName.length(); i++) {
char c = typeName.charAt(i);
if ('_' == c) {
upNext = true;
continue;
}
// enums are upper case by default
if (!upNext) {
c = Character.toLowerCase(c);
}
builder.append(c);
upNext = false;
}
return builder.toString();
}
private EntityUtils() {
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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);
});
}
}

Datei anzeigen

@ -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()
);
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -35,6 +35,7 @@ neoforge-minecraft = "21.0.0-beta"
mixin = "0.8.5"
mixinextras = "0.3.5"
minecraft = "1.21"
mockito = "5.+"
# plugin versions
indra = "3.1.3"
@ -124,6 +125,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
indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" }
shadow = { group = "com.github.johnrengelman", name = "shadow", version.ref = "shadow" }