Merge remote-tracking branch 'origin/master' into dev

# Conflicts:
#	src/main/java/net/william278/velocitab/Velocitab.java
#	src/main/java/net/william278/velocitab/packet/ScoreboardManager.java
#	src/main/java/net/william278/velocitab/tab/PlayerTabList.java
This commit is contained in:
AlexDev_ 2023-10-15 13:44:39 +02:00
commit f14da2500b
25 changed files with 509 additions and 384 deletions

View File

@ -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

101
.github/workflows/release.yml vendored Normal file
View File

@ -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

View File

@ -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'

View File

@ -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.

View File

@ -11,6 +11,7 @@ Please click through to the topic you wish to read about.
## Documentation
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* ✨ [[Animations]]

34
docs/Nametags.md Normal file
View File

@ -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.
<details>
<summary>Editing nametags (config.yml)</summary>
```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
```
</details>
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.

View File

@ -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.

View File

@ -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:
```
</details>
## 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.
<details>
<summary>Per-group formats</summary>
@ -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%'
```
</details>
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 &mdash; [[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 &mdash; 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:
```
</details>
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`.

View File

@ -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.
In these cases, you may need to disable the use of scoreboard packets through the `send_scoreboard_packets` option detailed earlier.

View File

@ -5,6 +5,7 @@
## Documentation
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* ✨ [[Animations]]

View File

@ -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

BIN
images/nametags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -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<SortingManager> 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<TabPlayer> getTabPlayer(String name) {
return server.getPlayer(name).map(this::getTabPlayer);
}

View File

@ -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<String> replace(@NotNull String format, @NotNull Velocitab plugin, @NotNull TabPlayer player) {
public static CompletableFuture<String> 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);
}
}

View File

@ -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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, String> 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<String, String> 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<String, List<String>> serverGroups = new LinkedHashMap<>(Map.of("default", List.of("lobby1", "lobby2", "lobby3")));
private LinkedHashMap<String, List<String>> 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<String, String> 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();
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -39,129 +39,127 @@ import java.util.function.Supplier;
// Based on VPacketEvents PacketRegistration API
public final class PacketRegistration<P extends MinecraftPacket> {
private final Class<P> packetClass;
private Supplier<P> packetSupplier;
private ProtocolUtils.Direction direction;
private StateRegistry stateRegistry;
private final List<StateRegistry.PacketMapping> mappings = new ArrayList<>();
private final Class<P> packetClass;
private Supplier<P> packetSupplier;
private ProtocolUtils.Direction direction;
private StateRegistry stateRegistry;
private final List<StateRegistry.PacketMapping> mappings = new ArrayList<>();
public PacketRegistration<P> packetSupplier(final @NotNull Supplier<P> packetSupplier) {
this.packetSupplier = packetSupplier;
return this;
public PacketRegistration<P> packetSupplier(final @NotNull Supplier<P> packetSupplier) {
this.packetSupplier = packetSupplier;
return this;
}
public PacketRegistration<P> direction(final ProtocolUtils.Direction direction) {
this.direction = direction;
return this;
}
public PacketRegistration<P> stateRegistry(final @NotNull StateRegistry stateRegistry) {
this.stateRegistry = stateRegistry;
return this;
}
public PacketRegistration<P> 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<P> 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<P> 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<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry> versions = (Map<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry>) PACKET_REGISTRY$versions.invoke(packetRegistry);
versions.forEach((protocolVersion, protocolRegistry) -> {
try {
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) 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<P> 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 <P extends MinecraftPacket> PacketRegistration<P> of(Class<P> packetClass) {
return new PacketRegistration<>(packetClass);
}
private PacketRegistration(final @NotNull Class<P> 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<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry> versions = (Map<ProtocolVersion, StateRegistry.PacketRegistry.ProtocolRegistry>) PACKET_REGISTRY$versions.invoke(packetRegistry);
versions.forEach((protocolVersion, protocolRegistry) -> {
try {
IntObjectMap<Supplier<?>> packetIdToSupplier = (IntObjectMap<Supplier<?>>) PACKET_REGISTRY$packetIdToSupplier.invoke(protocolRegistry);
Object2IntMap<Class<?>> packetClassToId = (Object2IntMap<Class<?>>) 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 <P extends MinecraftPacket> PacketRegistration<P> of(Class<P> packetClass) {
return new PacketRegistration<>(packetClass);
}
private PacketRegistration(final @NotNull Class<P> 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);
}
}
}

View File

@ -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
));
}

View File

@ -38,6 +38,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.*;
public class ScoreboardManager {
private static final String NAMETAG_DELIMITER = ":::";
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
private final Velocitab plugin;
private final Set<TeamsPacketAdapter> 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<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
List<Player> players = siblings.stream().map(RegisteredServer::getPlayersConnected).flatMap(Collection::stream).toList();
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final List<Player> 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<ServerConnection> optionalServerConnection = player.getCurrentServer();
final Optional<ServerConnection> optionalServerConnection = player.getCurrentServer();
if (optionalServerConnection.isEmpty()) {
return;
}
RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> {
try {
List<RegisteredServer> 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);

View File

@ -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)

View File

@ -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<Role> {
public static final int DEFAULT_WEIGHT = 0;
@ -49,7 +52,7 @@ public class Role implements Comparable<Role> {
@Override
public int compareTo(@NotNull Role o) {
return weight - o.weight;
return Double.compare(weight, o.weight);
}
public Optional<String> getName() {
@ -69,8 +72,30 @@ public class Role implements Comparable<Role> {
}
@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<Character> 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;
}
}

View File

@ -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<TabPlayer> {
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<TabPlayer> {
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<TabPlayer> {
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<TabPlayer> {
@NotNull
public CompletableFuture<String> 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<TabPlayer> {
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<TabPlayer, Velocitab, String> elementResolver;
SortableElement(@NotNull BiFunction<TabPlayer, Velocitab, String> elementResolver) {
this.elementResolver = elementResolver;
}
@NotNull
private String resolve(@NotNull TabPlayer tabPlayer, @NotNull Velocitab plugin) {
return elementResolver.apply(tabPlayer, plugin);
}
public static Optional<SortableElement> parse(@NotNull String s) {
return Arrays.stream(values()).filter(element -> element.name().equalsIgnoreCase(s)).findFirst();
}
}
}

View File

@ -1,78 +0,0 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* 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<String> 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<String> 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;
}
}

View File

@ -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<TabPlayer> 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<List<String>> serversInGroup = getGroupNames(joined.getCurrentServer()
final Optional<List<String>> 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<Component> 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<Component> getFooter(@NotNull TabPlayer player) {
final String footer = plugin.getSettings().getFooter(player.getServerGroup(plugin), player.getFooterIndex());
player.incrementFooterIndex(plugin);