From 1072b27b21461601100df01e9d3cd05a155f0f3e Mon Sep 17 00:00:00 2001 From: Kalle Struik Date: Sat, 8 Apr 2023 23:35:25 +0200 Subject: [PATCH] Lots of things --- .gitignore | 5 + build.gradle | 23 +- gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew.bat | 168 ++++++------ .../nametagedit/plugin/NametagManager.java | 167 ++++++++++++ .../nametagedit/plugin/api/INametagApi.java | 121 +++++++++ .../nametagedit/plugin/api/NametagAPI.java | 98 +++++++ .../nametagedit/plugin/api/data/FakeTeam.java | 66 +++++ .../plugin/api/data/GroupData.java | 37 +++ .../nametagedit/plugin/api/data/INametag.java | 11 + .../nametagedit/plugin/api/data/Nametag.java | 13 + .../plugin/api/data/PlayerData.java | 45 ++++ .../plugin/api/events/NametagEvent.java | 101 +++++++ .../api/events/NametagFirstLoadedEvent.java | 32 +++ .../plugin/packets/PacketAccessor.java | 146 +++++++++++ .../plugin/packets/PacketData.java | 31 +++ .../plugin/packets/PacketWrapper.java | 119 +++++++++ .../plugin/utils/Configuration.java | 247 ++++++++++++++++++ .../nametagedit/plugin/utils/UUIDFetcher.java | 117 +++++++++ .../com/nametagedit/plugin/utils/Utils.java | 88 +++++++ .../chat/Commands/CommandAdminChat.java | 2 +- .../chat/Commands/CommandAlert.java | 48 ++++ .../chat/Commands/CommandBuilderChat.java | 2 +- .../chat/Commands/CommandStaffChat.java | 2 +- .../java/com/tidefactions/chat/Config.java | 60 ++++- .../chat/Databases/PrefixDatabase.java | 86 +++--- .../chat/EventHandlers/ChatHandler.java | 30 ++- .../chat/EventHandlers/JoinHandler.java | 18 ++ .../Events/MessageSendInChannelEvent.java | 5 + .../com/tidefactions/chat/GUI/PrefixGui.java | 15 +- .../chat/Intergration/Discord.java | 57 ++++ src/main/java/com/tidefactions/chat/Main.java | 14 +- .../java/com/tidefactions/chat/Messages.java | 8 + .../com/tidefactions/chat/Types/ChatMode.java | 18 +- .../com/tidefactions/chat/Types/Prefix.java | 20 +- .../tidefactions/chat/Utils/ChatUtils.java | 60 +++-- .../tidefactions/chat/Utils/FormatUtils.java | 35 +++ .../tidefactions/chat/Utils/NameUtils.java | 22 ++ .../tidefactions/chat/Utils/PrefixUtils.java | 7 +- src/main/resources/config.yml | 35 ++- src/main/resources/messages.yml | 3 + src/main/resources/plugin.yml | 5 +- src/main/resources/prefixes.yml | 24 ++ 44 files changed, 2036 insertions(+), 181 deletions(-) create mode 100644 .gitignore create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 src/main/java/com/nametagedit/plugin/NametagManager.java create mode 100644 src/main/java/com/nametagedit/plugin/api/INametagApi.java create mode 100644 src/main/java/com/nametagedit/plugin/api/NametagAPI.java create mode 100644 src/main/java/com/nametagedit/plugin/api/data/FakeTeam.java create mode 100644 src/main/java/com/nametagedit/plugin/api/data/GroupData.java create mode 100644 src/main/java/com/nametagedit/plugin/api/data/INametag.java create mode 100644 src/main/java/com/nametagedit/plugin/api/data/Nametag.java create mode 100644 src/main/java/com/nametagedit/plugin/api/data/PlayerData.java create mode 100644 src/main/java/com/nametagedit/plugin/api/events/NametagEvent.java create mode 100644 src/main/java/com/nametagedit/plugin/api/events/NametagFirstLoadedEvent.java create mode 100644 src/main/java/com/nametagedit/plugin/packets/PacketAccessor.java create mode 100644 src/main/java/com/nametagedit/plugin/packets/PacketData.java create mode 100644 src/main/java/com/nametagedit/plugin/packets/PacketWrapper.java create mode 100644 src/main/java/com/nametagedit/plugin/utils/Configuration.java create mode 100644 src/main/java/com/nametagedit/plugin/utils/UUIDFetcher.java create mode 100644 src/main/java/com/nametagedit/plugin/utils/Utils.java create mode 100644 src/main/java/com/tidefactions/chat/Commands/CommandAlert.java create mode 100644 src/main/java/com/tidefactions/chat/EventHandlers/JoinHandler.java create mode 100644 src/main/java/com/tidefactions/chat/Intergration/Discord.java create mode 100644 src/main/java/com/tidefactions/chat/Utils/FormatUtils.java create mode 100644 src/main/java/com/tidefactions/chat/Utils/NameUtils.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c682f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.directory +.gradle/ +.idea/ +build/ +*.iml diff --git a/build.gradle b/build.gradle index 982b47b..42e15f6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,10 @@ -apply plugin: 'java' +plugins { + id "com.github.johnrengelman.shadow" version "2.0.4" + id "java" + id "maven" + id "net.ltgt.apt" version "0.10" + id "io.franzbecker.gradle-lombok" version "1.14" +} group = pluginGroup version = 1.1 @@ -6,27 +12,36 @@ version = 1.1 sourceCompatibility = 1.8 targetCompatibility = 1.8 +lombok { + version = '1.18.2' + sha256 = "" +} + repositories { mavenCentral() + mavenLocal() maven { name = 'spigotmc-repo' url = 'https://hub.spigotmc.org/nexus/content/groups/public/' } + maven { name = 'sonatype' url = 'https://oss.sonatype.org/content/groups/public/' } + maven { name = 'nexus-hc' - url = 'http://nexus.hc.to/content/repositories/pub_releases' + url = 'http://nexus.hc.to/content/repositories/pub_releases/' } } dependencies { - testCompile group: 'junit', name: 'junit', version: '4.12' - compile 'org.spigotmc:spigot-api:1.8.8-R0.1-SNAPSHOT' + compile 'org.spigotmc:spigot:1.8.8-R0.1-SNAPSHOT' compile 'net.milkbowl.vault:VaultAPI:1.6' + compile 'org.bukkit:bukkit:1.8.8-R0.1-SNAPSHOT' compile group: 'org.xerial', name: 'sqlite-jdbc', version: '3.23.1' + apt 'org.projectlombok:lombok:1.14.8' } import org.apache.tools.ant.filters.ReplaceTokens diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..742fabf23e861e616df85bbfad708d33f8da17c0 GIT binary patch literal 54208 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 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/com/nametagedit/plugin/NametagManager.java b/src/main/java/com/nametagedit/plugin/NametagManager.java new file mode 100644 index 0000000..9cd7e05 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/NametagManager.java @@ -0,0 +1,167 @@ +package com.nametagedit.plugin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; + +import com.nametagedit.plugin.api.data.FakeTeam; +import com.nametagedit.plugin.packets.PacketWrapper; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class NametagManager { + + private final HashMap TEAMS = new HashMap<>(); + private final HashMap CACHED_FAKE_TEAMS = new HashMap<>(); + + /** + * Gets the current team given a prefix and suffix + * If there is no team similar to this, then a new + * team is created. + */ + private FakeTeam getFakeTeam(String prefix, String suffix) { + for (FakeTeam fakeTeam : TEAMS.values()) { + if (fakeTeam.isSimilar(prefix, suffix)) { + return fakeTeam; + } + } + + return null; + } + + /** + * Adds a player to a FakeTeam. If they are already on this team, + * we do NOT change that. + */ + private void addPlayerToTeam(String player, String prefix, String suffix, int sortPriority, boolean playerTag) { + FakeTeam previous = getFakeTeam(player); + + if (previous != null && previous.isSimilar(prefix, suffix)) { + return; + } + + reset(player); + + FakeTeam joining = getFakeTeam(prefix, suffix); + if (joining != null) { + joining.addMember(player); + } else { + joining = new FakeTeam(prefix, suffix, sortPriority, playerTag); + joining.addMember(player); + TEAMS.put(joining.getName(), joining); + addTeamPackets(joining); + } + + Player adding = Bukkit.getPlayerExact(player); + if (adding != null) { + addPlayerToTeamPackets(joining, adding.getName()); + cache(adding.getName(), joining); + } else { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(player); + addPlayerToTeamPackets(joining, offlinePlayer.getName()); + cache(offlinePlayer.getName(), joining); + } + } + + public FakeTeam reset(String player) { + return reset(player, decache(player)); + } + + private FakeTeam reset(String player, FakeTeam fakeTeam) { + if (fakeTeam != null && fakeTeam.getMembers().remove(player)) { + boolean delete; + Player removing = Bukkit.getPlayerExact(player); + if (removing != null) { + delete = removePlayerFromTeamPackets(fakeTeam, removing.getName()); + } else { + OfflinePlayer toRemoveOffline = Bukkit.getOfflinePlayer(player); + delete = removePlayerFromTeamPackets(fakeTeam, toRemoveOffline.getName()); + } + + if (delete) { + removeTeamPackets(fakeTeam); + TEAMS.remove(fakeTeam.getName()); + } + } + + return fakeTeam; + } + + // ============================================================== + // Below are public methods to modify the cache + // ============================================================== + private FakeTeam decache(String player) { + return CACHED_FAKE_TEAMS.remove(player); + } + + public FakeTeam getFakeTeam(String player) { + return CACHED_FAKE_TEAMS.get(player); + } + + private void cache(String player, FakeTeam fakeTeam) { + CACHED_FAKE_TEAMS.put(player, fakeTeam); + } + + // ============================================================== + // Below are public methods to modify certain data + // ============================================================== + public void setNametag(String player, String prefix, String suffix) { + setNametag(player, prefix, suffix, -1); + } + + void setNametag(String player, String prefix, String suffix, int sortPriority) { + setNametag(player, prefix, suffix, sortPriority, false); + } + + void setNametag(String player, String prefix, String suffix, int sortPriority, boolean playerTag) { + addPlayerToTeam(player, prefix != null ? prefix : "", suffix != null ? suffix : "", sortPriority, playerTag); + } + + public void sendTeams(Player player) { + for (FakeTeam fakeTeam : TEAMS.values()) { + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 0, fakeTeam.getMembers()).send(player); + } + } + + void reset() { + for (FakeTeam fakeTeam : TEAMS.values()) { + removePlayerFromTeamPackets(fakeTeam, fakeTeam.getMembers()); + removeTeamPackets(fakeTeam); + } + CACHED_FAKE_TEAMS.clear(); + TEAMS.clear(); + } + + // ============================================================== + // Below are private methods to construct a new Scoreboard packet + // ============================================================== + private void removeTeamPackets(FakeTeam fakeTeam) { + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 1, new ArrayList<>()).send(); + } + + private boolean removePlayerFromTeamPackets(FakeTeam fakeTeam, String... players) { + return removePlayerFromTeamPackets(fakeTeam, Arrays.asList(players)); + } + + private boolean removePlayerFromTeamPackets(FakeTeam fakeTeam, List players) { + new PacketWrapper(fakeTeam.getName(), 4, players).send(); + fakeTeam.getMembers().removeAll(players); + return fakeTeam.getMembers().isEmpty(); + } + + private void addTeamPackets(FakeTeam fakeTeam) { + new PacketWrapper(fakeTeam.getName(), fakeTeam.getPrefix(), fakeTeam.getSuffix(), 0, fakeTeam.getMembers()).send(); + } + + private void addPlayerToTeamPackets(FakeTeam fakeTeam, String player) { + new PacketWrapper(fakeTeam.getName(), 3, Collections.singletonList(player)).send(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/INametagApi.java b/src/main/java/com/nametagedit/plugin/api/INametagApi.java new file mode 100644 index 0000000..ef4d928 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/INametagApi.java @@ -0,0 +1,121 @@ +package com.nametagedit.plugin.api; + +import com.nametagedit.plugin.api.data.FakeTeam; +import com.nametagedit.plugin.api.data.Nametag; +import org.bukkit.entity.Player; + +public interface INametagApi { + + /** + * Function gets the fake team data for + * player. + * + * @param player the player to check + * @return the fake team + */ + FakeTeam getFakeTeam(Player player); + + /** + * Function gets the nametag for a player if + * it exists. This will never return a null. + * + * @param player the player to check + * @return the nametag for the player + */ + Nametag getNametag(Player player); + + /** + * Removes a player's nametag in memory + * only. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player whose nametag to clear + */ + void clearNametag(Player player); + + /** + * Removes a player's nametag in memory + * only. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player whose nametag to clear + */ + void clearNametag(String player); + + /** + * Sets the prefix for a player. The previous + * suffix is kept if it exists. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param prefix the prefix to change to + */ + void setPrefix(Player player, String prefix); + + /** + * Sets the suffix for a player. The previous + * prefix is kept if it exists. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param suffix the suffix to change to + */ + void setSuffix(Player player, String suffix); + + /** + * Sets the prefix for a player. The previous + * suffix is kept if it exists. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param prefix the prefix to change to + */ + void setPrefix(String player, String prefix); + + /** + * Sets the suffix for a player. The previous + * prefix is kept if it exists. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param suffix the suffix to change to + */ + void setSuffix(String player, String suffix); + + /** + * Sets the nametag for a player. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param prefix the prefix to change to + * @param suffix the suffix to change to + */ + void setNametag(Player player, String prefix, String suffix); + + /** + * Sets the nametag for a player. + *

+ * Note: Only affects memory, does NOT + * add/remove from storage. + * + * @param player the player whose nametag to change + * @param prefix the prefix to change to + * @param suffix the suffix to change to + */ + void setNametag(String player, String prefix, String suffix); + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/NametagAPI.java b/src/main/java/com/nametagedit/plugin/api/NametagAPI.java new file mode 100644 index 0000000..f990252 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/NametagAPI.java @@ -0,0 +1,98 @@ +package com.nametagedit.plugin.api; + +import com.nametagedit.plugin.NametagManager; +import com.nametagedit.plugin.api.data.FakeTeam; +import com.nametagedit.plugin.api.data.Nametag; +import com.nametagedit.plugin.api.events.NametagEvent; +import lombok.AllArgsConstructor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +/** + * Implements the INametagAPI interface. There only + * exists one instance of this class. + */ +@AllArgsConstructor +public final class NametagAPI implements INametagApi { + + public NametagManager manager; + + @Override + public FakeTeam getFakeTeam(Player player) { + return manager.getFakeTeam(player.getName()); + } + + @Override + public Nametag getNametag(Player player) { + FakeTeam team = manager.getFakeTeam(player.getName()); + boolean nullTeam = team == null; + return new Nametag(nullTeam ? "" : team.getPrefix(), nullTeam ? "" : team.getSuffix()); + } + + @Override + public void clearNametag(Player player) { + if (shouldFireEvent(player, NametagEvent.ChangeType.CLEAR)) { + manager.reset(player.getName()); + } + } + + @Override + public void clearNametag(String player) { + manager.reset(player); + } + + @Override + public void setPrefix(Player player, String prefix) { + FakeTeam fakeTeam = manager.getFakeTeam(player.getName()); + setNametagAlt(player, prefix, fakeTeam == null ? null : fakeTeam.getSuffix()); + } + + @Override + public void setSuffix(Player player, String suffix) { + FakeTeam fakeTeam = manager.getFakeTeam(player.getName()); + setNametagAlt(player, fakeTeam == null ? null : fakeTeam.getPrefix(), suffix); + } + + @Override + public void setPrefix(String player, String prefix) { + FakeTeam fakeTeam = manager.getFakeTeam(player); + manager.setNametag(player, prefix, fakeTeam == null ? null : fakeTeam.getSuffix()); + } + + @Override + public void setSuffix(String player, String suffix) { + FakeTeam fakeTeam = manager.getFakeTeam(player); + manager.setNametag(player, fakeTeam == null ? null : fakeTeam.getPrefix(), suffix); + } + + @Override + public void setNametag(Player player, String prefix, String suffix) { + setNametagAlt(player, prefix, suffix); + } + + @Override + public void setNametag(String player, String prefix, String suffix) { + manager.setNametag(player, prefix, suffix); + } + + /** + * Private helper function to reduce redundancy + */ + private boolean shouldFireEvent(Player player, NametagEvent.ChangeType type) { + NametagEvent event = new NametagEvent(player.getName(), "", getNametag(player), type); + Bukkit.getPluginManager().callEvent(event); + return !event.isCancelled(); + } + + /** + * Private helper function to reduce redundancy + */ + private void setNametagAlt(Player player, String prefix, String suffix) { + Nametag nametag = new Nametag(prefix, suffix); + + NametagEvent event = new NametagEvent(player.getName(), prefix, nametag, NametagEvent.ChangeType.UNKNOWN); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled()) return; + manager.setNametag(player.getName(), nametag.getPrefix(), nametag.getSuffix()); + } +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/data/FakeTeam.java b/src/main/java/com/nametagedit/plugin/api/data/FakeTeam.java new file mode 100644 index 0000000..92f8400 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/data/FakeTeam.java @@ -0,0 +1,66 @@ +package com.nametagedit.plugin.api.data; + +import com.nametagedit.plugin.utils.Utils; +import lombok.Data; + +import java.util.ArrayList; + +/** + * This class represents a Scoreboard Team. It is used + * to keep track of the current members of a Team, and + * is responsible for + */ +@Data +public class FakeTeam { + + // Because some networks use NametagEdit on multiple servers, we may have clashes + // with the same Team names. The UNIQUE_ID ensures there will be no clashing. + private static final String UNIQUE_ID = Utils.generateUUID(); + // This represents the number of FakeTeams that have been created. + // It is used to generate a unique Team name. + private static int ID = 0; + private final ArrayList members = new ArrayList<>(); + private String name; + private String prefix = ""; + private String suffix = ""; + + public FakeTeam(String prefix, String suffix, int sortPriority, boolean playerTag) { + this.name = UNIQUE_ID + "_" + getNameFromInput(sortPriority) + ++ID + (playerTag ? "+P" : ""); + // It is possible the names of the Team exceeded the length of 16 in the past, + // and caused crashes as a result. This is a layer of protection against that. + this.name = this.name.length() > 16 ? this.name.substring(0, 16) : this.name; + this.prefix = prefix; + this.suffix = suffix; + } + + public void addMember(String player) { + if (!members.contains(player)) { + members.add(player); + } + } + + public boolean isSimilar(String prefix, String suffix) { + return this.prefix.equals(prefix) && this.suffix.equals(suffix); + } + + /** + * This is a special method to sort nametags in + * the tablist. It takes a priority and converts + * it to an alphabetic representation to force a + * specific sort. + * + * @param input the sort priority + * @return the team name + */ + private String getNameFromInput(int input) { + if (input < 0) return "Z"; + char letter = (char) ((input / 5) + 65); + int repeat = input % 5 + 1; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < repeat; i++) { + builder.append(letter); + } + return builder.toString(); + } + +} diff --git a/src/main/java/com/nametagedit/plugin/api/data/GroupData.java b/src/main/java/com/nametagedit/plugin/api/data/GroupData.java new file mode 100644 index 0000000..89fac5f --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/data/GroupData.java @@ -0,0 +1,37 @@ +package com.nametagedit.plugin.api.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; + +/** + * This class represents a group nametag. There + * are several properties available. + */ +@Data +@AllArgsConstructor +public class GroupData implements INametag { + + private String groupName; + private String prefix; + private String suffix; + private String permission; + private Permission bukkitPermission; + private int sortPriority; + + public GroupData() { + + } + + public void setPermission(String permission) { + this.permission = permission; + bukkitPermission = new Permission(permission, PermissionDefault.FALSE); + } + + @Override + public boolean isPlayerTag() { + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/data/INametag.java b/src/main/java/com/nametagedit/plugin/api/data/INametag.java new file mode 100644 index 0000000..2a6c265 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/data/INametag.java @@ -0,0 +1,11 @@ +package com.nametagedit.plugin.api.data; + +public interface INametag { + String getPrefix(); + + String getSuffix(); + + int getSortPriority(); + + boolean isPlayerTag(); +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/data/Nametag.java b/src/main/java/com/nametagedit/plugin/api/data/Nametag.java new file mode 100644 index 0000000..6ad6914 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/data/Nametag.java @@ -0,0 +1,13 @@ +package com.nametagedit.plugin.api.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class Nametag { + private String prefix; + private String suffix; +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/data/PlayerData.java b/src/main/java/com/nametagedit/plugin/api/data/PlayerData.java new file mode 100644 index 0000000..d75583f --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/data/PlayerData.java @@ -0,0 +1,45 @@ +package com.nametagedit.plugin.api.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.UUID; + +/** + * This class represents a player nametag. There + * are several properties available. + */ +@Getter +@Setter +@AllArgsConstructor +public class PlayerData implements INametag { + + private String name; + private UUID uuid; + private String prefix; + private String suffix; + private int sortPriority; + + public PlayerData() { + + } + + public static PlayerData fromFile(String key, YamlConfiguration file) { + if (!file.contains("Players." + key)) return null; + PlayerData data = new PlayerData(); + data.setUuid(UUID.fromString(key)); + data.setName(file.getString("Players." + key + ".Name")); + data.setPrefix(file.getString("Players." + key + ".Prefix", "")); + data.setSuffix(file.getString("Players." + key + ".Suffix", "")); + data.setSortPriority(file.getInt("Players." + key + ".SortPriority", -1)); + return data; + } + + @Override + public boolean isPlayerTag() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/events/NametagEvent.java b/src/main/java/com/nametagedit/plugin/api/events/NametagEvent.java new file mode 100644 index 0000000..acf82f4 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/events/NametagEvent.java @@ -0,0 +1,101 @@ +package com.nametagedit.plugin.api.events; + +import com.nametagedit.plugin.api.data.Nametag; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This class represents an Event that is fired when a + * nametag is changed. + */ +public class NametagEvent extends Event implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private boolean cancelled; + + @Getter + @Setter + @Deprecated + private String value; + + @Getter + @Setter + private Nametag nametag; + + @Getter + @Setter + private String player; + + @Getter + @Setter + private ChangeType changeType; + + @Getter + @Setter + private ChangeReason changeReason; + + @Getter + @Setter + private StorageType storageType; + + public NametagEvent(String player, String value) { + this(player, value, ChangeType.UNKNOWN); + } + + public NametagEvent(String player, String value, Nametag nametag, ChangeType type) { + this(player, value, type); + this.nametag = nametag; + } + + public NametagEvent(String player, String value, ChangeType changeType) { + this(player, value, changeType, StorageType.MEMORY, ChangeReason.UNKNOWN); + } + + public NametagEvent(String player, String value, ChangeType changeType, ChangeReason changeReason) { + this(player, value, changeType, StorageType.MEMORY, changeReason); + } + + public NametagEvent(String player, String value, ChangeType changeType, StorageType storageType, ChangeReason changeReason) { + this.player = player; + this.value = value; + this.changeType = changeType; + this.storageType = storageType; + this.changeReason = changeReason; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + public enum ChangeReason { + API, PLUGIN, UNKNOWN + } + + public enum ChangeType { + PREFIX, SUFFIX, GROUP, CLEAR, PREFIX_AND_SUFFIX, RELOAD, UNKNOWN + } + + public enum StorageType { + MEMORY, PERSISTENT + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/api/events/NametagFirstLoadedEvent.java b/src/main/java/com/nametagedit/plugin/api/events/NametagFirstLoadedEvent.java new file mode 100644 index 0000000..ca172d0 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/api/events/NametagFirstLoadedEvent.java @@ -0,0 +1,32 @@ +package com.nametagedit.plugin.api.events; + +import com.nametagedit.plugin.api.data.INametag; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +/** + * This class represents an Event that is fired when a + * player joins the server and receives their nametag. + */ +@Getter +@AllArgsConstructor +public class NametagFirstLoadedEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + private Player player; + private INametag nametag; + + public static HandlerList getHandlerList() { + return HANDLERS; + } + + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/packets/PacketAccessor.java b/src/main/java/com/nametagedit/plugin/packets/PacketAccessor.java new file mode 100644 index 0000000..39b174d --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/packets/PacketAccessor.java @@ -0,0 +1,146 @@ +package com.nametagedit.plugin.packets; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +class PacketAccessor { + + private static List legacyVersions = Arrays.asList("v1_7_R1","v1_7_R2","v1_7_R3","v1_7_R4","v1_8_R1","v1_8_R2","v1_8_R3","v1_9_R1","v1_9_R2","v1_10_R1","v1_11_R1","v1_12_R1"); + private static boolean CAULDRON_SERVER = false; + private static boolean LEGACY_SERVER = false; + + static Field MEMBERS; + static Field PREFIX; + static Field SUFFIX; + static Field TEAM_NAME; + static Field PARAM_INT; + static Field PACK_OPTION; + static Field DISPLAY_NAME; + static Field TEAM_COLOR; + static Field PUSH; + static Field VISIBILITY; + + private static Method getHandle; + private static Method sendPacket; + private static Field playerConnection; + + private static Class packetClass; + + static { + try { + Class.forName("cpw.mods.fml.common.Mod"); + CAULDRON_SERVER = true; + } catch (ClassNotFoundException ignored) { + // This is not a cauldron server + } + + try { + String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; + + if (legacyVersions.contains(version)) + LEGACY_SERVER = true; + + Class typeCraftPlayer = Class.forName("org.bukkit.craftbukkit." + version + ".entity.CraftPlayer"); + getHandle = typeCraftPlayer.getMethod("getHandle"); + + if (CAULDRON_SERVER) { + packetClass = Class.forName("net.minecraft.server.v1_7_R4.PacketPlayOutScoreboardTeam"); + Class typeNMSPlayer = Class.forName("net.minecraft.server.v1_7_R4.EntityPlayer"); + Class typePlayerConnection = Class.forName("net.minecraft.server.v1_7_R4.PlayerConnection"); + playerConnection = typeNMSPlayer.getField("field_71135_a"); + sendPacket = typePlayerConnection.getMethod("func_147359_a", Class.forName("net.minecraft.server.v1_7_R4.Packet")); + } else { + packetClass = Class.forName("net.minecraft.server." + version + ".PacketPlayOutScoreboardTeam"); + Class typeNMSPlayer = Class.forName("net.minecraft.server." + version + ".EntityPlayer"); + Class typePlayerConnection = Class.forName("net.minecraft.server." + version + ".PlayerConnection"); + playerConnection = typeNMSPlayer.getField("playerConnection"); + sendPacket = typePlayerConnection.getMethod("sendPacket", Class.forName("net.minecraft.server." + version + ".Packet")); + } + + PacketData currentVersion = null; + for (PacketData packetData : PacketData.values()) { + if (version.contains(packetData.name())) { + currentVersion = packetData; + } + } + + if (CAULDRON_SERVER) { + currentVersion = PacketData.cauldron; + } + + if (currentVersion != null) { + PREFIX = getNMS(currentVersion.getPrefix()); + SUFFIX = getNMS(currentVersion.getSuffix()); + MEMBERS = getNMS(currentVersion.getMembers()); + TEAM_NAME = getNMS(currentVersion.getTeamName()); + PARAM_INT = getNMS(currentVersion.getParamInt()); + PACK_OPTION = getNMS(currentVersion.getPackOption()); + DISPLAY_NAME = getNMS(currentVersion.getDisplayName()); + + if (!isLegacyVersion()) { + TEAM_COLOR = getNMS(currentVersion.getColor()); + } + + if (isPushVersion(version)) { + PUSH = getNMS(currentVersion.getPush()); + } + + if (isVisibilityVersion(version)) { + VISIBILITY = getNMS(currentVersion.getVisibility()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static boolean isLegacyVersion() { + return LEGACY_SERVER; + } + + private static boolean isPushVersion(String version) { + return Integer.parseInt(version.split("_")[1]) >= 9; + } + + private static boolean isVisibilityVersion(String version) { + return Integer.parseInt(version.split("_")[1]) >= 8; + } + + private static Field getNMS(String path) throws Exception { + Field field = packetClass.getDeclaredField(path); + field.setAccessible(true); + return field; + } + + static Object createPacket() { + try { + return packetClass.newInstance(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + static void sendPacket(Collection players, Object packet) { + for (Player player : players) { + sendPacket(player, packet); + } + } + + static void sendPacket(Player player, Object packet) { + try { + Object nmsPlayer = getHandle.invoke(player); + Object connection = playerConnection.get(nmsPlayer); + sendPacket.invoke(connection, packet); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/packets/PacketData.java b/src/main/java/com/nametagedit/plugin/packets/PacketData.java new file mode 100644 index 0000000..9597aaf --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/packets/PacketData.java @@ -0,0 +1,31 @@ +package com.nametagedit.plugin.packets; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +enum PacketData { + + v1_7("e", "c", "d", "a", "f", "g", "b", "NA", "NA", "NA"), + cauldron("field_149317_e", "field_149319_c", "field_149316_d", "field_149320_a", + "field_149314_f", "field_149315_g", "field_149318_b", "NA", "NA", "NA"), + v1_8("g", "c", "d", "a", "h", "i", "b", "NA", "NA", "e"), + v1_9("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_10("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_11("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_12("h", "c", "d", "a", "i", "j", "b", "NA", "f", "e"), + v1_13("h", "c", "d", "a", "i", "j", "b", "g", "f", "e"); + + private String members; + private String prefix; + private String suffix; + private String teamName; + private String paramInt; + private String packOption; + private String displayName; + private String color; + private String push; + private String visibility; + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/packets/PacketWrapper.java b/src/main/java/com/nametagedit/plugin/packets/PacketWrapper.java new file mode 100644 index 0000000..91b7649 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/packets/PacketWrapper.java @@ -0,0 +1,119 @@ +package com.nametagedit.plugin.packets; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.entity.Player; + +import com.nametagedit.plugin.utils.Utils; + +public class PacketWrapper { + + public String error; + private Object packet = PacketAccessor.createPacket(); + + private static Constructor ChatComponentText; + private static Class typeEnumChatFormat; + + static { + try { + if (!PacketAccessor.isLegacyVersion()) { + String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; + + Class typeChatComponentText = Class.forName("net.minecraft.server." + version + ".ChatComponentText"); + ChatComponentText = typeChatComponentText.getConstructor(String.class); + typeEnumChatFormat = (Class) Class.forName("net.minecraft.server." + version + ".EnumChatFormat"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public PacketWrapper(String name, int param, List members) { + if (param != 3 && param != 4) { + throw new IllegalArgumentException("Method must be join or leave for player constructor"); + } + setupDefaults(name, param); + setupMembers(members); + } + + @SuppressWarnings("unchecked") + public PacketWrapper(String name, String prefix, String suffix, int param, Collection players) { + setupDefaults(name, param); + if (param == 0 || param == 2) { + try { + if (PacketAccessor.isLegacyVersion()) { + PacketAccessor.DISPLAY_NAME.set(packet, name); + PacketAccessor.PREFIX.set(packet, prefix); + PacketAccessor.SUFFIX.set(packet, suffix); + } else { + String color = ChatColor.getLastColors(prefix); + String colorCode = null; + + if (!color.isEmpty()) { + colorCode = color.substring(color.length() - 1); + String chatColor = ChatColor.getByChar(colorCode).name(); + + if (chatColor.equalsIgnoreCase("MAGIC")) + chatColor = "OBFUSCATED"; + + Enum colorEnum = Enum.valueOf(typeEnumChatFormat, chatColor); + PacketAccessor.TEAM_COLOR.set(packet, colorEnum); + } + + PacketAccessor.DISPLAY_NAME.set(packet, ChatComponentText.newInstance(name)); + PacketAccessor.PREFIX.set(packet, ChatComponentText.newInstance(prefix)); + + if (colorCode != null) + suffix = ChatColor.getByChar(colorCode) + suffix; + + PacketAccessor.SUFFIX.set(packet, ChatComponentText.newInstance(suffix)); + } + + PacketAccessor.PACK_OPTION.set(packet, 1); + + if (PacketAccessor.VISIBILITY != null) { + PacketAccessor.VISIBILITY.set(packet, "always"); + } + + if (param == 0) { + ((Collection) PacketAccessor.MEMBERS.get(packet)).addAll(players); + } + } catch (Exception e) { + error = e.getMessage(); + } + } + } + + @SuppressWarnings("unchecked") + private void setupMembers(Collection players) { + try { + players = players == null || players.isEmpty() ? new ArrayList<>() : players; + ((Collection) PacketAccessor.MEMBERS.get(packet)).addAll(players); + } catch (Exception e) { + error = e.getMessage(); + } + } + + private void setupDefaults(String name, int param) { + try { + PacketAccessor.TEAM_NAME.set(packet, name); + PacketAccessor.PARAM_INT.set(packet, param); + } catch (Exception e) { + error = e.getMessage(); + } + } + + public void send() { + PacketAccessor.sendPacket(Utils.getOnline(), packet); + } + + public void send(Player player) { + PacketAccessor.sendPacket(player, packet); + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/utils/Configuration.java b/src/main/java/com/nametagedit/plugin/utils/Configuration.java new file mode 100644 index 0000000..7f6ff5b --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/utils/Configuration.java @@ -0,0 +1,247 @@ +package com.nametagedit.plugin.utils; + +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.bukkit.Bukkit; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.regex.Pattern; + +public class Configuration extends YamlConfiguration { + + private final Map> headers = Maps.newConcurrentMap(); + private final File file; + private List mainHeader = Lists.newArrayList(); + private boolean loadHeaders; + + public Configuration(File file) { + this.file = file; + } + + /** + * Set the main header displayed at top of config. + * + * @param header header + */ + public void mainHeader(String... header) { + mainHeader = Arrays.asList(header); + } + + /** + * Get main header displayed at top of config. + * + * @return header + */ + public List mainHeader() { + return mainHeader; + } + + /** + * Set option header. + * + * @param key of option (or section) + * @param header of option (or section) + */ + public void header(String key, String... header) { + headers.put(key, Arrays.asList(header)); + } + + /** + * Get header of option + * + * @param key of option (or section) + * @return Header + */ + public List header(String key) { + return headers.get(key); + } + + public T get(String key, Class type) { + return type.cast(get(key)); + } + + /** + * Reload config from file. + */ + public void reload() { + reload(headers.isEmpty()); + } + + /** + * Reload config from file. + * + * @param loadHeaders Whether or not to load headers. + */ + public void reload(boolean loadHeaders) { + this.loadHeaders = loadHeaders; + try { + load(file); + } catch (Exception e) { + Bukkit.getLogger().log(Level.WARNING, "failed to reload file", e); + } + } + + @Override + public void loadFromString(String contents) throws InvalidConfigurationException { + if (!loadHeaders) { + super.loadFromString(contents); + return; + } + + StringBuilder memoryData = new StringBuilder(); + + // Parse headers + final int indentLength = options().indent(); + final String pathSeparator = Character.toString(options().pathSeparator()); + int currentIndents = 0; + String key = ""; + List headers = Lists.newArrayList(); + for (String line : contents.split("\n")) { + if (line.isEmpty()) continue; // Skip empty lines + int indent = getSuccessiveCharCount(line, ' '); + String subline = indent > 0 ? line.substring(indent) : line; + if (subline.startsWith("#")) { + if (subline.startsWith("#>")) { + String txt = subline.startsWith("#> ") ? subline.substring(3) : subline.substring(2); + mainHeader.add(txt); + continue; // Main header, handled by bukkit + } + + // Add header to list + String txt = subline.startsWith("# ") ? subline.substring(2) : subline.substring(1); + headers.add(txt); + continue; + } + + int indents = indent / indentLength; + if (indents <= currentIndents) { + // Remove last section of key + String[] array = key.split(Pattern.quote(pathSeparator)); + int backspace = currentIndents - indents + 1; + key = join(array, options().pathSeparator(), 0, array.length - backspace); + } + + // Add new section to key + String separator = key.length() > 0 ? pathSeparator : ""; + String lineKey = line.contains(":") ? line.split(Pattern.quote(":"))[0] : line; + key += separator + lineKey.substring(indent); + + currentIndents = indents; + + memoryData.append(line).append('\n'); + if (!headers.isEmpty()) { + this.headers.put(key, headers); + headers = Lists.newArrayList(); + } + } + + // Parse remaining text + super.loadFromString(memoryData.toString()); + + // Clear bukkit header + options().header(null); + } + + /** + * Save config to file + */ + public void save() { + if (headers.isEmpty() && mainHeader.isEmpty()) { + try { + super.save(file); + } catch (IOException e) { + Bukkit.getLogger().log(Level.WARNING, "Failed to save file", e); + } + return; + } + + // Custom save + final int indentLength = options().indent(); + final String pathSeparator = Character.toString(options().pathSeparator()); + String content = saveToString(); + StringBuilder fileData = new StringBuilder(buildHeader()); + int currentIndents = 0; + String key = ""; + for (String h : mainHeader) { + // Append main header to top of file + fileData.append("#> ").append(h).append('\n'); + } + + for (String line : content.split("\n")) { + if (line.isEmpty()) continue; // Skip empty lines + int indent = getSuccessiveCharCount(line, ' '); + int indents = indent / indentLength; + String indentText = indent > 0 ? line.substring(0, indent) : ""; + if (indents <= currentIndents) { + // Remove last section of key + String[] array = key.split(Pattern.quote(pathSeparator)); + int backspace = currentIndents - indents + 1; + key = join(array, options().pathSeparator(), 0, array.length - backspace); + } + + // Add new section to key + String separator = key.length() > 0 ? pathSeparator : ""; + String lineKey = line.contains(":") ? line.split(Pattern.quote(":"))[0] : line; + key += separator + lineKey.substring(indent); + + currentIndents = indents; + + List header = headers.get(key); + String headerText = header != null ? addHeaderTags(header, indentText) : ""; + fileData.append(headerText).append(line).append('\n'); + } + + // Write data to file + FileWriter writer = null; + try { + writer = new FileWriter(file); + writer.write(fileData.toString()); + writer.flush(); + } catch (IOException e) { + Bukkit.getLogger().log(Level.WARNING, "Failed to save file", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException ignored) { + } + } + } + } + + private String addHeaderTags(List header, String indent) { + StringBuilder builder = new StringBuilder(); + for (String line : header) { + builder.append(indent).append("# ").append(line).append('\n'); + } + return builder.toString(); + } + + private String join(String[] array, char joinChar, int start, int length) { + String[] copy = new String[length - start]; + System.arraycopy(array, start, copy, 0, length - start); + return Joiner.on(joinChar).join(copy); + } + + private int getSuccessiveCharCount(String text, char key) { + int count = 0; + for (int i = 0; i < text.length(); i++) { + if (text.charAt(i) == key) { + count += 1; + } else { + break; + } + } + return count; + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/utils/UUIDFetcher.java b/src/main/java/com/nametagedit/plugin/utils/UUIDFetcher.java new file mode 100644 index 0000000..88f9bbd --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/utils/UUIDFetcher.java @@ -0,0 +1,117 @@ +package com.nametagedit.plugin.utils; + +import com.google.common.collect.ImmutableList; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; +import java.util.concurrent.Callable; + +/** + * This class is responsible for retrieving UUIDs from Names + * + * @author evilmidget38 + */ +public class UUIDFetcher implements Callable> { + + private static final double PROFILES_PER_REQUEST = 100; + private static final String PROFILE_URL = "https://api.mojang.com/profiles/minecraft"; + private final JSONParser jsonParser = new JSONParser(); + private final List names; + private final boolean rateLimiting; + + private UUIDFetcher(List names, boolean rateLimiting) { + this.names = ImmutableList.copyOf(names); + this.rateLimiting = rateLimiting; + } + + private UUIDFetcher(List names) { + this(names, true); + } + + public static void lookupUUID(final String name, final Plugin plugin, final UUIDLookup uuidLookup) { + new BukkitRunnable() { + @Override + public void run() { + UUID response = null; + try { + response = getUUIDOf(name); + } catch (Exception e) { + // Swallow + } + + final UUID finalResponse = response; + new BukkitRunnable() { + @Override + public void run() { + uuidLookup.response(finalResponse); + } + }.runTask(plugin); + } + }.runTaskAsynchronously(plugin); + } + + private static void writeBody(HttpURLConnection connection, String body) throws Exception { + try (OutputStream stream = connection.getOutputStream()) { + stream.write(body.getBytes()); + stream.flush(); + } + } + + private static HttpURLConnection createConnection() throws Exception { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setDoOutput(true); + return connection; + } + + private static UUID getUUID(String id) { + return UUID.fromString(id.substring(0, 8) + "-" + id.substring(8, 12) + "-" + id.substring(12, 16) + + "-" + id.substring(16, 20) + "-" + id.substring(20, 32)); + } + + private static UUID getUUIDOf(String name) throws Exception { + return new UUIDFetcher(Collections.singletonList(name)).call().get(name); + } + + @Override + public Map call() throws Exception { + Map uuidMap = new HashMap<>(); + int requests = (int) Math.ceil(names.size() / PROFILES_PER_REQUEST); + for (int i = 0; i < requests; i++) { + HttpURLConnection connection = createConnection(); + String body = JSONArray.toJSONString(names.subList(i * 100, Math.min((i + 1) * 100, names.size()))); + writeBody(connection, body); + JSONArray array = (JSONArray) jsonParser.parse(new InputStreamReader(connection.getInputStream())); + + for (Object profile : array) { + JSONObject jsonProfile = (JSONObject) profile; + String id = (String) jsonProfile.get("id"); + String name = (String) jsonProfile.get("name"); + UUID uuid = UUIDFetcher.getUUID(id); + uuidMap.put(name, uuid); + } + + if (rateLimiting && i != requests - 1) { + Thread.sleep(100L); + } + } + return uuidMap; + } + + public interface UUIDLookup { + void response(UUID uuid); + } + +} \ No newline at end of file diff --git a/src/main/java/com/nametagedit/plugin/utils/Utils.java b/src/main/java/com/nametagedit/plugin/utils/Utils.java new file mode 100644 index 0000000..e806af3 --- /dev/null +++ b/src/main/java/com/nametagedit/plugin/utils/Utils.java @@ -0,0 +1,88 @@ +package com.nametagedit.plugin.utils; + +import org.apache.commons.lang.StringUtils; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.World; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.io.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Utils { + + public static String format(String[] text, int to, int from) { + return StringUtils.join(text, ' ', to, from).replace("'", ""); + } + + public static String deformat(String input) { + return input.replace("ยง", "&"); + } + + public static String format(String input) { + return format(input, false); + } + + public static String format(String input, boolean limitChars) { + String colored = ChatColor.translateAlternateColorCodes('&', input); + return limitChars && colored.length() > 16 ? colored.substring(0, 16) : colored; + } + + public static List getOnline() { + List list = new ArrayList<>(); + + for (World world : Bukkit.getWorlds()) { + list.addAll(world.getPlayers()); + } + + return Collections.unmodifiableList(list); + } + + public static YamlConfiguration getConfig(File file) { + try { + if (!file.exists()) { + file.createNewFile(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return YamlConfiguration.loadConfiguration(file); + } + + public static YamlConfiguration getConfig(File file, String resource, Plugin plugin) { + try { + if (!file.exists()) { + file.createNewFile(); + InputStream inputStream = plugin.getResource(resource); + OutputStream outputStream = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + inputStream.close(); + outputStream.flush(); + outputStream.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + return YamlConfiguration.loadConfiguration(file); + } + + public static String generateUUID() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 5; i++) { + builder.append(chars.charAt((int) (Math.random() * chars.length()))); + } + return builder.toString(); + } + +} diff --git a/src/main/java/com/tidefactions/chat/Commands/CommandAdminChat.java b/src/main/java/com/tidefactions/chat/Commands/CommandAdminChat.java index a3996b9..45c910e 100644 --- a/src/main/java/com/tidefactions/chat/Commands/CommandAdminChat.java +++ b/src/main/java/com/tidefactions/chat/Commands/CommandAdminChat.java @@ -15,7 +15,7 @@ public class CommandAdminChat implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { - if (sender.hasPermission(ChatMode.ADMIN.getPermission())) { + if (sender.hasPermission(ChatMode.ADMIN.getPermission() + ".send")) { if (args.length >= 1) { if (sender instanceof Player) { ChatUtils.sendMessageInChannel( diff --git a/src/main/java/com/tidefactions/chat/Commands/CommandAlert.java b/src/main/java/com/tidefactions/chat/Commands/CommandAlert.java new file mode 100644 index 0000000..fa375b7 --- /dev/null +++ b/src/main/java/com/tidefactions/chat/Commands/CommandAlert.java @@ -0,0 +1,48 @@ +package com.tidefactions.chat.Commands; + +import com.tidefactions.chat.Messages; +import com.tidefactions.chat.Types.ChatMode; +import com.tidefactions.chat.Utils.ChatUtils; +import org.apache.commons.lang.StringUtils; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +public class CommandAlert implements CommandExecutor { + + @Override + public boolean onCommand(CommandSender sender, Command cmd, String s, String[] args) { + if (sender.hasPermission(ChatMode.ALERT.getPermission() + ".send")) { + if (args.length >= 1) { + if (sender instanceof Player) { + ChatUtils.sendMessageInChannel( + ((Player) sender), + ChatMode.ALERT, + StringUtils.join(args, " "), + "Command"); + } else { + ChatUtils.sendMessageInChannelAsConsole( + ChatMode.ALERT, + StringUtils.join(args, " "), + "Command"); + } + } else { + if (sender instanceof Player) { + if (ChatUtils.getChatMode(((Player) sender)) == ChatMode.ALERT) { + ChatUtils.setChatMode(((Player) sender), ChatMode.PUBLIC); + sender.sendMessage(Messages.PREFIX + Messages.ALERT_TOGGLE_OFF); + } else { + ChatUtils.setChatMode(((Player) sender), ChatMode.ALERT); + sender.sendMessage(Messages.PREFIX + Messages.ALERT_TOGGLE_ON); + } + } else { + sender.sendMessage(Messages.PREFIX + Messages.ALERT_CONSOLE); + } + } + } else { + sender.sendMessage(Messages.PREFIX + Messages.NO_PERMISSION); + } + return true; + } +} diff --git a/src/main/java/com/tidefactions/chat/Commands/CommandBuilderChat.java b/src/main/java/com/tidefactions/chat/Commands/CommandBuilderChat.java index 05f8d87..df61e84 100644 --- a/src/main/java/com/tidefactions/chat/Commands/CommandBuilderChat.java +++ b/src/main/java/com/tidefactions/chat/Commands/CommandBuilderChat.java @@ -14,7 +14,7 @@ public class CommandBuilderChat implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { - if (sender.hasPermission(ChatMode.BUILDER.getPermission())) { + if (sender.hasPermission(ChatMode.BUILDER.getPermission() + ".send")) { if (args.length >= 1) { if (sender instanceof Player) { ChatUtils.sendMessageInChannel( diff --git a/src/main/java/com/tidefactions/chat/Commands/CommandStaffChat.java b/src/main/java/com/tidefactions/chat/Commands/CommandStaffChat.java index 165976c..f95f96d 100644 --- a/src/main/java/com/tidefactions/chat/Commands/CommandStaffChat.java +++ b/src/main/java/com/tidefactions/chat/Commands/CommandStaffChat.java @@ -14,7 +14,7 @@ public class CommandStaffChat implements CommandExecutor { @Override public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { - if (sender.hasPermission(ChatMode.STAFF.getPermission())) { + if (sender.hasPermission(ChatMode.STAFF.getPermission() + ".send")) { if (args.length >= 1) { if (sender instanceof Player) { ChatUtils.sendMessageInChannel( diff --git a/src/main/java/com/tidefactions/chat/Config.java b/src/main/java/com/tidefactions/chat/Config.java index a9bb2d9..9e55e5a 100644 --- a/src/main/java/com/tidefactions/chat/Config.java +++ b/src/main/java/com/tidefactions/chat/Config.java @@ -5,22 +5,49 @@ import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; import java.io.IOException; -import java.util.HashMap; import static com.tidefactions.chat.Main.colorCodes; public class Config { + // Settings + public static Boolean LOG_CHAT_TO_CONSOLE; + + // Channel prefixes + public static String GLOBAL_CHANNEL_PREFIX; + public static String STAFF_CHANNEL_PREFIX; + public static String ADMIN_CHANNEL_PREFIX; + public static String BUILDER_CHANNEL_PREFIX; + public static String ALERT_CHANNEL_PREFIX; + // Chat formats public static String GLOBAL_CHAT_FORMAT; public static String STAFF_CHAT_FORMAT; public static String ADMIN_CHAT_FORMAT; public static String BUILDER_CHAT_FORMAT; + public static String ALERT_CHAT_FORMAT; + + // Name + public static String NAME_PREFIX_FORMAT; + public static String NAME_SUFFIX_FORMAT; // Console chat settings public static String CONSOLE_PREFIX; public static String CONSOLE_TITLE; + public static String CONSOLE_NAME_COLOR; public static String CONSOLE_CHAT_COLOR; + // Discord + public static Boolean DISCORD_ENABLED; + public static String DISCORD_ALERT_IMAGE; + public static String DISCORD_ALERT_NAME; + public static String DISCORD_CONSOLE_IMAGE; + public static String DISCORD_PLAYER_IMAGE; + public static String DISCORD_GLOBAL_WEBHOOK; + public static String DISCORD_STAFF_WEBHOOK; + public static String DISCORD_ADMIN_WEBHOOK; + public static String DISCORD_BUILDER_WEBHOOK; + public static String DISCORD_ALERT_WEBHOOK; + public static void load(File file) { try { if (!file.getParentFile().exists()) @@ -30,15 +57,46 @@ public class Config { YamlConfiguration config = new YamlConfiguration(); config.load(file); + // Settings + LOG_CHAT_TO_CONSOLE = config.getBoolean("settings.log-chat-to-console"); + + // Channel prefixes + GLOBAL_CHANNEL_PREFIX = colorCodes(config.getString("prefixes.global")); + STAFF_CHANNEL_PREFIX = colorCodes(config.getString("prefixes.staff")); + ADMIN_CHANNEL_PREFIX = colorCodes(config.getString("prefixes.admin")); + BUILDER_CHANNEL_PREFIX = colorCodes(config.getString("prefixes.builder")); + ALERT_CHANNEL_PREFIX = colorCodes(config.getString("prefixes.alert")); + // Chat formats GLOBAL_CHAT_FORMAT = colorCodes(config.getString("format.global")); STAFF_CHAT_FORMAT = colorCodes(config.getString("format.staff")); ADMIN_CHAT_FORMAT = colorCodes(config.getString("format.admin")); BUILDER_CHAT_FORMAT = colorCodes(config.getString("format.builder")); + ALERT_CHAT_FORMAT = colorCodes(config.getString("format.alert")); + + + // Nametag + NAME_PREFIX_FORMAT = colorCodes(config.getString("name.prefix-format")); + NAME_SUFFIX_FORMAT = colorCodes(config.getString("name.suffix-format")); + // Console chat settings CONSOLE_PREFIX = colorCodes(config.getString("console.prefix")); CONSOLE_TITLE = colorCodes(config.getString("console.title")); + CONSOLE_NAME_COLOR = colorCodes(config.getString("console.name-color")); CONSOLE_CHAT_COLOR = colorCodes(config.getString("console.chat-color")); + + // Discord + DISCORD_ENABLED = config.getBoolean("discord.enable"); + DISCORD_ALERT_NAME = config.getString("discord.alert-name"); + DISCORD_ALERT_IMAGE = config.getString("discord.alert-image"); + DISCORD_CONSOLE_IMAGE = config.getString("discord.console-image"); + DISCORD_PLAYER_IMAGE = config.getString("discord.player-image"); + DISCORD_GLOBAL_WEBHOOK = config.getString("discord.webhooks.global"); + DISCORD_STAFF_WEBHOOK = config.getString("discord.webhooks.staff"); + DISCORD_ADMIN_WEBHOOK = config.getString("discord.webhooks.admin"); + DISCORD_BUILDER_WEBHOOK = config.getString("discord.webhooks.builder"); + DISCORD_ALERT_WEBHOOK = config.getString("discord.webhooks.alert"); + } catch (IOException | InvalidConfigurationException e) { e.printStackTrace(); } diff --git a/src/main/java/com/tidefactions/chat/Databases/PrefixDatabase.java b/src/main/java/com/tidefactions/chat/Databases/PrefixDatabase.java index 1a5dc30..985e58c 100644 --- a/src/main/java/com/tidefactions/chat/Databases/PrefixDatabase.java +++ b/src/main/java/com/tidefactions/chat/Databases/PrefixDatabase.java @@ -11,55 +11,79 @@ public class PrefixDatabase { private static String url; public static void init(String path) { - File dbFile = new File(path + "/playerData.db"); - url = "jdbc:sqlite:" + path + "/playerData.db"; - if (!dbFile.getParentFile().exists()) - dbFile.getParentFile().mkdirs(); - if (!dbFile.exists()) { - createTable(); + try { + Class.forName("org.sqlite.JDBC"); + + File dbFile = new File(path + "/playerData.db"); + url = "jdbc:sqlite:" + path + "/playerData.db"; + if (!dbFile.getParentFile().exists()) + dbFile.getParentFile().mkdirs(); + if (!dbFile.exists()) { + createTable(); + } + } catch (Exception e) { + e.printStackTrace(); } } public static void createTable() { - // SQL statement for creating a new table - String sql = "CREATE TABLE IF NOT EXISTS prefixes (\n" - + " uuid TEXT PRIMARY KEY,\n" - + " prefix TEXT NOT NULL\n" - + ");"; + try { + Class.forName("org.sqlite.JDBC"); - try (Connection conn = DriverManager.getConnection(url); - Statement stmt = conn.createStatement()) { - stmt.execute(sql); - } catch (SQLException e) { + // SQL statement for creating a new table + String sql = "CREATE TABLE IF NOT EXISTS prefixes (\n" + + " uuid TEXT PRIMARY KEY,\n" + + " prefix TEXT NOT NULL\n" + + ");"; + + try (Connection conn = DriverManager.getConnection(url); + Statement stmt = conn.createStatement()) { + stmt.execute(sql); + } catch (SQLException e) { + e.printStackTrace(); + } + } catch (Exception e) { e.printStackTrace(); } } public static String getPrefixIDForPlayer(Player player) { - try (Connection conn = DriverManager.getConnection(url); - Statement stmt = conn.createStatement()) { - ResultSet result = stmt.executeQuery("SELECT * FROM prefixes WHERE uuid='" + player.getUniqueId() + "'"); - while (result.next()) { - return result.getString("prefix"); + try { + Class.forName("org.sqlite.JDBC"); + + try (Connection conn = DriverManager.getConnection(url); + Statement stmt = conn.createStatement()) { + ResultSet result = stmt.executeQuery("SELECT * FROM prefixes WHERE uuid='" + player.getUniqueId() + "'"); + while (result.next()) { + return result.getString("prefix"); + } + } catch (SQLException e) { + e.printStackTrace(); } - } catch (SQLException e) { + } catch (Exception e) { e.printStackTrace(); } return "Default"; } public static void setPrefix(Player player, Prefix prefix) { - try (Connection conn = DriverManager.getConnection(url); - Statement stmt = conn.createStatement()) { - stmt.execute("DELETE FROM prefixes WHERE uuid='" + player.getUniqueId() + "'"); - } catch (SQLException e) { - e.printStackTrace(); - } + try { + Class.forName("org.sqlite.JDBC"); - try (Connection conn = DriverManager.getConnection(url); - Statement stmt = conn.createStatement()) { - stmt.execute("INSERT INTO prefixes ('uuid', 'prefix') VALUES ('" + player.getUniqueId() + "', '" + prefix.getName() + "')"); - } catch (SQLException e) { + try (Connection conn = DriverManager.getConnection(url); + Statement stmt = conn.createStatement()) { + stmt.execute("DELETE FROM prefixes WHERE uuid='" + player.getUniqueId() + "'"); + } catch (SQLException e) { + e.printStackTrace(); + } + + try (Connection conn = DriverManager.getConnection(url); + Statement stmt = conn.createStatement()) { + stmt.execute("INSERT INTO prefixes ('uuid', 'prefix') VALUES ('" + player.getUniqueId() + "', '" + prefix.getName() + "')"); + } catch (SQLException e) { + e.printStackTrace(); + } + } catch (Exception e) { e.printStackTrace(); } } diff --git a/src/main/java/com/tidefactions/chat/EventHandlers/ChatHandler.java b/src/main/java/com/tidefactions/chat/EventHandlers/ChatHandler.java index 4aa6511..1c25b65 100644 --- a/src/main/java/com/tidefactions/chat/EventHandlers/ChatHandler.java +++ b/src/main/java/com/tidefactions/chat/EventHandlers/ChatHandler.java @@ -1,8 +1,11 @@ package com.tidefactions.chat.EventHandlers; -import com.tidefactions.chat.Types.ChatMode; +import com.tidefactions.chat.Config; +import com.tidefactions.chat.Events.MessageSendInChannelEvent; +import com.tidefactions.chat.Intergration.Discord; import com.tidefactions.chat.Utils.ChatUtils; import org.bukkit.Bukkit; +import org.bukkit.ChatColor; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -13,6 +16,9 @@ public class ChatHandler implements Listener { @EventHandler(priority = EventPriority.HIGH) public void onAsyncChatEvent(AsyncPlayerChatEvent event) { + if (event.isCancelled()) + return; + Player player = event.getPlayer(); ChatUtils.sendMessageInChannel( player, @@ -20,5 +26,27 @@ public class ChatHandler implements Listener { event.getMessage(), "Chat"); event.setCancelled(true); + event.setMessage(ChatColor.RED + "[THIS EVENT WAS CANCELED BY CHAT]"); + } + + @EventHandler + public void onMessageSendInChannelEvent(MessageSendInChannelEvent event) { + if (Config.LOG_CHAT_TO_CONSOLE) { + String finalMessage; + if (event.getPlayer().equals("CONSOLE")) { + finalMessage = ChatUtils.getMessageForChannelAsConsole( + event.getChatMode(), + event.getMessage()); + } else { + finalMessage = ChatUtils.getMessageForChannel( + Bukkit.getPlayer(event.getPlayer()), + event.getChatMode(), + event.getMessage()); + } + Bukkit.getConsoleSender().sendMessage("[CHAT] " + finalMessage); + } + if (Config.DISCORD_ENABLED) { + Discord.forward(event); + } } } diff --git a/src/main/java/com/tidefactions/chat/EventHandlers/JoinHandler.java b/src/main/java/com/tidefactions/chat/EventHandlers/JoinHandler.java new file mode 100644 index 0000000..23f1bb5 --- /dev/null +++ b/src/main/java/com/tidefactions/chat/EventHandlers/JoinHandler.java @@ -0,0 +1,18 @@ +package com.tidefactions.chat.EventHandlers; + +import com.tidefactions.chat.Main; +import com.tidefactions.chat.Utils.NameUtils; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class JoinHandler implements Listener { + + @EventHandler + public void onPlayerJoinEvent(PlayerJoinEvent event) { + Bukkit.getScheduler().scheduleSyncDelayedTask(Main.plugin, () -> { + NameUtils.updatePlayer(event.getPlayer()); + }, 100); + } +} diff --git a/src/main/java/com/tidefactions/chat/Events/MessageSendInChannelEvent.java b/src/main/java/com/tidefactions/chat/Events/MessageSendInChannelEvent.java index 1004b47..98ba6c6 100644 --- a/src/main/java/com/tidefactions/chat/Events/MessageSendInChannelEvent.java +++ b/src/main/java/com/tidefactions/chat/Events/MessageSendInChannelEvent.java @@ -41,4 +41,9 @@ public class MessageSendInChannelEvent extends Event { public HandlerList getHandlers() { return handlerList; } + + public static HandlerList getHandlerList() { + return handlerList; + } + } diff --git a/src/main/java/com/tidefactions/chat/GUI/PrefixGui.java b/src/main/java/com/tidefactions/chat/GUI/PrefixGui.java index 24fe947..9165b18 100644 --- a/src/main/java/com/tidefactions/chat/GUI/PrefixGui.java +++ b/src/main/java/com/tidefactions/chat/GUI/PrefixGui.java @@ -18,14 +18,16 @@ public class PrefixGui { public static List views = new ArrayList<>(); public void open(Player player) { - int size = (int) (Math.ceil(PrefixUtils.getPrefixes().size() / 9) * 9); - if (size < 9) - size = 9; + int goal = PrefixUtils.getPrefixes().size(); + int size = 9; + while (size < goal) + size = size+9; Inventory inv = Bukkit.createInventory(null, size, ChatColor.GREEN + "Prefix " + ChatColor.DARK_GRAY + " selection"); - String currentPrefixID = null; - if (PrefixUtils.getPrefixForPlayer(player) != null) { + String currentPrefixID; + if (PrefixUtils.getPrefixForPlayer(player) == null) + currentPrefixID = PrefixUtils.getPrefixByID("default").getName(); + else currentPrefixID = PrefixUtils.getPrefixForPlayer(player).getName(); - } for (Prefix prefix : PrefixUtils.getPrefixes()) { if (player.hasPermission(prefix.getPermission())) { ItemStack is = new ItemStack( @@ -45,7 +47,6 @@ public class PrefixGui { inv.addItem(is); } } - views.add(player.openInventory(inv)); } } diff --git a/src/main/java/com/tidefactions/chat/Intergration/Discord.java b/src/main/java/com/tidefactions/chat/Intergration/Discord.java new file mode 100644 index 0000000..c0d9c30 --- /dev/null +++ b/src/main/java/com/tidefactions/chat/Intergration/Discord.java @@ -0,0 +1,57 @@ +package com.tidefactions.chat.Intergration; + +import com.tidefactions.chat.Config; +import com.tidefactions.chat.Events.MessageSendInChannelEvent; +import com.tidefactions.chat.Types.ChatMode; +import org.bukkit.Bukkit; +import org.json.simple.JSONObject; + +import javax.net.ssl.HttpsURLConnection; +import java.io.DataOutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; + +public class Discord { + + @SuppressWarnings("all") + public static void forward(MessageSendInChannelEvent event) { + System.out.println("Forwarding message from " + event.getPlayer() + " to discord."); + if (event.getChatMode().getWebhook() != null && !event.getChatMode().getWebhook().equals("")) { + try { + // Open connection + URL url = new URL(event.getChatMode().getWebhook()); + HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); + + // Set properties + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:49.0) Gecko/20100101 Firefox/49.0"); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + + // Create JSON data + JSONObject data = new JSONObject(); + data.put("username", event.getChatMode() == ChatMode.ALERT ? + Config.DISCORD_ALERT_NAME : + event.getPlayer()); + data.put("avatar_url", event.getChatMode() == ChatMode.ALERT ? + Config.DISCORD_ALERT_IMAGE : + event.getPlayer() == "CONSOLE" ? + Config.DISCORD_CONSOLE_IMAGE : + Config.DISCORD_PLAYER_IMAGE.replaceAll("", + Bukkit.getPlayer(event.getPlayer()).getUniqueId().toString())); + data.put("content", event.getMessage()); + + // Send data + conn.setRequestProperty("Content-length", String.valueOf(data.toString().length())); + DataOutputStream stream = new DataOutputStream(conn.getOutputStream()); + stream.writeBytes(data.toString()); + stream.flush(); + stream.close(); + conn.getResponseMessage(); + } catch (Exception e) { + System.out.println("There was a problem forwarding chat to discord. Is the webhook url correct?"); + } + } + } +} diff --git a/src/main/java/com/tidefactions/chat/Main.java b/src/main/java/com/tidefactions/chat/Main.java index e2a6c1b..9c5c339 100644 --- a/src/main/java/com/tidefactions/chat/Main.java +++ b/src/main/java/com/tidefactions/chat/Main.java @@ -1,12 +1,12 @@ package com.tidefactions.chat; -import com.tidefactions.chat.Commands.CommandAdminChat; -import com.tidefactions.chat.Commands.CommandBuilderChat; -import com.tidefactions.chat.Commands.CommandPrefix; -import com.tidefactions.chat.Commands.CommandStaffChat; +import com.nametagedit.plugin.NametagManager; +import com.nametagedit.plugin.api.NametagAPI; +import com.tidefactions.chat.Commands.*; import com.tidefactions.chat.Databases.PrefixDatabase; import com.tidefactions.chat.EventHandlers.ChatHandler; import com.tidefactions.chat.EventHandlers.InventoryHandler; +import com.tidefactions.chat.EventHandlers.JoinHandler; import com.tidefactions.chat.Utils.PrefixUtils; import net.milkbowl.vault.chat.Chat; import net.milkbowl.vault.permission.Permission; @@ -24,30 +24,32 @@ public final class Main extends JavaPlugin { public static Logger logger; public static Main plugin; + public static NametagAPI nametagAPI; public static Permission perms = null; public static Chat chat = null; //TODO list - //- Save and load current prefix - //- Announcement support //- Titles @Override public void onEnable() { logger = this.getLogger(); plugin = this; + nametagAPI = new NametagAPI(new NametagManager()); setupPermissions(); PrefixDatabase.init(getDataFolder().getPath()); Config.load(new File(getDataFolder(), "config.yml")); Messages.load(new File(getDataFolder(), "messages.yml")); PrefixUtils.loadPrefixes(new File(plugin.getDataFolder(), "prefixes.yml")); getServer().getPluginManager().registerEvents(new ChatHandler(), this); + getServer().getPluginManager().registerEvents(new JoinHandler(), this); getServer().getPluginManager().registerEvents(new InventoryHandler(), this); getCommand("bc").setExecutor(new CommandBuilderChat()); getCommand("sc").setExecutor(new CommandStaffChat()); getCommand("ac").setExecutor(new CommandAdminChat()); + getCommand("alert").setExecutor(new CommandAlert()); getCommand("prefix").setExecutor(new CommandPrefix()); } diff --git a/src/main/java/com/tidefactions/chat/Messages.java b/src/main/java/com/tidefactions/chat/Messages.java index c56a4ca..48e77d9 100644 --- a/src/main/java/com/tidefactions/chat/Messages.java +++ b/src/main/java/com/tidefactions/chat/Messages.java @@ -21,6 +21,9 @@ public class Messages { public static String BC_CONSOLE; public static String BC_TOGGLE_ON; public static String BC_TOGGLE_OFF; + public static String ALERT_CONSOLE; + public static String ALERT_TOGGLE_ON; + public static String ALERT_TOGGLE_OFF; public static void load(File file) { @@ -50,6 +53,11 @@ public class Messages { BC_CONSOLE = colorCodes(config.getString("bc-console")); BC_TOGGLE_ON = colorCodes(config.getString("bc-toggle-on")); BC_TOGGLE_OFF = colorCodes(config.getString("bc-toggle-off")); + + // Alert messages + ALERT_CONSOLE = colorCodes(config.getString("alert-console")); + ALERT_TOGGLE_ON = colorCodes(config.getString("alert-toggle-on")); + ALERT_TOGGLE_OFF = colorCodes(config.getString("alert-toggle-off")); } catch (IOException | InvalidConfigurationException e) { e.printStackTrace(); } diff --git a/src/main/java/com/tidefactions/chat/Types/ChatMode.java b/src/main/java/com/tidefactions/chat/Types/ChatMode.java index 986028b..7bb3dcc 100644 --- a/src/main/java/com/tidefactions/chat/Types/ChatMode.java +++ b/src/main/java/com/tidefactions/chat/Types/ChatMode.java @@ -1,22 +1,24 @@ package com.tidefactions.chat.Types; import com.tidefactions.chat.Config; -import org.bukkit.ChatColor; public enum ChatMode { - PUBLIC("chat.public", "", Config.GLOBAL_CHAT_FORMAT), - STAFF("chat.staff", ChatColor.DARK_GRAY + "[" + ChatColor.AQUA + "STAFF" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET, Config.STAFF_CHAT_FORMAT), - ADMIN("chat.admin", ChatColor.DARK_GRAY + "[" + ChatColor.RED + "ADMIN" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET, Config.ADMIN_CHAT_FORMAT), - BUILDER("chat.builder", ChatColor.DARK_GRAY + "[" + ChatColor.YELLOW + "BUILDER" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET, Config.BUILDER_CHAT_FORMAT); + PUBLIC("chat.public", Config.GLOBAL_CHANNEL_PREFIX, Config.GLOBAL_CHAT_FORMAT, Config.DISCORD_GLOBAL_WEBHOOK), + STAFF("chat.staff", Config.STAFF_CHANNEL_PREFIX, Config.STAFF_CHAT_FORMAT, Config.DISCORD_STAFF_WEBHOOK), + ADMIN("chat.admin", Config.ADMIN_CHANNEL_PREFIX, Config.ADMIN_CHAT_FORMAT, Config.DISCORD_ADMIN_WEBHOOK), + BUILDER("chat.builder", Config.BUILDER_CHANNEL_PREFIX, Config.BUILDER_CHAT_FORMAT, Config.DISCORD_BUILDER_WEBHOOK), + ALERT("chat.alert", Config.ALERT_CHANNEL_PREFIX, Config.ALERT_CHAT_FORMAT, Config.DISCORD_ALERT_WEBHOOK); private String permission; private String prefix; private String format; + private String webhook; - ChatMode(String permission, String prefix, String format) { + ChatMode(String permission, String prefix, String format, String webhook) { this.permission = permission; this.prefix = prefix; this.format = format; + this.webhook = webhook; } public String getPermission() { @@ -30,4 +32,8 @@ public enum ChatMode { public String getFormat() { return format; } + + public String getWebhook() { + return webhook; + } } diff --git a/src/main/java/com/tidefactions/chat/Types/Prefix.java b/src/main/java/com/tidefactions/chat/Types/Prefix.java index f087fdc..efa87a2 100644 --- a/src/main/java/com/tidefactions/chat/Types/Prefix.java +++ b/src/main/java/com/tidefactions/chat/Types/Prefix.java @@ -9,15 +9,21 @@ public class Prefix { private String name; private String permission; private String prefix; + private String shortPrefix; + private ChatColor nameColor1; + private ChatColor nameColor2; private Material item; private Short itemMeta; private List description; private ChatColor chatColor; - public Prefix(String name, String permission, String prefix, Material item, Integer itemMeta, List description, ChatColor chatColor) { + public Prefix(String name, String permission, String prefix, String shortPrefix, ChatColor nameColor1, ChatColor nameColor2, Material item, Integer itemMeta, List description, ChatColor chatColor) { this.name = name; this.permission = permission; this.prefix = prefix; + this.shortPrefix = shortPrefix; + this.nameColor1 = nameColor1; + this.nameColor2 = nameColor2; this.item = item; this.itemMeta = itemMeta.shortValue(); this.description = description; @@ -36,6 +42,18 @@ public class Prefix { return prefix; } + public String getshortPrefix() { + return shortPrefix; + } + + public ChatColor getNameColor1() { + return nameColor1; + } + + public ChatColor getNameColor2() { + return nameColor2; + } + public Material getItem() { return item; } diff --git a/src/main/java/com/tidefactions/chat/Utils/ChatUtils.java b/src/main/java/com/tidefactions/chat/Utils/ChatUtils.java index 305f352..ca8749b 100644 --- a/src/main/java/com/tidefactions/chat/Utils/ChatUtils.java +++ b/src/main/java/com/tidefactions/chat/Utils/ChatUtils.java @@ -19,7 +19,37 @@ public class ChatUtils { String finalMessage = getMessageForChannel(player, mode, message); MessageSendInChannelEvent event = new MessageSendInChannelEvent(player.getName(), mode, message, source); Bukkit.getPluginManager().callEvent(event); - Bukkit.broadcast(finalMessage, mode.getPermission()); + Bukkit.broadcast(finalMessage, mode.getPermission() + ".receive"); + parseMentions(message); + + } + + public static void sendMessageInChannelAsConsole(ChatMode mode, String message, String source) { + String finalMessage = getMessageForChannelAsConsole(mode, message); + MessageSendInChannelEvent event = new MessageSendInChannelEvent("CONSOLE", mode, message, source); + Bukkit.getPluginManager().callEvent(event); + Bukkit.broadcast(finalMessage, mode.getPermission() + ".receive"); + parseMentions(message); + } + + public static String getMessageForChannel(Player player, ChatMode mode, String message) { + return FormatUtils.format(mode.getFormat() + .replaceAll("", mode.getPrefix()) + .replaceAll("", message), player); + } + + public static String getMessageForChannelAsConsole(ChatMode mode, String message) { + return mode.getFormat() + .replaceAll("", mode.getPrefix()) + .replaceAll("", Config.CONSOLE_PREFIX) + .replaceAll("", Config.CONSOLE_NAME_COLOR) + .replaceAll("", "CONSOLE") + .replaceAll("", Config.CONSOLE_TITLE) + .replaceAll("<CHAT_COLOR>", Config.CONSOLE_CHAT_COLOR) + .replaceAll("<MESSAGE>", message); + } + + public static void parseMentions(String message) { if (message.contains("@")) { String[] array = message.split(" "); for (String word : array) @@ -31,34 +61,6 @@ public class ChatUtils { } } - public static void sendMessageInChannelAsConsole(ChatMode mode, String message, String source) { - String finalMessage = getMessageForChannelAsConsole(mode, message); - MessageSendInChannelEvent event = new MessageSendInChannelEvent("CONSOLE", mode, message, source); - Bukkit.getPluginManager().callEvent(event); - Bukkit.broadcast(finalMessage, mode.getPermission()); - } - - public static String getMessageForChannel(Player player, ChatMode mode, String message) { - Prefix prefix = PrefixUtils.getPrefixForPlayer(player); - return mode.getFormat() - .replaceAll("<MODE_PREFIX>", mode.getPrefix()) - .replaceAll("<PREFIX>", prefix.getPrefix()) - .replaceAll("<PLAYER_NAME>", player.getName()) - .replaceAll("<TITLE>", TitleUtils.getTitleForPlayer(player)) - .replaceAll("<CHAT_COLOR>", prefix.getChatColor().toString()) - .replaceAll("<MESSAGE>", message); - } - - public static String getMessageForChannelAsConsole(ChatMode mode, String message) { - return mode.getFormat() - .replaceAll("<MODE_PREFIX>", mode.getPrefix()) - .replaceAll("<PREFIX>", Config.CONSOLE_PREFIX) - .replaceAll("<PLAYER_NAME>", "CONSOLE") - .replaceAll("<TITLE>", Config.CONSOLE_TITLE) - .replaceAll("<CHAT_COLOR>", Config.CONSOLE_CHAT_COLOR) - .replaceAll("<MESSAGE>", message); - } - public static void setChatMode(Player player, ChatMode mode) { currentChatMode.put(player.getUniqueId(), mode); } diff --git a/src/main/java/com/tidefactions/chat/Utils/FormatUtils.java b/src/main/java/com/tidefactions/chat/Utils/FormatUtils.java new file mode 100644 index 0000000..f8ebd66 --- /dev/null +++ b/src/main/java/com/tidefactions/chat/Utils/FormatUtils.java @@ -0,0 +1,35 @@ +package com.tidefactions.chat.Utils; + +import com.tidefactions.chat.Types.Prefix; +import org.bukkit.entity.Player; + +public class FormatUtils { + + /* + PREFIX + SHORT_PREFIX + NAME_COLOR_1 + NAME_COLOR_2 + PLAYER_NAME + CHAT_COLOR + */ + public static String format(String format, Player player) { + Prefix pr = PrefixUtils.getPrefixForPlayer(player); + return format(format, player, pr); + } + + public static String format(String format, Player player, Prefix pr) { + + // Standard placeholders + format = format + .replaceAll("<PREFIX>", pr.getPrefix()) + .replaceAll("<SHORT_PREFIX>", pr.getshortPrefix()) + .replaceAll("<NAME_COLOR_1>", pr.getNameColor1().toString()) + .replaceAll("<NAME_COLOR_2>", pr.getNameColor2().toString()) + .replaceAll("<PLAYER_NAME>", player.getName()) + .replaceAll("<CHAT_COLOR>", pr.getChatColor().toString()) + .replaceAll("<TITLE>", TitleUtils.getTitleForPlayer(player)); + + return format; + } +} diff --git a/src/main/java/com/tidefactions/chat/Utils/NameUtils.java b/src/main/java/com/tidefactions/chat/Utils/NameUtils.java new file mode 100644 index 0000000..c853dea --- /dev/null +++ b/src/main/java/com/tidefactions/chat/Utils/NameUtils.java @@ -0,0 +1,22 @@ +package com.tidefactions.chat.Utils; + +import com.tidefactions.chat.Config; +import com.tidefactions.chat.Main; +import com.tidefactions.chat.Types.Prefix; +import org.bukkit.entity.Player; + +public class NameUtils { + + public static void updatePlayer(Player player) { + Prefix pr = PrefixUtils.getPrefixForPlayer(player); + Main.nametagAPI.setPrefix( + player, + FormatUtils.format(Config.NAME_PREFIX_FORMAT, player, pr) + ); + Main.nametagAPI.setSuffix( + player, + FormatUtils.format(Config.NAME_SUFFIX_FORMAT, player, pr) + ); + Main.nametagAPI.manager.sendTeams(player); + } +} diff --git a/src/main/java/com/tidefactions/chat/Utils/PrefixUtils.java b/src/main/java/com/tidefactions/chat/Utils/PrefixUtils.java index 1c15d40..acee32b 100644 --- a/src/main/java/com/tidefactions/chat/Utils/PrefixUtils.java +++ b/src/main/java/com/tidefactions/chat/Utils/PrefixUtils.java @@ -13,12 +13,13 @@ import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashMap; import static com.tidefactions.chat.Main.logger; public class PrefixUtils { - private static HashMap<String, Prefix> prefixes = new HashMap<>(); + private static LinkedHashMap<String, Prefix> prefixes = new LinkedHashMap<>(); public static void loadPrefixes(File prefixFile) { if (!prefixFile.getParentFile().exists()) @@ -33,6 +34,9 @@ public class PrefixUtils { key, config.getString(key + ".permission"), ChatColor.translateAlternateColorCodes('&', config.getString(key + ".prefix")), + ChatColor.translateAlternateColorCodes('&', config.getString(key + ".short-prefix")), + ChatColor.valueOf(config.getString(key + ".name-color-1")), + ChatColor.valueOf(config.getString(key + ".name-color-1")), Material.getMaterial(config.getString(key + ".item")), config.getInt(key + ".item-meta"), config.getStringList(key + ".description"), @@ -52,6 +56,7 @@ public class PrefixUtils { public static void setPrefixForPlayer(Player player, Prefix prefix) { PrefixDatabase.setPrefix(player, prefix); + NameUtils.updatePlayer(player); } public static Prefix getPrefixByID(String id) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 0166925..7ec3d9c 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,9 +1,34 @@ +settings: + log-chat-to-console: true +prefixes: + global: "" + staff: "&8[&dSTAFF&8]&r" + admin: "&8[&cADMIN&8]&r" + builder: "&8[&eBUILDER&8]&r" + alert: "&8[&4ALERT&8]&r" format: - global: "<MODE_PREFIX><PREFIX> &r<PLAYER_NAME><TITLE>&7: <CHAT_COLOR><MESSAGE>" - staff: "<MODE_PREFIX><PREFIX> &r<PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" - admin: "<MODE_PREFIX><PREFIX> &r<PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" - builder: "<MODE_PREFIX><PREFIX> &r<PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" + global: "<CHANNEL_PREFIX><PREFIX> <NAME_COLOR><PLAYER_NAME><TITLE>&7: <CHAT_COLOR><MESSAGE>" + staff: "<CHANNEL_PREFIX> <PREFIX> <NAME_COLOR><PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" + admin: "<CHANNEL_PREFIX> <PREFIX> <NAME_COLOR><PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" + builder: "<CHANNEL_PREFIX> <PREFIX> <NAME_COLOR><PLAYER_NAME>&7: <CHAT_COLOR><MESSAGE>" + alert: "<CHANNEL_PREFIX> <MESSAGE>" +name: + prefix-format: "<TABLIST_PREFIX> <NAME_COLOR>" + suffix-format: " <TITLE>" console: prefix: "&8[&4CONSOLE&8]" title: "" - chat-color: "&4" \ No newline at end of file + name-color: "&f" + chat-color: "&4" +discord: + enable: false + alert-name: "Alert" + alert-image: "http://www.clker.com/cliparts/P/u/5/K/W/c/alert-icon-red-md.png" + console-image: "" + player-image: "https://crafatar.com/avatars/<UUID>?overlay" + webhooks: + global: "" + staff: "" + admin: "" + builder: "" + alert: "" \ No newline at end of file diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index e97a04b..58963b8 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -9,3 +9,6 @@ sc-toggle-off: "You turned off staff chat." bc-console: "Console cant use /bc without arguments it has to use /bc <message>." bc-toggle-on: "You turned on builder chat." bc-toggle-off: "You turned off builder chat." +alert-console: "Console cant use /alert without argument it has to use /alert <message>." +alert-toggle-on: "You turned on alert mode." +alert-toggle-off: "You turned off alert mode." diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index ec4ff72..844a2e3 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: Chat -version: @version@ +version: 1.1 main: com.tidefactions.chat.Main authors: [dragontamerfred] depend: [Vault] @@ -7,4 +7,5 @@ commands: bc: sc: ac: - prefix: \ No newline at end of file + alert: + prefix: diff --git a/src/main/resources/prefixes.yml b/src/main/resources/prefixes.yml index 5cde82c..b483d51 100644 --- a/src/main/resources/prefixes.yml +++ b/src/main/resources/prefixes.yml @@ -1,6 +1,9 @@ Default: permission: 'prefix.default' prefix: '&8[&fMember&8]' + short-prefix: '&8[&fM&8]' + name-color-1: '&f' + name-color-2: '&f' item: 'WOOL' item-meta: 0 chat-color: "GRAY" @@ -10,6 +13,9 @@ Default: Builder: permission: 'prefix.builder' prefix: '&8[&9Builder&8]' + short-prefix: '&8[&9B&8]' + name-color-1: '&f' + name-color-2: '&9' item: 'WOOL' item-meta: 11 chat-color: "BLUE" @@ -20,6 +26,9 @@ Builder: Helper: permission: 'prefix.helper' prefix: '&8[&eHelper&8]' + short-prefix: '&8[&eH&8]' + name-color-1: '&f' + name-color-2: '&e' item: 'WOOL' item-meta: 4 chat-color: "YELLOW" @@ -30,6 +39,9 @@ Helper: Moderator: permission: 'prefix.moderator' prefix: '&8[&6Moderator&8]' + short-prefix: '&8[&6M&8]' + name-color-1: '&f' + name-color-2: '&6' item: 'STAINED_CLAY' item-meta: 4 chat-color: "GOLD" @@ -40,6 +52,9 @@ Moderator: Sr-Moderator: permission: 'prefix.sr-moderator' prefix: '&8[&6Sr.Moderator&8]' + short-prefix: '&8[&6SM&8]' + name-color-1: '&f' + name-color-2: '&6' item: 'GOLD_BLOCK' item-meta: 0 chat-color: "GOLD" @@ -51,6 +66,9 @@ Sr-Moderator: Admin: permission: 'prefix.admin' prefix: '&8[&cAdmin&8]' + short-prefix: '&8[&cA&8]' + name-color-1: '&f' + name-color-2: '&c' item: 'WOOL' item-meta: 14 chat-color: "RED" @@ -61,6 +79,9 @@ Admin: Developer: permission: 'prefix.developer' prefix: '&8[&bDeveloper&8]' + short-prefix: '&8[&bD&8]' + name-color-1: '&f' + name-color-2: '&b' item: 'WOOL' item-meta: 3 chat-color: "AQUA" @@ -72,6 +93,9 @@ Developer: Owner: permission: 'prefix.owner' prefix: '&8[&4Owner&8]' + short-prefix: '&8[&4O&8]' + name-color-1: '&f' + name-color-2: '&4' item: 'WOOL' item-meta: 14 chat-color: "DARK_RED"