diff --git a/.github/workflows/java_ci.yml b/.github/workflows/java_ci.yml index 4ba9bd6..eaefd0c 100644 --- a/.github/workflows/java_ci.yml +++ b/.github/workflows/java_ci.yml @@ -56,11 +56,47 @@ jobs: papiproxybridge | suggests | * miniplaceholders | suggests | * game-versions: | + 1.8 + 1.8.1 + 1.8.2 + 1.8.3 + 1.8.4 + 1.8.5 + 1.8.6 + 1.8.7 + 1.8.8 + 1.8.9 + 1.12.2 + 1.13 + 1.13.1 + 1.13.2 + 1.14 + 1.14.1 + 1.14.2 + 1.14.3 + 1.14.4 + 1.15 + 1.15.1 + 1.15.2 + 1.16 + 1.16.1 + 1.16.2 + 1.16.3 + 1.16.4 1.16.5 + 1.17 1.17.1 + 1.18 + 1.18.1 1.18.2 + 1.19 + 1.19.1 + 1.19.2 + 1.19.3 1.19.4 + 1.20 1.20.1 + 1.20.2 java: 16 - name: Upload GitHub Artifact uses: actions/upload-artifact@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..92b35e9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,101 @@ +# Publishes a release to Modrinth and Hangar when a release is published on GitHub. +name: Release Test & Publish + +on: + release: + types: [ published ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up JDK 16 + uses: actions/setup-java@v3 + with: + java-version: '16' + distribution: 'temurin' + - name: Build with Gradle + uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1 + with: + arguments: build + - name: Query Version + run: | + echo "::set-output name=VERSION_NAME::$(${{github.workspace}}/gradlew properties --no-daemon --console=plain -q | grep "^version:" | awk '{printf $2}')" + id: fetch-version + - name: Get Version + run: | + echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV + - name: Upload to Modrinth & Hangar + uses: WiIIiam278/mc-publish@hangar + with: + modrinth-id: Q10irTG0 + modrinth-featured: true + modrinth-token: ${{ secrets.MODRINTH_TOKEN }} + modrinth-version-type: release + hangar-id: William278/Velocitab + hangar-token: ${{ secrets.HANGAR_API_KEY }} + hangar-version-type: Release + hangar-game-versions: | + 3.2 + files: target/Velocitab-*.jar + name: Velocitab v${{ github.event.release.tag_name }} + version: ${{ github.event.release.tag_name }} + changelog: ${{ github.event.release.body }} + loaders: | + velocity + dependencies: | + luckperms | suggests | * + papiproxybridge | suggests | * + miniplaceholders | suggests | * + game-versions: | + 1.8 + 1.8.1 + 1.8.2 + 1.8.3 + 1.8.4 + 1.8.5 + 1.8.6 + 1.8.7 + 1.8.8 + 1.8.9 + 1.12.2 + 1.13 + 1.13.1 + 1.13.2 + 1.14 + 1.14.1 + 1.14.2 + 1.14.3 + 1.14.4 + 1.15 + 1.15.1 + 1.15.2 + 1.16 + 1.16.1 + 1.16.2 + 1.16.3 + 1.16.4 + 1.16.5 + 1.17 + 1.17.1 + 1.18 + 1.18.1 + 1.18.2 + 1.19 + 1.19.1 + 1.19.2 + 1.19.3 + 1.19.4 + 1.20 + 1.20.1 + 1.20.2 + java: 16 + - name: Upload GitHub Artifact + uses: actions/upload-artifact@v2 + with: + name: Velocitab Plugin + path: target/Velocitab-*.jar \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0c4a369..029cca6 100644 --- a/build.gradle +++ b/build.gradle @@ -30,11 +30,11 @@ repositories { dependencies { compileOnly 'com.velocitypowered:velocity-api:3.2.0-SNAPSHOT' compileOnly 'com.velocitypowered:velocity-proxy:3.2.0-SNAPSHOT' - compileOnly 'io.netty:netty-codec-http:4.1.98.Final' + compileOnly 'io.netty:netty-codec-http:4.1.100.Final' compileOnly 'org.projectlombok:lombok:1.18.30' compileOnly 'net.luckperms:api:5.4' compileOnly 'io.github.miniplaceholders:miniplaceholders-api:2.0.0' - compileOnly 'net.william278:PAPIProxyBridge:1.4' + compileOnly 'net.william278:PAPIProxyBridge:1.4.2' compileOnly 'it.unimi.dsi:fastutil:8.5.12' diff --git a/docs/Config-File.md b/docs/Config-File.md index 87a3694..8d0ff38 100644 --- a/docs/Config-File.md +++ b/docs/Config-File.md @@ -65,6 +65,9 @@ Which formatting and the header/footer to use for a player's TAB list is determi ### Formatting Velocitab supports the full range of modern color formatting, including RGB colors and gradients, through either MineDown or MiniMessage syntax. See [[Formatting]] for more information. +## Nametags +As well as updating the text in the TAB menu, Velocitab supports updating player nametags (the text displayed above their heads). See [[Nametags]] for more information. + ### Animations Velocitab supports basic header and footer animations by adding multiple frames of animation and setting the update rate to a value greater than 0. diff --git a/docs/Home.md b/docs/Home.md index d0b5be1..a608b1e 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -11,6 +11,7 @@ Please click through to the topic you wish to read about. ## Documentation * ๐Ÿ‘ฅ [[Server Groups]] * ๐ŸŽจ [[Formatting]] +* ๐Ÿ“› [[Nametags]] * ๐Ÿ“Š [[Sorting]] * โœ๏ธ [[Placeholders]] * โœจ [[Animations]] diff --git a/docs/Nametags.md b/docs/Nametags.md new file mode 100644 index 0000000..8844dab --- /dev/null +++ b/docs/Nametags.md @@ -0,0 +1,34 @@ +Velocitab supports formatting the nametags of players (the text displayed above their heads). This can be used to display a player's rank, group, or other information using placeholders. Please note some limitations apply. + +![Nametags being updated by Velocitab in-game](https://raw.githubusercontent.com/WiIIiam278/Velocitab/master/images/nametags.png) + +> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues) + +## Setting name tags +You can configure nametags per-group using the `nametags` section of the config file. Each group should have one nametag format associated with it, which will be applied to all players on servers in that group. + +
+Editing nametags (config.yml) + +```yaml +# Nametag(s) to display above players' heads for each server group. Set to empty to disable. +# Nametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags +nametags: + default: '&f%prefix%%username%&f%suffix%' + +# (...) + +# Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting. +# Turn this off if you're using scoreboard teams on backend servers. +send_scoreboard_packets: true +``` +
+ +Only players on servers which are part of groups that specify nametag formats will have their nametag formatted. To disable nametag formatting, remove all groups from the `nametags` section of the config file (leaving it empty). + +## Formatting limitations +Nametags must adhere to the following restrictions: +* A %username% placeholder must be present. This is used for delimiting the scoreboard prefix, name, and suffix to facilitate formatting. +* Only legacy colors can be used in formats. If RGB colors are specified, they will automatically be downsampled to the nearest legacy color. This is a limitation of the scoreboard team system. +* Nametags cannot contain newlines (must be on a single line) +* Gradients are not supported. \ No newline at end of file diff --git a/docs/Placeholders.md b/docs/Placeholders.md index f1e2600..1d02313 100644 --- a/docs/Placeholders.md +++ b/docs/Placeholders.md @@ -3,23 +3,24 @@ Velocitab supports a number of Placeholders that will be replaced with their res ## Default placeholders Placeholders can be included in the header, footer and player name format of the TAB list. The following placeholders are supported out of the box: -| Placeholder | Description | Example | -|--------------------------|---------------------------------------------------|--------------------| -| `%players_online%` | Players online on the proxy | `6` | -| `%max_players_online%` | Player capacity of the proxy | `500` | -| `%local_players_online%` | Players online on the server the player is on | `3` | -| `%current_date%` | Current real-world date of the server | `24 Feb 2023` | -| `%current_time%` | Current real-world time of the server | `21:45:32` | -| `%username%` | The player's username | `William278` | -| `%server%` | Name of the server the player is on | `alpha` | -| `%ping%` | Ping of the player (in ms) | `6` | -| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` | -| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` | -| `%role%` | The player's primary LuckPerms group name | `admin` | -| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` | -| `%server_group%` | The name of the server group the player is on | `default` | -| `%server_group_index%` | Indexed order of the server group in the list | `0` | -| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` | +| Placeholder | Description | Example | +|--------------------------|------------------------------------------------------|--------------------| +| `%players_online%` | Players online on the proxy | `6` | +| `%max_players_online%` | Player capacity of the proxy | `500` | +| `%local_players_online%` | Players online on the server the player is on | `3` | +| `%current_date%` | Current real-world date of the server | `24 Feb 2023` | +| `%current_time%` | Current real-world time of the server | `21:45:32` | +| `%username%` | The player's username | `William278` | +| `%server%` | Name of the server the player is on | `alpha` | +| `%ping%` | Ping of the player (in ms) | `6` | +| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` | +| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` | +| `%role%` | The player's primary LuckPerms group name | `admin` | +| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` | +| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` | +| `%server_group%` | The name of the server group the player is on | `default` | +| `%server_group_index%` | Indexed order of the server group in the list | `0` | +| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` | ### Customising server display names You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name. diff --git a/docs/Server-Groups.md b/docs/Server-Groups.md index f680814..e742ee4 100644 --- a/docs/Server-Groups.md +++ b/docs/Server-Groups.md @@ -1,4 +1,4 @@ -Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list, alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on the server a player is on. +Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list, alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on the server a player is on. You can also set formatting to use for [[Nametags]] above players' heads per-group. ## Defining groups Groups are defined in the `server_groups` section of `config.yml`, as a list of servers following the group name (by default, a group `default` will be present, alongside a list of servers on your network. @@ -34,8 +34,8 @@ server_groups: ``` -## Mapping headers, footers & player formats to groups -Once you've defined your groups, you can modify the `headers`, `footers` and `formats` section of the file with different formats for each group. +## Mapping headers, footers, user formats, and nametags to groups +Once you've defined your groups, you can modify the `headers`, `footers`, `formats` and [`nametags`](nametags) section of the file with different formats for each group.
Per-group formats @@ -59,10 +59,14 @@ formats: lobbies: '&8[Lobby] &7%username%' creative: '&e[Creative] &7[%server%] &f%prefix%%username%' survival: '&2[Survival (%server%)] &f%prefix%%username%' +nametags: + lobbies: '&8[Lobby] &7%prefix%%username%&7%suffix%' + creative: '&e%prefix%%username%&7%suffix%' + survival: '&7%prefix%%username%&7%suffix%' ```
-See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list. +See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list. Note that some formatting limitations apply to nametags — [[Nametags]] for more information. ### Adding new lines If you want to add a new line to your header or footer format, you can use `\n` to insert one — but since this gets messy quickly, there's an easier way using the YAML markup pipe character to declare a multiline string: @@ -80,7 +84,7 @@ footers: ``` -Player name formats may only utilize one line. +Player name formats and nametags may only utilize one line. ## Default group If a player isn't connected to a server on your network, their TAB menu will be formatted as per the formats defined by `fallback_group` set in `config.yml`, provided `fallback_enabled` is set to `true`. diff --git a/docs/Sorting.md b/docs/Sorting.md index f18bb1a..a3463ee 100644 --- a/docs/Sorting.md +++ b/docs/Sorting.md @@ -1,5 +1,7 @@ Velocitab can sort players in the TAB list by a number of "sorting elements." Sorting is enabled by default, and can be disabled with the `sort_players` option in the [`config.yml`](Config-File) file. +> > **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](#compatibility-issues) + ## Sortable elements To modify what players are sorted by, modify the `sorting_placeholders` list in the [`config.yml`](Config-File) file. This option accepts an ordered list; the first element in the list is what players will be sorted by first, with subsequent elements being used to break ties. The default sorting strategy is to sort first by `%role_weight%` followed by `%username%`. @@ -37,4 +39,4 @@ There are a few compatibility caveats to bear in mind with sorting players in th * Some mods can interfere with scoreboard team packets, particularly if they internally deal with managing packets or scoreboard teams. * Sending fake scoreboard team packets might not work correctly on some Minecraft server implementations such as [Quilt](https://quiltmc.org/). -In these cases, you may need to disable sorting through the `sort_players` option detailed earlier. \ No newline at end of file +In these cases, you may need to disable the use of scoreboard packets through the `send_scoreboard_packets` option detailed earlier. \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index 55fc5e2..180bf75 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -5,6 +5,7 @@ ## Documentation * ๐Ÿ‘ฅ [[Server Groups]] * ๐ŸŽจ [[Formatting]] +* ๐Ÿ“› [[Nametags]] * ๐Ÿ“Š [[Sorting]] * โœ๏ธ [[Placeholders]] * โœจ [[Animations]] diff --git a/gradle.properties b/gradle.properties index aa870b0..219483c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,6 +3,6 @@ javaVersion=16 org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true -plugin_version=1.5 +plugin_version=1.5.1 plugin_archive=velocitab plugin_description=A super-simple (sorted!) Velocity TAB menu plugin diff --git a/images/nametags.png b/images/nametags.png new file mode 100644 index 0000000..a86be8b Binary files /dev/null and b/images/nametags.png differ diff --git a/src/main/java/net/william278/velocitab/Velocitab.java b/src/main/java/net/william278/velocitab/Velocitab.java index 909a412..33b43f8 100644 --- a/src/main/java/net/william278/velocitab/Velocitab.java +++ b/src/main/java/net/william278/velocitab/Velocitab.java @@ -47,7 +47,6 @@ import net.william278.velocitab.player.Role; import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.sorting.SortingManager; import net.william278.velocitab.tab.PlayerTabList; -import net.william278.velocitab.vanish.VanishManager; import org.bstats.charts.SimplePie; import org.bstats.velocity.Metrics; import org.jetbrains.annotations.NotNull; @@ -91,7 +90,6 @@ public class Velocitab { public void onProxyInitialization(@NotNull ProxyInitializeEvent event) { loadSettings(); loadHooks(); - prepareSortingManager(); prepareScoreboardManager(); prepareTabList(); prepareVanishManager(); @@ -165,21 +163,15 @@ public class Velocitab { Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(this).ifPresent(hooks::add)); } - private void prepareSortingManager() { - if (settings.isSortPlayers()) { - this.sortingManager = new SortingManager(this); - } - } - private void prepareScoreboardManager() { - if (settings.isSortPlayers()) { + if (settings.isSendScoreboardPackets()) { this.scoreboardManager = new ScoreboardManager(this); scoreboardManager.registerPacket(); } } private void disableScoreboardManager() { - if (scoreboardManager != null && settings.isSortPlayers()) { + if (scoreboardManager != null && settings.isSendScoreboardPackets()) { scoreboardManager.unregisterPacket(); } } @@ -193,10 +185,6 @@ public class Velocitab { return Optional.ofNullable(scoreboardManager); } - public Optional getSortingManager() { - return Optional.ofNullable(sortingManager); - } - @NotNull public PlayerTabList getTabList() { return tabList; @@ -210,11 +198,11 @@ public class Velocitab { @NotNull public TabPlayer getTabPlayer(@NotNull Player player) { return new TabPlayer(player, - getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE), - getLuckPermsHook().map(LuckPermsHook::getHighestWeight).orElse(0) + getLuckPermsHook().map(hook -> hook.getPlayerRole(player)).orElse(Role.DEFAULT_ROLE) ); } + @SuppressWarnings("unused") public Optional getTabPlayer(String name) { return server.getPlayer(name).map(this::getTabPlayer); } diff --git a/src/main/java/net/william278/velocitab/config/Placeholder.java b/src/main/java/net/william278/velocitab/config/Placeholder.java index c645f02..d20434f 100644 --- a/src/main/java/net/william278/velocitab/config/Placeholder.java +++ b/src/main/java/net/william278/velocitab/config/Placeholder.java @@ -50,7 +50,7 @@ public enum Placeholder { SUFFIX((plugin, player) -> player.getRole().getSuffix().orElse("")), ROLE((plugin, player) -> player.getRole().getName().orElse("")), ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName().orElse("")), - ROLE_WEIGHT((plugin, player) -> Integer.toString(player.getRole().getWeight())), + ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()), SERVER_GROUP((plugin, player) -> player.getServerGroup(plugin)), SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))), DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse(""))); @@ -65,7 +65,8 @@ public enum Placeholder { this.replacer = replacer; } - public static CompletableFuture replace(@NotNull String format, @NotNull Velocitab plugin, @NotNull TabPlayer player) { + public static CompletableFuture replace(@NotNull String format, @NotNull Velocitab plugin, + @NotNull TabPlayer player) { for (Placeholder placeholder : values()) { format = format.replace("%" + placeholder.name().toLowerCase() + "%", placeholder.replacer.apply(plugin, player)); } @@ -83,4 +84,9 @@ public enum Placeholder { }); } + @NotNull + public static String formatSortableInt(int value, int maxValue) { + return String.format("%0" + Integer.toString(maxValue).length() + "d", maxValue - value); + } + } diff --git a/src/main/java/net/william278/velocitab/config/Settings.java b/src/main/java/net/william278/velocitab/config/Settings.java index 3f44578..96da5af 100644 --- a/src/main/java/net/william278/velocitab/config/Settings.java +++ b/src/main/java/net/william278/velocitab/config/Settings.java @@ -46,22 +46,27 @@ public class Settings { private boolean checkForUpdates = true; @YamlKey("headers") - @YamlComment("Header(s) to display above the TAB list for each server group." + - "\nList multiple headers and set update_rate to the number of ticks between frames for basic animations") - private Map> headers = Map.of("default", List.of("&rainbow&Running Velocitab by William278")); + @YamlComment("Header(s) to display above the TAB list for each server group." + + "\nList multiple headers and set update_rate to the number of ticks between frames for basic animations") + private Map> headers = Map.of( + "default", + List.of("&rainbow&Running Velocitab by William278") + ); @YamlKey("footers") @YamlComment("Footer(s) to display below the TAB list for each server group, same as headers.") private Map> footers = Map.of( "default", - List.of("[There are currently %players_online%/%max_players_online% players online](gray)")); + List.of("[There are currently %players_online%/%max_players_online% players online](gray)") + ); @YamlKey("formats") private Map formats = Map.of("default", "&7[%server%] &f%prefix%%username%"); @Getter @YamlKey("nametags") - @YamlComment("Nametag(s) to display above players' heads for each server group. To disable, set to empty") + @YamlComment("Nametag(s) to display above players' heads for each server group. Set to empty to disable." + + "\nNametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags") private Map nametags = Map.of("default", "&f%prefix%%username%&f%suffix%"); @Getter @@ -72,12 +77,15 @@ public class Settings { @Getter @YamlKey("server_groups") @YamlComment("The servers in each group of servers. The order of groups is important when sorting by SERVER_GROUP.") - private LinkedHashMap> serverGroups = new LinkedHashMap<>(Map.of("default", List.of("lobby1", "lobby2", "lobby3"))); + private LinkedHashMap> serverGroups = new LinkedHashMap<>(Map.of( + "default", + List.of("lobby1", "lobby2", "lobby3")) + ); @Getter @YamlKey("fallback_enabled") - @YamlComment("All servers which are not in other groups will be put in the fallback group.\n" + - "\"false\" will exclude them from Velocitab.") + @YamlComment("All servers which are not in other groups will be put in the fallback group." + + "\n\"false\" will exclude them from Velocitab.") private boolean fallbackEnabled = true; @Getter @@ -92,8 +100,8 @@ public class Settings { @Getter @YamlKey("server_display_names") - @YamlComment("Define custom names to be shown in the TAB list for specific server names.\n" + - "If no custom display name is provided for a server, its original name will be used.") + @YamlComment("Define custom names to be shown in the TAB list for specific server names." + + "\nIf no custom display name is provided for a server, its original name will be used.") private Map serverDisplayNames = Map.of("very-long-server-name", "VLSN"); @Getter @@ -111,6 +119,12 @@ public class Settings { @YamlComment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.") private boolean enableMiniPlaceholdersHook = true; + @Getter + @YamlKey("send_scoreboard_packets") + @YamlComment("Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting." + + "\nTurn this off if you're using scoreboard teams on backend servers.") + private boolean sendScoreboardPackets = true; + @Getter @YamlKey("sort_players") @YamlComment("Whether to sort players in the TAB list.") @@ -176,7 +190,7 @@ public class Settings { nametags.getOrDefault(serverGroup, "")); } - public boolean areNametagsEnabled() { + public boolean doNametags() { return !nametags.isEmpty(); } diff --git a/src/main/java/net/william278/velocitab/hook/Hook.java b/src/main/java/net/william278/velocitab/hook/Hook.java index 25d73d4..fafcb01 100644 --- a/src/main/java/net/william278/velocitab/hook/Hook.java +++ b/src/main/java/net/william278/velocitab/hook/Hook.java @@ -35,7 +35,7 @@ public abstract class Hook { try { plugin.log("Successfully hooked into LuckPerms"); return Optional.of(new LuckPermsHook(plugin)); - } catch (Exception e) { + } catch (Throwable e) { plugin.log(Level.WARN, "LuckPerms hook was not loaded: " + e.getMessage(), e); } } @@ -46,7 +46,7 @@ public abstract class Hook { try { plugin.log("Successfully hooked into PAPIProxyBridge"); return Optional.of(new PAPIProxyBridgeHook(plugin)); - } catch (Exception e) { + } catch (Throwable e) { plugin.log(Level.WARN, "PAPIProxyBridge hook was not loaded: " + e.getMessage(), e); } } @@ -57,7 +57,7 @@ public abstract class Hook { try { plugin.log("Successfully hooked into MiniPlaceholders"); return Optional.of(new MiniPlaceholdersHook(plugin)); - } catch (Exception e) { + } catch (Throwable e) { plugin.log(Level.WARN, "MiniPlaceholders hook was not loaded: " + e.getMessage(), e); } } diff --git a/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java b/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java index 92e3969..e01b547 100644 --- a/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java +++ b/src/main/java/net/william278/velocitab/hook/LuckPermsHook.java @@ -89,8 +89,7 @@ public class LuckPermsHook extends Hook { .buildTask(plugin, () -> { final TabPlayer updatedPlayer = new TabPlayer( player, - getRoleFromMetadata(event.getData().getMetaData()), - getHighestWeight() + getRoleFromMetadata(event.getData().getMetaData()) ); tabList.replacePlayer(updatedPlayer); tabList.updatePlayer(updatedPlayer); @@ -113,18 +112,6 @@ public class LuckPermsHook extends Hook { return group.getWeight().orElse(Role.DEFAULT_WEIGHT); } - public int getHighestWeight() { - if (highestWeight == Role.DEFAULT_WEIGHT) { - api.getGroupManager().getLoadedGroups().forEach(group -> { - final OptionalInt weight = group.getWeight(); - if (weight.isPresent() && weight.getAsInt() > highestWeight) { - highestWeight = weight.getAsInt(); - } - }); - } - return highestWeight; - } - private User getUser(@NotNull UUID uuid) { return api.getUserManager().getUser(uuid); } diff --git a/src/main/java/net/william278/velocitab/packet/PacketRegistration.java b/src/main/java/net/william278/velocitab/packet/PacketRegistration.java index b37ffa3..d0e7e35 100644 --- a/src/main/java/net/william278/velocitab/packet/PacketRegistration.java +++ b/src/main/java/net/william278/velocitab/packet/PacketRegistration.java @@ -39,129 +39,127 @@ import java.util.function.Supplier; // Based on VPacketEvents PacketRegistration API public final class PacketRegistration

