getManager() {
+ return this.holograms;
+ }
+
+ @Override
+ public @NotNull HandlerList getHandlers() {
+ return handlerList;
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/Hologram.java b/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/Hologram.java
new file mode 100644
index 00000000..e9f2319c
--- /dev/null
+++ b/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/Hologram.java
@@ -0,0 +1,372 @@
+package de.oliver.fancyholograms.api.hologram;
+
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.data.property.Visibility;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.Color;
+import org.bukkit.World;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.lushplugins.chatcolorhandler.ModernChatColorHandler;
+
+import java.util.*;
+
+/**
+ * Abstract base class for creating, updating, and managing holograms.
+ *
+ * This class provides the basic functionality needed to work with holograms
+ * across multiple versions of Minecraft. To create a hologram specific to a version of Minecraft,
+ * extend this class and implement the abstract methods.
+ *
+ * Note that the specific way holograms are created, updated, and deleted
+ * will vary depending on the Minecraft version.
+ *
+ * A Hologram object includes data about the hologram and maintains a set of players to whom the hologram is shown.
+ */
+public abstract class Hologram {
+
+ public static final int LINE_WIDTH = 1000;
+ public static final Color TRANSPARENT = Color.fromARGB(0);
+ protected static final int MINIMUM_PROTOCOL_VERSION = 762;
+
+ protected final @NotNull HologramData data;
+ /**
+ * Set of UUIDs of players to whom the hologram is currently shown.
+ */
+ protected final @NotNull Set viewers = new HashSet<>();
+
+ protected Hologram(@NotNull final HologramData data) {
+ this.data = data;
+ }
+
+ @NotNull
+ public String getName() {
+ return data.getName();
+ }
+
+ public final @NotNull HologramData getData() {
+ return this.data;
+ }
+
+ /**
+ * Returns the entity id of this hologram
+ * This id is for packet use only as the entity is not registered to the server
+ * @return entity id
+ */
+ public abstract int getEntityId();
+
+ /**
+ * Returns the Display entity of this Hologram object.
+ * The entity is not registered in the world or server.
+ * Only use this method if you know what you're doing.
+ *
+ * This method will return null in 1.20.5 and newer versions
+ *
+ * @return the Display entity of this Hologram object
+ */
+ @ApiStatus.Internal
+ @Deprecated(forRemoval = true, since = "2.4.1")
+ public abstract @Nullable Display getDisplayEntity();
+
+ protected abstract void create();
+
+ protected abstract void delete();
+
+ protected abstract void update();
+
+ protected abstract boolean show(@NotNull final Player player);
+
+ protected abstract boolean hide(@NotNull final Player player);
+
+ protected abstract void refresh(@NotNull final Player player);
+
+ /**
+ * Create the hologram entity.
+ * Only run this if creating custom Hologram implementations as this is run in
+ * {@link de.oliver.fancyholograms.api.HologramManager#create(HologramData)}.
+ */
+ public final void createHologram() {
+ create();
+ }
+
+ /**
+ * Deletes the hologram entity.
+ */
+ public final void deleteHologram() {
+ delete();
+ }
+
+ /**
+ * Shows the hologram to a collection of players.
+ * Use {@link #forceShowHologram(Player)} if this hologram is not registered to the HologramManager.
+ *
+ * @param players The players to show the hologram to
+ */
+ public final void showHologram(Collection extends Player> players) {
+ players.forEach(this::showHologram);
+ }
+
+ /**
+ * Shows the hologram to a player.
+ * Use {@link #forceShowHologram(Player)} if this hologram is not registered to the HologramManager.
+ *
+ * @param player The player to show the hologram to
+ */
+ public final void showHologram(Player player) {
+ viewers.add(player.getUniqueId());
+ }
+
+ /**
+ * Forcefully shows the hologram to a player.
+ *
+ * @param player The player to show the hologram to
+ */
+ public final void forceShowHologram(Player player) {
+ show(player);
+
+ if (this.getData().getVisibility().equals(Visibility.MANUAL)) {
+ Visibility.ManualVisibility.addDistantViewer(this, player.getUniqueId());
+ }
+ }
+
+ /**
+ * Hides the hologram from a collection of players.
+ * Use {@link #forceHideHologram(Player)} if this hologram is not registered to the HologramManager.
+ *
+ * @param players The players to hide the hologram from
+ */
+ public final void hideHologram(Collection extends Player> players) {
+ players.forEach(this::hideHologram);
+ }
+
+ /**
+ * Hides the hologram from a player.
+ * Use {@link #forceHideHologram(Player)} if this hologram is not registered to the HologramManager.
+ *
+ * @param player The player to hide the hologram from
+ */
+ public final void hideHologram(Player player) {
+ viewers.remove(player.getUniqueId());
+ }
+
+ /**
+ * Forcefully hides the hologram from a player.
+ *
+ * @param player The player to show the hologram to
+ */
+ public final void forceHideHologram(Player player) {
+ hide(player);
+
+ if (this.getData().getVisibility().equals(Visibility.MANUAL)) {
+ Visibility.ManualVisibility.removeDistantViewer(this, player.getUniqueId());
+ }
+ }
+
+ /**
+ * Queues hologram to update and refresh for players.
+ *
+ * @deprecated in favour of {@link #queueUpdate()}
+ */
+ @Deprecated(forRemoval = true)
+ public final void updateHologram() {
+ queueUpdate();
+ }
+
+ /**
+ * Queues hologram to update and refresh for players
+ * Use {@link #forceUpdate()} if this hologram is not registered to the HologramManager.
+ */
+ public final void queueUpdate() {
+ data.setHasChanges(true);
+ }
+
+ /**
+ * Forcefully updates and refreshes hologram for players.
+ */
+ public final void forceUpdate() {
+ update();
+ }
+
+ /**
+ * Refreshes the hologram for the players currently viewing it.
+ */
+ public void refreshForViewers() {
+ final var players = getViewers()
+ .stream()
+ .map(Bukkit::getPlayer)
+ .toList();
+
+ refreshHologram(players);
+ }
+
+ /**
+ * Refreshes the hologram for players currently viewing it in the same world as the hologram.
+ */
+ public void refreshForViewersInWorld() {
+ World world = data.getLocation().getWorld();
+ final var players = getViewers()
+ .stream()
+ .map(Bukkit::getPlayer)
+ .filter(player -> player != null && player.getWorld().equals(world))
+ .toList();
+
+ refreshHologram(players);
+ }
+
+ /**
+ * Refreshes the hologram's data for a player.
+ *
+ * @param player the player to refresh for
+ */
+ public final void refreshHologram(@NotNull final Player player) {
+ refresh(player);
+ }
+
+ /**
+ * Refreshes the hologram's data for a collection of players.
+ *
+ * @param players the collection of players to refresh for
+ */
+ public final void refreshHologram(@NotNull final Collection extends Player> players) {
+ players.forEach(this::refreshHologram);
+ }
+
+ /**
+ * @return a copy of the set of UUIDs of players currently viewing the hologram
+ */
+ public final @NotNull Set getViewers() {
+ return new HashSet<>(this.viewers);
+ }
+
+ /**
+ * @param player the player to check for
+ * @return whether the player is currently viewing the hologram
+ */
+ public final boolean isViewer(@NotNull final Player player) {
+ return isViewer(player.getUniqueId());
+ }
+
+ /**
+ * @param player the uuid of the player to check for
+ * @return whether the player is currently viewing the hologram
+ */
+ public final boolean isViewer(@NotNull final UUID player) {
+ return this.viewers.contains(player);
+ }
+
+ protected boolean shouldShowTo(@NotNull final Player player) {
+ if (!meetsVisibilityConditions(player)) {
+ return false;
+ }
+
+ return isWithinVisibilityDistance(player);
+ }
+
+ public boolean meetsVisibilityConditions(@NotNull final Player player) {
+ return this.getData().getVisibility().canSee(player, this);
+ }
+
+ public boolean isWithinVisibilityDistance(@NotNull final Player player) {
+ final var location = getData().getLocation();
+ if (!location.getWorld().equals(player.getWorld())) {
+ return false;
+ }
+
+ int visibilityDistance = data.getVisibilityDistance();
+ double distanceSquared = location.distanceSquared(player.getLocation());
+
+ return distanceSquared <= visibilityDistance * visibilityDistance;
+ }
+
+ /**
+ * Checks and updates the shown state for a player.
+ * If the hologram is shown and should not be, it hides it.
+ * If the hologram is not shown and should be, it shows it.
+ * Use {@link #forceUpdateShownStateFor(Player)} if this hologram is not registered to the HologramManager.
+ *
+ * @param player the player to check and update the shown state for
+ */
+ public void updateShownStateFor(Player player) {
+ boolean isShown = isViewer(player);
+ boolean shouldBeShown = shouldShowTo(player);
+
+ if (isShown && !shouldBeShown) {
+ showHologram(player);
+ } else if (!isShown && shouldBeShown) {
+ hideHologram(player);
+ }
+ }
+
+ /**
+ * Checks and forcefully updates the shown state for a player.
+ * If the hologram is shown and should not be, it hides it.
+ * If the hologram is not shown and should be, it shows it.
+ *
+ * @param player the player to check and update the shown state for
+ */
+ public void forceUpdateShownStateFor(Player player) {
+ boolean isShown = isViewer(player);
+
+ if (meetsVisibilityConditions(player)) {
+ if (isWithinVisibilityDistance(player)) {
+ // Ran if the player meets the visibility conditions and is within visibility distance
+ if (!isShown) {
+ show(player);
+
+ if (getData().getVisibility().equals(Visibility.MANUAL)) {
+ Visibility.ManualVisibility.removeDistantViewer(this, player.getUniqueId());
+ }
+ }
+ } else {
+ // Ran if the player meets the visibility conditions but is not within visibility distance
+ if (isShown) {
+ hide(player);
+
+ if (getData().getVisibility().equals(Visibility.MANUAL)) {
+ Visibility.ManualVisibility.addDistantViewer(this, player.getUniqueId());
+ }
+ }
+ }
+ } else {
+ // Ran if the player does not meet visibility conditions
+ if (isShown) {
+ hide(player);
+ }
+ }
+ }
+
+ /**
+ * Gets the text shown in the hologram. If a player is specified, placeholders in the text are replaced
+ * with their corresponding values for the player.
+ *
+ * @param player the player to get the placeholders for, or null if no placeholders should be replaced
+ * @return the text shown in the hologram
+ */
+ public final Component getShownText(@Nullable final Player player) {
+ if (!(getData() instanceof TextHologramData textData)) {
+ return null;
+ }
+
+ var text = String.join("\n", textData.getText());
+
+ return ModernChatColorHandler.translate(text, player);
+ }
+
+ @Override
+ public final boolean equals(@Nullable final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Hologram that)) return false;
+
+ return Objects.equals(this.getData(), that.getData());
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(this.getData());
+ }
+}
diff --git a/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/HologramType.java b/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/HologramType.java
new file mode 100644
index 00000000..432fcdd8
--- /dev/null
+++ b/plugins/fancyholograms-v2/api/src/main/java/de/oliver/fancyholograms/api/hologram/HologramType.java
@@ -0,0 +1,31 @@
+package de.oliver.fancyholograms.api.hologram;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum HologramType {
+ TEXT(Arrays.asList("background", "textshadow", "textalignment", "seethrough", "setline", "removeline", "addline", "insertbefore", "insertafter", "updatetextinterval")),
+ ITEM(List.of("item")),
+ BLOCK(List.of("block"));
+
+ private final List commands;
+
+ HologramType(List commands) {
+ this.commands = commands;
+ }
+
+ public static HologramType getByName(String name) {
+ for (HologramType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+
+ return null;
+ }
+
+ public List getCommands() {
+ return commands;
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/build.gradle.kts b/plugins/fancyholograms-v2/build.gradle.kts
new file mode 100644
index 00000000..cd0d70ce
--- /dev/null
+++ b/plugins/fancyholograms-v2/build.gradle.kts
@@ -0,0 +1,209 @@
+import net.minecrell.pluginyml.bukkit.BukkitPluginDescription
+import net.minecrell.pluginyml.paper.PaperPluginDescription
+import java.io.BufferedReader
+import java.io.InputStreamReader
+
+plugins {
+ id("java-library")
+ id("maven-publish")
+
+ id("xyz.jpenilla.run-paper")
+ id("com.gradleup.shadow")
+ id("net.minecrell.plugin-yml.paper")
+ id("io.papermc.hangar-publish-plugin")
+ id("com.modrinth.minotaur")
+}
+
+runPaper.folia.registerTask()
+
+val supportedVersions =
+ listOf(
+ "1.19.4",
+ "1.20",
+ "1.20.1",
+ "1.20.2",
+ "1.20.3",
+ "1.20.4",
+ "1.20.5",
+ "1.20.6",
+ "1.21",
+ "1.21.1",
+ "1.21.2",
+ "1.21.3",
+ "1.21.4",
+ )
+
+allprojects {
+ group = "de.oliver"
+ val buildId = System.getenv("BUILD_ID")
+ version = "2.4.2" + (if (buildId != null) ".$buildId" else "")
+ description = "Simple, lightweight and fast hologram plugin using display entities"
+
+ repositories {
+ mavenLocal()
+ mavenCentral()
+
+ maven(url = "https://repo.papermc.io/repository/maven-public/")
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+
+ maven(url = "https://repo.fancyplugins.de/snapshots")
+ maven(url = "https://repo.fancyplugins.de/releases")
+ maven(url = "https://repo.lushplugins.org/releases")
+ maven(url = "https://repo.viaversion.com/")
+ maven(url = "https://repo.opencollab.dev/main/")
+ }
+}
+
+dependencies {
+ compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
+
+ implementation(project(":plugins:fancyholograms-v2:api"))
+ implementation(project(":plugins:fancyholograms-v2:implementation_1_20_4", configuration = "reobf"))
+ implementation(project(":plugins:fancyholograms-v2:implementation_1_20_2", configuration = "reobf"))
+ implementation(project(":plugins:fancyholograms-v2:implementation_1_20_1", configuration = "reobf"))
+ implementation(project(":plugins:fancyholograms-v2:implementation_1_19_4", configuration = "reobf"))
+
+ implementation("de.oliver:FancyLib:36")
+ implementation("de.oliver:FancySitula:0.0.13")
+ implementation("de.oliver.FancyAnalytics:api:0.1.6")
+ implementation("de.oliver.FancyAnalytics:logger:0.0.6")
+
+ compileOnly("de.oliver:FancyNpcs:2.4.2")
+ compileOnly("org.lushplugins:ChatColorHandler:5.1.2")
+ compileOnly("org.geysermc.floodgate:api:2.2.4-SNAPSHOT")
+}
+
+paper {
+ main = "de.oliver.fancyholograms.FancyHolograms"
+ bootstrapper = "de.oliver.fancyholograms.loaders.FancyHologramsBootstrapper"
+ loader = "de.oliver.fancyholograms.loaders.FancyHologramsLoader"
+ foliaSupported = true
+ version = rootProject.version.toString()
+ description = "Simple, lightweight and fast hologram plugin using display entities"
+ apiVersion = "1.19"
+ load = BukkitPluginDescription.PluginLoadOrder.POSTWORLD
+ serverDependencies {
+ register("FancyNpcs") {
+ required = false
+ load = PaperPluginDescription.RelativeLoadOrder.BEFORE
+ }
+ register("MiniPlaceholders") {
+ required = false
+ load = PaperPluginDescription.RelativeLoadOrder.BEFORE
+ }
+ register("PlaceholderAPI") {
+ required = false
+ load = PaperPluginDescription.RelativeLoadOrder.BEFORE
+ }
+ register("floodgate") {
+ required = false
+ load = PaperPluginDescription.RelativeLoadOrder.BEFORE
+ joinClasspath = true
+ }
+ }
+}
+
+tasks {
+ runServer {
+ minecraftVersion("1.21.4")
+
+ downloadPlugins {
+ modrinth("fancynpcs", "2.4.0")
+ hangar("ViaVersion", "5.2.0")
+ hangar("ViaBackwards", "5.2.0")
+// modrinth("multiverse-core", "4.3.11")
+ hangar("PlaceholderAPI", "2.11.6")
+// modrinth("DecentHolograms", "2.8.12")
+ }
+ }
+
+ shadowJar {
+ archiveClassifier.set("")
+ archiveBaseName.set("FancyHolograms")
+
+ dependsOn(":plugins:fancyholograms-v2:api:shadowJar")
+ }
+
+ compileJava {
+ options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything
+ options.release = 21
+ // For cloud-annotations, see https://cloud.incendo.org/annotations/#command-components
+ options.compilerArgs.add("-parameters")
+ }
+
+ javadoc {
+ options.encoding = Charsets.UTF_8.name() // We want UTF-8 for everything
+ }
+
+ processResources {
+ filteringCharset = Charsets.UTF_8.name() // We want UTF-8 for everything
+
+ val props = mapOf(
+ "description" to project.description,
+ "version" to project.version,
+ "hash" to gitCommitHash.get(),
+ "build" to (System.getenv("BUILD_ID") ?: "").ifEmpty { "undefined" }
+ )
+
+ inputs.properties(props)
+
+ filesMatching("paper-plugin.yml") {
+ expand(props)
+ }
+
+ filesMatching("version.yml") {
+ expand(props)
+ }
+ }
+}
+
+tasks.publishAllPublicationsToHangar {
+ dependsOn("shadowJar")
+}
+
+tasks.modrinth {
+ dependsOn("shadowJar")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21))
+}
+
+val gitCommitHash: Provider = providers.exec {
+ commandLine("git", "rev-parse", "HEAD")
+}.standardOutput.asText.map { it.trim() }
+
+val gitCommitMessage: Provider = providers.exec {
+ commandLine("git", "log", "-1", "--pretty=%B")
+}.standardOutput.asText.map { it.trim() }
+
+hangarPublish {
+ publications.register("plugin") {
+ version = project.version as String
+ id = "FancyHolograms"
+ channel = "Alpha"
+
+ apiKey.set(System.getenv("HANGAR_PUBLISH_API_TOKEN"))
+
+ platforms {
+ paper {
+ jar = tasks.shadowJar.flatMap { it.archiveFile }
+ platformVersions = supportedVersions
+ }
+ }
+
+ changelog = gitCommitMessage.get()
+ }
+}
+
+modrinth {
+ token.set(System.getenv("MODRINTH_PUBLISH_API_TOKEN"))
+ projectId.set("fancyholograms")
+ versionNumber.set(project.version.toString())
+ versionType.set("alpha")
+ uploadFile.set(file("build/libs/${project.name}-${project.version}.jar"))
+ gameVersions.addAll(supportedVersions)
+ loaders.add("paper")
+ loaders.add("folia")
+ changelog.set(gitCommitMessage.get())
+}
\ No newline at end of file
diff --git a/plugins/fancyholograms-v2/images/banner.png b/plugins/fancyholograms-v2/images/banner.png
new file mode 100644
index 00000000..60f5ec7a
Binary files /dev/null and b/plugins/fancyholograms-v2/images/banner.png differ
diff --git a/plugins/fancyholograms-v2/images/screenshots/example1.jpeg b/plugins/fancyholograms-v2/images/screenshots/example1.jpeg
new file mode 100644
index 00000000..97cc97d3
Binary files /dev/null and b/plugins/fancyholograms-v2/images/screenshots/example1.jpeg differ
diff --git a/plugins/fancyholograms-v2/images/screenshots/example2.jpeg b/plugins/fancyholograms-v2/images/screenshots/example2.jpeg
new file mode 100644
index 00000000..7dd724e4
Binary files /dev/null and b/plugins/fancyholograms-v2/images/screenshots/example2.jpeg differ
diff --git a/plugins/fancyholograms-v2/images/screenshots/example3.jpeg b/plugins/fancyholograms-v2/images/screenshots/example3.jpeg
new file mode 100644
index 00000000..db23574a
Binary files /dev/null and b/plugins/fancyholograms-v2/images/screenshots/example3.jpeg differ
diff --git a/plugins/fancyholograms-v2/images/screenshots/example4.jpeg b/plugins/fancyholograms-v2/images/screenshots/example4.jpeg
new file mode 100644
index 00000000..6f647d41
Binary files /dev/null and b/plugins/fancyholograms-v2/images/screenshots/example4.jpeg differ
diff --git a/plugins/fancyholograms-v2/images/screenshots/example5.jpeg b/plugins/fancyholograms-v2/images/screenshots/example5.jpeg
new file mode 100644
index 00000000..cce15620
Binary files /dev/null and b/plugins/fancyholograms-v2/images/screenshots/example5.jpeg differ
diff --git a/plugins/fancyholograms-v2/implementation_1_19_4/build.gradle.kts b/plugins/fancyholograms-v2/implementation_1_19_4/build.gradle.kts
new file mode 100644
index 00000000..0022b811
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_19_4/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("java-library")
+ id("io.papermc.paperweight.userdev")
+}
+
+
+val minecraftVersion = "1.19.4"
+
+
+dependencies {
+ paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT")
+
+ implementation(project(":plugins:fancyholograms-v2:api"))
+ implementation("de.oliver:FancyLib:36")
+ compileOnly("com.viaversion:viaversion-api:5.2.1")
+}
+
+
+tasks {
+ named("assemble") {
+ dependsOn(named("reobfJar"))
+ }
+
+ javadoc {
+ options.encoding = Charsets.UTF_8.name()
+ }
+
+ compileJava {
+ options.encoding = Charsets.UTF_8.name()
+
+ options.release.set(17)
+ }
+}
\ No newline at end of file
diff --git a/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_19_4.java b/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_19_4.java
new file mode 100644
index 00000000..74837bd9
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_19_4.java
@@ -0,0 +1,271 @@
+package de.oliver.fancyholograms.hologram.version;
+
+import com.mojang.math.Transformation;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramHideEvent;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancylib.ReflectionUtils;
+import io.papermc.paper.adventure.PaperAdventure;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
+import net.minecraft.network.syncher.EntityDataAccessor;
+import net.minecraft.network.syncher.SynchedEntityData.DataItem;
+import net.minecraft.network.syncher.SynchedEntityData.DataValue;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Brightness;
+import net.minecraft.world.entity.Display;
+import net.minecraft.world.entity.Display.TextDisplay;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.Block;
+import org.bukkit.craftbukkit.v1_19_R3.CraftWorld;
+import org.bukkit.craftbukkit.v1_19_R3.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+
+import java.util.ArrayList;
+
+import static de.oliver.fancylib.ReflectionUtils.getValue;
+
+public final class Hologram1_19_4 extends Hologram {
+
+ @Nullable
+ private Display display;
+
+ public Hologram1_19_4(@NotNull final HologramData data) {
+ super(data);
+ }
+
+ @Override
+ public int getEntityId() {
+ return display.getId();
+ }
+
+ @Override
+ public @Nullable org.bukkit.entity.Display getDisplayEntity() {
+ return display != null ? (org.bukkit.entity.Display) display.getBukkitEntity() : null;
+ }
+
+ @Override
+ public void create() {
+ final var location = data.getLocation();
+ if (location.getWorld() == null) {
+ return; // no location data, cannot be created
+ }
+
+ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle();
+
+ switch (data.getType()) {
+ case TEXT -> this.display = new Display.TextDisplay(EntityType.TEXT_DISPLAY, world);
+ case BLOCK -> this.display = new Display.BlockDisplay(EntityType.BLOCK_DISPLAY, world);
+ case ITEM -> this.display = new Display.ItemDisplay(EntityType.ITEM_DISPLAY, world);
+ }
+
+ if (data instanceof DisplayHologramData dd) {
+ final var DATA_INTERPOLATION_DURATION_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_19_4.DATA_INTERPOLATION_DURATION_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_DURATION_ID, dd.getInterpolationDuration());
+
+ final var DATA_INTERPOLATION_START_DELTA_TICKS_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_19_4.DATA_INTERPOLATION_START_DELTA_TICKS_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_START_DELTA_TICKS_ID, 0);
+ }
+
+ update();
+ }
+
+ @Override
+ public void delete() {
+ this.display = null;
+ }
+
+ @Override
+ public void update() {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to update
+ }
+
+ // location data
+ final var location = data.getLocation();
+ if (!location.isWorldLoaded()) {
+ return;
+ } else {
+ display.setPosRaw(location.x(), location.y(), location.z());
+ display.setYRot(location.getYaw());
+ display.setXRot(location.getPitch());
+ }
+
+ if (display instanceof TextDisplay textDisplay && data instanceof TextHologramData textData) {
+ // line width
+ final var DATA_LINE_WIDTH_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_19_4.DATA_LINE_WIDTH_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_LINE_WIDTH_ID, Hologram.LINE_WIDTH);
+
+ // background
+ final var DATA_BACKGROUND_COLOR_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_19_4.DATA_BACKGROUND_COLOR_ID.getMapping());
+
+ final var background = textData.getBackground();
+ if (background == null) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, TextDisplay.INITIAL_BACKGROUND);
+ } else if (background == Hologram.TRANSPARENT) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, 0);
+ } else {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, background.asARGB());
+ }
+
+ // text shadow
+ if (textData.hasTextShadow()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SHADOW));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SHADOW));
+ }
+
+ // text alignment
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.LEFT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_LEFT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_LEFT));
+ }
+
+ // see through
+ if (textData.isSeeThrough()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SEE_THROUGH));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SEE_THROUGH));
+ }
+
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.RIGHT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_RIGHT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_RIGHT));
+ }
+
+ } else if (display instanceof Display.ItemDisplay itemDisplay && data instanceof ItemHologramData itemData) {
+ // item
+ itemDisplay.setItemStack(ItemStack.fromBukkitCopy(itemData.getItemStack()));
+
+ } else if (display instanceof Display.BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) {
+ Block block = BuiltInRegistries.BLOCK.get(ResourceLocation.of("minecraft:" + blockData.getBlock().name().toLowerCase(), ':'));
+ blockDisplay.setBlockState(block.defaultBlockState());
+ }
+
+ if (data instanceof DisplayHologramData displayData) {
+ // billboard data
+ display.setBillboardConstraints(switch (displayData.getBillboard()) {
+ case FIXED -> Display.BillboardConstraints.FIXED;
+ case VERTICAL -> Display.BillboardConstraints.VERTICAL;
+ case HORIZONTAL -> Display.BillboardConstraints.HORIZONTAL;
+ case CENTER -> Display.BillboardConstraints.CENTER;
+ });
+
+ // brightness
+ if (displayData.getBrightness() != null) {
+ display.setBrightnessOverride(new Brightness(displayData.getBrightness().getBlockLight(), displayData.getBrightness().getSkyLight()));
+ }
+
+ // entity scale AND MORE!
+ display.setTransformation(new Transformation(
+ displayData.getTranslation(),
+ new Quaternionf(),
+ displayData.getScale(),
+ new Quaternionf())
+ );
+
+ // entity shadow
+ display.setShadowRadius(displayData.getShadowRadius());
+ display.setShadowStrength(displayData.getShadowStrength());
+
+ // view range
+ display.setViewRange(displayData.getVisibilityDistance());
+ }
+ }
+
+
+ @Override
+ public boolean show(@NotNull final Player player) {
+ if (!new HologramShowEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (this.display == null) {
+ create(); // try to create it if it doesn't exist every time
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // could not be created, nothing to show
+ }
+
+ if (!data.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
+ return false;
+ }
+
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+
+ // TODO: cache player protocol version
+ // TODO: fix this
+// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player) : MINIMUM_PROTOCOL_VERSION;
+// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) {
+// return false;
+// }
+
+ serverPlayer.connection.send(new ClientboundAddEntityPacket(display));
+ this.viewers.add(player.getUniqueId());
+ refreshHologram(player);
+
+ return true;
+ }
+
+ @Override
+ public boolean hide(@NotNull final Player player) {
+ if (!new HologramHideEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // doesn't exist, nothing to hide
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundRemoveEntitiesPacket(display.getId()));
+
+ this.viewers.remove(player.getUniqueId());
+ return true;
+ }
+
+
+ @Override
+ public void refresh(@NotNull final Player player) {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to refresh
+ }
+
+ if (!isViewer(player)) {
+ return;
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundTeleportEntityPacket(display));
+
+ if (display instanceof TextDisplay textDisplay) {
+ textDisplay.setText(PaperAdventure.asVanilla(getShownText(player)));
+ }
+
+ final var values = new ArrayList>();
+
+ //noinspection unchecked
+ for (final var item : ((Int2ObjectMap>) getValue(display.getEntityData(), "e")).values()) {
+ values.add(item.value());
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundSetEntityDataPacket(display.getId(), values));
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_19_4.java b/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_19_4.java
new file mode 100644
index 00000000..75115d4a
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_19_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_19_4.java
@@ -0,0 +1,20 @@
+package de.oliver.fancyholograms.hologram.version;
+
+public enum MappingKeys1_19_4 {
+
+ DATA_INTERPOLATION_DURATION_ID("r"),
+ DATA_INTERPOLATION_START_DELTA_TICKS_ID("q"),
+ DATA_LINE_WIDTH_ID("aL"),
+ DATA_BACKGROUND_COLOR_ID("aM"),
+ ;
+
+ private final String mapping;
+
+ MappingKeys1_19_4(String mapping) {
+ this.mapping = mapping;
+ }
+
+ public String getMapping() {
+ return mapping;
+ }
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_1/build.gradle.kts b/plugins/fancyholograms-v2/implementation_1_20_1/build.gradle.kts
new file mode 100644
index 00000000..adc4e1c9
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_1/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("java-library")
+ id("io.papermc.paperweight.userdev")
+}
+
+
+val minecraftVersion = "1.20.1"
+
+
+dependencies {
+ paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT")
+
+ implementation(project(":plugins:fancyholograms-v2:api"))
+ implementation("de.oliver:FancyLib:36")
+ compileOnly("com.viaversion:viaversion-api:5.2.1")
+}
+
+
+tasks {
+ named("assemble") {
+ dependsOn(named("reobfJar"))
+ }
+
+ javadoc {
+ options.encoding = Charsets.UTF_8.name()
+ }
+
+ compileJava {
+ options.encoding = Charsets.UTF_8.name()
+
+ options.release.set(17)
+ }
+}
\ No newline at end of file
diff --git a/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_1.java b/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_1.java
new file mode 100644
index 00000000..d7afb5c9
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_1.java
@@ -0,0 +1,271 @@
+package de.oliver.fancyholograms.hologram.version;
+
+import com.mojang.math.Transformation;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramHideEvent;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancylib.ReflectionUtils;
+import io.papermc.paper.adventure.PaperAdventure;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
+import net.minecraft.network.syncher.EntityDataAccessor;
+import net.minecraft.network.syncher.SynchedEntityData.DataItem;
+import net.minecraft.network.syncher.SynchedEntityData.DataValue;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Brightness;
+import net.minecraft.world.entity.Display;
+import net.minecraft.world.entity.Display.TextDisplay;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.Block;
+import org.bukkit.craftbukkit.v1_20_R1.CraftWorld;
+import org.bukkit.craftbukkit.v1_20_R1.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+
+import java.util.ArrayList;
+
+import static de.oliver.fancylib.ReflectionUtils.getValue;
+
+public final class Hologram1_20_1 extends Hologram {
+
+ @Nullable
+ private Display display;
+
+ public Hologram1_20_1(@NotNull final HologramData data) {
+ super(data);
+ }
+
+ @Override
+ public int getEntityId() {
+ return display.getId();
+ }
+
+ @Override
+ public @Nullable org.bukkit.entity.Display getDisplayEntity() {
+ return display != null ? (org.bukkit.entity.Display) display.getBukkitEntity() : null;
+ }
+
+ @Override
+ public void create() {
+ final var location = data.getLocation();
+ if (!location.isWorldLoaded()) {
+ return; // no location data, cannot be created
+ }
+
+ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle();
+
+ switch (data.getType()) {
+ case TEXT -> this.display = new Display.TextDisplay(EntityType.TEXT_DISPLAY, world);
+ case BLOCK -> this.display = new Display.BlockDisplay(EntityType.BLOCK_DISPLAY, world);
+ case ITEM -> this.display = new Display.ItemDisplay(EntityType.ITEM_DISPLAY, world);
+ }
+
+ if (data instanceof DisplayHologramData dd){
+ final var DATA_INTERPOLATION_DURATION_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_1.DATA_INTERPOLATION_DURATION_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_DURATION_ID, dd.getInterpolationDuration());
+
+ final var DATA_INTERPOLATION_START_DELTA_TICKS_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_1.DATA_INTERPOLATION_START_DELTA_TICKS_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_START_DELTA_TICKS_ID, 0);
+ }
+
+ update();
+ }
+
+ @Override
+ public void delete() {
+ this.display = null;
+ }
+
+ @Override
+ public void update() {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to update
+ }
+
+ // location data
+ final var location = data.getLocation();
+ if (location.getWorld() == null || !location.isWorldLoaded()) {
+ return;
+ } else {
+ display.setPosRaw(location.x(), location.y(), location.z());
+ display.setYRot(location.getYaw());
+ display.setXRot(location.getPitch());
+ }
+
+ if (display instanceof TextDisplay textDisplay && data instanceof TextHologramData textData) {
+ // line width
+ final var DATA_LINE_WIDTH_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_1.DATA_LINE_WIDTH_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_LINE_WIDTH_ID, Hologram.LINE_WIDTH);
+
+ // background
+ final var DATA_BACKGROUND_COLOR_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_1.DATA_BACKGROUND_COLOR_ID.getMapping()); //DATA_BACKGROUND_COLOR_ID
+
+ final var background = textData.getBackground();
+ if (background == null) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, TextDisplay.INITIAL_BACKGROUND);
+ } else if (background == Hologram.TRANSPARENT) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, 0);
+ } else {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, background.asARGB());
+ }
+
+ // text shadow
+ if (textData.hasTextShadow()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SHADOW));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SHADOW));
+ }
+
+ // text alignment
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.LEFT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_LEFT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_LEFT));
+ }
+
+ // see through
+ if (textData.isSeeThrough()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SEE_THROUGH));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SEE_THROUGH));
+ }
+
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.RIGHT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_RIGHT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_RIGHT));
+ }
+
+ } else if (display instanceof Display.ItemDisplay itemDisplay && data instanceof ItemHologramData itemData) {
+ // item
+ itemDisplay.setItemStack(ItemStack.fromBukkitCopy(itemData.getItemStack()));
+
+ } else if (display instanceof Display.BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) {
+ Block block = BuiltInRegistries.BLOCK.get(ResourceLocation.of("minecraft:" + blockData.getBlock().name().toLowerCase(), ':'));
+ blockDisplay.setBlockState(block.defaultBlockState());
+ }
+
+ if (data instanceof DisplayHologramData displayData) {
+ // billboard data
+ display.setBillboardConstraints(switch (displayData.getBillboard()) {
+ case FIXED -> Display.BillboardConstraints.FIXED;
+ case VERTICAL -> Display.BillboardConstraints.VERTICAL;
+ case HORIZONTAL -> Display.BillboardConstraints.HORIZONTAL;
+ case CENTER -> Display.BillboardConstraints.CENTER;
+ });
+
+ // brightness
+ if (displayData.getBrightness() != null) {
+ display.setBrightnessOverride(new Brightness(displayData.getBrightness().getBlockLight(), displayData.getBrightness().getSkyLight()));
+ }
+
+ // entity scale AND MORE!
+ display.setTransformation(new Transformation(
+ displayData.getTranslation(),
+ new Quaternionf(),
+ displayData.getScale(),
+ new Quaternionf())
+ );
+
+ // entity shadow
+ display.setShadowRadius(displayData.getShadowRadius());
+ display.setShadowStrength(displayData.getShadowStrength());
+
+ // view range
+ display.setViewRange(displayData.getVisibilityDistance());
+ }
+ }
+
+
+ @Override
+ public boolean show(@NotNull final Player player) {
+ if (!new HologramShowEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (this.display == null) {
+ create(); // try to create it if it doesn't exist every time
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // could not be created, nothing to show
+ }
+
+ if (!data.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
+ return false;
+ }
+
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+
+ // TODO: cache player protocol version
+ // TODO: fix this
+// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player) : MINIMUM_PROTOCOL_VERSION;
+// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) {
+// return false;
+// }
+
+ serverPlayer.connection.send(new ClientboundAddEntityPacket(display));
+ this.viewers.add(player.getUniqueId());
+ refreshHologram(player);
+
+ return true;
+ }
+
+ @Override
+ public boolean hide(@NotNull final Player player) {
+ if (!new HologramHideEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // doesn't exist, nothing to hide
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundRemoveEntitiesPacket(display.getId()));
+
+ this.viewers.remove(player.getUniqueId());
+ return true;
+ }
+
+
+ @Override
+ public void refresh(@NotNull final Player player) {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to refresh
+ }
+
+ if (!isViewer(player)) {
+ return;
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundTeleportEntityPacket(display));
+
+ if (display instanceof TextDisplay textDisplay) {
+ textDisplay.setText(PaperAdventure.asVanilla(getShownText(player)));
+ }
+
+ final var values = new ArrayList>();
+
+ //noinspection unchecked
+ for (final var item : ((Int2ObjectMap>) getValue(display.getEntityData(), "e")).values()) {
+ values.add(item.value());
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundSetEntityDataPacket(display.getId(), values));
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_1.java b/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_1.java
new file mode 100644
index 00000000..9fffe629
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_1/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_1.java
@@ -0,0 +1,20 @@
+package de.oliver.fancyholograms.hologram.version;
+
+public enum MappingKeys1_20_1 {
+
+ DATA_INTERPOLATION_DURATION_ID("q"),
+ DATA_INTERPOLATION_START_DELTA_TICKS_ID("p"),
+ DATA_LINE_WIDTH_ID("aM"),
+ DATA_BACKGROUND_COLOR_ID("aN"),
+ ;
+
+ private final String mapping;
+
+ MappingKeys1_20_1(String mapping) {
+ this.mapping = mapping;
+ }
+
+ public String getMapping() {
+ return mapping;
+ }
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_2/build.gradle.kts b/plugins/fancyholograms-v2/implementation_1_20_2/build.gradle.kts
new file mode 100644
index 00000000..fc0daa42
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_2/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("java-library")
+ id("io.papermc.paperweight.userdev")
+}
+
+
+val minecraftVersion = "1.20.2"
+
+
+dependencies {
+ paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT")
+
+ implementation(project(":plugins:fancyholograms-v2:api"))
+ implementation("de.oliver:FancyLib:36")
+ compileOnly("com.viaversion:viaversion-api:5.2.1")
+}
+
+
+tasks {
+ named("assemble") {
+ dependsOn(named("reobfJar"))
+ }
+
+ javadoc {
+ options.encoding = Charsets.UTF_8.name()
+ }
+
+ compileJava {
+ options.encoding = Charsets.UTF_8.name()
+
+ options.release.set(17)
+ }
+}
\ No newline at end of file
diff --git a/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_2.java b/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_2.java
new file mode 100644
index 00000000..f7d248fe
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_2.java
@@ -0,0 +1,271 @@
+package de.oliver.fancyholograms.hologram.version;
+
+import com.mojang.math.Transformation;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramHideEvent;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancylib.ReflectionUtils;
+import io.papermc.paper.adventure.PaperAdventure;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
+import net.minecraft.network.syncher.EntityDataAccessor;
+import net.minecraft.network.syncher.SynchedEntityData.DataItem;
+import net.minecraft.network.syncher.SynchedEntityData.DataValue;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Brightness;
+import net.minecraft.world.entity.Display;
+import net.minecraft.world.entity.Display.TextDisplay;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.Block;
+import org.bukkit.craftbukkit.v1_20_R2.CraftWorld;
+import org.bukkit.craftbukkit.v1_20_R2.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+
+import java.util.ArrayList;
+
+import static de.oliver.fancylib.ReflectionUtils.getValue;
+
+public final class Hologram1_20_2 extends Hologram {
+
+ @Nullable
+ private Display display;
+
+ public Hologram1_20_2(@NotNull final HologramData data) {
+ super(data);
+ }
+
+ @Override
+ public int getEntityId() {
+ return display.getId();
+ }
+
+ @Override
+ public @Nullable org.bukkit.entity.Display getDisplayEntity() {
+ return display != null ? (org.bukkit.entity.Display) display.getBukkitEntity() : null;
+ }
+
+ @Override
+ public void create() {
+ final var location = data.getLocation();
+ if (!location.isWorldLoaded()) {
+ return; // no location data, cannot be created
+ }
+
+ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle();
+
+ switch (data.getType()) {
+ case TEXT -> this.display = new Display.TextDisplay(EntityType.TEXT_DISPLAY, world);
+ case BLOCK -> this.display = new Display.BlockDisplay(EntityType.BLOCK_DISPLAY, world);
+ case ITEM -> this.display = new Display.ItemDisplay(EntityType.ITEM_DISPLAY, world);
+ }
+
+ if (data instanceof DisplayHologramData dd) {
+ final var DATA_INTERPOLATION_DURATION_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_2.DATA_INTERPOLATION_DURATION_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_DURATION_ID, dd.getInterpolationDuration());
+
+ final var DATA_INTERPOLATION_START_DELTA_TICKS_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_2.DATA_INTERPOLATION_START_DELTA_TICKS_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_INTERPOLATION_START_DELTA_TICKS_ID, 0);
+ }
+
+ update();
+ }
+
+ @Override
+ public void delete() {
+ this.display = null;
+ }
+
+ @Override
+ public void update() {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to update
+ }
+
+ // location data
+ final var location = data.getLocation();
+ if (location.getWorld() == null || !location.isWorldLoaded()) {
+ return;
+ } else {
+ display.setPosRaw(location.x(), location.y(), location.z());
+ display.setYRot(location.getYaw());
+ display.setXRot(location.getPitch());
+ }
+
+ if (display instanceof TextDisplay textDisplay && data instanceof TextHologramData textData) {
+ // line width
+ final var DATA_LINE_WIDTH_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_2.DATA_LINE_WIDTH_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_LINE_WIDTH_ID, Hologram.LINE_WIDTH);
+
+ // background
+ final var DATA_BACKGROUND_COLOR_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_2.DATA_BACKGROUND_COLOR_ID.getMapping());
+
+ final var background = textData.getBackground();
+ if (background == null) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, TextDisplay.INITIAL_BACKGROUND);
+ } else if (background == Hologram.TRANSPARENT) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, 0);
+ } else {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, background.asARGB());
+ }
+
+ // text shadow
+ if (textData.hasTextShadow()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SHADOW));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SHADOW));
+ }
+
+ // text alignment
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.LEFT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_LEFT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_LEFT));
+ }
+
+ // see through
+ if (textData.isSeeThrough()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SEE_THROUGH));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SEE_THROUGH));
+ }
+
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.RIGHT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_RIGHT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_RIGHT));
+ }
+
+ } else if (display instanceof Display.ItemDisplay itemDisplay && data instanceof ItemHologramData itemData) {
+ // item
+ itemDisplay.setItemStack(ItemStack.fromBukkitCopy(itemData.getItemStack()));
+
+ } else if (display instanceof Display.BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) {
+ Block block = BuiltInRegistries.BLOCK.get(ResourceLocation.of("minecraft:" + blockData.getBlock().name().toLowerCase(), ':'));
+ blockDisplay.setBlockState(block.defaultBlockState());
+ }
+
+ if (data instanceof DisplayHologramData displayData) {
+ // billboard data
+ display.setBillboardConstraints(switch (displayData.getBillboard()) {
+ case FIXED -> Display.BillboardConstraints.FIXED;
+ case VERTICAL -> Display.BillboardConstraints.VERTICAL;
+ case HORIZONTAL -> Display.BillboardConstraints.HORIZONTAL;
+ case CENTER -> Display.BillboardConstraints.CENTER;
+ });
+
+ // brightness
+ if (displayData.getBrightness() != null) {
+ display.setBrightnessOverride(new Brightness(displayData.getBrightness().getBlockLight(), displayData.getBrightness().getSkyLight()));
+ }
+
+ // entity scale AND MORE!
+ display.setTransformation(new Transformation(
+ displayData.getTranslation(),
+ new Quaternionf(),
+ displayData.getScale(),
+ new Quaternionf())
+ );
+
+ // entity shadow
+ display.setShadowRadius(displayData.getShadowRadius());
+ display.setShadowStrength(displayData.getShadowStrength());
+
+ // view range
+ display.setViewRange(displayData.getVisibilityDistance());
+ }
+ }
+
+
+ @Override
+ public boolean show(@NotNull final Player player) {
+ if (!new HologramShowEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (this.display == null) {
+ create(); // try to create it if it doesn't exist every time
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // could not be created, nothing to show
+ }
+
+ if (!data.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
+ return false;
+ }
+
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+
+ // TODO: cache player protocol version
+ // TODO: fix this
+// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player) : MINIMUM_PROTOCOL_VERSION;
+// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) {
+// return false;
+// }
+
+ serverPlayer.connection.send(new ClientboundAddEntityPacket(display));
+ this.viewers.add(player.getUniqueId());
+ refreshHologram(player);
+
+ return true;
+ }
+
+ @Override
+ public boolean hide(@NotNull final Player player) {
+ if (!new HologramHideEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // doesn't exist, nothing to hide
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundRemoveEntitiesPacket(display.getId()));
+
+ this.viewers.remove(player.getUniqueId());
+ return true;
+ }
+
+
+ @Override
+ public void refresh(@NotNull final Player player) {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to refresh
+ }
+
+ if (!isViewer(player)) {
+ return;
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundTeleportEntityPacket(display));
+
+ if (display instanceof TextDisplay textDisplay) {
+ textDisplay.setText(PaperAdventure.asVanilla(getShownText(player)));
+ }
+
+ final var values = new ArrayList>();
+
+ //noinspection unchecked
+ for (final var item : ((Int2ObjectMap>) getValue(display.getEntityData(), "e")).values()) {
+ values.add(item.value());
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundSetEntityDataPacket(display.getId(), values));
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_2.java b/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_2.java
new file mode 100644
index 00000000..59ee90ee
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_2/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_2.java
@@ -0,0 +1,20 @@
+package de.oliver.fancyholograms.hologram.version;
+
+public enum MappingKeys1_20_2 {
+
+ DATA_INTERPOLATION_DURATION_ID("r"),
+ DATA_INTERPOLATION_START_DELTA_TICKS_ID("q"),
+ DATA_LINE_WIDTH_ID("aN"),
+ DATA_BACKGROUND_COLOR_ID("aO"),
+ ;
+
+ private final String mapping;
+
+ MappingKeys1_20_2(String mapping) {
+ this.mapping = mapping;
+ }
+
+ public String getMapping() {
+ return mapping;
+ }
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_4/build.gradle.kts b/plugins/fancyholograms-v2/implementation_1_20_4/build.gradle.kts
new file mode 100644
index 00000000..f75d64a3
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_4/build.gradle.kts
@@ -0,0 +1,33 @@
+plugins {
+ id("java-library")
+ id("io.papermc.paperweight.userdev")
+}
+
+
+val minecraftVersion = "1.20.4"
+
+
+dependencies {
+ paperweight.paperDevBundle("$minecraftVersion-R0.1-SNAPSHOT")
+
+ implementation(project(":plugins:fancyholograms-v2:api"))
+ implementation("de.oliver:FancyLib:36")
+ compileOnly("com.viaversion:viaversion-api:5.2.1")
+}
+
+
+tasks {
+ named("assemble") {
+ dependsOn(named("reobfJar"))
+ }
+
+ javadoc {
+ options.encoding = Charsets.UTF_8.name()
+ }
+
+ compileJava {
+ options.encoding = Charsets.UTF_8.name()
+
+ options.release.set(17)
+ }
+}
\ No newline at end of file
diff --git a/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_4.java b/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_4.java
new file mode 100644
index 00000000..4f1faaea
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/Hologram1_20_4.java
@@ -0,0 +1,272 @@
+package de.oliver.fancyholograms.hologram.version;
+
+import com.mojang.math.Transformation;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramHideEvent;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancylib.ReflectionUtils;
+import io.papermc.paper.adventure.PaperAdventure;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket;
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
+import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket;
+import net.minecraft.network.syncher.EntityDataAccessor;
+import net.minecraft.network.syncher.SynchedEntityData.DataItem;
+import net.minecraft.network.syncher.SynchedEntityData.DataValue;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.level.ServerLevel;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Brightness;
+import net.minecraft.world.entity.Display;
+import net.minecraft.world.entity.Display.TextDisplay;
+import net.minecraft.world.entity.EntityType;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.Block;
+import org.bukkit.craftbukkit.v1_20_R3.CraftWorld;
+import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+
+import java.util.ArrayList;
+
+import static de.oliver.fancylib.ReflectionUtils.getValue;
+
+public final class Hologram1_20_4 extends Hologram {
+
+ @Nullable
+ private Display display;
+
+ public Hologram1_20_4(@NotNull final HologramData data) {
+ super(data);
+ }
+
+ @Override
+ public int getEntityId() {
+ return display.getId();
+ }
+
+ @Override
+ public @Nullable org.bukkit.entity.Display getDisplayEntity() {
+ return display != null ? (org.bukkit.entity.Display) display.getBukkitEntity() : null;
+ }
+
+ @Override
+ public void create() {
+ final var location = data.getLocation();
+ if (!location.isWorldLoaded()) {
+ return; // no location data, cannot be created
+ }
+
+ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle();
+
+ switch (data.getType()) {
+ case TEXT -> this.display = new Display.TextDisplay(EntityType.TEXT_DISPLAY, world);
+ case BLOCK -> this.display = new Display.BlockDisplay(EntityType.BLOCK_DISPLAY, world);
+ case ITEM -> this.display = new Display.ItemDisplay(EntityType.ITEM_DISPLAY, world);
+ }
+
+ if (data instanceof DisplayHologramData dd) {
+ final var DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_4.DISPLAY__DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID, dd.getInterpolationDuration());
+
+ final var DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID = ReflectionUtils.getStaticValue(Display.class, MappingKeys1_20_4.DISPLAY__DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID, 0);
+ }
+
+ update();
+ }
+
+ @Override
+ public void delete() {
+ this.display = null;
+ }
+
+ @Override
+ public void update() {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to update
+ }
+
+ // location data
+ final var location = data.getLocation();
+ if (location.getWorld() == null || !location.isWorldLoaded()) {
+ return;
+ } else {
+ display.setPosRaw(location.x(), location.y(), location.z());
+ display.setYRot(location.getYaw());
+ display.setXRot(location.getPitch());
+ }
+
+ if (display instanceof TextDisplay textDisplay && data instanceof TextHologramData textData) {
+ // line width
+ final var DATA_LINE_WIDTH_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_4.TEXT_DISPLAY__DATA_LINE_WIDTH_ID.getMapping());
+ display.getEntityData().set((EntityDataAccessor) DATA_LINE_WIDTH_ID, Hologram.LINE_WIDTH);
+
+ // background
+ final var DATA_BACKGROUND_COLOR_ID = ReflectionUtils.getStaticValue(TextDisplay.class, MappingKeys1_20_4.TEXT_DISPLAY__DATA_BACKGROUND_COLOR_ID.getMapping());
+
+ final var background = textData.getBackground();
+ if (background == null) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, TextDisplay.INITIAL_BACKGROUND);
+ } else if (background == Hologram.TRANSPARENT) {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, 0);
+ } else {
+ display.getEntityData().set((EntityDataAccessor) DATA_BACKGROUND_COLOR_ID, background.asARGB());
+ }
+
+ // text shadow
+ if (textData.hasTextShadow()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SHADOW));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SHADOW));
+ }
+
+ // text alignment
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.LEFT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_LEFT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_LEFT));
+ }
+
+ // see through
+ if (textData.isSeeThrough()) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_SEE_THROUGH));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_SEE_THROUGH));
+ }
+
+ if (textData.getTextAlignment() == org.bukkit.entity.TextDisplay.TextAlignment.RIGHT) {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() | TextDisplay.FLAG_ALIGN_RIGHT));
+ } else {
+ textDisplay.setFlags((byte) (textDisplay.getFlags() & ~TextDisplay.FLAG_ALIGN_RIGHT));
+ }
+
+ } else if (display instanceof Display.ItemDisplay itemDisplay && data instanceof ItemHologramData itemData) {
+ // item
+ itemDisplay.setItemStack(ItemStack.fromBukkitCopy(itemData.getItemStack()));
+
+ } else if (display instanceof Display.BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) {
+ Block block = BuiltInRegistries.BLOCK.get(ResourceLocation.of("minecraft:" + blockData.getBlock().name().toLowerCase(), ':'));
+ blockDisplay.setBlockState(block.defaultBlockState());
+ }
+
+ if (data instanceof DisplayHologramData displayData) {
+ // billboard data
+ display.setBillboardConstraints(switch (displayData.getBillboard()) {
+ case FIXED -> Display.BillboardConstraints.FIXED;
+ case VERTICAL -> Display.BillboardConstraints.VERTICAL;
+ case HORIZONTAL -> Display.BillboardConstraints.HORIZONTAL;
+ case CENTER -> Display.BillboardConstraints.CENTER;
+ });
+
+ // brightness
+ if (displayData.getBrightness() != null) {
+ display.setBrightnessOverride(new Brightness(displayData.getBrightness().getBlockLight(), displayData.getBrightness().getSkyLight()));
+ }
+
+ // entity scale AND MORE!
+ display.setTransformation(new Transformation(
+ displayData.getTranslation(),
+ new Quaternionf(),
+ displayData.getScale(),
+ new Quaternionf())
+ );
+
+ // entity shadow
+ display.setShadowRadius(displayData.getShadowRadius());
+ display.setShadowStrength(displayData.getShadowStrength());
+
+ // view range
+ display.setViewRange(displayData.getVisibilityDistance());
+ }
+ }
+
+
+ @Override
+ public boolean show(@NotNull final Player player) {
+ if (!new HologramShowEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (this.display == null) {
+ create(); // try to create it if it doesn't exist every time
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // could not be created, nothing to show
+ }
+
+ if (!data.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
+ return false;
+ }
+
+ ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
+
+ // TODO: cache player protocol version
+ // TODO: fix this
+// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player.getUniqueId()) : MINIMUM_PROTOCOL_VERSION;
+// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) {
+// System.out.println("nope protocol");
+// return false;
+// }
+
+ serverPlayer.connection.send(new ClientboundAddEntityPacket(display));
+ this.viewers.add(player.getUniqueId());
+ refreshHologram(player);
+
+ return true;
+ }
+
+ @Override
+ public boolean hide(@NotNull final Player player) {
+ if (!new HologramHideEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ final var display = this.display;
+ if (display == null) {
+ return false; // doesn't exist, nothing to hide
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundRemoveEntitiesPacket(display.getId()));
+
+ this.viewers.remove(player.getUniqueId());
+ return true;
+ }
+
+
+ @Override
+ public void refresh(@NotNull final Player player) {
+ final var display = this.display;
+ if (display == null) {
+ return; // doesn't exist, nothing to refresh
+ }
+
+ if (!isViewer(player)) {
+ return;
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundTeleportEntityPacket(display));
+
+ if (display instanceof TextDisplay textDisplay) {
+ textDisplay.setText(PaperAdventure.asVanilla(getShownText(player)));
+ }
+
+ final var values = new ArrayList>();
+
+ //noinspection unchecked
+ for (final var item : ((Int2ObjectMap>) getValue(display.getEntityData(), "e")).values()) {
+ values.add(item.value());
+ }
+
+ ((CraftPlayer) player).getHandle().connection.send(new ClientboundSetEntityDataPacket(display.getId(), values));
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_4.java b/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_4.java
new file mode 100644
index 00000000..f9736289
--- /dev/null
+++ b/plugins/fancyholograms-v2/implementation_1_20_4/src/main/java/de/oliver/fancyholograms/hologram/version/MappingKeys1_20_4.java
@@ -0,0 +1,20 @@
+package de.oliver.fancyholograms.hologram.version;
+
+public enum MappingKeys1_20_4 {
+
+ DISPLAY__DATA_TRANSFORMATION_INTERPOLATION_DURATION_ID("r"),
+ DISPLAY__DATA_TRANSFORMATION_INTERPOLATION_START_DELTA_TICKS_ID("q"),
+ TEXT_DISPLAY__DATA_LINE_WIDTH_ID("aN"),
+ TEXT_DISPLAY__DATA_BACKGROUND_COLOR_ID("aO"),
+ ;
+
+ private final String mapping;
+
+ MappingKeys1_20_4(String mapping) {
+ this.mapping = mapping;
+ }
+
+ public String getMapping() {
+ return mapping;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FHFeatureFlags.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FHFeatureFlags.java
new file mode 100644
index 00000000..e5b4a05b
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FHFeatureFlags.java
@@ -0,0 +1,16 @@
+package de.oliver.fancyholograms;
+
+import de.oliver.fancylib.featureFlags.FeatureFlag;
+import de.oliver.fancylib.featureFlags.FeatureFlagConfig;
+
+public class FHFeatureFlags {
+
+ public static final FeatureFlag DISABLE_HOLOGRAMS_FOR_BEDROCK_PLAYERS = new FeatureFlag("disable-holograms-for-bedrock-players", "Do not show holograms to bedrock players", false);
+
+ public static void load() {
+ FeatureFlagConfig config = new FeatureFlagConfig(FancyHolograms.get());
+ config.addFeatureFlag(DISABLE_HOLOGRAMS_FOR_BEDROCK_PLAYERS);
+ config.load();
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHolograms.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHolograms.java
new file mode 100644
index 00000000..541f7f53
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHolograms.java
@@ -0,0 +1,387 @@
+package de.oliver.fancyholograms;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import de.oliver.fancyanalytics.api.FancyAnalyticsAPI;
+import de.oliver.fancyanalytics.api.metrics.MetricSupplier;
+import de.oliver.fancyanalytics.logger.ExtendedFancyLogger;
+import de.oliver.fancyanalytics.logger.LogLevel;
+import de.oliver.fancyanalytics.logger.appender.Appender;
+import de.oliver.fancyanalytics.logger.appender.ConsoleAppender;
+import de.oliver.fancyanalytics.logger.appender.JsonAppender;
+import de.oliver.fancyholograms.api.FancyHologramsPlugin;
+import de.oliver.fancyholograms.api.HologramConfiguration;
+import de.oliver.fancyholograms.api.HologramManager;
+import de.oliver.fancyholograms.api.HologramStorage;
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.FancyHologramsCMD;
+import de.oliver.fancyholograms.commands.FancyHologramsTestCMD;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.hologram.version.*;
+import de.oliver.fancyholograms.listeners.BedrockPlayerListener;
+import de.oliver.fancyholograms.listeners.NpcListener;
+import de.oliver.fancyholograms.listeners.PlayerListener;
+import de.oliver.fancyholograms.listeners.WorldListener;
+import de.oliver.fancyholograms.storage.FlatFileHologramStorage;
+import de.oliver.fancyholograms.storage.converter.FHConversionRegistry;
+import de.oliver.fancyholograms.util.PluginUtils;
+import de.oliver.fancylib.FancyLib;
+import de.oliver.fancylib.Metrics;
+import de.oliver.fancylib.VersionConfig;
+import de.oliver.fancylib.serverSoftware.ServerSoftware;
+import de.oliver.fancylib.versionFetcher.MasterVersionFetcher;
+import de.oliver.fancylib.versionFetcher.VersionFetcher;
+import de.oliver.fancysitula.api.IFancySitula;
+import de.oliver.fancysitula.api.utils.ServerVersion;
+import org.apache.maven.artifact.versioning.ComparableVersion;
+import org.bukkit.Bukkit;
+import org.bukkit.command.Command;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import static java.util.concurrent.CompletableFuture.supplyAsync;
+
+public final class FancyHolograms extends JavaPlugin implements FancyHologramsPlugin {
+
+ private static @Nullable FancyHolograms INSTANCE;
+ private final ExtendedFancyLogger fancyLogger;
+ private final VersionFetcher versionFetcher = new MasterVersionFetcher("FancyHolograms");
+ private final VersionConfig versionConfig = new VersionConfig(this, versionFetcher);
+ private final ScheduledExecutorService hologramThread = Executors.newSingleThreadScheduledExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat("FancyHolograms-Holograms")
+ .build()
+ );
+ private final ExecutorService fileStorageExecutor = Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setDaemon(true)
+ .setPriority(Thread.MIN_PRIORITY + 1)
+ .setNameFormat("FancyHolograms-FileStorageExecutor")
+ .build()
+ );
+ private FancyAnalyticsAPI fancyAnalytics;
+ private HologramConfiguration configuration = new FancyHologramsConfiguration();
+ private HologramStorage hologramStorage = new FlatFileHologramStorage();
+ private @Nullable HologramManagerImpl hologramsManager;
+
+ public FancyHolograms() {
+ INSTANCE = this;
+
+ Appender consoleAppender = new ConsoleAppender("[{loggerName}] ({threadName}) {logLevel}: {message}");
+ String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date(System.currentTimeMillis()));
+ File logsFile = new File("plugins/FancyHolograms/logs/FH-logs-" + date + ".txt");
+ if (!logsFile.exists()) {
+ try {
+ logsFile.getParentFile().mkdirs();
+ logsFile.createNewFile();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ JsonAppender jsonAppender = new JsonAppender(false, false, true, logsFile.getPath());
+ this.fancyLogger = new ExtendedFancyLogger("FancyHolograms", LogLevel.INFO, List.of(consoleAppender, jsonAppender), new ArrayList<>());
+ }
+
+ public static @NotNull FancyHolograms get() {
+ return Objects.requireNonNull(INSTANCE, "plugin is not initialized");
+ }
+
+ public static boolean canGet() {
+ return INSTANCE != null;
+ }
+
+ @Override
+ public void onLoad() {
+ final var adapter = resolveHologramAdapter();
+
+ if (adapter == null) {
+ List supportedVersions = new ArrayList<>(List.of("1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4"));
+ supportedVersions.addAll(ServerVersion.getSupportedVersions());
+
+ fancyLogger.warn("""
+ --------------------------------------------------
+ Unsupported minecraft server version.
+ Please update the server to one of (%s).
+ Disabling the FancyHolograms plugin.
+ --------------------------------------------------
+ """.formatted(String.join(" / ", supportedVersions)));
+ Bukkit.getPluginManager().disablePlugin(this);
+ return;
+ }
+
+ hologramsManager = new HologramManagerImpl(this, adapter);
+
+ fancyLogger.info("Successfully loaded FancyHolograms version %s".formatted(getDescription().getVersion()));
+ }
+
+ @Override
+ public void onEnable() {
+ getHologramConfiguration().reload(this); // initialize configuration
+
+ new FancyLib(INSTANCE); // initialize FancyLib
+
+ if (!ServerSoftware.isPaper()) {
+ fancyLogger.warn("""
+ --------------------------------------------------
+ It is recommended to use Paper as server software.
+ Because you are not using paper, the plugin
+ might not work correctly.
+ --------------------------------------------------
+ """);
+ }
+
+ LogLevel logLevel;
+ try {
+ logLevel = LogLevel.valueOf(getHologramConfiguration().getLogLevel());
+ } catch (IllegalArgumentException e) {
+ logLevel = LogLevel.INFO;
+ }
+ fancyLogger.setCurrentLevel(logLevel);
+ IFancySitula.LOGGER.setCurrentLevel(logLevel);
+
+ FHFeatureFlags.load();
+
+ reloadCommands();
+
+ registerListeners();
+
+ versionConfig.load();
+ if (!getHologramConfiguration().areVersionNotificationsMuted()) {
+ checkForNewerVersion();
+ }
+
+ registerMetrics();
+
+ getHologramsManager().initializeTasks();
+
+ if (getHologramConfiguration().isAutosaveEnabled()) {
+ getHologramThread().scheduleAtFixedRate(() -> {
+ if (hologramsManager != null) {
+ hologramsManager.saveHolograms();
+ }
+ }, getHologramConfiguration().getAutosaveInterval(), getHologramConfiguration().getAutosaveInterval() * 60L, TimeUnit.SECONDS);
+ }
+
+ FHConversionRegistry.registerBuiltInConverters();
+
+ fancyLogger.info("Successfully enabled FancyHolograms version %s".formatted(getDescription().getVersion()));
+ }
+
+ @Override
+ public void onDisable() {
+ hologramsManager.saveHolograms();
+ hologramThread.shutdown();
+ fileStorageExecutor.shutdown();
+ INSTANCE = null;
+
+ fancyLogger.info("Successfully disabled FancyHolograms version %s".formatted(getDescription().getVersion()));
+ }
+
+ @Override
+ public JavaPlugin getPlugin() {
+ return INSTANCE;
+ }
+
+ @Override
+ public ExtendedFancyLogger getFancyLogger() {
+ return fancyLogger;
+ }
+
+ public @NotNull VersionFetcher getVersionFetcher() {
+ return versionFetcher;
+ }
+
+ public @NotNull VersionConfig getVersionConfig() {
+ return versionConfig;
+ }
+
+ @ApiStatus.Internal
+ public @NotNull HologramManagerImpl getHologramsManager() {
+ return Objects.requireNonNull(this.hologramsManager, "plugin is not initialized");
+ }
+
+ @Override
+ public HologramManager getHologramManager() {
+ return Objects.requireNonNull(this.hologramsManager, "plugin is not initialized");
+ }
+
+ @Override
+ public HologramConfiguration getHologramConfiguration() {
+ return configuration;
+ }
+
+ @Override
+ public void setHologramConfiguration(HologramConfiguration configuration, boolean reload) {
+ this.configuration = configuration;
+
+ if (reload) {
+ configuration.reload(this);
+ reloadCommands();
+ }
+ }
+
+ @Override
+ public HologramStorage getHologramStorage() {
+ return hologramStorage;
+ }
+
+ @Override
+ public void setHologramStorage(HologramStorage storage, boolean reload) {
+ this.hologramStorage = storage;
+
+ if (reload) {
+ getHologramsManager().reloadHolograms();
+ }
+ }
+
+ public ScheduledExecutorService getHologramThread() {
+ return hologramThread;
+ }
+
+ public ExecutorService getFileStorageExecutor() {
+ return this.fileStorageExecutor;
+ }
+
+ private @Nullable Function resolveHologramAdapter() {
+ final var version = Bukkit.getMinecraftVersion();
+
+ // check if the server version is supported by FancySitula
+ if (ServerVersion.isVersionSupported(version)) {
+ return HologramImpl::new;
+ }
+
+ return switch (version) {
+ case "1.20.3", "1.20.4" -> Hologram1_20_4::new;
+ case "1.20.2" -> Hologram1_20_2::new;
+ case "1.20", "1.20.1" -> Hologram1_20_1::new;
+ case "1.19.4" -> Hologram1_19_4::new;
+ default -> null;
+ };
+ }
+
+ public void reloadCommands() {
+ Collection commands = Arrays.asList(new HologramCMD(this), new FancyHologramsCMD(this));
+
+ if (getHologramConfiguration().isRegisterCommands()) {
+ commands.forEach(command -> getServer().getCommandMap().register("fancyholograms", command));
+ } else {
+ commands.stream().filter(Command::isRegistered).forEach(command ->
+ command.unregister(getServer().getCommandMap()));
+ }
+
+ if (false) {
+ FancyHologramsTestCMD fancyHologramsTestCMD = new FancyHologramsTestCMD(this);
+ getServer().getCommandMap().register("fancyholograms", fancyHologramsTestCMD);
+ }
+ }
+
+ private void registerListeners() {
+ getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
+ getServer().getPluginManager().registerEvents(new WorldListener(), this);
+
+ if (PluginUtils.isFancyNpcsEnabled()) {
+ getServer().getPluginManager().registerEvents(new NpcListener(this), this);
+ }
+
+ if (FHFeatureFlags.DISABLE_HOLOGRAMS_FOR_BEDROCK_PLAYERS.isEnabled() && PluginUtils.isFloodgateEnabled()) {
+ getServer().getPluginManager().registerEvents(new BedrockPlayerListener(), this);
+ }
+ }
+
+ private void checkForNewerVersion() {
+ final var current = new ComparableVersion(versionConfig.getVersion());
+
+ supplyAsync(getVersionFetcher()::fetchNewestVersion).thenApply(Objects::requireNonNull).whenComplete((newest, error) -> {
+ if (error != null || newest.compareTo(current) <= 0) {
+ return; // could not get the newest version or already on latest
+ }
+
+ fancyLogger.warn("""
+
+ -------------------------------------------------------
+ You are not using the latest version of the FancyHolograms plugin.
+ Please update to the newest version (%s).
+ %s
+ -------------------------------------------------------
+ """.formatted(newest, getVersionFetcher().getDownloadUrl()));
+ });
+ }
+
+ private void registerMetrics() {
+ boolean isDevelopmentBuild = !versionConfig.getBuild().equalsIgnoreCase("undefined");
+
+ Metrics metrics = new Metrics(this, 17990);
+ metrics.addCustomChart(new Metrics.SingleLineChart("total_holograms", () -> hologramsManager.getHolograms().size()));
+ metrics.addCustomChart(new Metrics.SimplePie("update_notifications", () -> configuration.areVersionNotificationsMuted() ? "No" : "Yes"));
+ metrics.addCustomChart(new Metrics.SimplePie("using_development_build", () -> isDevelopmentBuild ? "Yes" : "No"));
+
+ fancyAnalytics = new FancyAnalyticsAPI("3b77bd59-2b01-46f2-b3aa-a9584401797f", "E2gW5zc2ZTk1OGFkNGY2ZDQ0ODlM6San");
+ fancyAnalytics.getConfig().setDisableLogging(true);
+
+ if (!isDevelopmentBuild) {
+ return;
+ }
+
+ fancyAnalytics.registerMinecraftPluginMetrics(INSTANCE);
+ fancyAnalytics.getExceptionHandler().registerLogger(getLogger());
+ fancyAnalytics.getExceptionHandler().registerLogger(Bukkit.getLogger());
+ fancyAnalytics.getExceptionHandler().registerLogger(fancyLogger);
+
+ fancyAnalytics.registerStringMetric(new MetricSupplier<>("commit_hash", () -> versionConfig.getHash().substring(0, 7)));
+
+ fancyAnalytics.registerStringMetric(new MetricSupplier<>("server_size", () -> {
+ long onlinePlayers = Bukkit.getOnlinePlayers().size();
+
+ if (onlinePlayers == 0) {
+ return "empty";
+ }
+
+ if (onlinePlayers <= 25) {
+ return "small";
+ }
+
+ if (onlinePlayers <= 100) {
+ return "medium";
+ }
+
+ if (onlinePlayers <= 500) {
+ return "large";
+ }
+
+ return "very_large";
+ }));
+
+ fancyAnalytics.registerNumberMetric(new MetricSupplier<>("amount_holograms", () -> (double) hologramsManager.getHolograms().size()));
+ fancyAnalytics.registerStringMetric(new MetricSupplier<>("enabled_update_notifications", () -> configuration.areVersionNotificationsMuted() ? "false" : "true"));
+ fancyAnalytics.registerStringMetric(new MetricSupplier<>("fflag_disable_holograms_for_bedrock_players", () -> FHFeatureFlags.DISABLE_HOLOGRAMS_FOR_BEDROCK_PLAYERS.isEnabled() ? "true" : "false"));
+ fancyAnalytics.registerStringMetric(new MetricSupplier<>("using_development_build", () -> isDevelopmentBuild ? "true" : "false"));
+
+ fancyAnalytics.registerStringArrayMetric(new MetricSupplier<>("hologram_type", () -> {
+ if (hologramsManager == null) {
+ return new String[0];
+ }
+
+ return hologramsManager.getHolograms().stream()
+ .map(h -> h.getData().getType().name())
+ .toArray(String[]::new);
+ }));
+
+
+ fancyAnalytics.initialize();
+ }
+
+ public FancyAnalyticsAPI getFancyAnalytics() {
+ return fancyAnalytics;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHologramsConfiguration.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHologramsConfiguration.java
new file mode 100644
index 00000000..aae64303
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/FancyHologramsConfiguration.java
@@ -0,0 +1,139 @@
+package de.oliver.fancyholograms;
+
+import de.oliver.fancyholograms.api.FancyHologramsPlugin;
+import de.oliver.fancyholograms.api.HologramConfiguration;
+import de.oliver.fancylib.ConfigHelper;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * The FancyHologramsConfig class is responsible for managing the configuration of the FancyHolograms plugin.
+ * It handles loading and saving hologram data, as well as providing access to various configuration settings.
+ */
+public final class FancyHologramsConfiguration implements HologramConfiguration {
+
+ /**
+ * Indicates whether version notifications are muted.
+ */
+ private boolean versionNotifsMuted;
+
+ /**
+ * Indicates whether autosave is enabled.
+ */
+ private boolean autosaveEnabled;
+
+ /**
+ * The interval at which autosave is performed.
+ */
+ private int autosaveInterval;
+
+ /**
+ * Indicates whether the plugin should save holograms when they are changed.
+ */
+ private boolean saveOnChangedEnabled;
+
+ /**
+ * The default visibility distance for holograms.
+ */
+ private int defaultVisibilityDistance;
+
+ /**
+ * Indicates whether commands should be registered.
+ *
+ * This is useful for users who want to use the plugin's API only.
+ */
+ private boolean registerCommands;
+
+ /**
+ * The log level for the plugin.
+ */
+ private String logLevel;
+
+ /**
+ * The interval at which hologram visibility is updated.
+ */
+ private int updateVisibilityInterval;
+
+ @Override
+ public void reload(@NotNull FancyHologramsPlugin plugin) {
+ FancyHolograms pluginImpl = (FancyHolograms) plugin;
+ pluginImpl.reloadConfig();
+
+ final var config = pluginImpl.getConfig();
+
+ versionNotifsMuted = (boolean) ConfigHelper.getOrDefault(config, "mute_version_notification", false);
+ config.setInlineComments("mute_version_notification", List.of("Whether version notifications are muted."));
+
+ autosaveEnabled = (boolean) ConfigHelper.getOrDefault(config, "enable_autosave", true);
+ config.setInlineComments("enable_autosave", List.of("Whether autosave is enabled."));
+
+ autosaveInterval = (int) ConfigHelper.getOrDefault(config, "autosave_interval", 15);
+ config.setInlineComments("autosave_interval", List.of("The interval at which autosave is performed in minutes."));
+
+ saveOnChangedEnabled = (boolean) ConfigHelper.getOrDefault(config, "save_on_changed", true);
+ config.setInlineComments("save_on_changed", List.of("Whether the plugin should save holograms when they are changed."));
+
+ defaultVisibilityDistance = (int) ConfigHelper.getOrDefault(config, "visibility_distance", 20);
+ config.setInlineComments("visibility_distance", List.of("The default visibility distance for holograms."));
+
+ registerCommands = (boolean) ConfigHelper.getOrDefault(config, "register_commands", true);
+ config.setInlineComments("register_commands", List.of("Whether the plugin should register its commands."));
+
+ config.set("report_errors_to_sentry", null);
+ config.setInlineComments("report_errors_to_sentry", null);
+
+ config.setInlineComments("log_level", List.of("The log level for the plugin (DEBUG, INFO, WARN, ERROR)."));
+ logLevel = (String) ConfigHelper.getOrDefault(config, "log_level", "INFO");
+
+ updateVisibilityInterval = (int) ConfigHelper.getOrDefault(config, "update_visibility_interval", 20);
+ config.setInlineComments("update_visibility_interval", List.of("The interval at which hologram visibility is updated in ticks."));
+
+ if (pluginImpl.isEnabled()) {
+ plugin.getHologramThread().submit(pluginImpl::saveConfig);
+ } else {
+ // Can't dispatch task if plugin is disabled
+ pluginImpl.saveConfig();
+ }
+ }
+
+ @Override
+ public boolean areVersionNotificationsMuted() {
+ return versionNotifsMuted;
+ }
+
+ @Override
+ public boolean isAutosaveEnabled() {
+ return autosaveEnabled;
+ }
+
+ @Override
+ public int getAutosaveInterval() {
+ return autosaveInterval;
+ }
+
+ @Override
+ public boolean isSaveOnChangedEnabled() {
+ return saveOnChangedEnabled;
+ }
+
+ @Override
+ public int getDefaultVisibilityDistance() {
+ return defaultVisibilityDistance;
+ }
+
+ @Override
+ public boolean isRegisterCommands() {
+ return registerCommands;
+ }
+
+ @Override
+ public String getLogLevel() {
+ return logLevel;
+ }
+
+ @Override
+ public int getUpdateVisibilityInterval() {
+ return updateVisibilityInterval;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/HologramManagerImpl.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/HologramManagerImpl.java
new file mode 100644
index 00000000..8db467c3
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/HologramManagerImpl.java
@@ -0,0 +1,313 @@
+package de.oliver.fancyholograms;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableList;
+import de.oliver.fancyholograms.api.HologramManager;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramsLoadedEvent;
+import de.oliver.fancyholograms.api.events.HologramsUnloadedEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancynpcs.api.FancyNpcsPlugin;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.UnmodifiableView;
+import org.joml.Vector3f;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+/**
+ * The FancyHologramsManager class is responsible for managing holograms in the FancyHolograms plugin.
+ * It provides methods for adding, removing, and retrieving holograms, as well as other related operations.
+ */
+public final class HologramManagerImpl implements HologramManager {
+
+ private final @NotNull FancyHolograms plugin;
+ /**
+ * The adapter function used to create holograms from hologram data.
+ */
+ private final @NotNull Function adapter;
+ /**
+ * A map of hologram names to their corresponding hologram instances.
+ */
+ private final Map holograms = new ConcurrentHashMap<>();
+ /**
+ * Whether holograms are loaded or not
+ */
+ private boolean isLoaded = false;
+
+ HologramManagerImpl(@NotNull final FancyHolograms plugin, @NotNull final Function adapter) {
+ this.plugin = plugin;
+ this.adapter = adapter;
+ }
+
+ /**
+ * @return A read-only collection of loaded holograms.
+ */
+ @Override
+ public @NotNull
+ @UnmodifiableView Collection getHolograms() {
+ return Collections.unmodifiableCollection(this.holograms.values());
+ }
+
+ /**
+ * Returns a read-only view of the currently loaded persistent holograms.
+ *
+ * @return A read-only collection of holograms.
+ */
+ @Override
+ public @NotNull
+ @UnmodifiableView Collection getPersistentHolograms() {
+ return this.holograms.values().stream().filter(hologram -> hologram.getData().isPersistent()).toList();
+ }
+
+
+ /**
+ * Finds a hologram by name.
+ *
+ * @param name The name of the hologram to lookup.
+ * @return An optional containing the found hologram, or empty if not found.
+ */
+ public @NotNull Optional getHologram(@NotNull final String name) {
+ return Optional.ofNullable(this.holograms.get(name.toLowerCase(Locale.ROOT)));
+ }
+
+ /**
+ * Adds a hologram to this manager.
+ *
+ * @param hologram The hologram to add.
+ */
+ public void addHologram(@NotNull final Hologram hologram) {
+ this.holograms.put(hologram.getData().getName().toLowerCase(Locale.ROOT), hologram);
+ }
+
+ /**
+ * Removes a hologram from this manager.
+ *
+ * @param hologram The hologram to remove.
+ */
+ public void removeHologram(@NotNull final Hologram hologram) {
+ removeHologram(hologram.getData().getName());
+ }
+
+ /**
+ * Removes a hologram from this manager by name.
+ *
+ * @param name The name of the hologram to remove.
+ * @return An optional containing the removed hologram, or empty if not found.
+ */
+ public @NotNull Optional removeHologram(@NotNull final String name) {
+ Optional optionalHologram = Optional.ofNullable(this.holograms.remove(name.toLowerCase(Locale.ROOT)));
+
+ optionalHologram.ifPresent(hologram -> {
+ for (UUID viewer : hologram.getViewers()) {
+ Player player = Bukkit.getPlayer(viewer);
+ if (player != null) {
+ FancyHolograms.get().getHologramThread().submit(() -> hologram.forceHideHologram(player));
+ }
+ }
+
+ FancyHolograms.get().getHologramThread().submit(() -> plugin.getHologramStorage().delete(hologram));
+ }
+ );
+
+ return optionalHologram;
+ }
+
+ /**
+ * Creates a new hologram with the specified hologram data.
+ *
+ * @param data The hologram data for the new hologram.
+ * @return The created hologram.
+ */
+ public @NotNull Hologram create(@NotNull final HologramData data) {
+ Hologram hologram = this.adapter.apply(data);
+ hologram.createHologram();
+ return hologram;
+ }
+
+ public void saveHolograms() {
+ if (!isLoaded) {
+ return;
+ }
+
+ plugin.getHologramStorage().saveBatch(getPersistentHolograms(), false);
+ }
+
+ @Override
+ public void loadHolograms() {
+ List allLoaded = new ArrayList<>();
+
+ for (World world : Bukkit.getWorlds()) {
+ Collection loaded = plugin.getHologramStorage().loadAll(world.getName());
+ loaded.forEach(this::addHologram);
+
+ allLoaded.addAll(loaded);
+ }
+ isLoaded = true;
+
+ FancyHolograms.get().getHologramThread().submit(() -> Bukkit.getPluginManager().callEvent(new HologramsLoadedEvent(ImmutableList.copyOf(allLoaded))));
+
+ FancyHolograms.get().getFancyLogger().info(String.format("Loaded %d holograms for all loaded worlds", allLoaded.size()));
+ }
+
+ public void loadHolograms(String world) {
+ ImmutableList loaded = ImmutableList.copyOf(plugin.getHologramStorage().loadAll(world));
+ loaded.forEach(this::addHologram);
+
+ isLoaded = true;
+
+ Bukkit.getPluginManager().callEvent(new HologramsLoadedEvent(ImmutableList.copyOf(loaded)));
+
+ FancyHolograms.get().getFancyLogger().info(String.format("Loaded %d holograms for world %s", loaded.size(), world));
+ }
+
+ /**
+ * Initializes tasks for managing holograms, such as loading and refreshing them.
+ *
+ * This method is intended to be called internally by the plugin.
+ */
+ void initializeTasks() {
+ ScheduledExecutorService hologramThread = plugin.getHologramThread();
+ hologramThread.submit(() -> {
+ loadHolograms();
+
+ hologramThread.scheduleAtFixedRate(() -> {
+ for (final Hologram hologram : this.plugin.getHologramsManager().getHolograms()) {
+ for (final Player player : Bukkit.getOnlinePlayers()) {
+ hologram.forceUpdateShownStateFor(player);
+ }
+ }
+ }, 0, plugin.getHologramConfiguration().getUpdateVisibilityInterval() * 50, TimeUnit.MILLISECONDS);
+ });
+
+ final var updateTimes = CacheBuilder.newBuilder()
+ .expireAfterAccess(Duration.ofMinutes(5))
+ .build();
+
+ hologramThread.scheduleAtFixedRate(() -> {
+ final var time = System.currentTimeMillis();
+
+ for (final var hologram : getHolograms()) {
+ HologramData data = hologram.getData();
+ if (data.hasChanges()) {
+ hologram.forceUpdate();
+ hologram.refreshForViewersInWorld();
+ data.setHasChanges(false);
+
+ if (data instanceof TextHologramData) {
+ updateTimes.put(hologram.getData().getName(), time);
+ }
+ }
+ }
+ }, 50, 1000, TimeUnit.MILLISECONDS);
+
+ hologramThread.scheduleWithFixedDelay(() -> {
+ final var time = System.currentTimeMillis();
+
+ for (final var hologram : getHolograms()) {
+ if (hologram.getData() instanceof TextHologramData textData) {
+ final var interval = textData.getTextUpdateInterval();
+ if (interval < 1) {
+ continue; // doesn't update
+ }
+
+ final var lastUpdate = updateTimes.asMap().get(textData.getName());
+ if (lastUpdate != null && time < (lastUpdate + interval)) {
+ continue;
+ }
+
+ if (lastUpdate == null || time > (lastUpdate + interval)) {
+ hologram.refreshForViewersInWorld();
+ updateTimes.put(textData.getName(), time);
+ }
+ }
+ }
+ }, 50, 50, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Reloads holograms by clearing the existing holograms and loading them again from the plugin's configuration.
+ */
+ public void reloadHolograms() {
+ unloadHolograms();
+ loadHolograms();
+ }
+
+ public void unloadHolograms() {
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ List unloaded = new ArrayList<>();
+
+ for (final var hologram : this.getPersistentHolograms()) {
+ this.holograms.remove(hologram.getName());
+ unloaded.add(hologram);
+
+ for (UUID viewer : hologram.getViewers()) {
+ Player player = Bukkit.getPlayer(viewer);
+ if (player != null) {
+ hologram.forceHideHologram(player);
+ }
+ }
+ }
+
+ Bukkit.getPluginManager().callEvent(new HologramsUnloadedEvent(ImmutableList.copyOf(unloaded)));
+ });
+ }
+
+ public void unloadHolograms(String world) {
+ final var online = List.copyOf(Bukkit.getOnlinePlayers());
+
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ List h = getPersistentHolograms().stream()
+ .filter(hologram -> hologram.getData().getLocation().getWorld().getName().equals(world))
+ .toList();
+
+ FancyHolograms.get().getHologramStorage().saveBatch(h, false);
+
+ for (final Hologram hologram : h) {
+ this.holograms.remove(hologram.getName());
+ online.forEach(hologram::forceHideHologram);
+ }
+
+ Bukkit.getPluginManager().callEvent(new HologramsUnloadedEvent(ImmutableList.copyOf(h)));
+ });
+ }
+
+ /**
+ * Syncs a hologram with its linked NPC, if any.
+ *
+ * @param hologram The hologram to sync.
+ */
+ public void syncHologramWithNpc(@NotNull final Hologram hologram) {
+ final var linkedNpcName = hologram.getData().getLinkedNpcName();
+ if (linkedNpcName == null) {
+ return;
+ }
+
+ final var npc = FancyNpcsPlugin.get().getNpcManager().getNpc(linkedNpcName);
+ if (npc == null) {
+ return;
+ }
+
+ npc.getData().setDisplayName("");
+ npc.getData().setShowInTab(false);
+ npc.updateForAll();
+
+ final var npcScale = npc.getData().getScale();
+
+ if (hologram.getData() instanceof DisplayHologramData displayData) {
+ displayData.setScale(new Vector3f(npcScale));
+ }
+
+ final var location = npc.getData().getLocation().clone().add(0, (npc.getEyeHeight() * npcScale) + (0.5 * npcScale), 0);
+ hologram.getData().setLocation(location);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsCMD.java
new file mode 100644
index 00000000..13648770
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsCMD.java
@@ -0,0 +1,139 @@
+package de.oliver.fancyholograms.commands;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.storage.converter.ConverterTarget;
+import de.oliver.fancyholograms.storage.converter.FHConversionRegistry;
+import de.oliver.fancyholograms.storage.converter.HologramConversionSession;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancylib.MessageHelper;
+import de.oliver.fancylib.translations.message.Message;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+public final class FancyHologramsCMD extends Command {
+
+ @NotNull
+ private final FancyHolograms plugin;
+
+ public FancyHologramsCMD(@NotNull final FancyHolograms plugin) {
+ super("fancyholograms");
+ setPermission("fancyholograms.admin");
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) {
+ if (!testPermission(sender)) {
+ return false;
+ }
+
+ if (args.length < 1) {
+ MessageHelper.info(sender, Constants.FH_COMMAND_USAGE);
+ return false;
+ }
+
+ switch (args[0].toLowerCase(Locale.ROOT)) {
+ case "save" -> {
+ this.plugin.getHologramsManager().saveHolograms();
+ MessageHelper.success(sender, "Saved all holograms");
+ }
+ case "reload" -> {
+ this.plugin.getHologramConfiguration().reload(plugin);
+ this.plugin.getHologramsManager().reloadHolograms();
+ this.plugin.reloadCommands();
+
+ MessageHelper.success(sender, "Reloaded config and holograms");
+ }
+ case "version" -> {
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ FancyHolograms.get().getVersionConfig().checkVersionAndDisplay(sender, false);
+ });
+ }
+ case "convert" -> {
+ if (args.length < 3) {
+ MessageHelper.info(sender, "Usage: /fancyholograms convert [args...]");
+ return false;
+ }
+
+ final String converterId = args[1];
+ FHConversionRegistry.getConverterById(converterId)
+ .ifPresentOrElse((converter) -> {
+ final String[] converterArgs = Arrays.asList(args)
+ .subList(2, args.length)
+ .toArray(String[]::new);
+
+ final ConverterTarget target = ConverterTarget.ofStringNullable(args[2]);
+
+ if (target == null) {
+ MessageHelper.error(sender, "Invalid regex for your conversion target!");
+ return;
+ }
+
+ final HologramConversionSession session = new HologramConversionSession(target, sender, converterArgs);
+
+ try {
+ final List holograms = converter.convert(session);
+
+ for (final HologramData data : holograms) {
+ final Hologram hologram = this.plugin.getHologramsManager().create(data);
+ this.plugin.getHologramsManager().addHologram(hologram);
+ }
+
+ this.plugin.getHologramsManager().saveHolograms();
+ // TODO(matt): Give options to delete them or teleport and a list of IDs please
+
+ MessageHelper.success(sender, String.format("Converted successfully, produced %s total holograms!", holograms.size()));
+ } catch (Exception error) {
+ MessageHelper.error(sender, error.getMessage());
+ }
+ }, () -> MessageHelper.error(sender, "That converter is not registered. Look at the developer documentation if you are adding converters."));
+ }
+ default -> {
+ MessageHelper.info(sender, Constants.FH_COMMAND_USAGE);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) throws IllegalArgumentException {
+ if (args.length < 1) {
+ return Collections.emptyList();
+ }
+
+ List suggestions = new ArrayList<>();
+
+ if (args.length == 1) {
+ suggestions.addAll(Arrays.asList("version", "reload", "save", "convert"));
+ } else {
+ if (Objects.equals(args[0], "convert")) {
+
+ if (args.length == 2) {
+ suggestions.addAll(FHConversionRegistry.getAllUsableConverterIds());
+ } else if (args.length == 3) {
+ final String converterId = args[1];
+ FHConversionRegistry.getConverterById(converterId)
+ .ifPresent((converter) -> {
+ suggestions.addAll(converter.getConvertableHolograms());
+ suggestions.add("*");
+ });
+ }
+ }
+ }
+
+ String lastArgument = args[args.length - 1];
+
+ return suggestions.stream()
+ .filter(alias -> alias.startsWith(lastArgument.toLowerCase(Locale.ROOT)))
+ .toList();
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsTestCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsTestCMD.java
new file mode 100644
index 00000000..12dbc22f
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/FancyHologramsTestCMD.java
@@ -0,0 +1,103 @@
+package de.oliver.fancyholograms.commands;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Color;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.TextDisplay;
+import org.jetbrains.annotations.NotNull;
+import org.joml.Vector3f;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class FancyHologramsTestCMD extends Command {
+
+ @NotNull
+ private final FancyHolograms plugin;
+
+ public FancyHologramsTestCMD(@NotNull final FancyHolograms plugin) {
+ super("FancyHologramsTest");
+ setPermission("fancyholograms.admin");
+ this.plugin = plugin;
+ }
+
+ @Override
+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
+ if (args.length == 1) {
+ return Arrays.asList("spawn100", "test1");
+ }
+
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) {
+ if (!testPermission(sender)) {
+ return false;
+ }
+
+ if (!(sender instanceof Player p)) {
+ MessageHelper.error(sender, "Only players can use this command!");
+ return false;
+ }
+
+ if (args.length == 1 && args[0].equalsIgnoreCase("spawn100")) {
+ for (int i = 0; i < 10; i++) {
+ for (int j = 0; j < 10; j++) {
+ int n = (i * 10 + j) + 1;
+ TextHologramData textData = new TextHologramData("holo-" + n, p.getLocation().clone().add(5 * i + 1, 0, 5 * j + 1));
+ textData.setText(Arrays.asList(
+ "This is a test hologram! (#" + n + ")",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris."
+ ));
+ textData.setTextUpdateInterval(100)
+ .setScale(new Vector3f(.5f, .5f, .5f))
+ .setVisibilityDistance(100);
+
+ Hologram hologram = this.plugin.getHologramsManager().create(textData);
+ hologram.createHologram();
+ hologram.updateShownStateFor(p);
+ }
+ }
+
+ return true;
+ } else if (args.length == 1 && args[0].equalsIgnoreCase("test1")) {
+ TextHologramData textData = new TextHologramData("holo-test1", p.getLocation());
+ textData.setText(Arrays.asList(
+ "This is a test hologram!",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris.",
+ "Lorem ipsum dolor sit amet, consec tetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris."
+ ))
+ .setTextUpdateInterval(100)
+ .setTextAlignment(TextDisplay.TextAlignment.CENTER)
+ .setBackground(Color.fromARGB(15, 78, 237, 176))
+ .setTextShadow(true)
+ .setScale(new Vector3f(2, 2, 2))
+ .setBillboard(Display.Billboard.CENTER)
+ .setBrightness(new Display.Brightness(15, 15))
+ .setShadowRadius(3)
+ .setShadowStrength(3)
+ .setVisibilityDistance(100);
+
+ Hologram hologram = this.plugin.getHologramsManager().create(textData);
+ hologram.createHologram();
+ hologram.updateShownStateFor(p);
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/HologramCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/HologramCMD.java
new file mode 100644
index 00000000..6e0821a3
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/HologramCMD.java
@@ -0,0 +1,350 @@
+package de.oliver.fancyholograms.commands;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.hologram.HologramType;
+import de.oliver.fancyholograms.commands.hologram.*;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancyholograms.util.PluginUtils;
+import de.oliver.fancylib.MessageHelper;
+import de.oliver.fancynpcs.api.FancyNpcsPlugin;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Material;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.TextDisplay;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+public final class HologramCMD extends Command {
+
+ @NotNull
+ private final FancyHolograms plugin;
+
+ public HologramCMD(@NotNull final FancyHolograms plugin) {
+ super("hologram", "Main command for the FancyHolograms plugin", "/hologram help", List.of("holograms", "holo", "fholo"));
+
+ setPermission("fancyholograms.admin");
+
+ this.plugin = plugin;
+ }
+
+ public static boolean callModificationEvent(@NotNull final Hologram hologram, @NotNull final CommandSender player, @NotNull final HologramData updatedData, @NotNull final HologramUpdateEvent.HologramModification modification) {
+ final var result = new HologramUpdateEvent(hologram, player, updatedData, modification).callEvent();
+
+ if (!result) {
+ MessageHelper.error(player, "Cancelled hologram modification");
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean execute(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) {
+ if (!testPermission(sender)) {
+ return false;
+ }
+
+ if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
+ MessageHelper.info(sender, Constants.HELP_TEXT + (!PluginUtils.isFancyNpcsEnabled() ? "" : "\n" + Constants.HELP_TEXT_NPCS));
+ return false;
+ }
+
+
+ if (args[0].equalsIgnoreCase("list")) {
+ return new ListCMD().run(sender, null, args);
+ }
+
+
+ if (args.length < 2) {
+ MessageHelper.error(sender, "Wrong usage: /hologram help");
+ return false;
+ }
+
+
+ if (args[0].equalsIgnoreCase("create")) {
+ return new CreateCMD().run(sender, null, args);
+ }
+
+ if (args[0].equalsIgnoreCase("nearby")) {
+ return new NearbyCMD().run(sender, null, args);
+ }
+
+ final var hologram = this.plugin.getHologramsManager().getHologram(args[1]).orElse(null);
+ if (hologram == null) {
+ MessageHelper.error(sender, "Could not find hologram: '" + args[1] + "'");
+ return false;
+ }
+
+
+ return switch (args[0].toLowerCase(Locale.ROOT)) {
+ case "info" -> new InfoCMD().run(sender, hologram, args);
+ case "remove" -> new RemoveCMD().run(sender, hologram, args);
+ case "teleport" -> new TeleportCMD().run(sender, hologram, args);
+ case "copy" -> new CopyCMD().run(sender, hologram, args);
+ case "edit" -> {
+ if (args.length < 3) {
+ MessageHelper.error(sender, "Wrong usage: /hologram help");
+ yield false;
+ }
+
+ final var updated = edit(sender, hologram, args);
+
+ if (updated) {
+ if (sender instanceof Player p) {
+ hologram.forceUpdate();
+ hologram.refreshHologram(p);
+ }
+ hologram.queueUpdate();
+ }
+
+ yield updated;
+ }
+ default -> false;
+ };
+ }
+
+ @Override
+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) throws IllegalArgumentException {
+ if (args.length == 0) {
+ return Collections.emptyList();
+ }
+
+ // /holo {tab:action}
+ if (args.length == 1) {
+ return Stream.of("help", "list", "teleport", "create", "remove", "edit", "copy", "info", "nearby").filter(input -> input.startsWith(args[0].toLowerCase(Locale.ROOT))).toList();
+ }
+
+ // /holo create {tab:type}
+ if (args.length == 2 && args[0].equalsIgnoreCase("create")) {
+ return Arrays.asList("text", "item", "block");
+ }
+
+ // /holo [action] {tab:hologram}
+ if (args.length == 2) {
+ final var action = args[0].toLowerCase(Locale.ROOT);
+
+ if (!Set.of("teleport", "remove", "edit", "copy", "info").contains(action)) {
+ return Collections.emptyList();
+ }
+
+ return this.plugin.getHologramsManager().getPersistentHolograms().stream().map(hologram -> hologram.getData().getName()).filter(input -> input.toLowerCase().startsWith(args[1].toLowerCase(Locale.ROOT))).toList();
+ }
+
+ final var hologram = this.plugin.getHologramsManager().getHologram(args[1]).orElse(null);
+ if (hologram == null) {
+ return Collections.emptyList();
+ }
+
+ HologramType type = hologram.getData().getType();
+
+ // /holo edit [hologram] {tab:option}
+ if (args.length == 3) {
+ if (!args[0].equalsIgnoreCase("edit")) {
+ return Collections.emptyList();
+ }
+
+ final var usingNpcs = PluginUtils.isFancyNpcsEnabled();
+
+ List suggestions = new ArrayList<>(Arrays.asList("position", "moveHere", "center", "moveTo", "rotate", "rotatepitch", "billboard", "scale", "translate", "visibilityDistance", "visibility", "shadowRadius", "shadowStrength", "brightness", usingNpcs ? "linkWithNpc" : "", usingNpcs ? "unlinkWithNpc" : ""));
+ suggestions.addAll(type.getCommands());
+
+ return suggestions.stream().filter(input -> input.toLowerCase().startsWith(args[2].toLowerCase(Locale.ROOT))).toList();
+ }
+
+ if (!args[0].equalsIgnoreCase("edit")) {
+ return Collections.emptyList();
+ }
+
+ // /holo edit [hologram] [option] {tab:contextual}
+ if (args.length == 4) {
+ final var suggestions = switch (args[2].toLowerCase(Locale.ROOT)) {
+ case "billboard" -> {
+ final var values = new ArrayList<>(List.of(Display.Billboard.values()));
+
+ if (hologram.getData() instanceof DisplayHologramData displayData) {
+ values.remove(displayData.getBillboard());
+ }
+
+ yield values.stream().map(Enum::name);
+ }
+ case "background" -> {
+ TextHologramData textData = (TextHologramData) hologram.getData();
+ final var colors = new ArrayList<>(NamedTextColor.NAMES.keys());
+
+ colors.add("reset");
+ colors.add("default");
+ colors.add("transparent");
+
+ final var current = textData.getBackground();
+
+ if (current == null) {
+ colors.remove("reset");
+ colors.remove("default");
+ } else if (current == Hologram.TRANSPARENT) {
+ colors.remove("transparent");
+ } else {
+ NamedTextColor named = current.getAlpha() == 255 ? NamedTextColor.namedColor(current.asRGB()) : null;
+ colors.add(named != null ? named.toString() : '#' + Integer.toHexString(current.asARGB()));
+ }
+
+ yield colors.stream();
+ }
+ case "textshadow" -> {
+ TextHologramData textData = (TextHologramData) hologram.getData();
+ yield Stream.of(!textData.hasTextShadow()).map(Object::toString);
+ }
+ case "brightness" -> Stream.of("block", "sky");
+ case "textalignment" -> Arrays.stream(TextDisplay.TextAlignment.values()).map(Enum::name);
+ case "setline", "removeline" -> {
+ TextHologramData textData = (TextHologramData) hologram.getData();
+ yield IntStream.range(1, textData.getText().size() + 1).mapToObj(Integer::toString);
+ }
+ case "linkwithnpc" -> {
+ if (!PluginUtils.isFancyNpcsEnabled()) {
+ yield Stream.empty();
+ }
+
+ yield FancyNpcsPlugin.get().getNpcManager().getAllNpcs().stream().map(npc -> npc.getData().getName());
+ }
+ case "block" -> Arrays.stream(Material.values()).filter(Material::isBlock).map(Enum::name);
+ case "seethrough" -> Stream.of("true", "false");
+ case "visibility" -> new VisibilityCMD().tabcompletion(sender, hologram, args).stream();
+
+ default -> null;
+ };
+
+ if (suggestions != null) {
+ return suggestions.filter(input -> input.toLowerCase().startsWith(args[3].toLowerCase(Locale.ROOT))).toList();
+ }
+ }
+
+ // /holo edit [hologram] setline [number] {tab:line_text}
+ if (args[2].equalsIgnoreCase("setline")) {
+ TextHologramData textData = (TextHologramData) hologram.getData();
+
+ final var index = Ints.tryParse(args[3]);
+ if (index == null || index < 1 || index > textData.getText().size()) {
+ return Collections.emptyList();
+ }
+
+ return List.of(textData.getText().get(index - 1));
+ }
+
+ // /holo edit [hologram] moveto {tab:x} {tab:y} {tab:z}
+ if (args[2].equalsIgnoreCase("moveto")) {
+ if (!(sender instanceof Player player)) {
+ return Collections.emptyList();
+ }
+
+ final var suggestions = new ArrayList();
+ suggestions.add("~");
+ suggestions.add("~~");
+
+ if (args.length == 7) {
+ suggestions.add(String.valueOf(player.getLocation().getYaw()));
+ }
+
+ if (args.length == 8) {
+ suggestions.add(String.valueOf(player.getLocation().getPitch()));
+ }
+
+ final var target = player.getTargetBlockExact(10);
+ if (target != null) {
+ final var coordinate = switch (args.length) {
+ case 4 -> target.getX();
+ case 5 -> target.getY();
+ case 6 -> target.getZ();
+ default -> null;
+ };
+
+ suggestions.add(String.valueOf(coordinate));
+ }
+
+ return suggestions;
+ }
+
+ if(args[2].equalsIgnoreCase("brightness")) {
+ if(args.length == 4) {
+ return List.of("block", "sky");
+ }
+
+ if(args.length > 5) {
+ return Collections.emptyList();
+ }
+
+ return List.of("0", "5", "10", "15");
+ }
+
+ return Collections.emptyList();
+ }
+
+ private boolean edit(@NotNull final CommandSender player, @NotNull final Hologram hologram, @NotNull final String[] args) {
+ final var action = args[2].toLowerCase();
+
+ // actions without a data
+ switch (action) {
+ case "position", "movehere" -> {
+ return new MoveHereCMD().run(player, hologram, args);
+ }
+ case "center" -> {
+ return new CenterCMD().run(player, hologram, args);
+ }
+ case "unlinkwithnpc" -> {
+ return new UnlinkWithNpcCMD().run(player, hologram, args);
+ }
+ case "item" -> {
+ return new ItemCMD().run(player, hologram, args);
+ }
+ }
+
+ if (args.length == 3) {
+ MessageHelper.error(player, "Wrong usage: /hologram help");
+ return false;
+ }
+
+ return switch (action) {
+ // display data
+ case "moveto" -> new MoveToCMD().run(player, hologram, args);
+ case "rotate" -> new RotateCMD().run(player, hologram, args);
+ case "rotatepitch" -> new RotatePitchCMD().run(player, hologram, args);
+ case "billboard" -> new BillboardCMD().run(player, hologram, args);
+ case "scale" -> new ScaleCMD().run(player, hologram, args);
+ case "translate" -> new TranslateCommand().run(player, hologram, args);
+ case "updatetextinterval" -> new UpdateTextIntervalCMD().run(player, hologram, args);
+ case "visibilitydistance" -> new VisibilityDistanceCMD().run(player, hologram, args);
+ case "visibility" -> new VisibilityCMD().run(player, hologram, args);
+ case "linkwithnpc" -> new LinkWithNpcCMD().run(player, hologram, args);
+ case "shadowradius" -> new ShadowRadiusCMD().run(player, hologram, args);
+ case "shadowstrength" -> new ShadowStrengthCMD().run(player, hologram, args);
+ case "brightness" -> new BrightnessCMD().run(player, hologram, args);
+
+ // text data
+ case "background" -> new BackgroundCMD().run(player, hologram, args);
+ case "addline" -> new AddLineCMD().run(player, hologram, args);
+ case "setline" -> new SetLineCMD().run(player, hologram, args);
+ case "removeline" -> new RemoveLineCMD().run(player, hologram, args);
+ case "insertbefore" -> new InsertBeforeCMD().run(player, hologram, args);
+ case "insertafter" -> new InsertAfterCMD().run(player, hologram, args);
+ case "textshadow" -> new TextShadowCMD().run(player, hologram, args);
+ case "textalignment" -> new TextAlignmentCMD().run(player, hologram, args);
+ case "seethrough" -> new SeeThroughCMD().run(player, hologram, args);
+
+ // block data
+ case "block" -> new BlockCMD().run(player, hologram, args);
+
+ default -> false;
+ };
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/Subcommand.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/Subcommand.java
new file mode 100644
index 00000000..edd4985d
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/Subcommand.java
@@ -0,0 +1,16 @@
+package de.oliver.fancyholograms.commands;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public interface Subcommand {
+
+ List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args);
+
+ boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args);
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/AddLineCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/AddLineCMD.java
new file mode 100644
index 00000000..4563b8d6
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/AddLineCMD.java
@@ -0,0 +1,40 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class AddLineCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ if (!(player.hasPermission("fancyholograms.hologram.edit.line.add"))) {
+ MessageHelper.error(player, "You don't have the required permission to add a line to this hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ String text = "";
+ for (int i = 3; i < args.length; i++) {
+ text += args[i] + " ";
+ }
+ text = text.substring(0, text.length() - 1);
+
+ return SetLineCMD.setLine(player, hologram, Integer.MAX_VALUE, text);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BackgroundCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BackgroundCMD.java
new file mode 100644
index 00000000..1c3a9f5f
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BackgroundCMD.java
@@ -0,0 +1,91 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.Color;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+public class BackgroundCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ if (!(player.hasPermission("fancyholograms.hologram.edit.background"))) {
+ MessageHelper.error(player, "You don't have the required permission to chnage the background of a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+
+ final var color = args[3].toLowerCase(Locale.ROOT);
+
+ final Color background;
+
+ if (color.equals("reset") || color.equals("default")) {
+ background = null;
+ } else {
+ if (color.equals("transparent")) {
+ background = Hologram.TRANSPARENT;
+ } else if (color.startsWith("#")) {
+ Color parsed = Color.fromARGB((int) Long.parseLong(color.substring(1), 16));
+ //make background solid color if RGB hex provided
+ if (color.length() == 7) background = parsed.setAlpha(255);
+ else background = parsed;
+ } else {
+ NamedTextColor named = NamedTextColor.NAMES.value(color.replace(' ', '_'));
+ background = named == null ? null : Color.fromARGB(named.value() | 0xC8000000);
+ }
+
+ if (background == null) {
+ MessageHelper.error(player, "Could not parse background color");
+ return false;
+ }
+ }
+
+ if (Objects.equals(background, textData.getBackground())) {
+ MessageHelper.warning(player, "This hologram already has this background color");
+ return false;
+ }
+
+ final var copied = textData.copy(textData.getName());
+ copied.setBackground(background);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.BACKGROUND)) {
+ return false;
+ }
+
+ if (Objects.equals(copied.getBackground(), textData.getBackground())) {
+ MessageHelper.warning(player, "This hologram already has this background color");
+ return false;
+ }
+
+ textData.setBackground(copied.getBackground());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed background color");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BillboardCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BillboardCMD.java
new file mode 100644
index 00000000..b76762e5
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BillboardCMD.java
@@ -0,0 +1,73 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.base.Enums;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Display;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class BillboardCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.billboard"))) {
+ MessageHelper.error(player, "You don't have the required permission to change the billboard of a hologram");
+ return false;
+ }
+
+ final var billboard = Enums.getIfPresent(Display.Billboard.class, args[3].toUpperCase(Locale.ROOT)).orNull();
+
+ if (billboard == null) {
+ MessageHelper.error(player, "Could not parse billboard");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if (billboard == displayData.getBillboard()) {
+ MessageHelper.warning(player, "This billboard is already set");
+ return false;
+ }
+
+ final var copied = displayData.copy(displayData.getName());
+ copied.setBillboard(billboard);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.BILLBOARD)) {
+ return false;
+ }
+
+ if (copied.getBillboard() == displayData.getBillboard()) {
+ MessageHelper.warning(player, "This billboard is already set");
+ return false;
+ }
+
+ displayData.setBillboard(copied.getBillboard());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed the billboard to " + StringUtils.capitalize(billboard.name().toLowerCase(Locale.ROOT)));
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BlockCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BlockCMD.java
new file mode 100644
index 00000000..a26269a3
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BlockCMD.java
@@ -0,0 +1,68 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.BlockHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Material;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class BlockCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.block"))) {
+ MessageHelper.error(player, "You don't have the required permission to change the block of this hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof BlockHologramData blockData)) {
+ MessageHelper.error(player, "This command can only be used on item holograms");
+ return false;
+ }
+ Material block = Material.getMaterial(args[3]);
+ if (block == null) {
+ MessageHelper.error(player, "Could not find block type");
+ return false;
+ }
+
+ if (block == blockData.getBlock()) {
+ MessageHelper.warning(player, "This block is already set");
+ return false;
+ }
+
+ final var copied = blockData.copy(blockData.getName());
+ copied.setBlock(block);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.BILLBOARD)) {
+ return false;
+ }
+
+ if (copied.getBlock() == blockData.getBlock()) {
+ MessageHelper.warning(player, "This block is already set");
+ return false;
+ }
+
+ blockData.setBlock(block);
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Set block to '" + block.name() + "'");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BrightnessCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BrightnessCMD.java
new file mode 100644
index 00000000..688054b0
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/BrightnessCMD.java
@@ -0,0 +1,71 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.NumberHelper;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Display;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class BrightnessCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if(args.length < 5) {
+ MessageHelper.error(player, "You must provide a brightness type and value.");
+ return false;
+ }
+
+ final var brightnessType = args[3];
+
+ if(!brightnessType.equalsIgnoreCase("block") && !brightnessType.equalsIgnoreCase("sky")) {
+ MessageHelper.error(player, "Invalid brightness type, valid options are BLOCK or SKY");
+ return false;
+ }
+
+ final var parsedNumber = NumberHelper.parseInt(args[4]);
+
+ if(parsedNumber.isEmpty()) {
+ MessageHelper.error(player, "Invalid brightness value.");
+ return false;
+ }
+
+ final var brightnessValue = parsedNumber.get();
+
+ if(brightnessValue < 0 || brightnessValue > 15) {
+ MessageHelper.error(player, "Invalid brightness value, must be between 0 and 15");
+ return false;
+ }
+
+ final var currentBrightness = displayData.getBrightness();
+ final var blockBrightness = brightnessType.equalsIgnoreCase("block") ? brightnessValue :
+ currentBrightness == null ? 0 : currentBrightness.getBlockLight();
+ final var skyBrightness = brightnessType.equalsIgnoreCase("sky") ? brightnessValue :
+ currentBrightness == null ? 0 : currentBrightness.getSkyLight();
+
+ displayData.setBrightness(new Display.Brightness(blockBrightness, skyBrightness));
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed " + brightnessType.toLowerCase() + " brightness to " + brightnessValue);
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CenterCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CenterCMD.java
new file mode 100644
index 00000000..83f97cf2
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CenterCMD.java
@@ -0,0 +1,52 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class CenterCMD implements Subcommand {
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.center"))) {
+ MessageHelper.error(player, "You don't have the required permission to center a hologram");
+ return false;
+ }
+
+ Location location = hologram.getData().getLocation();
+
+ location.set(
+ Math.floor(location.x()) + 0.5,
+ location.y(),
+ Math.floor(location.z()) + 0.5
+ );
+
+ hologram.getData().setLocation(location);
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Centered the hologram to %s/%s/%s %s\u00B0 %s\u00B0".formatted(
+ Constants.COORDINATES_DECIMAL_FORMAT.format(location.x()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format(location.y()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format(location.z()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format((location.getYaw() + 180f) % 360f),
+ Constants.COORDINATES_DECIMAL_FORMAT.format((location.getPitch()) % 360f)
+ ));
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CopyCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CopyCMD.java
new file mode 100644
index 00000000..e1e7117d
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CopyCMD.java
@@ -0,0 +1,83 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.events.HologramCreateEvent;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class CopyCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+ if (!(sender.hasPermission("fancyholograms.hologram.copy"))) {
+ MessageHelper.error(sender, "You don't have the required permission to clone a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+
+ if (args.length < 3) {
+ MessageHelper.error(sender, "Wrong usage: /hologram help");
+ return false;
+ }
+
+ String name = args[2];
+
+ if (FancyHolograms.get().getHologramsManager().getHologram(name).isPresent()) {
+ MessageHelper.error(sender, "There already exists a hologram with this name");
+ return false;
+ }
+
+ if (name.contains(".")) {
+ MessageHelper.error(sender, "The name of the hologram cannot contain a dot");
+ return false;
+ }
+
+ final var data = hologram.getData().copy(name);
+ Location originalLocation = data.getLocation();
+ Location location = player.getLocation();
+ location.setPitch(originalLocation.getPitch());
+ location.setYaw(originalLocation.getYaw());
+ data.setLocation(location);
+
+ final var copy = FancyHolograms.get().getHologramsManager().create(data);
+
+ if (!new HologramCreateEvent(copy, player).callEvent()) {
+ MessageHelper.error(sender, "Creating the copied hologram was cancelled");
+ return false;
+ }
+
+ copy.createHologram();
+ for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
+ copy.updateShownStateFor(onlinePlayer);
+ }
+
+ FancyHolograms.get().getHologramsManager().addHologram(copy);
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(sender, "Copied the hologram");
+ return true;
+
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CreateCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CreateCMD.java
new file mode 100644
index 00000000..d85ccf65
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/CreateCMD.java
@@ -0,0 +1,90 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.hologram.HologramType;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramCreateEvent;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Display;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class CreateCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.create"))) {
+ MessageHelper.error(sender, "You don't have the required permission to create a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+ if (args.length < 3) {
+ MessageHelper.error(player, "Wrong usage: /hologram help");
+ return false;
+ }
+
+ HologramType type = HologramType.getByName(args[1]);
+ if (type == null) {
+ MessageHelper.error(player, "Could not find type: " + args[1]);
+ return false;
+ }
+
+ String name = args[2];
+
+ if (FancyHolograms.get().getHologramsManager().getHologram(name).isPresent()) {
+ MessageHelper.error(player, "There already exists a hologram with this name");
+ return false;
+ }
+
+ if (name.contains(".")) {
+ MessageHelper.error(player, "The name of the hologram cannot contain a dot");
+ return false;
+ }
+
+ DisplayHologramData displayData = null;
+ switch (type) {
+ case TEXT -> displayData = new TextHologramData(name, player.getLocation());
+ case ITEM -> {
+ displayData = new ItemHologramData(name, player.getLocation());
+ displayData.setBillboard(Display.Billboard.FIXED);
+ }
+ case BLOCK -> {
+ displayData = new BlockHologramData(name, player.getLocation());
+ displayData.setBillboard(Display.Billboard.FIXED);
+ }
+ }
+
+ final var holo = FancyHolograms.get().getHologramsManager().create(displayData);
+ if (!new HologramCreateEvent(holo, player).callEvent()) {
+ MessageHelper.error(player, "Creating the hologram was cancelled");
+ return false;
+ }
+
+ holo.createHologram();
+ for (Player onlinePlayer : Bukkit.getOnlinePlayers()) {
+ holo.updateShownStateFor(onlinePlayer);
+ }
+
+ FancyHolograms.get().getHologramsManager().addHologram(holo);
+
+ MessageHelper.success(player, "Created the hologram");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InfoCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InfoCMD.java
new file mode 100644
index 00000000..d147cce8
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InfoCMD.java
@@ -0,0 +1,82 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3f;
+
+import java.util.List;
+
+public class InfoCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.info"))) {
+ MessageHelper.error(player, "You don't have the required permission to view the info of a hologram");
+ return false;
+ }
+
+ HologramData data = hologram.getData();
+
+ MessageHelper.info(player, "Information about the " + hologram.getData().getName() + " hologram:");
+ MessageHelper.info(player, "Name: " + hologram.getData().getName());
+ MessageHelper.info(player, "Type: " + hologram.getData().getType().name());
+ MessageHelper.info(player, "Location: " + data.getLocation().getWorld().getName() + " " + data.getLocation().getX() + " / " + data.getLocation().getY() + " / " + data.getLocation().getZ());
+ MessageHelper.info(player, "Visibility distance: " + data.getVisibilityDistance() + " blocks");
+
+ if (data instanceof DisplayHologramData displayData) {
+ Vector3f scale = displayData.getScale();
+ if (scale.x() == scale.y() && scale.y() == scale.z()) {
+ MessageHelper.info(player, "Scale: x" + displayData.getScale().x());
+ } else {
+ MessageHelper.info(player, "Scale: " + displayData.getScale().x() + ", " + displayData.getScale().y() + ", " + displayData.getScale().z());
+ }
+
+ MessageHelper.info(player, "Billboard: " + displayData.getBillboard().name());
+ MessageHelper.info(player, "Shadow radius: " + displayData.getShadowRadius());
+ MessageHelper.info(player, "Shadow strength: " + displayData.getShadowStrength());
+ }
+
+ if (data.getLinkedNpcName() != null) {
+ MessageHelper.info(player, "Linked npc: " + data.getLinkedNpcName());
+ }
+
+ if (data instanceof TextHologramData textData) {
+ MessageHelper.info(player, "Text: ");
+ for (String line : textData.getText()) {
+ MessageHelper.info(player, " " + line);
+ }
+
+ if (textData.getBackground() != null) {
+ MessageHelper.info(player, "Background: " + '#' + Integer.toHexString(textData.getBackground().asARGB()));
+ } else {
+ MessageHelper.info(player, "Background: default");
+ }
+
+ MessageHelper.info(player, "Text alignment: " + textData.getTextAlignment().name());
+ MessageHelper.info(player, "See through: " + (textData.isSeeThrough() ? "enabled" : "disabled"));
+ MessageHelper.info(player, "Text shadow: " + (textData.hasTextShadow() ? "enabled" : "disabled"));
+ if (textData.getTextUpdateInterval() == -1) {
+ MessageHelper.info(player, "Update text interval: not updating");
+ } else {
+ MessageHelper.info(player, "Update text interval: " + textData.getTextUpdateInterval() + " ticks");
+ }
+ } else if (data instanceof BlockHologramData blockData) {
+ MessageHelper.info(player, "Block: " + blockData.getBlock().name());
+ } else if (data instanceof ItemHologramData itemData) {
+ MessageHelper.info(player, "Item: " + itemData.getItemStack().getType().name());
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertAfterCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertAfterCMD.java
new file mode 100644
index 00000000..7b468434
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertAfterCMD.java
@@ -0,0 +1,80 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InsertAfterCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.insert_after"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ var index = Ints.tryParse(args[3]);
+ if (index == null) {
+ MessageHelper.error(player, "Could not parse line number");
+ return false;
+ }
+
+ if (index < 0) {
+ MessageHelper.error(player, "Invalid line index");
+ return false;
+ }
+
+ String text = "";
+ for (int i = 4; i < args.length; i++) {
+ text += args[i] + " ";
+ }
+
+ if (text.isEmpty()) {
+ MessageHelper.error(player, "You need to provide a text to insert");
+ return true;
+ }
+
+ text = text.substring(0, text.length() - 1);
+
+ final var lines = new ArrayList<>(textData.getText());
+ lines.add(Math.min(index, lines.size()), text);
+
+ final var copied = textData.copy(textData.getName());
+ copied.setText(lines);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TEXT)) {
+ return false;
+ }
+
+ textData.setText(copied.getText());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Inserted line");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertBeforeCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertBeforeCMD.java
new file mode 100644
index 00000000..2affa56d
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/InsertBeforeCMD.java
@@ -0,0 +1,82 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class InsertBeforeCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.insert_before"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ var index = Ints.tryParse(args[3]);
+ if (index == null) {
+ MessageHelper.error(player, "Could not parse line number");
+ return false;
+ }
+
+ index--;
+
+ if (index < 0) {
+ MessageHelper.error(player, "Invalid line index");
+ return false;
+ }
+
+ String text = "";
+ for (int i = 4; i < args.length; i++) {
+ text += args[i] + " ";
+ }
+
+ if (text.isEmpty()) {
+ MessageHelper.error(player, "You need to provide a text to insert");
+ return true;
+ }
+
+ text = text.substring(0, text.length() - 1);
+
+ final var lines = new ArrayList<>(textData.getText());
+ lines.add(Math.min(index, lines.size()), text);
+
+ final var copied = textData.copy(textData.getName());
+ copied.setText(lines);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TEXT)) {
+ return false;
+ }
+
+ textData.setText(copied.getText());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Inserted line");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ItemCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ItemCMD.java
new file mode 100644
index 00000000..3a8323b1
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ItemCMD.java
@@ -0,0 +1,76 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.ItemHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Material;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class ItemCMD implements Subcommand {
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.edit.item"))) {
+ MessageHelper.error(sender, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof ItemHologramData itemData)) {
+ MessageHelper.error(player, "This command can only be used on item holograms");
+ return false;
+ }
+
+ ItemStack item = player.getInventory().getItemInMainHand();
+ if (item.getType() == Material.AIR || item.getAmount() < 1) {
+ MessageHelper.error(player, "You need to hold an item in your hand");
+ return false;
+ }
+
+
+ if (item == itemData.getItemStack()) {
+ MessageHelper.warning(player, "This item is already set");
+ return false;
+ }
+
+ final var copied = itemData.copy(itemData.getName());
+ copied.setItemStack(item);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.BILLBOARD)) {
+ return false;
+ }
+
+ if (copied.getItemStack() == itemData.getItemStack()) {
+ MessageHelper.warning(player, "This item is already set");
+ return false;
+ }
+
+ itemData.setItemStack(item);
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Set the item to '" + item.getType().name() + "'");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/LinkWithNpcCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/LinkWithNpcCMD.java
new file mode 100644
index 00000000..284fe0d4
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/LinkWithNpcCMD.java
@@ -0,0 +1,60 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.PluginUtils;
+import de.oliver.fancylib.MessageHelper;
+import de.oliver.fancynpcs.api.FancyNpcsPlugin;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class LinkWithNpcCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.link"))) {
+ MessageHelper.error(player, "You don't have the required permission to link a hologram");
+ return false;
+ }
+
+ if (!PluginUtils.isFancyNpcsEnabled()) {
+ MessageHelper.warning(player, "You need to install the FancyNpcs plugin for this functionality to work");
+ MessageHelper.warning(player, "Download link: click here.");
+ return false;
+ }
+
+ String name = args[3];
+
+ if (hologram.getData().getLinkedNpcName() != null) {
+ MessageHelper.error(player, "This hologram is already linked with an NPC");
+ return false;
+ }
+
+ final var npc = FancyNpcsPlugin.get().getNpcManager().getNpc(name);
+ if (npc == null) {
+ MessageHelper.error(player, "Could not find NPC with that name");
+ return false;
+ }
+
+ hologram.getData().setLinkedNpcName(npc.getData().getName());
+
+ FancyHolograms.get().getHologramsManager().syncHologramWithNpc(hologram);
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Linked hologram with NPC");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ListCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ListCMD.java
new file mode 100644
index 00000000..bf296ded
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ListCMD.java
@@ -0,0 +1,78 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class ListCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.list"))) {
+ MessageHelper.error(player, "You don't have the required permission to list the holograms");
+ return false;
+ }
+
+ final var holograms = FancyHolograms.get().getHologramsManager().getPersistentHolograms();
+
+ if (holograms.isEmpty()) {
+ MessageHelper.warning(player, "There are no holograms. Use '/hologram create' to create one");
+ } else {
+ int page;
+ if (args.length < 2) {
+ page = 1;
+ } else {
+ final var index = Ints.tryParse(args[1]);
+ if (index == null) {
+ MessageHelper.error(player, "Could not parse page number");
+ return false;
+ }
+ page = index;
+ }
+
+ var pages = holograms.size() / 10 + 1;
+ if (page > pages) {
+ MessageHelper.error(player, "Page %s does not exist".formatted(page));
+ return true;
+ }
+ MessageHelper.info(player, "List of holograms:");
+ MessageHelper.info(player, "Page %s/%s".formatted(page, pages));
+ holograms.stream()
+ .skip((page - 1) * 10)
+ .limit(10)
+ .forEach(holo -> {
+ final var location = holo.getData().getLocation();
+ if (location == null || location.getWorld() == null) {
+ return;
+ }
+
+ MessageHelper.info(player,
+ "Click to teleport'> - %s (%s/%s/%s in %s)"
+ .formatted("/hologram teleport " + holo.getData().getName(),
+ holo.getData().getName(),
+ Constants.DECIMAL_FORMAT.format(location.x()),
+ Constants.DECIMAL_FORMAT.format(location.y()),
+ Constants.DECIMAL_FORMAT.format(location.z()),
+ location.getWorld().getName()
+ ));
+ });
+
+ }
+
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveHereCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveHereCMD.java
new file mode 100644
index 00000000..dd603cc7
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveHereCMD.java
@@ -0,0 +1,94 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Doubles;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancylib.MessageHelper;
+import org.apache.commons.lang3.StringUtils;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.function.Function;
+
+public class MoveHereCMD implements Subcommand {
+
+ public static boolean setLocation(Player player, Hologram hologram, Location location, boolean applyRotation) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.move_here"))) {
+ MessageHelper.error(player, "You don't have the required permission to move a hologram");
+ return false;
+ }
+
+ final var copied = hologram.getData().copy(hologram.getName());
+ final Location newLocation = (applyRotation)
+ ? location
+ : new Location(location.getWorld(), location.x(), location.y(), location.z(), copied.getLocation().getYaw(), copied.getLocation().getPitch());
+ copied.setLocation(newLocation);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.POSITION)) {
+ return false;
+ }
+
+ hologram.getData().setLocation(copied.getLocation());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Moved the hologram to %s/%s/%s %s\u00B0 %s\u00B0".formatted(
+ Constants.COORDINATES_DECIMAL_FORMAT.format(newLocation.x()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format(newLocation.y()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format(newLocation.z()),
+ Constants.COORDINATES_DECIMAL_FORMAT.format((newLocation.getYaw() + 180f) % 360f),
+ Constants.COORDINATES_DECIMAL_FORMAT.format((newLocation.getPitch()) % 360f)
+ ));
+
+ return true;
+ }
+
+ public static @Nullable Double calculateCoordinate(@NotNull final String text, @Nullable final Location originLocation, @NotNull final Location callerLocation, @NotNull final Function extractor) {
+ final var number = Doubles.tryParse(StringUtils.stripStart(text, "~"));
+ final var target = text.startsWith("~~") ? callerLocation : text.startsWith("~") ? originLocation : null;
+
+ if (number == null) {
+ return target == null ? null : extractor.apply(target).doubleValue();
+ }
+
+ if (target == null) {
+ return number;
+ }
+
+ return number + extractor.apply(target).doubleValue();
+ }
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ if (hologram.getData().getLinkedNpcName() != null) {
+ MessageHelper.error(player, "This hologram is linked with an NPC");
+ MessageHelper.error(player, "To unlink: /hologram edit " + hologram.getData().getName() + " unlinkWithNpc");
+ return false;
+ }
+
+ final var location = player.getLocation();
+
+ return setLocation(player, hologram, location, false);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveToCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveToCMD.java
new file mode 100644
index 00000000..f921c580
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/MoveToCMD.java
@@ -0,0 +1,73 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class MoveToCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.edit.move_to"))) {
+ MessageHelper.error(sender, "You don't have the required permission to move a hologram");
+ return false;
+ }
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ if (args.length < 3) {
+ MessageHelper.error(player, "Wrong usage: /hologram help");
+ return false;
+ }
+
+ final var x = MoveHereCMD.calculateCoordinate(args[3], hologram.getData().getLocation(), player.getLocation(), Location::x);
+ final var y = MoveHereCMD.calculateCoordinate(args[4], hologram.getData().getLocation(), player.getLocation(), Location::y);
+ final var z = MoveHereCMD.calculateCoordinate(args[5], hologram.getData().getLocation(), player.getLocation(), Location::z);
+
+ if (x == null || y == null || z == null) {
+ MessageHelper.error(player, "Could not parse position");
+ return false;
+ }
+
+ final var location = new Location(player.getWorld(), x, y, z, hologram.getData().getLocation().getYaw(), hologram.getData().getLocation().getPitch());
+
+ if (args.length > 6) {
+ final var yaw = MoveHereCMD.calculateCoordinate(args[6], hologram.getData().getLocation(), player.getLocation(), loc -> loc.getYaw() + 180f);
+
+ if (yaw == null) {
+ MessageHelper.error(player, "Could not parse yaw");
+ return false;
+ }
+
+ location.setYaw(yaw.floatValue() - 180f);
+ }
+
+ if (args.length > 7) {
+ final var pitch = MoveHereCMD.calculateCoordinate(args[7], hologram.getData().getLocation(), player.getLocation(), Location::getPitch);
+
+ if (pitch == null) {
+ MessageHelper.error(player, "Could not parse pitch");
+ return false;
+ }
+
+ location.setPitch(pitch.floatValue());
+ }
+
+ return MoveHereCMD.setLocation(player, hologram, location, true);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/NearbyCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/NearbyCMD.java
new file mode 100644
index 00000000..bd1868c3
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/NearbyCMD.java
@@ -0,0 +1,94 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.Constants;
+import de.oliver.fancyholograms.util.NumberHelper;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class NearbyCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.nearby"))) {
+ MessageHelper.error(player, "You don't have the required permission to see nearby holograms");
+ return false;
+ }
+
+ if (!(player instanceof Player)) {
+ MessageHelper.error(player, "This is a player only command.");
+ return false;
+ }
+
+
+ if (args.length < 2) {
+ MessageHelper.error(player, Constants.INVALID_NEARBY_RANGE);
+ return false;
+ }
+
+ Optional range = NumberHelper.parseInt(args[1]);
+
+ if (range.isEmpty()) {
+ MessageHelper.error(player, Constants.INVALID_NEARBY_RANGE);
+ return false;
+ }
+
+ Location playerLocation = ((Player) player).getLocation().clone();
+
+ List> nearby = FancyHolograms.get()
+ .getHologramsManager()
+ .getPersistentHolograms()
+ .stream()
+ .filter((holo) -> holo.getData().getLocation().getWorld() == playerLocation.getWorld())
+ .map((holo) -> Map.entry(holo, holo.getData().getLocation().distance(playerLocation)))
+ .filter((entry) -> entry.getValue() <= range.get())
+ .sorted(Comparator.comparingInt(a -> a.getValue().intValue()))
+ .toList();
+
+ if (nearby.isEmpty()) {
+ MessageHelper.error(player, "There are no nearby holograms in a radius of %s blocks.".formatted(range.get()));
+ return true;
+ }
+
+ MessageHelper.info(player, "Holograms nearby (%s radius)".formatted(range.get()));
+ nearby.forEach((entry) -> {
+ Hologram holo = entry.getKey();
+ double distance = entry.getValue();
+
+ final var location = holo.getData().getLocation();
+ if (location == null || location.getWorld() == null) {
+ return;
+ }
+
+ MessageHelper.info(player,
+ "Click to teleport'> - %s (%s/%s/%s in %s, %s blocks away)"
+ .formatted(
+ "/hologram teleport " + holo.getData().getName(),
+ holo.getData().getName(),
+ Constants.DECIMAL_FORMAT.format(location.x()),
+ Constants.DECIMAL_FORMAT.format(location.y()),
+ Constants.DECIMAL_FORMAT.format(location.z()),
+ location.getWorld().getName(),
+ Constants.DECIMAL_FORMAT.format(distance)
+ ));
+ });
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveCMD.java
new file mode 100644
index 00000000..d978ad66
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveCMD.java
@@ -0,0 +1,42 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.FancyHologramsPlugin;
+import de.oliver.fancyholograms.api.events.HologramDeleteEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class RemoveCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.remove"))) {
+ MessageHelper.error(player, "You don't have the required permission to remove a hologram");
+ return false;
+ }
+
+ if (!new HologramDeleteEvent(hologram, player).callEvent()) {
+ MessageHelper.error(player, "Removing the hologram was cancelled");
+ return false;
+ }
+
+ FancyHologramsPlugin.get().getHologramThread().submit(() -> {
+ FancyHolograms.get().getHologramsManager().removeHologram(hologram);
+ MessageHelper.success(player, "Removed the hologram");
+ });
+
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveLineCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveLineCMD.java
new file mode 100644
index 00000000..26b93612
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RemoveLineCMD.java
@@ -0,0 +1,42 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class RemoveLineCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.line.remove"))) {
+ MessageHelper.error(player, "You don't have the required permission to remove a line from a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var index = Ints.tryParse(args[3]);
+ if (index == null) {
+ MessageHelper.error(player, "Could not parse line number");
+ return false;
+ }
+
+ return SetLineCMD.setLine(player, hologram, index - 1, null);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotateCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotateCMD.java
new file mode 100644
index 00000000..ea2be61c
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotateCMD.java
@@ -0,0 +1,40 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class RotateCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.edit.rotate"))) {
+ MessageHelper.error(sender, "You don't have the required permission to rotate a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ final var yaw = MoveHereCMD.calculateCoordinate(args[3], hologram.getData().getLocation(), player.getLocation(), loc -> loc.getYaw() + 180f);
+ Location location = hologram.getData().getLocation().clone();
+ location.setYaw(yaw.floatValue() - 180f);
+
+ return MoveHereCMD.setLocation(player, hologram, location, true);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotatePitchCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotatePitchCMD.java
new file mode 100644
index 00000000..8757be35
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/RotatePitchCMD.java
@@ -0,0 +1,40 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class RotatePitchCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.edit.rotate_pitch"))) {
+ MessageHelper.error(sender, "You don't have the required permission to rotate a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ final var pitch = MoveHereCMD.calculateCoordinate(args[3], hologram.getData().getLocation(), player.getLocation(), loc -> loc.getPitch() - 180f);
+ Location location = hologram.getData().getLocation().clone();
+ location.setPitch(pitch.floatValue());
+
+ return MoveHereCMD.setLocation(player, hologram, location, true);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ScaleCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ScaleCMD.java
new file mode 100644
index 00000000..38612f93
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ScaleCMD.java
@@ -0,0 +1,80 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Floats;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3f;
+
+import java.util.List;
+
+public class ScaleCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.scale"))) {
+ MessageHelper.error(player, "You don't have the required permission to change the scale of a hologram");
+ return false;
+ }
+
+ final var scaleX = Floats.tryParse(args[3]);
+ final var scaleY = args.length >= 6 ? Floats.tryParse(args[4]) : scaleX;
+ final var scaleZ = args.length >= 6 ? Floats.tryParse(args[5]) : scaleX;
+
+ if (scaleX == null || scaleY == null || scaleZ == null) {
+ MessageHelper.error(player, "Could not parse scale");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if (Float.compare(scaleX, displayData.getScale().x()) == 0 &&
+ Float.compare(scaleY, displayData.getScale().y()) == 0 &&
+ Float.compare(scaleZ, displayData.getScale().z()) == 0) {
+ MessageHelper.warning(player, "This hologram is already at this scale");
+ return false;
+ }
+
+ final var copied = displayData.copy(displayData.getName());
+ copied.setScale(new Vector3f(scaleX, scaleY, scaleZ));
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.SCALE)) {
+ return false;
+ }
+
+ if (Float.compare(copied.getScale().x(), displayData.getScale().x()) == 0 &&
+ Float.compare(copied.getScale().y(), displayData.getScale().y()) == 0 &&
+ Float.compare(copied.getScale().z(), displayData.getScale().z()) == 0) {
+ MessageHelper.warning(player, "This hologram is already at this scale");
+ return false;
+ }
+
+ displayData.setScale(new Vector3f(
+ copied.getScale().x(),
+ copied.getScale().y(),
+ copied.getScale().z()));
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed scale to " + scaleX + ", " + scaleY + ", " + scaleZ);
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SeeThroughCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SeeThroughCMD.java
new file mode 100644
index 00000000..a4468440
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SeeThroughCMD.java
@@ -0,0 +1,74 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class SeeThroughCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.see_trough"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var enabled = switch (args[3].toLowerCase(Locale.ROOT)) {
+ case "true" -> true;
+ case "false" -> false;
+ default -> null;
+ };
+
+ if (enabled == null) {
+ MessageHelper.error(player, "Could not parse see through flag");
+ return false;
+ }
+
+ if (enabled == textData.isSeeThrough()) {
+ MessageHelper.warning(player, "This hologram already has see through " + (enabled ? "enabled" : "disabled"));
+ return false;
+ }
+
+ final var copied = textData.copy(textData.getName());
+ copied.setSeeThrough(enabled);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.SEE_THROUGH)) {
+ return false;
+ }
+
+ if (enabled == textData.isSeeThrough()) {
+ MessageHelper.warning(player, "This hologram already has see through " + (enabled ? "enabled" : "disabled"));
+ return false;
+ }
+
+ textData.setSeeThrough(copied.isSeeThrough());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed see through");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SetLineCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SetLineCMD.java
new file mode 100644
index 00000000..a240cc3e
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/SetLineCMD.java
@@ -0,0 +1,87 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SetLineCMD implements Subcommand {
+
+ public static boolean setLine(CommandSender player, Hologram hologram, int index, String text) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.line.set"))) {
+ MessageHelper.error(player, "You don't have the required permission to set a line to this hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var lines = new ArrayList<>(textData.getText());
+
+ if (index >= lines.size()) {
+ lines.add(text == null ? " " : text);
+ } else if (text == null) {
+ lines.remove(index);
+ } else {
+ lines.set(index, text);
+ }
+
+ final var copied = textData.copy(textData.getName());
+ copied.setText(lines);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TEXT)) {
+ return false;
+ }
+
+ textData.setText(copied.getText());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed text for line " + (Math.min(index, lines.size() - 1) + 1));
+ return true;
+ }
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ var index = Ints.tryParse(args[3]);
+ if (index == null) {
+ MessageHelper.error(player, "Could not parse line number");
+ return false;
+ }
+
+ if (index < 0) {
+ MessageHelper.error(player, "Invalid line index");
+ return false;
+ }
+
+ index--;
+
+ String text = "";
+ for (int i = 4; i < args.length; i++) {
+ text += args[i] + " ";
+ }
+ text = text.substring(0, text.length() - 1);
+
+ return setLine(player, hologram, index, text);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowRadiusCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowRadiusCMD.java
new file mode 100644
index 00000000..28105469
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowRadiusCMD.java
@@ -0,0 +1,70 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Floats;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class ShadowRadiusCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.shadow_radius"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ final var radius = Floats.tryParse(args[3]);
+
+ if (radius == null) {
+ MessageHelper.error(player, "Could not parse shadow radius");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if (Float.compare(radius, displayData.getShadowRadius()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this shadow radius");
+ return false;
+ }
+
+ final var copied = displayData.copy(displayData.getName());
+ copied.setShadowRadius(radius);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.SHADOW_RADIUS)) {
+ return false;
+ }
+
+ if (Float.compare(copied.getShadowRadius(), displayData.getShadowRadius()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this shadow radius");
+ return false;
+ }
+
+ displayData.setShadowRadius(copied.getShadowRadius());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed shadow radius");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowStrengthCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowStrengthCMD.java
new file mode 100644
index 00000000..40daac04
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/ShadowStrengthCMD.java
@@ -0,0 +1,70 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Floats;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class ShadowStrengthCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.shadow_strength"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ final var strength = Floats.tryParse(args[3]);
+
+ if (strength == null) {
+ MessageHelper.error(player, "Could not parse shadow strength");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if (Float.compare(strength, displayData.getShadowStrength()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this shadow strength");
+ return false;
+ }
+
+ final var copied = displayData.copy(displayData.getName());
+ copied.setShadowStrength(strength);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.SHADOW_STRENGTH)) {
+ return false;
+ }
+
+ if (Float.compare(copied.getShadowStrength(), displayData.getShadowStrength()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this shadow strength");
+ return false;
+ }
+
+ displayData.setShadowStrength(copied.getShadowStrength());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed shadow strength");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TeleportCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TeleportCMD.java
new file mode 100644
index 00000000..9335a716
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TeleportCMD.java
@@ -0,0 +1,47 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class TeleportCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender sender, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(sender.hasPermission("fancyholograms.hologram.teleport"))) {
+ MessageHelper.error(sender, "You don't have the required permission to teleport you to a hologram");
+ return false;
+ }
+
+ if (!(sender instanceof Player player)) {
+ MessageHelper.error(sender, "You must be a sender to use this command");
+ return false;
+ }
+
+ final var location = hologram.getData().getLocation();
+
+ if (location == null || location.getWorld() == null) {
+ MessageHelper.error(player, "Could not teleport to the hologram");
+ return false;
+ }
+
+ player.teleportAsync(location).thenAccept(success -> {
+ if (success) MessageHelper.success(player, "Teleported you to the hologram");
+ else MessageHelper.error(player, "Could not teleport to the hologram");
+ });
+
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextAlignmentCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextAlignmentCMD.java
new file mode 100644
index 00000000..a4e3e259
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextAlignmentCMD.java
@@ -0,0 +1,72 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.base.Enums;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.TextDisplay;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class TextAlignmentCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.text_alignment"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var alignment = Enums.getIfPresent(TextDisplay.TextAlignment.class, args[3].toUpperCase(Locale.ROOT)).orNull();
+
+ if (alignment == null) {
+ MessageHelper.error(player, "Could not parse text alignment");
+ return false;
+ }
+
+ if (textData.getTextAlignment() == alignment) {
+ MessageHelper.warning(player, "This hologram already has this text alignment");
+ return false;
+ }
+
+ final var copied = textData.copy(textData.getName());
+ copied.setTextAlignment(alignment);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TEXT_ALIGNMENT)) {
+ return false;
+ }
+
+ if (textData.getTextAlignment() == alignment) {
+ MessageHelper.warning(player, "This hologram already has this text alignment");
+ return false;
+ }
+
+ textData.setTextAlignment(((TextHologramData) copied).getTextAlignment());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed text alignment");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextShadowCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextShadowCMD.java
new file mode 100644
index 00000000..2564497d
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TextShadowCMD.java
@@ -0,0 +1,74 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class TextShadowCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.text_shadow"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var enabled = switch (args[3].toLowerCase(Locale.ROOT)) {
+ case "true" -> true;
+ case "false" -> false;
+ default -> null;
+ };
+
+ if (enabled == null) {
+ MessageHelper.error(player, "Could not parse text shadow flag");
+ return false;
+ }
+
+ if (enabled == textData.hasTextShadow()) {
+ MessageHelper.warning(player, "This hologram already has text shadow " + (enabled ? "enabled" : "disabled"));
+ return false;
+ }
+
+ final var copied = textData.copy(textData.getName());
+ copied.setTextShadow(enabled);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TEXT_SHADOW)) {
+ return false;
+ }
+
+ if (enabled == textData.hasTextShadow()) {
+ MessageHelper.warning(player, "This hologram already has text shadow " + (enabled ? "enabled" : "disabled"));
+ return false;
+ }
+
+ textData.setTextShadow(copied.hasTextShadow());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed text shadow");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TranslateCommand.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TranslateCommand.java
new file mode 100644
index 00000000..0e405489
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/TranslateCommand.java
@@ -0,0 +1,80 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Floats;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3f;
+
+import java.util.List;
+
+public class TranslateCommand implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.translate"))) {
+ MessageHelper.error(player, "You don't have the required permission to change the translation of a hologram");
+ return false;
+ }
+
+ final var translateX = Floats.tryParse(args[3]);
+ final var translateY = args.length >= 6 ? Floats.tryParse(args[4]) : translateX;
+ final var translateZ = args.length >= 6 ? Floats.tryParse(args[5]) : translateX;
+
+ if (translateX == null || translateY == null || translateZ == null) {
+ MessageHelper.error(player, "Could not parse translation");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof DisplayHologramData displayData)) {
+ MessageHelper.error(player, "This command can only be used on display holograms");
+ return false;
+ }
+
+ if (Float.compare(translateX, displayData.getTranslation().x()) == 0 &&
+ Float.compare(translateY, displayData.getTranslation().y()) == 0 &&
+ Float.compare(translateZ, displayData.getTranslation().z()) == 0) {
+ MessageHelper.warning(player, "This hologram is already at this translation");
+ return false;
+ }
+
+ final var copied = displayData.copy(displayData.getName());
+ copied.setTranslation(new Vector3f(translateX, translateY, translateZ));
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.TRANSLATION)) {
+ return false;
+ }
+
+ if (Float.compare(copied.getTranslation().x(), displayData.getTranslation().x()) == 0 &&
+ Float.compare(copied.getTranslation().y(), displayData.getTranslation().y()) == 0 &&
+ Float.compare(copied.getTranslation().z(), displayData.getTranslation().z()) == 0) {
+ MessageHelper.warning(player, "This hologram is already at this translation");
+ return false;
+ }
+
+ displayData.setTranslation(new Vector3f(
+ copied.getTranslation().x(),
+ copied.getTranslation().y(),
+ copied.getTranslation().z()));
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed translation to " + translateX + ", " + translateY + ", " + translateZ);
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UnlinkWithNpcCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UnlinkWithNpcCMD.java
new file mode 100644
index 00000000..09d9a3ef
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UnlinkWithNpcCMD.java
@@ -0,0 +1,57 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancyholograms.util.PluginUtils;
+import de.oliver.fancylib.MessageHelper;
+import de.oliver.fancynpcs.api.FancyNpcsPlugin;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class UnlinkWithNpcCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.unlink"))) {
+ MessageHelper.error(player, "You don't have the required permission to unlink a hologram");
+ return false;
+ }
+
+ if (!PluginUtils.isFancyNpcsEnabled()) {
+ MessageHelper.warning(player, "You need to install the FancyNpcs plugin for this functionality to work");
+ MessageHelper.warning(player, "Download link: click here.");
+ return false;
+ }
+
+ if (hologram.getData().getLinkedNpcName() == null) {
+ MessageHelper.error(player, "This hologram is not linked with an NPC");
+ return false;
+ }
+
+ final var npc = FancyNpcsPlugin.get().getNpcManager().getNpc(hologram.getData().getLinkedNpcName());
+
+ hologram.getData().setLinkedNpcName(null);
+
+ if (npc != null) {
+ npc.getData().setDisplayName(npc.getData().getName());
+ npc.updateForAll();
+ }
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Unlinked hologram with NPC");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UpdateTextIntervalCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UpdateTextIntervalCMD.java
new file mode 100644
index 00000000..712b1a9f
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/UpdateTextIntervalCMD.java
@@ -0,0 +1,97 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Locale;
+
+public class UpdateTextIntervalCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.text_interval"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ if (!(hologram.getData() instanceof TextHologramData textData)) {
+ MessageHelper.error(player, "This command can only be used on text holograms");
+ return false;
+ }
+
+ final var text = args[3].toLowerCase(Locale.ROOT);
+
+ Integer interval;
+
+ if (text.equals("never") || text.equals("off") || text.equals("none")) {
+ interval = -1;
+ } else {
+
+ var multiplier = 1;
+
+ if (!text.isEmpty()) {
+ switch (text.charAt(text.length() - 1)) {
+ case 's' -> multiplier = 1000;
+ case 'm' -> multiplier = 1000 * 60;
+ }
+ }
+
+ final var time = Ints.tryParse(multiplier == 1 ? text : text.substring(0, text.length() - 1));
+
+ if (time == null) {
+ interval = null;
+ } else {
+ interval = time * multiplier;
+ }
+ }
+
+ if (interval == null) {
+ MessageHelper.error(player, "Could not parse text update interval");
+ return false;
+ }
+
+ if (interval == textData.getTextUpdateInterval()) {
+ MessageHelper.warning(player, "This hologram already has this text update interval");
+ return false;
+ }
+
+ interval = Math.max(-1, interval);
+
+ final var copied = textData.copy(textData.getName());
+ copied.setTextUpdateInterval(interval);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.UPDATE_TEXT_INTERVAL)) {
+ return false;
+ }
+
+ if (copied.getTextUpdateInterval() == textData.getTextUpdateInterval()) {
+ MessageHelper.warning(player, "This hologram already has this text update interval");
+ return false;
+ }
+
+ textData.setTextUpdateInterval(copied.getTextUpdateInterval());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed the text update interval");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityCMD.java
new file mode 100644
index 00000000..2152c213
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityCMD.java
@@ -0,0 +1,56 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.data.property.Visibility;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+public class VisibilityCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return Arrays.stream(
+ Visibility.values()
+ ).map(Objects::toString).toList();
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.visibility"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ final var optionalVisibility = Visibility.byString(args[3]);
+ if (hologram == null || optionalVisibility.isEmpty()) {
+ return false;
+ }
+ final var visibility = optionalVisibility.get();
+
+ final var copied = hologram.getData().copy(hologram.getName());
+ copied.setVisibility(visibility);
+
+ if (hologram.getData().getVisibility() == copied.getVisibility()) {
+ MessageHelper.warning(player, "This hologram already has visibility set to " + visibility);
+ return false;
+ }
+
+ hologram.getData().setVisibility(copied.getVisibility());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed visibility to " + visibility);
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityDistanceCMD.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityDistanceCMD.java
new file mode 100644
index 00000000..97c092b1
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/commands/hologram/VisibilityDistanceCMD.java
@@ -0,0 +1,68 @@
+package de.oliver.fancyholograms.commands.hologram;
+
+import com.google.common.primitives.Ints;
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.events.HologramUpdateEvent;
+import de.oliver.fancyholograms.commands.HologramCMD;
+import de.oliver.fancyholograms.commands.Subcommand;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class VisibilityDistanceCMD implements Subcommand {
+
+ @Override
+ public List tabcompletion(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+ return null;
+ }
+
+ @Override
+ public boolean run(@NotNull CommandSender player, @Nullable Hologram hologram, @NotNull String[] args) {
+
+ if (!(player.hasPermission("fancyholograms.hologram.edit.visibility_distance"))) {
+ MessageHelper.error(player, "You don't have the required permission to edit a hologram");
+ return false;
+ }
+
+ var visibilityDistance = Ints.tryParse(args[3]);
+
+ if (visibilityDistance == null) {
+ MessageHelper.error(player, "Could not parse visibility distance");
+ return false;
+ }
+
+ if (visibilityDistance <= 0) {
+ visibilityDistance = -1;
+ }
+
+ if (Ints.compare(visibilityDistance, hologram.getData().getVisibilityDistance()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this visibility distance");
+ return false;
+ }
+
+ final var copied = hologram.getData().copy(hologram.getName());
+ copied.setVisibilityDistance(visibilityDistance);
+
+ if (!HologramCMD.callModificationEvent(hologram, player, copied, HologramUpdateEvent.HologramModification.UPDATE_VISIBILITY_DISTANCE)) {
+ return false;
+ }
+
+ if (Ints.compare(copied.getVisibilityDistance(), hologram.getData().getVisibilityDistance()) == 0) {
+ MessageHelper.warning(player, "This hologram already has this visibility distance");
+ return false;
+ }
+
+ hologram.getData().setVisibilityDistance(copied.getVisibilityDistance());
+
+ if (FancyHolograms.get().getHologramConfiguration().isSaveOnChangedEnabled()) {
+ FancyHolograms.get().getHologramStorage().save(hologram);
+ }
+
+ MessageHelper.success(player, "Changed visibility distance");
+ return true;
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java
new file mode 100644
index 00000000..20325f56
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/hologram/version/HologramImpl.java
@@ -0,0 +1,213 @@
+package de.oliver.fancyholograms.hologram.version;
+
+import de.oliver.fancyholograms.api.data.*;
+import de.oliver.fancyholograms.api.events.HologramHideEvent;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancysitula.api.entities.*;
+import de.oliver.fancysitula.factories.FancySitula;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Quaternionf;
+
+public final class HologramImpl extends Hologram {
+
+ private FS_Display fsDisplay;
+
+ public HologramImpl(@NotNull final HologramData data) {
+ super(data);
+ }
+
+ @Override
+ public int getEntityId() {
+ return fsDisplay.getId();
+ }
+
+ @Override
+ public @Nullable org.bukkit.entity.Display getDisplayEntity() {
+ return null;
+ }
+
+ @Override
+ public void create() {
+ final var location = data.getLocation();
+ if (!location.isWorldLoaded()) {
+ return;
+ }
+
+ switch (data.getType()) {
+ case TEXT -> this.fsDisplay = new FS_TextDisplay();
+ case ITEM -> this.fsDisplay = new FS_ItemDisplay();
+ case BLOCK -> this.fsDisplay = new FS_BlockDisplay();
+ }
+
+ if (data instanceof DisplayHologramData dd) {
+ fsDisplay.setTransformationInterpolationDuration(dd.getInterpolationDuration());
+ fsDisplay.setTransformationInterpolationStartDeltaTicks(0);
+ }
+
+ update();
+ }
+
+ @Override
+ public void delete() {
+ this.fsDisplay = null;
+ }
+
+ @Override
+ public void update() {
+ if (fsDisplay == null) {
+ return;
+ }
+
+ // location data
+ final var location = data.getLocation();
+ if (location.getWorld() == null || !location.isWorldLoaded()) {
+ return;
+ }
+ fsDisplay.setLocation(location);
+
+ if (fsDisplay instanceof FS_TextDisplay textDisplay && data instanceof TextHologramData textData) {
+ // line width
+ textDisplay.setLineWidth(Hologram.LINE_WIDTH);
+
+ // background
+ final var background = textData.getBackground();
+ if (background == null) {
+ textDisplay.setBackground(1073741824); // default background
+ } else if (background == Hologram.TRANSPARENT) {
+ textDisplay.setBackground(0);
+ } else {
+ textDisplay.setBackground(background.asARGB());
+ }
+
+ textDisplay.setStyleFlags((byte) 0);
+ textDisplay.setShadow(textData.hasTextShadow());
+ textDisplay.setSeeThrough(textData.isSeeThrough());
+
+ switch (textData.getTextAlignment()) {
+ case LEFT -> textDisplay.setAlignLeft(true);
+ case RIGHT -> textDisplay.setAlignRight(true);
+ case CENTER -> {
+ textDisplay.setAlignLeft(false);
+ textDisplay.setAlignRight(false);
+ }
+ }
+ } else if (fsDisplay instanceof FS_ItemDisplay itemDisplay && data instanceof ItemHologramData itemData) {
+ // item
+ itemDisplay.setItem(itemData.getItemStack());
+ } else if (fsDisplay instanceof FS_BlockDisplay blockDisplay && data instanceof BlockHologramData blockData) {
+ // block
+
+// BlockType blockType = RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK).get(blockData.getBlock().getKey());
+ blockDisplay.setBlock(blockData.getBlock().createBlockData().createBlockState());
+ }
+
+ if (data instanceof DisplayHologramData displayData) {
+ // billboard data
+ fsDisplay.setBillboard(FS_Display.Billboard.valueOf(displayData.getBillboard().name()));
+
+ // brightness
+ if (displayData.getBrightness() != null) {
+ fsDisplay.setBrightnessOverride(displayData.getBrightness().getBlockLight() << 4 | displayData.getBrightness().getSkyLight() << 20);
+ }
+
+ // entity transformation
+ fsDisplay.setTranslation(displayData.getTranslation());
+ fsDisplay.setScale(displayData.getScale());
+ fsDisplay.setLeftRotation(new Quaternionf());
+ fsDisplay.setRightRotation(new Quaternionf());
+
+ // entity shadow
+ fsDisplay.setShadowRadius(displayData.getShadowRadius());
+ fsDisplay.setShadowStrength(displayData.getShadowStrength());
+
+ fsDisplay.setViewRange(displayData.getVisibilityDistance());
+ }
+ }
+
+
+ @Override
+ public boolean show(@NotNull final Player player) {
+ if (!new HologramShowEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (this.fsDisplay == null) {
+ create(); // try to create it if it doesn't exist every time
+ }
+
+ if (fsDisplay == null) {
+ return false; // could not be created, nothing to show
+ }
+
+ if (!data.getLocation().getWorld().getName().equals(player.getLocation().getWorld().getName())) {
+ return false;
+ }
+
+ // TODO: cache player protocol version
+ // TODO: fix this
+// final var protocolVersion = FancyHologramsPlugin.get().isUsingViaVersion() ? Via.getAPI().getPlayerVersion(player) : MINIMUM_PROTOCOL_VERSION;
+// if (protocolVersion < MINIMUM_PROTOCOL_VERSION) {
+// return false;
+// }
+
+ FS_RealPlayer fsPlayer = new FS_RealPlayer(player);
+ FancySitula.ENTITY_FACTORY.spawnEntityFor(fsPlayer, fsDisplay);
+
+ this.viewers.add(player.getUniqueId());
+ refreshHologram(player);
+
+ return true;
+ }
+
+ @Override
+ public boolean hide(@NotNull final Player player) {
+ if (!new HologramHideEvent(this, player).callEvent()) {
+ return false;
+ }
+
+ if (fsDisplay == null) {
+ return false; // doesn't exist, nothing to hide
+ }
+
+ FS_RealPlayer fsPlayer = new FS_RealPlayer(player);
+ FancySitula.ENTITY_FACTORY.despawnEntityFor(fsPlayer, fsDisplay);
+
+ this.viewers.remove(player.getUniqueId());
+ return true;
+ }
+
+
+ @Override
+ public void refresh(@NotNull final Player player) {
+ if (fsDisplay == null) {
+ return; // doesn't exist, nothing to refresh
+ }
+
+ if (!isViewer(player)) {
+ return;
+ }
+
+ FS_RealPlayer fsPlayer = new FS_RealPlayer(player);
+
+ FancySitula.PACKET_FACTORY.createTeleportEntityPacket(
+ fsDisplay.getId(),
+ data.getLocation().x(),
+ data.getLocation().y(),
+ data.getLocation().z(),
+ data.getLocation().getYaw(),
+ data.getLocation().getPitch(),
+ true)
+ .send(fsPlayer);
+
+
+ if (fsDisplay instanceof FS_TextDisplay textDisplay) {
+ textDisplay.setText(getShownText(player));
+ }
+
+ FancySitula.ENTITY_FACTORY.setEntityDataFor(fsPlayer, fsDisplay);
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/BedrockPlayerListener.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/BedrockPlayerListener.java
new file mode 100644
index 00000000..92e532ef
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/BedrockPlayerListener.java
@@ -0,0 +1,22 @@
+package de.oliver.fancyholograms.listeners;
+
+import de.oliver.fancyholograms.FHFeatureFlags;
+import de.oliver.fancyholograms.api.events.HologramShowEvent;
+import de.oliver.fancyholograms.util.PluginUtils;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.geysermc.floodgate.api.FloodgateApi;
+
+public class BedrockPlayerListener implements Listener {
+
+ @EventHandler
+ public void onHologramShow(final HologramShowEvent event) {
+ if (FHFeatureFlags.DISABLE_HOLOGRAMS_FOR_BEDROCK_PLAYERS.isEnabled() && PluginUtils.isFloodgateEnabled()) {
+ boolean isBedrockPlayer = FloodgateApi.getInstance().isFloodgatePlayer(event.getPlayer().getUniqueId());
+ if (isBedrockPlayer) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/NpcListener.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/NpcListener.java
new file mode 100644
index 00000000..dc8ae301
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/NpcListener.java
@@ -0,0 +1,58 @@
+package de.oliver.fancyholograms.listeners;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancylib.FancyLib;
+import de.oliver.fancylib.MessageHelper;
+import de.oliver.fancynpcs.api.events.NpcModifyEvent;
+import de.oliver.fancynpcs.api.events.NpcRemoveEvent;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.jetbrains.annotations.NotNull;
+
+public final class NpcListener implements Listener {
+
+ private final @NotNull FancyHolograms plugin;
+
+ public NpcListener(@NotNull final FancyHolograms plugin) {
+ this.plugin = plugin;
+ }
+
+ @EventHandler
+ public void onRemove(@NotNull final NpcRemoveEvent event) {
+ this.plugin.getHologramsManager()
+ .getHolograms()
+ .stream()
+ .filter(hologram -> event.getNpc().getData().getName().equals(hologram.getData().getLinkedNpcName()))
+ .forEach(hologram -> hologram.getData().setLinkedNpcName(null));
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
+ public void onModify(@NotNull final NpcModifyEvent event) {
+ final var holograms = this.plugin.getHologramsManager().getHolograms();
+
+ switch (event.getModification()) {
+ case TYPE, LOCATION, SCALE -> {
+ final var needsToBeUpdated = holograms.stream()
+ .filter(hologram -> event.getNpc().getData().getName().equals(hologram.getData().getLinkedNpcName()))
+ .toList();
+
+ FancyLib.getInstance().getScheduler().runTaskLater(null, 1L, () -> needsToBeUpdated.forEach(this.plugin.getHologramsManager()::syncHologramWithNpc));
+ }
+ case DISPLAY_NAME, SHOW_IN_TAB -> {
+ final var isLinked = holograms.stream()
+ .map(Hologram::getData)
+ .map(HologramData::getLinkedNpcName)
+ .anyMatch(event.getNpc().getData().getName()::equals);
+
+ if (isLinked) {
+ event.setCancelled(true);
+ MessageHelper.error(event.getModifier(), "This modification is not allowed on a hologram linked npc");
+ }
+ }
+ }
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java
new file mode 100644
index 00000000..fad939aa
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/PlayerListener.java
@@ -0,0 +1,102 @@
+package de.oliver.fancyholograms.listeners;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import net.kyori.adventure.resource.ResourcePackStatus;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.*;
+import org.bukkit.event.player.PlayerResourcePackStatusEvent.Status;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.jetbrains.annotations.NotNull;
+
+public final class PlayerListener implements Listener {
+
+ private final @NotNull FancyHolograms plugin;
+
+ private final Map> loadingResourcePacks;
+
+ public PlayerListener(@NotNull final FancyHolograms plugin) {
+ this.plugin = plugin;
+ this.loadingResourcePacks = new HashMap<>();
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onJoin(@NotNull final PlayerJoinEvent event) {
+ for (final var hologram : this.plugin.getHologramsManager().getHolograms()) {
+ hologram.updateShownStateFor(event.getPlayer());
+ }
+
+ if (!this.plugin.getHologramConfiguration().areVersionNotificationsMuted() && event.getPlayer().hasPermission("fancyholograms.admin")) {
+ FancyHolograms.get().getHologramThread().submit(() -> FancyHolograms.get().getVersionConfig().checkVersionAndDisplay(event.getPlayer(), true));
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onQuit(@NotNull final PlayerQuitEvent event) {
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ for (final var hologram : this.plugin.getHologramsManager().getHolograms()) {
+ hologram.hideHologram(event.getPlayer());
+ }
+ });
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onTeleport(@NotNull final PlayerTeleportEvent event) {
+ for (final Hologram hologram : this.plugin.getHologramsManager().getHolograms()) {
+ hologram.updateShownStateFor(event.getPlayer());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onWorldChange(@NotNull final PlayerChangedWorldEvent event) {
+ for (final Hologram hologram : this.plugin.getHologramsManager().getHolograms()) {
+ hologram.updateShownStateFor(event.getPlayer());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onResourcePackStatus(@NotNull final PlayerResourcePackStatusEvent event) {
+ // Skipping event calls before player has fully loaded to the server.
+ // This should fix NPE due to vanillaPlayer.connection being null when sending resource-packs in the configuration stage.
+ if (!event.getPlayer().isOnline())
+ return;
+ final UUID playerUniqueId = event.getPlayer().getUniqueId();
+ final UUID packUniqueId = getResourcePackID(event);
+ // Adding accepted resource-pack to the list of currently loading resource-packs for that player.
+ if (event.getStatus() == Status.ACCEPTED)
+ loadingResourcePacks.computeIfAbsent(playerUniqueId, (___) -> new ArrayList<>()).add(packUniqueId);
+ // Once successfully loaded (or failed to download), removing resource-pack from the map.
+ else if (event.getStatus() == Status.SUCCESSFULLY_LOADED || event.getStatus() == Status.FAILED_DOWNLOAD) {
+ loadingResourcePacks.computeIfAbsent(playerUniqueId, (___) -> new ArrayList<>()).removeIf(uuid -> uuid.equals(packUniqueId));
+ // Refreshing holograms once (possibly) all resource-packs are loaded.
+ if (loadingResourcePacks.get(playerUniqueId) != null && loadingResourcePacks.get(playerUniqueId).isEmpty()) {
+ // Removing player from the map, as they're no longer needed here.
+ loadingResourcePacks.remove(playerUniqueId);
+ // Refreshing holograms as to make sure custom textures are loaded.
+ for (final Hologram hologram : this.plugin.getHologramsManager().getHolograms()) {
+ hologram.refreshHologram(event.getPlayer());
+ }
+ }
+ }
+ }
+
+ // For 1.20.2 and higher this method returns actual pack identifier, while for older versions, the identifier is a dummy UUID full of zeroes.
+ // Versions prior 1.20.2 supports sending and receiving only one resource-pack and a dummy, constant identifier can be used as a key.
+ private static @NotNull UUID getResourcePackID(final @NotNull PlayerResourcePackStatusEvent event) {
+ try {
+ event.getClass().getMethod("getID");
+ return event.getID();
+ } catch (final @NotNull NoSuchMethodException e) {
+ return new UUID(0,0);
+ }
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/WorldListener.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/WorldListener.java
new file mode 100644
index 00000000..84d55edf
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/listeners/WorldListener.java
@@ -0,0 +1,27 @@
+package de.oliver.fancyholograms.listeners;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.world.WorldLoadEvent;
+import org.bukkit.event.world.WorldUnloadEvent;
+
+public class WorldListener implements Listener {
+
+ @EventHandler
+ public void onWorldLoad(WorldLoadEvent event) {
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ FancyHolograms.get().getFancyLogger().info("Loading holograms for world " + event.getWorld().getName());
+ FancyHolograms.get().getHologramsManager().loadHolograms(event.getWorld().getName());
+ });
+ }
+
+ @EventHandler
+ public void onWorldUnload(WorldUnloadEvent event) {
+ FancyHolograms.get().getHologramThread().submit(() -> {
+ FancyHolograms.get().getFancyLogger().info("Unloading holograms for world " + event.getWorld().getName());
+ FancyHolograms.get().getHologramsManager().unloadHolograms(event.getWorld().getName());
+ });
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsBootstrapper.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsBootstrapper.java
new file mode 100644
index 00000000..e6ee815e
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsBootstrapper.java
@@ -0,0 +1,19 @@
+package de.oliver.fancyholograms.loaders;
+
+import io.papermc.paper.plugin.bootstrap.BootstrapContext;
+import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
+import io.papermc.paper.plugin.bootstrap.PluginProviderContext;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.jetbrains.annotations.NotNull;
+
+public class FancyHologramsBootstrapper implements PluginBootstrap {
+ @Override
+ public void bootstrap(@NotNull BootstrapContext bootstrapContext) {
+
+ }
+
+ @Override
+ public @NotNull JavaPlugin createPlugin(@NotNull PluginProviderContext context) {
+ return PluginBootstrap.super.createPlugin(context);
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsLoader.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsLoader.java
new file mode 100644
index 00000000..a4c0c716
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/loaders/FancyHologramsLoader.java
@@ -0,0 +1,12 @@
+package de.oliver.fancyholograms.loaders;
+
+import io.papermc.paper.plugin.loader.PluginClasspathBuilder;
+import io.papermc.paper.plugin.loader.PluginLoader;
+import org.jetbrains.annotations.NotNull;
+
+public class FancyHologramsLoader implements PluginLoader {
+ @Override
+ public void classloader(@NotNull PluginClasspathBuilder pluginClasspathBuilder) {
+
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/FlatFileHologramStorage.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/FlatFileHologramStorage.java
new file mode 100644
index 00000000..aad9ecf7
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/FlatFileHologramStorage.java
@@ -0,0 +1,224 @@
+package de.oliver.fancyholograms.storage;
+
+import de.oliver.fancyholograms.FancyHolograms;
+import de.oliver.fancyholograms.api.HologramStorage;
+import de.oliver.fancyholograms.api.data.BlockHologramData;
+import de.oliver.fancyholograms.api.data.DisplayHologramData;
+import de.oliver.fancyholograms.api.data.ItemHologramData;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancyholograms.api.hologram.Hologram;
+import de.oliver.fancyholograms.api.hologram.HologramType;
+import org.bukkit.Location;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+public class FlatFileHologramStorage implements HologramStorage {
+
+ private static final ReadWriteLock lock = new ReentrantReadWriteLock();
+ private static final File HOLOGRAMS_CONFIG_FILE = new File("plugins/FancyHolograms/holograms.yml");
+
+ @Override
+ public void saveBatch(Collection holograms, boolean override) {
+ lock.readLock().lock();
+
+ boolean success = false;
+ YamlConfiguration config = null;
+ try {
+ config = YamlConfiguration.loadConfiguration(HOLOGRAMS_CONFIG_FILE);
+
+ if (override) {
+ config.set("holograms", null);
+ }
+
+ for (final var hologram : holograms) {
+ writeHologram(config, hologram);
+ }
+
+ success = true;
+ } finally {
+ lock.readLock().unlock();
+ if (success) {
+ saveConfig(config);
+ }
+ }
+
+ FancyHolograms.get().getFancyLogger().debug("Saved " + holograms.size() + " holograms to file (override=" + override + ")");
+ }
+
+ @Override
+ public void save(Hologram hologram) {
+ lock.readLock().lock();
+
+ boolean success = false;
+ YamlConfiguration config = null;
+ try {
+ config = YamlConfiguration.loadConfiguration(HOLOGRAMS_CONFIG_FILE);
+ writeHologram(config, hologram);
+
+ success = true;
+ } finally {
+ lock.readLock().unlock();
+ if (success) {
+ saveConfig(config);
+ }
+ }
+
+ FancyHolograms.get().getFancyLogger().debug("Saved hologram " + hologram.getData().getName() + " to file");
+ }
+
+ @Override
+ public void delete(Hologram hologram) {
+ lock.readLock().lock();
+
+ boolean success = false;
+ YamlConfiguration config = null;
+ try {
+ config = YamlConfiguration.loadConfiguration(HOLOGRAMS_CONFIG_FILE);
+ config.set("holograms." + hologram.getData().getName(), null);
+
+ success = true;
+ } finally {
+ lock.readLock().unlock();
+ if (success) {
+ saveConfig(config);
+ }
+ }
+
+ FancyHolograms.get().getFancyLogger().debug("Deleted hologram " + hologram.getData().getName() + " from file");
+ }
+
+ @Override
+ public Collection loadAll() {
+ List holograms = readHolograms(FlatFileHologramStorage.HOLOGRAMS_CONFIG_FILE, null);
+ FancyHolograms.get().getFancyLogger().debug("Loaded " + holograms.size() + " holograms from file");
+ return holograms;
+ }
+
+ @Override
+ public Collection loadAll(String world) {
+ List holograms = readHolograms(FlatFileHologramStorage.HOLOGRAMS_CONFIG_FILE, world);
+ FancyHolograms.get().getFancyLogger().debug("Loaded " + holograms.size() + " holograms from file (world=" + world + ")");
+ return holograms;
+ }
+
+ /**
+ * @param world The world to load the holograms from. (null for all worlds)
+ */
+ private List readHolograms(@NotNull File configFile, @Nullable String world) {
+ lock.readLock().lock();
+ try {
+ YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile);
+
+ if (!config.isConfigurationSection("holograms")) {
+ FancyHolograms.get().getFancyLogger().warn("No holograms section found in config");
+ return new ArrayList<>(0);
+ }
+
+ int configVersion = config.getInt("version", 1);
+ if (configVersion != 2) {
+ FancyHolograms.get().getFancyLogger().warn("Config version is not 2, skipping loading holograms");
+ FancyHolograms.get().getFancyLogger().warn("Old config version detected, skipping loading holograms");
+ return new ArrayList<>(0);
+ }
+
+ List holograms = new ArrayList<>();
+
+ ConfigurationSection hologramsSection = config.getConfigurationSection("holograms");
+ for (String name : hologramsSection.getKeys(false)) {
+ ConfigurationSection holoSection = hologramsSection.getConfigurationSection(name);
+ if (holoSection == null) {
+ FancyHolograms.get().getFancyLogger().warn("Could not load hologram section in config");
+ continue;
+ }
+
+ if (world != null && !holoSection.getString("location.world").equals(world)) {
+ continue;
+ }
+
+ String typeName = holoSection.getString("type");
+ if (typeName == null) {
+ FancyHolograms.get().getFancyLogger().warn("HologramType was not saved");
+ continue;
+ }
+
+ HologramType type = HologramType.getByName(typeName);
+ if (type == null) {
+ FancyHolograms.get().getFancyLogger().warn("Could not parse HologramType");
+ continue;
+ }
+
+ DisplayHologramData displayData = null;
+ switch (type) {
+ case TEXT -> displayData = new TextHologramData(name, new Location(null, 0, 0, 0));
+ case ITEM -> displayData = new ItemHologramData(name, new Location(null, 0, 0, 0));
+ case BLOCK -> displayData = new BlockHologramData(name, new Location(null, 0, 0, 0));
+ }
+
+ if (!displayData.read(holoSection, name)) {
+ FancyHolograms.get().getFancyLogger().warn("Could not read hologram data - skipping hologram");
+ continue;
+ }
+
+ Hologram hologram = FancyHolograms.get().getHologramManager().create(displayData);
+ holograms.add(hologram);
+ }
+
+ FancyHolograms.get().getFancyLogger().debug("Loaded " + holograms.size() + " holograms from file");
+ return holograms;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ private void writeHologram(YamlConfiguration config, Hologram hologram) {
+ @NotNull ConfigurationSection section;
+ if (!config.isConfigurationSection("holograms")) {
+ section = config.createSection("holograms");
+ } else {
+ section = Objects.requireNonNull(config.getConfigurationSection("holograms"));
+ }
+
+ String holoName = hologram.getData().getName();
+
+ ConfigurationSection holoSection = section.getConfigurationSection(holoName);
+ if (holoSection == null) {
+ holoSection = section.createSection(holoName);
+ }
+
+ hologram.getData().write(holoSection, holoName);
+ FancyHolograms.get().getFancyLogger().debug("Wrote hologram " + holoName + " to config");
+ }
+
+ private void saveConfig(YamlConfiguration config) {
+ config.set("version", 2);
+ config.setInlineComments("version", List.of("DO NOT CHANGE"));
+
+ FancyHolograms.get().getFileStorageExecutor().execute(() -> {
+ lock.writeLock().lock();
+ try {
+ config.save(HOLOGRAMS_CONFIG_FILE);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ lock.writeLock().unlock();
+ }
+
+ if(!FancyHolograms.canGet()) {
+ return;
+ }
+
+ FancyHolograms.get().getFancyLogger().debug("Saved config to file");
+ });
+ }
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/ConverterTarget.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/ConverterTarget.java
new file mode 100644
index 00000000..3fd91912
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/ConverterTarget.java
@@ -0,0 +1,57 @@
+package de.oliver.fancyholograms.storage.converter;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+public class ConverterTarget {
+ private final @NotNull Pattern hologramIdRegex;
+
+ public ConverterTarget(@NotNull Pattern matching) {
+ this.hologramIdRegex = matching;
+ }
+
+ public @NotNull Pattern getRegex() {
+ return hologramIdRegex;
+ }
+
+ public boolean matches(@NotNull String hologramId) {
+ return hologramIdRegex.asMatchPredicate().test(hologramId);
+ }
+
+ private static final ConverterTarget ALL = new ConverterTarget(Pattern.compile(".*"));
+ public static @NotNull ConverterTarget all() {
+ return ALL;
+ }
+
+ public static @NotNull ConverterTarget ofAll(@NotNull String first, @NotNull String... others) {
+ StringBuilder builder = new StringBuilder(first);
+
+ if (others.length > 0) {
+ builder.append("|");
+ }
+
+ builder.append(String.join("|", others));
+
+ return new ConverterTarget(Pattern.compile(builder.toString()));
+ }
+
+ public static @NotNull ConverterTarget ofSingle(@NotNull String match) {
+ return new ConverterTarget(Pattern.compile(match));
+ }
+
+ public static @Nullable ConverterTarget ofStringNullable(@NotNull String match) {
+
+ if (match.equalsIgnoreCase("*")) {
+ return all();
+ }
+
+ try {
+ return new ConverterTarget(Pattern.compile(match));
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+}
diff --git a/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/DecentHologramsConverter.java b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/DecentHologramsConverter.java
new file mode 100644
index 00000000..21a36b4b
--- /dev/null
+++ b/plugins/fancyholograms-v2/src/main/java/de/oliver/fancyholograms/storage/converter/DecentHologramsConverter.java
@@ -0,0 +1,256 @@
+package de.oliver.fancyholograms.storage.converter;
+
+import de.oliver.fancyholograms.api.data.HologramData;
+import de.oliver.fancyholograms.api.data.ItemHologramData;
+import de.oliver.fancyholograms.api.data.TextHologramData;
+import de.oliver.fancylib.MessageHelper;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Display;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector3f;
+
+import java.io.File;
+import java.util.*;
+
+public class DecentHologramsConverter extends HologramConverter {
+ private static final float VANILLA_PIXEL_BLOCK_SIZE = 0.0625f;
+ private static final float TEXT_DISPLAY_PIXEL = VANILLA_PIXEL_BLOCK_SIZE / 3;
+ private static final float TEXT_DISPLAY_LINE_HEIGHT = TEXT_DISPLAY_PIXEL * 14;
+ private static final String PROCESS_ICONS_FLAG = "--processIcons";
+ private static final String ICON_PREFIX = "#ICON: ";
+ private static final File DECENT_HOLOGRAMS_DATA = new File("./plugins/DecentHolograms/holograms/");
+
+ @Override
+ public @NotNull String getId() {
+ return "DecentHolograms";
+ }
+
+ @Override
+ public boolean canRunConverter() {
+ return DECENT_HOLOGRAMS_DATA.exists();
+ }
+
+ @Override
+ public @NotNull List convertHolograms(@NotNull HologramConversionSession spec) {
+ boolean processIcons = Arrays.stream(spec.getAdditionalArguments()).anyMatch((arg) -> arg.equalsIgnoreCase(PROCESS_ICONS_FLAG));
+
+ if (processIcons) {
+ MessageHelper.warning(
+ spec.getInvoker(),
+ "--processIcons argument is experimental and may produce unexpected results."
+ );
+ } else {
+ MessageHelper.info(
+ spec.getInvoker(),
+ "Any lines containing an #ICON will be removed. You may run with --processIcons to attempt conversion, but this is experimental."
+ );
+ }
+
+ final List targetHolograms = getConvertableHolograms()
+ .stream()
+ .filter((id) -> spec.getTarget().matches(id))
+ .toList();
+
+ if (targetHolograms.isEmpty()) {
+ throw new RuntimeException("The provided target matches no holograms.");
+ }
+
+ ArrayList converted = new ArrayList<>();
+
+ for (final String id : targetHolograms) {
+ final List results = convert(id, processIcons);
+
+ if (results.isEmpty()) {
+ spec.logUnsuccessfulConversion(id, "Unable to convert this hologram, there is no convertable content.");
+ } else {
+ spec.logSuccessfulConversion(id, results);
+ }
+
+ converted.addAll(results);
+ }
+
+ return converted;
+ }
+
+ @Override
+ public @NotNull List getConvertableHolograms() {
+ final File[] files = DECENT_HOLOGRAMS_DATA.listFiles();
+
+ if (files == null || files.length == 0) {
+ return Collections.emptyList();
+ }
+
+ return Arrays.stream(files)
+ .map((file) -> file.getName().replace(".yml", ""))
+ .toList();
+ }
+
+ private @NotNull List convert(@NotNull String hologramId, boolean processIcons) {
+ final File file = DECENT_HOLOGRAMS_DATA.toPath()
+ .resolve(hologramId.endsWith(".yml") ? hologramId : hologramId + ".yml")
+ .toFile();
+
+ if (!file.exists() || !file.canRead()) {
+ throw new RuntimeException("File does not exist or is not readable.");
+ }
+
+ FileConfiguration data = YamlConfiguration.loadConfiguration(file);
+ Objects.requireNonNull(data, "No data could be read from the DecentHolograms file!");
+
+ final Location location = parseLocation(data.getString("location"));
+ final double displayRange = data.getDouble("display-range");
+ final int updateInterval = data.getInt("update-interval");
+
+ // TODO handle exceptions here
+ final Object firstPage = data.getMapList("pages")
+ .stream()
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException(String.format("There are no pages for %s!", hologramId)))
+ .get("lines");
+
+ Objects.requireNonNull(firstPage, String.format("There is no first page for %s!", hologramId));
+
+ final List