diff --git a/CHANGELOG/kelp-v0.1.0.md b/CHANGELOG/kelp-v0.1.0.md
new file mode 100644
index 00000000..bde9df21
--- /dev/null
+++ b/CHANGELOG/kelp-v0.1.0.md
@@ -0,0 +1,23 @@
+# v0.1.0
+> Release date: 28.01.2021
+
+**The sidebar update**:
+* Rework sidebar system:
+ * The old, annotation-based sidebar system using `@CreateSidebar` is not available anymore. Plugins that have been using this system will have to upgrade.
+ * Rework component system (stateful & stateless)
+ * Add new component `StatefulListComponent` to display dynamic lists in sidebars
+ * Developers can now choose between lazy updating (manipulating team prefixes, flicker free) or normal updating (resetting the scoreboard), which was not possible with the old system.
+ * Move version dependent scoreboard code out of the core module to the version modules (scoreboard team handling, etc.)
+ * Rework animation system by replacing it with a cluster-based system
+ * Add new events for handling sidebars:
+ * `KelpSidebarRenderEvent`
+ * `KelpSidebarUpdateEvent`
+ * `KelpSidebarRemoveEvent`
+ * Create new event base: `KelpPlayerEvent`, which should replace the `PlayerEvent` for better integration of `KelpPlayer` into event handling. So if you create anything player-related with custom events, use this class instead.
+ * Sidebar components and sidebar objects are now created using static factory methods. If you like the old approach more, you can still rely on the old factory classes such as `SidebarComponentFactory`
+ * Sidebars can now be properly removed/hidden from a player
+* Documentation improvements in `KelpPlayer` and all newly added classes.
+* `TextAnimations` can now be created using static factory methods.
+* Add `ksidebar` command in testing-module to demonstrate some sidebar components.
+* Refactor testing-module by assigning dedicated packages to each feature to test
+* Fix bug that `TimeConverter` did not convert milliseconds <-> ticks correctly
\ No newline at end of file
diff --git a/README.md b/README.md
index aa5f70ca..5cb0661d 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,8 @@ playerConnection.sendPacket(spawnPacket);
- **Particle engine:** Easily create custom and prebuilt particle effects
- **Schedulers**: Create sync and async schedulers and make use of powerful thread synchronization tools
- **Events**: Custom events for NPCs, Inventories and more as well as new listener techniques
-
+- **Prompts**: Interact with your player by prompting input from chat, anvils ans signs.
+- **and more** to discover in the [Wiki](https://github.com/PXAV/kelp/wiki)
## Support & Requirements
@@ -119,28 +120,23 @@ There are version implementations for the following version implementations avai
## Downloading
-#### Maven
+### Maven
```xml
com.github.pxav.kelpcore
- 0.0.4
+ 0.1.0
```
### Gradle
```shell script
-implementation 'com.github.pxav.kelp:core:0.0.4'
+compile group: 'com.github.pxav.kelp', name: 'core', version: '0.1.0'
```
-### Bazel
-```shell script
-maven_jar(
- name = "core",
- artifact = "com.github.pxav.kelp:core:0.0.4",
- sha1 = "4743f29c20f3b033de5fe8c1eddb374511fa31d8",
-)
-```
+### Other build tools
+The dependency can be found here including suggestions for other build tools.
+[Kelp Mavenrepository](https://mvnrepository.com/artifact/com.github.pxav.kelp/core/)
### Pre-built files
If you are a server owner who simply needs the jar files or a developer who does not use a built tool like Maven, you can simply download the pre-built jar files from the [Releaes page](https://github.com/PXAV/kelp/releases). There you can find all versions, but it's recommended to use the latest.
diff --git a/core/pom.xml b/core/pom.xml
index 5d4767cb..0d5a39d2 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -5,7 +5,7 @@
com.github.pxav.kelpparent
- 0.0.5
+ 0.1.04.0.0
diff --git a/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java b/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
index f31f2fe0..1dca462b 100644
--- a/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
+++ b/core/src/main/java/de/pxav/kelp/core/KelpPlugin.java
@@ -35,7 +35,7 @@
*
* @author pxav
*/
-@Plugin(name = "Kelp", version = "0.0.4")
+@Plugin(name = "Kelp", version = "0.1.0")
@Author("pxav")
@Description("A cross version spigot framework.")
@Singleton
@@ -100,9 +100,6 @@ public void onEnable() {
injector.getInstance(EventHandlerRegistration.class).initialize(this.getClass().getPackage().getName());
injector.getInstance(KelpEventRepository.class).detectSubscriptions(this.getClass().getPackage().getName());
- injector.getInstance(SidebarRepository.class).loadSidebars(this.getClass().getPackage().getName());
- injector.getInstance(SidebarRepository.class).schedule();
-
injector.getInstance(KelpCommandRepository.class).loadCommands(this.getClass().getPackage().getName());
injector.getInstance(KelpNpcRepository.class).startScheduler();
@@ -130,7 +127,8 @@ public void onDisable() {
injector.getInstance(KelpApplicationRepository.class).disableApplications();
injector.getInstance(KelpNpcRepository.class).stopScheduler();
- injector.getInstance(SidebarRepository.class).interruptAnimations();
+ injector.getInstance(SidebarRepository.class).stopAllClusters();
+ logger().log("[SIDEBAR] Removed all animation clusters.");
injector.getInstance(ParticleEffectRepository.class).stopAllTimers();
injector.getInstance(KelpSchedulerRepository.class).interruptAll();
diff --git a/core/src/main/java/de/pxav/kelp/core/animation/BuildingTextAnimation.java b/core/src/main/java/de/pxav/kelp/core/animation/BuildingTextAnimation.java
index ea3a2643..83981c9f 100644
--- a/core/src/main/java/de/pxav/kelp/core/animation/BuildingTextAnimation.java
+++ b/core/src/main/java/de/pxav/kelp/core/animation/BuildingTextAnimation.java
@@ -1,5 +1,6 @@
package de.pxav.kelp.core.animation;
+import de.pxav.kelp.core.KelpPlugin;
import de.pxav.kelp.core.common.StringUtils;
import java.util.ArrayList;
@@ -32,6 +33,12 @@ public BuildingTextAnimation(StringUtils stringUtils) {
this.stringUtils = stringUtils;
}
+ public static BuildingTextAnimation create() {
+ return new BuildingTextAnimation(
+ KelpPlugin.getInjector().getInstance(StringUtils.class)
+ );
+ }
+
public BuildingTextAnimation text(String text) {
this.text = text;
return this;
diff --git a/core/src/main/java/de/pxav/kelp/core/animation/CustomTextAnimation.java b/core/src/main/java/de/pxav/kelp/core/animation/CustomTextAnimation.java
index 74d48447..34f50dc7 100644
--- a/core/src/main/java/de/pxav/kelp/core/animation/CustomTextAnimation.java
+++ b/core/src/main/java/de/pxav/kelp/core/animation/CustomTextAnimation.java
@@ -23,6 +23,10 @@ public class CustomTextAnimation implements TextAnimation {
public CustomTextAnimation() {}
+ public static CustomTextAnimation create() {
+ return new CustomTextAnimation();
+ }
+
public CustomTextAnimation addStates(String... states) {
this.states.addAll(Arrays.asList(states));
return this;
diff --git a/core/src/main/java/de/pxav/kelp/core/animation/FloatingTextAnimation.java b/core/src/main/java/de/pxav/kelp/core/animation/FloatingTextAnimation.java
index 139aaa90..81504a98 100644
--- a/core/src/main/java/de/pxav/kelp/core/animation/FloatingTextAnimation.java
+++ b/core/src/main/java/de/pxav/kelp/core/animation/FloatingTextAnimation.java
@@ -16,8 +16,10 @@ public class FloatingTextAnimation implements TextAnimation {
private boolean slideIn;
private SlideDirection slideDirection;
- public FloatingTextAnimation() {
+ public FloatingTextAnimation() {}
+ public static FloatingTextAnimation create() {
+ return new FloatingTextAnimation();
}
public FloatingTextAnimation text(String text) {
@@ -74,7 +76,6 @@ private List slideAnimation(SlideDirection slideDirection) {
@Override
public List states() {
-
return Lists.newArrayList();
}
diff --git a/core/src/main/java/de/pxav/kelp/core/animation/StaticTextAnimation.java b/core/src/main/java/de/pxav/kelp/core/animation/StaticTextAnimation.java
index 9adb0ff3..34e9325b 100644
--- a/core/src/main/java/de/pxav/kelp/core/animation/StaticTextAnimation.java
+++ b/core/src/main/java/de/pxav/kelp/core/animation/StaticTextAnimation.java
@@ -1,6 +1,5 @@
package de.pxav.kelp.core.animation;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -19,6 +18,10 @@ public final class StaticTextAnimation implements TextAnimation {
StaticTextAnimation() {}
+ public static StaticTextAnimation create() {
+ return new StaticTextAnimation();
+ }
+
public StaticTextAnimation text(String text) {
this.text = text;
return this;
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerEvent.java
new file mode 100644
index 00000000..b01509da
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/KelpPlayerEvent.java
@@ -0,0 +1,35 @@
+package de.pxav.kelp.core.event.kelpevent;
+
+import de.pxav.kelp.core.player.KelpPlayer;
+import org.bukkit.event.Event;
+
+/**
+ * Represents a specific type of event. It is basically a replacement
+ * class for bukkit's normal {@link org.bukkit.event.player.PlayerEvent}, but
+ * does not hold a normal {@link org.bukkit.entity.Player} instance. Instead
+ * the {@code #getPlayer()} method returns a {@link KelpPlayer} to offer better
+ * integration of custom Kelp events into your plugins. You don't need to
+ * manually fetch the player anymore with the {@link de.pxav.kelp.core.player.KelpPlayerRepository}
+ * but can directly retrieve it from the event.
+ *
+ * @author pxav
+ */
+public abstract class KelpPlayerEvent extends Event {
+
+ protected KelpPlayer player;
+
+ public KelpPlayerEvent(KelpPlayer who) {
+ this.player = who;
+ }
+
+ /**
+ * Gets the player who has triggered the event or
+ * the player who should be handled by this event.
+ *
+ * @return The player of this event.
+ */
+ public final KelpPlayer getPlayer() {
+ return this.player;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRemoveEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRemoveEvent.java
new file mode 100644
index 00000000..cf565055
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRemoveEvent.java
@@ -0,0 +1,32 @@
+package de.pxav.kelp.core.event.kelpevent.sidebar;
+
+import de.pxav.kelp.core.event.kelpevent.KelpPlayerEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import org.bukkit.event.HandlerList;
+
+/**
+ * This event is triggered when any sidebar is removed from a player
+ * and the corresponding animation schedulers, etc. are stopped. It
+ * does only handle {@link de.pxav.kelp.core.sidebar.type.KelpSidebar<>} ano
+ * no default bukkit scoreboards.
+ *
+ * @author pxav
+ */
+public class KelpSidebarRemoveEvent extends KelpPlayerEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ public KelpSidebarRemoveEvent(KelpPlayer who) {
+ super(who);
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRenderEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRenderEvent.java
new file mode 100644
index 00000000..1fb05af7
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarRenderEvent.java
@@ -0,0 +1,43 @@
+package de.pxav.kelp.core.event.kelpevent.sidebar;
+
+import de.pxav.kelp.core.event.kelpevent.KelpPlayerEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.sidebar.type.KelpSidebar;
+import org.bukkit.event.HandlerList;
+
+/**
+ * This event is triggered when a {@link KelpSidebar} is rendered to a player,
+ * which means that the sidebar is displayed to them for the first time.
+ *
+ * @author pxav
+ */
+public class KelpSidebarRenderEvent extends KelpPlayerEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ private KelpSidebar sidebar;
+
+ public KelpSidebarRenderEvent(KelpPlayer who, KelpSidebar sidebar) {
+ super(who);
+ this.sidebar = sidebar;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+ /**
+ * Gets the instance of the sidebar that has been rendered to the player.
+ *
+ * @return The current scoreboard object.
+ */
+ public KelpSidebar getSidebar() {
+ return sidebar;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarUpdateEvent.java b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarUpdateEvent.java
new file mode 100644
index 00000000..7183f7ec
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/event/kelpevent/sidebar/KelpSidebarUpdateEvent.java
@@ -0,0 +1,58 @@
+package de.pxav.kelp.core.event.kelpevent.sidebar;
+
+import de.pxav.kelp.core.event.kelpevent.KelpPlayerEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.sidebar.type.KelpSidebar;
+import org.bukkit.event.HandlerList;
+
+/**
+ * This event is triggered when any {@link KelpSidebar<>} is updated no
+ * matter if it is a lazy or normal update. Title updates are not
+ * included.
+ *
+ * @author pxav
+ */
+public class KelpSidebarUpdateEvent extends KelpPlayerEvent {
+
+ private static final HandlerList handlers = new HandlerList();
+
+ private boolean lazyUpdate;
+ private KelpSidebar sidebar;
+
+ public KelpSidebarUpdateEvent(KelpPlayer who, KelpSidebar sidebar, boolean lazyUpdate) {
+ super(who);
+ this.sidebar = sidebar;
+ this.lazyUpdate = lazyUpdate;
+ }
+
+ public static HandlerList getHandlerList() {
+ return handlers;
+ }
+
+ /**
+ * Checks if the update was performed lazy, which means that
+ * no scores have been added/deleted from the board.
+ *
+ * @return {@code true} if the update was lazy.
+ */
+ public boolean isLazyUpdate() {
+ return lazyUpdate;
+ }
+
+ /**
+ * Gets an instance of the scoreboard that has been updated.
+ * It can be casted to the specific type such as {@link de.pxav.kelp.core.sidebar.type.AnimatedSidebar}
+ * for example if needed.
+ *
+ * @return The current sidebar object.
+ */
+ public KelpSidebar getSidebar() {
+ return sidebar;
+ }
+
+ @Override
+ public HandlerList getHandlers() {
+ return handlers;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java
index 34ee9b2c..4f6da573 100644
--- a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java
+++ b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayer.java
@@ -5,6 +5,7 @@
import de.pxav.kelp.core.entity.LivingKelpEntity;
import de.pxav.kelp.core.entity.version.EntityVersionTemplate;
import de.pxav.kelp.core.entity.version.LivingEntityVersionTemplate;
+import de.pxav.kelp.core.event.kelpevent.sidebar.KelpSidebarRemoveEvent;
import de.pxav.kelp.core.inventory.KelpInventoryRepository;
import de.pxav.kelp.core.inventory.type.KelpInventory;
import de.pxav.kelp.core.particle.type.ParticleType;
@@ -20,9 +21,13 @@
import de.pxav.kelp.core.player.prompt.sign.SignPrompt;
import de.pxav.kelp.core.player.prompt.sign.SignPromptVersionTemplate;
import de.pxav.kelp.core.sidebar.SidebarRepository;
+import de.pxav.kelp.core.sidebar.type.KelpSidebar;
import de.pxav.kelp.core.sound.KelpSound;
+import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
+import org.bukkit.scoreboard.DisplaySlot;
+import org.bukkit.scoreboard.Scoreboard;
import java.util.Collection;
import java.util.UUID;
@@ -78,9 +83,10 @@ public class KelpPlayer extends LivingKelpEntity {
private String tabListHeader;
private String tabListFooter;
+ private KelpSidebar kelpSidebar;
+
public KelpPlayer(Player bukkitPlayer,
PlayerVersionTemplate playerVersionTemplate,
- SidebarRepository sidebarRepository,
KelpInventoryRepository inventoryRepository,
KelpPlayerRepository kelpPlayerRepository,
ParticleVersionTemplate particleVersionTemplate,
@@ -101,7 +107,6 @@ public KelpPlayer(Player bukkitPlayer,
bukkitPlayer);
this.bukkitPlayer = bukkitPlayer;
this.playerVersionTemplate = playerVersionTemplate;
- this.sidebarRepository = sidebarRepository;
this.inventoryRepository = inventoryRepository;
this.particleVersionTemplate = particleVersionTemplate;
this.signPromptVersionTemplate = signPromptVersionTemplate;
@@ -122,26 +127,50 @@ public SimpleChatPrompt openSimpleChatPrompt() {
}
/**
- * Opens a kelp sidebar with the given identifier. This method
- * only applies to sidebars created using an annotation.
+ * Checks if the player has any scoreboard with content stored in any
+ * objective type ({@code SIDEBAR, PLAYER_LIST}, etc.)
*
- * @param identifier The identifier of the sidebar you want to show
- * to the player
- * @return the current instance of the player.
+ * @return {@code true} if the player has a scoreboard with an objective.
*/
- public KelpPlayer openKelpSidebar(String identifier) {
- this.sidebarRepository.openSidebar(identifier, bukkitPlayer);
- return this;
+ public boolean hasScoreboard() {
+ Scoreboard scoreboard = bukkitPlayer.getScoreboard();
+ return scoreboard.getObjective(DisplaySlot.SIDEBAR) != null
+ || scoreboard.getObjective(DisplaySlot.BELOW_NAME) != null
+ || scoreboard.getObjective(DisplaySlot.PLAYER_LIST) != null;
}
/**
- * Makes the current sidebar of the player disappear.
+ * Removes the {@link de.pxav.kelp.core.sidebar.type.KelpSidebar} from the player.
+ * This hides the sidebar from the screen, but also stops all schedulers connected
+ * to it (such as title animation). This method does not effect other parts
+ * of the scoreboard such as the tab list.
+ */
+ public void removeSidebar() {
+ setSidebarInternally(null);
+ Bukkit.getPluginManager().callEvent(new KelpSidebarRemoveEvent(this));
+ playerVersionTemplate.removeSidebar(bukkitPlayer);
+ }
+
+ /**
+ * Caches the sidebar object of the player locally. This does not
+ * render nor update the given sidebar. It simply changes the internal
+ * sidebar object which can then be retrieved to update it for
+ * example using {@link #getCurrentSidebar()}.
*
- * @return the current instance of the player.
+ * @param sidebar The current sidebar of the player.
*/
- public KelpPlayer removeKelpSidebar() {
- this.sidebarRepository.removeSidebar(bukkitPlayer);
- return this;
+ public void setSidebarInternally(KelpSidebar sidebar) {
+ this.kelpSidebar = sidebar;
+ }
+
+ /**
+ * Gets the sidebar the player is currently seeing.
+ * Will return {@code null} of the player has no sidebar.
+ *
+ * @return The current sidebar of the player.
+ */
+ public KelpSidebar getCurrentSidebar() {
+ return kelpSidebar;
}
/**
@@ -342,6 +371,13 @@ public KelpPlayer chat(String message) {
return this;
}
+ public KelpPlayer clearChat() {
+ for (int i = 0; i < 103; i++) {
+ sendMessage(" ");
+ }
+ return this;
+ }
+
public boolean mayFly() {
return playerVersionTemplate.getAllowFlight(bukkitPlayer);
}
diff --git a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java
index d93c061d..8ce8f7a4 100644
--- a/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java
+++ b/core/src/main/java/de/pxav/kelp/core/player/KelpPlayerRepository.java
@@ -56,9 +56,18 @@ public class KelpPlayerRepository {
private ChatPromptVersionTemplate chatPromptVersionTemplate;
@Inject
- public KelpPlayerRepository(PlayerVersionTemplate playerVersionTemplate, SidebarRepository sidebarRepository, KelpInventoryRepository inventoryRepository, KelpLogger logger, EntityVersionTemplate entityVersionTemplate, LivingEntityVersionTemplate livingEntityVersionTemplate, ParticleVersionTemplate particleVersionTemplate, SignPromptVersionTemplate signPromptVersionTemplate, AnvilPromptVersionTemplate anvilPromptVersionTemplate, ChatPromptVersionTemplate chatPromptVersionTemplate) {
+ public KelpPlayerRepository(PlayerVersionTemplate playerVersionTemplate,
+ //SidebarRepository sidebarRepository,
+ KelpInventoryRepository inventoryRepository,
+ KelpLogger logger,
+ EntityVersionTemplate entityVersionTemplate,
+ LivingEntityVersionTemplate livingEntityVersionTemplate,
+ ParticleVersionTemplate particleVersionTemplate,
+ SignPromptVersionTemplate signPromptVersionTemplate,
+ AnvilPromptVersionTemplate anvilPromptVersionTemplate,
+ ChatPromptVersionTemplate chatPromptVersionTemplate) {
this.playerVersionTemplate = playerVersionTemplate;
- this.sidebarRepository = sidebarRepository;
+ //this.sidebarRepository = sidebarRepository;
this.inventoryRepository = inventoryRepository;
this.logger = logger;
this.entityVersionTemplate = entityVersionTemplate;
@@ -217,7 +226,7 @@ public void removeKelpPlayer(UUID uuid) {
private KelpPlayer newKelpPlayerFrom(Player bukkitPlayer) {
return new KelpPlayer(bukkitPlayer,
playerVersionTemplate,
- sidebarRepository,
+ //sidebarRepository,
inventoryRepository,
this,
particleVersionTemplate,
diff --git a/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java b/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java
index fe98e01b..ff033cd2 100644
--- a/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java
+++ b/core/src/main/java/de/pxav/kelp/core/player/PlayerVersionTemplate.java
@@ -935,4 +935,12 @@ public abstract class PlayerVersionTemplate {
*/
public abstract void sendInteractiveMessage(Player player, InteractiveMessage interactiveMessage);
+ /**
+ * If the player currently sees a sidebar, it will be hidden for the given
+ * player. This mostly happens by replacing it with a new, empty scoreboard.
+ *
+ * @param player The player whose sidebar you want to hide/remove.
+ */
+ public abstract void removeSidebar(Player player);
+
}
diff --git a/core/src/main/java/de/pxav/kelp/core/scheduler/TimeConverter.java b/core/src/main/java/de/pxav/kelp/core/scheduler/TimeConverter.java
index ad42cdfe..0dd872c2 100644
--- a/core/src/main/java/de/pxav/kelp/core/scheduler/TimeConverter.java
+++ b/core/src/main/java/de/pxav/kelp/core/scheduler/TimeConverter.java
@@ -49,7 +49,7 @@ public static int secondsToTicks(int seconds) {
public static int getTicks(int value, TimeUnit timeUnit) {
switch (timeUnit) {
case MILLISECONDS:
- return secondsToTicks(value * 1000);
+ return value / 50;
case SECONDS:
return secondsToTicks(value);
case MINUTES:
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/CreateSidebar.java b/core/src/main/java/de/pxav/kelp/core/sidebar/CreateSidebar.java
deleted file mode 100644
index a9a4f9cb..00000000
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/CreateSidebar.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package de.pxav.kelp.core.sidebar;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * This annotation is used to mark methods
- * which provide a sidebar that can be used
- * and opened.
- *
- * @author pxav
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface CreateSidebar {
-
- /**
- * Each sidebar must have a unique identifier.
- * This identifier is used to for example
- * open the sidebar, etc.
- *
- * @return The unique identifier string.
- */
- String identifier();
-
- int switchInterval() default 100;
-
- /**
- * If your sidebar title is animated
- * you should specify an interval in which
- * the sidebar title should be updated.
- * The default value is 1000 milliseconds.
- *
- * @return The interval in milliseconds.
- */
- int titleAnimationInterval() default 1000;
-
- /**
- * Should your sidebar be handled asynchronously?
- * This only applies for general updates, the title updates
- * (if your sidebar is animated) are always asynchronous.
- *
- * @return {@code true} if sidebar updates should happen async.
- */
- boolean async() default true;
-
- /**
- * This attribute describes whether the scoreboard
- * should be displayed automatically when a player
- * joins the server.
- * @return {@code true} if it should be the default scoreboard.
- */
- boolean setOnJoin() default false;
-
-}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarRepository.java b/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarRepository.java
old mode 100644
new mode 100755
index b33b1bda..9f46547a
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarRepository.java
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarRepository.java
@@ -1,342 +1,170 @@
package de.pxav.kelp.core.sidebar;
-import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
+import com.google.common.collect.Sets;
+import de.pxav.kelp.core.animation.TextAnimation;
+import de.pxav.kelp.core.event.kelpevent.sidebar.KelpSidebarRemoveEvent;
+import de.pxav.kelp.core.player.KelpPlayer;
+import de.pxav.kelp.core.player.KelpPlayerRepository;
+import de.pxav.kelp.core.scheduler.KelpSchedulerRepository;
+import de.pxav.kelp.core.scheduler.type.SchedulerFactory;
import de.pxav.kelp.core.sidebar.type.AnimatedSidebar;
-import de.pxav.kelp.core.logger.KelpLogger;
-import de.pxav.kelp.core.logger.LogLevel;
-import de.pxav.kelp.core.reflect.MethodCriterion;
-import de.pxav.kelp.core.reflect.MethodFinder;
-import de.pxav.kelp.core.sidebar.type.KelpSidebar;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
+import de.pxav.kelp.core.sidebar.version.SidebarUpdaterVersionTemplate;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.player.PlayerQuitEvent;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
/**
- * This repository class is used to manage your sidebars.
- * You can open, close and update sidebars using the
- * unique identifier which is passed in the {@code CreateSidebar}
- * annotation.
+ * A class description goes here.
*
* @author pxav
*/
@Singleton
public class SidebarRepository {
- // saves the methods which build the sidebar. Identifier -> Method
- private final Map methods = Maps.newHashMap();
+ private ConcurrentHashMap animationStates;
+ private ConcurrentHashMap> clusters;
+ private ConcurrentHashMap clusterTasks;
+ private ConcurrentHashMap playerTasks;
+ private ConcurrentHashMap animations;
- // should the sidebar be updated asynchronously? Identifier -> async?
- private final Map asyncMode = Maps.newHashMap();
-
- // The sidebar which is currently opened by a player. Player -> Sidebar identifier
- private final Map playerSidebars = Maps.newHashMap();
-
- // The schedulers for the title animation of animated sidebars. Identifier -> Scheduler
- private final Map titleScheduler = Maps.newHashMap();
-
- // The current state of animation for each player. Player -> State
- private final Map animationStates = Maps.newHashMap();
-
- // When should the next animation state be called? Identifier -> Time in millis
- private final Map titleAnimationInterval = Maps.newHashMap();
-
- // the identifier of the scoreboard which should be set on join.
- private String defaultScoreboard = "NONE";
-
- private MethodFinder methodFinder;
- private KelpLogger kelpLogger;
- private Injector injector;
- private ExecutorService executorService;
+ private KelpSchedulerRepository schedulerRepository;
+ private SchedulerFactory schedulerFactory;
+ private SidebarUpdaterVersionTemplate updaterVersionTemplate;
+ private KelpPlayerRepository playerRepository;
@Inject
- public SidebarRepository(MethodFinder methodFinder,
- KelpLogger kelpLogger,
- Injector injector,
- ExecutorService executorService) {
- this.methodFinder = methodFinder;
- this.kelpLogger = kelpLogger;
- this.injector = injector;
- this.executorService = executorService;
+ public SidebarRepository(KelpSchedulerRepository schedulerRepository,
+ SchedulerFactory schedulerFactory,
+ SidebarUpdaterVersionTemplate updaterVersionTemplate,
+ KelpPlayerRepository playerRepository) {
+ this.animationStates = new ConcurrentHashMap<>();
+ this.clusters = new ConcurrentHashMap<>();
+ this.clusterTasks = new ConcurrentHashMap<>();
+ this.playerTasks = new ConcurrentHashMap<>();
+ this.animations = new ConcurrentHashMap<>();
+ this.schedulerFactory = schedulerFactory;
+ this.schedulerRepository = schedulerRepository;
+ this.updaterVersionTemplate = updaterVersionTemplate;
+ this.playerRepository = playerRepository;
}
- /**
- * Searches for methods annotated with a {@code CreateSidebar} annotation
- * and saves as a sidebar.
- *
- * @param packageNames The packages in which you want to search.
- * @see CreateSidebar
- */
- public void loadSidebars(String... packageNames) {
- kelpLogger.log("[SIDEBAR] Loading sidebars in " + Arrays.toString(packageNames));
- this.methodFinder.filter(packageNames, MethodCriterion.annotatedWith(CreateSidebar.class))
- .forEach(method -> {
- CreateSidebar annotation = method.getAnnotation(CreateSidebar.class);
- String identifier = annotation.identifier();
- if (identifier.equalsIgnoreCase("NONE")) {
- kelpLogger.log(LogLevel.ERROR, "[SIDEBAR] Sidebar identifier 'NONE' is not allowed, " +
- "because it's reserved for the system. Please choose another name.");
- return;
- }
-
- if (!identifierAvailable(identifier)) {
- kelpLogger.log(LogLevel.ERROR, "[SIDEBAR] Sidebar identifier " + identifier
- + " is already in use, but identifiers must be unique!" +
- " Please change the identifier and reload the system.");
- return;
- }
-
- methods.put(identifier, method);
- asyncMode.put(identifier, annotation.async());
- if (annotation.titleAnimationInterval() <= 0) {
- kelpLogger.log(LogLevel.ERROR, "[SIDEBAR] Animation interval of sidebar '" + identifier
- + "' is smaller than or equal to 0. Please change the delay to at least 1.");
- return;
- }
- titleAnimationInterval.put(identifier, annotation.titleAnimationInterval());
-
- if (defaultScoreboard.equalsIgnoreCase("NONE")) {
- defaultScoreboard = annotation.identifier();
- }
-
- kelpLogger.log("[SIDEBAR] Sidebar " + identifier + " successfully loaded!");
- });
- kelpLogger.log("[SIDEBAR] Loading process complete. Loaded " + methods.size() + " sidebars in total so far.");
- }
+ public void addAnimatedSidebar(AnimatedSidebar sidebar, KelpPlayer player) {
- /**
- * Starts the schedulers for the scoreboard animations.
- *
- * Each sidebar gets an own scheduler which uses the interval
- * which is passed in the {@code CreateSidebar} annotation,
- * while animation states are linked to each player individually,
- * because players can have different animations in the same sidebar
- * when for example their name is displayed in the title:
- * §apxav -> 4 states
- * §aOpi_CAN -> 7 states
- */
- public void schedule() {
- kelpLogger.log("[SIDEBAR] Enabling animation schedulers.");
- for (Map.Entry entry : Maps.newHashMap(this.titleAnimationInterval).entrySet()) {
- String identifier = entry.getKey();
+ animationStates.put(player.getUUID(), 0);
+ animations.put(player.getUUID(), sidebar.getTitle());
- // check if the current sidebar is really an animated one.
- // if not, remove it from the collection and continue with the next one.
- if (!this.isAnimated(identifier)) {
- this.titleAnimationInterval.remove(identifier);
- continue;
+ // add player to cluster or create a new cluster if needed
+ if (sidebar.getClusterId() != null) {
+ if (!clusters.containsKey(sidebar.getClusterId())) {
+ this.addCluster(sidebar.getClusterId(), sidebar.getTitleAnimationInterval());
}
-
- // create a new thread containing a new scheduler
- ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
-
-
- // schedule using the time passed in the annotation.
- scheduledExecutorService.scheduleAtFixedRate(() -> {
- try {
-
- // iterate all players on the server.
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (!animationStates.containsKey(player)) {
- continue;
- }
-
- int state = this.animationStates.get(player);
-
- if (this.playerSidebars.containsKey(player)
- && !this.playerSidebars.get(player).equalsIgnoreCase(identifier))
- continue;
-
- // load the sidebar for the player
- AnimatedSidebar sidebar = (AnimatedSidebar) getSidebar(identifier, player);
- Preconditions.checkNotNull(sidebar);
-
- // update the state. If the state index it out of bounds, reset it to 0.
- animationStates.put(player, animationStates.get(player) + 1);
- if (state >= sidebar.maxStates() - 1) {
- this.animationStates.put(player, 0);
- }
-
- // finally update the title.
- sidebar.updateTitleOnly(player, state);
-
- }
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }, 200L, titleAnimationInterval.get(identifier), TimeUnit.MILLISECONDS);
-
- // save the scheduler object in the map so that it can be canceled later.
- this.titleScheduler.put(identifier, scheduledExecutorService);
- }
- }
-
- /**
- * Iterates through all sidebars and cancels the animation
- * scheduler if existing.
- */
- public void interruptAnimations() {
- for (Map.Entry entry : this.titleScheduler.entrySet()) {
- entry.getValue().shutdown();
+ this.addPlayerToCluster(sidebar.getClusterId(), player);
+ } else {
+ // if the sidebar does not have clusters, an individual scheduler
+ // has to be set up
+ UUID task = schedulerFactory.newRepeatingScheduler()
+ .async()
+ .every(sidebar.getTitleAnimationInterval())
+ .milliseconds()
+ .run(taskId -> {
+ String updateTo = incrementAnimationState(player.getUUID());
+ updaterVersionTemplate.updateTitleOnly(updateTo, player);
+ });
+ playerTasks.put(player.getUUID(), task);
}
- kelpLogger.log("[SIDEBAR] Interrupted all animation schedulers.");
- }
-
- /**
- * Open the given sidebar for the given player.
- *
- * @param identifier The identifier of the desired sidebar.
- * @param player The player who should see the sidebar.
- */
- public void openSidebar(String identifier, Player player) {
- checkAvailability(identifier);
- KelpSidebar sidebar = getSidebar(identifier, player);
- Preconditions.checkNotNull(sidebar);
-
- playerSidebars.put(player, identifier);
- animationStates.put(player, 0);
- sidebar.renderAndOpenSidebar(player);
}
- /**
- * Updates the sidebar of a player.
- * The type ((a-)sync) depends on the value
- * passed in the {@code CreateSidebar} annotation
- * and will be selected automatically.
- *
- * @param player The player whose sidebar you want to update.
- */
- public void updateSidebar(Player player) {
- String identifier = playerSidebars.get(player);
- if (isAsync(identifier)) {
- this.updateSidebarAsynchronously(player);
+ public void removeAnimatedSidebar(KelpPlayer player) {
+ if (playerTasks.containsKey(player.getUUID())) {
+ UUID task = playerTasks.get(player.getUUID());
+ schedulerRepository.interruptScheduler(task);
+ this.playerTasks.remove(player.getUUID());
} else {
- this.updateSidebarSynchronously(player);
+ Maps.newHashMap(this.clusters).forEach((clusterId, playerSet)
+ -> playerSet.stream()
+ .filter(uuid -> player.getUUID() == uuid)
+ .findFirst()
+ .ifPresent(uuid -> {
+ playerSet.remove(uuid);
+ if (playerSet.isEmpty()) {
+ stopCluster(clusterId);
+ return;
+ }
+ clusters.put(clusterId, playerSet);
+ }));
}
+ this.animationStates.remove(player.getUUID());
+ this.animations.remove(player.getUUID());
}
- /**
- * Updates the sidebar of the given player inside the
- * main thread of the server.
- *
- * @param player The player whose sidebar you want to update.
- */
- public void updateSidebarSynchronously(Player player) {
- String identifier = playerSidebars.get(player);
- checkAvailability(identifier);
-
- KelpSidebar kelpSidebar = getSidebar(identifier, player);
- Preconditions.checkNotNull(kelpSidebar);
- kelpSidebar.update(player);
+ public void stopAllClusters() {
+ Maps.newHashMap(this.clusters).forEach((clusterId, playerSet)
+ -> stopCluster(clusterId));
}
- /**
- * Updates the sidebar of the given player in a
- * separate thread.
- *
- * @param player The player whose sidebar you want to update.
- */
- public void updateSidebarAsynchronously(Player player) {
- this.executorService.execute(() -> {
- String identifier = playerSidebars.get(player);
- checkAvailability(identifier);
-
- KelpSidebar kelpSidebar = getSidebar(identifier, player);
- Preconditions.checkNotNull(kelpSidebar);
- kelpSidebar.update(player);
- });
+ @EventHandler
+ public void handleClusterRemove(PlayerQuitEvent event) {
+ KelpPlayer player = playerRepository.getKelpPlayer(event.getPlayer());
+ if (animationStates.containsKey(player.getUUID())) {
+ removeAnimatedSidebar(player);
+ }
}
- /**
- * Removes a player from all lists in the cache and clears
- * its sidebar.
- *
- * @param player The player whose sidebar should be removed.
- */
- public void removeSidebar(Player player) {
- this.playerSidebars.remove(player);
- this.animationStates.remove(player);
+ @EventHandler
+ public void handleSidebarRemove(KelpSidebarRemoveEvent event) {
+ // stops all schedulers and removes the player from the list when
+ // their sidebar is removed.
+ this.removeAnimatedSidebar(event.getPlayer());
}
- /**
- * Invokes the creation method of the sidebar with the
- * given identifier and returns the result.
- *
- * @param identifier The identifier of the sidebar you want to get.
- * @param player Each sidebar method needs a player as parameter
- * to also load player-specific data as well.
- * So you need to give the player who should be passed
- * as parameter.
- * @return The final sidebar object.
- */
- private KelpSidebar getSidebar(String identifier, Player player) {
- if (this.identifierAvailable(identifier)) return null;
-
- try {
- Method method = this.methods.get(identifier);
- return (KelpSidebar) method.invoke(injector.getInstance(method.getDeclaringClass()), player);
- } catch (IllegalAccessException | InvocationTargetException ignore) {}
- return null;
+ private void addPlayerToCluster(String clusterId, KelpPlayer player) {
+ Set players = clusters.get(clusterId);
+ players.add(player.getUUID());
+ clusters.put(clusterId, players);
}
- /**
- * Checks whether the requested sidebar is animated.
- * This means if the sidebar is of type {@code AnimatedSidebar}
- * ano not just {@code SimpleSidebar} for example.
- *
- * @param identifier The identifier of the sidebar you want to check.
- * @return {@code true} if the sidebar is of type {@code AnimatedSidebar}.
- * @see AnimatedSidebar
- */
- private boolean isAnimated(String identifier) {
- checkAvailability(identifier);
- Method method = this.methods.get(identifier);
- return method.getReturnType() == AnimatedSidebar.class;
+ private void addCluster(String clusterId, int interval) {
+ clusters.put(clusterId, Sets.newHashSet());
+ UUID task = schedulerFactory.newRepeatingScheduler()
+ .async()
+ .every(interval)
+ .milliseconds()
+ .run(taskId -> clusters.get(clusterId).forEach(current -> {
+ String updateTo = incrementAnimationState(current);
+ updaterVersionTemplate.updateTitleOnly(updateTo, playerRepository.getKelpPlayer(current));
+ }));
+ clusterTasks.put(clusterId, task);
}
- /**
- * Checks if the requested identifier exits in the cache.
- * If this is false an error message is sent to the log.
- *
- * @param identifier The identifier you want to check.
- */
- private void checkAvailability(String identifier) {
- if (identifierAvailable(identifier)) {
- kelpLogger.log(LogLevel.ERROR, "Cannot access sidebar: " +
- " Sidebar with identifier " + identifier + " does not exist.");
+ private String incrementAnimationState(UUID uuid) {
+ List states = animations.get(uuid).states();
+ int current = animationStates.get(uuid);
+ int max = states.size();
+
+ current += 1;
+ if (current == max) {
+ current = 0;
}
- }
- /**
- * @param identifier The identifier you want to check.
- * @return {@code true} if the identifier is not in use already.
- */
- private boolean identifierAvailable(String identifier) {
- return !methods.containsKey(identifier);
+ animationStates.put(uuid, current);
+ return states.get(current);
}
- /**
- * @param identifier The identifier of the sidebar you want to check.
- * @return {@code true} if the sidebar should be handled asynchronously.
- */
- private boolean isAsync(String identifier) {
- return this.asyncMode.get(identifier);
+ private void stopCluster(String clusterId) {
+ if (this.clusterTasks.get(clusterId) == null) {
+ return;
+ }
+ schedulerRepository.interruptScheduler(clusterTasks.get(clusterId));
+ clusterTasks.remove(clusterId);
+ clusters.remove(clusterId);
}
- public String getDefaultScoreboard() {
- return defaultScoreboard;
- }
}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarStateListener.java b/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarStateListener.java
deleted file mode 100644
index 55c9212e..00000000
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarStateListener.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package de.pxav.kelp.core.sidebar;
-
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.player.PlayerJoinEvent;
-import org.bukkit.event.player.PlayerQuitEvent;
-
-/**
- * This class handles the states of sidebars.
- * It gives default sidebars on every player join
- * and removes any type of sidebar on every player
- * quit.
- *
- * This avoids useless calculation for the server.
- *
- * @author pxav
- */
-@Singleton
-public class SidebarStateListener {
-
- private SidebarRepository sidebarRepository;
-
- @Inject
- public SidebarStateListener(SidebarRepository sidebarRepository) {
- this.sidebarRepository = sidebarRepository;
- }
-
- /**
- * This event is triggered when a player joins the server.
- * In this case it gives the player the default sidebar,
- * if there has been defined one (with 'setOnJoin' to true
- * in the {@code @CreateSidebar} annotation).
- *
- * @param event Instance of the current event.
- */
- @EventHandler
- public void onPlayerJoin(PlayerJoinEvent event) {
- Player player = event.getPlayer();
-
- // if a scoreboard has 'setOnJoin' set to true, it will be shown to the player
- if (!sidebarRepository.getDefaultScoreboard().equalsIgnoreCase("NONE")) {
- sidebarRepository.openSidebar(sidebarRepository.getDefaultScoreboard(), player);
- }
-
- }
-
- /**
- * This event is triggered when a player quits the server.
- * In this case it removes the sidebar for every player so
- * that the server does not do useless updates on their
- * sidebar.
- *
- * @param event Instance of the current event.
- */
- @EventHandler
- public void onPlayerQuit(PlayerQuitEvent event) {
- Player player = event.getPlayer();
- sidebarRepository.removeSidebar(player);
- }
-
-}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarUtils.java b/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarUtils.java
old mode 100644
new mode 100755
index cd03c5a1..4257eda8
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarUtils.java
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/SidebarUtils.java
@@ -22,12 +22,10 @@
@Singleton
public class SidebarUtils {
- private KelpLogger logger;
private StringUtils stringUtils;
@Inject
- public SidebarUtils(KelpLogger logger, StringUtils stringUtils) {
- this.logger = logger;
+ public SidebarUtils(StringUtils stringUtils) {
this.stringUtils = stringUtils;
}
@@ -79,7 +77,6 @@ public void setTeamData(String text, Team team) {
* @return The final generation result.
*/
public String randomEmptyEntry(Scoreboard scoreboard) {
- int index = ThreadLocalRandom.current().nextInt(1);
int colorAmount = ThreadLocalRandom.current().nextInt(3);
StringBuilder stringBuilder = new StringBuilder();
Collection forbidden = usedEntries(scoreboard);
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/EmptyLineComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/EmptyLineComponent.java
old mode 100644
new mode 100755
index 41a34e09..ab6b3df1
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/component/EmptyLineComponent.java
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/EmptyLineComponent.java
@@ -1,15 +1,19 @@
package de.pxav.kelp.core.sidebar.component;
+import com.google.common.collect.Maps;
+import de.pxav.kelp.core.KelpPlugin;
import de.pxav.kelp.core.sidebar.SidebarUtils;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
+import java.util.Map;
+
/**
* This sidebar component can be used to generate empty lines.
- * You could also write them manually as a {@code SimpleTextComponent},
- * but this way makes it more maintainable, because you don't
+ * You could also write them manually as a {@code StatelessTextComponent}
+ * for example, this way however makes it more maintainable, because you don't
* have to choose color codes manually:
*
* To display an empty line in a scoreboard you have to use invisible
@@ -22,34 +26,45 @@
*
* @author pxav
*/
-public class EmptyLineComponent implements SimpleSidebarComponent {
+public class EmptyLineComponent extends SidebarComponent {
+ // the line to place the component in the sidebar
private int line;
- private SidebarUtils sidebarUtils;
-
- public EmptyLineComponent(SidebarUtils sidebarUtils) {
- this.sidebarUtils = sidebarUtils;
+ public static EmptyLineComponent create() {
+ return new EmptyLineComponent();
}
- public EmptyLineComponent score(int line) {
+ /**
+ * Sets the line number of the component to be placed in the sidebar.
+ * Please note that this line id represents an absolute position and
+ * should therefore be unique. No components should have the same line
+ * number.
+ *
+ * @param line The line of the sidebar, where the component should
+ * be visible.
+ * @return The current component instance for fluent builder design.
+ */
+ public EmptyLineComponent line(int line) {
this.line = line;
return this;
}
+ /**
+ * Renders all the information provided in the component
+ * to a map containing the final information to be rendered
+ * to the sidebar (the lines where the text should be placed and
+ * the text to write there).
+ *
+ * @return A map, where the key is the absolute line where
+ * the component should be placed in the sidebar and
+ * the value is the actual text for that line.
+ */
@Override
- public void render(Scoreboard parent) {
- String entry = sidebarUtils.randomEmptyEntry(parent);
- Objective objective = parent.getObjective(DisplaySlot.SIDEBAR);
-
- objective.getScore(entry).setScore(line);
-
- Team team = parent.registerNewTeam("entry_" + line);
- team.addEntry(entry);
- sidebarUtils.setTeamData(sidebarUtils.randomEmptyEntry(parent), team);
+ public Map render() {
+ Map output = Maps.newHashMap();
+ output.put(line, " ");
+ return output;
}
- @Override
- public void update(Scoreboard parent) {}
-
}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/LineSeparatorComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/LineSeparatorComponent.java
old mode 100644
new mode 100755
index 45084bc8..ee285813
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/component/LineSeparatorComponent.java
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/LineSeparatorComponent.java
@@ -1,5 +1,6 @@
package de.pxav.kelp.core.sidebar.component;
+import com.google.common.collect.Maps;
import de.pxav.kelp.core.sidebar.SidebarUtils;
import org.bukkit.ChatColor;
import org.bukkit.scoreboard.DisplaySlot;
@@ -7,6 +8,8 @@
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
+import java.util.Map;
+
/**
* This scoreboard component is used to easily create line separators.
* With these you can simply create separators between paragraphs of your
@@ -22,89 +25,117 @@
*
* @author pxav
*/
-public class LineSeparatorComponent implements SimpleSidebarComponent {
+public class LineSeparatorComponent extends SidebarComponent {
private int line;
private int length;
- private char symbol;
+ private String symbol;
private ChatColor[] colors;
- private SidebarUtils sidebarUtils;
-
- LineSeparatorComponent(SidebarUtils sidebarUtils) {
- this.sidebarUtils = sidebarUtils;
-
+ public LineSeparatorComponent() {
this.length = SeparatorLength.FULL;
- this.symbol = '-';
+ this.symbol = "-";
this.colors = new ChatColor[] {ChatColor.DARK_GRAY, ChatColor.STRIKETHROUGH};
}
- public LineSeparatorComponent score(int line) {
+ public static LineSeparatorComponent create() {
+ return new LineSeparatorComponent();
+ }
+
+ /**
+ * Sets the line number of the component to be placed in the sidebar.
+ * Please note that this line id represents an absolute position and
+ * should therefore be unique. No components should have the same line
+ * number.
+ *
+ * @param line The line of the sidebar, where the component should
+ * be visible.
+ * @return The current component instance for fluent builder design.
+ */
+ public LineSeparatorComponent line(int line) {
this.line = line;
return this;
}
+ /**
+ * Sets the symbol to be repeated n times by the component, while
+ * {@code n} is equal to the length set by {@link #length(int)}
+ *
+ * @param symbol The symbol you want to set.
+ * @return Instance of the current component for more fluent builder design.
+ */
public LineSeparatorComponent symbol(char symbol) {
+ this.symbol = String.valueOf(symbol);
+ return this;
+ }
+
+ /**
+ * Sets the symbol to be repeated n times by the component, while
+ * {@code n} is equal to the length set by {@link #length(int)}
+ *
+ * @param symbol The symbol you want to set.
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public LineSeparatorComponent symbol(String symbol) {
this.symbol = symbol;
return this;
}
+ /**
+ * The colors in which the line separator symbols should be displayed.
+ * You can also apply style codes such as {@link ChatColor#STRIKETHROUGH}
+ * here.
+ *
+ * @param colors The colors to be displayed.
+ * @return Instance of the current component for more fluent builder design.
+ */
public LineSeparatorComponent color(ChatColor... colors) {
this.colors = colors;
return this;
}
+ /**
+ * Sets the amount of times the given symbol should be repeated.
+ *
+ * @param length The length of your line separator.
+ * @return Instance of the current component for more fluent builder design.
+ */
public LineSeparatorComponent length(int length) {
this.length = length;
return this;
}
+ /**
+ * Renders all the information provided in the component
+ * to a map containing the final information to be rendered
+ * to the sidebar (the lines where the text should be placed and
+ * the text to write there).
+ *
+ * @return A map, where the key is the absolute line where
+ * the component should be placed in the sidebar and
+ * the value is the actual text for that line.
+ */
@Override
- public void render(Scoreboard parent) {
- String entry = sidebarUtils.randomEmptyEntry(parent);
- Objective objective = parent.getObjective(DisplaySlot.SIDEBAR);
-
- objective.getScore(entry).setScore(line);
-
- Team team = parent.registerNewTeam("entry_" + line);
- team.addEntry(entry);
- update(parent);
- }
-
- @Override
- public void update(Scoreboard parent) {
- Team team = parent.getTeam("entry_" + line);
- StringBuilder prefix = new StringBuilder();
- StringBuilder suffix = new StringBuilder();
- int totalLength = this.length + colors.length;
+ public Map render() {
+ Map output = Maps.newHashMap();
+ StringBuilder builder = new StringBuilder();
for (ChatColor color : colors) {
- prefix.append(color);
- if (totalLength > 16) {
- suffix.append(color);
- }
+ builder.append(color);
}
for (int i = 0; i < length; i++) {
- prefix.append(symbol);
- if (totalLength > 16) {
- suffix.append(symbol);
- }
- }
-
- if (prefix.toString().length() > 16) {
- if (suffix.toString().length() > 16) {
- team.setSuffix(suffix.toString().substring(0, 16));
- } else {
- team.setSuffix(suffix.toString());
- }
- team.setPrefix(prefix.toString().substring(0, 16));
- return;
+ builder.append(symbol);
}
- team.setPrefix(prefix.toString());
+ output.put(line, builder.toString());
+ return output;
}
+ /**
+ * Contains some static values for possible length of
+ * a line separator.
+ */
public static class SeparatorLength {
public static final int FULL = 30;
public static final int HALF = 15;
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponent.java
new file mode 100755
index 00000000..c3016a3a
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponent.java
@@ -0,0 +1,27 @@
+package de.pxav.kelp.core.sidebar.component;
+
+import java.util.Map;
+
+/**
+ * This superclass is inherited by every sidebar component.
+ * It contains a {@link #render()} method converting the information
+ * provided in your component to the final content that is
+ * displayed in the sidebar.
+ *
+ * @author pxav
+ */
+public abstract class SidebarComponent {
+
+ /**
+ * Renders all the information provided in the component
+ * to a map containing the final information to be rendered
+ * to the sidebar (the lines where the text should be placed and
+ * the text to write there).
+ *
+ * @return A map, where the key is the absolute line where
+ * the component should be placed in the sidebar and
+ * the value is the actual text for that line.
+ */
+ public abstract Map render();
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponentFactory.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponentFactory.java
old mode 100644
new mode 100755
index 450b3dac..17761aa1
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponentFactory.java
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SidebarComponentFactory.java
@@ -27,88 +27,4 @@ public SidebarComponentFactory(KelpLogger kelpLogger, SidebarUtils sidebarUtils)
this.sidebarUtils = sidebarUtils;
}
- public SimpleTextComponent simpleTextComponent() {
- return new SimpleTextComponent(sidebarUtils, kelpLogger);
- }
-
- public SimpleTextComponent simpleTextComponent(String text) {
- return new SimpleTextComponent(sidebarUtils, kelpLogger)
- .text(text);
- }
-
- public SimpleTextComponent simpleTextComponent(String text, int line) {
- return new SimpleTextComponent(sidebarUtils, kelpLogger)
- .text(text)
- .line(line);
- }
-
- public EmptyLineComponent emptyLineComponent() {
- return new EmptyLineComponent(sidebarUtils);
- }
-
- public EmptyLineComponent emptyLineComponent(int line) {
- return new EmptyLineComponent(sidebarUtils).score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent() {
- return new LineSeparatorComponent(sidebarUtils);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line) {
- return new LineSeparatorComponent(sidebarUtils).score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(char symbol) {
- return new LineSeparatorComponent(sidebarUtils).symbol(symbol);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line, char symbol) {
- return new LineSeparatorComponent(sidebarUtils)
- .symbol(symbol)
- .score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(char symbol, ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils).symbol(symbol).color(colors);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line, char symbol, ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils)
- .symbol(symbol)
- .color(colors)
- .score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils)
- .color(colors);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line, ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils)
- .color(colors)
- .score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(char symbol, int length, ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils)
- .color(colors)
- .symbol(symbol)
- .length(length);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line, char symbol, int length, ChatColor... colors) {
- return new LineSeparatorComponent(sidebarUtils)
- .color(colors)
- .symbol(symbol)
- .length(length)
- .score(line);
- }
-
- public LineSeparatorComponent lineSeparatorComponent(int line, int length) {
- return new LineSeparatorComponent(sidebarUtils)
- .score(line)
- .length(length);
- }
-
}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleSidebarComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleSidebarComponent.java
deleted file mode 100644
index 1582f425..00000000
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleSidebarComponent.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package de.pxav.kelp.core.sidebar.component;
-
-import org.bukkit.scoreboard.Scoreboard;
-
-/**
- * This interface is the template for all sidebar
- * components. It provides methods to render and update
- * the component.
- *
- * @author pxav
- */
-public interface SimpleSidebarComponent {
-
- /**
- * This method creates and renders the component
- * to the given scoreboard.
- * So it will create a new team and set the content.
- *
- * @param parent The scoreboard you want to render the component on.
- */
- void render(Scoreboard parent);
-
- /**
- * Updates the component, which means that no
- * new team will be created, but the existing one
- * will be updated to avoid flickering.
- *
- * @param parent The scoreboard on which the component should be rendered.
- */
- void update(Scoreboard parent);
-
-}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleTextComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleTextComponent.java
deleted file mode 100644
index a814d66c..00000000
--- a/core/src/main/java/de/pxav/kelp/core/sidebar/component/SimpleTextComponent.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package de.pxav.kelp.core.sidebar.component;
-
-import de.pxav.kelp.core.sidebar.SidebarUtils;
-import de.pxav.kelp.core.logger.KelpLogger;
-import de.pxav.kelp.core.logger.LogLevel;
-import org.bukkit.scoreboard.DisplaySlot;
-import org.bukkit.scoreboard.Objective;
-import org.bukkit.scoreboard.Scoreboard;
-import org.bukkit.scoreboard.Team;
-
-/**
- * This scoreboard component is used to display
- * custom text on your sidebar.
- *
- * @author pxav
- */
-public class SimpleTextComponent implements SimpleSidebarComponent {
-
- private String text;
- private int line;
-
- private SidebarUtils sidebarUtils;
- private KelpLogger logger;
-
- public SimpleTextComponent(SidebarUtils sidebarUtils, KelpLogger logger) {
- this.sidebarUtils = sidebarUtils;
- this.logger = logger;
- }
-
- public SimpleTextComponent text(String text) {
- this.text = text;
- return this;
- }
-
- public SimpleTextComponent line(int line) {
- this.line = line;
- return this;
- }
-
- /**
- * This method creates and renders the component
- * to the given scoreboard.
- * So it will create a new team and set the content.
- *
- * @param parent The scoreboard you want to render the component on.
- */
- @Override
- public void render(Scoreboard parent) {
- String entry = sidebarUtils.randomEmptyEntry(parent);
- Objective objective = parent.getObjective(DisplaySlot.SIDEBAR);
-
- objective.getScore(entry).setScore(line);
-
- Team team = parent.registerNewTeam("entry_" + line);
- team.addEntry(entry);
- update(parent);
- }
-
- /**
- * Updates the component, which means that no
- * new team will be created, but the existing one
- * will be updated to avoid flickering.
- *
- * @param parent The scoreboard on which the component should be rendered.
- */
- @Override
- public void update(Scoreboard parent) {
- String teamName = "entry_" + line;
- Team team = parent.getTeam(teamName);
-
- if (team == null) {
- logger.log(LogLevel.ERROR, "Cannot update component at score " + line + ", "
- + "because there is no entry assigned to this score.");
- return;
- }
- sidebarUtils.setTeamData(text, team);
- }
-
-}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulListComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulListComponent.java
new file mode 100644
index 00000000..94300508
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulListComponent.java
@@ -0,0 +1,246 @@
+package de.pxav.kelp.core.sidebar.component;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * This component is used to automate list displays in sidebars.
+ * Imagine a JumpLeague plugin for example where a list of top players
+ * and their progression is displayed in the sidebar. Then it would be
+ * useful to be able to
+ * - limit your displayed list to a specific size (to avoid having a cluttered
+ * scoreboard when you have 20+ players in a round)
+ * - display dynamic content (using a {@link Supplier} for example) instead of static text
+ * - keeping the list size constant for a more convenient design (when players quit for example)
+ *
+ * All this is supported by this component.
+ *
+ * By default this component does not support {@code lazyUpdates}, because
+ * the number of lines set by it may vary. To avoid this use a limiter
+ * {@link #limitTo(int)} and {@link #enableAutoFill()}.
+ *
+ * @author pxav
+ */
+public class StatefulListComponent extends SidebarComponent {
+
+ private Supplier> list;
+ private int startLine;
+ private int maxLine;
+ private boolean enableLimiter;
+ private boolean ascendingOrder;
+ private boolean autoFill;
+
+ public StatefulListComponent() {
+ // set default values
+ this.list = Lists::newArrayList;
+ this.startLine = 0;
+ this.maxLine = 0;
+ this.enableLimiter = false;
+ this.ascendingOrder = false;
+ }
+
+ public static StatefulListComponent create() {
+ return new StatefulListComponent();
+ }
+
+ /**
+ * The list to be displayed in the sidebar. The order of your list
+ * should be in the order of how they should be finally displayed.
+ * depending on whether you sort them with {@link #ascendingOrder()} or
+ * {@link #descendingOrder()}.
+ *
+ * @param listSupplier The supplier providing the list to be displayed.
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent list(Supplier> listSupplier) {
+ this.list = listSupplier;
+ return this;
+ }
+
+ /**
+ * Defines the line in the scoreboard where the first list element should
+ * be placed and from where the list items should be placed. If you selected
+ * {@link #ascendingOrder()} the number for the next list item will be incremented
+ * and if you choose {@link #descendingOrder()} the number will be decremented.
+ *
+ * Please note that the given line ids are absolute and you should check whether
+ * no components are in range of your list items, which could cause weird behaviour.
+ *
+ * @param startLine The line from where the list should start.
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent startFrom(int startLine) {
+ this.startLine = startLine;
+ return this;
+ }
+
+ /**
+ * Enables the line limiter for the list. So there will be only as many
+ * list items in the sidebar as you input here. Generally this is recommended
+ * if you are not completely sure how many list items will be displayed. Then
+ * you can make sure that those list items do not interfere with any other
+ * items.
+ *
+ * Please make sure that the number is reachable if you set it. If your
+ * list order is ascending and this number is smaller than your start line,
+ * this number will have no effect.
+ *
+ * @param maxLine The last line where a list item can be displayed.
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent limitTo(int maxLine) {
+ this.maxLine = maxLine;
+ this.enableLimiter = true;
+ return this;
+ }
+
+ /**
+ * Normally, this component is not compatible with {@code lazyUpdating}, because
+ * the amount of lines covered by it may vary with each update. To avoid this, enable
+ * the limiter with {@link #limitTo(int)} and enable auto fill by calling this method.
+ *
+ * Auto fill makes sure that if the amount of list items is smaller than the given
+ * maximum, more empty lines are inserted to keep the total line amount constant.
+ * This allows you to create flicker free sidebars using this component.
+ *
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent enableAutoFill() {
+ this.autoFill = true;
+ return this;
+ }
+
+ /**
+ * Disables the auto fill feature described in {@link #enableAutoFill()}.
+ *
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent disableAutoFill() {
+ this.autoFill = false;
+ return this;
+ }
+
+ /**
+ * Disables the item limiter described in {@link #limitTo(int)}. If you have
+ * auto fill enabled, this will also disable auto fill!
+ *
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent disableLimiter() {
+ this.enableLimiter = false;
+ return this;
+ }
+
+ /**
+ * Sets the list item order to {@code ascending}. This means that
+ * if your start line is 10 for example, the second item will be placed
+ * on line 11, the third one on 12, and so one.
+ *
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent ascendingOrder() {
+ this.ascendingOrder = true;
+ return this;
+ }
+
+ /**
+ * Sets the list item order to {@code descending}. This means that
+ * if your start line is 10 for example, the second item will be placed
+ * on line 9, the third one on 8, and so one.
+ *
+ * @return Instance of the current component for more fluent builder design.
+ */
+ public StatefulListComponent descendingOrder() {
+ this.ascendingOrder = false;
+ return this;
+ }
+
+ /**
+ * Renders all the information provided in the component
+ * to a map containing the final information to be rendered
+ * to the sidebar (the lines where the text should be placed and
+ * the text to write there).
+ *
+ * @return A map, where the key is the absolute line where
+ * the component should be placed in the sidebar and
+ * the value is the actual text for that line.
+ */
+ @Override
+ public Map render() {
+ Map output = Maps.newHashMap();
+ List lines = list.get();
+
+ Iterator iterator = lines.iterator();
+
+ // before iterating through the list, the plugin checks whether
+ // the list should be placed in ascending or descending order, which
+ // is important to check whether indexes have to be decremented or incremented.
+ if (ascendingOrder) {
+ for (int i = startLine; i < Integer.MAX_VALUE; i++) {
+
+ if (!iterator.hasNext()) {
+ // if the iterator has arrived at the last item it has to check
+ // whether there are enough lines to fill the minimum required area
+ // if such an area is set with autoFill. It will then add any missing
+ // lines.
+ if (enableLimiter && autoFill && i <= maxLine) {
+ for (int fillIndex = i; fillIndex <= maxLine; fillIndex++) {
+ output.put(fillIndex, " ");
+ }
+ return output;
+ }
+ return output;
+ }
+ if (enableLimiter && i == maxLine) {
+ return output;
+ }
+ output.put(i, iterator.next());
+ }
+ } else {
+ for (int i = startLine; i < Integer.MAX_VALUE; i--) {
+
+ // here you can find pretty much the same as described above
+ // but for the descending list order. The major difference is that
+ // the numbers are inverted here. (++ -> -- for example)
+ if (!iterator.hasNext()) {
+ if (enableLimiter && autoFill && i >= maxLine) {
+ for (int fillIndex = i; fillIndex >= maxLine; fillIndex--) {
+ output.put(fillIndex, " ");
+ }
+ return output;
+ }
+ return output;
+ }
+ if (enableLimiter && i == maxLine) {
+ return output;
+ }
+ output.put(i, iterator.next());
+ }
+ }
+
+ // if the list is empty, there have been no checks whether it is
+ // necessary to fill up empty lines. So this is done here. If the
+ // limiter and auto fill mode are enabled, the plugin will add as
+ // many lines as needed to fill the space.
+ if (output.isEmpty() && enableLimiter && autoFill) {
+ if (ascendingOrder) {
+ for (int fillIndex = startLine; fillIndex <= maxLine; fillIndex++) {
+ output.put(fillIndex, " ");
+ }
+ } else {
+ for (int fillIndex = startLine; fillIndex >= maxLine; fillIndex--) {
+ output.put(fillIndex, " ");
+ }
+ }
+
+ }
+
+ return output;
+ }
+
+}
diff --git a/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulTextComponent.java b/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulTextComponent.java
new file mode 100755
index 00000000..c08f668d
--- /dev/null
+++ b/core/src/main/java/de/pxav/kelp/core/sidebar/component/StatefulTextComponent.java
@@ -0,0 +1,75 @@
+package de.pxav.kelp.core.sidebar.component;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * This component allows you to display dynamic content
+ * on sidebars. While the {@link StatelessTextComponent} can only display
+ * static, non-updatable content, this component can be updated.
+ * Example use cases for that would be displaying the player's
+ * coins or a timer.
+ *
+ * @author pxav
+ */
+public class StatefulTextComponent extends SidebarComponent {
+
+ // absolute position to place the component
+ private int line;
+
+ private Supplier