Fix missing group members on 2.5 clients
Build AsminVoicechat / build-and-release (push) Successful in 2m27s

This commit is contained in:
asminme committed 2026-10-24 21:15:00 +02:00
1 parent 578e01eda3
commit 7a91c8a41d
7 files changed
+84 -8

No files matched your search

+6 -2
View File
@@ -10,8 +10,12 @@ The plugin is compiled to Java 8 bytecode and runs on newer JVMs.
The server negotiates the protocol generation during the existing voice-chat
handshake and stores it with that player's 16-byte secret. The UDP path then
uses AES-CBC for 2.5 clients and AES-GCM for 2.6 clients. Shared packet layouts
are passed through the existing 2.6 voice routing. The fork changes only the
per-client cipher selection and compatibility-version acceptance.
are passed through the existing 2.6 voice routing. State messages are sent on
the channel names expected by each client generation, so 2.5 clients receive
their player and group-member updates as well. For compatibility 18 the server
maps `voicechat:state` and `voicechat:states` to the older
`voicechat:player_state` and `voicechat:player_states` channels. When a player
leaves, it sends the disconnected state because 2.5 has no remove-state packet.
The Bukkit plugin source and API source are in `source/bukkit/` and
`source/api/`. The cipher and compatibility changes are in the regular source
@@ -112,6 +112,8 @@ public class VoiceChatCommands implements CommandExecutor, TabCompleter {
return true;
}
int clientCompatibilityVersion = Voicechat.SERVER.getClientCompatibilityVersion(player.getUniqueId());
ClientConnection clientConnection = Voicechat.SERVER.getServer().getConnections().get(player.getUniqueId());
if (clientConnection == null) {
@@ -130,6 +132,8 @@ public class VoiceChatCommands implements CommandExecutor, TabCompleter {
} else {
NetManager.sendMessage(commandSender, "message.voicechat.ping_received_attempt", String.valueOf(pingMilliseconds), String.valueOf(attempts));
}
commandSender.sendMessage("Voice chat connection verified (client compatibility version "
+ clientCompatibilityVersion + ").");
}
@Override
@@ -38,6 +38,9 @@ public class NetManager implements Listener {
registerOutgoingPacket(JoinedGroupPacket.class);
registerOutgoingPacket(AddCategoryPacket.class);
registerOutgoingPacket(RemoveCategoryPacket.class);
registerOutgoingChannel(ProtocolChannelCompatibility.LEGACY_PLAYER_STATE);
registerOutgoingChannel(ProtocolChannelCompatibility.LEGACY_PLAYER_STATES);
} catch (Exception e) {
throw new IllegalStateException("Failed to register voice chat packets", e);
}
@@ -100,6 +103,10 @@ public class NetManager implements Listener {
private <T extends Packet<?>> void registerOutgoingPacket(Class<T> packetClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String id = packetClass.getDeclaredConstructor().newInstance().getID().toString();
registerOutgoingChannel(id);
}
private void registerOutgoingChannel(String id) {
packets.add(id);
Bukkit.getMessenger().registerOutgoingPluginChannel(Voicechat.INSTANCE, id);
}
@@ -114,7 +121,11 @@ public class NetManager implements Listener {
p.toBytes(buf);
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
player.sendPluginMessage(Voicechat.INSTANCE, p.getID().toString(), bytes);
int compatibilityVersion = Voicechat.SERVER.getClientCompatibilityVersion(player.getUniqueId());
String channel = ProtocolChannelCompatibility.outgoingChannel(p.getID().toString(), compatibilityVersion);
if (channel != null) {
player.sendPluginMessage(Voicechat.INSTANCE, channel, bytes);
}
} catch (Exception e) {
Voicechat.LOGGER.error("Failed to send packet to '{}'", player.getName(), e);
}
@@ -0,0 +1,27 @@
package de.maxhenkel.voicechat.net;
public final class ProtocolChannelCompatibility {
public static final int LEGACY_COMPATIBILITY_VERSION = 18;
public static final String LEGACY_PLAYER_STATE = "voicechat:player_state";
public static final String LEGACY_PLAYER_STATES = "voicechat:player_states";
private ProtocolChannelCompatibility() {
}
public static String outgoingChannel(String channel, int compatibilityVersion) {
if (compatibilityVersion != LEGACY_COMPATIBILITY_VERSION) {
return channel;
}
switch (channel) {
case "voicechat:state":
return LEGACY_PLAYER_STATE;
case "voicechat:states":
return LEGACY_PLAYER_STATES;
case "voicechat:remove_state":
return null;
default:
return channel;
}
}
}
@@ -56,8 +56,7 @@ public class PlayerStateManager implements Listener {
}
public void onPlayerHide(PlayerHideEvent event) {
RemovePlayerStatePacket packet = new RemovePlayerStatePacket(event.getHiddenPlayer().getUniqueId());
NetManager.sendToClient(event.getObserver(), packet);
sendRemoveState(event.getObserver(), event.getHiddenPlayer());
Voicechat.LOGGER.debug("Removing state of {} for {}", event.getHiddenPlayer().getName(), event.getObserver().getName());
}
@@ -83,14 +82,22 @@ public class PlayerStateManager implements Listener {
}
public void broadcastRemoveState(Player stateOwner) {
RemovePlayerStatePacket packet = new RemovePlayerStatePacket(stateOwner.getUniqueId());
for (Player receiver : Voicechat.INSTANCE.getServer().getOnlinePlayers()) {
NetManager.sendToClient(receiver, packet);
sendRemoveState(receiver, stateOwner);
}
// Send the default disconnected state to the API when disconnecting
PluginManager.instance().onPlayerStateChanged(defaultDisconnectedState(stateOwner));
}
private void sendRemoveState(Player receiver, Player stateOwner) {
if (Voicechat.SERVER.getClientCompatibilityVersion(receiver.getUniqueId())
== de.maxhenkel.voicechat.net.ProtocolChannelCompatibility.LEGACY_COMPATIBILITY_VERSION) {
NetManager.sendToClient(receiver, new PlayerStatePacket(defaultDisconnectedState(stateOwner)));
return;
}
NetManager.sendToClient(receiver, new RemovePlayerStatePacket(stateOwner.getUniqueId()));
}
public void onPlayerCompatibilityCheckSucceeded(Player player) {
List<PlayerState> stateList = new ArrayList<>(states.size());
for (PlayerState state : states.values()) {
@@ -83,6 +83,10 @@ public class ServerVoiceEvents implements Listener {
return isSupportedCompatibilityVersion(clientCompatibilities.getOrDefault(playerUuid, -1));
}
public int getClientCompatibilityVersion(UUID playerUuid) {
return clientCompatibilities.getOrDefault(playerUuid, -1);
}
public static void sendIncompatibleMessage(Player player, int clientCompatibilityVersion) {
if (clientCompatibilityVersion <= 6) {
player.sendMessage(String.format(Voicechat.TRANSLATIONS.voicechatNotCompatibleMessage.get(),
+20 -1
View File
@@ -1,4 +1,5 @@
import de.maxhenkel.voicechat.voice.common.Secret;
import de.maxhenkel.voicechat.net.ProtocolChannelCompatibility;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
@@ -9,6 +10,8 @@ import javax.crypto.spec.SecretKeySpec;
public class ProtocolCryptoSelfTest {
public static void main(String[] args) throws Exception {
testStateChannelCompatibility();
byte[] key = new byte[16];
for (int i = 0; i < key.length; i++) key[i] = (byte) (i * 13 + 7);
UUID legacyKey = uuidFromBytes(key);
@@ -34,7 +37,23 @@ public class ProtocolCryptoSelfTest {
throw new AssertionError("Hybrid server AES-GCM round-trip failed");
}
}
System.out.println("PASS: 100 CBC client/server interop cycles and 100 GCM round-trips");
System.out.println("PASS: legacy state-channel mapping, 100 CBC interop cycles and 100 GCM round-trips");
}
private static void testStateChannelCompatibility() {
check("voicechat:player_state", ProtocolChannelCompatibility.outgoingChannel("voicechat:state", 18));
check("voicechat:player_states", ProtocolChannelCompatibility.outgoingChannel("voicechat:states", 18));
if (ProtocolChannelCompatibility.outgoingChannel("voicechat:remove_state", 18) != null) {
throw new AssertionError("The 2.5 client has no remove-state packet");
}
check("voicechat:state", ProtocolChannelCompatibility.outgoingChannel("voicechat:state", 20));
check("voicechat:states", ProtocolChannelCompatibility.outgoingChannel("voicechat:states", 20));
}
private static void check(String expected, String actual) {
if (!expected.equals(actual)) {
throw new AssertionError("Expected channel " + expected + ", got " + actual);
}
}
private static UUID uuidFromBytes(byte[] bytes) {