{ - private final Class

packetClass; - private Supplier

packetSupplier; - private ProtocolUtils.Direction direction; - private StateRegistry stateRegistry; - private final List mappings = new ArrayList<>(); + private final Class

packetClass; + private Supplier

packetSupplier; + private ProtocolUtils.Direction direction; + private StateRegistry stateRegistry; + private final List mappings = new ArrayList<>(); - public PacketRegistration

packetSupplier(final @NotNull Supplier

packetSupplier) { - this.packetSupplier = packetSupplier; - return this; + public PacketRegistration

packetSupplier(final @NotNull Supplier

packetSupplier) { + this.packetSupplier = packetSupplier; + return this; + } + + public PacketRegistration

direction(final ProtocolUtils.Direction direction) { + this.direction = direction; + return this; + } + + public PacketRegistration

stateRegistry(final @NotNull StateRegistry stateRegistry) { + this.stateRegistry = stateRegistry; + return this; + } + + public PacketRegistration

mapping( + final int id, + final ProtocolVersion version, + final boolean encodeOnly + ) { + try { + final StateRegistry.PacketMapping mapping = (StateRegistry.PacketMapping) PACKET_MAPPING$map.invoke( + id, version, encodeOnly); + this.mappings.add(mapping); + } catch (Throwable t) { + throw new RuntimeException(t); } + return this; + } - public PacketRegistration

direction(final ProtocolUtils.Direction direction) { - this.direction = direction; - return this; + public void register() { + try { + final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND + ? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry) + : (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry); + + PACKET_REGISTRY$register.invoke( + packetRegistry, + packetClass, + packetSupplier, + mappings.toArray(StateRegistry.PacketMapping[]::new) + ); + + } catch (Throwable t) { + throw new RuntimeException(t); } + } - public PacketRegistration

stateRegistry(final @NotNull StateRegistry stateRegistry) { - this.stateRegistry = stateRegistry; - return this; + @SuppressWarnings("unchecked") + public void unregister() { + try { + final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND + ? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry) + : (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry); + + Map versions = (Map) PACKET_REGISTRY$versions.invoke(packetRegistry); + versions.forEach((protocolVersion, protocolRegistry) -> { + try { + IntObjectMap> packetIdToSupplier = (IntObjectMap>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry); + Object2IntMap> packetClassToId = (Object2IntMap>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry); + packetIdToSupplier.keySet().stream() + .filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass)) + .forEach(packetIdToSupplier::remove); + packetClassToId.values().intStream() + .filter(id -> Objects.equals(packetClassToId.getInt(packetClass), id)) + .forEach(packetClassToId::removeInt); + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + + } catch (Throwable t) { + throw new RuntimeException(t); } + } - public PacketRegistration

mapping( - final int id, - final ProtocolVersion version, - final boolean encodeOnly - ) { - try { - final StateRegistry.PacketMapping mapping = (StateRegistry.PacketMapping) PACKET_MAPPING$map.invoke( - id, version, encodeOnly); - this.mappings.add(mapping); - } catch (Throwable t) { - throw new RuntimeException(t); - } - return this; + public static

PacketRegistration

of(Class

packetClass) { + return new PacketRegistration<>(packetClass); + } + + private PacketRegistration(final @NotNull Class

packetClass) { + this.packetClass = packetClass; + } + + private static final MethodHandle STATE_REGISTRY$clientBound; + private static final MethodHandle STATE_REGISTRY$serverBound; + private static final MethodHandle PACKET_REGISTRY$register; + private static final MethodHandle PACKET_REGISTRY$packetIdToSupplier; + private static final MethodHandle PACKET_REGISTRY$packetClassToId; + private static final MethodHandle PACKET_REGISTRY$versions; + private static final MethodHandle PACKET_MAPPING$map; + + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup); + STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class); + STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class); + + final MethodType mapType = MethodType.methodType(StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE); + PACKET_MAPPING$map = stateRegistryLookup.findStatic(StateRegistry.class, "map", mapType); + + final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.class, lookup); + final MethodType registerType = MethodType.methodType(void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class); + PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(StateRegistry.PacketRegistry.class, "register", registerType); + PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(StateRegistry.PacketRegistry.class, "versions", Map.class); + + final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup); + PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class); + PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class); + + + } catch (Throwable e) { + throw new RuntimeException(e); } + } - public void register() { - try { - final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND - ? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry) - : (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry); - - PACKET_REGISTRY$register.invoke( - packetRegistry, - packetClass, - packetSupplier, - mappings.toArray(StateRegistry.PacketMapping[]::new) - ); - - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - @SuppressWarnings("unchecked") - public void unregister() { - try { - final StateRegistry.PacketRegistry packetRegistry = direction == ProtocolUtils.Direction.CLIENTBOUND - ? (StateRegistry.PacketRegistry) STATE_REGISTRY$clientBound.invoke(stateRegistry) - : (StateRegistry.PacketRegistry) STATE_REGISTRY$serverBound.invoke(stateRegistry); - - Map versions = (Map) PACKET_REGISTRY$versions.invoke(packetRegistry); - versions.forEach((protocolVersion, protocolRegistry) -> { - try { - IntObjectMap> packetIdToSupplier = (IntObjectMap>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry); - Object2IntMap> packetClassToId = (Object2IntMap>) PACKET_REGISTRY$packetClassToId.invoke(protocolRegistry); - packetIdToSupplier.keySet().stream() - .filter(supplier -> packetIdToSupplier.get(supplier).get().getClass().equals(packetClass)) - .forEach(packetIdToSupplier::remove); - packetClassToId.values().intStream() - .filter(id -> Objects.equals(packetClassToId.getInt(packetClass), id)) - .forEach(packetClassToId::removeInt); - } catch (Throwable t) { - throw new RuntimeException(t); - } - }); - - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - public static

PacketRegistration

of(Class

packetClass) { - return new PacketRegistration<>(packetClass); - } - - private PacketRegistration(final @NotNull Class

packetClass) { - this.packetClass = packetClass; - } - - private static final MethodHandle STATE_REGISTRY$clientBound; - private static final MethodHandle STATE_REGISTRY$serverBound; - private static final MethodHandle PACKET_REGISTRY$register; - private static final MethodHandle PACKET_REGISTRY$packetIdToSupplier; - private static final MethodHandle PACKET_REGISTRY$packetClassToId; - private static final MethodHandle PACKET_REGISTRY$versions; - private static final MethodHandle PACKET_MAPPING$map; - - - - - static { - final MethodHandles.Lookup lookup = MethodHandles.lookup(); - try { - final MethodHandles.Lookup stateRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.class, lookup); - STATE_REGISTRY$clientBound = stateRegistryLookup.findGetter(StateRegistry.class, "clientbound", StateRegistry.PacketRegistry.class); - STATE_REGISTRY$serverBound = stateRegistryLookup.findGetter(StateRegistry.class, "serverbound", StateRegistry.PacketRegistry.class); - - final MethodType mapType = MethodType.methodType(StateRegistry.PacketMapping.class, Integer.TYPE, ProtocolVersion.class, Boolean.TYPE); - PACKET_MAPPING$map = stateRegistryLookup.findStatic(StateRegistry.class, "map", mapType); - - final MethodHandles.Lookup packetRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.class, lookup); - final MethodType registerType = MethodType.methodType(void.class, Class.class, Supplier.class, StateRegistry.PacketMapping[].class); - PACKET_REGISTRY$register = packetRegistryLookup.findVirtual(StateRegistry.PacketRegistry.class, "register", registerType); - PACKET_REGISTRY$versions = packetRegistryLookup.findGetter(StateRegistry.PacketRegistry.class, "versions", Map.class); - - final MethodHandles.Lookup protocolRegistryLookup = MethodHandles.privateLookupIn(StateRegistry.PacketRegistry.ProtocolRegistry.class, lookup); - PACKET_REGISTRY$packetIdToSupplier = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetIdToSupplier", IntObjectMap.class); - PACKET_REGISTRY$packetClassToId = protocolRegistryLookup.findGetter(StateRegistry.PacketRegistry.ProtocolRegistry.class, "packetClassToId", Object2IntMap.class); - - - } catch (Throwable e) { - throw new RuntimeException(e); - } - } } diff --git a/src/main/java/net/william278/velocitab/packet/Protocol403Adapter.java b/src/main/java/net/william278/velocitab/packet/Protocol403Adapter.java index 4dd339a..50097e1 100644 --- a/src/main/java/net/william278/velocitab/packet/Protocol403Adapter.java +++ b/src/main/java/net/william278/velocitab/packet/Protocol403Adapter.java @@ -30,20 +30,38 @@ import java.util.List; import java.util.Set; /** - * Adapter for handling the UpdateTeamsPacket for Minecraft 1.13+ + * Adapter for handling the UpdateTeamsPacket for Minecraft 1.13.2+ */ @SuppressWarnings("DuplicatedCode") public class Protocol403Adapter extends TeamsPacketAdapter { public Protocol403Adapter() { super(Set.of(ProtocolVersion.MINECRAFT_1_13_2, + ProtocolVersion.MINECRAFT_1_14, + ProtocolVersion.MINECRAFT_1_14_1, + ProtocolVersion.MINECRAFT_1_14_2, + ProtocolVersion.MINECRAFT_1_14_3, ProtocolVersion.MINECRAFT_1_14_4, + ProtocolVersion.MINECRAFT_1_15, + ProtocolVersion.MINECRAFT_1_15_1, ProtocolVersion.MINECRAFT_1_15_2, + ProtocolVersion.MINECRAFT_1_16, + ProtocolVersion.MINECRAFT_1_16_1, + ProtocolVersion.MINECRAFT_1_16_2, + ProtocolVersion.MINECRAFT_1_16_3, ProtocolVersion.MINECRAFT_1_16_4, + ProtocolVersion.MINECRAFT_1_17, ProtocolVersion.MINECRAFT_1_17_1, + ProtocolVersion.MINECRAFT_1_18, + //ProtocolVersion.MINECRAFT_1_18_1, ProtocolVersion.MINECRAFT_1_18_2, + ProtocolVersion.MINECRAFT_1_19, + ProtocolVersion.MINECRAFT_1_19_1, + //ProtocolVersion.MINECRAFT_1_19_2, + ProtocolVersion.MINECRAFT_1_19_3, ProtocolVersion.MINECRAFT_1_19_4, - ProtocolVersion.MINECRAFT_1_20 + ProtocolVersion.MINECRAFT_1_20, + ProtocolVersion.MINECRAFT_1_20_2 )); } diff --git a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java index ba7f4fa..cc3ec88 100644 --- a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java +++ b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java @@ -38,6 +38,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.*; public class ScoreboardManager { + private static final String NAMETAG_DELIMITER = ":::"; private PacketRegistration packetRegistration; private final Velocitab plugin; private final Set versions; @@ -67,7 +68,7 @@ public class ScoreboardManager { } public void resetCache(@NotNull Player player) { - String team = createdTeams.remove(player.getUniqueId()); + final String team = createdTeams.remove(player.getUniqueId()); if (team != null) { dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), player); } @@ -87,16 +88,15 @@ public class ScoreboardManager { String suffix = split.length > 1 ? split[1] : ""; if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) { - if (createdTeams.containsKey(player.getUniqueId())) { dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())), player); } createdTeams.put(player.getUniqueId(), role); - this.nametags.put(role, prefix + ":::" + suffix); + this.nametags.put(role, prefix + NAMETAG_DELIMITER + suffix); dispatchGroupPacket(UpdateTeamsPacket.create(plugin, role, "", prefix, suffix, name), player); - } else if (!this.nametags.getOrDefault(role, "").equals(prefix + ":::" + suffix)) { - this.nametags.put(role, prefix + ":::" + suffix); + } else if (!this.nametags.getOrDefault(role, "").equals(prefix + NAMETAG_DELIMITER + suffix)) { + this.nametags.put(role, prefix + NAMETAG_DELIMITER + suffix); dispatchGroupPacket(UpdateTeamsPacket.changeNameTag(plugin, role, prefix, suffix), player); } }).exceptionally(e -> { @@ -107,8 +107,7 @@ public class ScoreboardManager { public void resendAllNameTags(Player player) { - - if (!plugin.getSettings().areNametagsEnabled()) { + if (!plugin.getSettings().doNametags()) { return; } @@ -117,12 +116,12 @@ public class ScoreboardManager { return; } - RegisteredServer serverInfo = optionalServerConnection.get().getServer(); - - List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); - - List players = siblings.stream().map(RegisteredServer::getPlayersConnected).flatMap(Collection::stream).toList(); - + final RegisteredServer serverInfo = optionalServerConnection.get().getServer(); + final List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); + final List players = siblings.stream() + .map(RegisteredServer::getPlayersConnected) + .flatMap(Collection::stream) + .toList(); players.forEach(p -> { if (p == player || !p.isActive()) { return; @@ -138,10 +137,9 @@ public class ScoreboardManager { return; } - String[] split = nametag.split(":::", 2); - String prefix = split[0]; - String suffix = split.length > 1 ? split[1] : ""; - + final String[] split = nametag.split(NAMETAG_DELIMITER, 2); + final String prefix = split[0]; + final String suffix = split.length > 1 ? split[1] : ""; dispatchPacket(UpdateTeamsPacket.create(plugin, role, "", prefix, suffix, p.getUsername()), player); }); } @@ -155,39 +153,35 @@ public class ScoreboardManager { try { final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player; connectedPlayer.getConnection().write(packet); - } catch (Exception e) { - plugin.log(Level.ERROR, "Failed to dispatch packet (is the client or server modded or using an illegal version?)", e); + } catch (Throwable e) { + plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e); } } private void dispatchGroupPacket(@NotNull UpdateTeamsPacket packet, @NotNull Player player) { - Optional optionalServerConnection = player.getCurrentServer(); - + final Optional optionalServerConnection = player.getCurrentServer(); if (optionalServerConnection.isEmpty()) { return; } - RegisteredServer serverInfo = optionalServerConnection.get().getServer(); + final RegisteredServer serverInfo = optionalServerConnection.get().getServer(); + final List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); + siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> { + try { - List siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName()); - - siblings.forEach(s -> { - s.getPlayersConnected().forEach(p -> { - - boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) || plugin.getVanishManager().canSee(player.getUsername(), player.getUsername()); + boolean canSee = !plugin.getVanishManager().isVanished(p.getUsername()) + || plugin.getVanishManager().canSee(player.getUsername(), player.getUsername()); if (!canSee) { return; } - try { - final ConnectedPlayer connectedPlayer = (ConnectedPlayer) p; - connectedPlayer.getConnection().write(packet); - } catch (Exception e) { - plugin.log(Level.ERROR, "Failed to dispatch packet (is the client or server modded or using an illegal version?)", e); - } - }); - }); + final ConnectedPlayer connectedPlayer = (ConnectedPlayer) connected; + connectedPlayer.getConnection().write(packet); + } catch (Throwable e) { + plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e); + } + })); } public void registerPacket() { @@ -204,7 +198,8 @@ public class ScoreboardManager { .mapping(0x55, MINECRAFT_1_17, true) .mapping(0x58, MINECRAFT_1_19_1, true) .mapping(0x56, MINECRAFT_1_19_3, true) - .mapping(0x5A, MINECRAFT_1_19_4, true); + .mapping(0x5A, MINECRAFT_1_19_4, true) + .mapping(0x5C, MINECRAFT_1_20_2, true); packetRegistration.register(); } catch (Throwable e) { plugin.log(Level.ERROR, "Failed to register UpdateTeamsPacket", e); diff --git a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java index f801b44..1097dda 100644 --- a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java +++ b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java @@ -30,7 +30,6 @@ import net.william278.velocitab.Velocitab; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -62,7 +61,9 @@ public class UpdateTeamsPacket implements MinecraftPacket { } @NotNull - protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull String teamName, @NotNull String displayName, @Nullable String prefix, @Nullable String suffix, @NotNull String... teamMembers) { + protected static UpdateTeamsPacket create(@NotNull Velocitab plugin, @NotNull String teamName, + @NotNull String displayName, @Nullable String prefix, + @Nullable String suffix, @NotNull String... teamMembers) { return new UpdateTeamsPacket(plugin) .teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName) .mode(UpdateMode.CREATE_TEAM) @@ -77,7 +78,8 @@ public class UpdateTeamsPacket implements MinecraftPacket { } @NotNull - protected static UpdateTeamsPacket changeNameTag(@NotNull Velocitab plugin, @NotNull String teamName, @Nullable String prefix, @Nullable String suffix) { + protected static UpdateTeamsPacket changeNameTag(@NotNull Velocitab plugin, @NotNull String teamName, + @Nullable String prefix, @Nullable String suffix) { return new UpdateTeamsPacket(plugin) .teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName) .mode(UpdateMode.UPDATE_INFO) @@ -91,7 +93,8 @@ public class UpdateTeamsPacket implements MinecraftPacket { } @NotNull - protected static UpdateTeamsPacket addToTeam(@NotNull Velocitab plugin, @NotNull String teamName, @NotNull String... teamMembers) { + protected static UpdateTeamsPacket addToTeam(@NotNull Velocitab plugin, @NotNull String teamName, + @NotNull String... teamMembers) { return new UpdateTeamsPacket(plugin) .teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName) .mode(UpdateMode.ADD_PLAYERS) @@ -99,7 +102,8 @@ public class UpdateTeamsPacket implements MinecraftPacket { } @NotNull - protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName, @NotNull String... teamMembers) { + protected static UpdateTeamsPacket removeFromTeam(@NotNull Velocitab plugin, @NotNull String teamName, + @NotNull String... teamMembers) { return new UpdateTeamsPacket(plugin) .teamName(teamName.length() > 16 ? teamName.substring(0, 16) : teamName) .mode(UpdateMode.REMOVE_PLAYERS) @@ -117,13 +121,12 @@ public class UpdateTeamsPacket implements MinecraftPacket { if (text == null) { return 15; } - int intvar = text.lastIndexOf("ยง"); - - if (intvar == -1 || intvar == text.length() - 1) { + int lastFormatIndex = text.lastIndexOf("ยง"); + if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) { return 15; } - String last = text.substring(intvar, intvar + 2); + final String last = text.substring(lastFormatIndex, lastFormatIndex + 2); return TeamColor.getColorId(last.charAt(1)); } @@ -155,12 +158,15 @@ public class UpdateTeamsPacket implements MinecraftPacket { private final int id; TeamColor(char colorChar, int id) { - this.colorChar= colorChar; + this.colorChar = colorChar; this.id = id; } public static int getColorId(char var) { - return Arrays.stream(values()).filter(color -> color.colorChar == var).map(c -> c.id).findFirst().orElse(15); + return Arrays.stream(values()) + .filter(color -> color.colorChar == var) + .map(c -> c.id).findFirst() + .orElse(15); } } @@ -176,7 +182,6 @@ public class UpdateTeamsPacket implements MinecraftPacket { if (optionalManager.isEmpty()) { return; } - optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this); } @@ -202,6 +207,7 @@ public class UpdateTeamsPacket implements MinecraftPacket { return id; } + @Nullable public static UpdateMode byId(byte id) { return Arrays.stream(values()) .filter(mode -> mode.id == id) diff --git a/src/main/java/net/william278/velocitab/player/Role.java b/src/main/java/net/william278/velocitab/player/Role.java index 8cbacd2..dde0d2b 100644 --- a/src/main/java/net/william278/velocitab/player/Role.java +++ b/src/main/java/net/william278/velocitab/player/Role.java @@ -23,7 +23,10 @@ import lombok.Getter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; public class Role implements Comparable { public static final int DEFAULT_WEIGHT = 0; @@ -49,7 +52,7 @@ public class Role implements Comparable { @Override public int compareTo(@NotNull Role o) { - return weight - o.weight; + return Double.compare(weight, o.weight); } public Optional getName() { @@ -69,8 +72,30 @@ public class Role implements Comparable { } @NotNull - protected String getWeightString(int highestWeight) { - return String.format("%0" + Integer.toString(highestWeight).length() + "d", highestWeight - weight); + protected String getWeightString() { + return compressNumber(Integer.MAX_VALUE / 4d - weight); + } + + public String compressNumber(double number) { + int wholePart = (int) number; + + final char decimalChar = (char) ((number - wholePart) * Character.MAX_VALUE); + + final List charList = new ArrayList<>(); + + while (wholePart > 0) { + char digit = (char) (wholePart % Character.MAX_VALUE); + + charList.add(0, digit); + + wholePart /= Character.MAX_VALUE; + } + + if (charList.isEmpty()) { + charList.add((char) 0); + } + + return charList.stream().map(String::valueOf).collect(Collectors.joining()) + decimalChar; } } diff --git a/src/main/java/net/william278/velocitab/player/TabPlayer.java b/src/main/java/net/william278/velocitab/player/TabPlayer.java index cd51251..31d3eee 100644 --- a/src/main/java/net/william278/velocitab/player/TabPlayer.java +++ b/src/main/java/net/william278/velocitab/player/TabPlayer.java @@ -28,15 +28,12 @@ import net.william278.velocitab.tab.PlayerTabList; import org.jetbrains.annotations.NotNull; import org.slf4j.event.Level; -import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; public final class TabPlayer implements Comparable { private final Player player; private final Role role; - private final int highestWeight; @Getter private int headerIndex = 0; @Getter @@ -45,10 +42,9 @@ public final class TabPlayer implements Comparable { private Component lastDisplayname; private String teamName; - public TabPlayer(@NotNull Player player, @NotNull Role role, int highestWeight) { + public TabPlayer(@NotNull Player player, @NotNull Role role) { this.player = player; this.role = role; - this.highestWeight = highestWeight; } @NotNull @@ -61,6 +57,11 @@ public final class TabPlayer implements Comparable { return role; } + @NotNull + public String getRoleWeightString() { + return getRole().getWeightString(); + } + /** * Get the server name the player is currently on. * Isn't affected by server aliases defined in the config. @@ -125,13 +126,17 @@ public final class TabPlayer implements Comparable { @NotNull public CompletableFuture getTeamName(@NotNull Velocitab plugin) { - return plugin.getSortingManager().map(sortingManager -> sortingManager.getTeamName(this)) - .orElseGet(() -> CompletableFuture.completedFuture("")) - .thenApply(teamName -> { - this.teamName = teamName; - return teamName; - }).exceptionally(e -> { - plugin.log(Level.ERROR, "Failed to get team name for " + player.getUsername(), e); + if (!plugin.getSettings().isSortPlayers()) { + return CompletableFuture.completedFuture(""); + } + + final String sortingFormat = String.join("", plugin.getSettings().getSortingElements()); + return Placeholder.replace(sortingFormat, plugin, this) // Replace placeholders + .thenApply(formatted -> formatted.length() > 12 ? formatted.substring(0, 12) : formatted) // Truncate + .thenApply(truncated -> truncated + getPlayer().getUniqueId().toString().substring(0, 4)) // Make unique + .thenApply(teamName -> this.teamName = teamName) + .exceptionally(e -> { + plugin.log(Level.ERROR, "Failed to get team name for " + player.getUsername(), e); return ""; }); } @@ -174,39 +179,4 @@ public final class TabPlayer implements Comparable { return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId()); } - /** - * Elements for sorting players - */ - @SuppressWarnings("unused") - public enum SortableElement { - ROLE_WEIGHT((player, plugin) -> player.getRole().getWeightString(player.highestWeight)), - ROLE_NAME((player, plugin) -> player.getRole().getName() - .map(name -> name.length() > 3 ? name.substring(0, 3) : name) - .orElse("")), - SERVER_NAME((player, plugin) -> player.getServerName()), - SERVER_GROUP((player, plugin) -> { - int orderSize = plugin.getSettings().getServerGroups().size(); - int position = player.getServerGroupPosition(plugin); - return position >= 0 - ? String.format("%0" + Integer.toString(orderSize).length() + "d", position) - : String.valueOf(orderSize); - }), - SERVER_GROUP_NAME(TabPlayer::getServerGroup); - - private final BiFunction elementResolver; - - SortableElement(@NotNull BiFunction elementResolver) { - this.elementResolver = elementResolver; - } - - @NotNull - private String resolve(@NotNull TabPlayer tabPlayer, @NotNull Velocitab plugin) { - return elementResolver.apply(tabPlayer, plugin); - } - - public static Optional parse(@NotNull String s) { - return Arrays.stream(values()).filter(element -> element.name().equalsIgnoreCase(s)).findFirst(); - } - } - } diff --git a/src/main/java/net/william278/velocitab/sorting/SortingManager.java b/src/main/java/net/william278/velocitab/sorting/SortingManager.java deleted file mode 100644 index 324939e..0000000 --- a/src/main/java/net/william278/velocitab/sorting/SortingManager.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of Velocitab, licensed under the Apache License 2.0. - * - * Copyright (c) William278 - * Copyright (c) contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.william278.velocitab.sorting; - -import com.google.common.base.Strings; -import net.william278.velocitab.Velocitab; -import net.william278.velocitab.config.Placeholder; -import net.william278.velocitab.player.TabPlayer; -import org.slf4j.event.Level; - -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -public class SortingManager { - - private final Velocitab plugin; - private static final String DELIMITER = ":::"; - - public SortingManager(Velocitab plugin) { - this.plugin = plugin; - } - - public CompletableFuture getTeamName(TabPlayer player) { - return Placeholder.replace(String.join(DELIMITER, plugin.getSettings().getSortingElements()), plugin, player) - .thenApply(s -> Arrays.asList(s.split(DELIMITER))) - .thenApply(v -> v.stream().map(this::adaptValue).collect(Collectors.toList())) - .thenApply(v -> handleList(player, v)); - } - - private String handleList(TabPlayer player, List values) { - String result = String.join("", values); - - if (result.length() > 12) { - result = result.substring(0, 12); - plugin.log(Level.WARN, "Sorting element list is too long, truncating to 16 characters"); - } - - result += player.getPlayer().getUniqueId().toString().substring(0, 4); - - return result; - } - - private String adaptValue(String value) { - if (value.isEmpty()) { - return ""; - } - if (value.matches("[0-9]+")) { - int integer = Integer.parseInt(value); - int intSortSize = 3; - return (integer >= 0 ? 0 : 1) + String.format("%0" + intSortSize + "d", Integer.parseInt(Strings.repeat("9", intSortSize)) - Math.abs(integer)); - } - - if (value.length() > 6) { - return value.substring(0, 4); - } - - return value; - } -} diff --git a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java index 090dc7a..c92cf43 100644 --- a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java +++ b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java @@ -42,6 +42,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; +/** + * The main class for tracking the server TAB list + */ public class PlayerTabList { private final Velocitab plugin; private final ConcurrentLinkedQueue players; @@ -68,19 +71,21 @@ public class PlayerTabList { // Remove the player from the tracking list if they are switching servers final RegisteredServer previousServer = event.getPreviousServer(); - if (previousServer == null) { + if (previousServer != null) { players.removeIf(player -> player.getPlayer().getUniqueId().equals(joined.getUniqueId())); } // Get the servers in the group from the joined server name // If the server is not in a group, use fallback - Optional> serversInGroup = getGroupNames(joined.getCurrentServer() + final Optional> serversInGroup = getGroupNames(joined.getCurrentServer() .map(ServerConnection::getServerInfo) .map(ServerInfo::getName) .orElse("?")); + // If the server is not in a group, use fallback. - // If fallback is disabled, permit the player to switch excluded servers without header or footer override - if (serversInGroup.isEmpty() && (previousServer != null && !this.fallbackServers.contains(previousServer.getServerInfo().getName()))) { + // If fallback is disabled, permit the player to switch excluded servers without a header or footer override + if (serversInGroup.isEmpty() && + (previousServer != null && !this.fallbackServers.contains(previousServer.getServerInfo().getName()))) { event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty()); return; } @@ -128,8 +133,7 @@ public class PlayerTabList { plugin.getScoreboardManager().ifPresent(s -> { s.resendAllNameTags(joined); - plugin.getTabPlayer(joined).getTeamName(plugin) - .thenAccept(t -> s.updateRole(joined, t)); + plugin.getTabPlayer(joined).getTeamName(plugin).thenAccept(t -> s.updateRole(joined, t)); }); }) .delay(500, TimeUnit.MILLISECONDS) @@ -164,20 +168,24 @@ public class PlayerTabList { @Subscribe public void onPlayerQuit(@NotNull DisconnectEvent event) { - if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) return; + if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) { + return; + } // Remove the player from the tracking list, Print warning if player was not removed - if (!players.removeIf(player -> player.getPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()))) { - plugin.log("Failed to remove disconnecting player " + event.getPlayer().getUsername() + " (UUID: " + event.getPlayer().getUniqueId() + ")"); + final UUID uuid = event.getPlayer().getUniqueId(); + if (!players.removeIf(listed -> listed.getPlayer().getUniqueId().equals(uuid))) { + plugin.log(String.format("Failed to remove disconnecting player %s (UUID: %s)", + event.getPlayer().getUsername(), uuid.toString())); } // Remove the player from the tab list of all other players - plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(event.getPlayer().getUniqueId())); + plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid)); // Update the tab list of all players plugin.getServer().getScheduler() .buildTask(plugin, () -> players.forEach(player -> { - player.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId()); + player.getPlayer().getTabList().removeEntry(uuid); player.sendHeaderAndFooter(this); })) .delay(500, TimeUnit.MILLISECONDS) @@ -201,19 +209,21 @@ public class PlayerTabList { } tabPlayer.getTeamName(plugin).thenAccept(teamName -> { - if (teamName == null) return; - + if (teamName.isBlank()) { + return; + } plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole( - tabPlayer.getPlayer(), - teamName + tabPlayer.getPlayer(), teamName )); }); } public void updatePlayerDisplayName(TabPlayer tabPlayer) { - Component lastDisplayName = tabPlayer.getLastDisplayname(); + final Component lastDisplayName = tabPlayer.getLastDisplayname(); tabPlayer.getDisplayName(plugin).thenAccept(displayName -> { - if (displayName == null || displayName.equals(lastDisplayName)) return; + if (displayName == null || displayName.equals(lastDisplayName)) { + return; + } boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername()); @@ -233,10 +243,12 @@ public class PlayerTabList { }); } + // Update the display names of all listed players public void updateDisplayNames() { players.forEach(this::updatePlayerDisplayName); } + // Get the component for the TAB list header public CompletableFuture getHeader(@NotNull TabPlayer player) { final String header = plugin.getSettings().getHeader(player.getServerGroup(plugin), player.getHeaderIndex()); player.incrementHeaderIndex(plugin); @@ -245,6 +257,7 @@ public class PlayerTabList { .thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin)); } + // Get the component for the TAB list footer public CompletableFuture getFooter(@NotNull TabPlayer player) { final String footer = plugin.getSettings().getFooter(player.getServerGroup(plugin), player.getFooterIndex()); player.incrementFooterIndex(plugin);