+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package net.momirealms.customnameplates.api.manager;
+
+import net.momirealms.customnameplates.api.requirement.Condition;
+import net.momirealms.customnameplates.api.requirement.Requirement;
+import net.momirealms.customnameplates.api.requirement.RequirementFactory;
+import org.bukkit.configuration.ConfigurationSection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public interface RequirementManager {
+
+ /**
+ * Registers a custom requirement type with its corresponding factory.
+ *
+ * @param type The type identifier of the requirement.
+ * @param requirementFactory The factory responsible for creating instances of the requirement.
+ * @return True if registration was successful, false if the type is already registered.
+ */
+ boolean registerRequirement(String type, RequirementFactory requirementFactory);
+
+ /**
+ * Unregisters a custom requirement type.
+ *
+ * @param type The type identifier of the requirement to unregister.
+ * @return True if unregistration was successful, false if the type is not registered.
+ */
+ boolean unregisterRequirement(String type);
+
+ /**
+ * Retrieves an array of requirements based on a configuration section.
+ *
+ * @param section The configuration section containing requirement definitions.
+ * @return An array of Requirement objects based on the configuration section
+ */
+ @Nullable Requirement[] getRequirements(ConfigurationSection section);
+
+ /**
+ * Retrieves a Requirement object based on a configuration section and advanced flag.
+ *
+ * requirement_1: <- section
+ * type: xxx
+ * value: xxx
+ *
+ * @param section The configuration section containing requirement definitions.
+ * @return A Requirement object based on the configuration section, or an EmptyRequirement if the section is null or invalid.
+ */
+ @NotNull Requirement getRequirement(ConfigurationSection section);
+
+ /**
+ * Gets a requirement based on the provided type and value.
+ * If a valid RequirementFactory is found for the type, it is used to create the requirement.
+ * If no factory is found, a warning is logged, and an empty requirement instance is returned.
+ *
+ * world: <- type
+ * - world <- value
+ *
+ * @param type The type representing the requirement type.
+ * @param value The value associated with the requirement.
+ * @return A Requirement instance based on the type and value, or an EmptyRequirement if the type is invalid.
+ */
+ @NotNull Requirement getRequirement(String type, Object value);
+
+ /**
+ * Retrieves a RequirementFactory based on the specified requirement type.
+ *
+ * @param type The requirement type for which to retrieve a factory.
+ * @return A RequirementFactory for the specified type, or null if no factory is found.
+ */
+ @Nullable RequirementFactory getRequirementFactory(String type);
+
+ /**
+ * Checks if an array of requirements is met for a given condition.
+ *
+ * @param condition The Condition object to check against the requirements.
+ * @param requirements An array of Requirement instances to be evaluated.
+ * @return True if all requirements are met, false otherwise. Returns true if the requirements array is null.
+ */
+ static boolean isRequirementMet(Condition condition, Requirement... requirements) {
+ if (requirements == null) return true;
+ for (Requirement requirement : requirements) {
+ if (!requirement.isConditionMet(condition)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/ResourcePackManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/ResourcePackManager.java
new file mode 100644
index 0000000..4df85fd
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/ResourcePackManager.java
@@ -0,0 +1,15 @@
+package net.momirealms.customnameplates.api.manager;
+
+import java.io.File;
+
+public interface ResourcePackManager {
+
+ /**
+ * Generate the resource pack
+ */
+ void generateResourcePack();
+
+ void deleteDirectory(File file);
+
+ String native2ascii(char c);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/StorageManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/StorageManager.java
new file mode 100644
index 0000000..5165ca6
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/StorageManager.java
@@ -0,0 +1,88 @@
+package net.momirealms.customnameplates.api.manager;
+
+import net.momirealms.customnameplates.api.data.OnlineUser;
+import net.momirealms.customnameplates.api.data.PlayerData;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public interface StorageManager {
+
+ /**
+ * Get online users
+ *
+ * @return online users
+ */
+ Collection getOnlineUsers();
+
+ /**
+ * Get a player's data by uuid
+ * The player can be an offline one
+ *
+ * @param uuid uuid
+ * @return player data
+ */
+ CompletableFuture> getPlayerData(UUID uuid);
+
+ /**
+ * Save online players' data
+ *
+ * @param uuid uuid
+ * @return success or not
+ */
+ CompletableFuture saveOnlinePlayerData(UUID uuid);
+
+ /**
+ * Save specified data
+ *
+ * @param uuid uuid
+ * @param playerData playerData
+ * @return success or not
+ */
+ CompletableFuture savePlayerData(UUID uuid, PlayerData playerData);
+
+ /**
+ * Get an online user by uuid
+ *
+ * @param uuid uuid
+ * @return online user
+ */
+ Optional getOnlineUser(UUID uuid);
+
+ /**
+ * Get player data from json
+ *
+ * @param json json
+ * @return data
+ */
+ @NotNull
+ PlayerData fromJson(String json);
+
+ /**
+ * Get player data from bytes
+ *
+ * @param data data
+ * @return data
+ */
+ PlayerData fromBytes(byte[] data);
+
+ /**
+ * Convert player data to bytes
+ *
+ * @param playerData playerData
+ * @return bytes
+ */
+ byte[] toBytes(PlayerData playerData);
+
+ /**
+ * Convert player data to json
+ *
+ * @param playerData playerData
+ * @return json
+ */
+ @NotNull
+ String toJson(@NotNull PlayerData playerData);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamManager.java
new file mode 100644
index 0000000..14e4721
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamManager.java
@@ -0,0 +1,47 @@
+package net.momirealms.customnameplates.api.manager;
+
+import net.kyori.adventure.text.Component;
+import net.momirealms.customnameplates.api.mechanic.team.TeamColor;
+import net.momirealms.customnameplates.api.mechanic.team.TeamTagVisibility;
+import org.bukkit.entity.Player;
+
+public interface TeamManager {
+
+ /**
+ * Create team for a player
+ *
+ * @param player player
+ */
+ void createTeam(Player player);
+
+ /**
+ * Create a team for a player on proxy
+ *
+ * @param player player
+ */
+ void createProxyTeam(Player player);
+
+ /**
+ * Remove a team for a player
+ *
+ * @param player player
+ */
+ void removeTeam(Player player);
+
+ /**
+ * Remove a team for a player on proxy
+ *
+ * @param player player
+ */
+ void removeProxyTeam(Player player);
+
+ void updateTeam(Player owner, Player viewer, Component prefix, Component suffix, TeamColor color, TeamTagVisibility visibility);
+
+ /**
+ * Get the team player in
+ *
+ * @param player player
+ * @return team name
+ */
+ String getTeamName(Player player);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamTagManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamTagManager.java
new file mode 100644
index 0000000..ffa408d
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/TeamTagManager.java
@@ -0,0 +1,29 @@
+package net.momirealms.customnameplates.api.manager;
+
+import net.momirealms.customnameplates.api.mechanic.tag.team.TeamPlayer;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public interface TeamTagManager {
+
+ /**
+ * Create team tag for a player
+ * If failed, the return value would be null
+ * This happens when there already exists a team tag for a player
+ *
+ * @return team tag
+ */
+ @Nullable
+ TeamPlayer createTagForPlayer(Player player, String prefix, String suffix);
+
+ /**
+ * Remove a team tag from map by uuid
+ *
+ * @param uuid uuid
+ * @return team tag
+ */
+ @Nullable
+ TeamPlayer removeTeamPlayerFromMap(UUID uuid);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/UnlimitedTagManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/UnlimitedTagManager.java
new file mode 100644
index 0000000..305e03c
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/UnlimitedTagManager.java
@@ -0,0 +1,56 @@
+package net.momirealms.customnameplates.api.manager;
+
+import net.momirealms.customnameplates.api.mechanic.tag.unlimited.NamedEntity;
+import net.momirealms.customnameplates.api.mechanic.tag.unlimited.UnlimitedObject;
+import net.momirealms.customnameplates.api.mechanic.tag.unlimited.UnlimitedPlayer;
+import net.momirealms.customnameplates.api.mechanic.tag.unlimited.UnlimitedTagSetting;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface UnlimitedTagManager {
+
+ /**
+ * Create a named entity (ArmorStand) for a player
+ * To apply the changes, you should add it the named player instance
+ *
+ * @param player player
+ * @param setting setting
+ * @return named entity
+ */
+ @NotNull
+ NamedEntity createNamedEntity(UnlimitedPlayer player, UnlimitedTagSetting setting);
+
+ /**
+ * Create unlimited tags for a player
+ * If failed, the return value would be null
+ * This happens when there already exists an UnlimitedObject for a player
+ *
+ * @param player player
+ * @param settings settings
+ * @return unlimited tag
+ */
+ @Nullable
+ UnlimitedPlayer createTagForPlayer(Player player, List settings);
+
+ /**
+ * Remove UnlimitedObject from map by uuid
+ *
+ * @param uuid uuid
+ * @return The removed unlimited object
+ */
+ @Nullable
+ UnlimitedObject removeUnlimitedObjectFromMap(UUID uuid);
+
+ /**
+ * Get an UnlimitedObject from map by uuid
+ *
+ * @param uuid uuid
+ * @return The unlimited object
+ */
+ @Nullable
+ UnlimitedObject getUnlimitedObject(UUID uuid);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/VersionManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/VersionManager.java
new file mode 100644
index 0000000..261ae9d
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/VersionManager.java
@@ -0,0 +1,26 @@
+package net.momirealms.customnameplates.api.manager;
+
+import java.util.concurrent.CompletionStage;
+
+public interface VersionManager {
+
+ boolean isFolia();
+
+ String getServerVersion();
+
+ CompletionStage checkUpdate();
+
+ boolean isVersionNewerThan1_19_R2();
+
+ boolean isVersionNewerThan1_20();
+
+ boolean isVersionNewerThan1_20_R2();
+
+ String getPluginVersion();
+
+ boolean isLatest();
+
+ boolean isVersionNewerThan1_19();
+
+ int getPackFormat();
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/manager/WidthManager.java b/api/src/main/java/net/momirealms/customnameplates/api/manager/WidthManager.java
new file mode 100644
index 0000000..96b17e7
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/manager/WidthManager.java
@@ -0,0 +1,5 @@
+package net.momirealms.customnameplates.api.manager;
+
+public interface WidthManager {
+
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/background/BackGround.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/background/BackGround.java
new file mode 100644
index 0000000..785c55b
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/background/BackGround.java
@@ -0,0 +1,212 @@
+package net.momirealms.customnameplates.api.mechanic.background;
+
+import net.momirealms.customnameplates.api.mechanic.character.ConfiguredChar;
+import net.momirealms.customnameplates.api.mechanic.font.OffsetFont;
+
+public class BackGround {
+
+ private ConfiguredChar left, offset_1, offset_2, offset_4, offset_8, offset_16, offset_32, offset_64, offset_128, right;
+ private int leftMargin, rightMargin;
+
+ private BackGround() {
+ }
+
+ public BackGround(
+ ConfiguredChar left,
+ ConfiguredChar offset_1,
+ ConfiguredChar offset_2,
+ ConfiguredChar offset_4,
+ ConfiguredChar offset_8,
+ ConfiguredChar offset_16,
+ ConfiguredChar offset_32,
+ ConfiguredChar offset_64,
+ ConfiguredChar offset_128,
+ ConfiguredChar right,
+ int leftMargin,
+ int rightMargin
+ ) {
+ this.left = left;
+ this.offset_1 = offset_1;
+ this.offset_2 = offset_2;
+ this.offset_4 = offset_4;
+ this.offset_8 = offset_8;
+ this.offset_16 = offset_16;
+ this.offset_32 = offset_32;
+ this.offset_64 = offset_64;
+ this.offset_128 = offset_128;
+ this.right = right;
+ this.leftMargin = leftMargin;
+ this.rightMargin = rightMargin;
+ }
+
+ public ConfiguredChar getLeft() {
+ return left;
+ }
+
+ public ConfiguredChar getOffset_1() {
+ return offset_1;
+ }
+
+ public ConfiguredChar getOffset_2() {
+ return offset_2;
+ }
+
+ public ConfiguredChar getOffset_4() {
+ return offset_4;
+ }
+
+ public ConfiguredChar getOffset_8() {
+ return offset_8;
+ }
+
+ public ConfiguredChar getOffset_16() {
+ return offset_16;
+ }
+
+ public ConfiguredChar getOffset_32() {
+ return offset_32;
+ }
+
+ public ConfiguredChar getOffset_64() {
+ return offset_64;
+ }
+
+ public ConfiguredChar getOffset_128() {
+ return offset_128;
+ }
+
+ public ConfiguredChar getRight() {
+ return right;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getBackGroundImage(int n) {
+ String offset = OffsetFont.getShortestNegChars(n + rightMargin + 2);
+ n = n + leftMargin + rightMargin + 2;
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(left.getCharacter());
+ while (n >= 128) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_128.getCharacter());
+ n -= 128;
+ }
+ if (n - 64 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_64.getCharacter());
+ n -= 64;
+ }
+ if (n - 32 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_32.getCharacter());
+ n -= 32;
+ }
+ if (n - 16 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_16.getCharacter());
+ n -= 16;
+ }
+ if (n - 8 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_8.getCharacter());
+ n -= 8;
+ }
+ if (n - 4 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_4.getCharacter());
+ n -= 4;
+ }
+ if (n - 2 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_2.getCharacter());
+ n -= 2;
+ }
+ if (n - 1 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(offset_1.getCharacter());
+ }
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(right.getCharacter());
+ stringBuilder.append(offset);
+ return stringBuilder.toString();
+ }
+
+ public static class Builder {
+
+ private final BackGround backGround;
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder() {
+ this.backGround = new BackGround();
+ }
+
+ public Builder left(ConfiguredChar configuredChar) {
+ backGround.left = configuredChar;
+ return this;
+ }
+
+ public Builder right(ConfiguredChar configuredChar) {
+ backGround.right = configuredChar;
+ return this;
+ }
+
+ public Builder offset_1(ConfiguredChar configuredChar) {
+ backGround.offset_1 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_2(ConfiguredChar configuredChar) {
+ backGround.offset_2 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_4(ConfiguredChar configuredChar) {
+ backGround.offset_4 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_8(ConfiguredChar configuredChar) {
+ backGround.offset_8 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_16(ConfiguredChar configuredChar) {
+ backGround.offset_16 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_32(ConfiguredChar configuredChar) {
+ backGround.offset_32 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_64(ConfiguredChar configuredChar) {
+ backGround.offset_64 = configuredChar;
+ return this;
+ }
+
+ public Builder offset_128(ConfiguredChar configuredChar) {
+ backGround.offset_128 = configuredChar;
+ return this;
+ }
+
+ public Builder leftMargin(int margin) {
+ backGround.leftMargin = margin;
+ return this;
+ }
+
+ public Builder rightMargin(int margin) {
+ backGround.rightMargin = margin;
+ return this;
+ }
+
+ public BackGround build() {
+ return backGround;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/CharacterArranger.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/CharacterArranger.java
new file mode 100644
index 0000000..aac98ec
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/CharacterArranger.java
@@ -0,0 +1,25 @@
+package net.momirealms.customnameplates.api.mechanic.character;
+
+public class CharacterArranger {
+
+ public static char currentChar;
+
+ public static void increase() {
+ currentChar = (char) (currentChar + '\u0001');
+ }
+
+ public static char getAndIncrease() {
+ char temp = currentChar;
+ increase();
+ return temp;
+ }
+
+ public static char increaseAndGet() {
+ increase();
+ return currentChar;
+ }
+
+ public static void reset(char c) {
+ currentChar = c;
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/ConfiguredChar.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/ConfiguredChar.java
new file mode 100644
index 0000000..cc70116
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/character/ConfiguredChar.java
@@ -0,0 +1,107 @@
+package net.momirealms.customnameplates.api.mechanic.character;
+
+import net.momirealms.customnameplates.api.util.LogUtils;
+
+public class ConfiguredChar {
+
+ private char character;
+ private String pngFile;
+ private int height;
+ private int width;
+ private int ascent;
+
+ private ConfiguredChar() {
+
+ }
+
+ public ConfiguredChar(char character, String pngFile, int height, int width, int ascent) {
+ this.character = character;
+ this.pngFile = pngFile;
+ this.height = height;
+ this.width = width;
+ this.ascent = ascent;
+ }
+
+ public char getCharacter() {
+ return character;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getPngFile() {
+ return pngFile;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public int getAscent() {
+ return ascent;
+ }
+
+ public String getFile() {
+ return pngFile + ".png";
+ }
+
+ public static class Builder {
+
+ private final ConfiguredChar configuredChar;
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder() {
+ this.configuredChar = new ConfiguredChar();
+ }
+
+ public Builder character(char character) {
+ configuredChar.character = character;
+ return this;
+ }
+
+ public Builder png(String png) {
+ configuredChar.pngFile = png;
+ return this;
+ }
+
+ public Builder height(int height) {
+ configuredChar.height = height;
+ return this;
+ }
+
+ public Builder ascent(int ascent) {
+ configuredChar.ascent = ascent;
+ if (ascent >= configuredChar.height) {
+ LogUtils.warn("Invalid config for " + configuredChar.pngFile);
+ LogUtils.warn("Ascent " + ascent + " should be no higher than Height " + configuredChar.height);
+ }
+ return this;
+ }
+
+ public Builder descent(int descent) {
+ if (descent < 0) {
+ LogUtils.warn("Invalid config for " + configuredChar.pngFile);
+ LogUtils.warn("Descent " + descent + " should be no lower than 0");
+ }
+ configuredChar.ascent = configuredChar.height - descent;
+ return this;
+ }
+
+ public Builder width(int width) {
+ configuredChar.width = width;
+ return this;
+ }
+
+ public ConfiguredChar build() {
+ return configuredChar;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/font/OffsetFont.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/font/OffsetFont.java
new file mode 100644
index 0000000..4d24ab1
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/font/OffsetFont.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) <2022>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package net.momirealms.customnameplates.api.mechanic.font;
+
+public enum OffsetFont {
+
+ NEG_1('\uf801', -1, -3),
+ NEG_2('\uf802', -2, -4),
+ NEG_3('\uf803', -3, -5),
+ NEG_4('\uf804', -4, -6),
+ NEG_5('\uf805', -5, -7),
+ NEG_6('\uf806', -6, -8),
+ NEG_7('\uf807', -7, -9),
+ NEG_8('\uf808', -8, -10),
+ NEG_16('\uf809', -16, -18),
+ NEG_32('\uf80a', -32, -34),
+ NEG_64('\uf80b', -64, -66),
+ NEG_128('\uf80c', -128, -130),
+ POS_1('\uf811', 1, -1),
+ POS_2('\uf812', 2, 1),
+ POS_3('\uf813', 3, 2),
+ POS_4('\uf814', 4, 3),
+ POS_5('\uf815', 5, 4),
+ POS_6('\uf816', 6, 5),
+ POS_7('\uf817', 7, 6),
+ POS_8('\uf818', 8, 7),
+ POS_16('\uf819', 16, 15),
+ POS_32('\uf81a', 32, 31),
+ POS_64('\uf81b', 64, 63),
+ POS_128('\uf81c', 128, 127);
+
+ private final char character;
+ private final int space;
+ private final int height;
+
+ OffsetFont(char character, int space, int height) {
+ this.character = character;
+ this.space = space;
+ this.height = height;
+ }
+
+ public char getCharacter() {
+ return this.character;
+ }
+
+ public int getSpace() {
+ return this.space;
+ }
+
+ public int getHeight() {
+ return this.height;
+ }
+
+ public static String getOffsetChars(int offset) {
+ if (offset >= 0) {
+ return getShortestPosChars(offset);
+ } else {
+ return getShortestNegChars(-offset);
+ }
+ }
+
+ public static String getShortestNegChars(int n) {
+ StringBuilder stringBuilder = new StringBuilder();
+ while (n >= 128) {
+ stringBuilder.append(OffsetFont.NEG_128.getCharacter());
+ n -= 128;
+ }
+ if (n - 64 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_64.getCharacter());
+ n -= 64;
+ }
+ if (n - 32 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_32.getCharacter());
+ n -= 32;
+ }
+ if (n - 16 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_16.getCharacter());
+ n -= 16;
+ }
+ if (n - 8 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_8.getCharacter());
+ n -= 8;
+ }
+ if (n - 7 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_7.getCharacter());
+ n -= 7;
+ }
+ if (n - 6 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_6.getCharacter());
+ n -= 6;
+ }
+ if (n - 5 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_5.getCharacter());
+ n -= 5;
+ }
+ if (n - 4 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_4.getCharacter());
+ n -= 4;
+ }
+ if (n - 3 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_3.getCharacter());
+ n -= 3;
+ }
+ if (n - 2 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_2.getCharacter());
+ n -= 2;
+ }
+ if (n - 1 >= 0) {
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ }
+ return stringBuilder.toString();
+ }
+
+ public static String getShortestPosChars(int n) {
+ StringBuilder stringBuilder = new StringBuilder();
+ while (n >= 128) {
+ stringBuilder.append(OffsetFont.POS_128.getCharacter());
+ n -= 128;
+ }
+ if (n - 64 >= 0) {
+ stringBuilder.append(OffsetFont.POS_64.getCharacter());
+ n -= 64;
+ }
+ if (n - 32 >= 0) {
+ stringBuilder.append(OffsetFont.POS_32.getCharacter());
+ n -= 32;
+ }
+ if (n - 16 >= 0) {
+ stringBuilder.append(OffsetFont.POS_16.getCharacter());
+ n -= 16;
+ }
+ if (n - 8 >= 0) {
+ stringBuilder.append(OffsetFont.POS_8.getCharacter());
+ n -= 8;
+ }
+ if (n - 7 >= 0) {
+ stringBuilder.append(OffsetFont.POS_7.getCharacter());
+ n -= 7;
+ }
+ if (n - 6 >= 0) {
+ stringBuilder.append(OffsetFont.POS_6.getCharacter());
+ n -= 6;
+ }
+ if (n - 5 >= 0) {
+ stringBuilder.append(OffsetFont.POS_5.getCharacter());
+ n -= 5;
+ }
+ if (n - 4 >= 0) {
+ stringBuilder.append(OffsetFont.POS_4.getCharacter());
+ n -= 4;
+ }
+ if (n - 3 >= 0) {
+ stringBuilder.append(OffsetFont.POS_3.getCharacter());
+ n -= 3;
+ }
+ if (n - 2 >= 0) {
+ stringBuilder.append(OffsetFont.POS_2.getCharacter());
+ n -= 2;
+ }
+ if (n - 1 >= 0) {
+ stringBuilder.append(OffsetFont.POS_1.getCharacter());
+ }
+ return stringBuilder.toString();
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/misc/ViewerText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/misc/ViewerText.java
new file mode 100644
index 0000000..ad7af9b
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/misc/ViewerText.java
@@ -0,0 +1,111 @@
+package net.momirealms.customnameplates.api.mechanic.misc;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.CustomNameplatesPlugin;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ViewerText {
+
+ private final Player owner;
+ private String processedText;
+ private final ClaimedText[] placeholders;
+ private final ConcurrentHashMap valueMap;
+
+ public ViewerText(Player owner, String rawText) {
+ this.processedText = rawText;
+ this.valueMap = new ConcurrentHashMap<>();
+ this.owner = owner;
+ List placeholders = CustomNameplatesPlugin.get().getPlaceholderManager().detectPlaceholders(rawText);
+ this.placeholders = new ClaimedText[placeholders.size()];
+ int i = 0;
+ for (String placeholder : placeholders) {
+ processedText = processedText.replace(placeholder, "%s");
+ if (placeholder.startsWith("%viewer_")) {
+ this.placeholders[i] = new ClaimedText(null, "%" + placeholder.substring("%viewer_".length()));
+ } else {
+ this.placeholders[i] = new ClaimedText(owner, placeholder);
+ }
+ i++;
+ }
+ }
+
+ public void updateForOwner() {
+ for (ClaimedText text : placeholders) {
+ text.update();
+ }
+ }
+
+ public boolean updateForViewer(Player viewer) {
+ String string;
+ if ("%s".equals(processedText)) {
+ string = placeholders[0].getValue(viewer);
+ } else if (placeholders.length != 0) {
+ Object[] values = new String[placeholders.length];
+ for (int i = 0; i < placeholders.length; i++) {
+ values[i] = placeholders[i].getValue(viewer);
+ }
+ string = String.format(processedText, values);
+ } else {
+ string = processedText;
+ }
+ var uuid = viewer.getUniqueId();
+ if (!valueMap.containsKey(uuid)) {
+ valueMap.put(uuid, string);
+ return true;
+ }
+ String previousValue = valueMap.get(uuid);
+ if (!previousValue.equals(string)) {
+ valueMap.put(uuid, string);
+ return true;
+ }
+ return false;
+ }
+
+ public void removeViewer(Player viewer) {
+ valueMap.remove(viewer.getUniqueId());
+ }
+
+ public String getProcessedText() {
+ return processedText;
+ }
+
+ public String getLatestValue(Player viewer) {
+ return valueMap.get(viewer.getUniqueId());
+ }
+
+ public Entity getOwner() {
+ return owner;
+ }
+
+ public static class ClaimedText {
+
+ private final String placeholder;
+ private final Player owner;
+ private String latestValue;
+
+ public ClaimedText(Player owner, String placeholder) {
+ this.placeholder = placeholder;
+ this.owner = owner;
+ this.latestValue = null;
+ this.update();
+ }
+
+ public void update() {
+ if (owner == null) return;
+ this.latestValue = PlaceholderAPI.setPlaceholders(owner, placeholder);
+ }
+
+ public String getValue(Player viewer) {
+ return Objects.requireNonNullElseGet(
+ latestValue,
+ () -> PlaceholderAPI.setPlaceholders(owner == null ? viewer : owner, placeholder)
+ );
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/CachedNameplate.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/CachedNameplate.java
new file mode 100644
index 0000000..40c1e6b
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/CachedNameplate.java
@@ -0,0 +1,70 @@
+package net.momirealms.customnameplates.api.mechanic.nameplate;
+
+import net.momirealms.customnameplates.api.mechanic.team.TeamColor;
+
+public class CachedNameplate {
+
+ private TeamColor teamColor;
+ private String tagPrefix;
+ private String tagSuffix;
+ private String namePrefix;
+ private String nameSuffix;
+ private String playerName;
+
+ public CachedNameplate() {
+ this.tagPrefix = "";
+ this.tagSuffix = "";
+ this.namePrefix = "";
+ this.nameSuffix = "";
+ this.playerName = "";
+ this.teamColor = TeamColor.WHITE;
+ }
+
+ public String getTagPrefix() {
+ return tagPrefix;
+ }
+
+ public void setTagPrefix(String prefix) {
+ this.tagPrefix = prefix;
+ }
+
+ public String getTagSuffix() {
+ return tagSuffix;
+ }
+
+ public void setTagSuffix(String suffix) {
+ this.tagSuffix = suffix;
+ }
+
+ public void setNamePrefix(String namePrefix) {
+ this.namePrefix = namePrefix;
+ }
+
+ public void setNameSuffix(String nameSuffix) {
+ this.nameSuffix = nameSuffix;
+ }
+
+ public String getNamePrefix() {
+ return namePrefix;
+ }
+
+ public String getNameSuffix() {
+ return nameSuffix;
+ }
+
+ public TeamColor getTeamColor() {
+ return teamColor;
+ }
+
+ public void setTeamColor(TeamColor teamColor) {
+ this.teamColor = teamColor;
+ }
+
+ public String getPlayerName() {
+ return playerName;
+ }
+
+ public void setPlayerName(String playerName) {
+ this.playerName = playerName;
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/Nameplate.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/Nameplate.java
new file mode 100644
index 0000000..bdd40e3
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/Nameplate.java
@@ -0,0 +1,166 @@
+package net.momirealms.customnameplates.api.mechanic.nameplate;
+
+import net.momirealms.customnameplates.api.mechanic.character.ConfiguredChar;
+import net.momirealms.customnameplates.api.mechanic.font.OffsetFont;
+import net.momirealms.customnameplates.api.mechanic.team.TeamColor;
+import net.momirealms.customnameplates.api.util.FontUtils;
+
+public class Nameplate {
+
+ private String displayName;
+ private TeamColor teamColor;
+ private String namePrefix;
+ private String nameSuffix;
+ private ConfiguredChar left;
+ private ConfiguredChar middle;
+ private ConfiguredChar right;
+
+ private Nameplate() {
+ }
+
+ public Nameplate(
+ String displayName,
+ TeamColor teamColor,
+ String namePrefix,
+ String nameSuffix,
+ ConfiguredChar left,
+ ConfiguredChar middle,
+ ConfiguredChar right
+ ) {
+ this.displayName = displayName;
+ this.teamColor = teamColor;
+ this.left = left;
+ this.middle = middle;
+ this.right = right;
+ this.namePrefix = namePrefix;
+ this.nameSuffix = nameSuffix;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public TeamColor getTeamColor() {
+ return teamColor;
+ }
+
+ public ConfiguredChar getLeft() {
+ return left;
+ }
+
+ public ConfiguredChar getMiddle() {
+ return middle;
+ }
+
+ public ConfiguredChar getRight() {
+ return right;
+ }
+
+ public String getNamePrefix() {
+ if (teamColor == TeamColor.NONE) {
+ return "";
+ }
+ if (teamColor == TeamColor.CUSTOM) {
+ return namePrefix;
+ }
+ return "<" + teamColor.name() + ">";
+ }
+
+ public String getNameSuffix() {
+ if (teamColor == TeamColor.NONE) {
+ return "";
+ }
+ if (teamColor == TeamColor.CUSTOM) {
+ return nameSuffix;
+ }
+ return "" + teamColor.name() + ">";
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getPrefixWithFont(int textWidth) {
+ return FontUtils.surroundNameplateFont(getPrefix(textWidth));
+ }
+
+ public String getPrefix(int textWidth) {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append(OffsetFont.getShortestNegChars(textWidth % 2 == 0 ? textWidth + left.getWidth() : textWidth + left.getWidth() + 1));
+ stringBuilder.append(left.getCharacter());
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ int mid_amount = (textWidth - 1) / (middle.getWidth());
+ if (mid_amount != 0) {
+ for (int i = 0; i < mid_amount; i++) {
+ stringBuilder.append(middle.getCharacter());
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ }
+ stringBuilder.append(OffsetFont.getShortestNegChars(middle.getWidth() - textWidth % middle.getWidth())); // +1
+ }
+ stringBuilder.append(middle.getCharacter());
+ stringBuilder.append(OffsetFont.NEG_1.getCharacter());
+ stringBuilder.append(right.getCharacter());
+ stringBuilder.append(OffsetFont.getShortestNegChars(textWidth + right.getWidth() - 1)); // -1
+ return stringBuilder.toString();
+ }
+
+ public String getSuffixWithFont(int textWidth) {
+ return FontUtils.surroundNameplateFont(getSuffix(textWidth));
+ }
+
+ public String getSuffix(int textWidth) {
+ return OffsetFont.getShortestNegChars(textWidth + textWidth % 2 + 1);
+ }
+
+ public static class Builder {
+
+ private final Nameplate nameplate;
+
+ public Builder() {
+ this.nameplate = new Nameplate();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder displayName(String display) {
+ nameplate.displayName = display;
+ return this;
+ }
+
+ public Builder teamColor(TeamColor teamColor) {
+ nameplate.teamColor = teamColor;
+ return this;
+ }
+
+ public Builder namePrefix(String namePrefix) {
+ nameplate.namePrefix = namePrefix;
+ return this;
+ }
+
+ public Builder nameSuffix(String nameSuffix) {
+ nameplate.nameSuffix = nameSuffix;
+ return this;
+ }
+
+ public Builder left(ConfiguredChar configuredChar) {
+ nameplate.left = configuredChar;
+ return this;
+ }
+
+ public Builder middle(ConfiguredChar configuredChar) {
+ nameplate.middle = configuredChar;
+ return this;
+ }
+
+ public Builder right(ConfiguredChar configuredChar) {
+ nameplate.right = configuredChar;
+ return this;
+ }
+
+ public Nameplate build() {
+ return nameplate;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/TagMode.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/TagMode.java
new file mode 100644
index 0000000..774c228
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/nameplate/TagMode.java
@@ -0,0 +1,7 @@
+package net.momirealms.customnameplates.api.mechanic.nameplate;
+
+public enum TagMode {
+ TEAM,
+ UNLIMITED,
+ DISABLE
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/BackGroundText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/BackGroundText.java
new file mode 100644
index 0000000..a359e64
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/BackGroundText.java
@@ -0,0 +1,65 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.mechanic.background.BackGround;
+import net.momirealms.customnameplates.api.util.FontUtils;
+import org.bukkit.OfflinePlayer;
+
+public class BackGroundText {
+
+ private String text;
+ private BackGround backGround;
+
+ private BackGroundText() {
+ }
+
+ public BackGroundText(String text, BackGround backGround) {
+ this.text = text;
+ this.backGround = backGround;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public BackGround getBackGround() {
+ return backGround;
+ }
+
+ public String getValue(OfflinePlayer player) {
+ String parsed = PlaceholderAPI.setPlaceholders(player, text);
+ int parsedWidth = FontUtils.getTextWidth(parsed);
+ return backGround.getBackGroundImage(parsedWidth) + parsed;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final BackGroundText text;
+
+ public Builder() {
+ this.text = new BackGroundText();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder text(String value) {
+ text.text = value;
+ return this;
+ }
+
+ public Builder background(BackGround backGround) {
+ text.backGround = backGround;
+ return this;
+ }
+
+ public BackGroundText build() {
+ return text;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/CachedText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/CachedText.java
new file mode 100644
index 0000000..b52ba7e
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/CachedText.java
@@ -0,0 +1,54 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+public class CachedText {
+
+ private long refreshInterval;
+ private String text;
+
+ private CachedText() {
+ }
+
+ public CachedText(long refreshInterval, String text) {
+ this.refreshInterval = refreshInterval;
+ this.text = text;
+ }
+
+ public long getRefreshInterval() {
+ return refreshInterval;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final CachedText text;
+
+ public Builder() {
+ this.text = new CachedText();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder refreshInterval(long time) {
+ this.text.refreshInterval = time;
+ return this;
+ }
+
+ public Builder text(String text) {
+ this.text.text = text;
+ return this;
+ }
+
+ public CachedText build() {
+ return text;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/ConditionalText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/ConditionalText.java
new file mode 100644
index 0000000..bf64792
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/ConditionalText.java
@@ -0,0 +1,58 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.common.Pair;
+import net.momirealms.customnameplates.api.manager.RequirementManager;
+import net.momirealms.customnameplates.api.requirement.Condition;
+import net.momirealms.customnameplates.api.requirement.Requirement;
+import org.bukkit.OfflinePlayer;
+
+import java.util.List;
+
+public class ConditionalText {
+
+ private List> textList;
+
+ private ConditionalText() {
+ }
+
+ public ConditionalText(List> textList) {
+ this.textList = textList;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getValue(OfflinePlayer player) {
+ Condition condition = new Condition(player);
+ for (Pair pair : textList) {
+ if (RequirementManager.isRequirementMet(condition, pair.right())) {
+ return PlaceholderAPI.setPlaceholders(player, pair.left());
+ }
+ }
+ return "";
+ }
+
+ public static class Builder {
+
+ private final ConditionalText conditionalText;
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder() {
+ this.conditionalText = new ConditionalText();
+ }
+
+ public Builder textList(List> textList) {
+ conditionalText.textList = textList;
+ return this;
+ }
+
+ public ConditionalText build() {
+ return conditionalText;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/DescentText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/DescentText.java
new file mode 100644
index 0000000..e3a3379
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/DescentText.java
@@ -0,0 +1,68 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.util.FontUtils;
+import org.bukkit.OfflinePlayer;
+
+public class DescentText {
+
+ private int ascent;
+ private String text;
+ private boolean isUnicode;
+
+ public DescentText(int ascent, String text, boolean isUnicode) {
+ this.ascent = ascent;
+ this.text = text;
+ this.isUnicode = isUnicode;
+ }
+
+ private DescentText() {
+
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public String getValue(OfflinePlayer player) {
+ var parsed = PlaceholderAPI.setPlaceholders(player, text);
+ return isUnicode ? FontUtils.surroundAscentUnicodeFont(parsed, ascent) : FontUtils.surroundAscentFont(parsed, ascent);
+ }
+
+ public static class Builder {
+
+ private final DescentText descentText;
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder() {
+ this.descentText = new DescentText();
+ }
+
+ public Builder ascent(int ascent) {
+ descentText.ascent = ascent;
+ return this;
+ }
+
+ public Builder descent(int descent) {
+ descentText.ascent = 8 - descent;
+ return this;
+ }
+
+ public Builder text(String text) {
+ descentText.text = text;
+ return this;
+ }
+
+ public Builder unicode(boolean unicode) {
+ descentText.isUnicode = unicode;
+ return this;
+ }
+
+ public DescentText build() {
+ return descentText;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/NameplateText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/NameplateText.java
new file mode 100644
index 0000000..932823c
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/NameplateText.java
@@ -0,0 +1,71 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.mechanic.nameplate.Nameplate;
+import net.momirealms.customnameplates.api.util.FontUtils;
+import org.bukkit.OfflinePlayer;
+
+public class NameplateText {
+
+ private String text;
+ private Nameplate nameplate;
+
+ private NameplateText() {
+ }
+
+ public NameplateText(String text, Nameplate nameplate) {
+ this.text = text;
+ this.nameplate = nameplate;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public Nameplate getNameplate() {
+ return nameplate;
+ }
+
+ public String getValue(OfflinePlayer player) {
+ String temp;
+ switch (nameplate.getTeamColor()) {
+ case CUSTOM -> temp = nameplate.getNamePrefix() + text + nameplate.getNameSuffix();
+ case NONE -> temp = text;
+ default -> temp = "<" + nameplate.getTeamColor().name() + ">" + text + "" + nameplate.getTeamColor().name() + ">";
+ }
+ String parsed = PlaceholderAPI.setPlaceholders(player, temp);
+ int parsedWidth = FontUtils.getTextWidth(parsed);
+ return nameplate.getPrefixWithFont(parsedWidth) + parsed + nameplate.getSuffixWithFont(parsedWidth);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final NameplateText text;
+
+ public Builder() {
+ this.text = new NameplateText();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder text(String value) {
+ text.text = value;
+ return this;
+ }
+
+ public Builder nameplate(Nameplate value) {
+ text.nameplate = value;
+ return this;
+ }
+
+ public NameplateText build() {
+ return text;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/StaticText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/StaticText.java
new file mode 100644
index 0000000..a2bf8a4
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/StaticText.java
@@ -0,0 +1,98 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.mechanic.font.OffsetFont;
+import net.momirealms.customnameplates.api.util.FontUtils;
+import org.bukkit.OfflinePlayer;
+
+public class StaticText {
+
+ private String text;
+ private int value;
+ private StaticState staticState;
+
+ private StaticText() {
+ }
+
+ public StaticText(String text, int value, StaticState staticState) {
+ this.text = text;
+ this.value = value;
+ this.staticState = staticState;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public int getValue() {
+ return value;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public StaticState getStaticState() {
+ return staticState;
+ }
+
+ public String getValue(OfflinePlayer player) {
+ String parsed = PlaceholderAPI.setPlaceholders(player, text);
+ int parsedWidth = FontUtils.getTextWidth(parsed);
+ switch (staticState) {
+ case LEFT -> {
+ return parsed + FontUtils.surroundNameplateFont(OffsetFont.getOffsetChars(value - parsedWidth));
+ }
+ case RIGHT -> {
+ return FontUtils.surroundNameplateFont(OffsetFont.getOffsetChars(value - parsedWidth)) + parsed;
+ }
+ case MIDDLE -> {
+ int half = (value - parsedWidth) / 2;
+ String left = FontUtils.surroundNameplateFont(OffsetFont.getOffsetChars(half));
+ String right = FontUtils.surroundNameplateFont(OffsetFont.getOffsetChars(value - parsedWidth - half));
+ return left + parsed + right;
+ }
+ default -> {
+ return "";
+ }
+ }
+ }
+
+ public static class Builder {
+
+ private final StaticText text;
+
+ public Builder() {
+ this.text = new StaticText();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder value(int value) {
+ text.value = value;
+ return this;
+ }
+
+ public Builder text(String value) {
+ text.text = value;
+ return this;
+ }
+
+ public Builder state(StaticState state) {
+ text.staticState = state;
+ return this;
+ }
+
+ public StaticText build() {
+ return text;
+ }
+ }
+
+ public enum StaticState {
+ LEFT,
+ MIDDLE,
+ RIGHT
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/SwitchText.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/SwitchText.java
new file mode 100644
index 0000000..4b54934
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/SwitchText.java
@@ -0,0 +1,63 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+
+public class SwitchText {
+
+ private HashMap valueMap;
+ private String toParse;
+ private String defaultValue;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private SwitchText() {
+
+ }
+
+ public SwitchText(HashMap valueMap, String toParse) {
+ this.valueMap = valueMap;
+ this.toParse = toParse;
+ }
+
+ public String getValue(Player player) {
+ String parsed = PlaceholderAPI.setPlaceholders(player, toParse);
+ return valueMap.getOrDefault(parsed, defaultValue);
+ }
+
+ public static class Builder {
+
+ private final SwitchText switchText;
+
+ public Builder() {
+ this.switchText = new SwitchText();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder toParse(String toParse) {
+ this.switchText.toParse = toParse;
+ return this;
+ }
+
+ public Builder defaultValue(String value) {
+ this.switchText.defaultValue = value;
+ return this;
+ }
+
+ public Builder valueMap(HashMap valueMap) {
+ this.switchText.valueMap = valueMap;
+ return this;
+ }
+
+ public SwitchText build() {
+ return switchText;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/VanillaHud.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/VanillaHud.java
new file mode 100644
index 0000000..6a8d137
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/placeholder/VanillaHud.java
@@ -0,0 +1,58 @@
+package net.momirealms.customnameplates.api.mechanic.placeholder;
+
+import me.clip.placeholderapi.PlaceholderAPI;
+import net.momirealms.customnameplates.api.CustomNameplatesPlugin;
+import net.momirealms.customnameplates.api.util.FontUtils;
+import net.momirealms.customnameplates.api.util.LogUtils;
+import org.bukkit.entity.Player;
+
+public class VanillaHud {
+
+ private final char empty;
+ private final char half;
+ private final char full;
+ private final String maxPapi;
+ private final String currentPapi;
+ private final boolean reverse;
+
+ public VanillaHud(String empty, String half, String full, String maxPapi, String currentPapi, boolean reverse) {
+ this.empty = CustomNameplatesPlugin.get().getImageManager().getImage(empty).getCharacter();
+ this.half = CustomNameplatesPlugin.get().getImageManager().getImage(half).getCharacter();
+ this.full = CustomNameplatesPlugin.get().getImageManager().getImage(full).getCharacter();
+ this.maxPapi = maxPapi;
+ this.currentPapi = currentPapi;
+ this.reverse = reverse;
+ }
+
+ public String getValue(Player player) {
+ double current;
+ double max;
+ try {
+ current= Double.parseDouble(PlaceholderAPI.setPlaceholders(player, currentPapi));
+ max = Double.parseDouble(PlaceholderAPI.setPlaceholders(player, maxPapi));
+ } catch (NumberFormatException e) {
+ current = 1;
+ max = 1;
+ LogUtils.warn("Invalid number format when parsing: " + currentPapi + "/" + maxPapi);
+ }
+ if (current >= max) current = max;
+ if (current < 0) current = 0;
+ int point = (int) ((current / max) * 20);
+ int full_amount = point / 2;
+ int half_amount = point % 2;
+ int empty_amount = 10 - full_amount - half_amount;
+ StringBuilder builder = new StringBuilder();
+ if (reverse) {
+ builder
+ .append(String.valueOf(empty).repeat(empty_amount))
+ .append(String.valueOf(half).repeat(half_amount))
+ .append(String.valueOf(full).repeat(full_amount));
+ } else {
+ builder
+ .append(String.valueOf(full).repeat(full_amount))
+ .append(String.valueOf(half).repeat(half_amount))
+ .append(String.valueOf(empty).repeat(empty_amount));
+ }
+ return FontUtils.surroundNameplateFont(builder.toString());
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/NameplatePlayer.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/NameplatePlayer.java
new file mode 100644
index 0000000..0c33ecf
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/NameplatePlayer.java
@@ -0,0 +1,13 @@
+package net.momirealms.customnameplates.api.mechanic.tag;
+
+import net.momirealms.customnameplates.api.mechanic.nameplate.Nameplate;
+import org.bukkit.entity.Player;
+
+public interface NameplatePlayer {
+
+ void preview();
+
+ void preview(Nameplate nameplate);
+
+ Player getOwner();
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/team/TeamPlayer.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/team/TeamPlayer.java
new file mode 100644
index 0000000..b4813e7
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/team/TeamPlayer.java
@@ -0,0 +1,109 @@
+package net.momirealms.customnameplates.api.mechanic.tag.team;
+
+import net.kyori.adventure.text.Component;
+import net.momirealms.customnameplates.api.CustomNameplatesPlugin;
+import net.momirealms.customnameplates.api.manager.TeamTagManager;
+import net.momirealms.customnameplates.api.mechanic.misc.ViewerText;
+import net.momirealms.customnameplates.api.mechanic.nameplate.Nameplate;
+import net.momirealms.customnameplates.api.mechanic.tag.NameplatePlayer;
+import net.momirealms.customnameplates.api.mechanic.team.TeamColor;
+import net.momirealms.customnameplates.api.mechanic.team.TeamTagVisibility;
+import org.bukkit.entity.Player;
+
+import java.util.Vector;
+
+public class TeamPlayer implements NameplatePlayer {
+
+ private final TeamTagManager manager;
+ private final Player owner;
+ private final ViewerText prefix;
+ private final ViewerText suffix;
+ private final Vector nearbyPlayers;
+
+ public TeamPlayer(TeamTagManager manager, Player owner, String prefix, String suffix) {
+ this.manager = manager;
+ this.owner = owner;
+ this.prefix = new ViewerText(owner, prefix);
+ this.suffix = new ViewerText(owner, suffix);
+ this.nearbyPlayers = new Vector<>();
+ this.prefix.updateForOwner();
+ this.suffix.updateForOwner();
+ }
+
+ public void updateForNearbyPlayers(boolean force) {
+ this.prefix.updateForOwner();
+ this.suffix.updateForOwner();
+ for (Player viewer : nearbyPlayers) {
+ updateForOne(viewer, force);
+ }
+ }
+
+ public void removeNearbyPlayer(Player player) {
+ if (!nearbyPlayers.contains(player)) {
+ return;
+ }
+ nearbyPlayers.remove(player);
+ removeForOne(player);
+ prefix.removeViewer(player);
+ suffix.removeViewer(player);
+ }
+
+ public void addNearbyPlayer(Player player) {
+ if (nearbyPlayers.contains(player)) {
+ return;
+ }
+ nearbyPlayers.add(player);
+ updateForOne(player, false);
+ }
+
+ public void destroy() {
+ manager.removeTeamPlayerFromMap(owner.getUniqueId());
+ for (Player viewer : nearbyPlayers) {
+ removeForOne(viewer);
+ }
+ nearbyPlayers.clear();
+ }
+
+ private void updateForOne(Player viewer, boolean force) {
+ try {
+ if ((prefix.updateForViewer(viewer) | suffix.updateForViewer(viewer)) || force) {
+ CustomNameplatesPlugin.get().getTeamManager().updateTeam(
+ owner,
+ viewer,
+ CustomNameplatesPlugin.get().getAdventure().getComponentFromMiniMessage(prefix.getLatestValue(viewer)),
+ CustomNameplatesPlugin.get().getAdventure().getComponentFromMiniMessage(suffix.getLatestValue(viewer)),
+ CustomNameplatesPlugin.get().getNameplateManager().getTeamColor(owner),
+ TeamTagVisibility.ALWAYS
+ );
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void removeForOne(Player viewer) {
+ CustomNameplatesPlugin.get().getTeamManager().updateTeam(
+ owner,
+ viewer,
+ Component.text(""),
+ Component.text(""),
+ TeamColor.WHITE,
+ TeamTagVisibility.ALWAYS
+ );
+ }
+
+ @Override
+ public void preview() {
+
+ }
+
+ @Override
+ public void preview(Nameplate nameplate) {
+
+ }
+
+ @Override
+ public Player getOwner() {
+ return owner;
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/NamedEntity.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/NamedEntity.java
new file mode 100644
index 0000000..9627010
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/NamedEntity.java
@@ -0,0 +1,60 @@
+package net.momirealms.customnameplates.api.mechanic.tag.unlimited;
+
+import net.momirealms.customnameplates.api.mechanic.misc.ViewerText;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Pose;
+
+import java.util.UUID;
+
+public interface NamedEntity {
+
+ boolean canSee(Player viewer);
+
+ void timer();
+
+ boolean canShow();
+
+ boolean isShownTo(Player viewer);
+
+ void spawn(Player viewer, Pose pose);
+
+ void spawn(Pose pose);
+
+ void destroy();
+
+ void destroy(Player viewer);
+
+ void teleport(double x, double y, double z, boolean onGround);
+
+ void teleport(Player viewer, double x, double y, double z, boolean onGround);
+
+ void setSneak(boolean sneaking, boolean respawn);
+
+ void removePlayerFromViewers(Player player);
+
+ void addPlayerToViewers(Player player);
+
+ double getOffset();
+
+ void setOffset(double v);
+
+ ViewerText getViewerText();
+
+ int getEntityId();
+
+ void move(short x, short y, short z, boolean onGround);
+
+ void move(Player viewer, short x, short y, short z, boolean onGround);
+
+ void respawn(Player viewer, Pose pose);
+
+ void respawn(Pose pose);
+
+ void updateText();
+
+ void updateText(Player viewer);
+
+ UUID getUuid();
+
+ void handlePose(Pose previous, Pose pose);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedObject.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedObject.java
new file mode 100644
index 0000000..f31689f
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedObject.java
@@ -0,0 +1,39 @@
+package net.momirealms.customnameplates.api.mechanic.tag.unlimited;
+
+import net.momirealms.customnameplates.api.manager.UnlimitedTagManager;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Pose;
+
+import java.util.Vector;
+
+public abstract class UnlimitedObject {
+
+ protected final UnlimitedTagManager manager;
+ protected final Entity entity;
+ protected final Vector nearbyPlayers;
+
+ public UnlimitedObject(UnlimitedTagManager manager, Entity entity) {
+ this.manager = manager;
+ this.entity = entity;
+ this.nearbyPlayers = new Vector<>();
+ }
+
+ public Vector getNearbyPlayers() {
+ return nearbyPlayers;
+ }
+
+ public abstract void addNearbyPlayer(Player player);
+
+ public abstract void removeNearbyPlayer(Player player);
+
+ public abstract void move(Player receiver, short x, short y, short z, boolean onGround);
+
+ public abstract void teleport(Player receiver, double x, double y, double z, boolean onGround);
+
+ public abstract void destroy();
+
+ public abstract void sneak(boolean sneaking, boolean flying);
+
+ public abstract void handlePose(Pose previous, Pose pose);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedPlayer.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedPlayer.java
new file mode 100644
index 0000000..dd04925
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedPlayer.java
@@ -0,0 +1,201 @@
+package net.momirealms.customnameplates.api.mechanic.tag.unlimited;
+
+import net.kyori.adventure.text.Component;
+import net.momirealms.customnameplates.api.CustomNameplatesPlugin;
+import net.momirealms.customnameplates.api.manager.UnlimitedTagManager;
+import net.momirealms.customnameplates.api.mechanic.nameplate.Nameplate;
+import net.momirealms.customnameplates.api.mechanic.tag.NameplatePlayer;
+import net.momirealms.customnameplates.api.mechanic.team.TeamColor;
+import net.momirealms.customnameplates.api.mechanic.team.TeamTagVisibility;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.Pose;
+
+import java.util.Vector;
+
+public class UnlimitedPlayer extends UnlimitedObject implements NameplatePlayer {
+
+ private final Player owner;
+ private final Vector tags;
+ private double hatOffset;
+
+ public UnlimitedPlayer(UnlimitedTagManager manager, Player player) {
+ super(manager, player);
+ this.owner = player;
+ this.tags = new Vector<>();
+ }
+
+ /**
+ * Add a tag for a player
+ *
+ * @param tag tag
+ */
+ public void addTag(NamedEntity tag) {
+ if (tags.contains(tag)) {
+ return;
+ }
+ tags.add(tag);
+ for (Player all : nearbyPlayers) {
+ if (tag.canShow() && tag.canSee(all)) {
+ tag.addPlayerToViewers(all);
+ }
+ }
+ }
+
+ /**
+ * Remove a tag for a player
+ *
+ * @param tag tag
+ */
+ public void removeTag(NamedEntity tag) {
+ if (tags.remove(tag)) {
+ tag.destroy();
+ }
+ }
+
+ /**
+ * Get the tags that player own
+ *
+ * @return tags
+ */
+ public Vector getTags() {
+ return tags;
+ }
+
+ /**
+ * Set hat offset. This is useful for cosmetics plugins
+ * because hat might hide the name tags
+ *
+ * @param hatOffset hat offset
+ */
+ public void setHatOffset(double hatOffset) {
+ this.hatOffset = hatOffset;
+ }
+
+ @Override
+ public void preview() {
+
+ }
+
+ @Override
+ public void preview(Nameplate nameplate) {
+
+ }
+
+ /**
+ * Get the owner
+ */
+ @Override
+ public Player getOwner() {
+ return owner;
+ }
+
+ /**
+ * Get the hat offset
+ */
+ public double getHatOffset() {
+ return hatOffset;
+ }
+
+ /**
+ * Add a nearby player so he could see the tag
+ * This process is automatically handled by CustomNameplates
+ *
+ * @param player player
+ */
+ @Override
+ public void addNearbyPlayer(Player player) {
+ if (nearbyPlayers.contains(player)) {
+ return;
+ }
+ nearbyPlayers.add(player);
+ addForOne(player);
+ for (NamedEntity tag : tags) {
+ if (tag.canShow() && tag.canSee(player)) {
+ tag.addPlayerToViewers(player);
+ }
+ }
+ }
+
+ /**
+ * Remove a nearby player so he would no longer receive tag updates
+ * This process is automatically handled by CustomNameplates
+ *
+ * @param player player
+ */
+ @Override
+ public void removeNearbyPlayer(Player player) {
+ if (!nearbyPlayers.contains(player)) {
+ return;
+ }
+ super.nearbyPlayers.remove(player);
+ removeForOne(player);
+ for (NamedEntity tag : tags) {
+ tag.removePlayerFromViewers(player);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ manager.removeUnlimitedObjectFromMap(entity.getUniqueId());
+ for (Player viewer : nearbyPlayers) {
+ removeForOne(viewer);
+ }
+ for (NamedEntity tag : tags) {
+ tag.destroy();
+ }
+ nearbyPlayers.clear();
+ tags.clear();
+ }
+
+ public void move(Player receiver, short x, short y, short z, boolean onGround) {
+ for (NamedEntity tag : tags) {
+ tag.move(receiver, x, y, z, onGround);
+ }
+ }
+
+ public void teleport(Player receiver, double x, double y, double z, boolean onGround) {
+ for (NamedEntity tag : tags) {
+ tag.teleport(receiver, x, y, z, onGround);
+ }
+ }
+
+ public void sneak(boolean sneaking, boolean flying) {
+ for (NamedEntity tag : tags) {
+ tag.setSneak(sneaking, !flying);
+ }
+ }
+
+ public void handlePose(Pose previous, Pose pose) {
+ for (NamedEntity tag : tags) {
+ tag.handlePose(previous, pose);
+ }
+ }
+
+ private void addForOne(Player viewer) {
+ CustomNameplatesPlugin.get().getTeamManager().updateTeam(
+ owner,
+ viewer,
+ Component.text(""),
+ Component.text(""),
+ TeamColor.WHITE,
+ TeamTagVisibility.NEVER
+ );
+ }
+
+ private void removeForOne(Player viewer) {
+ CustomNameplatesPlugin.get().getTeamManager().updateTeam(
+ owner,
+ viewer,
+ Component.text(""),
+ Component.text(""),
+ TeamColor.WHITE,
+ TeamTagVisibility.ALWAYS
+ );
+ }
+
+ public void timer() {
+ for (NamedEntity tag : tags) {
+ tag.timer();
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedTagSetting.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedTagSetting.java
new file mode 100644
index 0000000..8905345
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/tag/unlimited/UnlimitedTagSetting.java
@@ -0,0 +1,106 @@
+package net.momirealms.customnameplates.api.mechanic.tag.unlimited;
+
+import net.momirealms.customnameplates.api.requirement.Requirement;
+
+public class UnlimitedTagSetting {
+
+ private double verticalOffset;
+ private String rawText;
+ private int checkFrequency;
+ private int refreshFrequency;
+ private Requirement[] viewerRequirements;
+ private Requirement[] ownerRequirements;
+
+ private UnlimitedTagSetting() {
+ verticalOffset = 0;
+ rawText = "";
+ checkFrequency = 10;
+ refreshFrequency = 10;
+ viewerRequirements = new Requirement[0];
+ ownerRequirements = new Requirement[0];
+ }
+
+ public UnlimitedTagSetting(double verticalOffset, String rawText, int checkFrequency, int refreshFrequency, Requirement[] viewerRequirements, Requirement[] ownerRequirements) {
+ this.verticalOffset = verticalOffset;
+ this.rawText = rawText;
+ this.checkFrequency = checkFrequency;
+ this.refreshFrequency = refreshFrequency;
+ this.viewerRequirements = viewerRequirements;
+ this.ownerRequirements = ownerRequirements;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public double getVerticalOffset() {
+ return verticalOffset;
+ }
+
+ public String getRawText() {
+ return rawText;
+ }
+
+ public int getCheckFrequency() {
+ return checkFrequency;
+ }
+
+ public int getRefreshFrequency() {
+ return refreshFrequency;
+ }
+
+ public Requirement[] getViewerRequirements() {
+ return viewerRequirements;
+ }
+
+ public Requirement[] getOwnerRequirements() {
+ return ownerRequirements;
+ }
+
+ public static class Builder {
+
+ private final UnlimitedTagSetting setting;
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder() {
+ this.setting = new UnlimitedTagSetting();
+ }
+
+ public Builder checkFrequency(int checkFrequency) {
+ this.setting.checkFrequency = checkFrequency;
+ return this;
+ }
+
+ public Builder refreshFrequency(int refreshFrequency) {
+ this.setting.refreshFrequency = refreshFrequency;
+ return this;
+ }
+
+ public Builder rawText(String rawText) {
+ this.setting.rawText = rawText;
+ return this;
+ }
+
+ public Builder verticalOffset(double verticalOffset) {
+ this.setting.verticalOffset = verticalOffset;
+ return this;
+ }
+
+ public Builder ownerRequirements(Requirement[] ownerRequirements) {
+ this.setting.ownerRequirements = ownerRequirements;
+ return this;
+ }
+
+ public Builder viewerRequirements(Requirement[] viewerRequirements) {
+ this.setting.viewerRequirements = viewerRequirements;
+ return this;
+ }
+
+ public UnlimitedTagSetting build() {
+ return setting;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCollisionRule.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCollisionRule.java
new file mode 100644
index 0000000..341ce87
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCollisionRule.java
@@ -0,0 +1,31 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+
+public enum TeamCollisionRule {
+
+ ALWAYS("always"),
+ NEVER("never"),
+ PUSH_OTHER_TEAMS("pushOtherTeams"),
+ PUSH_OWN_TEAM("pushOwnTeam");
+
+ private final String id;
+
+ TeamCollisionRule(@NotNull String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @NotNull
+ public static TeamCollisionRule byId(String id) {
+ return Arrays.stream(values())
+ .filter(mode -> mode.id.equals(id))
+ .findFirst()
+ .orElse(ALWAYS);
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamColor.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamColor.java
new file mode 100644
index 0000000..fb20fdf
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamColor.java
@@ -0,0 +1,29 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+import java.util.Locale;
+
+public enum TeamColor {
+
+ NONE,
+ BLACK,
+ DARK_BLUE,
+ DARK_GREEN,
+ DARK_AQUA,
+ DARK_RED,
+ DARK_PURPLE,
+ GOLD,
+ GRAY,
+ DARK_GRAY,
+ BLUE,
+ GREEN,
+ AQUA,
+ RED,
+ LIGHT_PURPLE,
+ YELLOW,
+ WHITE,
+ CUSTOM;
+
+ public TeamColor getById(String id) throws IllegalArgumentException {
+ return valueOf(id.toUpperCase(Locale.ENGLISH));
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCreatePacket.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCreatePacket.java
new file mode 100644
index 0000000..d1cede2
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamCreatePacket.java
@@ -0,0 +1,154 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+import net.kyori.adventure.text.Component;
+
+import java.util.Collection;
+import java.util.Collections;
+
+public class TeamCreatePacket {
+
+ // String i
+ private String teamName;
+ // Collection j
+ private Collection members;
+
+ /*
+ Optional k
+ */
+ // IChatBaseComponent a
+ private Component teamDisplay;
+ // IChatBaseComponent b
+ private Component teamPrefix;
+ // IChatBaseComponent c
+ private Component teamSuffix;
+ // String d
+ private TeamTagVisibility tagVisibility;
+ // String e
+ private TeamCollisionRule collisionRule;
+ // Enum f
+ private TeamColor teamColor;
+
+ private TeamCreatePacket() {
+ this.teamName = "";
+ this.members = Collections.singleton("");
+ this.teamDisplay = Component.text("");
+ this.teamPrefix = Component.text("");
+ this.teamSuffix = Component.text("");
+ this.tagVisibility = TeamTagVisibility.ALWAYS;
+ this.collisionRule = TeamCollisionRule.ALWAYS;
+ this.teamColor = TeamColor.WHITE;
+ }
+
+ public TeamCreatePacket(
+ String teamName,
+ Collection members,
+ Component teamDisplay,
+ Component teamPrefix,
+ Component teamSuffix,
+ TeamTagVisibility tagVisibility,
+ TeamCollisionRule collisionRule,
+ TeamColor teamColor
+ ) {
+ this.teamName = teamName;
+ this.members = members;
+ this.teamDisplay = teamDisplay;
+ this.teamPrefix = teamPrefix;
+ this.teamSuffix = teamSuffix;
+ this.tagVisibility = tagVisibility;
+ this.collisionRule = collisionRule;
+ this.teamColor = teamColor;
+ }
+
+ public String getTeamName() {
+ return teamName;
+ }
+
+ public Collection getMembers() {
+ return members;
+ }
+
+ public Component getTeamDisplay() {
+ return teamDisplay;
+ }
+
+ public Component getTeamPrefix() {
+ return teamPrefix;
+ }
+
+ public Component getTeamSuffix() {
+ return teamSuffix;
+ }
+
+ public TeamTagVisibility getTagVisibility() {
+ return tagVisibility;
+ }
+
+ public TeamCollisionRule getCollisionRule() {
+ return collisionRule;
+ }
+
+ public TeamColor getTeamColor() {
+ return teamColor;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final TeamCreatePacket packet;
+
+ public Builder() {
+ this.packet = new TeamCreatePacket();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder teamName(String name) {
+ packet.teamName = name;
+ return this;
+ }
+
+ public Builder members(Collection members) {
+ packet.members = members;
+ return this;
+ }
+
+ public Builder display(Component display) {
+ packet.teamDisplay = display;
+ return this;
+ }
+
+ public Builder prefix(Component prefix) {
+ packet.teamPrefix = prefix;
+ return this;
+ }
+
+ public Builder suffix(Component suffix) {
+ packet.teamSuffix = suffix;
+ return this;
+ }
+
+ public Builder color(TeamColor color) {
+ packet.teamColor = color;
+ return this;
+ }
+
+ public Builder tagVisibility(TeamTagVisibility visibility) {
+ packet.tagVisibility = visibility;
+ return this;
+ }
+
+ public Builder collisionRule(TeamCollisionRule rule) {
+ packet.collisionRule = rule;
+ return this;
+ }
+
+ public TeamCreatePacket build() {
+ return packet;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamRemovePacket.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamRemovePacket.java
new file mode 100644
index 0000000..60d3038
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamRemovePacket.java
@@ -0,0 +1,39 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+public class TeamRemovePacket {
+
+ private String teamName;
+
+ private TeamRemovePacket() {
+ }
+
+ public TeamRemovePacket(String teamName) {
+ this.teamName = teamName;
+ }
+
+ public String getTeamName() {
+ return teamName;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final TeamRemovePacket packet;
+
+ public Builder() {
+ this.packet = new TeamRemovePacket();
+ }
+
+ public Builder teamName(String teamName) {
+ packet.teamName = teamName;
+ return this;
+ }
+
+ public TeamRemovePacket build() {
+ return packet;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamTagVisibility.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamTagVisibility.java
new file mode 100644
index 0000000..421d983
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamTagVisibility.java
@@ -0,0 +1,31 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Arrays;
+
+public enum TeamTagVisibility {
+
+ ALWAYS("always"),
+ HIDE_FOR_OTHER_TEAMS("hideForOtherTeams"),
+ HIDE_FOR_OWN_TEAM("hideForOwnTeam"),
+ NEVER("never");
+
+ private final String id;
+
+ TeamTagVisibility(@NotNull String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @NotNull
+ public static TeamTagVisibility byId(String id) {
+ return Arrays.stream(values())
+ .filter(mode -> mode.id.equals(id))
+ .findFirst()
+ .orElse(ALWAYS);
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamUpdatePacket.java b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamUpdatePacket.java
new file mode 100644
index 0000000..4b53360
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/mechanic/team/TeamUpdatePacket.java
@@ -0,0 +1,136 @@
+package net.momirealms.customnameplates.api.mechanic.team;
+
+import net.kyori.adventure.text.Component;
+
+public class TeamUpdatePacket {
+
+ private String teamName;
+
+ /*
+ Optional k
+ */
+ // IChatBaseComponent a
+ private Component teamDisplay;
+ // IChatBaseComponent b
+ private Component teamPrefix;
+ // IChatBaseComponent c
+ private Component teamSuffix;
+ // String d
+ private TeamTagVisibility tagVisibility;
+ // String e
+ private TeamCollisionRule collisionRule;
+ // Enum f
+ private TeamColor teamColor;
+
+ private TeamUpdatePacket() {
+ this.teamName = "";
+ this.teamDisplay = Component.text("");
+ this.teamPrefix = Component.text("");
+ this.teamSuffix = Component.text("");
+ this.tagVisibility = TeamTagVisibility.ALWAYS;
+ this.collisionRule = TeamCollisionRule.ALWAYS;
+ this.teamColor = TeamColor.WHITE;
+ }
+
+ public TeamUpdatePacket(
+ String teamName,
+ Component teamDisplay,
+ Component teamPrefix,
+ Component teamSuffix,
+ TeamTagVisibility tagVisibility,
+ TeamCollisionRule collisionRule,
+ TeamColor teamColor
+ ) {
+ this.teamName = teamName;
+ this.teamDisplay = teamDisplay;
+ this.teamPrefix = teamPrefix;
+ this.teamSuffix = teamSuffix;
+ this.tagVisibility = tagVisibility;
+ this.collisionRule = collisionRule;
+ this.teamColor = teamColor;
+ }
+
+ public String getTeamName() {
+ return teamName;
+ }
+
+ public Component getTeamDisplay() {
+ return teamDisplay;
+ }
+
+ public Component getTeamPrefix() {
+ return teamPrefix;
+ }
+
+ public Component getTeamSuffix() {
+ return teamSuffix;
+ }
+
+ public TeamTagVisibility getTagVisibility() {
+ return tagVisibility;
+ }
+
+ public TeamCollisionRule getCollisionRule() {
+ return collisionRule;
+ }
+
+ public TeamColor getTeamColor() {
+ return teamColor;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private final TeamUpdatePacket packet;
+
+ public Builder() {
+ this.packet = new TeamUpdatePacket();
+ }
+
+ public static Builder of() {
+ return new Builder();
+ }
+
+ public Builder teamName(String name) {
+ packet.teamName = name;
+ return this;
+ }
+
+ public Builder display(Component display) {
+ packet.teamDisplay = display;
+ return this;
+ }
+
+ public Builder prefix(Component prefix) {
+ packet.teamPrefix = prefix;
+ return this;
+ }
+
+ public Builder suffix(Component suffix) {
+ packet.teamSuffix = suffix;
+ return this;
+ }
+
+ public Builder color(TeamColor color) {
+ packet.teamColor = color;
+ return this;
+ }
+
+ public Builder tagVisibility(TeamTagVisibility visibility) {
+ packet.tagVisibility = visibility;
+ return this;
+ }
+
+ public Builder collisionRule(TeamCollisionRule rule) {
+ packet.collisionRule = rule;
+ return this;
+ }
+
+ public TeamUpdatePacket build() {
+ return packet;
+ }
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/requirement/Condition.java b/api/src/main/java/net/momirealms/customnameplates/api/requirement/Condition.java
new file mode 100644
index 0000000..60238bb
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/requirement/Condition.java
@@ -0,0 +1,16 @@
+package net.momirealms.customnameplates.api.requirement;
+
+import org.bukkit.OfflinePlayer;
+
+public class Condition {
+
+ private final OfflinePlayer player;
+
+ public Condition(OfflinePlayer player) {
+ this.player = player;
+ }
+
+ public OfflinePlayer getOfflinePlayer() {
+ return player;
+ }
+}
diff --git a/src/main/java/net/momirealms/customnameplates/object/requirements/Requirement.java b/api/src/main/java/net/momirealms/customnameplates/api/requirement/Requirement.java
similarity index 75%
rename from src/main/java/net/momirealms/customnameplates/object/requirements/Requirement.java
rename to api/src/main/java/net/momirealms/customnameplates/api/requirement/Requirement.java
index 8ee8b79..9424121 100644
--- a/src/main/java/net/momirealms/customnameplates/object/requirements/Requirement.java
+++ b/api/src/main/java/net/momirealms/customnameplates/api/requirement/Requirement.java
@@ -15,10 +15,15 @@
* along with this program. If not, see .
*/
-package net.momirealms.customnameplates.object.requirements;
-
-import org.bukkit.entity.Player;
+package net.momirealms.customnameplates.api.requirement;
public interface Requirement {
- boolean isConditionMet(Player player);
+
+ /**
+ * Is condition met the requirement
+ *
+ * @param condition condition
+ * @return meet or not
+ */
+ boolean isConditionMet(Condition condition);
}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementExpansion.java b/api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementExpansion.java
new file mode 100644
index 0000000..df9ce14
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementExpansion.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) <2022>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package net.momirealms.customnameplates.api.requirement;
+
+/**
+ * An abstract class representing a requirement expansion
+ * Requirement expansions are used to define custom requirements for various functionalities.
+ */
+public abstract class RequirementExpansion {
+
+ /**
+ * Get the version of this requirement expansion.
+ *
+ * @return The version of the expansion.
+ */
+ public abstract String getVersion();
+
+ /**
+ * Get the author of this requirement expansion.
+ *
+ * @return The author of the expansion.
+ */
+ public abstract String getAuthor();
+
+ /**
+ * Get the type of requirement provided by this expansion.
+ *
+ * @return The type of requirement.
+ */
+ public abstract String getRequirementType();
+
+ /**
+ * Get the factory for creating requirements defined by this expansion.
+ *
+ * @return The requirement factory.
+ */
+ public abstract RequirementFactory getRequirementFactory();
+}
diff --git a/src/main/java/net/momirealms/customnameplates/object/Function.java b/api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementFactory.java
similarity index 64%
rename from src/main/java/net/momirealms/customnameplates/object/Function.java
rename to api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementFactory.java
index 8b0061b..74b2840 100644
--- a/src/main/java/net/momirealms/customnameplates/object/Function.java
+++ b/api/src/main/java/net/momirealms/customnameplates/api/requirement/RequirementFactory.java
@@ -15,24 +15,18 @@
* along with this program. If not, see .
*/
-package net.momirealms.customnameplates.object;
+package net.momirealms.customnameplates.api.requirement;
-import org.bukkit.entity.Player;
+/**
+ * An interface for a requirement factory that builds requirements.
+ */
+public interface RequirementFactory {
-public abstract class Function {
-
- public void load() {
- }
-
- public void unload() {
- }
-
- public void disable() {
- }
-
- public void onJoin(Player player) {
- }
-
- public void onQuit(Player player) {
- }
+ /**
+ * Build a requirement with the given arguments.
+ *
+ * @param args The arguments used to build the requirement.
+ * @return The built requirement.
+ */
+ Requirement build(Object args);
}
diff --git a/src/main/java/net/momirealms/customnameplates/object/scheduler/TimerTask.java b/api/src/main/java/net/momirealms/customnameplates/api/scheduler/CancellableTask.java
similarity index 76%
rename from src/main/java/net/momirealms/customnameplates/object/scheduler/TimerTask.java
rename to api/src/main/java/net/momirealms/customnameplates/api/scheduler/CancellableTask.java
index 793de07..d226bf8 100644
--- a/src/main/java/net/momirealms/customnameplates/object/scheduler/TimerTask.java
+++ b/api/src/main/java/net/momirealms/customnameplates/api/scheduler/CancellableTask.java
@@ -15,11 +15,19 @@
* along with this program. If not, see .
*/
-package net.momirealms.customnameplates.object.scheduler;
+package net.momirealms.customnameplates.api.scheduler;
-public interface TimerTask {
+public interface CancellableTask {
+ /**
+ * Cancel the task
+ */
void cancel();
+ /**
+ * Get if the task is cancelled or not
+ *
+ * @return cancelled or not
+ */
boolean isCancelled();
}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/scheduler/Scheduler.java b/api/src/main/java/net/momirealms/customnameplates/api/scheduler/Scheduler.java
new file mode 100644
index 0000000..9b20769
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/scheduler/Scheduler.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) <2022>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package net.momirealms.customnameplates.api.scheduler;
+
+import org.bukkit.Location;
+
+import java.util.concurrent.TimeUnit;
+
+public interface Scheduler {
+
+ /**
+ * Runs a task synchronously on the main server thread or region thread.
+ *
+ * @param runnable The task to run.
+ * @param location The location associated with the task.
+ */
+ void runTaskSync(Runnable runnable, Location location);
+
+ /**
+ * Runs a task synchronously with a specified delay and period.
+ *
+ * @param runnable The task to run.
+ * @param location The location associated with the task.
+ * @param delayTicks The delay in ticks before the first execution.
+ * @param periodTicks The period between subsequent executions in ticks.
+ * @return A CancellableTask for managing the scheduled task.
+ */
+ CancellableTask runTaskSyncTimer(Runnable runnable, Location location, long delayTicks, long periodTicks);
+
+ /**
+ * Runs a task asynchronously with a specified delay.
+ *
+ * @param runnable The task to run.
+ * @param delay The delay before the task execution.
+ * @param timeUnit The time unit for the delay.
+ * @return A CancellableTask for managing the scheduled task.
+ */
+ CancellableTask runTaskAsyncLater(Runnable runnable, long delay, TimeUnit timeUnit);
+
+ /**
+ * Runs a task asynchronously.
+ *
+ * @param runnable The task to run.
+ */
+ void runTaskAsync(Runnable runnable);
+
+ /**
+ * Runs a task synchronously with a specified delay.
+ *
+ * @param runnable The task to run.
+ * @param location The location associated with the task.
+ * @param delay The delay before the task execution.
+ * @param timeUnit The time unit for the delay.
+ * @return A CancellableTask for managing the scheduled task.
+ */
+ CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delay, TimeUnit timeUnit);
+
+ /**
+ * Runs a task synchronously with a specified delay in ticks.
+ *
+ * @param runnable The task to run.
+ * @param location The location associated with the task.
+ * @param delayTicks The delay in ticks before the task execution.
+ * @return A CancellableTask for managing the scheduled task.
+ */
+ CancellableTask runTaskSyncLater(Runnable runnable, Location location, long delayTicks);
+
+ /**
+ * Runs a task asynchronously with a specified delay and period.
+ *
+ * @param runnable The task to run.
+ * @param delay The delay before the first execution.
+ * @param period The period between subsequent executions.
+ * @param timeUnit The time unit for the delay and period.
+ * @return A CancellableTask for managing the scheduled task.
+ */
+ CancellableTask runTaskAsyncTimer(Runnable runnable, long delay, long period, TimeUnit timeUnit);
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/util/FontUtils.java b/api/src/main/java/net/momirealms/customnameplates/api/util/FontUtils.java
new file mode 100644
index 0000000..c1afdd0
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/util/FontUtils.java
@@ -0,0 +1,76 @@
+package net.momirealms.customnameplates.api.util;
+
+public class FontUtils {
+
+ private static String namespace;
+ private static String font;
+
+ /**
+ * Surround the text with ascent font
+ *
+ * @param text text
+ * @param ascent ascent
+ * @return ascent font text
+ */
+ public static String surroundAscentFont(String text, int ascent) {
+ return getAscentFontTag(ascent) + text + getFontTagCloser();
+ }
+
+ /**
+ * Surround the text with ascent unicode
+ *
+ * @param text text
+ * @param ascent ascent
+ * @return ascent font text
+ */
+ public static String surroundAscentUnicodeFont(String text, int ascent) {
+ return getAscentUnicodeFontTag(ascent) + text + getFontTagCloser();
+ }
+
+ /**
+ * Surround the text with custom nameplates font
+ *
+ * @param text text
+ * @return font text
+ */
+ public static String surroundNameplateFont(String text) {
+ return getMiniMessageFontTag() + text + getFontTagCloser();
+ }
+
+ private static String getAscentFontTag(int ascent) {
+ return "";
+ }
+
+ private static String getAscentUnicodeFontTag(int ascent) {
+ return "";
+ }
+
+ private static String getMiniMessageFontTag() {
+ return "";
+ }
+
+ private static String getFontTagCloser() {
+ return "";
+ }
+
+ /**
+ * Get a text's width
+ *
+ * @param text text
+ * @return width
+ */
+ public static int getTextWidth(String text) {
+ return 0;
+ }
+
+ /**
+ * Set namespace and font
+ *
+ * @param n namespace
+ * @param f font
+ */
+ public static void setNameSpaceAndFont(String n, String f) {
+ namespace = n;
+ font = f;
+ }
+}
diff --git a/api/src/main/java/net/momirealms/customnameplates/api/util/LogUtils.java b/api/src/main/java/net/momirealms/customnameplates/api/util/LogUtils.java
new file mode 100644
index 0000000..dde8f82
--- /dev/null
+++ b/api/src/main/java/net/momirealms/customnameplates/api/util/LogUtils.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) <2022>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package net.momirealms.customnameplates.api.util;
+
+import net.momirealms.customnameplates.api.CustomNameplatesPlugin;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.logging.Level;
+
+public final class LogUtils {
+
+ /**
+ * Log an informational message.
+ *
+ * @param message The message to log.
+ */
+ public static void info(@NotNull String message) {
+ CustomNameplatesPlugin.getInstance().getLogger().info(message);
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * @param message The message to log.
+ */
+ public static void warn(@NotNull String message) {
+ CustomNameplatesPlugin.getInstance().getLogger().warning(message);
+ }
+
+ /**
+ * Log a severe error message.
+ *
+ * @param message The message to log.
+ */
+ public static void severe(@NotNull String message) {
+ CustomNameplatesPlugin.getInstance().getLogger().severe(message);
+ }
+
+ /**
+ * Log a warning message with a throwable exception.
+ *
+ * @param message The message to log.
+ * @param throwable The throwable exception to log.
+ */
+ public static void warn(@NotNull String message, Throwable throwable) {
+ CustomNameplatesPlugin.getInstance().getLogger().log(Level.WARNING, message, throwable);
+ }
+
+ /**
+ * Log a severe error message with a throwable exception.
+ *
+ * @param message The message to log.
+ * @param throwable The throwable exception to log.
+ */
+ public static void severe(@NotNull String message, Throwable throwable) {
+ CustomNameplatesPlugin.getInstance().getLogger().log(Level.SEVERE, message, throwable);
+ }
+
+ private LogUtils() {
+ throw new UnsupportedOperationException("This class cannot be instantiated");
+ }
+}
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 28a7e0e..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,81 +0,0 @@
-plugins {
- id 'java'
- id 'com.github.johnrengelman.shadow' version '7.1.2'
-}
-
-group = 'net.momirealms'
-version = '2.2.3.18.1'
-
-repositories {
- maven {name = "aliyun-repo"; url = "https://maven.aliyun.com/repository/public/"}
- maven {name = "sk89q-repo"; url = "https://maven.enginehub.org/repo/"}
- maven {name = 'sonatype'; url = 'https://oss.sonatype.org/content/groups/public/'}
- maven {name = 'sonatype-snapshot'; url = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'}
- maven {name = "dmulloy2-repo"; url = "https://repo.dmulloy2.net/repository/public/"}
- maven {name = "clip-repo"; url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/'}
- maven {name = "jitpack"; url = 'https://jitpack.io'}
- maven {name = "codecrafter47-repo"; url = 'https://nexus.codecrafter47.de/content/repositories/public/'}
- maven {name = "opencollab-snapshot-repo"; url = 'https://repo.opencollab.dev/main/'}
- maven {name = 'papermc-repo'; url = 'https://papermc.io/repo/repository/maven-public/'}
- maven {name = 'William278-repo'; url = 'https://repo.william278.net/releases/'}
- maven {name = 'kryptonmc-repo'; url = 'https://repo.kryptonmc.org/releases'}
- mavenCentral()
-}
-
-dependencies {
- compileOnly fileTree(dir:'libs',includes:['*.jar'])
- compileOnly('dev.folia:folia-api:1.20.1-R0.1-SNAPSHOT')
- compileOnly ('me.clip:placeholderapi:2.11.3')
- compileOnly ('com.zaxxer:HikariCP:5.0.1')
- compileOnly ('commons-io:commons-io:2.11.0')
- compileOnly ('dev.dejvokep:boosted-yaml:1.3.1')
- compileOnly ("com.comphenix.protocol:ProtocolLib:5.1.0")
- compileOnly ('net.md-5:bungeecord-api:1.19-R0.1-SNAPSHOT')
- compileOnly ('com.github.LoneDev6:api-itemsadder:3.4.1-r4')
- compileOnly ("com.velocitypowered:velocity-api:3.1.1")
- compileOnly ('org.geysermc.geyser:api:2.2.0-SNAPSHOT')
- compileOnly ("net.william278:velocitab:1.5.1")
- compileOnly ('me.neznamy:tab-api:4.0.2')
- compileOnly ('com.github.FrancoBM12:API-MagicCosmetics:2.2.5')
- annotationProcessor ('com.velocitypowered:velocity-api:3.1.1')
-
- compileOnly ('org.apache.commons:commons-lang3:3.12.0')
- implementation ('net.kyori:adventure-api:4.14.0')
- implementation ('net.kyori:adventure-platform-bukkit:4.3.3-SNAPSHOT')
- implementation ('net.kyori:adventure-text-minimessage:4.14.0')
- implementation ('net.kyori:adventure-text-serializer-gson:4.14.0')
- implementation ("org.bstats:bstats-bukkit:3.0.1")
- implementation fileTree (dir:'libs',includes:['BiomeAPI.jar'])
-}
-
-def targetJavaVersion = 17
-java {
- def javaVersion = JavaVersion.toVersion(targetJavaVersion)
- sourceCompatibility = javaVersion
- targetCompatibility = javaVersion
-}
-
-tasks.withType(JavaCompile).configureEach {
- options.release = targetJavaVersion
- options.encoding = "UTF-8"
-}
-
-processResources {
- def props = [version: version]
- inputs.properties props
- filteringCharset 'UTF-8'
- filesMatching('plugin.yml') {
- expand props
- }
-}
-
-shadowJar {
- relocate ('net.kyori', 'net.momirealms.customnameplates.libs.net.kyori')
- relocate ('org.bstats', 'net.momirealms.customnameplates.libs.org.bstats')
- relocate ('net.momirealms.biomeapi', 'net.momirealms.customnameplates.libs.net.momirealms.biomeapi')
-}
-
-tasks.register("delete", Delete).get().delete("build/libs/"+project.name+"-"+project.version+".jar")
-tasks.named("build").get().dependsOn("shadowJar").finalizedBy("delete").doLast {
- println("Deleting: "+ "build/libs/"+project.name+"-"+project.version+".jar")
-}
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..1241825
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,80 @@
+plugins {
+ id("java")
+ id("application")
+ id("maven-publish")
+ id("com.github.johnrengelman.shadow") version "8.1.1"
+}
+
+allprojects {
+
+ version = "2.3.0.0"
+
+ apply()
+ apply(plugin = "java")
+ apply(plugin = "application")
+ apply(plugin = "com.github.johnrengelman.shadow")
+ apply(plugin = "org.gradle.maven-publish")
+
+ application {
+ mainClass.set("")
+ }
+
+ repositories {
+ maven("https://maven.aliyun.com/repository/public/")
+ mavenCentral()
+ maven("https://papermc.io/repo/repository/maven-public/")
+ maven("https://oss.sonatype.org/content/groups/public/")
+ maven("https://repo.dmulloy2.net/repository/public/")
+ maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
+ maven("https://repo.codemc.org/repository/maven-public/")
+ maven("https://maven.enginehub.org/repo/")
+ maven("https://jitpack.io/")
+ maven("https://mvn.lumine.io/repository/maven-public/")
+ maven("https://repo.rapture.pw/repository/maven-releases/")
+ maven("https://nexus.phoenixdevt.fr/repository/maven-public/")
+ maven("https://r.irepo.space/maven/")
+ maven("https://repo.auxilor.io/repository/maven-public/")
+ maven("https://betonquest.org/nexus/repository/betonquest/")
+ maven("https://repo.william278.net/releases/")
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+ maven("https://repo.minebench.de/")
+ maven("https://repo.xenondevs.xyz/releases/")
+ maven("https://repo.kryptonmc.org/releases")
+ maven("https://repo.oraxen.com/releases")
+ }
+}
+
+subprojects {
+ tasks.processResources {
+ val props = mapOf("version" to version)
+ inputs.properties(props)
+ filteringCharset = "UTF-8"
+ filesMatching("*plugin.yml") {
+ expand(props)
+ }
+ }
+
+ tasks.withType {
+ options.encoding = "UTF-8"
+ options.release.set(17)
+ }
+
+ tasks.shadowJar {
+ destinationDirectory.set(file("$rootDir/target"))
+ archiveClassifier.set("")
+ archiveFileName.set("CustomNameplates-" + project.name + "-" + project.version + ".jar")
+ }
+
+ if ("api" == project.name) {
+ publishing {
+ publications {
+ create("mavenJava") {
+ groupId = "net.momirealms"
+ artifactId = "CustomNameplates"
+ version = rootProject.version.toString()
+ artifact(tasks.shadowJar)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/bungeecord/.gitignore b/bungeecord/.gitignore
new file mode 100644
index 0000000..b63da45
--- /dev/null
+++ b/bungeecord/.gitignore
@@ -0,0 +1,42 @@
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/bungeecord/build.gradle.kts b/bungeecord/build.gradle.kts
new file mode 100644
index 0000000..0ce6a16
--- /dev/null
+++ b/bungeecord/build.gradle.kts
@@ -0,0 +1,3 @@
+dependencies {
+
+}
\ No newline at end of file
diff --git a/bungeecord/src/main/resources/plugin.yml b/bungeecord/src/main/resources/plugin.yml
new file mode 100644
index 0000000..5bf5d9b
--- /dev/null
+++ b/bungeecord/src/main/resources/plugin.yml
@@ -0,0 +1,9 @@
+name: CustomNameplates
+version: '${version}'
+main: net.momirealms.customnameplates.paper.Main
+api-version: 1.17
+authors: [ XiaoMoMi ]
+folia-supported: true
+depend:
+ - ProtocolLib
+ - PlaceholderAPI
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..c1962a79e29d3e0ab67b14947c167a862655af9b 100644
GIT binary patch
delta 40133
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