Compare commits

...
25 Commits
Author SHA1 Message Date
NEZNAMY 9da0db5f74 Disable creation of blank issues 2023-11-04 11:55:58 +01:00
NEZNAMY 562a973c84 Create new issue template 2023-11-04 11:53:10 +01:00
NEZNAMY 967980ddf7 Update bug_report.yml 2023-11-03 21:27:24 +01:00
NEZNAMY 81d3675a87 Check for bundle packet on 1.19.4+ when reading entity packets for unlimited nametags 2023-11-03 21:25:21 +01:00
NEZNAMY 7be786bbaf Fix vanish change being called on join even if vanish status did not change 2023-11-02 17:24:02 +01:00
NEZNAMY f41c9be1f7 Fix race condition on Velocity causing errors on shutdown 2023-11-02 17:21:48 +01:00
NEZNAMY e88062d028 Fix unvanish method not being called on server switch when clearing vanish status 2023-11-02 16:08:15 +01:00
NEZNAMY edb2828643 Fix wrong java instruction 2023-11-02 15:58:34 +01:00
NEZNAMY 58c189cd0e Catch all exceptions/errors on load/unload to prevent players from getting kicked on bungeecord if something changes again in the future 2023-10-31 16:41:29 +01:00
NEZNAMY 012994847e Unregister teams of players who vanished for those who cannot see vanished players to prevent 3rd party clients from revealing vanished players by finding out who is vanished and who actually left. 2023-10-30 20:39:21 +01:00
NEZNAMY d87801be5f Add support for latest BungeeCord (#1123) 2023-10-30 16:02:31 +01:00
NEZNAMY 6d78c0c104 Create a class storing values of minecraft limitations 2023-10-29 15:25:00 +01:00
NEZNAMY 3a83519122 Fix tablist entry related issues on both BungeeCord and Velocity (each one was affected by a different issue) 2023-10-27 22:27:42 +02:00
NEZNAMY a9f940ebed Avoid attempts to send packets to offline players on bungeecord 2023-10-27 19:17:56 +02:00
NEZNAMY db73d9ef81 Simplify 2023-10-24 17:46:25 +02:00
NEZNAMY e2bd21c717 Cache adventure components & use Paper API for header/footer by default. This has reduced CPU usage of the feature by 50% in the test I have done. 2023-10-24 17:40:22 +02:00
NEZNAMY 871215b7b5 Do not attempt to send scoreboard-related packets between login packet send and process 2023-10-24 15:18:17 +02:00
NEZNAMY a6153ef46f Force-fix NPE that doesn't make sense 2023-10-24 13:42:55 +02:00
NEZNAMY 35475a6103 Fix the previous error on Fabric & Sponge as well 2023-10-24 13:42:35 +02:00
NEZNAMY bf77ffc1a5 Fix error (#1117) 2023-10-22 23:01:53 +02:00
NEZNAMY 7a64a9a534 Code style 2023-10-22 20:08:27 +02:00
NEZNAMY 68e057b99c Fabric - Implement world name recognition and everything related to it (#1115) 2023-10-22 17:44:34 +02:00
NEZNAMY 7338afff96 Fix error when using 3 years old version of PlaceholderAPI 2023-10-21 11:27:38 +02:00
NEZNAMY 97ae7813aa Add vanish status updating to RedisBungee (#1109) 2023-10-18 16:48:01 +02:00
NEZNAMY c0f3667ac1 4.0.8-SNAPSHOT 2023-10-18 16:47:37 +02:00
63 changed files with 781 additions and 191 deletions

No files matched your search

+2
View File
@@ -60,4 +60,6 @@ body:
- label: I ticked all of the boxes without actually reading them
required: false
- label: I have read the Compatibility wiki page and am not trying to run the plugin on an unsupported server version / platform
required: true
- label: I am willing to cooperate to get this issue resolved faster, including providing any additional information requested or testing new builds
required: true
@@ -0,0 +1,38 @@
name: Compatibility issue
description: A compatibility issue with another plugin/mod or a specific version of a platform marked as supported
labels: ["Type: Compatibility issue"]
body:
- type: input
attributes:
label: TAB version
description: Full version in numbers, "latest" is not a version.
placeholder: 4.0.0
validations:
required: true
- type: textarea
attributes:
label: Software causing compatibility issue
description: plugin/mod/platform version causing the issue
validations:
required: true
- type: textarea
attributes:
label: Additional info
description: Anything else you want to provide that will help in resolving the issue faster.
validations:
required: false
- type: checkboxes
attributes:
label: Checklist
description: Let's make sure this report is valid
options:
- label: I am running latest version of the plugin
required: true
- label: I ticked all of the boxes without actually reading them
required: false
- label: The compatibility wiki page does not mention this incompatibility
required: true
- label: I am willing to cooperate to get this issue resolved faster, including providing any additional information requested or testing new builds
required: true
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+1 -1
View File
@@ -4,7 +4,7 @@ plugins {
allprojects {
group = "me.neznamy"
version = "4.0.7"
version = "4.0.8-SNAPSHOT"
description = "An all-in-one solution that works"
ext.set("id", "tab")
@@ -62,11 +62,7 @@ public class BukkitUtils {
sb.append(component.getModifier().getColor().getLegacyColor().getFormat());
}
}
if (component.getModifier().isBold()) sb.append(EnumChatFormat.BOLD.getFormat());
if (component.getModifier().isStrikethrough()) sb.append(EnumChatFormat.STRIKETHROUGH.getFormat());
if (component.getModifier().isItalic()) sb.append(EnumChatFormat.ITALIC.getFormat());
if (component.getModifier().isObfuscated()) sb.append(EnumChatFormat.OBFUSCATED.getFormat());
if (component.getModifier().isUnderlined()) sb.append(EnumChatFormat.UNDERLINE.getFormat());
sb.append(component.getModifier().getMagicCodes());
if (component.getText() != null) sb.append(component.getText());
for (IChatBaseComponent extra : component.getExtra()) {
sb.append(toBukkitFormat(extra, rgbClient));
@@ -7,6 +7,7 @@ import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.placeholders.expansion.TabExpansion;
import me.neznamy.tab.shared.util.ReflectionUtils;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -112,4 +113,15 @@ public class BukkitTabExpansion extends PlaceholderExpansion implements TabExpan
public void setValue(@NotNull TabPlayer player, @NotNull String key, @NotNull String value) {
values.computeIfAbsent(player, p -> new HashMap<>()).put(key, value);
}
@Override
@SuppressWarnings({"UnstableApiUsage", "deprecation"})
public void unregisterExpansion() {
if (ReflectionUtils.methodExists(PlaceholderExpansion.class, "unregister")) {
// Added in 2.10.7 (Jul 28, 2020)
unregister();
} else {
PlaceholderAPI.unregisterExpansion(this);
}
}
}
@@ -23,19 +23,17 @@ public abstract class HeaderFooter {
* Finds the best available instance for current server software.
*/
public static void findInstance() {
if (PacketHeaderFooter.isAvailable()) {
instance = new PacketHeaderFooter();
} else if (PaperHeaderFooter.isAvailable()) {
if (PaperHeaderFooter.isAvailable()) {
instance = new PaperHeaderFooter();
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.RED.getFormat() + "[TAB] Failed to initialize NMS fields for " +
"sending Header/Footer due to a compatibility error. Using fallback solution using Paper API.");
} else if (PacketHeaderFooter.isAvailable()) {
instance = new PacketHeaderFooter();
} else if (BukkitHeaderFooter.isAvailable()) {
instance = new BukkitHeaderFooter();
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.RED.getFormat() + "[TAB] Failed to initialize NMS fields for " +
"sending Header/Footer due to a compatibility error. Using fallback solution using Bukkit API. " +
"This will drastically drop performance, as well as miss support for fonts (added in 1.16). " +
"Please update the plugin with to version proper support for your server version.");
} else if (BukkitReflection.getMinorVersion() >= 8){
} else if (BukkitReflection.getMinorVersion() >= 8) {
Bukkit.getConsoleSender().sendMessage(EnumChatFormat.RED.getFormat() + "[TAB] Failed to initialize NMS fields for " +
"sending Header/Footer due to a compatibility error. No fallback solution was found either. This will " +
"result in the feature not working. " +
@@ -66,6 +66,9 @@ public class PacketEntityView implements EntityView {
private static Class<?> PacketPlayOutNamedEntitySpawn;
private static Field PacketPlayOutNamedEntitySpawn_ENTITYID;
private static Class<?> ClientboundBundlePacket;
private static Field ClientboundBundlePacket_packets;
private static Object dummyEntity;
@Getter
@@ -86,6 +89,10 @@ public class PacketEntityView implements EntityView {
loadEntityTeleport();
loadEntityMove();
loadEntitySpawn();
if (BukkitReflection.is1_19_4Plus()) {
ClientboundBundlePacket = Class.forName("net.minecraft.network.protocol.game.ClientboundBundlePacket");
ClientboundBundlePacket_packets = ReflectionUtils.getOnlyField(ClientboundBundlePacket.getSuperclass(), Iterable.class);
}
available = true;
}
@@ -366,6 +373,17 @@ public class PacketEntityView implements EntityView {
}
}
@Override
public boolean isBundlePacket(@NotNull Object packet) {
return BukkitReflection.is1_19_4Plus() && ClientboundBundlePacket.isInstance(packet);
}
@Override
@SneakyThrows
public Iterable<Object> getPackets(@NotNull Object bundlePacket) {
return (Iterable<Object>) ClientboundBundlePacket_packets.get(bundlePacket);
}
/**
* Returns class from given potential names. Supports modded servers, such as Thermos.
*
@@ -142,7 +142,7 @@ public class BukkitPlatform implements BackendPlatform {
@Override
public void registerUnknownPlaceholder(@NotNull String identifier) {
if (!placeholderAPI) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, -1, () -> identifier);
registerDummyPlaceholder(identifier);
return;
}
PlaceholderManagerImpl pl = TAB.getInstance().getPlaceholderManager();
@@ -4,6 +4,7 @@ import lombok.Getter;
import me.neznamy.tab.platforms.bukkit.BukkitTabPlayer;
import me.neznamy.tab.platforms.bukkit.BukkitUtils;
import me.neznamy.tab.platforms.bukkit.nms.BukkitReflection;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.EnumChatFormat;
@@ -155,22 +156,22 @@ public class BukkitScoreboard extends Scoreboard<BukkitTabPlayer> {
public void newObjective(String objectiveName, String criteria, String title, @NotNull HealthDisplay display) {
if (serverMinorVersion >= 14) {
scoreboard.registerNewObjective(objectiveName, criteria, transform(title, 128, 32), RenderType.valueOf(display.name()));
scoreboard.registerNewObjective(objectiveName, criteria, transform(title, 128, Limitations.SCOREBOARD_TITLE_PRE_1_13), RenderType.valueOf(display.name()));
} else {
setDisplayName(scoreboard.registerNewObjective(objectiveName, display == HealthDisplay.HEARTS ? "health" : "dummy"), title);
}
}
public void setDisplayName(@NotNull Objective objective, @NotNull String displayName) {
objective.setDisplayName(transform(displayName, 128, 32));
objective.setDisplayName(transform(displayName, 128, Limitations.SCOREBOARD_TITLE_PRE_1_13));
}
public void setPrefix(@NotNull Team team, @NotNull String prefix) {
team.setPrefix(transform(prefix, 64, 16));
team.setPrefix(transform(prefix, 64, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
}
public void setSuffix(@NotNull Team team, @NotNull String suffix) {
team.setSuffix(transform(suffix, 64, 16));
team.setSuffix(transform(suffix, 64, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13));
}
/**
@@ -4,7 +4,6 @@ import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.platform.bossbar.BossBar;
import me.neznamy.tab.api.bossbar.BarColor;
import me.neznamy.tab.api.bossbar.BarStyle;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
@@ -35,7 +34,7 @@ public class BungeeBossBar implements BossBar {
net.md_5.bungee.protocol.packet.BossBar packet = new net.md_5.bungee.protocol.packet.BossBar(id, 0);
packet.setHealth(progress);
packet.setTitle(IChatBaseComponent.optimizedComponent(title).toString(player.getVersion()));
BungeeMultiVersion.setTitle(packet, title, player.getVersion());
packet.setColor(color.ordinal());
packet.setDivision(style.ordinal());
player.sendPacket(packet);
@@ -49,7 +48,7 @@ public class BungeeBossBar implements BossBar {
if (player.getVersion().getMinorVersion() < 9) return;
net.md_5.bungee.protocol.packet.BossBar packet = new net.md_5.bungee.protocol.packet.BossBar(id, 3);
packet.setTitle(IChatBaseComponent.optimizedComponent(title).toString(player.getVersion()));
BungeeMultiVersion.setTitle(packet, title, player.getVersion());
player.sendPacket(packet);
}
@@ -1,5 +1,6 @@
package me.neznamy.tab.platforms.bungeecord;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.platform.EventListener;
@@ -25,7 +26,29 @@ public class BungeeEventListener extends EventListener<ProxiedPlayer> implements
@EventHandler
public void onSwitch(ServerSwitchEvent e) {
serverChange(e.getPlayer(), e.getPlayer().getUniqueId(), e.getPlayer().getServer().getInfo().getName(), false);
TAB tab = TAB.getInstance();
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player == null) {
player = createPlayer(e.getPlayer());
// I don't think this is actually needed, but someone said it fixed geyser warn in console
// Doing this won't hurt, so whatever
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_20_2.getNetworkId()) {
player.getScoreboard().freeze();
}
tab.getFeatureManager().onJoin(player);
} else {
tab.getFeatureManager().onServerChange(player.getUniqueId(), e.getPlayer().getServer().getInfo().getName());
if (player.getVersion().getNetworkId() < ProtocolVersion.V1_20_2.getNetworkId()) {
// For versions below 1.20.2 the tablist is already clean when this event is called
// For 1.20.2+ this event is called before, so we listen to Login packet instead
tab.getFeatureManager().onTabListClear(player);
}
}
});
}
@EventHandler
@@ -0,0 +1,200 @@
package me.neznamy.tab.platforms.bungeecord;
import lombok.SneakyThrows;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.ChatModifier;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.*;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.util.stream.Collectors;
public class BungeeMultiVersion {
/**
* Flag tracking if the current BungeeCord version contains changes made in build 1732.
* This build changes "ScoreboardDisplay" field "position" from "byte" to "int".
*/
private static final boolean b1732Plus = ReflectionUtils.getFields(ScoreboardDisplay.class, int.class).size() > 0;
/**
* Flag tracking if the current BungeeCord version contains changes made in build 1738.
* This build adds "sendPacketQueued" method to UserConnection class.
*/
private static final boolean b1738Plus = ReflectionUtils.methodExists(UserConnection.class, "sendPacketQueued", DefinedPacket.class);
/**
* Flag tracking if the current BungeeCord version contains changes made in build 1760.
* This build changes all packet fields for displaying information from String to BaseComponent.
*/
private static final boolean b1760Plus = ReflectionUtils.methodExists(BossBar.class, "setTitle", BaseComponent.class);
@SneakyThrows
@NotNull
public static ScoreboardDisplay newScoreboardDisplay(int position, @NotNull String name) {
if (b1732Plus) {
return ScoreboardDisplay.class.getConstructor(int.class, String.class).newInstance(position, name);
} else {
return new ScoreboardDisplay((byte) position, name);
}
}
@SneakyThrows
public static void sendPacket(@NotNull BungeeTabPlayer player, @NotNull DefinedPacket packet) {
if (b1738Plus) {
try {
UserConnection.class.getDeclaredMethod("sendPacketQueued", DefinedPacket.class).invoke(player.getPlayer(), packet);
} catch (InvocationTargetException BungeeCordBug) { // Since we use reflection we must catch this one
// java.lang.NullPointerException: Cannot invoke "net.md_5.bungee.protocol.MinecraftEncoder.getProtocol()" because the return value of "io.netty.channel.ChannelPipeline.get(java.lang.Class)" is null
// at net.md_5.bungee.netty.ChannelWrapper.getEncodeProtocol(ChannelWrapper.java:51)
// at net.md_5.bungee.UserConnection.sendPacketQueued(UserConnection.java:194)
if (TAB.getInstance().getConfiguration().isDebugMode()) {
TAB.getInstance().getErrorManager().printError("Failed to deliver packet to player " + player.getName() +
" (online = " + player.getPlayer().isConnected() + "): " + packet, BungeeCordBug);
}
}
} else {
player.getPlayer().unsafe().sendPacket(packet);
}
}
@SneakyThrows
public static void setDisplayName(@NotNull Item item, @NotNull IChatBaseComponent displayName, @NotNull ProtocolVersion version) {
if (b1760Plus) {
Item.class.getMethod("setDisplayName", BaseComponent.class).invoke(item, toComponent(displayName, version));
} else {
item.setDisplayName(displayName.toString(version));
}
}
@SneakyThrows
@NotNull
public static PlayerListHeaderFooter newPlayerListHeaderFooter(@NotNull IChatBaseComponent header,
@NotNull IChatBaseComponent footer, ProtocolVersion version) {
if (b1760Plus) {
return PlayerListHeaderFooter.class.getConstructor(BaseComponent.class, BaseComponent.class).newInstance(
toComponent(header, version),
toComponent(footer, version)
);
} else {
return new PlayerListHeaderFooter(
header.toString(version),
footer.toString(version)
);
}
}
@SneakyThrows
public static void setTitle(@NotNull BossBar bossBar, @NotNull String title, @NotNull ProtocolVersion version) {
if (b1760Plus) {
BossBar.class.getMethod("setTitle", BaseComponent.class).invoke(bossBar, toComponent(IChatBaseComponent.optimizedComponent(title), version));
} else {
bossBar.setTitle(IChatBaseComponent.optimizedComponent(title).toString(version));
}
}
@NotNull
@SneakyThrows
public static ScoreboardObjective newScoreboardObjective(@NotNull String objective, @NotNull IChatBaseComponent title,
@Nullable ScoreboardObjective.HealthDisplay healthDisplay, byte action,
@NotNull ProtocolVersion version) {
if (b1760Plus) {
return ScoreboardObjective.class
.getConstructor(String.class, BaseComponent.class, ScoreboardObjective.HealthDisplay.class, byte.class)
.newInstance(objective, toComponent(title, version), healthDisplay, action);
} else {
return new ScoreboardObjective(objective, title.toString(version), healthDisplay, action);
}
}
@NotNull
@SneakyThrows
public static Team newTeam(@NotNull String name, byte mode, @NotNull String prefix, @NotNull String suffix,
@NotNull String nameTagVisibility, @NotNull String collisionRule, int color,
byte options, @Nullable String[] players, @NotNull ProtocolVersion version) {
if (b1760Plus) {
return Team.class.getConstructor(String.class, byte.class, BaseComponent.class, BaseComponent.class,
BaseComponent.class, String.class, String.class, int.class, byte.class, String[].class)
.newInstance(
name,
mode,
toComponent(IChatBaseComponent.optimizedComponent(name), version),
toComponent(IChatBaseComponent.optimizedComponent(prefix), version),
toComponent(IChatBaseComponent.optimizedComponent(suffix), version),
nameTagVisibility,
collisionRule,
color,
options,
players
);
} else {
return new Team(
name,
mode,
jsonOrRaw(name, version),
jsonOrRaw(prefix, version),
jsonOrRaw(suffix, version),
nameTagVisibility,
collisionRule,
color,
options,
players
);
}
}
/**
* If player's version is 1.13+, creates a component from given text and returns
* it as a serialized component, which BungeeCord uses.
* <p>
* If player's version is 1.12-, the text is returned
*
* @param text
* Text to convert
* @param version
* Protocol version to create json string for
* @return serialized component for 1.13+ clients, cut string for 1.12-
*/
@NotNull
private static String jsonOrRaw(@NotNull String text, @NotNull ProtocolVersion version) {
if (version.getMinorVersion() >= 13) {
return IChatBaseComponent.optimizedComponent(text).toString(version);
} else {
return text;
}
}
/**
* Converts internal component class to platform's component class
*
* @param component
* Component to convert
* @param version
* Game version to convert component for
* @return Converted component
*/
private static BaseComponent toComponent(@NotNull IChatBaseComponent component, @NotNull ProtocolVersion version) {
TextComponent textComponent = new TextComponent(component.getText());
ChatModifier modifier = component.getModifier();
if (modifier.getColor() != null) textComponent.setColor(ChatColor.of(
modifier.getColor().toString(version.getMinorVersion() >= 16)));
if (modifier.isBold()) textComponent.setBold(true);
if (modifier.isItalic()) textComponent.setItalic(true);
if (modifier.isObfuscated()) textComponent.setObfuscated(true);
if (modifier.isStrikethrough()) textComponent.setStrikethrough(true);
if (modifier.isUnderlined()) textComponent.setUnderlined(true);
if (!component.getExtra().isEmpty()) textComponent.setExtra(
component.getExtra().stream().map(c -> toComponent(c, version)).collect(Collectors.toList()));
return textComponent;
}
}
@@ -163,7 +163,7 @@ public class BungeePipelineInjector extends NettyPipelineInjector {
for (PlayerListItem.Item item : update.getItems()) {
if (update.getActions().contains(PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME)) {
IChatBaseComponent newDisplayName = TAB.getInstance().getFeatureManager().onDisplayNameChange(receiver, item.getUuid());
if (newDisplayName != null) item.setDisplayName(newDisplayName.toString(receiver.getVersion()));
if (newDisplayName != null) BungeeMultiVersion.setDisplayName(item, newDisplayName, receiver.getVersion());
}
if (update.getActions().contains(PlayerListItemUpdate.Action.UPDATE_LATENCY)) {
item.setPing(TAB.getInstance().getFeatureManager().onLatencyChange(receiver, item.getUuid(), item.getPing()));
@@ -177,7 +177,7 @@ public class BungeePipelineInjector extends NettyPipelineInjector {
for (PlayerListItem.Item item : listItem.getItems()) {
if (listItem.getAction() == PlayerListItem.Action.UPDATE_DISPLAY_NAME || listItem.getAction() == PlayerListItem.Action.ADD_PLAYER) {
IChatBaseComponent newDisplayName = TAB.getInstance().getFeatureManager().onDisplayNameChange(receiver, item.getUuid());
if (newDisplayName != null) item.setDisplayName(newDisplayName.toString(receiver.getVersion()));
if (newDisplayName != null) BungeeMultiVersion.setDisplayName(item, newDisplayName, receiver.getVersion());
}
if (listItem.getAction() == PlayerListItem.Action.UPDATE_LATENCY || listItem.getAction() == PlayerListItem.Action.ADD_PLAYER) {
item.setPing(TAB.getInstance().getFeatureManager().onLatencyChange(receiver, item.getUuid(), item.getPing()));
@@ -4,14 +4,11 @@ import lombok.SneakyThrows;
import me.neznamy.tab.shared.chat.EnumChatFormat;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import me.neznamy.tab.shared.platform.Scoreboard;
import net.md_5.bungee.protocol.packet.ScoreboardDisplay;
import net.md_5.bungee.protocol.packet.ScoreboardObjective;
import net.md_5.bungee.protocol.packet.ScoreboardScore;
import net.md_5.bungee.protocol.packet.Team;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.Collection;
/**
@@ -21,11 +18,6 @@ import java.util.Collection;
*/
public class BungeeScoreboard extends Scoreboard<BungeeTabPlayer> {
/** Constructor to support both <1.20.2 and 1.20.2+ builds */
@SuppressWarnings("unchecked")
private static final Constructor<ScoreboardDisplay> newScoreboardDisplay = (Constructor<ScoreboardDisplay>)
Arrays.stream(ScoreboardDisplay.class.getConstructors()).filter(c -> c.getParameterCount() == 2).findAny().orElse(null);
public BungeeScoreboard(@NotNull BungeeTabPlayer player) {
super(player);
}
@@ -33,36 +25,39 @@ public class BungeeScoreboard extends Scoreboard<BungeeTabPlayer> {
@Override
@SneakyThrows
public void setDisplaySlot(@NotNull DisplaySlot slot, @NotNull String objective) {
player.sendPacket(newScoreboardDisplay.newInstance((byte)slot.ordinal(), objective));
player.sendPacket(BungeeMultiVersion.newScoreboardDisplay(slot.ordinal(), objective));
}
@Override
public void registerObjective0(@NotNull String objectiveName, @NotNull String title, @NotNull HealthDisplay display) {
player.sendPacket(new ScoreboardObjective(
player.sendPacket(BungeeMultiVersion.newScoreboardObjective(
objectiveName,
jsonOrRaw(title),
IChatBaseComponent.optimizedComponent(title),
ScoreboardObjective.HealthDisplay.valueOf(display.name()),
(byte) 0
(byte) 0,
player.getVersion()
));
}
@Override
public void unregisterObjective0(@NotNull String objectiveName) {
player.sendPacket(new ScoreboardObjective(
player.sendPacket(BungeeMultiVersion.newScoreboardObjective(
objectiveName,
"", // Empty string to prevent kick on 1.7
new IChatBaseComponent(), // Empty value instead of null to prevent NPE kick on 1.7
null,
(byte) 1
(byte) 1,
player.getVersion()
));
}
@Override
public void updateObjective0(@NotNull String objectiveName, @NotNull String title, @NotNull HealthDisplay display) {
player.sendPacket(new ScoreboardObjective(
player.sendPacket(BungeeMultiVersion.newScoreboardObjective(
objectiveName,
jsonOrRaw(title),
IChatBaseComponent.optimizedComponent(title),
ScoreboardObjective.HealthDisplay.valueOf(display.name()),
(byte) 2
(byte) 2,
player.getVersion()
));
}
@@ -74,8 +69,18 @@ public class BungeeScoreboard extends Scoreboard<BungeeTabPlayer> {
if (player.getVersion().getMinorVersion() >= 13) {
color = EnumChatFormat.lastColorsOf(prefix).ordinal();
}
player.sendPacket(new Team(name, (byte) 0, jsonOrRaw(name), jsonOrRaw(prefix), jsonOrRaw(suffix),
visibility.toString(), collision.toString(), color, (byte)options, players.toArray(new String[0])));
player.sendPacket(BungeeMultiVersion.newTeam(
name,
(byte) 0,
prefix,
suffix,
visibility.toString(),
collision.toString(),
color,
(byte)options,
players.toArray(new String[0]),
player.getVersion()
));
}
@Override
@@ -90,8 +95,18 @@ public class BungeeScoreboard extends Scoreboard<BungeeTabPlayer> {
if (player.getVersion().getMinorVersion() >= 13) {
color = EnumChatFormat.lastColorsOf(prefix).ordinal();
}
player.sendPacket(new Team(name, (byte) 2, jsonOrRaw(name), jsonOrRaw(prefix),
jsonOrRaw(suffix), visibility.toString(), collision.toString(), color, (byte)options, null));
player.sendPacket(BungeeMultiVersion.newTeam(
name,
(byte) 2,
prefix,
suffix,
visibility.toString(),
collision.toString(),
color,
(byte)options,
null,
player.getVersion()
));
}
@Override
@@ -103,23 +118,4 @@ public class BungeeScoreboard extends Scoreboard<BungeeTabPlayer> {
public void removeScore0(@NotNull String objective, @NotNull String playerName) {
player.sendPacket(new ScoreboardScore(playerName, (byte) 1, objective, 0));
}
/**
* If player's version is 1.13+, creates a component from given text and returns
* it as a serialized component, which BungeeCord uses.
* <p>
* If player's version is 1.12-, the text is returned
*
* @param text
* Text to convert
* @return serialized component for 1.13+ clients, cut string for 1.12-
*/
@NotNull
private String jsonOrRaw(@NotNull String text) {
if (player.getVersion().getMinorVersion() >= 13) {
return IChatBaseComponent.optimizedComponent(text).toString(player.getVersion());
} else {
return text;
}
}
}
@@ -14,7 +14,7 @@ public class BungeeTAB extends Plugin {
public void onEnable() {
if (!ReflectionUtils.classExists("net.md_5.bungee.protocol.packet.PlayerListItemUpdate")) {
getLogger().info(EnumChatFormat.color("&cThe plugin requires BungeeCord build #1671 " +
"(with 1.19.3 support) and up (or an equivalent fork) to work."));
"(released on December 7th, 2022) and up (or an equivalent fork) to work."));
return;
}
TAB.create(new BungeePlatform(this));
@@ -14,8 +14,6 @@ import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.platform.bossbar.BossBar;
import me.neznamy.tab.shared.proxy.ProxyTabPlayer;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.chat.ComponentSerializer;
@@ -26,8 +24,6 @@ import net.md_5.bungee.protocol.Property;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
/**
* TabPlayer implementation for BungeeCord
*/
@@ -37,9 +33,6 @@ public class BungeeTabPlayer extends ProxyTabPlayer {
/** Flag tracking plugin presence */
private static final boolean premiumVanish = ProxyServer.getInstance().getPluginManager().getPlugin("PremiumVanish") != null;
/** Flag tracking version of bungeecord with packet queue due to configuration phase */
private static final boolean packetQueue = ReflectionUtils.methodExists(UserConnection.class, "sendPacketQueued", DefinedPacket.class);
/** Player's scoreboard */
@NotNull
private final Scoreboard<BungeeTabPlayer> scoreboard = new BungeeScoreboard(this);
@@ -113,6 +106,7 @@ public class BungeeTabPlayer extends ProxyTabPlayer {
@Override
public void sendPluginMessage(byte[] message) {
if (!getPlayer().isConnected()) return;
try {
getPlayer().getServer().sendData(TabConstants.PLUGIN_MESSAGE_CHANNEL_NAME, message);
} catch (NullPointerException BungeeCordBug) {
@@ -169,20 +163,7 @@ public class BungeeTabPlayer extends ProxyTabPlayer {
*/
@SneakyThrows
public void sendPacket(@NotNull DefinedPacket packet) {
if (packetQueue) {
try {
UserConnection.class.getDeclaredMethod("sendPacketQueued", DefinedPacket.class).invoke(player, packet);
} catch (InvocationTargetException BungeeCordBug) { // Since we use reflection we must catch this one
// java.lang.NullPointerException: Cannot invoke "net.md_5.bungee.protocol.MinecraftEncoder.getProtocol()" because the return value of "io.netty.channel.ChannelPipeline.get(java.lang.Class)" is null
// at net.md_5.bungee.netty.ChannelWrapper.getEncodeProtocol(ChannelWrapper.java:51)
// at net.md_5.bungee.UserConnection.sendPacketQueued(UserConnection.java:194)
if (TAB.getInstance().getConfiguration().isDebugMode()) {
TAB.getInstance().getErrorManager().printError("Failed to deliver packet to player " + getName() +
" (online = " + getPlayer().isConnected() + "): " + packet, BungeeCordBug);
}
}
} else {
getPlayer().unsafe().sendPacket(packet);
}
if (!getPlayer().isConnected()) return;
BungeeMultiVersion.sendPacket(this, packet);
}
}
@@ -1,13 +1,13 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import lombok.SneakyThrows;
import me.neznamy.tab.platforms.bungeecord.BungeeMultiVersion;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import me.neznamy.tab.shared.platform.TabList;
import me.neznamy.tab.shared.util.ReflectionUtils;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.protocol.Property;
import net.md_5.bungee.protocol.packet.PlayerListHeaderFooter;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
import net.md_5.bungee.tab.ServerUnique;
import org.jetbrains.annotations.NotNull;
@@ -33,10 +33,7 @@ public abstract class BungeeTabList implements TabList {
@Override
public void setPlayerListHeaderFooter(@NotNull IChatBaseComponent header, @NotNull IChatBaseComponent footer) {
player.sendPacket(new PlayerListHeaderFooter(
header.toString(player.getVersion()),
footer.toString(player.getVersion())
));
player.sendPacket(BungeeMultiVersion.newPlayerListHeaderFooter(header, footer, player.getVersion()));
}
@NotNull
@@ -49,7 +46,9 @@ public abstract class BungeeTabList implements TabList {
@NotNull
public Item entryToItem(Entry entry) {
Item item = item(entry.getUniqueId());
if (entry.getDisplayName() != null) item.setDisplayName(entry.getDisplayName().toString(player.getVersion()));
if (entry.getDisplayName() != null) {
BungeeMultiVersion.setDisplayName(item, entry.getDisplayName(), player.getVersion());
}
item.setGamemode(entry.getGameMode());
item.setListed(true);
item.setPing(entry.getLatency());
@@ -1,5 +1,6 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import me.neznamy.tab.platforms.bungeecord.BungeeMultiVersion;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem.Item;
@@ -32,7 +33,7 @@ public class BungeeTabList1193 extends BungeeTabList {
@Override
public void updateDisplayName(@NotNull UUID entry, @Nullable IChatBaseComponent displayName) {
Item item = item(entry);
item.setDisplayName(displayName == null ? null : displayName.toString(player.getVersion()));
if (displayName != null) BungeeMultiVersion.setDisplayName(item, displayName, player.getVersion());
sendPacket(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME), item);
}
@@ -1,6 +1,8 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import me.neznamy.tab.platforms.bungeecord.BungeeMultiVersion;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem;
import org.jetbrains.annotations.NotNull;
@@ -63,7 +65,8 @@ public class BungeeTabList17 extends BungeeTabList {
public void addEntry(@NotNull Entry entry) {
addUuid(entry.getUniqueId());
String displayNameString = entry.getDisplayName() == null ? String.valueOf(entry.getName()) : entry.getDisplayName().toLegacyText();
if (displayNameString.length() > 16) displayNameString = displayNameString.substring(0, 16); // 16 character limit
if (displayNameString.length() > Limitations.MAX_DISPLAY_NAME_LENGTH_1_7)
displayNameString = displayNameString.substring(0, Limitations.MAX_DISPLAY_NAME_LENGTH_1_7);
update(PlayerListItem.Action.ADD_PLAYER, createItem(entry.getName(), displayNameString, entry.getLatency()));
// Add to map
@@ -81,7 +84,7 @@ public class BungeeTabList17 extends BungeeTabList {
private PlayerListItem.Item createItem(@Nullable String username, @NotNull String displayName, int latency) {
PlayerListItem.Item item = new PlayerListItem.Item();
item.setUsername(username);
item.setDisplayName(displayName);
BungeeMultiVersion.setDisplayName(item, IChatBaseComponent.optimizedComponent(displayName), player.getVersion());
item.setPing(latency);
return item;
}
@@ -1,5 +1,6 @@
package me.neznamy.tab.platforms.bungeecord.tablist;
import me.neznamy.tab.platforms.bungeecord.BungeeMultiVersion;
import me.neznamy.tab.platforms.bungeecord.BungeeTabPlayer;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import net.md_5.bungee.protocol.packet.PlayerListItem;
@@ -28,7 +29,7 @@ public class BungeeTabList18 extends BungeeTabList {
@Override
public void updateDisplayName(@NotNull UUID entry, @Nullable IChatBaseComponent displayName) {
Item item = item(entry);
item.setDisplayName(displayName == null ? null : displayName.toString(player.getVersion()));
if (displayName != null) BungeeMultiVersion.setDisplayName(item, displayName, player.getVersion());
sendPacket(PlayerListItem.Action.UPDATE_DISPLAY_NAME, item);
}
@@ -84,4 +84,15 @@ public record FabricEntityView(FabricTabPlayer player) implements EntityView {
public int[] getDestroyedEntities(@NotNull Object destroyPacket) {
return FabricMultiVersion.getDestroyedEntities(destroyPacket);
}
@Override
public boolean isBundlePacket(@NotNull Object packet) {
return FabricMultiVersion.isBundlePacket(packet);
}
@SuppressWarnings("unchecked")
@Override
public Iterable<Object> getPackets(@NotNull Object bundlePacket) {
return (Iterable<Object>) (Object) FabricMultiVersion.getPackets(bundlePacket);
}
}
@@ -12,7 +12,7 @@ public class FabricEventListener extends EventListener<ServerPlayer> {
public void register() {
ServerPlayConnectionEvents.DISCONNECT.register((connection, $) -> quit(connection.player.getUUID()));
ServerPlayConnectionEvents.JOIN.register((connection, $, $$) -> join(connection.player));
FabricMultiVersion.registerEntityEvents(this::replacePlayer);
FabricMultiVersion.registerEntityEvents(this::replacePlayer, this::worldChange);
//TODO command preprocess
}
@@ -19,6 +19,8 @@ import net.minecraft.server.level.ServerBossEvent;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ServerLevelData;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.scores.Objective;
import net.minecraft.world.scores.PlayerTeam;
@@ -48,6 +50,20 @@ public class FabricMultiVersion {
return 13;
}
}
public static String getWorldName(Level level) {
// 1.15.2-
// return level.getLevelData().getLevelName() + level.dimension.getType().getFileSuffix();
// 1.16+
String path = level.dimension().location().getPath();
String dimensionSuffix = switch (path) {
case "overworld" -> ""; // No suffix for overworld
case "the_nether" -> "_nether";
default -> "_" + path; // End + default behavior for other dimensions created by mods
};
return ((ServerLevelData)level.getLevelData()).getLevelName() + dimensionSuffix;
}
public static void sendSystemMessage(CommandSourceStack source, Component message) {
// 1.19.4-
@@ -73,11 +89,16 @@ public class FabricMultiVersion {
// TODO sneaking
}
public static void registerEntityEvents(BiConsumer<UUID, ServerPlayer> action) {
public static void registerEntityEvents(BiConsumer<UUID, ServerPlayer> respawnAction, BiConsumer<UUID, String> worldChangeAction) {
// 1.16+
net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) ->
action.accept(oldPlayer.getUUID(), newPlayer));
// TODO world switch with proper world name
net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents.AFTER_RESPAWN.register(
(oldPlayer, newPlayer, alive) -> {
respawnAction.accept(newPlayer.getUUID(), newPlayer);
// respawning from death & taking end portal in the end do not call world change event
worldChangeAction.accept(newPlayer.getUUID(), getWorldName(newPlayer.level));
});
net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents.AFTER_PLAYER_CHANGE_WORLD.register(
(player, origin, destination) -> worldChangeAction.accept(player.getUUID(), getWorldName(destination)));
}
public static ClientboundTabListPacket setHeaderFooter(Component header, Component footer) {
@@ -259,6 +280,16 @@ public class FabricMultiVersion {
return new ClientboundSetEntityDataPacket(entityId, (List<SynchedEntityData.DataValue<?>>) data.build());
}
public static boolean isBundlePacket(Object packet) {
// 1.19.4+
return packet instanceof ClientboundBundlePacket;
}
public static Iterable<Packet<ClientGamePacketListener>> getPackets(Object bundlePacket) {
// 1.19.4+
return ((ClientboundBundlePacket)bundlePacket).subPackets();
}
public static ClientboundSetDisplayObjectivePacket newDisplayObjective(int slot, Objective objective) {
// 1.20.1-
// return new ClientboundSetDisplayObjectivePacket(slot, objective);
@@ -29,7 +29,7 @@ public record FabricPlatform(MinecraftServer server) implements BackendPlatform
@Override
public void registerUnknownPlaceholder(@NotNull String identifier) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, -1, () -> identifier);
registerDummyPlaceholder(identifier);
}
@Override
@@ -31,7 +31,7 @@ public class FabricTabPlayer extends BackendTabPlayer {
private final EntityView entityView = new FabricEntityView(this);
public FabricTabPlayer(@NotNull FabricPlatform platform, @NotNull ServerPlayer player) {
super(platform, player, player.getUUID(), player.getGameProfile().getName(), "N/A");
super(platform, player, player.getUUID(), player.getGameProfile().getName(), FabricMultiVersion.getWorldName(player.level));
}
@Override
@@ -69,7 +69,7 @@ public class FeatureManager {
((UnLoadable) f).unload();
TAB.getInstance().debug("Feature " + f.getClass().getSimpleName() + " processed unload in " + (System.currentTimeMillis()-time) + "ms");
}
TAB.getInstance().getPlaceholderManager().getTabExpansion().unregister();
TAB.getInstance().getPlaceholderManager().getTabExpansion().unregisterExpansion();
if (TAB.getInstance().getPlatform() instanceof ProxyPlatform) {
for (TabPlayer player : TAB.getInstance().getOnlinePlayers()) {
((ProxyTabPlayer)player).sendPluginMessage("Unload");
@@ -159,7 +159,7 @@ public class FeatureManager {
((PlayerPlaceholder)TAB.getInstance().getPlaceholderManager().getPlaceholder(TabConstants.Placeholder.WORLD)).updateValue(changed, to);
}
public void onServerChange(@NotNull UUID playerUUID, @NotNull String to, boolean callOnLoginPacket) {
public void onServerChange(@NotNull UUID playerUUID, @NotNull String to) {
TabPlayer changed = TAB.getInstance().getPlayer(playerUUID);
if (changed == null) return;
String from = changed.getServer();
@@ -172,7 +172,6 @@ public class FeatureManager {
TAB.getInstance().getCPUManager().addTime(f, TabConstants.CpuUsageCategory.SERVER_SWITCH, System.nanoTime()-time);
}
((PlayerPlaceholder)TAB.getInstance().getPlaceholderManager().getPlaceholder(TabConstants.Placeholder.SERVER)).updateValue(changed, to);
if (callOnLoginPacket) onLoginPacket(changed);
}
public boolean onCommand(@Nullable TabPlayer sender, @NotNull String command) {
@@ -272,7 +271,7 @@ public class FeatureManager {
}
public void onLoginPacket(TabPlayer packetReceiver) {
packetReceiver.getScoreboard().clearRegisteredObjectives();
packetReceiver.getScoreboard().unfreeze();
for (TabFeature f : values) {
if (!(f instanceof LoginPacketListener)) continue;
long time = System.nanoTime();
@@ -281,6 +280,15 @@ public class FeatureManager {
}
}
public void onTabListClear(TabPlayer packetReceiver) {
for (TabFeature f : values) {
if (!(f instanceof TabListClearListener)) continue;
long time = System.nanoTime();
((TabListClearListener)f).onTabListClear(packetReceiver);
TAB.getInstance().getCPUManager().addTime(f, TabConstants.CpuUsageCategory.TABLIST_CLEAR, System.nanoTime() - time);
}
}
public void registerFeature(@NotNull String featureName, @NotNull TabFeature featureHandler) {
features.put(featureName, featureHandler);
values = features.values().toArray(new TabFeature[0]);
@@ -0,0 +1,27 @@
package me.neznamy.tab.shared;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
/**
* Class containing constants representing value length
* limits on different versions.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Limitations {
/** Player name length limit for scoreboard score on 1.7 and lower */
public static final int SCOREBOARD_SCORE_LENGTH_1_7 = 16;
/** Player name length limit for scoreboard score on 1.8 and higher */
public static final int SCOREBOARD_SCORE_LENGTH_1_8 = 40;
/** Max scoreboard title length for versions below 1.13 */
public static final int SCOREBOARD_TITLE_PRE_1_13 = 32;
/** Max team prefix/suffix length for versions below 1.13 */
public static final int TEAM_PREFIX_SUFFIX_PRE_1_13 = 16;
/** Max tablist entry length for 1.7 and lower */
public static final int MAX_DISPLAY_NAME_LENGTH_1_7 = 16;
}
@@ -181,7 +181,7 @@ public class TAB extends TabAPI {
kill();
return (configuration == null ? "&4Failed to reload, file %file% has broken syntax. Check console for more info."
: configuration.getMessages().getReloadFailBrokenFile()).replace("%file%", brokenFile);
} catch (Exception e) {
} catch (Throwable e) {
errorManager.criticalError("Failed to enable. Did you just invent a new way to break the plugin by misconfiguring it?", e);
kill();
return "&cFailed to enable due to an internal plugin error. Check console for more info.";
@@ -199,7 +199,7 @@ public class TAB extends TabAPI {
if (configuration.getMysql() != null) configuration.getMysql().closeConnection();
featureManager.unload();
platform.logInfo(IChatBaseComponent.fromColoredText("&aDisabled in " + (System.currentTimeMillis()-time) + "ms"));
} catch (Exception | NoClassDefFoundError e) {
} catch (Throwable e) {
errorManager.criticalError("Failed to disable", e);
}
kill();
@@ -53,6 +53,7 @@ public class TabConstants {
public static final String PACKET_ENTITY_DESTROY = "PacketPlayOutEntityDestroy";
public static final String RAW_PACKET_OUT = "Packet reading (out)";
public static final String PACKET_LOGIN = "Login packet";
public static final String TABLIST_CLEAR = "TabList entry re-add";
public static final String PLACEHOLDER_REFRESHING = "Refreshing placeholders";
@@ -37,6 +37,21 @@ public interface BackendPlatform extends Platform {
registry.registerPlaceholders(manager);
}
/**
* Registers a dummy placeholder implementation for specified identifier in case
* no placeholder plugin was found.
*
* @param identifier
* Placeholder identifier to register
*/
default void registerDummyPlaceholder(@NotNull String identifier) {
if (identifier.startsWith("%rel_")) { // To prevent placeholder identifier check from throwing
TAB.getInstance().getPlaceholderManager().registerRelationalPlaceholder(identifier, -1, (viewer, target) -> identifier);
} else {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, -1, () -> identifier);
}
}
/**
* Returns server's TPS for {@link TabConstants.Placeholder#TPS} placeholder
*
@@ -72,4 +72,14 @@ public class DummyEntityView implements EntityView {
public int[] getDestroyedEntities(Object destroyPacket) {
throw new UnsupportedOperationException("Not available on this platform");
}
@Override
public boolean isBundlePacket(Object packet) {
throw new UnsupportedOperationException("Not available on this platform");
}
@Override
public Iterable<Object> getPackets(Object bundlePacket) {
throw new UnsupportedOperationException("Not available on this platform");
}
}
@@ -33,4 +33,8 @@ public interface EntityView {
int getSpawnedPlayer(Object playerSpawnPacket);
int[] getDestroyedEntities(Object destroyPacket);
boolean isBundlePacket(Object packet);
Iterable<Object> getPackets(Object bundlePacket);
}
@@ -163,6 +163,16 @@ public abstract class BackendNameTagX extends NameTagX implements GameModeListen
if (receiver.getVersion().getMinorVersion() < 8) return;
if (!receiver.isLoaded() || getDisableChecker().isDisabledPlayer(receiver) || getUnlimitedDisableChecker().isDisabledPlayer(receiver)) return;
BackendTabPlayer player = (BackendTabPlayer) receiver;
if (player.getEntityView().isBundlePacket(packet)) {
for (Object wrappedPacket : player.getEntityView().getPackets(packet)) {
checkPacket(player, wrappedPacket);
}
} else {
checkPacket(player, packet);
}
}
private void checkPacket(@NotNull BackendTabPlayer player, @NotNull Object packet) {
if (player.getEntityView().isMovePacket(packet) && !player.getEntityView().isLookPacket(packet)) { //ignoring head rotation only packets
packetListener.onEntityMove(player, player.getEntityView().getMoveEntityId(packet));
} else if (player.getEntityView().isTeleportPacket(packet)) {
@@ -43,14 +43,20 @@ public class ChatModifier {
return json;
}
/**
* Returns a String consisting of magic codes (color symbol + character) of
* each magic code used. If none are used, empty String is returned.
*
* @return Magic codes of this modifier as String
*/
@NotNull
public String getMagicCodes() {
StringBuilder builder = new StringBuilder();
if (isBold()) builder.append(EnumChatFormat.BOLD.getFormat());
if (isItalic()) builder.append(EnumChatFormat.ITALIC.getFormat());
if (isUnderlined()) builder.append(EnumChatFormat.UNDERLINE.getFormat());
if (isStrikethrough()) builder.append(EnumChatFormat.STRIKETHROUGH.getFormat());
if (isObfuscated()) builder.append(EnumChatFormat.OBFUSCATED.getFormat());
if (bold) builder.append(EnumChatFormat.BOLD.getFormat());
if (italic) builder.append(EnumChatFormat.ITALIC.getFormat());
if (underlined) builder.append(EnumChatFormat.UNDERLINE.getFormat());
if (strikethrough) builder.append(EnumChatFormat.STRIKETHROUGH.getFormat());
if (obfuscated) builder.append(EnumChatFormat.OBFUSCATED.getFormat());
return builder.toString();
}
}
@@ -232,6 +232,8 @@ public class PlaceholderManagerImpl extends TabFeature implements PlaceholderMan
tabExpansion.setPlaceholderValue(connectedPlayer, p.getIdentifier(), ((ServerPlaceholderImpl) p).getLastValue());
}
}
// Initialize to avoid onVanishStatusChange being called in the loop after joining because previous value was null
((PlayerPlaceholderImpl)registeredPlaceholders.get(TabConstants.Placeholder.VANISHED)).update(connectedPlayer);
}
@Override
@@ -16,7 +16,7 @@ import org.jetbrains.annotations.NotNull;
* Feature handler for global PlayerList feature
*/
public class GlobalPlayerList extends TabFeature implements JoinListener, QuitListener, VanishListener, GameModeListener,
Loadable, UnLoadable, ServerSwitchListener, LoginPacketListener {
Loadable, UnLoadable, ServerSwitchListener, TabListClearListener {
// config options
private final List<String> spyServers = TAB.getInstance().getConfiguration().getConfig().getStringList("global-playerlist.spy-servers", Collections.singletonList("spyserver1"));
@@ -118,8 +118,7 @@ public class GlobalPlayerList extends TabFeature implements JoinListener, QuitLi
}
@Override
public void onLoginPacket(TabPlayer player) {
// Login packet itself does not clear this, but BungeeCord does when the packet is received
public void onTabListClear(TabPlayer player) {
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
// Ignore players on the same server, since the server already sends add packet
if (!all.getServer().equals(player.getServer()) && shouldSee(player, all)) {
@@ -9,6 +9,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.TabConstants;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.platform.TabPlayer;
@@ -113,10 +114,16 @@ public abstract class NettyPipelineInjector extends PipelineInjector {
TAB.getInstance().getCPUManager().addTime("NameTags", TabConstants.CpuUsageCategory.ANTI_OVERRIDE, System.nanoTime()-time);
}
if (isLogin(packet)) {
// Logic must be processed after packet is actually sent
player.getScoreboard().freeze();
super.write(context, packet, channelPromise);
TAB.getInstance().getCPUManager().runTaskLater(200, getFeatureName(), TabConstants.CpuUsageCategory.PACKET_LOGIN,
() -> TAB.getInstance().getFeatureManager().onLoginPacket(player));
TAB.getInstance().getCPUManager().runTaskLater(200, getFeatureName(),
TabConstants.CpuUsageCategory.PACKET_LOGIN, () -> {
TAB.getInstance().getFeatureManager().onLoginPacket(player);
if (player.getVersion().getNetworkId() >= ProtocolVersion.V1_20_2.getNetworkId()) {
// For 1.20.2+ we need to do this, because server switch event is called before tablist is cleared
TAB.getInstance().getFeatureManager().onTabListClear(player);
}
});
return;
}
TAB.getInstance().getFeatureManager().onPacketSend(player, packet);
@@ -24,7 +24,7 @@ import org.jetbrains.annotations.Nullable;
@Getter
public class LayoutManagerImpl extends TabFeature implements LayoutManager, JoinListener, QuitListener, VanishListener, Loadable,
UnLoadable, Refreshable, LoginPacketListener {
UnLoadable, Refreshable, TabListClearListener {
/** Config options */
private final Direction direction = parseDirection(TAB.getInstance().getConfig().getString("layout.direction", "COLUMNS"));
@@ -199,7 +199,7 @@ public class LayoutManagerImpl extends TabFeature implements LayoutManager, Join
}
@Override
public void onLoginPacket(TabPlayer player) {
public void onTabListClear(TabPlayer player) {
LayoutView view = views.get(player);
if (view != null) view.send();
}
@@ -21,7 +21,8 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
public class NameTag extends TabFeature implements NameTagManager, JoinListener, QuitListener,
Loadable, UnLoadable, WorldSwitchListener, ServerSwitchListener, Refreshable, LoginPacketListener {
Loadable, UnLoadable, WorldSwitchListener, ServerSwitchListener, Refreshable, LoginPacketListener,
VanishListener {
@Getter private final String featureName = "NameTags";
@Getter private final String refreshDisplayName = "Updating prefix/suffix";
@@ -77,7 +78,7 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
public void unload() {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
for (TabPlayer target : TAB.getInstance().getOnlinePlayers()) {
if (hasTeamHandlingPaused(target) || disableChecker.isDisabledPlayer(target)) return;
if (hasTeamHandlingPaused(target) || disableChecker.isDisabledPlayer(target)) continue;
viewer.getScoreboard().unregisterTeam(sorting.getShortTeamName(target));
}
}
@@ -121,6 +122,7 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
if (!disableChecker.isDisabledPlayer(disconnectedPlayer) && !hasTeamHandlingPaused(disconnectedPlayer)) {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (viewer == disconnectedPlayer) continue; //player who just disconnected
if (disconnectedPlayer.isVanished() && !viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) continue;
viewer.getScoreboard().unregisterTeam(sorting.getShortTeamName(disconnectedPlayer));
}
}
@@ -227,6 +229,7 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
}
public void updateTeamData(@NonNull TabPlayer p, @NonNull TabPlayer viewer) {
if (p.isVanished() && !viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) return;
boolean visible = getTeamVisibility(p, viewer);
String currentPrefix = p.getProperty(TabConstants.Property.TAGPREFIX).getFormat(viewer);
String currentSuffix = p.getProperty(TabConstants.Property.TAGSUFFIX).getFormat(viewer);
@@ -243,6 +246,7 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
public void unregisterTeam(@NonNull TabPlayer p, @NonNull String teamName) {
if (hasTeamHandlingPaused(p)) return;
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (p.isVanished() && !viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) continue;
viewer.getScoreboard().unregisterTeam(teamName);
}
}
@@ -255,6 +259,7 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
private void registerTeam(@NonNull TabPlayer p, @NonNull TabPlayer viewer) {
if (hasTeamHandlingPaused(p)) return;
if (p.isVanished() && !viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) return;
String replacedPrefix = p.getProperty(TabConstants.Property.TAGPREFIX).getFormat(viewer);
String replacedSuffix = p.getProperty(TabConstants.Property.TAGSUFFIX).getFormat(viewer);
viewer.getScoreboard().registerTeam(
@@ -345,4 +350,16 @@ public class NameTag extends TabFeature implements NameTagManager, JoinListener,
if (!disableChecker.isDisabledPlayer(all) && all.isLoaded()) registerTeam(all, player);
}
}
@Override
public void onVanishStatusChange(@NotNull TabPlayer player) {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (viewer.hasPermission(TabConstants.Permission.SEE_VANISHED)) continue;
if (player.isVanished()) {
viewer.getScoreboard().unregisterTeam(sorting.getShortTeamName(player));
} else {
registerTeam(player, viewer);
}
}
}
}
@@ -29,7 +29,8 @@ import java.util.function.Supplier;
@SuppressWarnings("UnstableApiUsage")
@Getter
public abstract class RedisSupport extends TabFeature implements JoinListener, QuitListener,
DisplayNameListener, Loadable, UnLoadable, ServerSwitchListener, LoginPacketListener {
DisplayNameListener, Loadable, UnLoadable, ServerSwitchListener, LoginPacketListener,
VanishListener, TabListClearListener {
@NotNull private final String featureName = "RedisSupport";
@@ -56,6 +57,7 @@ public abstract class RedisSupport extends TabFeature implements JoinListener, Q
registerMessage("join", PlayerJoin.class, PlayerJoin::new);
registerMessage("quit", PlayerQuit.class, PlayerQuit::new);
registerMessage("server", ServerSwitch.class, ServerSwitch::new);
registerMessage("vanish", UpdateVanishStatus.class, UpdateVanishStatus::new);
}
public void updateTabFormat(@NotNull TabPlayer p, @NotNull String format) {
@@ -212,4 +214,14 @@ public abstract class RedisSupport extends TabFeature implements JoinListener, Q
public void onLoginPacket(TabPlayer player) {
features.forEach(f -> f.onLoginPacket(player));
}
@Override
public void onTabListClear(TabPlayer player) {
features.forEach(f -> f.onTabListClear(player));
}
@Override
public void onVanishStatusChange(@NotNull TabPlayer player) {
sendMessage(new UpdateVanishStatus(player.getUniqueId(), player.isVanished()));
}
}
@@ -23,4 +23,8 @@ public abstract class RedisFeature {
public abstract void read(@NotNull ByteArrayDataInput in, @NotNull RedisPlayer player);
public void onLoginPacket(@NotNull TabPlayer player) {/* Do nothing by default */}
public void onTabListClear(@NotNull TabPlayer player) {/* Do nothing by default */}
public void onVanishStatusChange(@NotNull RedisPlayer player) {/* Do nothing by default */}
}
@@ -89,7 +89,7 @@ public class RedisGlobalPlayerList extends RedisFeature {
}
@Override
public void onLoginPacket(@NotNull TabPlayer player) {
public void onTabListClear(@NotNull TabPlayer player) {
onJoin(player);
}
@@ -104,4 +104,21 @@ public class RedisGlobalPlayerList extends RedisFeature {
redisSupport.getRedisPlayerList() == null ? null :
IChatBaseComponent.optimizedComponent(redisSupport.getRedisPlayerList().getFormat(player)));
}
@Override
public void onVanishStatusChange(@NotNull RedisPlayer player) {
if (player.isVanished()) {
for (TabPlayer all : TAB.getInstance().getOnlinePlayers()) {
if (!shouldSee(all, player)) {
all.getTabList().removeEntry(player.getUniqueId());
}
}
} else {
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (shouldSee(viewer, player)) {
viewer.getTabList().addEntry(getEntry(player));
}
}
}
}
}
@@ -66,6 +66,15 @@ public class RedisPlayerList extends RedisFeature {
return values.get(player);
}
@Override
public void onVanishStatusChange(@NotNull RedisPlayer player) {
if (player.isVanished()) return;
for (TabPlayer viewer : TAB.getInstance().getOnlinePlayers()) {
if (viewer.getVersion().getMinorVersion() < 8) continue;
viewer.getTabList().updateDisplayName(player.getUniqueId(), IChatBaseComponent.optimizedComponent(getFormat(player)));
}
}
@NoArgsConstructor
@AllArgsConstructor
public class Update extends RedisMessage {
@@ -0,0 +1,39 @@
package me.neznamy.tab.shared.features.redis.message;
import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteArrayDataOutput;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import me.neznamy.tab.shared.features.redis.RedisPlayer;
import me.neznamy.tab.shared.features.redis.RedisSupport;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@NoArgsConstructor
@AllArgsConstructor
public class UpdateVanishStatus extends RedisMessage {
private UUID playerId;
private boolean vanished;
@Override
public void write(@NotNull ByteArrayDataOutput out) {
writeUUID(out, playerId);
out.writeBoolean(vanished);
}
@Override
public void read(@NotNull ByteArrayDataInput in) {
playerId = readUUID(in);
vanished = in.readBoolean();
}
@Override
public void process(@NotNull RedisSupport redisSupport) {
RedisPlayer target = redisSupport.getRedisPlayers().get(playerId);
if (target == null) return; // Print warn?
target.setVanished(vanished);
redisSupport.getFeatures().forEach(f -> f.onVanishStatusChange(target));
}
}
@@ -2,6 +2,7 @@ package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.chat.rgb.RGBUtils;
import me.neznamy.tab.shared.platform.Scoreboard;
import me.neznamy.tab.shared.platform.TabPlayer;
@@ -46,7 +47,11 @@ public class LongLine extends ScoreboardLine implements Refreshable {
"", Scoreboard.NameVisibility.ALWAYS, Scoreboard.CollisionRule.ALWAYS, 0);
} else {
removeLine(refreshed, refreshed.getProperty(nameProperty).get());
String[] values = splitText(getPlayerName(lineNumber), RGBUtils.getInstance().convertRGBtoLegacy(refreshed.getProperty(textProperty).get()), refreshed.getVersion().getMinorVersion() >= 8 ? 40 : 16);
String[] values = splitText(
getPlayerName(lineNumber),
RGBUtils.getInstance().convertRGBtoLegacy(refreshed.getProperty(textProperty).get()),
refreshed.getVersion().getMinorVersion() >= 8 ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
);
addLine(refreshed, values[1], values[0], values[2]);
refreshed.setProperty(this, nameProperty, values[1]);
}
@@ -61,7 +66,11 @@ public class LongLine extends ScoreboardLine implements Refreshable {
addLine(p, playerName, value, "");
p.setProperty(this, nameProperty, playerName);
} else {
String[] values = splitText(playerName, RGBUtils.getInstance().convertRGBtoLegacy(value), p.getVersion().getMinorVersion() >= 8 ? 40 : 16);
String[] values = splitText(
playerName,
RGBUtils.getInstance().convertRGBtoLegacy(value),
p.getVersion().getMinorVersion() >= 8 ? Limitations.SCOREBOARD_SCORE_LENGTH_1_8 : Limitations.SCOREBOARD_SCORE_LENGTH_1_7
);
addLine(p, values[1], values[0], values[2]);
p.setProperty(this, nameProperty, values[1]);
}
@@ -2,6 +2,7 @@ package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.Getter;
import lombok.NonNull;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.chat.EnumChatFormat;
import me.neznamy.tab.api.scoreboard.Line;
import me.neznamy.tab.shared.features.types.TabFeature;
@@ -164,7 +165,7 @@ public abstract class ScoreboardLine extends TabFeature implements Line {
* @param text
* text to display
* @param maxNameLength
* maximum length of name field, used values are 16 characters for &lt;1.8 and 40 for 1.8+
* maximum length of name field
* @return Split text as an array of 3 elements
*/
protected String[] splitText(@NonNull String playerNameStart, @NonNull String text, int maxNameLength) {
@@ -176,7 +177,7 @@ public abstract class ScoreboardLine extends TabFeature implements Line {
nameValue = playerNameStart + text;
suffixValue = "";
} else {
String[] prefixOther = split(text, 16);
String[] prefixOther = split(text, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13);
prefixValue = prefixOther[0];
String other = prefixOther[1];
if (playerNameStart.length() > 0) {
@@ -1,6 +1,7 @@
package me.neznamy.tab.shared.features.scoreboard.lines;
import lombok.NonNull;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.platform.TabPlayer;
import me.neznamy.tab.shared.chat.EnumChatFormat;
import me.neznamy.tab.shared.chat.rgb.RGBUtils;
@@ -15,12 +16,12 @@ import me.neznamy.tab.shared.features.scoreboard.ScoreboardImpl;
*/
public class StaticLine extends ScoreboardLine {
//values for 1.7 clients with 16-character limit for player name
//values for 1.7 clients
protected String prefix17;
protected String name17;
protected String suffix17;
//values for 1.8-1.12 clients with 40-character limit for player name
//values for 1.8-1.12 clients
protected String prefix;
protected String name;
protected String suffix;
@@ -35,12 +36,12 @@ public class StaticLine extends ScoreboardLine {
super.text = text;
String legacy = RGBUtils.getInstance().convertRGBtoLegacy(this.text);
//1.8+
String[] v18 = splitText(getPlayerName(lineNumber), legacy, 40);
String[] v18 = splitText(getPlayerName(lineNumber), legacy, Limitations.SCOREBOARD_SCORE_LENGTH_1_8);
prefix = v18[0];
name = v18[1];
suffix = v18[2];
//1.7-
String[] v17 = splitText(getPlayerName(lineNumber), legacy, 16);
String[] v17 = splitText(getPlayerName(lineNumber), legacy, Limitations.SCOREBOARD_SCORE_LENGTH_1_7);
prefix17 = v17[0];
name17 = v17[1];
suffix17 = v17[2];
@@ -0,0 +1,19 @@
package me.neznamy.tab.shared.features.types;
import me.neznamy.tab.shared.platform.TabPlayer;
/**
* Interface for features listening to tablist entry clear
* on server switch to re-add entries.
*/
public interface TabListClearListener {
/**
* Called after player's TabList is cleared by the proxy
* on server switch.
*
* @param player
* Player who got their TabList entries cleared
*/
void onTabListClear(TabPlayer player);
}
@@ -3,6 +3,7 @@ package me.neznamy.tab.shared.hook;
import me.neznamy.tab.shared.ProtocolVersion;
import me.neznamy.tab.shared.chat.ChatModifier;
import me.neznamy.tab.shared.chat.IChatBaseComponent;
import me.neznamy.tab.shared.util.ComponentCache;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.TextColor;
@@ -15,6 +16,10 @@ import java.util.stream.Collectors;
public class AdventureHook {
/** Component cache for adventure components */
private static final ComponentCache<IChatBaseComponent, Component> cache =
new ComponentCache<>(1000, AdventureHook::toAdventureComponent0);
/**
* Converts component to adventure component
*
@@ -26,8 +31,22 @@ public class AdventureHook {
*/
@NotNull
public static Component toAdventureComponent(@NotNull IChatBaseComponent component, @NotNull ProtocolVersion clientVersion) {
return cache.get(component, clientVersion);
}
/**
* Converts component to adventure component
*
* @param component
* Component to convert
* @param clientVersion
* Version to create component for
* @return Adventure component from this component.
*/
@NotNull
private static Component toAdventureComponent0(@NotNull IChatBaseComponent component, @NotNull ProtocolVersion clientVersion) {
ChatModifier modifier = component.getModifier();
net.kyori.adventure.text.format.TextColor color = null;
TextColor color = null;
if (modifier.getColor() != null) {
if (clientVersion.getMinorVersion() >= 16) {
color = TextColor.color(modifier.getColor().getRgb());
@@ -41,9 +60,14 @@ public class AdventureHook {
if (modifier.isObfuscated()) decorations.add(TextDecoration.OBFUSCATED);
if (modifier.isStrikethrough()) decorations.add(TextDecoration.STRIKETHROUGH);
if (modifier.isUnderlined()) decorations.add(TextDecoration.UNDERLINED);
Key font = modifier.getFont() == null ? null : Key.key(modifier.getFont());
return Component.text(component.getText(), color, decorations)
.font(font)
.children(component.getExtra().stream().map(c -> toAdventureComponent(c, clientVersion)).collect(Collectors.toList()));
Component adventureComponent = Component.text(component.getText(), color, decorations);
if (modifier.getFont() != null) {
adventureComponent = adventureComponent.font(Key.key(modifier.getFont()));
}
if (!component.getExtra().isEmpty()) {
adventureComponent = adventureComponent.children(component.getExtra().stream().map(
c -> toAdventureComponent(c, clientVersion)).collect(Collectors.toList()));
}
return adventureComponent;
}
}
@@ -129,11 +129,12 @@ public class RelationalPlaceholderImpl extends TabPlaceholder implements Relatio
* @return last known value for entered player duo
*/
public String getLastValue(@NonNull TabPlayer viewer, @NonNull TabPlayer target) {
if (!lastValues.computeIfAbsent(viewer, v -> new WeakHashMap<>()).containsKey(target)) {
lastValues.get(viewer).put(target, getReplacements().findReplacement(identifier));
WeakHashMap<me.neznamy.tab.api.TabPlayer, String> viewerMap = lastValues.computeIfAbsent(viewer, v -> new WeakHashMap<>());
if (!viewerMap.containsKey(target)) {
viewerMap.put(target, getReplacements().findReplacement(identifier));
update(viewer, target);
}
return setPlaceholders(EnumChatFormat.color(lastValues.computeIfAbsent(viewer, v -> new WeakHashMap<>()).get(target)), target);
return setPlaceholders(EnumChatFormat.color(viewerMap.get(target)), target);
}
@Override
@@ -12,5 +12,5 @@ public class EmptyTabExpansion implements TabExpansion {
public void setValue(@NonNull TabPlayer player, @NonNull String key, @NonNull String value) {/*Do nothing*/}
@Override
public boolean unregister() { return false; }
public void unregisterExpansion() {/* Do nothing */}
}
@@ -39,5 +39,5 @@ public interface TabExpansion {
void setValue(@NotNull TabPlayer player, @NotNull String key, @NotNull String value);
boolean unregister();
void unregisterExpansion();
}
@@ -50,29 +50,6 @@ public abstract class EventListener<T> {
TAB.getInstance().getFeatureManager().onWorldChange(player, world));
}
/**
* Processes server change by forwarding it to all features.
*
* @param player
* Player who switched server or joined
* @param uuid
* UUID of the player
* @param server
* New server
* @param callOnLoginPacket
* Whether onLoginPacket method should be called as well or not
*/
public void serverChange(@NotNull T player, @NotNull UUID uuid, @NotNull String server, boolean callOnLoginPacket) {
if (TAB.getInstance().isPluginDisabled()) return;
TAB.getInstance().getCPUManager().runTask(() -> {
if (TAB.getInstance().getPlayer(uuid) == null) {
TAB.getInstance().getFeatureManager().onJoin(createPlayer(player));
} else {
TAB.getInstance().getFeatureManager().onServerChange(uuid, server, callOnLoginPacket);
}
});
}
/**
* Processes plugin message.
*
@@ -2,6 +2,7 @@ package me.neznamy.tab.shared.platform;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import me.neznamy.tab.shared.Limitations;
import me.neznamy.tab.shared.TAB;
import me.neznamy.tab.shared.chat.EnumChatFormat;
import me.neznamy.tab.shared.chat.rgb.RGBUtils;
@@ -23,7 +24,11 @@ public abstract class Scoreboard<T extends TabPlayer> {
/** Scoreboard objectives player has registered */
private final Set<String> registeredObjectives = new HashSet<>();
/** Flag tracking time between Login packet send and its processing */
private boolean frozen;
public void setScore(@NotNull String objective, @NotNull String playerName, int score) {
if (frozen) return;
if (!registeredObjectives.contains(objective)) {
error("Tried to update score (%s) without the existence of its requested objective '%s' to player ", playerName, objective);
return;
@@ -32,6 +37,7 @@ public abstract class Scoreboard<T extends TabPlayer> {
}
public void removeScore(@NotNull String objective, @NotNull String playerName) {
if (frozen) return;
if (!registeredObjectives.contains(objective)) {
error("Tried to remove score (%s) without the existence of its requested objective '%s' to player ", playerName, objective);
return;
@@ -40,14 +46,16 @@ public abstract class Scoreboard<T extends TabPlayer> {
}
public void registerObjective(@NotNull String objectiveName, @NotNull String title, @NotNull HealthDisplay display) {
if (frozen) return;
if (!registeredObjectives.add(objectiveName)) {
error("Tried to register duplicated objective %s to player ", objectiveName);
return;
}
registerObjective0(objectiveName, cutTo(title, 32), display);
registerObjective0(objectiveName, cutTo(title, Limitations.SCOREBOARD_TITLE_PRE_1_13), display);
}
public void unregisterObjective(@NotNull String objectiveName) {
if (frozen) return;
if (!registeredObjectives.remove(objectiveName)) {
error("Tried to unregister non-existing objective %s for player ", objectiveName);
return;
@@ -56,23 +64,34 @@ public abstract class Scoreboard<T extends TabPlayer> {
}
public void updateObjective(@NotNull String objectiveName, @NotNull String title, @NotNull HealthDisplay display) {
if (frozen) return;
if (!registeredObjectives.contains(objectiveName)) {
error("Tried to modify non-existing objective %s for player ", objectiveName);
return;
}
updateObjective0(objectiveName, cutTo(title, 32), display);
updateObjective0(objectiveName, cutTo(title, Limitations.SCOREBOARD_TITLE_PRE_1_13), display);
}
public void registerTeam(@NotNull String name, @NotNull String prefix, @NotNull String suffix, @NotNull NameVisibility visibility,
@NotNull CollisionRule collision, @NotNull Collection<String> players, int options) {
if (frozen) return;
if (!registeredTeams.add(name)) {
error("Tried to register duplicated team %s to player ", name);
return;
}
registerTeam0(name, cutTo(prefix, 16), cutTo(suffix, 16), visibility, collision, players, options);
registerTeam0(
name,
cutTo(prefix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
cutTo(suffix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
visibility,
collision,
players,
options
);
}
public void unregisterTeam(@NotNull String name) {
if (frozen) return;
if (!registeredTeams.remove(name)) {
error("Tried to unregister non-existing team %s for player ", name);
return;
@@ -82,11 +101,19 @@ public abstract class Scoreboard<T extends TabPlayer> {
public void updateTeam(@NotNull String name, @NotNull String prefix, @NotNull String suffix, @NotNull NameVisibility visibility,
@NotNull CollisionRule collision, int options) {
if (frozen) return;
if (!registeredTeams.contains(name)) {
error("Tried to modify non-existing team %s for player ", name);
return;
}
updateTeam0(name, cutTo(prefix, 16), cutTo(suffix, 16), visibility, collision, options);
updateTeam0(
name,
cutTo(prefix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
cutTo(suffix, Limitations.TEAM_PREFIX_SUFFIX_PRE_1_13),
visibility,
collision,
options
);
}
private void error(@NotNull String format, @NotNull Object... args) {
@@ -94,11 +121,19 @@ public abstract class Scoreboard<T extends TabPlayer> {
}
/**
* Clears maps of registered teams and objectives on server switch, as proxy sends Login packet
* Marks for freeze.
*/
public void clearRegisteredObjectives() {
public void freeze() {
frozen = true;
}
/**
* Clears frozen flag and clears maps of registered teams and objectives.
*/
public void unfreeze() {
registeredTeams.clear();
registeredObjectives.clear();
frozen = false;
}
/**
@@ -70,9 +70,13 @@ public class PluginMessageHandler {
}
public void vanished(@NotNull ProxyTabPlayer player, @NotNull ByteArrayDataInput in) {
player.setVanished(in.readBoolean());
TAB.getInstance().getFeatureManager().onVanishStatusChange(player);
((PlayerPlaceholderImpl) TAB.getInstance().getPlaceholderManager().getPlaceholder(TabConstants.Placeholder.VANISHED)).updateValue(player, player.isVanished());
boolean oldVanish = player.isVanished();
boolean newVanish = in.readBoolean();
if (oldVanish != newVanish) {
player.setVanished(newVanish);
TAB.getInstance().getFeatureManager().onVanishStatusChange(player);
((PlayerPlaceholderImpl) TAB.getInstance().getPlaceholderManager().getPlaceholder(TabConstants.Placeholder.VANISHED)).updateValue(player, player.isVanished());
}
}
public void disguised(@NotNull ProxyTabPlayer player, @NotNull ByteArrayDataInput in) {
@@ -105,7 +109,10 @@ public class PluginMessageHandler {
if (TAB.getInstance().getGroupManager().getPermissionPlugin().contains("Vault") &&
!TAB.getInstance().getGroupManager().isGroupsByPermissions()) player.setGroup(in.readUTF());
// reset attributes from previous server to default false values, new server will send separate update packets if needed
player.setVanished(false);
if (player.vanished) { // Only trigger if bridge says player is vanished, do not trigger on proxy vanish
player.vanished = false;
TAB.getInstance().getFeatureManager().onVanishStatusChange(player);
}
player.setDisguised(false);
player.setInvisibilityPotion(false);
int placeholderCount = in.readInt();
@@ -20,9 +20,8 @@ public class ProxyTabExpansion implements TabExpansion {
}
@Override
public boolean unregister() {
public void unregisterExpansion() {
// Don't do anything on proxy side
return false;
}
public void resendAllValues(@NotNull TabPlayer player) {
@@ -26,7 +26,7 @@ import java.util.UUID;
public abstract class ProxyTabPlayer extends TabPlayer {
/** Player's vanish status from backend server */
private boolean vanished;
public boolean vanished;
/** Player's disguise status from backend server */
private boolean disguised;
@@ -34,7 +34,7 @@ public class SpongePlatform implements BackendPlatform {
@Override
public void registerUnknownPlaceholder(@NotNull String identifier) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, -1, () -> identifier);
registerDummyPlaceholder(identifier);
}
@Override
@@ -34,7 +34,7 @@ public class SpongePlatform implements BackendPlatform {
@Override
public void registerUnknownPlaceholder(@NotNull String identifier) {
TAB.getInstance().getPlaceholderManager().registerServerPlaceholder(identifier, -1, () -> identifier);
registerDummyPlaceholder(identifier);
}
@Override
@@ -28,11 +28,21 @@ public class VelocityEventListener extends EventListener<Player> {
@Subscribe
public void onConnect(@NotNull ServerPostConnectEvent e) {
serverChange(
e.getPlayer(), e.getPlayer().getUniqueId(),
e.getPlayer().getCurrentServer().map(s -> s.getServerInfo().getName()).orElse("null"),
true
);
TAB tab = TAB.getInstance();
if (tab.isPluginDisabled()) return;
tab.getCPUManager().runTask(() -> {
TabPlayer player = tab.getPlayer(e.getPlayer().getUniqueId());
if (player == null) {
tab.getFeatureManager().onJoin(createPlayer(e.getPlayer()));
} else {
tab.getFeatureManager().onServerChange(
player.getUniqueId(),
e.getPlayer().getCurrentServer().map(s -> s.getServerInfo().getName()).orElse("null")
);
tab.getFeatureManager().onTabListClear(player);
tab.getFeatureManager().onLoginPacket(player);
}
});
}
@Subscribe
@@ -62,6 +62,7 @@ public class VelocityTAB {
*/
@Subscribe
public void onProxyShutdown(@Nullable ProxyShutdownEvent event) {
TAB.getInstance().unload();
// Fix race condition as Velocity calls DisconnectEvent for each player before this event
TAB.getInstance().getCPUManager().runTask(() -> TAB.getInstance().unload());
}
}
@@ -63,7 +63,6 @@ public class VelocityTabList implements TabList {
@Override
public void addEntry(@NotNull Entry entry) {
if (player.getPlayer().getTabList().containsEntry(entry.getUniqueId())) return;
Component displayName = entry.getDisplayName() == null ? null : AdventureHook.toAdventureComponent(entry.getDisplayName(), player.getVersion());
TabListEntry e = TabListEntry.builder()
.tabList(player.getPlayer().getTabList())
@@ -77,8 +76,17 @@ public class VelocityTabList implements TabList {
.gameMode(entry.getGameMode())
.displayName(displayName)
.build();
player.getPlayer().getTabList().addEntry(e);
expectedDisplayNames.put(e, displayName);
// Remove entry because:
// #1 - If player is 1.8 - 1.19.2, KeyedVelocityTabList#addEntry will throw IllegalArgumentException
// if the entry is already present (most likely due to an accident trying to add existing player in global playerlist)
// #2 - If player is 1.20.2+, tablist is cleared by the client itself without requirement to remove
// manually by the proxy, however velocity's tablist entry tracker still thinks they are present
// and therefore will refuse to add them
removeEntry(entry.getUniqueId());
player.getPlayer().getTabList().addEntry(e);
}
@Override