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); + } +}