Scoreboard improvements (#2078)

* Fixed some Scoreboard bugs and ScoreboardUpdater is now global

* Begin implementing below name support and better name display

* Use final for classes

* Revert "Begin implementing below name support and better name display"

This reverts commit 01babd636a.

* Don't remove objective if we're showing it

* Prevent concurrency exceptions when switching servers

* Properly fix the concurrency issue

* Fix inconsistencies in update cycle

* Few minor changes

* Port over this fix

* Fixed a problem that was introduced yesterday

* Cleanup

* Scores don't have to be removed before removing the objective itself

* Moved away from the general thread pool and some more changes

* Small changes

* Converted switch statements

Co-authored-by: Camotoy <20743703+Camotoy@users.noreply.github.com>
This commit is contained in:
Tim203 2021-09-29 20:36:27 +02:00 committed by GitHub
parent 7f4d3def58
commit 3323e5732c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 384 additions and 284 deletions

View file

@ -53,6 +53,7 @@ import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator; import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.world.WorldManager; import org.geysermc.connector.network.translators.world.WorldManager;
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator; import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.skin.FloodgateSkinUploader; import org.geysermc.connector.skin.FloodgateSkinUploader;
import org.geysermc.connector.utils.*; import org.geysermc.connector.utils.*;
import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesCipher;
@ -155,6 +156,7 @@ public class GeyserConnector {
ItemTranslator.init(); ItemTranslator.init();
MessageTranslator.init(); MessageTranslator.init();
LocaleUtils.init(); LocaleUtils.init();
ScoreboardUpdater.init();
ResourcePack.loadPacks(); ResourcePack.loadPacks();

View file

@ -29,38 +29,33 @@ import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.scoreboard.Objective;
import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.Scoreboard;
import org.geysermc.connector.scoreboard.ScoreboardUpdater; import org.geysermc.connector.scoreboard.ScoreboardUpdater.ScoreboardSession;
@Getter @Getter
public class WorldCache { public class WorldCache {
private final GeyserSession session; private final GeyserSession session;
private final ScoreboardSession scoreboardSession;
private Scoreboard scoreboard;
@Setter @Setter
private Difficulty difficulty = Difficulty.EASY; private Difficulty difficulty = Difficulty.EASY;
private Scoreboard scoreboard;
private final ScoreboardUpdater scoreboardUpdater;
public WorldCache(GeyserSession session) { public WorldCache(GeyserSession session) {
this.session = session; this.session = session;
this.scoreboard = new Scoreboard(session); this.scoreboard = new Scoreboard(session);
scoreboardUpdater = new ScoreboardUpdater(this); scoreboardSession = new ScoreboardSession(session);
scoreboardUpdater.start();
} }
public void removeScoreboard() { public void removeScoreboard() {
if (scoreboard != null) { if (scoreboard != null) {
for (Objective objective : scoreboard.getObjectives().values()) { scoreboard.removeScoreboard();
scoreboard.despawnObjective(objective);
}
scoreboard = new Scoreboard(session); scoreboard = new Scoreboard(session);
} }
} }
public int increaseAndGetScoreboardPacketsPerSecond() { public int increaseAndGetScoreboardPacketsPerSecond() {
int pendingPps = scoreboardUpdater.incrementAndGetPacketsPerSecond(); int pendingPps = scoreboardSession.getPendingPacketsPerSecond().incrementAndGet();
int pps = scoreboardUpdater.getPacketsPerSecond(); int pps = scoreboardSession.getPacketsPerSecond();
return Math.max(pps, pendingPps); return Math.max(pps, pendingPps);
} }
} }

View file

@ -25,12 +25,11 @@
package org.geysermc.connector.network.translators.java.scoreboard; package org.geysermc.connector.network.translators.java.scoreboard;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerDisplayScoreboardPacket;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerDisplayScoreboardPacket;
@Translator(packet = ServerDisplayScoreboardPacket.class) @Translator(packet = ServerDisplayScoreboardPacket.class)
public class JavaDisplayScoreboardTranslator extends PacketTranslator<ServerDisplayScoreboardPacket> { public class JavaDisplayScoreboardTranslator extends PacketTranslator<ServerDisplayScoreboardPacket> {

View file

@ -25,30 +25,39 @@
package org.geysermc.connector.network.translators.java.scoreboard; package org.geysermc.connector.network.translators.java.scoreboard;
import com.github.steveice10.mc.protocol.data.game.scoreboard.ObjectiveAction;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerScoreboardObjectivePacket;
import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.cache.WorldCache; import org.geysermc.connector.network.session.cache.WorldCache;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.scoreboard.Objective; import org.geysermc.connector.scoreboard.Objective;
import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.Scoreboard;
import org.geysermc.connector.scoreboard.ScoreboardUpdater; import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.network.translators.chat.MessageTranslator; import org.geysermc.connector.scoreboard.UpdateType;
import com.github.steveice10.mc.protocol.data.game.scoreboard.ObjectiveAction;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerScoreboardObjectivePacket;
@Translator(packet = ServerScoreboardObjectivePacket.class) @Translator(packet = ServerScoreboardObjectivePacket.class)
public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerScoreboardObjectivePacket> { public class JavaScoreboardObjectiveTranslator extends PacketTranslator<ServerScoreboardObjectivePacket> {
private final GeyserLogger logger = GeyserConnector.getInstance().getLogger();
@Override @Override
public void translate(GeyserSession session, ServerScoreboardObjectivePacket packet) { public void translate(GeyserSession session, ServerScoreboardObjectivePacket packet) {
WorldCache worldCache = session.getWorldCache(); WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard(); Scoreboard scoreboard = worldCache.getScoreboard();
Objective objective = scoreboard.getObjective(packet.getName());
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond(); int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
if (objective == null && packet.getAction() != ObjectiveAction.REMOVE) { Objective objective = scoreboard.getObjective(packet.getName());
objective = scoreboard.registerNewObjective(packet.getName(), false); 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");
return;
}
if ((objective == null || objective.getUpdateType() == UpdateType.REMOVE) && packet.getAction() != ObjectiveAction.REMOVE) {
objective = scoreboard.registerNewObjective(packet.getName());
} }
switch (packet.getAction()) { switch (packet.getAction()) {

View file

@ -25,26 +25,25 @@
package org.geysermc.connector.network.translators.java.scoreboard; package org.geysermc.connector.network.translators.java.scoreboard;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamAction;
import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerTeamPacket; import com.github.steveice10.mc.protocol.packet.ingame.server.scoreboard.ServerTeamPacket;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.GeyserLogger; import org.geysermc.connector.GeyserLogger;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.translators.PacketTranslator; import org.geysermc.connector.network.translators.PacketTranslator;
import org.geysermc.connector.network.translators.Translator; import org.geysermc.connector.network.translators.Translator;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import org.geysermc.connector.scoreboard.Scoreboard; import org.geysermc.connector.scoreboard.Scoreboard;
import org.geysermc.connector.scoreboard.ScoreboardUpdater; import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.scoreboard.Team; import org.geysermc.connector.scoreboard.Team;
import org.geysermc.connector.scoreboard.UpdateType; import org.geysermc.connector.scoreboard.UpdateType;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
import org.geysermc.connector.network.translators.chat.MessageTranslator;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set;
@Translator(packet = ServerTeamPacket.class) @Translator(packet = ServerTeamPacket.class)
public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> { public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
private static final GeyserLogger LOGGER = GeyserConnector.getInstance().getLogger(); private final GeyserLogger LOGGER = GeyserConnector.getInstance().getLogger();
@Override @Override
public void translate(GeyserSession session, ServerTeamPacket packet) { public void translate(GeyserSession session, ServerTeamPacket packet) {
@ -52,12 +51,16 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
LOGGER.debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers())); LOGGER.debug("Team packet " + packet.getTeamName() + " " + packet.getAction() + " " + Arrays.toString(packet.getPlayers()));
} }
if ((packet.getAction() == TeamAction.ADD_PLAYER || packet.getAction() == TeamAction.REMOVE_PLAYER) && packet.getPlayers().length == 0) {
return;
}
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond(); int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
Scoreboard scoreboard = session.getWorldCache().getScoreboard(); Scoreboard scoreboard = session.getWorldCache().getScoreboard();
Team team = scoreboard.getTeam(packet.getTeamName()); Team team = scoreboard.getTeam(packet.getTeamName());
switch (packet.getAction()) { switch (packet.getAction()) {
case CREATE -> scoreboard.registerNewTeam(packet.getTeamName(), toPlayerSet(packet.getPlayers())) case CREATE -> scoreboard.registerNewTeam(packet.getTeamName(), packet.getPlayers())
.setName(MessageTranslator.convertMessage(packet.getDisplayName())) .setName(MessageTranslator.convertMessage(packet.getDisplayName()))
.setColor(packet.getColor()) .setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility()) .setNameTagVisibility(packet.getNameTagVisibility())
@ -108,8 +111,4 @@ public class JavaTeamTranslator extends PacketTranslator<ServerTeamPacket> {
scoreboard.onUpdate(); scoreboard.onUpdate();
} }
} }
private Set<String> toPlayerSet(String[] players) {
return new ObjectOpenHashSet<>(players);
}
} }

View file

@ -66,7 +66,7 @@ public class JavaUpdateScoreTranslator extends PacketTranslator<ServerUpdateScor
if (objective != null) { if (objective != null) {
objective.removeScore(packet.getEntry()); objective.removeScore(packet.getEntry());
} else { } else {
for (Objective objective1 : scoreboard.getObjectives().values()) { for (Objective objective1 : scoreboard.getObjectives()) {
objective1.removeScore(packet.getEntry()); objective1.removeScore(packet.getEntry());
} }
} }

View file

@ -26,6 +26,7 @@
package org.geysermc.connector.scoreboard; package org.geysermc.connector.scoreboard;
import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition; import com.github.steveice10.mc.protocol.data.game.scoreboard.ScoreboardPosition;
import com.github.steveice10.mc.protocol.data.game.scoreboard.TeamColor;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -69,7 +70,7 @@ public final class Objective {
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) { public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
this(scoreboard); this(scoreboard);
this.objectiveName = objectiveName; this.objectiveName = objectiveName;
this.displaySlot = correctDisplaySlot(displaySlot); this.displaySlot = displaySlot;
this.displaySlotName = translateDisplaySlot(displaySlot); this.displaySlotName = translateDisplaySlot(displaySlot);
this.displayName = displayName; this.displayName = displayName;
this.type = type; this.type = type;
@ -83,14 +84,6 @@ public final class Objective {
}; };
} }
private static ScoreboardPosition correctDisplaySlot(ScoreboardPosition displaySlot) {
return switch (displaySlot) {
case BELOW_NAME -> ScoreboardPosition.BELOW_NAME;
case PLAYER_LIST -> ScoreboardPosition.PLAYER_LIST;
default -> ScoreboardPosition.SIDEBAR;
};
}
public void registerScore(String id, int score) { public void registerScore(String id, int score) {
if (!scores.containsKey(id)) { if (!scores.containsKey(id)) {
long scoreId = scoreboard.getNextId().getAndIncrement(); long scoreId = scoreboard.getNextId().getAndIncrement();
@ -145,12 +138,57 @@ public final class Objective {
public void setActive(ScoreboardPosition displaySlot) { public void setActive(ScoreboardPosition displaySlot) {
if (!active) { if (!active) {
active = true; active = true;
this.displaySlot = correctDisplaySlot(displaySlot); this.displaySlot = displaySlot;
displaySlotName = translateDisplaySlot(displaySlot); displaySlotName = translateDisplaySlot(displaySlot);
} }
} }
/**
* The objective will be removed on the next update
*/
public void pendingRemove() {
updateType = UpdateType.REMOVE;
}
public ScoreboardPosition getPositionCategory() {
return switch (displaySlot) {
case PLAYER_LIST -> ScoreboardPosition.PLAYER_LIST;
case BELOW_NAME -> ScoreboardPosition.BELOW_NAME;
default -> ScoreboardPosition.SIDEBAR;
};
}
public boolean hasTeamColor() {
return displaySlot != ScoreboardPosition.PLAYER_LIST &&
displaySlot != ScoreboardPosition.BELOW_NAME &&
displaySlot != ScoreboardPosition.SIDEBAR;
}
public 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 removed() { public void removed() {
active = false;
updateType = UpdateType.REMOVE;
scores = null; scores = null;
} }
} }

View file

@ -39,7 +39,7 @@ public final class Score {
/** /**
* Changes that have been made since the last cached data. * Changes that have been made since the last cached data.
*/ */
private Score.ScoreData currentData; private final Score.ScoreData currentData;
/** /**
* The data that is currently displayed to the Bedrock client. * The data that is currently displayed to the Bedrock client.
*/ */
@ -72,14 +72,14 @@ public final class Score {
if (currentData.team != null && team != null) { if (currentData.team != null && team != null) {
if (!currentData.team.equals(team)) { if (!currentData.team.equals(team)) {
currentData.team = team; currentData.team = team;
currentData.updateType = UpdateType.UPDATE; setUpdateType(UpdateType.UPDATE);
} }
return this; return this;
} }
// simplified from (this.team != null && team == null) || (this.team == null && team != null) // simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (currentData.team != null || team != null) { if (currentData.team != null || team != null) {
currentData.team = team; currentData.team = team;
currentData.updateType = UpdateType.UPDATE; setUpdateType(UpdateType.UPDATE);
} }
return this; return this;
} }
@ -126,13 +126,13 @@ public final class Score {
@Getter @Getter
public static final class ScoreData { public static final class ScoreData {
protected UpdateType updateType; private UpdateType updateType;
protected long updateTime; private long updateTime;
private Team team; private Team team;
private int score; private int score;
protected ScoreData() { private ScoreData() {
updateType = UpdateType.ADD; updateType = UpdateType.ADD;
} }
} }

View file

@ -42,14 +42,15 @@ import java.util.concurrent.atomic.AtomicLong;
import static org.geysermc.connector.scoreboard.UpdateType.*; import static org.geysermc.connector.scoreboard.UpdateType.*;
@Getter
public final class Scoreboard { public final class Scoreboard {
private final GeyserSession session; private final GeyserSession session;
private final GeyserLogger logger; private final GeyserLogger logger;
@Getter
private final AtomicLong nextId = new AtomicLong(0); private final AtomicLong nextId = new AtomicLong(0);
private final Map<String, Objective> objectives = new ConcurrentHashMap<>(); private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
private final Map<String, Team> teams = new HashMap<>(); private final Map<ScoreboardPosition, Objective> objectiveSlots = new HashMap<>();
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
private int lastAddScoreCount = 0; private int lastAddScoreCount = 0;
private int lastRemoveScoreCount = 0; private int lastRemoveScoreCount = 0;
@ -59,45 +60,52 @@ public final class Scoreboard {
this.logger = GeyserConnector.getInstance().getLogger(); this.logger = GeyserConnector.getInstance().getLogger();
} }
public Objective registerNewObjective(String objectiveId, boolean active) { public void removeScoreboard() {
Objective objective = objectives.get(objectiveId); Iterator<Objective> iterator = objectives.values().iterator();
if (active || objective != null) { while (iterator.hasNext()) {
return objective; Objective objective = iterator.next();
iterator.remove();
deleteObjective(objective, false);
} }
}
public 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;
}
deleteObjective(objective, true);
}
objective = new Objective(this, objectiveId); objective = new Objective(this, objectiveId);
objectives.put(objectiveId, objective); objectives.put(objectiveId, objective);
return objective; return objective;
} }
public Objective displayObjective(String objectiveId, ScoreboardPosition displaySlot) { public void displayObjective(String objectiveId, ScoreboardPosition displaySlot) {
Objective objective = objectives.get(objectiveId); Objective objective = objectives.get(objectiveId);
if (objective != null) { if (objective == null) {
if (!objective.isActive()) { return;
objective.setActive(displaySlot);
removeOldObjectives(objective);
return objective;
}
despawnObjective(objective);
} }
objective = new Objective(this, objectiveId, displaySlot, "unknown", 0); if (!objective.isActive()) {
objectives.put(objectiveId, objective); objective.setActive(displaySlot);
removeOldObjectives(objective); // for reactivated objectives
return objective; objective.setUpdateType(ADD);
}
private void removeOldObjectives(Objective newObjective) {
for (Objective next : objectives.values()) {
if (next.getId() == newObjective.getId()) {
continue;
}
if (next.getDisplaySlot() == newObjective.getDisplaySlot()) {
next.setUpdateType(REMOVE);
}
} }
Objective storedObjective = objectiveSlots.get(displaySlot);
if (storedObjective != null && storedObjective != objective) {
objective.pendingRemove();
}
objectiveSlots.put(displaySlot, objective);
} }
public Team registerNewTeam(String teamName, Set<String> players) { public Team registerNewTeam(String teamName, String[] players) {
Team team = teams.get(teamName); Team team = teams.get(teamName);
if (team != null) { if (team != null) {
logger.info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName)); logger.info(LanguageUtils.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
@ -113,6 +121,10 @@ public final class Scoreboard {
return objectives.get(objectiveName); return objectives.get(objectiveName);
} }
public Collection<Objective> getObjectives() {
return objectives.values();
}
public Team getTeam(String teamName) { public Team getTeam(String teamName) {
return teams.get(teamName); return teams.get(teamName);
} }
@ -120,7 +132,7 @@ public final class Scoreboard {
public void unregisterObjective(String objectiveName) { public void unregisterObjective(String objectiveName) {
Objective objective = getObjective(objectiveName); Objective objective = getObjective(objectiveName);
if (objective != null) { if (objective != null) {
objective.setUpdateType(REMOVE); objective.pendingRemove();
} }
} }
@ -132,114 +144,38 @@ public final class Scoreboard {
} }
public void onUpdate() { public void onUpdate() {
List<ScoreInfo> addScores = new ArrayList<>(getLastAddScoreCount()); List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount);
List<ScoreInfo> removeScores = new ArrayList<>(getLastRemoveScoreCount()); List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount);
List<Objective> removedObjectives = new ArrayList<>(); List<Objective> removedObjectives = new ArrayList<>();
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
Objective correctSidebar = null;
for (Objective objective : objectives.values()) { for (Objective objective : objectives.values()) {
if (!objective.isActive()) { // objective has been deleted
logger.debug("Ignoring non-active Scoreboard Objective '" + objective.getObjectiveName() + '\''); if (objective.getUpdateType() == REMOVE) {
continue;
}
// 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.getObjectiveName());
}
if (score.getUpdateType() != REMOVE && update) {
addScores.add(score.getCachedInfo());
}
if (score.getUpdateType() != ADD && update) {
removeScores.add(score.getCachedInfo());
}
}
continue;
}
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 = objectiveAdd || objectiveUpdate;
boolean remove = false;
if (team != null) {
if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
score.setTeam(null);
add = true;
remove = true;
}
}
add |= score.shouldUpdate();
remove |= score.shouldUpdate();
if (score.getUpdateType() == REMOVE || objectiveRemove) {
add = false;
}
if (score.getUpdateType() == ADD || objectiveRemove) {
remove = false;
}
if (objectiveRemove && score.getCachedData() != null) {
// This score has been sent to the client and needs to be removed since the objective is being removed
remove = true;
} else if (score.shouldUpdate()) {
score.update(objective.getObjectiveName());
}
if (add) {
addScores.add(score.getCachedInfo());
}
if (remove) {
removeScores.add(score.getCachedInfo());
}
// score is pending to be removed, so we can remove it from the objective
if (score.getUpdateType() == REMOVE) {
objective.removeScore0(score.getName());
}
score.setUpdateType(NOTHING);
}
if (objectiveRemove) {
removedObjectives.add(objective); removedObjectives.add(objective);
continue;
} }
if (objectiveUpdate) { if (playerTeam != null && playerTeam.getColor() == objective.getTeamColor()) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); correctSidebar = objective;
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
} }
if ((objectiveAdd || objectiveUpdate) && !objectiveRemove) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());
displayObjectivePacket.setCriteria("dummy");
displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName());
displayObjectivePacket.setSortOrder(1); // ??
session.sendUpstreamPacket(displayObjectivePacket);
}
objective.setUpdateType(NOTHING);
} }
if (correctSidebar == null) {
correctSidebar = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
}
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(); Iterator<Team> teamIterator = teams.values().iterator();
while (teamIterator.hasNext()) { while (teamIterator.hasNext()) {
Team current = teamIterator.next(); Team current = teamIterator.next();
switch (current.getUpdateType()) { switch (current.getCachedUpdateType()) {
case ADD, UPDATE -> current.markUpdated(); case ADD, UPDATE -> current.markUpdated();
case REMOVE -> teamIterator.remove(); case REMOVE -> teamIterator.remove();
} }
@ -259,17 +195,100 @@ public final class Scoreboard {
session.sendUpstreamPacket(setScorePacket); session.sendUpstreamPacket(setScorePacket);
} }
// prevents crashes in some cases
for (Objective objective : removedObjectives) { for (Objective objective : removedObjectives) {
despawnObjective(objective); deleteObjective(objective, true);
} }
lastAddScoreCount = addScores.size(); lastAddScoreCount = addScores.size();
lastRemoveScoreCount = removeScores.size(); lastRemoveScoreCount = removeScores.size();
} }
public void despawnObjective(Objective objective) { private void handleObjective(Objective objective, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
objectives.remove(objective.getObjectiveName()); if (objective == null || objective.getUpdateType() == REMOVE) {
return;
}
// 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.getObjectiveName());
}
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) {
removeScores.add(score.getCachedInfo());
// 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.getObjectiveName());
add = true;
}
if (add) {
addScores.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(); objective.removed();
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket(); RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();

View file

@ -25,100 +25,149 @@
package org.geysermc.connector.scoreboard; package org.geysermc.connector.scoreboard;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.geysermc.connector.GeyserConnector; import org.geysermc.connector.GeyserConnector;
import org.geysermc.connector.configuration.GeyserConfiguration; import org.geysermc.connector.configuration.GeyserConfiguration;
import org.geysermc.connector.network.session.GeyserSession; import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.cache.WorldCache; import org.geysermc.connector.network.session.cache.WorldCache;
import org.geysermc.connector.utils.LanguageUtils; import org.geysermc.connector.utils.LanguageUtils;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
public class ScoreboardUpdater extends Thread { public final class ScoreboardUpdater extends Thread {
public static final int FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD; public static final int FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD;
public static final int SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD = 250; public static final int SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD = 250;
private static final int FIRST_MILLIS_BETWEEN_UPDATES = 250; // 4 updates per second private static final int FIRST_MILLIS_BETWEEN_UPDATES = 250; // 4 updates per second
private static final int SECOND_MILLIS_BETWEEN_UPDATES = 1000 * 3; // 1 update per 3 seconds private static final int SECOND_MILLIS_BETWEEN_UPDATES = 1000; // 1 update per second
private static final boolean DEBUG_ENABLED; private static final boolean DEBUG_ENABLED;
private final WorldCache worldCache;
private final GeyserSession session;
private int millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES;
private long lastUpdate = System.currentTimeMillis();
private long lastLog = -1;
private long lastPacketsPerSecondUpdate = System.currentTimeMillis();
private final AtomicInteger packetsPerSecond = new AtomicInteger(0);
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
public ScoreboardUpdater(WorldCache worldCache) {
super("Scoreboard Updater");
this.worldCache = worldCache;
session = worldCache.getSession();
}
@Override
public void run() {
if (!session.isClosed()) {
long currentTime = System.currentTimeMillis();
// reset score-packets per second every second
if (currentTime - lastPacketsPerSecondUpdate > 1000) {
lastPacketsPerSecondUpdate = currentTime;
packetsPerSecond.set(pendingPacketsPerSecond.get());
pendingPacketsPerSecond.set(0);
}
if (currentTime - lastUpdate > millisBetweenUpdates) {
lastUpdate = currentTime;
int pps = packetsPerSecond.get();
if (pps >= FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
boolean reachedSecondThreshold = pps >= SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD;
if (reachedSecondThreshold) {
millisBetweenUpdates = SECOND_MILLIS_BETWEEN_UPDATES;
} else {
millisBetweenUpdates = FIRST_MILLIS_BETWEEN_UPDATES;
}
worldCache.getScoreboard().onUpdate();
if (DEBUG_ENABLED && (currentTime - lastLog > 60000)) { // one minute
int threshold = reachedSecondThreshold ?
SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD :
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD;
GeyserConnector.getInstance().getLogger().info(
LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached.log", session.getName(), threshold, pps) +
LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached", (millisBetweenUpdates / 1000.0))
);
lastLog = currentTime;
}
}
}
session.getConnector().getGeneralThreadPool().schedule(this, 50, TimeUnit.MILLISECONDS);
}
}
public int getPacketsPerSecond() {
return packetsPerSecond.get();
}
/**
* Increase the Scoreboard Packets Per Second and return the updated value
*/
public int incrementAndGetPacketsPerSecond() {
return pendingPacketsPerSecond.incrementAndGet();
}
static { static {
GeyserConfiguration config = GeyserConnector.getInstance().getConfig(); GeyserConfiguration config = GeyserConnector.getInstance().getConfig();
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD); FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD = Math.min(config.getScoreboardPacketThreshold(), SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD);
DEBUG_ENABLED = config.isDebugMode(); DEBUG_ENABLED = config.isDebugMode();
} }
private final GeyserConnector connector = GeyserConnector.getInstance();
private long lastUpdate = System.currentTimeMillis();
private long lastPacketsPerSecondUpdate = System.currentTimeMillis();
public static void init() {
new ScoreboardUpdater().start();
}
@Override
public void run() {
while (!connector.isShuttingDown()) {
long timeTillAction = getTimeTillNextAction();
if (timeTillAction > 0) {
sleepFor(timeTillAction);
continue;
}
long currentTime = System.currentTimeMillis();
// reset score-packets per second every second
if (currentTime - lastPacketsPerSecondUpdate >= 1000) {
lastPacketsPerSecondUpdate = currentTime;
for (GeyserSession session : connector.getPlayers()) {
ScoreboardSession scoreboardSession = session.getWorldCache().getScoreboardSession();
int oldPps = scoreboardSession.getPacketsPerSecond();
int newPps = scoreboardSession.getPendingPacketsPerSecond().get();
scoreboardSession.packetsPerSecond = newPps;
scoreboardSession.pendingPacketsPerSecond.set(0);
// just making sure that all updates are pushed before giving up control
if (oldPps >= FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD &&
newPps < FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
session.getWorldCache().getScoreboard().onUpdate();
}
}
}
if (currentTime - lastUpdate >= FIRST_MILLIS_BETWEEN_UPDATES) {
lastUpdate = currentTime;
for (GeyserSession session : connector.getPlayers()) {
WorldCache worldCache = session.getWorldCache();
ScoreboardSession scoreboardSession = worldCache.getScoreboardSession();
int pps = scoreboardSession.getPacketsPerSecond();
if (pps >= FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
boolean reachedSecondThreshold = pps >= SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD;
int millisBetweenUpdates = reachedSecondThreshold ?
SECOND_MILLIS_BETWEEN_UPDATES :
FIRST_MILLIS_BETWEEN_UPDATES;
if (currentTime - scoreboardSession.lastUpdate >= millisBetweenUpdates) {
worldCache.getScoreboard().onUpdate();
scoreboardSession.lastUpdate = currentTime;
if (DEBUG_ENABLED && (currentTime - scoreboardSession.lastLog >= 60000)) { // one minute
int threshold = reachedSecondThreshold ?
SECOND_SCORE_PACKETS_PER_SECOND_THRESHOLD :
FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD;
connector.getLogger().info(
LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached.log", session.getName(), threshold, pps) +
LanguageUtils.getLocaleStringLog("geyser.scoreboard.updater.threshold_reached", (millisBetweenUpdates / 1000.0))
);
scoreboardSession.lastLog = currentTime;
}
}
}
}
}
if (DEBUG_ENABLED) {
long timeSpent = System.currentTimeMillis() - currentTime;
if (timeSpent > 0) {
connector.getLogger().info(String.format(
"Scoreboard updater: took %s ms. Updated %s players",
timeSpent, connector.getPlayers().size()
));
}
}
long timeTillNextAction = getTimeTillNextAction();
sleepFor(timeTillNextAction);
}
}
private long getTimeTillNextAction() {
long currentTime = System.currentTimeMillis();
long timeUntilNextUpdate = FIRST_MILLIS_BETWEEN_UPDATES - (currentTime - lastUpdate);
long timeUntilPacketReset = 1000 - (currentTime - lastPacketsPerSecondUpdate);
return Math.min(timeUntilNextUpdate, timeUntilPacketReset);
}
private void sleepFor(long millis) {
if (millis <= 0) {
return;
}
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@RequiredArgsConstructor
@Getter
public static final class ScoreboardSession {
private final GeyserSession session;
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
private int packetsPerSecond;
private long lastUpdate;
private long lastLog;
}
} }

View file

@ -48,7 +48,7 @@ public final class Team {
@Setter private NameTagVisibility nameTagVisibility; @Setter private NameTagVisibility nameTagVisibility;
@Setter private TeamColor color; @Setter private TeamColor color;
private TeamData currentData; private final TeamData currentData;
private TeamData cachedData; private TeamData cachedData;
private boolean updating; private boolean updating;
@ -60,22 +60,6 @@ public final class Team {
entities = new ObjectOpenHashSet<>(); entities = new ObjectOpenHashSet<>();
} }
private void checkAddedEntities(List<String> added) {
if (added.size() == 0) {
return;
}
// we don't have to change the updateType,
// because the scores itself need updating, not the team
for (Objective objective : scoreboard.getObjectives().values()) {
for (String addedEntity : added) {
Score score = objective.getScores().get(addedEntity);
if (score != null) {
score.setTeam(this);
}
}
}
}
public Team addEntities(String... names) { public Team addEntities(String... names) {
List<String> added = new ArrayList<>(); List<String> added = new ArrayList<>();
for (String name : names) { for (String name : names) {
@ -83,18 +67,20 @@ public final class Team {
added.add(name); added.add(name);
} }
} }
checkAddedEntities(added);
return this;
}
public Team addEntities(Set<String> names) { if (added.size() == 0) {
List<String> added = new ArrayList<>(); return this;
for (String name : names) { }
if (entities.add(name)) { // we don't have to change the updateType,
added.add(name); // 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);
}
} }
} }
checkAddedEntities(added);
return this; return this;
} }
@ -169,6 +155,10 @@ public final class Team {
} }
public UpdateType getUpdateType() { public UpdateType getUpdateType() {
return currentData.updateType;
}
public UpdateType getCachedUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType; return cachedData != null ? cachedData.updateType : currentData.updateType;
} }
@ -196,14 +186,14 @@ public final class Team {
@Getter @Getter
public static final class TeamData { public static final class TeamData {
protected UpdateType updateType; private UpdateType updateType;
protected long updateTime; private long updateTime;
protected String name; private String name;
protected String prefix; private String prefix;
protected String suffix; private String suffix;
protected TeamData() { private TeamData() {
updateType = UpdateType.ADD; updateType = UpdateType.ADD;
} }