mirror of
https://github.com/mcMMO-Dev/mcMMO.git
synced 2026-08-18 03:05:10 +00:00
Implement polymorphic scoreboard backends with Folia-safe noop fallback
This commit is contained in:
@ -6,26 +6,14 @@ import org.bukkit.scoreboard.Objective;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
|
||||
/**
|
||||
* @deprecated No longer fired since mcMMO moved to a packet-based sidebar (scoreboard-library) for
|
||||
* Folia/Canvas compatibility. This event exposes a Bukkit {@link Objective}, which the packet
|
||||
* implementation does not have - sidebar lines are sent directly as packets and there is no Bukkit
|
||||
* objective to register/unregister. The event class is kept only for binary compatibility with
|
||||
* external plugins; it will never be called by mcMMO.
|
||||
* Fired when mcMMO's Bukkit scoreboard backend registers or unregisters the sidebar objective.
|
||||
* <p>
|
||||
* Changing the field type from {@link Objective} to {@code String} to "revive" this event was
|
||||
* considered but rejected: it would be a binary-incompatible API break (different method
|
||||
* signature on {@link #getTargetObjective()}) that would crash any plugin compiled against the
|
||||
* old API with {@code NoSuchMethodError}, and the cancellation semantics
|
||||
* ({@link ScoreboardObjectiveEventReason#UNREGISTER_THIS_OBJECTIVE} /
|
||||
* {@link ScoreboardObjectiveEventReason#REGISTER_NEW_OBJECTIVE}) no longer map to anything in the
|
||||
* packet implementation.
|
||||
* <p>
|
||||
* <b>Suggestion for maintainers:</b> if a hook for sidebar title/content changes is needed in
|
||||
* the packet era, please add a new dedicated event (e.g. {@code McMMOSidebarUpdateEvent}) that
|
||||
* carries a {@code String} title and a list of lines. That keeps the old event's binary contract
|
||||
* intact while giving the new API clean, unambiguous semantics.
|
||||
* This event is backend-specific:
|
||||
* <ul>
|
||||
* <li>Fired by the Bukkit backend (non-Folia servers).</li>
|
||||
* <li>Not fired by packet/no-op backends, which have no Bukkit {@link Objective}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Deprecated
|
||||
public class McMMOScoreboardObjectiveEvent extends McMMOScoreboardEvent implements Cancellable {
|
||||
protected boolean cancelled;
|
||||
|
||||
|
||||
@ -11,37 +11,42 @@ import com.gmail.nossr50.locale.LocaleLoader;
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import com.gmail.nossr50.util.LogUtils;
|
||||
import com.gmail.nossr50.util.Misc;
|
||||
import com.gmail.nossr50.util.PaperUtil;
|
||||
import com.gmail.nossr50.util.player.UserManager;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.BukkitScoreboardBackend;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.NoopScoreboardBackend;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.PlayerBoard;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.PacketScoreboardBackend;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.ScoreboardBackend;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.ScoreboardBackendSelector;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.ScoreboardBackendType;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.megavex.scoreboardlibrary.api.ScoreboardLibrary;
|
||||
import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableException;
|
||||
import net.megavex.scoreboardlibrary.api.noop.NoopScoreboardLibrary;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ObjectiveDisplaySlot;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ObjectiveManager;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreboardObjective;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Manages the Scoreboards used to display a variety of mcMMO related information to the player
|
||||
*/
|
||||
public class ScoreboardManager {
|
||||
static final Map<String, ScoreboardWrapper> PLAYER_SCOREBOARDS = new HashMap<>();
|
||||
static final Map<String, ScoreboardWrapper> PLAYER_SCOREBOARDS = new ConcurrentHashMap<>();
|
||||
|
||||
// do not localize; these are internal identifiers
|
||||
static final String SIDEBAR_OBJECTIVE = "mcmmo_sidebar";
|
||||
public static final String SIDEBAR_OBJECTIVE = "mcmmo_sidebar";
|
||||
// Randomized per server start so it can never collide with a leftover objective of the same
|
||||
// name on a client - e.g. the old Bukkit implementation's 'mcmmo_pwrlvl' lingering after an
|
||||
// in-place upgrade, or another plugin's objective. Minecraft caps objective names at 16 chars,
|
||||
// so this stays well under: "mcmmo_" (6) + 8 hex = 14.
|
||||
static final String POWER_OBJECTIVE = "mcmmo_" + java.util.UUID.randomUUID()
|
||||
public static final String POWER_OBJECTIVE = "mcmmo_" + java.util.UUID.randomUUID()
|
||||
.toString().replace("-", "").substring(0, 8);
|
||||
|
||||
static final String HEADER_STATS = LocaleLoader.getString("Scoreboard.Header.PlayerStats");
|
||||
@ -64,22 +69,17 @@ public class ScoreboardManager {
|
||||
static final Map<SuperAbilityType, String> abilityLabelsSkill;
|
||||
|
||||
public static final String DISPLAY_NAME = "powerLevel";
|
||||
public static ScoreboardLibrary scoreboardLibrary;
|
||||
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacySection();
|
||||
|
||||
// Packet-based below-name power level tag (replaces the old Bukkit main-scoreboard objective)
|
||||
private static ObjectiveManager powerLevelObjectiveManager;
|
||||
private static ScoreboardObjective powerLevelObjective;
|
||||
private static ScoreboardBackend backend;
|
||||
private static ScoreboardBackendType backendType = ScoreboardBackendType.NOOP;
|
||||
|
||||
/*
|
||||
* Initializes the static label maps for our scoreboards.
|
||||
*
|
||||
* Note: the packet ScoreboardLibrary is NOT loaded here. Loading it can fail on unsupported
|
||||
* server versions, and doing that inside a static initializer would turn the failure into an
|
||||
* unrecoverable ExceptionInInitializerError on first access to this class. Instead the library
|
||||
* is loaded explicitly from the main class via {@link #init()} during onEnable, guarded by the
|
||||
* scoreboards-enabled config, so it can fail gracefully.
|
||||
* Note: backend resources are not loaded here. Loading packet adapters can fail on unsupported
|
||||
* server versions, and doing that inside a static initializer would turn failures into
|
||||
* ExceptionInInitializerError on first access. Backends are initialized explicitly via
|
||||
* {@link #init()} during onEnable, guarded by the scoreboards-enabled config.
|
||||
*/
|
||||
static {
|
||||
/*
|
||||
@ -173,27 +173,55 @@ public class ScoreboardManager {
|
||||
abilityLabelsSkill = abilityLabelSkillBuilder.build();
|
||||
}
|
||||
|
||||
private static final List<String> dirtyPowerLevels = new ArrayList<>();
|
||||
private static final Set<String> dirtyPowerLevels = ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Loads the packet-based {@link ScoreboardLibrary}. Call this once from the main class during
|
||||
* onEnable (guarded by the scoreboards-enabled config).
|
||||
* <p>
|
||||
* If the running server version has no packet adapter, we fall back to a
|
||||
* {@link NoopScoreboardLibrary} as recommended by the library author: scoreboards simply won't
|
||||
* render, but nothing throws and the rest of the plugin keeps working.
|
||||
*/
|
||||
public static void init() {
|
||||
if (scoreboardLibrary != null && !scoreboardLibrary.closed()) {
|
||||
return; // already initialized
|
||||
if (backend != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mcMMO.p.getGeneralConfig().getScoreboardsEnabled()) {
|
||||
backendType = ScoreboardBackendType.NOOP;
|
||||
backend = new NoopScoreboardBackend();
|
||||
backend.init();
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean isFolia = PaperUtil.isFolia();
|
||||
final ScoreboardBackendType selectedType = ScoreboardBackendSelector.select(
|
||||
isFolia,
|
||||
mcMMO.getMinecraftGameVersion());
|
||||
backendType = selectedType;
|
||||
|
||||
switch (selectedType) {
|
||||
case BUKKIT -> backend = new BukkitScoreboardBackend();
|
||||
case PACKET -> backend = new PacketScoreboardBackend();
|
||||
case NOOP -> {
|
||||
backend = new NoopScoreboardBackend();
|
||||
mcMMO.p.getLogger().warning(
|
||||
"Folia detected on unsupported Minecraft version "
|
||||
+ mcMMO.getMinecraftGameVersion().getVersionStr()
|
||||
+ " (supported packet scoreboard max is "
|
||||
+ ScoreboardBackendSelector.MAX_PACKET_VERSION_STRING
|
||||
+ "). Scoreboards are disabled to avoid Folia async scoreboard issues. "
|
||||
+ "Please update mcMMO.");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
scoreboardLibrary = ScoreboardLibrary.loadScoreboardLibrary(mcMMO.p);
|
||||
} catch (NoPacketAdapterAvailableException e) {
|
||||
scoreboardLibrary = new NoopScoreboardLibrary();
|
||||
mcMMO.p.getLogger().warning(
|
||||
"Server version unsupported, scoreboard functionality will not be visible!");
|
||||
backend.init();
|
||||
} catch (RuntimeException e) {
|
||||
if (selectedType == ScoreboardBackendType.PACKET) {
|
||||
mcMMO.p.getLogger().warning(
|
||||
"Packet scoreboard backend failed to initialize ("
|
||||
+ e.getMessage()
|
||||
+ "). Falling back to a Folia-safe no-op scoreboard backend.");
|
||||
backendType = ScoreboardBackendType.NOOP;
|
||||
backend = new NoopScoreboardBackend();
|
||||
backend.init();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -234,48 +262,40 @@ public class ScoreboardManager {
|
||||
|
||||
// Called by PlayerJoinEvent listener
|
||||
public static void setupPlayer(Player player) {
|
||||
ensureBackendReady();
|
||||
teardownPlayer(player);
|
||||
PLAYER_SCOREBOARDS.put(player.getName(), makeNewScoreboard(player));
|
||||
dirtyPowerLevels.add(player.getName());
|
||||
|
||||
// Make the player see the below-name power level tag, if enabled
|
||||
if (mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled() && getPowerLevelObjective() != null) {
|
||||
powerLevelObjectiveManager.addPlayer(player);
|
||||
if (mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled() && backend.isPowerLevelTagActive()) {
|
||||
backend.setupPowerLevelTag(player);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by PlayerQuitEvent listener and OnPlayerTeleport under certain circumstances
|
||||
public static void teardownPlayer(Player player) {
|
||||
ensureBackendReady();
|
||||
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only remove the player from the shared power-level ObjectiveManager while they are still
|
||||
// online. scoreboard-library tracks players in a weak-key map and processes removals on an
|
||||
// async tick; if we enqueue a removal for an already-offline player, their Player object can
|
||||
// be garbage-collected before that tick runs, which makes the library's
|
||||
// ObjectiveManagerImpl.tick() throw an NPE (requireNonNull on the now-evicted map entry).
|
||||
// For offline players we let the library's weak-key map evict them on its own instead.
|
||||
if (powerLevelObjectiveManager != null && !powerLevelObjectiveManager.closed()
|
||||
&& player.isOnline()) {
|
||||
powerLevelObjectiveManager.removePlayer(player);
|
||||
// Remove the player's score entry so it doesn't accumulate for offline players.
|
||||
// Without this, every player who ever joined adds a persistent entry to the scores map;
|
||||
// when a new player connects, the library sends the full map to them (potentially
|
||||
// thousands of entries) causing a large initial packet burst.
|
||||
if (powerLevelObjective != null) {
|
||||
powerLevelObjective.removeScore(player.getName());
|
||||
}
|
||||
if (isBukkitBackendActive() && player.isOnline() && player.isValid()
|
||||
&& Bukkit.getScoreboardManager() != null) {
|
||||
player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
}
|
||||
|
||||
backend.removePowerLevelTag(player);
|
||||
|
||||
ScoreboardWrapper wrapper = PLAYER_SCOREBOARDS.remove(player.getName());
|
||||
if (wrapper != null) {
|
||||
wrapper.close(); // cancels revertTask, cooldownTask, and updateTask internally
|
||||
wrapper.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Called in onDisable()
|
||||
public static void teardownAll() {
|
||||
ensureBackendReady();
|
||||
ImmutableList<Player> onlinePlayers = ImmutableList.copyOf(
|
||||
mcMMO.p.getServer().getOnlinePlayers());
|
||||
LogUtils.debug(mcMMO.p.getLogger(),
|
||||
@ -284,24 +304,18 @@ public class ScoreboardManager {
|
||||
teardownPlayer(player);
|
||||
}
|
||||
|
||||
// The external plugin never closes the library; do it here so reload/disable doesn't
|
||||
// leak the packet listeners or registered players.
|
||||
if (powerLevelObjectiveManager != null && !powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager.close();
|
||||
if (backend != null) {
|
||||
backend.shutdown();
|
||||
}
|
||||
powerLevelObjectiveManager = null;
|
||||
powerLevelObjective = null;
|
||||
|
||||
if (scoreboardLibrary != null && !scoreboardLibrary.closed()) {
|
||||
scoreboardLibrary.close();
|
||||
}
|
||||
scoreboardLibrary = null;
|
||||
backend = null;
|
||||
backendType = ScoreboardBackendType.NOOP;
|
||||
}
|
||||
|
||||
// Called by ScoreboardWrapper when its Player logs off and an action tries to be performed
|
||||
public static void cleanup(ScoreboardWrapper wrapper) {
|
||||
ensureBackendReady();
|
||||
PLAYER_SCOREBOARDS.remove(wrapper.playerName);
|
||||
wrapper.close(); // cancels revertTask, cooldownTask, and updateTask internally
|
||||
wrapper.close();
|
||||
}
|
||||
|
||||
// Called by internal level-up event listener
|
||||
@ -556,10 +570,10 @@ public class ScoreboardManager {
|
||||
* @return false if power levels are disabled
|
||||
*/
|
||||
public static boolean powerLevelHeartbeat() {
|
||||
ScoreboardObjective mainObjective = getPowerLevelObjective();
|
||||
ensureBackendReady();
|
||||
|
||||
if (mainObjective == null) {
|
||||
return false; // indicates
|
||||
if (!backend.isPowerLevelTagActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String playerName : dirtyPowerLevels) {
|
||||
@ -569,9 +583,8 @@ public class ScoreboardManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
int power = mmoPlayer.getPowerLevel();
|
||||
|
||||
mainObjective.score(playerName, power);
|
||||
final int power = mmoPlayer.getPowerLevel();
|
||||
backend.setPowerLevel(playerName, power);
|
||||
}
|
||||
|
||||
dirtyPowerLevels.clear();
|
||||
@ -586,33 +599,8 @@ public class ScoreboardManager {
|
||||
* @return the power level objective, or null if disabled
|
||||
*/
|
||||
public static @Nullable ScoreboardObjective getPowerLevelObjective() {
|
||||
if (!mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled()) {
|
||||
if (powerLevelObjectiveManager != null && !powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager.close();
|
||||
LogUtils.debug(mcMMO.p.getLogger(),
|
||||
"Removed leftover objects from Power Level Tags.");
|
||||
}
|
||||
powerLevelObjectiveManager = null;
|
||||
powerLevelObjective = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scoreboardLibrary == null || scoreboardLibrary.closed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (powerLevelObjectiveManager == null || powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager = scoreboardLibrary.createObjectiveManager();
|
||||
powerLevelObjective = powerLevelObjectiveManager.create(POWER_OBJECTIVE);
|
||||
powerLevelObjective.value(LEGACY.deserialize(TAG_POWER_LEVEL));
|
||||
powerLevelObjectiveManager.display(ObjectiveDisplaySlot.belowName(), powerLevelObjective);
|
||||
|
||||
// Show the tag to everyone currently online
|
||||
powerLevelObjectiveManager.addPlayers(
|
||||
new ArrayList<>(mcMMO.p.getServer().getOnlinePlayers()));
|
||||
}
|
||||
|
||||
return powerLevelObjective;
|
||||
ensureBackendReady();
|
||||
return backend.getPacketPowerLevelObjective();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -635,20 +623,29 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
public static boolean isBoardShown(String playerName) {
|
||||
return PLAYER_SCOREBOARDS.get(playerName).isBoardShown();
|
||||
ScoreboardWrapper wrapper = PLAYER_SCOREBOARDS.get(playerName);
|
||||
return wrapper != null && wrapper.isBoardShown();
|
||||
}
|
||||
|
||||
public static void clearBoard(String playerName) {
|
||||
PLAYER_SCOREBOARDS.get(playerName).tryRevertBoard();
|
||||
ScoreboardWrapper wrapper = PLAYER_SCOREBOARDS.get(playerName);
|
||||
if (wrapper != null) {
|
||||
wrapper.tryRevertBoard();
|
||||
}
|
||||
}
|
||||
|
||||
public static void keepBoard(String playerName) {
|
||||
PLAYER_SCOREBOARDS.get(playerName).cancelRevert();
|
||||
ScoreboardWrapper wrapper = PLAYER_SCOREBOARDS.get(playerName);
|
||||
if (wrapper != null) {
|
||||
wrapper.cancelRevert();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setRevertTimer(String playerName, int seconds) {
|
||||
PLAYER_SCOREBOARDS.get(playerName)
|
||||
.showBoardAndScheduleRevert(seconds * Misc.TICK_CONVERSION_FACTOR);
|
||||
ScoreboardWrapper wrapper = PLAYER_SCOREBOARDS.get(playerName);
|
||||
if (wrapper != null) {
|
||||
wrapper.showBoardAndScheduleRevert(seconds * Misc.TICK_CONVERSION_FACTOR);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPlayerBoardSetup(@NotNull String playerName) {
|
||||
@ -656,21 +653,51 @@ public class ScoreboardManager {
|
||||
}
|
||||
|
||||
public static @NotNull ScoreboardWrapper makeNewScoreboard(Player player) {
|
||||
// The board is now a packet-based sidebar overlay, so there is no Bukkit Scoreboard to
|
||||
// create or swap. We still fire McMMOScoreboardMakeboardEvent so external plugins that
|
||||
// listened for board creation keep getting notified.
|
||||
//
|
||||
// Compatibility note: this event was designed around Bukkit Scoreboards (target/current
|
||||
// board). Since none exist here, we pass the player's current Bukkit scoreboard for both
|
||||
// arguments purely as a non-null placeholder. The event's board fields are NOT consumed by
|
||||
// the packet implementation - it is informational only, and setTargetBoard()/setTargetPlayer()
|
||||
// no longer influence which board is shown.
|
||||
ensureBackendReady();
|
||||
|
||||
final Scoreboard eventTargetBoard = backend.createEventTargetBoard(player);
|
||||
final Scoreboard currentBoard = player.getScoreboard();
|
||||
McMMOScoreboardMakeboardEvent event = new McMMOScoreboardMakeboardEvent(
|
||||
player.getScoreboard(), player.getScoreboard(), player,
|
||||
eventTargetBoard, currentBoard, player,
|
||||
ScoreboardEventReason.CREATING_NEW_SCOREBOARD);
|
||||
player.getServer().getPluginManager().callEvent(event);
|
||||
|
||||
// Honour any player reassignment the event performed; the sidebar follows that player.
|
||||
return new ScoreboardWrapper(event.getTargetPlayer());
|
||||
final Player targetPlayer = event.getTargetPlayer();
|
||||
final Scoreboard targetBoard = event.getTargetBoard() == null
|
||||
? currentBoard
|
||||
: event.getTargetBoard();
|
||||
final PlayerBoard playerBoard = backend.createPlayerBoard(targetPlayer, targetBoard);
|
||||
return new ScoreboardWrapper(targetPlayer, playerBoard);
|
||||
}
|
||||
|
||||
public static @NotNull ScoreboardBackend getBackend() {
|
||||
ensureBackendReady();
|
||||
return backend;
|
||||
}
|
||||
|
||||
public static @NotNull ScoreboardBackendType getBackendType() {
|
||||
ensureBackendReady();
|
||||
return backendType;
|
||||
}
|
||||
|
||||
public static boolean isBukkitBackendActive() {
|
||||
return getBackendType() == ScoreboardBackendType.BUKKIT;
|
||||
}
|
||||
|
||||
public static void onPlayerBoardClosed(@NotNull String playerName) {
|
||||
ensureBackendReady();
|
||||
backend.onPlayerBoardClosed(playerName);
|
||||
}
|
||||
|
||||
private static void ensureBackendReady() {
|
||||
if (backend == null) {
|
||||
init();
|
||||
}
|
||||
|
||||
if (backend == null) {
|
||||
backend = new NoopScoreboardBackend();
|
||||
backendType = ScoreboardBackendType.NOOP;
|
||||
backend.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,17 +9,17 @@ import com.gmail.nossr50.events.scoreboard.McMMOScoreboardRevertEvent;
|
||||
import com.gmail.nossr50.events.scoreboard.ScoreboardEventReason;
|
||||
import com.gmail.nossr50.locale.LocaleLoader;
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import com.gmail.nossr50.util.LogUtils;
|
||||
import com.gmail.nossr50.util.Misc;
|
||||
import com.gmail.nossr50.util.player.UserManager;
|
||||
import com.gmail.nossr50.util.scoreboards.ScoreboardManager.SidebarType;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.PlayerBoard;
|
||||
import com.gmail.nossr50.util.scoreboards.backend.SidebarLine;
|
||||
import com.gmail.nossr50.util.skills.SkillTools;
|
||||
import com.tcoded.folialib.wrapper.task.WrappedTask;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreFormat;
|
||||
import net.megavex.scoreboardlibrary.api.sidebar.Sidebar;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -27,24 +27,18 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Wraps a packet-based {@link Sidebar} (scoreboard-library) for a single player.
|
||||
* <p>
|
||||
* Unlike the previous Bukkit {@code Scoreboard} implementation, the sidebar here is sent via
|
||||
* packets and overlays whatever scoreboard the player already has. Showing/hiding the board is
|
||||
* just {@code sidebar.addPlayer/removePlayer}; there is no Bukkit board to swap or revert, which
|
||||
* removes the objective register/unregister race that crashed on Folia/Canvas.
|
||||
* Wraps a backend-specific player board for a single player.
|
||||
*/
|
||||
public class ScoreboardWrapper {
|
||||
// scoreboard-library hard-caps a sidebar at 15 lines.
|
||||
private static final int MAX_LINES = Sidebar.MAX_LINES;
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacySection();
|
||||
private static final int MAX_LINES = 15;
|
||||
|
||||
// Initialization variables
|
||||
public final String playerName;
|
||||
public final Player player;
|
||||
private final Sidebar sidebar;
|
||||
private final PlayerBoard playerBoard;
|
||||
private boolean tippedKeep = false;
|
||||
private boolean tippedClear = false;
|
||||
private Scoreboard oldBoard = null;
|
||||
|
||||
// Internal usage variables (should exist)
|
||||
private SidebarType sidebarType;
|
||||
@ -59,15 +53,11 @@ public class ScoreboardWrapper {
|
||||
private Map<PrimarySkillType, Integer> rankData = null;
|
||||
private List<PlayerStat> leaderboardData = null;
|
||||
|
||||
public ScoreboardWrapper(Player player) {
|
||||
public ScoreboardWrapper(Player player, PlayerBoard playerBoard) {
|
||||
this.player = player;
|
||||
this.playerName = player.getName();
|
||||
this.sidebarType = SidebarType.NONE;
|
||||
this.sidebar = ScoreboardManager.scoreboardLibrary.createSidebar(MAX_LINES);
|
||||
}
|
||||
|
||||
private Component toComponent(String legacy) {
|
||||
return LEGACY.deserialize(legacy == null ? "" : legacy);
|
||||
this.playerBoard = playerBoard;
|
||||
}
|
||||
|
||||
public WrappedTask updateTask = null;
|
||||
@ -127,7 +117,9 @@ public class ScoreboardWrapper {
|
||||
try {
|
||||
cooldownTask.cancel();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.debug(mcMMO.p.getLogger(),
|
||||
"Unable to cancel cooldown scoreboard task for " + playerName + ": "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
cooldownTask = null;
|
||||
@ -147,9 +139,9 @@ public class ScoreboardWrapper {
|
||||
}
|
||||
|
||||
public void showBoardWithNoRevert() {
|
||||
Player player = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
final Player onlinePlayer = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
|
||||
if (player == null) {
|
||||
if (onlinePlayer == null) {
|
||||
ScoreboardManager.cleanup(this);
|
||||
return;
|
||||
}
|
||||
@ -158,14 +150,24 @@ public class ScoreboardWrapper {
|
||||
revertTask.cancel();
|
||||
}
|
||||
|
||||
sidebar.addPlayer(player);
|
||||
final boolean alreadyShown = playerBoard.isShown();
|
||||
final Scoreboard previousBoard = playerBoard.show();
|
||||
|
||||
if (ScoreboardManager.isBukkitBackendActive()) {
|
||||
if (alreadyShown && oldBoard == null && ScoreboardManager.getScoreboardManager() != null) {
|
||||
oldBoard = ScoreboardManager.getScoreboardManager().getMainScoreboard();
|
||||
} else if (!alreadyShown) {
|
||||
oldBoard = previousBoard;
|
||||
}
|
||||
}
|
||||
|
||||
revertTask = null;
|
||||
}
|
||||
|
||||
public void showBoardAndScheduleRevert(int ticks) {
|
||||
Player player = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
final Player onlinePlayer = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
|
||||
if (player == null) {
|
||||
if (onlinePlayer == null) {
|
||||
ScoreboardManager.cleanup(this);
|
||||
return;
|
||||
}
|
||||
@ -174,15 +176,24 @@ public class ScoreboardWrapper {
|
||||
revertTask.cancel();
|
||||
}
|
||||
|
||||
sidebar.addPlayer(player);
|
||||
final boolean alreadyShown = playerBoard.isShown();
|
||||
final Scoreboard previousBoard = playerBoard.show();
|
||||
if (ScoreboardManager.isBukkitBackendActive()) {
|
||||
if (alreadyShown && oldBoard == null && ScoreboardManager.getScoreboardManager() != null) {
|
||||
oldBoard = ScoreboardManager.getScoreboardManager().getMainScoreboard();
|
||||
} else if (!alreadyShown) {
|
||||
oldBoard = previousBoard;
|
||||
}
|
||||
}
|
||||
|
||||
revertTask = mcMMO.p.getFoliaLib().getScheduler()
|
||||
.runAtEntityLater(player, new ScoreboardChangeTask(), ticks);
|
||||
.runAtEntityLater(onlinePlayer, new ScoreboardChangeTask(), ticks);
|
||||
|
||||
if (UserManager.getPlayer(playerName) == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerProfile profile = UserManager.getPlayer(player).getProfile();
|
||||
PlayerProfile profile = UserManager.getPlayer(onlinePlayer).getProfile();
|
||||
|
||||
if (profile.getScoreboardTipsShown() >= mcMMO.p.getGeneralConfig().getTipsAmount()) {
|
||||
return;
|
||||
@ -190,43 +201,52 @@ public class ScoreboardWrapper {
|
||||
|
||||
if (!tippedKeep) {
|
||||
tippedKeep = true;
|
||||
player.sendMessage(LocaleLoader.getString("Commands.Scoreboard.Tip.Keep"));
|
||||
onlinePlayer.sendMessage(LocaleLoader.getString("Commands.Scoreboard.Tip.Keep"));
|
||||
} else if (!tippedClear) {
|
||||
tippedClear = true;
|
||||
player.sendMessage(LocaleLoader.getString("Commands.Scoreboard.Tip.Clear"));
|
||||
onlinePlayer.sendMessage(LocaleLoader.getString("Commands.Scoreboard.Tip.Clear"));
|
||||
profile.increaseTipsShown();
|
||||
}
|
||||
}
|
||||
|
||||
public void tryRevertBoard() {
|
||||
Player player = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
Player onlinePlayer = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
|
||||
if (player == null) {
|
||||
if (onlinePlayer == null) {
|
||||
ScoreboardManager.cleanup(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire McMMOScoreboardRevertEvent so external plugins still get notified when an mcMMO
|
||||
// board is cleared. Previously this swapped the player's Bukkit scoreboard back to the
|
||||
// saved "old board"; with the packet sidebar there is nothing to swap, so we only need to
|
||||
// stop showing our overlay.
|
||||
//
|
||||
// Compatibility note: the event was built around Bukkit Scoreboards (target/current board).
|
||||
// None exist here, so we pass the player's current Bukkit scoreboard for both arguments as
|
||||
// a non-null placeholder. The board fields are informational only - the packet sidebar does
|
||||
// not read them back, so setTargetBoard() no longer has any effect. setTargetPlayer() is
|
||||
// still honoured to decide which player the overlay is removed from.
|
||||
if (isBoardShown()) {
|
||||
if (ScoreboardManager.isBukkitBackendActive()) {
|
||||
if (oldBoard != null && isBoardShown()) {
|
||||
McMMOScoreboardRevertEvent event = new McMMOScoreboardRevertEvent(
|
||||
oldBoard,
|
||||
onlinePlayer.getScoreboard(),
|
||||
onlinePlayer,
|
||||
ScoreboardEventReason.REVERTING_BOARD);
|
||||
onlinePlayer.getServer().getPluginManager().callEvent(event);
|
||||
onlinePlayer = event.getTargetPlayer();
|
||||
playerBoard.hide(onlinePlayer, event.getTargetBoard());
|
||||
oldBoard = null;
|
||||
} else if (oldBoard != null) {
|
||||
LogUtils.debug(mcMMO.p.getLogger(),
|
||||
"Not reverting scoreboard for "
|
||||
+ playerName
|
||||
+ " - scoreboard was changed by another plugin.");
|
||||
}
|
||||
} else if (isBoardShown()) {
|
||||
McMMOScoreboardRevertEvent event = new McMMOScoreboardRevertEvent(
|
||||
player.getScoreboard(), player.getScoreboard(), player,
|
||||
onlinePlayer.getScoreboard(),
|
||||
onlinePlayer.getScoreboard(),
|
||||
onlinePlayer,
|
||||
ScoreboardEventReason.REVERTING_BOARD);
|
||||
player.getServer().getPluginManager().callEvent(event);
|
||||
player = event.getTargetPlayer();
|
||||
onlinePlayer.getServer().getPluginManager().callEvent(event);
|
||||
onlinePlayer = event.getTargetPlayer();
|
||||
playerBoard.hide(onlinePlayer, event.getTargetBoard());
|
||||
} else {
|
||||
playerBoard.hide(onlinePlayer, null);
|
||||
}
|
||||
|
||||
// Packet sidebar: hiding is just removing the player from the sidebar overlay.
|
||||
sidebar.removePlayer(player);
|
||||
|
||||
cancelRevert();
|
||||
|
||||
sidebarType = SidebarType.NONE;
|
||||
@ -239,14 +259,14 @@ public class ScoreboardWrapper {
|
||||
}
|
||||
|
||||
public boolean isBoardShown() {
|
||||
Player player = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
Player onlinePlayer = mcMMO.p.getServer().getPlayerExact(playerName);
|
||||
|
||||
if (player == null) {
|
||||
if (onlinePlayer == null) {
|
||||
ScoreboardManager.cleanup(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
return sidebar.players().contains(player);
|
||||
return playerBoard.isShown();
|
||||
}
|
||||
|
||||
public void cancelRevert() {
|
||||
@ -259,8 +279,7 @@ public class ScoreboardWrapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the underlying packet sidebar. Must be called when the player logs off or the
|
||||
* plugin shuts down, otherwise the library keeps the player registered.
|
||||
* Releases backend resources and scheduled tasks.
|
||||
*/
|
||||
public void close() {
|
||||
try {
|
||||
@ -274,9 +293,8 @@ public class ScoreboardWrapper {
|
||||
// best-effort task cleanup
|
||||
}
|
||||
|
||||
if (!sidebar.closed()) {
|
||||
sidebar.close();
|
||||
}
|
||||
playerBoard.close();
|
||||
ScoreboardManager.onPlayerBoardClosed(playerName);
|
||||
}
|
||||
|
||||
// Board Type Changing 'API' methods
|
||||
@ -401,40 +419,22 @@ public class ScoreboardWrapper {
|
||||
* Sets the sidebar title and re-renders. Replaces the old register/unregister objective flow.
|
||||
*/
|
||||
protected void loadObjective(String displayName) {
|
||||
if (displayName.length() > 32) {
|
||||
displayName = displayName.substring(0, 32);
|
||||
}
|
||||
|
||||
sidebar.title(toComponent(displayName));
|
||||
playerBoard.setTitle(displayName);
|
||||
render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds one sidebar row: a legacy-formatted label and the integer shown on the right.
|
||||
*/
|
||||
private record SidebarLine(String label, int value) {
|
||||
}
|
||||
|
||||
private ScoreFormat redScore(int value) {
|
||||
return ScoreFormat.fixed(Component.text(value, net.kyori.adventure.text.format.NamedTextColor.RED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an ordered list of rows to the sidebar. {@code lines.get(0)} is drawn at the top.
|
||||
* scoreboard-library indexes line 0 at the bottom, so we map top rows to the highest indices.
|
||||
* Pushes an ordered list of rows to the active backend board.
|
||||
*/
|
||||
private void drawLines(List<SidebarLine> lines) {
|
||||
// Cap to the sidebar's hard line limit.
|
||||
int count = Math.min(lines.size(), MAX_LINES);
|
||||
List<SidebarLine> toRender = lines;
|
||||
|
||||
// Reset to a clean slate; the library diffs internally so only net changes are sent.
|
||||
sidebar.clearLines();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
SidebarLine line = lines.get(i);
|
||||
// scoreboard-library draws line index 0 at the TOP, so list order maps directly.
|
||||
sidebar.line(i, toComponent(line.label()), redScore(line.value()));
|
||||
if (count != lines.size()) {
|
||||
toRender = new ArrayList<>(lines.subList(0, count));
|
||||
}
|
||||
|
||||
playerBoard.draw(toRender);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -446,7 +446,9 @@ public class ScoreboardWrapper {
|
||||
try {
|
||||
updateTask.cancel();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LogUtils.debug(mcMMO.p.getLogger(),
|
||||
"Unable to cancel sidebar update task for " + playerName + ": "
|
||||
+ e.getMessage());
|
||||
}
|
||||
|
||||
updateTask = null;
|
||||
|
||||
@ -0,0 +1,175 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import com.gmail.nossr50.events.scoreboard.McMMOScoreboardObjectiveEvent;
|
||||
import com.gmail.nossr50.events.scoreboard.ScoreboardEventReason;
|
||||
import com.gmail.nossr50.events.scoreboard.ScoreboardObjectiveEventReason;
|
||||
import com.gmail.nossr50.locale.LocaleLoader;
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import com.gmail.nossr50.util.player.UserManager;
|
||||
import com.gmail.nossr50.datatypes.player.McMMOPlayer;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.DisplaySlot;
|
||||
import org.bukkit.scoreboard.Objective;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class BukkitPlayerBoard implements PlayerBoard {
|
||||
private static final String SIDEBAR_OBJECTIVE = "mcmmo_sidebar";
|
||||
private static final String SIDE_OBJECTIVE = "mcMMO_sideObjective";
|
||||
private static final String POWER_OBJECTIVE = "mcmmo_pwrlvl";
|
||||
|
||||
private final @NotNull Player owner;
|
||||
private @NotNull Scoreboard scoreboard;
|
||||
private final Set<String> renderedEntries;
|
||||
private @Nullable Objective sidebarObjective;
|
||||
private @Nullable Objective powerObjective;
|
||||
|
||||
public BukkitPlayerBoard(final @NotNull Player owner, final @NotNull Scoreboard scoreboard) {
|
||||
this.owner = owner;
|
||||
this.scoreboard = scoreboard;
|
||||
this.renderedEntries = new HashSet<>();
|
||||
this.sidebarObjective = scoreboard.getObjective(SIDEBAR_OBJECTIVE);
|
||||
this.powerObjective = null;
|
||||
setupPowerObjectiveIfEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Scoreboard show() {
|
||||
final Scoreboard previousBoard = owner.getScoreboard();
|
||||
owner.setScoreboard(scoreboard);
|
||||
return previousBoard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(final @NotNull Player targetPlayer, final @Nullable Scoreboard targetBoard) {
|
||||
if (targetBoard != null) {
|
||||
targetPlayer.setScoreboard(targetBoard);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Bukkit.getScoreboardManager() != null) {
|
||||
targetPlayer.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShown() {
|
||||
return owner.getScoreboard() == scoreboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(final @NotNull String displayName) {
|
||||
final McMMOScoreboardObjectiveEvent unregisterEvent = callObjectiveEvent(
|
||||
ScoreboardObjectiveEventReason.UNREGISTER_THIS_OBJECTIVE);
|
||||
if (!unregisterEvent.isCancelled() && sidebarObjective != null) {
|
||||
try {
|
||||
sidebarObjective.unregister();
|
||||
} catch (IllegalStateException e) {
|
||||
mcMMO.p.getLogger().fine("Ignoring stale sidebar objective while updating bukkit scoreboard for "
|
||||
+ owner.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
final McMMOScoreboardObjectiveEvent registerEvent = callObjectiveEvent(
|
||||
ScoreboardObjectiveEventReason.REGISTER_NEW_OBJECTIVE);
|
||||
|
||||
if (!registerEvent.isCancelled()) {
|
||||
scoreboard = registerEvent.getTargetBoard();
|
||||
Objective existingObjective = scoreboard.getObjective(SIDEBAR_OBJECTIVE);
|
||||
|
||||
if (existingObjective != null) {
|
||||
try {
|
||||
existingObjective.unregister();
|
||||
} catch (IllegalStateException e) {
|
||||
mcMMO.p.getLogger().fine("Ignoring stale existing objective while replacing bukkit scoreboard for "
|
||||
+ owner.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
sidebarObjective = scoreboard.registerNewObjective(SIDEBAR_OBJECTIVE, "dummy", SIDE_OBJECTIVE);
|
||||
}
|
||||
|
||||
if (sidebarObjective == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String safeDisplayName = displayName.length() > 32
|
||||
? displayName.substring(0, 32)
|
||||
: displayName;
|
||||
sidebarObjective.setDisplayName(safeDisplayName);
|
||||
sidebarObjective.setDisplaySlot(DisplaySlot.SIDEBAR);
|
||||
resetRenderedEntries();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final @NotNull List<SidebarLine> lines) {
|
||||
if (sidebarObjective == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetRenderedEntries();
|
||||
final int count = Math.min(lines.size(), 15);
|
||||
for (int i = 0; i < count; i++) {
|
||||
final SidebarLine line = lines.get(i);
|
||||
sidebarObjective.getScore(line.label()).setScore(line.value());
|
||||
renderedEntries.add(line.label());
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePowerLevel(final @NotNull String playerName, final int powerLevel) {
|
||||
if (powerObjective != null) {
|
||||
powerObjective.getScore(playerName).setScore(powerLevel);
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull McMMOScoreboardObjectiveEvent callObjectiveEvent(
|
||||
final @NotNull ScoreboardObjectiveEventReason reason) {
|
||||
final McMMOScoreboardObjectiveEvent event = new McMMOScoreboardObjectiveEvent(
|
||||
sidebarObjective,
|
||||
reason,
|
||||
scoreboard,
|
||||
scoreboard,
|
||||
owner,
|
||||
ScoreboardEventReason.OBJECTIVE);
|
||||
owner.getServer().getPluginManager().callEvent(event);
|
||||
return event;
|
||||
}
|
||||
|
||||
private void resetRenderedEntries() {
|
||||
for (String entry : renderedEntries) {
|
||||
scoreboard.resetScores(entry);
|
||||
}
|
||||
renderedEntries.clear();
|
||||
}
|
||||
|
||||
private void setupPowerObjectiveIfEnabled() {
|
||||
if (!mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Objective existingPowerObjective = scoreboard.getObjective(POWER_OBJECTIVE);
|
||||
if (existingPowerObjective == null) {
|
||||
existingPowerObjective = scoreboard.registerNewObjective(POWER_OBJECTIVE, "dummy", "mcMMO_powerObjective");
|
||||
}
|
||||
|
||||
existingPowerObjective.setDisplayName(LocaleLoader.getString("Scoreboard.Header.PowerLevel"));
|
||||
existingPowerObjective.setDisplaySlot(DisplaySlot.BELOW_NAME);
|
||||
|
||||
for (McMMOPlayer mmoPlayer : UserManager.getPlayers()) {
|
||||
existingPowerObjective.getScore(mmoPlayer.getProfile().getPlayerName())
|
||||
.setScore(mmoPlayer.getPowerLevel());
|
||||
}
|
||||
|
||||
this.powerObjective = existingPowerObjective;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
renderedEntries.clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import com.gmail.nossr50.util.LogUtils;
|
||||
import com.gmail.nossr50.locale.LocaleLoader;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreboardObjective;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.DisplaySlot;
|
||||
import org.bukkit.scoreboard.Objective;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.bukkit.scoreboard.ScoreboardManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class BukkitScoreboardBackend implements ScoreboardBackend {
|
||||
private static final String POWER_OBJECTIVE = "mcmmo_" + java.util.UUID.randomUUID()
|
||||
.toString().replace("-", "").substring(0, 8);
|
||||
private static final String DISPLAY_NAME = "powerLevel";
|
||||
|
||||
private final Map<String, BukkitPlayerBoard> activeBoards = new ConcurrentHashMap<>();
|
||||
private @Nullable Objective powerObjective;
|
||||
|
||||
@Override
|
||||
public @NotNull ScoreboardBackendType getType() {
|
||||
return ScoreboardBackendType.BUKKIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Scoreboard createEventTargetBoard(final @NotNull Player player) {
|
||||
final ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager();
|
||||
if (scoreboardManager == null) {
|
||||
return player.getScoreboard();
|
||||
}
|
||||
return scoreboardManager.getNewScoreboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull PlayerBoard createPlayerBoard(final @NotNull Player player,
|
||||
final @NotNull Scoreboard eventTargetBoard) {
|
||||
final BukkitPlayerBoard playerBoard = new BukkitPlayerBoard(player, eventTargetBoard);
|
||||
activeBoards.put(player.getName(), playerBoard);
|
||||
return playerBoard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupPowerLevelTag(final @NotNull Player player) {
|
||||
// No action required. BukkitPlayerBoard creates its local below-name objective during setup.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePowerLevelTag(final @NotNull Player player) {
|
||||
final Objective objective = getOrCreatePowerObjective();
|
||||
|
||||
if (objective != null) {
|
||||
objective.getScoreboard().resetScores(player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPowerLevel(final @NotNull String playerName, final int powerLevel) {
|
||||
final Objective objective = getOrCreatePowerObjective();
|
||||
|
||||
if (objective != null) {
|
||||
objective.getScore(playerName).setScore(powerLevel);
|
||||
}
|
||||
|
||||
for (BukkitPlayerBoard board : activeBoards.values()) {
|
||||
board.updatePowerLevel(playerName, powerLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPowerLevelTagActive() {
|
||||
return getOrCreatePowerObjective() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerBoardClosed(final @NotNull String playerName) {
|
||||
activeBoards.remove(playerName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScoreboardObjective getPacketPowerLevelObjective() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private @Nullable Objective getOrCreatePowerObjective() {
|
||||
if (!mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled()) {
|
||||
if (powerObjective != null) {
|
||||
try {
|
||||
powerObjective.unregister();
|
||||
} catch (IllegalStateException ignored) {
|
||||
}
|
||||
}
|
||||
powerObjective = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
final ScoreboardManager scoreboardManager = Bukkit.getScoreboardManager();
|
||||
if (scoreboardManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Scoreboard mainScoreboard = scoreboardManager.getMainScoreboard();
|
||||
Objective objective = mainScoreboard.getObjective(POWER_OBJECTIVE);
|
||||
|
||||
if (objective == null) {
|
||||
objective = mainScoreboard.registerNewObjective(POWER_OBJECTIVE, "dummy", DISPLAY_NAME);
|
||||
objective.setDisplayName(LocaleLoader.getString("Scoreboard.Header.PowerLevel"));
|
||||
objective.setDisplaySlot(DisplaySlot.BELOW_NAME);
|
||||
}
|
||||
|
||||
powerObjective = objective;
|
||||
return objective;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
for (BukkitPlayerBoard board : activeBoards.values()) {
|
||||
board.close();
|
||||
}
|
||||
activeBoards.clear();
|
||||
|
||||
if (powerObjective != null) {
|
||||
try {
|
||||
powerObjective.unregister();
|
||||
} catch (IllegalStateException e) {
|
||||
LogUtils.debug(mcMMO.p.getLogger(), "Power objective was already unregistered.");
|
||||
}
|
||||
}
|
||||
powerObjective = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class NoopPlayerBoard implements PlayerBoard {
|
||||
public NoopPlayerBoard(final @NotNull Player player) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Scoreboard show() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(final @NotNull Player targetPlayer, final @Nullable Scoreboard targetBoard) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(final @NotNull String displayName) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final @NotNull List<SidebarLine> lines) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreboardObjective;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class NoopScoreboardBackend implements ScoreboardBackend {
|
||||
@Override
|
||||
public @NotNull ScoreboardBackendType getType() {
|
||||
return ScoreboardBackendType.NOOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Scoreboard createEventTargetBoard(final @NotNull Player player) {
|
||||
return player.getScoreboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull PlayerBoard createPlayerBoard(final @NotNull Player player,
|
||||
final @NotNull Scoreboard eventTargetBoard) {
|
||||
return new NoopPlayerBoard(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupPowerLevelTag(final @NotNull Player player) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePowerLevelTag(final @NotNull Player player) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPowerLevel(final @NotNull String playerName, final int powerLevel) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPowerLevelTagActive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerBoardClosed(final @NotNull String playerName) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScoreboardObjective getPacketPowerLevelObjective() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import java.util.List;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreFormat;
|
||||
import net.megavex.scoreboardlibrary.api.sidebar.Sidebar;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class PacketPlayerBoard implements PlayerBoard {
|
||||
private final @NotNull Player owner;
|
||||
private final @NotNull Sidebar sidebar;
|
||||
private final LegacyComponentSerializer serializer = LegacyComponentSerializer.legacySection();
|
||||
|
||||
public PacketPlayerBoard(final @NotNull Player owner, final @NotNull Sidebar sidebar) {
|
||||
this.owner = owner;
|
||||
this.sidebar = sidebar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Scoreboard show() {
|
||||
sidebar.addPlayer(owner);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(final @NotNull Player targetPlayer, final @Nullable Scoreboard targetBoard) {
|
||||
sidebar.removePlayer(targetPlayer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShown() {
|
||||
return sidebar.players().contains(owner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(final @NotNull String displayName) {
|
||||
final String safeDisplayName = displayName.length() > 32
|
||||
? displayName.substring(0, 32)
|
||||
: displayName;
|
||||
sidebar.title(serializer.deserialize(safeDisplayName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(final @NotNull List<SidebarLine> lines) {
|
||||
final int count = Math.min(lines.size(), Sidebar.MAX_LINES);
|
||||
sidebar.clearLines();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
final SidebarLine line = lines.get(i);
|
||||
final Component component = serializer.deserialize(line.label() == null ? "" : line.label());
|
||||
sidebar.line(i, component, ScoreFormat.fixed(Component.text(line.value(), NamedTextColor.RED)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!sidebar.closed()) {
|
||||
sidebar.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import com.gmail.nossr50.locale.LocaleLoader;
|
||||
import com.gmail.nossr50.mcMMO;
|
||||
import java.util.ArrayList;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.megavex.scoreboardlibrary.api.ScoreboardLibrary;
|
||||
import net.megavex.scoreboardlibrary.api.exception.NoPacketAdapterAvailableException;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ObjectiveDisplaySlot;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ObjectiveManager;
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreboardObjective;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class PacketScoreboardBackend implements ScoreboardBackend {
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacySection();
|
||||
private static final String POWER_OBJECTIVE = "mcmmo_power_" + java.util.UUID.randomUUID()
|
||||
.toString().replace("-", "").substring(0, 5);
|
||||
|
||||
private @Nullable ScoreboardLibrary scoreboardLibrary;
|
||||
private @Nullable ObjectiveManager powerLevelObjectiveManager;
|
||||
private @Nullable ScoreboardObjective powerLevelObjective;
|
||||
|
||||
@Override
|
||||
public @NotNull ScoreboardBackendType getType() {
|
||||
return ScoreboardBackendType.PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
try {
|
||||
scoreboardLibrary = ScoreboardLibrary.loadScoreboardLibrary(mcMMO.p);
|
||||
} catch (NoPacketAdapterAvailableException e) {
|
||||
throw new RuntimeException("No packet adapter available for scoreboard-library", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Scoreboard createEventTargetBoard(final @NotNull Player player) {
|
||||
return player.getScoreboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull PlayerBoard createPlayerBoard(final @NotNull Player player,
|
||||
final @NotNull Scoreboard eventTargetBoard) {
|
||||
if (scoreboardLibrary == null || scoreboardLibrary.closed()) {
|
||||
return new NoopPlayerBoard(player);
|
||||
}
|
||||
|
||||
return new PacketPlayerBoard(player, scoreboardLibrary.createSidebar(15));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupPowerLevelTag(final @NotNull Player player) {
|
||||
final ScoreboardObjective objective = getOrCreatePowerLevelObjective();
|
||||
|
||||
if (objective == null || powerLevelObjectiveManager == null || powerLevelObjectiveManager.closed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
powerLevelObjectiveManager.addPlayer(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePowerLevelTag(final @NotNull Player player) {
|
||||
if (powerLevelObjectiveManager == null || powerLevelObjectiveManager.closed() || !player.isOnline()) {
|
||||
return;
|
||||
}
|
||||
|
||||
powerLevelObjectiveManager.removePlayer(player);
|
||||
|
||||
if (powerLevelObjective != null) {
|
||||
powerLevelObjective.removeScore(player.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPowerLevel(final @NotNull String playerName, final int powerLevel) {
|
||||
final ScoreboardObjective objective = getOrCreatePowerLevelObjective();
|
||||
|
||||
if (objective != null) {
|
||||
objective.score(playerName, powerLevel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPowerLevelTagActive() {
|
||||
return getOrCreatePowerLevelObjective() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerBoardClosed(final @NotNull String playerName) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ScoreboardObjective getPacketPowerLevelObjective() {
|
||||
return getOrCreatePowerLevelObjective();
|
||||
}
|
||||
|
||||
private @Nullable ScoreboardObjective getOrCreatePowerLevelObjective() {
|
||||
if (!mcMMO.p.getGeneralConfig().getPowerLevelTagsEnabled()) {
|
||||
if (powerLevelObjectiveManager != null && !powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager.close();
|
||||
}
|
||||
|
||||
powerLevelObjectiveManager = null;
|
||||
powerLevelObjective = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (scoreboardLibrary == null || scoreboardLibrary.closed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (powerLevelObjectiveManager == null || powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager = scoreboardLibrary.createObjectiveManager();
|
||||
powerLevelObjective = powerLevelObjectiveManager.create(POWER_OBJECTIVE);
|
||||
powerLevelObjective.value(LEGACY.deserialize(
|
||||
LocaleLoader.getString("Scoreboard.Header.PowerLevel")));
|
||||
powerLevelObjectiveManager.display(ObjectiveDisplaySlot.belowName(), powerLevelObjective);
|
||||
powerLevelObjectiveManager.addPlayers(new ArrayList<>(mcMMO.p.getServer().getOnlinePlayers()));
|
||||
}
|
||||
|
||||
return powerLevelObjective;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
if (powerLevelObjectiveManager != null && !powerLevelObjectiveManager.closed()) {
|
||||
powerLevelObjectiveManager.close();
|
||||
}
|
||||
|
||||
powerLevelObjectiveManager = null;
|
||||
powerLevelObjective = null;
|
||||
|
||||
if (scoreboardLibrary != null && !scoreboardLibrary.closed()) {
|
||||
scoreboardLibrary.close();
|
||||
}
|
||||
|
||||
scoreboardLibrary = null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface PlayerBoard {
|
||||
@Nullable Scoreboard show();
|
||||
|
||||
void hide(@NotNull Player targetPlayer, @Nullable Scoreboard targetBoard);
|
||||
|
||||
boolean isShown();
|
||||
|
||||
void setTitle(@NotNull String displayName);
|
||||
|
||||
void draw(@NotNull List<SidebarLine> lines);
|
||||
|
||||
void close();
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import net.megavex.scoreboardlibrary.api.objective.ScoreboardObjective;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface ScoreboardBackend {
|
||||
@NotNull ScoreboardBackendType getType();
|
||||
|
||||
void init();
|
||||
|
||||
@NotNull Scoreboard createEventTargetBoard(@NotNull Player player);
|
||||
|
||||
@NotNull PlayerBoard createPlayerBoard(@NotNull Player player, @NotNull Scoreboard eventTargetBoard);
|
||||
|
||||
void setupPowerLevelTag(@NotNull Player player);
|
||||
|
||||
void removePowerLevelTag(@NotNull Player player);
|
||||
|
||||
void setPowerLevel(@NotNull String playerName, int powerLevel);
|
||||
|
||||
boolean isPowerLevelTagActive();
|
||||
|
||||
void onPlayerBoardClosed(@NotNull String playerName);
|
||||
|
||||
@Nullable ScoreboardObjective getPacketPowerLevelObjective();
|
||||
|
||||
void shutdown();
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import com.gmail.nossr50.util.platform.MinecraftGameVersion;
|
||||
import java.util.Objects;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class ScoreboardBackendSelector {
|
||||
public static final int MAX_PACKET_MAJOR = 26;
|
||||
public static final int MAX_PACKET_MINOR = 1;
|
||||
public static final int MAX_PACKET_PATCH = 2;
|
||||
public static final String MAX_PACKET_VERSION_STRING = "26.1.2";
|
||||
|
||||
private ScoreboardBackendSelector() {
|
||||
}
|
||||
|
||||
public static @NotNull ScoreboardBackendType select(final boolean isFolia,
|
||||
final @NotNull MinecraftGameVersion version) {
|
||||
final MinecraftGameVersion gameVersion = Objects.requireNonNull(version,
|
||||
"version cannot be null");
|
||||
|
||||
if (!isFolia) {
|
||||
return ScoreboardBackendType.BUKKIT;
|
||||
}
|
||||
|
||||
final boolean versionWithinPacketSupport = !gameVersion.isAtLeast(
|
||||
MAX_PACKET_MAJOR,
|
||||
MAX_PACKET_MINOR,
|
||||
MAX_PACKET_PATCH + 1);
|
||||
|
||||
return versionWithinPacketSupport
|
||||
? ScoreboardBackendType.PACKET
|
||||
: ScoreboardBackendType.NOOP;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
public enum ScoreboardBackendType {
|
||||
BUKKIT,
|
||||
PACKET,
|
||||
NOOP
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
public record SidebarLine(String label, int value) {
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scoreboard.Scoreboard;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class NoopScoreboardBackendTest {
|
||||
@Test
|
||||
void noopBackendMethodsShouldBeSafeNoOps() {
|
||||
// Given - A noop backend with a mocked player and scoreboard
|
||||
final NoopScoreboardBackend backend = new NoopScoreboardBackend();
|
||||
final Player player = mock(Player.class);
|
||||
final Scoreboard scoreboard = mock(Scoreboard.class);
|
||||
when(player.getScoreboard()).thenReturn(scoreboard);
|
||||
|
||||
// When - Running no-op operations
|
||||
backend.init();
|
||||
final PlayerBoard playerBoard = backend.createPlayerBoard(player, scoreboard);
|
||||
|
||||
// Then - No operation throws and all expected no-op values are returned
|
||||
assertThatCode(() -> playerBoard.setTitle("test")).doesNotThrowAnyException();
|
||||
assertThatCode(() -> playerBoard.draw(java.util.List.of())).doesNotThrowAnyException();
|
||||
assertThatCode(() -> playerBoard.show()).doesNotThrowAnyException();
|
||||
assertThatCode(() -> playerBoard.hide(player, scoreboard)).doesNotThrowAnyException();
|
||||
assertThatCode(playerBoard::close).doesNotThrowAnyException();
|
||||
assertThat(playerBoard.isShown()).isFalse();
|
||||
assertThat(backend.isPowerLevelTagActive()).isFalse();
|
||||
assertThat(backend.getPacketPowerLevelObjective()).isNull();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package com.gmail.nossr50.util.scoreboards.backend;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.gmail.nossr50.util.platform.MinecraftGameVersion;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ScoreboardBackendSelectorTest {
|
||||
@Test
|
||||
void selectShouldReturnBukkitWhenServerIsNotFolia() {
|
||||
// Given - A non-Folia server with any version
|
||||
final MinecraftGameVersion gameVersion = new MinecraftGameVersion(99, 9, 9);
|
||||
|
||||
// When - Selecting a backend
|
||||
final ScoreboardBackendType backendType = ScoreboardBackendSelector.select(false, gameVersion);
|
||||
|
||||
// Then - Bukkit backend is always selected
|
||||
assertThat(backendType).isEqualTo(ScoreboardBackendType.BUKKIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectShouldReturnPacketWhenFoliaAndVersionAtOrBelowSupportedCap() {
|
||||
// Given - Folia with a version at the supported cap
|
||||
final MinecraftGameVersion gameVersion = new MinecraftGameVersion(26, 1, 2);
|
||||
|
||||
// When - Selecting a backend
|
||||
final ScoreboardBackendType backendType = ScoreboardBackendSelector.select(true, gameVersion);
|
||||
|
||||
// Then - Packet backend is selected
|
||||
assertThat(backendType).isEqualTo(ScoreboardBackendType.PACKET);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectShouldReturnNoopWhenFoliaAndVersionAboveSupportedCap() {
|
||||
// Given - Folia with an unsupported newer version
|
||||
final MinecraftGameVersion gameVersion = new MinecraftGameVersion(26, 1, 3);
|
||||
|
||||
// When - Selecting a backend
|
||||
final ScoreboardBackendType backendType = ScoreboardBackendSelector.select(true, gameVersion);
|
||||
|
||||
// Then - Noop backend is selected
|
||||
assertThat(backendType).isEqualTo(ScoreboardBackendType.NOOP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectShouldNeverReturnBukkitWhenServerIsFolia() {
|
||||
// Given - Multiple Folia versions across supported and unsupported ranges
|
||||
final MinecraftGameVersion oldVersion = new MinecraftGameVersion(1, 20, 5);
|
||||
final MinecraftGameVersion supportedCap = new MinecraftGameVersion(26, 1, 2);
|
||||
final MinecraftGameVersion unsupportedVersion = new MinecraftGameVersion(27, 0, 0);
|
||||
|
||||
// When - Selecting backends
|
||||
final ScoreboardBackendType oldResult = ScoreboardBackendSelector.select(true, oldVersion);
|
||||
final ScoreboardBackendType capResult = ScoreboardBackendSelector.select(true, supportedCap);
|
||||
final ScoreboardBackendType unsupportedResult = ScoreboardBackendSelector.select(true,
|
||||
unsupportedVersion);
|
||||
|
||||
// Then - Folia never selects Bukkit
|
||||
assertThat(oldResult).isIn(ScoreboardBackendType.PACKET, ScoreboardBackendType.NOOP);
|
||||
assertThat(capResult).isIn(ScoreboardBackendType.PACKET, ScoreboardBackendType.NOOP);
|
||||
assertThat(unsupportedResult).isIn(ScoreboardBackendType.PACKET, ScoreboardBackendType.NOOP);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user