diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/NJNConfig.java b/src/main/java/me/youhavetrouble/notjustnameplates/NJNConfig.java new file mode 100644 index 0000000..356e102 --- /dev/null +++ b/src/main/java/me/youhavetrouble/notjustnameplates/NJNConfig.java @@ -0,0 +1,123 @@ +package me.youhavetrouble.notjustnameplates; + +import me.youhavetrouble.notjustnameplates.displays.DisplayContent; +import me.youhavetrouble.notjustnameplates.displays.DisplayFrame; +import org.bukkit.Color; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.entity.Display; + +import javax.annotation.Nullable; +import java.util.HashMap; + +public class NJNConfig { + + private final NotJustNameplates plugin; + private FileConfiguration config; + + private final HashMap displayContents = new HashMap<>(); + + protected NJNConfig(NotJustNameplates plugin) { + this.plugin = plugin; + plugin.saveDefaultConfig(); + plugin.reloadConfig(); + this.config = plugin.getConfig(); + reload(); + } + + public void reload() { + displayContents.clear(); + this.config = plugin.getConfig(); + + ConfigurationSection namePlatesSection = config.getConfigurationSection("nameplates"); + if (namePlatesSection == null) { + plugin.getLogger().severe("No nameplates section found in config! Correct your config and reload."); + return; + } + + for (String sectionName : namePlatesSection.getKeys(false)) { + ConfigurationSection displayContentSection = namePlatesSection.getConfigurationSection(sectionName); + if (displayContentSection == null) continue; + DisplayContent displayContent = createDisplayContent(displayContentSection); + if (displayContent == null) continue; + displayContents.put(sectionName, displayContent); + } + } + + public DisplayContent getDisplayContent(String name) { + return displayContents.get(name); + } + + protected HashMap getDisplayContents() { + return displayContents; + } + + private DisplayContent createDisplayContent(ConfigurationSection displayContentSection) { + + ConfigurationSection framesSection = displayContentSection.getConfigurationSection("frames"); + if (framesSection == null) { + plugin.getLogger().severe("No frames section found in " + displayContentSection.getName()); + return null; + } + + DisplayContent displayContent = new DisplayContent(); + + displayContent.setRefreshRate(displayContentSection.getInt("refresh-rate", 0)); + + Display.Billboard billboard = Display.Billboard.HORIZONTAL; + try { + billboard = Display.Billboard.valueOf(displayContentSection.getString("billboard", "horizontal").toUpperCase()); + } catch (IllegalArgumentException e) { + plugin.getLogger().warning("Invalid billboard type in " + displayContentSection.getName() + ": " + displayContentSection.getString("billboard")+". Using horizontal."); + } + displayContent.setBillboard(billboard); + + framesSection.getKeys(false).forEach(frameName -> { + ConfigurationSection frameSection = framesSection.getConfigurationSection(frameName); + if (frameSection == null) return; + String text = frameSection.getString("text"); + String backgroundColor = frameSection.getString("background"); + displayContent.addFrame(new DisplayFrame(text, colorFromHex(backgroundColor))); + }); + return displayContent; + } + + private Color colorFromHex(@Nullable String hex) { + if (hex == null) return null; + if (!hex.startsWith("#")) { + plugin.getLogger().warning("Invalid hex color: " + hex + " (does not start with '#')"); + return null; + } + + hex = hex.substring(1); // Remove the '#' character + + int r, g, b, a; + + return switch (hex.length()) { + case 3 -> { + r = Integer.parseInt(String.valueOf(hex.charAt(0) + hex.charAt(0)), 16); + g = Integer.parseInt(String.valueOf(hex.charAt(1) + hex.charAt(1)), 16); + b = Integer.parseInt(String.valueOf(hex.charAt(2) + hex.charAt(2)), 16); + yield Color.fromRGB(r, g, b); + } + case 6 -> { + r = Integer.parseInt(hex.substring(0, 2), 16); + g = Integer.parseInt(hex.substring(2, 4), 16); + b = Integer.parseInt(hex.substring(4, 6), 16); + yield Color.fromRGB(r, g, b); + } + case 8 -> { + r = Integer.parseInt(hex.substring(0, 2), 16); + g = Integer.parseInt(hex.substring(2, 4), 16); + b = Integer.parseInt(hex.substring(4, 6), 16); + a = Integer.parseInt(hex.substring(6, 8), 16); + yield Color.fromARGB(a, r, g, b); + } + default -> { + plugin.getLogger().warning("Invalid hex color: " + hex + " (invalid length)"); + yield null; + } + }; + } + +} diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/NotJustNameplates.java b/src/main/java/me/youhavetrouble/notjustnameplates/NotJustNameplates.java index de8a11f..dc675d8 100644 --- a/src/main/java/me/youhavetrouble/notjustnameplates/NotJustNameplates.java +++ b/src/main/java/me/youhavetrouble/notjustnameplates/NotJustNameplates.java @@ -1,20 +1,46 @@ package me.youhavetrouble.notjustnameplates; import me.youhavetrouble.notjustnameplates.nameplates.TeamManagementListener; +import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.util.permissions.DefaultPermissions; public final class NotJustNameplates extends JavaPlugin { private static NotJustNameplates instance; + private static NJNConfig config; + private static long time = Long.MIN_VALUE; @Override public void onEnable() { instance = this; + config = new NJNConfig(this); + + DefaultPermissions.registerPermission("notjustnameplates.seeown", "Allows a player to see their own nameplate", PermissionDefault.FALSE); + getServer().getPluginManager().registerEvents(new TeamManagementListener(this), this); + getServer().getScheduler().runTaskTimerAsynchronously(this, () -> { + time++; + if (config == null) return; + config.getDisplayContents().values().forEach(displayContent -> { + if (displayContent == null) return; + if (displayContent.getRefreshRate() <= 0) return; + if (time % displayContent.getRefreshRate() != 0) return; + displayContent.advanceFrame(); + }); + }, 1, 1); } public static NotJustNameplates getInstance() { return instance; } + public static long getTime() { + return time; + } + + public static NJNConfig getPluginConfig() { + return config; + } + } diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayContent.java b/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayContent.java new file mode 100644 index 0000000..57c334f --- /dev/null +++ b/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayContent.java @@ -0,0 +1,56 @@ +package me.youhavetrouble.notjustnameplates.displays; + +import org.bukkit.entity.Display; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DisplayContent { + + private final List frames = new ArrayList<>(); + private int refreshRate = 0; + private int currentFrame = 0; + + private Display.Billboard billboard = Display.Billboard.HORIZONTAL; + + public DisplayContent() {} + + /** + * Set the refresh rate of the display in ticks. 0 means no refresh rate. + */ + public void setRefreshRate(int refreshRate) { + this.refreshRate = refreshRate; + } + + public int getRefreshRate() { + return refreshRate; + } + + public void setBillboard(@NotNull Display.Billboard billboard) { + this.billboard = billboard; + } + + public Display.Billboard getBillboard() { + return billboard; + } + + public void addFrame(DisplayFrame frame) { + frames.add(frame); + } + + public List getFrames() { + return Collections.unmodifiableList(frames); + } + + public DisplayFrame getCurrentFrame() { + if (frames.isEmpty()) return null; + return frames.get(currentFrame); + } + + public void advanceFrame() { + if (frames.isEmpty()) return; + currentFrame = currentFrame + 1 >= frames.size() ? 0 : currentFrame + 1; + } +} diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayFrame.java b/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayFrame.java new file mode 100644 index 0000000..dd9779e --- /dev/null +++ b/src/main/java/me/youhavetrouble/notjustnameplates/displays/DisplayFrame.java @@ -0,0 +1,13 @@ +package me.youhavetrouble.notjustnameplates.displays; + +import org.bukkit.Color; +import org.jetbrains.annotations.Nullable; + +public record DisplayFrame(String text, Color backgroundColor) { + + public DisplayFrame(@Nullable String text, @Nullable Color backgroundColor) { + this.text = text; + this.backgroundColor = backgroundColor; + } + +} diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/Nameplate.java b/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/Nameplate.java index 93e9219..d355a02 100644 --- a/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/Nameplate.java +++ b/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/Nameplate.java @@ -1,17 +1,17 @@ package me.youhavetrouble.notjustnameplates.nameplates; import me.youhavetrouble.notjustnameplates.NotJustNameplates; +import me.youhavetrouble.notjustnameplates.displays.DisplayContent; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Bukkit; import org.bukkit.Color; -import org.bukkit.entity.Display; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.entity.TextDisplay; import org.bukkit.event.entity.CreatureSpawnEvent; import org.bukkit.util.Transformation; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.joml.AxisAngle4f; import org.joml.Vector3f; @@ -19,25 +19,26 @@ import java.util.UUID; public class Nameplate { - private Component name; + private DisplayContent content; private final UUID playerUuid; - private float heightOffset = 0.7f; - private Color backgroundColor = null; - private Display.Billboard billboard = Display.Billboard.CENTER; + private final float heightOffset = 0.7f; private TextDisplay.TextAlignment alignment = TextDisplay.TextAlignment.CENTER; - private boolean visibleForOwner = true; + private boolean visibleForOwner = false; private TextDisplay textDisplay; - public Nameplate(@NotNull UUID playerUuid, Component name) { + public Nameplate(@NotNull UUID playerUuid, DisplayContent content) { this.playerUuid = playerUuid; - this.name = name; + this.content = content; } private void createDisplayEntity() { if (textDisplay != null && !textDisplay.isDead()) return; + if (this.content == null) return; + if (content.getCurrentFrame().text() == null) return; Player player = Bukkit.getPlayer(playerUuid); if (player == null) return; + this.textDisplay = (TextDisplay) player.getWorld().spawnEntity( player.getEyeLocation(), EntityType.TEXT_DISPLAY, @@ -46,9 +47,14 @@ public class Nameplate { textDisplay.setInvulnerable(true); textDisplay.setPersistent(false); textDisplay.setAlignment(alignment); - textDisplay.setBillboard(billboard); + textDisplay.setBillboard(this.content.getBillboard()); textDisplay.setShadowRadius(0); - if (this.backgroundColor != null) textDisplay.setBackgroundColor(this.backgroundColor); + + Color backgroundColor = this.content.getCurrentFrame().backgroundColor(); + if (backgroundColor != null) textDisplay.setBackgroundColor(backgroundColor); + + textDisplay.text(parseText(this.content.getCurrentFrame().text(), player)); + textDisplay.setTransformation( new Transformation( new Vector3f(0, heightOffset, 0), // offset @@ -63,31 +69,8 @@ public class Nameplate { player.addPassenger(textDisplay); } - /** - * Get content of the nameplate - * @return content of the nameplate - */ - public Component getName() { - return name; - } - - /** - * Set content of the nameplate - */ - public void setName(Component name) { - this.name = name; - if (textDisplay == null || textDisplay.isDead()) return; - textDisplay.text(name); - } - - public void setBillboard(@NotNull Display.Billboard billboard) { - this.billboard = billboard; - if (textDisplay == null || textDisplay.isDead()) return; - textDisplay.setBillboard(billboard); - } - - public Display.Billboard getBillboard() { - return billboard; + public void setContent(@NotNull DisplayContent content) { + this.content = content; } public void setAlignment(@NotNull TextDisplay.TextAlignment alignment) { @@ -100,20 +83,6 @@ public class Nameplate { return alignment; } - public void setBackgroundColor(@Nullable Color color) { - this.backgroundColor = color; - if (textDisplay == null || textDisplay.isDead()) return; - if (this.backgroundColor == null) { - textDisplay.setDefaultBackground(true); - return; - } - textDisplay.setBackgroundColor(this.backgroundColor); - } - - public Color getBackgroundColor() { - return backgroundColor; - } - public void setVisibleForOwner(boolean visible) { this.visibleForOwner = visible; if (textDisplay == null || textDisplay.isDead()) return; @@ -130,31 +99,35 @@ public class Nameplate { return this.visibleForOwner; } - /** - * Set height offset from the player's eye location - */ - public void setHeightOffset(float heightOffset) { - this.heightOffset = heightOffset; - } - - public float getHeightOffset() { - return heightOffset; - } - /** * Update the nameplate position */ public void update() { Player player = Bukkit.getPlayer(playerUuid); - if (player == null || player.isDead() || name == null) { + if (player == null || player.isDead() || content.getCurrentFrame().text() == null) { remove(); return; } createDisplayEntity(); + if (textDisplay == null || textDisplay.isDead()) return; if (!player.getPassengers().contains(textDisplay)) { player.addPassenger(textDisplay); } - textDisplay.text(this.name); + + textDisplay.text(parseText(this.content.getCurrentFrame().text(), player)); + + textDisplay.setBillboard(this.content.getBillboard()); + textDisplay.setInterpolationDuration(content.getRefreshRate()); + + Color backgroundColor = this.content.getCurrentFrame().backgroundColor(); + if (backgroundColor == null) { + textDisplay.setDefaultBackground(true); + } else { + textDisplay.setBackgroundColor(backgroundColor); + } + + setVisibleForOwner(this.visibleForOwner || player.hasPermission("notjustnameplates.seeown")); + } protected void remove() { @@ -162,4 +135,18 @@ public class Nameplate { textDisplay.remove(); } + private Component parseText(String text, Player player) { + + Component component = MiniMessage.miniMessage().deserialize(text); + + if (player == null || !player.isOnline()) return component; + + component = component.replaceText(builder -> { + builder.matchLiteral("%displayname%"); + builder.replacement(player.displayName()); + }); + + return component; + } + } diff --git a/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/TeamManagementListener.java b/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/TeamManagementListener.java index 4cd2871..9b7d165 100644 --- a/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/TeamManagementListener.java +++ b/src/main/java/me/youhavetrouble/notjustnameplates/nameplates/TeamManagementListener.java @@ -1,6 +1,7 @@ package me.youhavetrouble.notjustnameplates.nameplates; import me.youhavetrouble.notjustnameplates.NotJustNameplates; +import me.youhavetrouble.notjustnameplates.displays.DisplayContent; import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; @@ -32,7 +33,8 @@ public class TeamManagementListener implements Listener { @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) public void onPlayerJoin(PlayerJoinEvent event) { Player joiner = event.getPlayer(); - players.put(joiner.getName(), new Nameplate(joiner.getUniqueId(), joiner.displayName())); + DisplayContent displayContent = NotJustNameplates.getPluginConfig().getDisplayContent("default"); + players.put(joiner.getName(), new Nameplate(joiner.getUniqueId(), displayContent != null ? displayContent : new DisplayContent())); for (Player player : event.getPlayer().getServer().getOnlinePlayers()) { addPlayerToTeam(joiner, player); } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 800ae60..7f64320 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,12 +1,23 @@ +# Config is subject to change, take regular backups nameplates: default: - text: - - "%player_displayname%" + # How often to switch betwwen frames (in ticks) + # If set to 0, transitions will be disabled + refresh-rate: 10 + + # Animation frames + frames: + 1: + text: "%displayname%" + background: "#0000FFAA" + 2: + text: "%displayname%" + background: "#FF0000AA" # Billboard options are as follows: # "center" - pivots around center point # "vertical" - pivots around vertical axis # "horizontal" - pivots around horizontal axis # "fixed" - no rotation - billboard: "center" + billboard: "horizontal" diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index 8c768c0..cb67766 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,5 +1,5 @@ name: NotJustNameplates -version: '${version}' +version: "${version}" main: me.youhavetrouble.notjustnameplates.NotJustNameplates api-version: "1.20" authors: ["YouHaveTrouble"]