diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java
index 64584c0d9..3bb4f42b2 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java
@@ -10,11 +10,15 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.scheduler.Scheduler;
import com.velocitypowered.api.util.ProxyVersion;
+import com.velocitypowered.api.util.bossbar.BossBar;
+import com.velocitypowered.api.util.bossbar.BossBarColor;
+import com.velocitypowered.api.util.bossbar.BossBarOverlay;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
import net.kyori.text.Component;
+import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Provides an interface to a Minecraft server proxy.
@@ -173,4 +177,17 @@ public interface ProxyServer {
* @return the proxy version
*/
ProxyVersion getVersion();
+
+ /**
+ * Creates a new {@link BossBar}.
+ *
+ * @param title boss bar title
+ * @param color boss bar color
+ * @param overlay boss bar overlay
+ * @param progress boss bar progress
+ * @return a completely new and fresh boss bar
+ */
+ @NonNull
+ BossBar createBossBar(@NonNull Component title, @NonNull BossBarColor color,
+ @NonNull BossBarOverlay overlay, float progress);
}
diff --git a/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBar.java b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBar.java
new file mode 100644
index 000000000..cdad72698
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBar.java
@@ -0,0 +1,163 @@
+package com.velocitypowered.api.util.bossbar;
+
+import com.velocitypowered.api.proxy.Player;
+import java.util.Collection;
+import net.kyori.text.Component;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Represents a boss bar, which can be send to a (group of) player(s).
+ * Boss bars only work on 1.9 and above.
+ */
+public interface BossBar {
+
+ /**
+ * Adds all specified players to this boss bar.
+ *
+ * @param players players
+ * @see #addPlayer(Player)
+ */
+ void addPlayers(Iterable players);
+
+ /**
+ * Adds player to this boss bar. This adds the player to the {@link #getPlayers()} and makes him
+ * see the boss bar.
+ *
+ * @param player the player you wish to add
+ */
+ void addPlayer(@NonNull Player player);
+
+ /**
+ * Removes player from this boss bar. This removes the player from {@link #getPlayers()} and makes
+ * him not see the boss bar.
+ *
+ * @param player the player you wish to remove
+ */
+ void removePlayer(@NonNull Player player);
+
+ /**
+ * Removes all specified players from this boss bar.
+ *
+ * @param players players
+ * @see #removePlayer(Player)
+ */
+ void removePlayers(Iterable players);
+
+ /**
+ * Removes all players, that see this boss bar.
+ *
+ * @see #removePlayer(Player)
+ */
+ void removeAllPlayers();
+
+ /**
+ * Gets the title of this boss bar.
+ *
+ * @return title
+ */
+ @NonNull
+ Component getTitle();
+
+ /**
+ * Sets a new title of the boss bar.
+ *
+ * @param title new title
+ */
+ void setTitle(@NonNull Component title);
+
+ /**
+ * Gets the boss bar's progress. In Minecraft, this is called 'health' of the boss bar.
+ *
+ * @return progress
+ */
+ float getProgress();
+
+ /**
+ * Sets a new progress of the boss bar. In Minecraft, this is called 'health' of the boss bar.
+ *
+ * @param progress a float between 0 and 1, representing boss bar's progress
+ * @throws IllegalArgumentException if the new progress is not between 0 and 1
+ */
+ void setProgress(float progress);
+
+ /**
+ * Returns a copy of the {@link Collection} of all {@link Player} added to the boss bar.
+ * Can be empty.
+ *
+ * @return players
+ */
+ Collection getPlayers();
+
+ /**
+ * Gets the color of the boss bar.
+ *
+ * @return boss bar color
+ */
+ @NonNull
+ BossBarColor getColor();
+
+ /**
+ * Sets a new color of the boss bar.
+ *
+ * @param color the color you wish the boss bar be displayed with
+ */
+ void setColor(@NonNull BossBarColor color);
+
+ /**
+ * Gets the overlay of the boss bar.
+ *
+ * @return boss bar overlay
+ */
+ @NonNull
+ BossBarOverlay getOverlay();
+
+ /**
+ * Sets a new overlay of the boss bar.
+ *
+ * @param overlay the overlay you wish the boss bar be displayed with
+ */
+ void setOverlay(@NonNull BossBarOverlay overlay);
+
+ /**
+ * Returns whenever this boss bar is visible to all added {@link #getPlayers()}. By default, it
+ * returns true
.
+ *
+ * @return true
if visible, otherwise false
+ */
+ boolean isVisible();
+
+ /**
+ * Sets a new visibility to the boss bar.
+ *
+ * @param visible boss bar visibility value
+ */
+ void setVisible(boolean visible);
+
+ /**
+ * Returns a copy of of the {@link Collection} of all {@link BossBarFlag}s added to the boss bar.
+ *
+ * @return flags
+ */
+ Collection getFlags();
+
+ /**
+ * Adds new flags to the boss bar.
+ *
+ * @param flags the flags you wish to add
+ */
+ void addFlags(BossBarFlag... flags);
+
+ /**
+ * Removes flag from the boss bar.
+ *
+ * @param flag the flag you wish to remove
+ */
+ void removeFlag(BossBarFlag flag);
+
+ /**
+ * Removes flags from the boss bar.
+ *
+ * @param flags the flags you wish to remove
+ */
+ void removeFlags(BossBarFlag... flags);
+}
diff --git a/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarColor.java b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarColor.java
new file mode 100644
index 000000000..e31fdaadd
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarColor.java
@@ -0,0 +1,14 @@
+package com.velocitypowered.api.util.bossbar;
+
+/**
+ * Represents a color of a {@link BossBar}.
+ */
+public enum BossBarColor {
+ PINK,
+ BLUE,
+ RED,
+ GREEN,
+ YELLOW,
+ PURPLE,
+ WHITE;
+}
diff --git a/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarFlag.java b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarFlag.java
new file mode 100644
index 000000000..d94a2cb29
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarFlag.java
@@ -0,0 +1,10 @@
+package com.velocitypowered.api.util.bossbar;
+
+/**
+ * Represents any {@link BossBar}'s flags.
+ */
+public enum BossBarFlag {
+ DARKEN_SKY,
+ DRAGON_BAR,
+ CREATE_FOG
+}
diff --git a/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarOverlay.java b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarOverlay.java
new file mode 100644
index 000000000..892710bd5
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/util/bossbar/BossBarOverlay.java
@@ -0,0 +1,12 @@
+package com.velocitypowered.api.util.bossbar;
+
+/**
+ * Represents a overlay of a {@link BossBar}.
+ */
+public enum BossBarOverlay {
+ SOLID,
+ SEGMENTED_6,
+ SEGMENTED_10,
+ SEGMENTED_12,
+ SEGMENTED_20
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 5b3b178aa..a8ee585b5 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -17,6 +17,9 @@ import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.ProxyVersion;
+import com.velocitypowered.api.util.bossbar.BossBar;
+import com.velocitypowered.api.util.bossbar.BossBarColor;
+import com.velocitypowered.api.util.bossbar.BossBarOverlay;
import com.velocitypowered.proxy.command.GlistCommand;
import com.velocitypowered.proxy.command.ServerCommand;
import com.velocitypowered.proxy.command.ShutdownCommand;
@@ -38,6 +41,7 @@ import com.velocitypowered.proxy.server.ServerMap;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.VelocityChannelRegistrar;
+import com.velocitypowered.proxy.util.bossbar.VelocityBossBar;
import com.velocitypowered.proxy.util.ratelimit.Ratelimiter;
import com.velocitypowered.proxy.util.ratelimit.Ratelimiters;
import io.netty.bootstrap.Bootstrap;
@@ -66,6 +70,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public class VelocityServer implements ProxyServer {
@@ -133,6 +138,15 @@ public class VelocityServer implements ProxyServer {
return new ProxyVersion(implName, implVendor, implVersion);
}
+ @Override
+ public @NonNull BossBar createBossBar(
+ @NonNull Component title,
+ @NonNull BossBarColor color,
+ @NonNull BossBarOverlay overlay,
+ float progress) {
+ return new VelocityBossBar(title, color, overlay, progress);
+ }
+
@Override
public VelocityCommandManager getCommandManager() {
return commandManager;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java
new file mode 100644
index 000000000..6162fe7d3
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java
@@ -0,0 +1,276 @@
+package com.velocitypowered.proxy.util.bossbar;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.ImmutableList;
+import com.velocitypowered.api.network.ProtocolVersion;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.util.bossbar.BossBarColor;
+import com.velocitypowered.api.util.bossbar.BossBarFlag;
+import com.velocitypowered.api.util.bossbar.BossBarOverlay;
+import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
+import com.velocitypowered.proxy.protocol.MinecraftPacket;
+import com.velocitypowered.proxy.protocol.packet.BossBar;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import net.kyori.text.Component;
+import net.kyori.text.serializer.gson.GsonComponentSerializer;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.BossBar {
+
+ private final List players;
+ private final Set flags;
+ private final UUID uuid;
+ private boolean visible;
+ private Component title;
+ private float progress;
+ private BossBarColor color;
+ private BossBarOverlay overlay;
+
+ /**
+ * Creates a new boss bar.
+ * @param title the title for the bar
+ * @param color the color of the bar
+ * @param overlay the overlay to use
+ * @param progress the progress of the bar
+ */
+ public VelocityBossBar(
+ Component title, BossBarColor color, BossBarOverlay overlay, float progress) {
+ this.title = checkNotNull(title, "title");
+ this.color = checkNotNull(color, "color");
+ this.overlay = checkNotNull(overlay, "overlay");
+ this.progress = progress;
+ if (progress > 1 || progress < 0) {
+ throw new IllegalArgumentException("Progress not between 0 and 1");
+ }
+ this.uuid = UUID.randomUUID();
+ visible = true;
+ players = new ArrayList<>();
+ flags = EnumSet.noneOf(BossBarFlag.class);
+ }
+
+ @Override
+ public void addPlayers(@NonNull Iterable players) {
+ checkNotNull(players, "players");
+ for (Player player : players) {
+ addPlayer(player);
+ }
+ }
+
+ @Override
+ public void addPlayer(@NonNull Player player) {
+ checkNotNull(player, "player");
+ if (!players.contains(player)) {
+ players.add(player);
+ }
+ if (player.isActive() && visible) {
+ sendPacket(player, addPacket());
+ }
+ }
+
+ @Override
+ public void removePlayer(@NonNull Player player) {
+ checkNotNull(player, "player");
+ players.remove(player);
+ if (player.isActive()) {
+ sendPacket(player, removePacket());
+ }
+ }
+
+ @Override
+ public void removePlayers(@NonNull Iterable players) {
+ checkNotNull(players, "players");
+ for (Player player : players) {
+ removePlayer(player);
+ }
+ }
+
+ @Override
+ public void removeAllPlayers() {
+ removePlayers(ImmutableList.copyOf(players));
+ }
+
+ @Override
+ public @NonNull Component getTitle() {
+ return title;
+ }
+
+ @Override
+ public void setTitle(@NonNull Component title) {
+ this.title = checkNotNull(title, "title");
+ if (visible) {
+ BossBar bar = new BossBar();
+ bar.setUuid(uuid);
+ bar.setAction(BossBar.UPDATE_NAME);
+ bar.setName(GsonComponentSerializer.INSTANCE.serialize(title));
+ sendToAffected(bar);
+ }
+ }
+
+ @Override
+ public float getProgress() {
+ return progress;
+ }
+
+ @Override
+ public void setProgress(float progress) {
+ if (progress > 1 || progress < 0) {
+ throw new IllegalArgumentException("Progress should be between 0 and 1");
+ }
+ this.progress = progress;
+ if (visible) {
+ BossBar bar = new BossBar();
+ bar.setUuid(uuid);
+ bar.setAction(BossBar.UPDATE_PERCENT);
+ bar.setPercent(progress);
+ sendToAffected(bar);
+ }
+ }
+
+ @Override
+ public @Nullable Collection getPlayers() {
+ return ImmutableList.copyOf(players);
+ }
+
+ @Override
+ public @NonNull BossBarColor getColor() {
+ return color;
+ }
+
+ @Override
+ public void setColor(@NonNull BossBarColor color) {
+ this.color = checkNotNull(color, "color");
+ if (visible) {
+ sendDivisions(color, overlay);
+ }
+ }
+
+ @Override
+ public @NonNull BossBarOverlay getOverlay() {
+ return overlay;
+ }
+
+ @Override
+ public void setOverlay(@NonNull BossBarOverlay overlay) {
+ this.overlay = checkNotNull(overlay, "overlay");
+ if (visible) {
+ sendDivisions(color, overlay);
+ }
+ }
+
+ private void sendDivisions(BossBarColor color, BossBarOverlay overlay) {
+ BossBar bar = new BossBar();
+ bar.setUuid(uuid);
+ bar.setAction(BossBar.UPDATE_STYLE);
+ bar.setColor(color.ordinal());
+ bar.setOverlay(overlay.ordinal());
+ sendToAffected(bar);
+ }
+
+ @Override
+ public boolean isVisible() {
+ return visible;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ boolean previous = this.visible;
+ if (previous && !visible) {
+ // The bar is being hidden
+ sendToAffected(removePacket());
+ } else if (!previous && visible) {
+ // The bar is being shown
+ sendToAffected(addPacket());
+ }
+ this.visible = visible;
+ }
+
+ @Override
+ public Collection getFlags() {
+ return ImmutableList.copyOf(flags);
+ }
+
+ @Override
+ public void addFlags(BossBarFlag... flags) {
+ if (this.flags.addAll(Arrays.asList(flags)) && visible) {
+ sendToAffected(updateFlags());
+ }
+ }
+
+ @Override
+ public void removeFlag(BossBarFlag flag) {
+ checkNotNull(flag, "flag");
+ if (this.flags.remove(flag) && visible) {
+ sendToAffected(updateFlags());
+ }
+ }
+
+ @Override
+ public void removeFlags(BossBarFlag... flags) {
+ if (this.flags.removeAll(Arrays.asList(flags)) && visible) {
+ sendToAffected(updateFlags());
+ }
+ }
+
+ private short serializeFlags() {
+ short flagMask = 0x0;
+ if (flags.contains(BossBarFlag.DARKEN_SKY)) {
+ flagMask |= 0x1;
+ }
+ if (flags.contains(BossBarFlag.DRAGON_BAR)) {
+ flagMask |= 0x2;
+ }
+ if (flags.contains(BossBarFlag.CREATE_FOG)) {
+ flagMask |= 0x4;
+ }
+ return flagMask;
+ }
+
+ private BossBar addPacket() {
+ BossBar bossBar = new BossBar();
+ bossBar.setUuid(uuid);
+ bossBar.setAction(BossBar.ADD);
+ bossBar.setName(GsonComponentSerializer.INSTANCE.serialize(title));
+ bossBar.setColor(color.ordinal());
+ bossBar.setOverlay(overlay.ordinal());
+ bossBar.setPercent(progress);
+ bossBar.setFlags(serializeFlags());
+ return bossBar;
+ }
+
+ private BossBar removePacket() {
+ BossBar bossBar = new BossBar();
+ bossBar.setUuid(uuid);
+ bossBar.setAction(BossBar.REMOVE);
+ return bossBar;
+ }
+
+ private BossBar updateFlags() {
+ BossBar bossBar = new BossBar();
+ bossBar.setUuid(uuid);
+ bossBar.setAction(BossBar.UPDATE_PROPERTIES);
+ bossBar.setFlags(serializeFlags());
+ return bossBar;
+ }
+
+ private void sendToAffected(MinecraftPacket packet) {
+ for (Player player : players) {
+ if (player.isActive() && player.getProtocolVersion().getProtocol()
+ >= ProtocolVersion.MINECRAFT_1_9.getProtocol()) {
+ sendPacket(player, packet);
+ }
+ }
+ }
+
+ private void sendPacket(Player player, MinecraftPacket packet) {
+ ConnectedPlayer connected = (ConnectedPlayer) player;
+ connected.getMinecraftConnection().write(packet);
+ }
+}