Fixed some issues related to Scoreboards (#1446)

* Fixes some issues related to Scoreboard Teams

* The cached Score info should update no matter what kind of update the Team got.
* Team entities specified at the create Team packet should also be checked if they exist as Score in the registered Objectives

* Rewrote some Scoreboard code and fixed various issues

* Minor formatting changes
This commit is contained in:
Tim203 2020-11-09 10:34:27 +01:00 committed by GitHub
parent 2d95302b10
commit e00715ceab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 333 additions and 154 deletions

View File

@ -27,7 +27,6 @@ package org.geysermc.connector.entity;
import com.github.steveice10.mc.auth.data.GameProfile;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility;
import com.github.steveice10.mc.protocol.data.message.TextMessage;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.nukkitx.math.vector.Vector3f;
@ -235,18 +234,12 @@ public class PlayerEntity extends LivingEntity {
}
Team team = session.getWorldCache().getScoreboard().getTeamFor(username);
if (team != null) {
// Cover different visibility settings
if (team.getNameTagVisibility() == NameTagVisibility.NEVER) {
metadata.put(EntityData.NAMETAG, "");
} else if (team.getNameTagVisibility() == NameTagVisibility.HIDE_FOR_OTHER_TEAMS &&
!team.getEntities().contains(session.getPlayerEntity().getUsername())) {
metadata.put(EntityData.NAMETAG, "");
} else if (team.getNameTagVisibility() == NameTagVisibility.HIDE_FOR_OWN_TEAM &&
team.getEntities().contains(session.getPlayerEntity().getUsername())) {
metadata.put(EntityData.NAMETAG, "");
} else {
metadata.put(EntityData.NAMETAG, team.getPrefix() + MessageUtils.toChatColor(team.getColor()) + username + team.getSuffix());
String displayName = "";
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
displayName = MessageUtils.toChatColor(team.getColor()) + username;
displayName = team.getCurrentData().getDisplayName(displayName);
}
metadata.put(EntityData.NAMETAG, displayName);
}
}

View File

@ -36,8 +36,7 @@ public class JavaDisplayScoreboardTranslator extends PacketTranslator<ServerDisp
@Override
public void translate(ServerDisplayScoreboardPacket packet, GeyserSession session) {
session.getWorldCache().getScoreboard().registerNewObjective(
packet.getName(), packet.getPosition()
);
session.getWorldCache().getScoreboard()
.displayObjective(packet.getName(), packet.getPosition());
}
}

View File

