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

Initial version of the great scoreboard rework

Dieser Commit ist enthalten in:
Tim203 2024-08-07 18:19:23 +02:00
Ursprung 13dfc7c173
Commit a5345b37fb
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
24 geänderte Dateien mit 1784 neuen und 1020 gelöschten Zeilen

Datei anzeigen

@ -60,7 +60,6 @@ import java.util.*;
@Getter
@Setter
public class Entity implements GeyserEntity {
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
protected final GeyserSession session;
@ -68,6 +67,12 @@ public class Entity implements GeyserEntity {
protected int entityId;
protected final long geyserId;
protected UUID uuid;
/**
* Do not call this setter directly!
* This will bypass the scoreboard and setting the metadata
*/
@Setter(AccessLevel.NONE)
protected String nametag = "";
protected Vector3f position;
protected Vector3f motion;
@ -97,7 +102,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE)
private float boundingBoxWidth;
@Setter(AccessLevel.NONE)
protected String nametag = "";
private String displayName = "";
@Setter(AccessLevel.NONE)
protected boolean silent = false;
/* Metadata end */
@ -411,16 +416,39 @@ public class Entity implements GeyserEntity {
}
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
Optional<Component> name = entityMetadata.getValue();
if (name.isPresent()) {
nametag = MessageTranslator.convertMessage(name.get(), session.locale());
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
} else if (!nametag.isEmpty()) {
// Clear nametag
dirtyMetadata.put(EntityDataTypes.NAME, "");
var displayName = MessageTranslator.convertMessage(name.get(), session.locale());
this.displayName = displayName;
setNametag(displayName, true);
} else {
this.displayName = "";
setNametag(null, true);
}
}
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
var hide = nametag == null;
if (hide) {
nametag = "";
}
var changed = Objects.equals(this.nametag, nametag);
this.nametag = nametag;
// we only update metadata if the value has changed
if (!changed) {
return;
}
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
// if nametag (player with team) is hidden for player, so should the score (belowname)
scoreVisibility(!hide);
}
protected void scoreVisibility(boolean show) {}
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
}

Datei anzeigen

@ -44,8 +44,10 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.MathUtils;
@ -65,11 +67,11 @@ 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 {
protected ItemData helmet = ItemData.AIR;
protected ItemData chestplate = ItemData.AIR;
protected ItemData leggings = ItemData.AIR;
@ -144,6 +146,45 @@ public class LivingEntity extends Entity {
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
}
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);
}
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