@ -31,6 +31,7 @@ import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.scoreboard.Objective;
import org.geysermc.connector.scoreboard.Scoreboard;
import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.utils.MessageUtils;
import com.github.steveice10.mc.protocol.data.game.scoreboard.ObjectiveAction;
@ -41,8 +42,10 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerSc
@Override
public void translate(ServerScoreboardObjectivePacket packet, GeyserSession session) {
Scoreboard scoreboard = session.getWorldCache().getScoreboard();
WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard();
Objective objective = scoreboard.getObjective(packet.getName());
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
if (objective == null && packet.getAction() != ObjectiveAction.REMOVE) {
objective = scoreboard.registerNewObjective(packet.getName(), false);
@ -51,15 +54,21 @@ public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerSc
switch (packet.getAction()) {
case ADD:
case UPDATE:
objective.setDisplayName(MessageUtils.getBedrockMessage(packet.getDisplayName()));
objective.setType(packet.getType().ordinal());
objective.setDisplayName(MessageUtils.getBedrockMessage(packet.getDisplayName()))
.setType(packet.getType().ordinal());
break;
case REMOVE:
scoreboard.unregisterObjective(packet.getName());
break;
}
if (objective != null && objective.isActive()) {
if (objective == null || !objective.isActive()) {
return;
}
// ScoreboardUpdater will handle it for us if the packets per second
// (for score and team packets) is higher then the first threshold
if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
scoreboard.onUpdate();
}
}

View File

@ -33,14 +33,16 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Getter
public class Objective {
public final class Objective {
private final Scoreboard scoreboard;
private final long id;
private boolean active = true;
@Setter
private UpdateType updateType = UpdateType.ADD;
private String objectiveName;
private ScoreboardPosition displaySlot;
private String displaySlotName;
private String displayName = "unknown";
private int type = 0; // 0 = integer, 1 = heart
@ -67,39 +69,59 @@ public class Objective {
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
this(scoreboard);
this.objectiveName = objectiveName;
this.displaySlot = correctDisplaySlot(displaySlot);
this.displaySlotName = translateDisplaySlot(displaySlot);
this.displayName = displayName;
this.type = type;
}
private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
switch (displaySlot) {
case BELOW_NAME:
return "belowname";
case PLAYER_LIST:
return "list";
default:
return "sidebar";
}
}
private static ScoreboardPosition correctDisplaySlot(ScoreboardPosition displaySlot) {
switch (displaySlot) {
case BELOW_NAME:
return ScoreboardPosition.BELOW_NAME;
case PLAYER_LIST:
return ScoreboardPosition.PLAYER_LIST;
default:
return ScoreboardPosition.SIDEBAR;
}
}
public void registerScore(String id, int score) {
if (!scores.containsKey(id)) {
Score score1 = new Score(this, id)
long scoreId = scoreboard.getNextId().getAndIncrement();
Score scoreObject = new Score(scoreId, id)
.setScore(score)
.setTeam(scoreboard.getTeamFor(id))
.setUpdateType(UpdateType.ADD);
scores.put(id, score1);
scores.put(id, scoreObject);
}
}
public void setScore(String id, int score) {
if (scores.containsKey(id)) {
scores.get(id).setScore(score);
Score stored = scores.get(id);
if (stored != null) {
stored.setScore(score)
.setUpdateType(UpdateType.UPDATE);
return;
}
registerScore(id, score);
}
public int getScore(String id) {
if (scores.containsKey(id)) {
return scores.get(id).getScore();
}
return 0;
}
public void removeScore(String id) {
if (scores.containsKey(id)) {
scores.get(id).setUpdateType(UpdateType.REMOVE);
Score stored = scores.get(id);
if (stored != null) {
stored.setUpdateType(UpdateType.REMOVE);
}
}
@ -129,6 +151,7 @@ public class Objective {
public void setActive(ScoreboardPosition displaySlot) {
if (!active) {
active = true;
this.displaySlot = correctDisplaySlot(displaySlot);
displaySlotName = translateDisplaySlot(displaySlot);
}
}
@ -136,15 +159,4 @@ public class Objective {
public void removed() {
scores = null;
}
private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
switch (displaySlot) {
case BELOW_NAME:
return "belowname";
case PLAYER_LIST:
return "list";
default:
return "sidebar";
}
}
}

View File

@ -27,60 +27,107 @@ package org.geysermc.connector.scoreboard;
import com.nukkitx.protocol.bedrock.data.ScoreInfo;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
@Getter
@Accessors(chain = true)
public class Score {
private final Objective objective;
private ScoreInfo cachedInfo;
public final class Score {
private final long id;
@Setter
private UpdateType updateType = UpdateType.ADD;
private final String name;
private Team team;
private int score;
@Setter
private int oldScore = Integer.MIN_VALUE;
private ScoreInfo cachedInfo;
public Score(Objective objective, String name) {
this.id = objective.getScoreboard().getNextId().getAndIncrement();
this.objective = objective;
private ScoreData currentData;
private ScoreData cachedData;
public Score(long id, String name) {
this.id = id;
this.name = name;
this.currentData = new ScoreData();
}
public String getDisplayName() {
Team team = cachedData.team;
if (team != null) {
return team.getPrefix() + name + team.getSuffix();
return team.getDisplayName(name);
}
return name;
}
public Score setScore(int score) {
this.score = score;
updateType = UpdateType.UPDATE;
currentData.score = score;
return this;
}
public Team getTeam() {
return currentData.team;
}
public Score setTeam(Team team) {
if (this.team != null && team != null) {
if (!this.team.equals(team)) {
this.team = team;
updateType = UpdateType.UPDATE;
if (currentData.team != null && team != null) {
if (!currentData.team.equals(team)) {
currentData.team = team;
currentData.updateType = UpdateType.UPDATE;
}
return this;
}
// simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (this.team != null || team != null) {
this.team = team;
updateType = UpdateType.UPDATE;
if (currentData.team != null || team != null) {
currentData.team = team;
currentData.updateType = UpdateType.UPDATE;
}
return this;
}
public void update() {
cachedInfo = new ScoreInfo(id, objective.getObjectiveName(), score, getDisplayName());
public UpdateType getUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType;
}
public Score setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.updateTime = System.currentTimeMillis();
}
currentData.updateType = updateType;
return this;
}
public boolean shouldUpdate() {
return cachedData == null || currentData.updateTime > cachedData.updateTime ||
(currentData.team != null && currentData.team.shouldUpdate());
}
public void update(String objectiveName) {
if (cachedData == null) {
cachedData = new ScoreData();
cachedData.updateType = UpdateType.ADD;
if (currentData.updateType == UpdateType.REMOVE) {
cachedData.updateType = UpdateType.REMOVE;
}
} else {
cachedData.updateType = currentData.updateType;
}
cachedData.updateTime = currentData.updateTime;
cachedData.team = currentData.team;
cachedData.score = currentData.score;
String name = this.name;
if (cachedData.team != null) {
cachedData.team.prepareUpdate();
name = cachedData.team.getDisplayName(name);
}
cachedInfo = new ScoreInfo(id, objectiveName, cachedData.score, name);
}
@Getter
public static final class ScoreData {
protected UpdateType updateType;
protected long updateTime;
private Team team;
private int score;
protected ScoreData() {
updateType = UpdateType.ADD;
}
}
}

View File

@ -43,7 +43,7 @@ import java.util.concurrent.atomic.AtomicLong;
import static org.geysermc.connector.scoreboard.UpdateType.*;
@Getter
public class Scoreboard {
public final class Scoreboard {
private final GeyserSession session;
private final GeyserLogger logger;
private final AtomicLong nextId = new AtomicLong(0);
@ -51,7 +51,8 @@ public class Scoreboard {
private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
private final Map<String, Team> teams = new HashMap<>();
private int lastScoreCount = 0;
private int lastAddScoreCount = 0;
private int lastRemoveScoreCount = 0;
public Scoreboard(GeyserSession session) {
this.session = session;
@ -59,19 +60,21 @@ public class Scoreboard {
}
public Objective registerNewObjective(String objectiveId, boolean active) {
if (active || objectives.containsKey(objectiveId)) {
return objectives.get(objectiveId);
Objective objective = objectives.get(objectiveId);
if (active || objective != null) {
return objective;
}
Objective objective = new Objective(this, objectiveId);
objective = new Objective(this, objectiveId);
objectives.put(objectiveId, objective);
return objective;
}
public Objective registerNewObjective(String objectiveId, ScoreboardPosition displaySlot) {
public Objective displayObjective(String objectiveId, ScoreboardPosition displaySlot) {
Objective objective = objectives.get(objectiveId);
if (objective != null) {
if (!objective.isActive()) {
objective.setActive(displaySlot);
removeOldObjectives(objective);
return objective;
}
despawnObjective(objective);
@ -79,9 +82,21 @@ public class Scoreboard {
objective = new Objective(this, objectiveId, displaySlot, "unknown", 0);
objectives.put(objectiveId, objective);
removeOldObjectives(objective);
return objective;
}
private void removeOldObjectives(Objective newObjective) {
for (Objective next : objectives.values()) {
if (next.getId() == newObjective.getId()) {
continue;
}
if (next.getDisplaySlot() == newObjective.getDisplaySlot()) {
next.setUpdateType(REMOVE);
}
}
}
public Team registerNewTeam(String teamName, Set<String> players) {
Team team = teams.get(teamName);
if (team != null) {
@ -89,7 +104,7 @@ public class Scoreboard {
return team;
}
team = new Team(this, teamName).setEntities(players);
team = new Team(this, teamName).addEntities(players);
teams.put(teamName, team);
return team;
}
@ -117,8 +132,9 @@ public class Scoreboard {
}
public void onUpdate() {
List<ScoreInfo> addScores = new ArrayList<>(getLastScoreCount());
List<ScoreInfo> removeScores = new ArrayList<>(getLastScoreCount());
List<ScoreInfo> addScores = new ArrayList<>(getLastAddScoreCount());
List<ScoreInfo> removeScores = new ArrayList<>(getLastRemoveScoreCount());
List<Objective> removedObjectives = new ArrayList<>();
for (Objective objective : objectives.values()) {
if (!objective.isActive()) {
@ -129,65 +145,58 @@ public class Scoreboard {
// hearts can't hold teams, so we treat them differently
if (objective.getType() == 1) {
for (Score score : objective.getScores().values()) {
if (score.getUpdateType() == NOTHING) {
continue;
}
boolean update = score.shouldUpdate();
boolean update = score.getUpdateType() == UPDATE;
if (update) {
score.update();
score.update(objective.getObjectiveName());
}
if (score.getUpdateType() == ADD || update) {
if (score.getUpdateType() != REMOVE && update) {
addScores.add(score.getCachedInfo());
}
if (score.getUpdateType() == REMOVE || update) {
if (score.getUpdateType() != ADD && update) {
removeScores.add(score.getCachedInfo());
}
}
continue;
}
boolean globalUpdate = objective.getUpdateType() == UPDATE;
boolean globalAdd = objective.getUpdateType() == ADD;
boolean globalRemove = objective.getUpdateType() == REMOVE;
boolean objectiveUpdate = objective.getUpdateType() == UPDATE;
boolean objectiveAdd = objective.getUpdateType() == ADD;
boolean objectiveRemove = objective.getUpdateType() == REMOVE;
for (Score score : objective.getScores().values()) {
Team team = score.getTeam();
boolean add = globalAdd || globalUpdate;
boolean remove = globalRemove;
boolean teamChanged = false;
boolean add = objectiveAdd || objectiveUpdate;
boolean remove = false;
if (team != null) {
if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
score.setTeam(null);
teamChanged = true;
add = true;
remove = true;
}
teamChanged |= team.getUpdateType() == UPDATE;
add |= team.getUpdateType() == ADD || team.getUpdateType() == UPDATE;
remove |= team.getUpdateType() != NOTHING;
}
add |= score.getUpdateType() == ADD || score.getUpdateType() == UPDATE;
remove |= score.getUpdateType() == REMOVE || score.getUpdateType() == UPDATE;
add |= score.shouldUpdate();
remove |= score.shouldUpdate();
if (score.getUpdateType() == REMOVE || globalRemove) {
if (score.getUpdateType() == REMOVE || objectiveRemove) {
add = false;
}
if (score.getUpdateType() == ADD) {
if (score.getUpdateType() == ADD || objectiveRemove) {
remove = false;
}
if (score.getUpdateType() == ADD || score.getUpdateType() == UPDATE || teamChanged) {
score.update();
if (score.shouldUpdate()) {
score.update(objective.getObjectiveName());
}
if (add) {
addScores.add(score.getCachedInfo());
}
if (remove) {
removeScores.add(score.getCachedInfo());
}
@ -200,17 +209,17 @@ public class Scoreboard {
score.setUpdateType(NOTHING);
}
if (globalRemove || globalUpdate) {
if (objectiveRemove) {
removedObjectives.add(objective);
}
if (objectiveUpdate) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
if (globalRemove) {
objectives.remove(objective.getObjectiveName()); // now we can deregister
objective.removed();
}
}
if ((globalAdd || globalUpdate) && !globalRemove) {
if ((objectiveAdd || objectiveUpdate) && !objectiveRemove) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());
@ -223,6 +232,20 @@ public class Scoreboard {
objective.setUpdateType(NOTHING);
}
Iterator<Team> teamIterator = teams.values().iterator();
while (teamIterator.hasNext()) {
Team current = teamIterator.next();
switch (current.getUpdateType()) {
case ADD:
case UPDATE:
current.markUpdated();
break;
case REMOVE:
teamIterator.remove();
}
}
if (!removeScores.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.REMOVE);
@ -237,37 +260,27 @@ public class Scoreboard {
session.sendUpstreamPacket(setScorePacket);
}
lastScoreCount = addScores.size();
// prevents crashes in some cases
for (Objective objective : removedObjectives) {
despawnObjective(objective);
}
lastAddScoreCount = addScores.size();
lastRemoveScoreCount = removeScores.size();
}
public void despawnObjective(Objective objective) {
objectives.remove(objective.getObjectiveName());
objective.removed();
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
objectives.remove(objective.getDisplayName());
List<ScoreInfo> toRemove = new ArrayList<>();
for (String identifier : objective.getScores().keySet()) {
Score score = objective.getScores().get(identifier);
toRemove.add(new ScoreInfo(
score.getId(), score.getObjective().getObjectiveName(),
0, ""
));
}
objective.removed();
if (!toRemove.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.REMOVE);
setScorePacket.setInfos(toRemove);
session.sendUpstreamPacket(setScorePacket);
}
}
public Team getTeamFor(String entity) {
for (Team team : teams.values()) {
if (team.getEntities().contains(entity)) {
if (team.hasEntity(entity)) {
return team;
}
}

View File

@ -28,6 +28,7 @@ package org.geysermc.connector.scoreboard;
import com.github.steveice10.mc.protocol.data.game.scoreboard.NameTagVisibility;
import com.github.steveice10.mc.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;
@ -36,62 +37,90 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Getter @Setter
@Getter
@Accessors(chain = true)
public class Team {
public final class Team {
private final Scoreboard scoreboard;
private final String id;
private UpdateType updateType = UpdateType.ADD;
private String name;
@Getter(AccessLevel.NONE)
private final Set<String> entities;
@Setter private NameTagVisibility nameTagVisibility;
@Setter private TeamColor color;
private NameTagVisibility nameTagVisibility;
private String prefix;
private TeamColor color;
private String suffix;
private Set<String> entities = new ObjectOpenHashSet<>();
private TeamData currentData;
private TeamData cachedData;
private boolean updating;
public Team(Scoreboard scoreboard, String id) {
this.scoreboard = scoreboard;
this.id = id;
currentData = new TeamData();
entities = new ObjectOpenHashSet<>();
}
public void addEntities(String... names) {
List<String> added = new ArrayList<>();
for (String name : names) {
if (entities.add(name)) {
added.add(name);
}
private void checkAddedEntities(List<String> added) {
if (added.size() == 0) {
return;
}
setUpdateType(UpdateType.UPDATE);
// we don't have to change the updateType,
// because the scores itself need updating, not the team
for (Objective objective : scoreboard.getObjectives().values()) {
for (Score score : objective.getScores().values()) {
if (added.contains(score.getName())) {
for (String addedEntity : added) {
Score score = objective.getScores().get(addedEntity);
if (score != null) {
score.setTeam(this);
}
}
}
}
public Team addEntities(String... names) {
List<String> added = new ArrayList<>();
for (String name : names) {
if (entities.add(name)) {
added.add(name);
}
}
checkAddedEntities(added);
return this;
}
public Team addEntities(Set<String> names) {
List<String> added = new ArrayList<>();
for (String name : names) {
if (entities.add(name)) {
added.add(name);
}
}
checkAddedEntities(added);
return this;
}
public void removeEntities(String... names) {
for (String name : names) {
entities.remove(name);
}
setUpdateType(UpdateType.UPDATE);
}
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)) {
this.prefix = "";
currentData.prefix = "";
return this;
}
this.prefix = prefix;
currentData.prefix = prefix;
return this;
}
@ -99,15 +128,92 @@ public class Team {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (suffix.length() == 4 && "null".equals(suffix)) {
this.suffix = "";
currentData.suffix = "";
return this;
}
this.suffix = suffix;
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.updateTime > cachedData.updateTime;
}
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;
}
cachedData.updateTime = currentData.updateTime;
cachedData.name = currentData.name;
cachedData.prefix = currentData.prefix;
cachedData.suffix = currentData.suffix;
}
public UpdateType getUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType;
}
public Team setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.updateTime = System.currentTimeMillis();
}
currentData.updateType = updateType;
return this;
}
public boolean isVisibleFor(String entity) {
switch (nameTagVisibility) {
case HIDE_FOR_OTHER_TEAMS:
return hasEntity(entity);
case HIDE_FOR_OWN_TEAM:
return !hasEntity(entity);
case ALWAYS:
return true;
case NEVER:
return false;
}
return true;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Getter
public static final class TeamData {
protected UpdateType updateType;
protected long updateTime;
protected String name;
protected String prefix;
protected String suffix;
protected TeamData() {
updateType = UpdateType.ADD;
}
public String getDisplayName(String score) {
return prefix + score + suffix;
}
}
}