@ -32,7 +32,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.Ability;
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
import org.cloudburstmc.protocol.bedrock.data.GameType;
@ -49,26 +48,13 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Score;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.Collections;
import java.util.List;
@ -92,6 +78,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
private String username;
private String cachedScore;
private boolean scoreVisible = true;
/**
* The textures property from the GameProfile.
*/
@ -128,17 +117,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override
public void spawnEntity() {
// Check to see if the player should have a belowname counterpart added
Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (objective != null) {
setBelowNameText(objective);
}
// Update in case this entity has been despawned, then respawned
this.nametag = this.username;
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
addPlayerPacket.setUuid(uuid);
addPlayerPacket.setUsername(username);
@ -173,6 +151,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Since we re-use player entities: Clear flags, held item, etc
this.resetMetadata();
this.nametag = username;
this.hand = ItemData.AIR;
this.offhand = ItemData.AIR;
this.boots = ItemData.AIR;
@ -376,38 +355,30 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
}
}
@Override
public String getDisplayName() {
return username;
}
@Override
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// Doesn't do anything for players
}
//todo this will become common entity logic once UUID support is implemented for them
public void updateDisplayName(@Nullable Team team) {
boolean needsUpdate;
if (team != null) {
String newDisplayName;
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.getColor();
String chatColor = MessageTranslator.toChatColor(color);
// We have to emulate what modern Java text already does for us and add the color to each section
String prefix = team.getCurrentData().getPrefix();
String suffix = team.getCurrentData().getSuffix();
newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix;
} else {
// The name is not visible to the session player; clear name
newDisplayName = "";
}
needsUpdate = !newDisplayName.equals(this.nametag);
this.nametag = newDisplayName;
} else {
// The name has reset, if it was previously something else
needsUpdate = !this.nametag.equals(this.username);
this.nametag = this.username;
}
@Override
public String teamIdentifier() {
return username;
}
if (needsUpdate) {
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag);
@Override
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that
// setNametag is called again with fromDisplayName on false
if (nametag == null && !fromDisplayName) {
// nametag = null means reset, so reset it back to username
nametag = username;
}
super.setNametag(nametag, fromDisplayName);
}
@Override
@ -415,6 +386,26 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Doesn't do anything for players
}
public void setBelowNameText(String text) {
if (text == null) {
text = "";
}
cachedScore = text;
if (scoreVisible) {
dirtyMetadata.put(EntityDataTypes.SCORE, text);
}
}
@Override
protected void scoreVisibility(boolean show) {
var changed = scoreVisible != show;
scoreVisible = show;
if (changed) {
dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
}
}
@Override
protected void setDimensions(Pose pose) {
float height;
@ -441,64 +432,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
setBoundingBoxHeight(height);
}
public void setBelowNameText(Objective objective) {
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
Score score = objective.getScores().get(username);
String numberString;
NumberFormat numberFormat;
int amount;
if (score != null) {
amount = score.getScore();
numberFormat = score.getNumberFormat();
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
} else {
amount = 0;
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof BlankFormat) {
numberString = "";
} else if (numberFormat instanceof FixedFormat fixedFormat) {
numberString = MessageTranslator.convertMessage(fixedFormat.getValue());
} else if (numberFormat instanceof StyledFormat styledFormat) {
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
styledAmount.putString("text", String.valueOf(amount));
numberString = MessageTranslator.convertJsonMessage(
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
} else {
numberString = String.valueOf(amount);
}
String displayString = numberString + " " + ChatColor.RESET + objective.getDisplayName();
if (valid) {
// Already spawned - we still need to run the rest of this code because the spawn packet will be
// providing the information
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, displayString);
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, displayString);
}
} else {
if (valid) {
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, "");
}
}
}
/**
* @return the UUID that should be used when dealing with Bedrock's tab list.
*/

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -25,43 +25,61 @@
package org.geysermc.geyser.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.HashSet;
import java.util.Set;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
@Getter
@Accessors(chain = true)
public final class Team {
public static final long LAST_UPDATE_DEFAULT = -1;
private static final long LAST_UPDATE_REMOVE = -2;
private final Scoreboard scoreboard;
private final String id;
@Getter(AccessLevel.PACKAGE)
private final Set<String> entities;
private final Set<LivingEntity> managedEntities;
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
@Setter private TeamColor color;
private TeamColor color;
private final TeamData currentData;
private TeamData cachedData;
private String name;
private String prefix;
private String suffix;
private long lastUpdate;
private boolean updating;
public Team(Scoreboard scoreboard, String id) {
public Team(
Scoreboard scoreboard,
String id,
String[] players,
Component name,
Component prefix,
Component suffix,
NameTagVisibility visibility,
TeamColor color
) {
this.scoreboard = scoreboard;
this.id = id;
currentData = new TeamData();
entities = new ObjectOpenHashSet<>();
this.entities = new ObjectOpenHashSet<>();
this.managedEntities = new ObjectOpenHashSet<>();
addEntitiesNoUpdate(players);
// this calls the update
updateProperties(name, prefix, suffix, visibility, color);
lastUpdate = LAST_UPDATE_DEFAULT;
}
public Set<String> addEntities(String... names) {
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)) {
@ -80,24 +98,13 @@ public final class Team {
if (added.isEmpty()) {
return added;
}
// we don't have to change the updateType,
// we don't have to change our updateType,
// because the scores itself need updating, not the team
for (Objective objective : scoreboard.getObjectives()) {
for (String addedEntity : added) {
Score score = objective.getScores().get(addedEntity);
if (score != null) {
score.setTeam(this);
}
}
}
scoreboard.setTeamFor(this, added);
return added;
}
/**
* @return all removed entities from this team
*/
public Set<String> removeEntities(String... names) {
public void removeEntities(String... names) {
Set<String> removed = new HashSet<>();
for (String name : names) {
if (entities.remove(name)) {
@ -105,87 +112,15 @@ public final class Team {
}
scoreboard.getPlayerToTeam().remove(name, this);
}
return removed;
removeRemovedEntities(removed);
}
public boolean hasEntity(String name) {
return entities.contains(name);
}
public Team setName(String name) {
currentData.name = name;
return this;
}
public Team setPrefix(String prefix) {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (prefix.length() == 4 && "null".equals(prefix)) {
currentData.prefix = "";
return this;
}
currentData.prefix = prefix;
return this;
}
public Team setSuffix(String suffix) {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (suffix.length() == 4 && "null".equals(suffix)) {
currentData.suffix = "";
return this;
}
currentData.suffix = suffix;
return this;
}
public String getDisplayName(String score) {
return cachedData != null ?
cachedData.getDisplayName(score) :
currentData.getDisplayName(score);
}
public void markUpdated() {
updating = false;
}
public boolean shouldUpdate() {
return updating || cachedData == null || currentData.changed;
}
public void prepareUpdate() {
if (updating) {
return;
}
updating = true;
if (cachedData == null) {
cachedData = new TeamData();
cachedData.updateType = currentData.updateType != UpdateType.REMOVE ? UpdateType.ADD : UpdateType.REMOVE;
} else {
cachedData.updateType = currentData.updateType;
}
currentData.changed = false;
cachedData.name = currentData.name;
cachedData.prefix = currentData.prefix;
cachedData.suffix = currentData.suffix;
}
public UpdateType getUpdateType() {
return currentData.updateType;
}
public UpdateType getCachedUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType;
}
public Team setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.changed = true;
}
currentData.updateType = updateType;
return this;
public String displayName(String score) {
return prefix + score + suffix;
}
public boolean isVisibleFor(String entity) {
@ -201,34 +136,193 @@ public final class Team {
};
}
public Team setNameTagVisibility(@Nullable NameTagVisibility nameTagVisibility) {
if (nameTagVisibility != null) {
// Null check like this (and this.nameTagVisibility defaults to ALWAYS) as of Java 1.19.4
this.nameTagVisibility = nameTagVisibility;
public void updateProperties(Component name, Component prefix, Component suffix, NameTagVisibility visibility, TeamColor color) {
// this shouldn't happen but hey!
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
return this;
var oldName = this.name;
var oldPrefix = this.prefix;
var oldSuffix = this.suffix;
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());
// matches vanilla behaviour, the visibility is not reset (to ALWAYS) if it is null.
// instead the visibility is not altered
if (visibility != null) {
this.nameTagVisibility = visibility;
}
this.color = color;
if (lastUpdate == LAST_UPDATE_DEFAULT) {
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;
}
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;
}
if (!this.name.equals(oldName)
|| !this.prefix.equals(oldPrefix)
|| !this.suffix.equals(oldSuffix)
|| color != oldColor) {
markChanged();
updateEntities();
return;
}
if (isVisibleFor(playerName()) != oldVisible) {
// if just the visibility changed, we only have to update the entities.
// We don't have to mark it as changed
updateEntities();
}
}
public boolean shouldRemove() {
return lastUpdate == LAST_UPDATE_REMOVE;
}
public void markChanged() {
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
lastUpdate = System.currentTimeMillis();
}
public void remove() {
lastUpdate = LAST_UPDATE_REMOVE;
for (String name : entities()) {
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
scoreboard.getPlayerToTeam().remove(name);
}
if (entities().contains(playerName())) {
refreshAllEntities();
return;
}
for (LivingEntity entity : managedEntities) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
private void hideEntities() {
for (LivingEntity entity : managedEntities) {
entity.hideNametag();
}
}
private void updateEntities() {
for (LivingEntity entity : managedEntities) {
entity.updateNametag(this);
}
}
private void addAddedEntities(Set<String> names) {
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 (!containsSelf) {
living.updateNametag(this);
living.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void removeRemovedEntities(Set<String> names) {
var containsSelf = names.contains(playerName());
var iterator = managedEntities.iterator();
while (iterator.hasNext()) {
var entity = iterator.next();
if (names.contains(entity.teamIdentifier())) {
iterator.remove();
if (!containsSelf) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void refreshAllEntities() {
for (Entity entity : session().getEntityCache().getEntities().values()) {
if (!(entity instanceof LivingEntity living)) {
continue;
}
living.updateNametag(scoreboard.getTeamFor(living.teamIdentifier()));
living.updateBedrockMetadata();
}
}
private GeyserSession session() {
return scoreboard.session();
}
private String playerName() {
return session().getPlayerEntity().getUsername();
}
public String id() {
return id;
}
public TeamColor color() {
return color;
}
public String prefix() {
return prefix;
}
public String suffix() {
return suffix;
}
public long lastUpdate() {
return lastUpdate;
}
public Set<String> entities() {
return entities;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Getter
public static final class TeamData {
private UpdateType updateType;
private boolean changed;
private String name;
private String prefix;
private String suffix;
private TeamData() {
updateType = UpdateType.ADD;
}
public String getDisplayName(String score) {
return prefix + score + suffix;
}
}
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -126,15 +126,35 @@ public class EntityCache {
public void addPlayerEntity(PlayerEntity entity) {
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
playerEntities.putIfAbsent(entity.getUuid(), entity);
var exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
if (exists) {
return;
}
// notify scoreboard for new entity
session.getWorldCache().getScoreboard().playerRegistered(entity);
}
public PlayerEntity getPlayerEntity(UUID uuid) {
return playerEntities.get(uuid);
}
public List<PlayerEntity> getPlayersByName(String name) {
var list = new ArrayList<PlayerEntity>();
for (PlayerEntity player : playerEntities.values()) {
if (name.equals(player.getUsername())) {
list.add(player);
}
}
return list;
}
public PlayerEntity removePlayerEntity(UUID uuid) {
return playerEntities.remove(uuid);
var player = playerEntities.remove(uuid);
if (player != null) {
// notify scoreboard
session.getWorldCache().getScoreboard().playerRemoved(player);
}
return player;
}
public Collection<PlayerEntity> getAllPlayerEntities() {

Datei anzeigen

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

Datei anzeigen

@ -84,4 +84,30 @@ public class ChatColor {
string = string.replace(WHITE, (char) 0x1b + "[37;1m");
return string;
}
}
public static String styleOrder(int index) {
return switch (index) {
case 0 -> BLACK;
case 1 -> DARK_BLUE;
case 2 -> DARK_GREEN;
case 3 -> DARK_AQUA;
case 4 -> DARK_RED;
case 5 -> DARK_PURPLE;
case 6 -> GOLD;
case 7 -> GRAY;
case 8 -> DARK_GRAY;
case 9 -> BLUE;
case 10 -> GREEN;
case 11 -> AQUA;
case 12 -> RED;
case 13 -> LIGHT_PURPLE;
case 14 -> YELLOW;
case 15 -> WHITE;
case 16 -> OBFUSCATED;
case 17 -> BOLD;
case 18 -> STRIKETHROUGH;
case 19 -> UNDERLINE;
default -> ITALIC;
};
}
}

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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