diff --git a/.gitea/workflows/build-plugin.yml b/.gitea/workflows/build-plugin.yml index d8cfbd5..0877036 100644 --- a/.gitea/workflows/build-plugin.yml +++ b/.gitea/workflows/build-plugin.yml @@ -33,7 +33,7 @@ jobs: run: | latest="$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)" if [[ -z "$latest" ]]; then - version="1.0.0" + version="1.0.3" else current="${latest#v}" IFS='.' read -r major minor patch <<< "$current" diff --git a/.gitignore b/.gitignore index 394a356..72d0a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ *.class *.log +/LOCAL_AI_NOTES.md +AsminVoiceChat-*.jar diff --git a/AsminVoiceChat-1.0.0.jar b/AsminVoiceChat-1.0.0.jar deleted file mode 100644 index a3971f9..0000000 Binary files a/AsminVoiceChat-1.0.0.jar and /dev/null differ diff --git a/README.md b/README.md index b1397f6..efe9559 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/build.gradle b/build.gradle index c4162a4..ce28bd0 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'de.maxhenkel.voicechat' -version = findProperty('buildVersion') ?: '1.0.0' +version = findProperty('buildVersion') ?: '1.0.3' java { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/plugin.yml b/plugin.yml index 5803eba..fb85163 100644 --- a/plugin.yml +++ b/plugin.yml @@ -1,5 +1,5 @@ name: AsminVoicechat -version: 1.0.0 +version: 1.0.3 main: de.maxhenkel.voicechat.Voicechat api-version: '1.13' prefix: AsminVoicechat diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/command/VoiceChatCommands.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/command/VoiceChatCommands.java index 086a3d3..a671856 100644 --- a/source/bukkit/src/main/java/de/maxhenkel/voicechat/command/VoiceChatCommands.java +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/command/VoiceChatCommands.java @@ -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 @@ -326,4 +330,4 @@ public class VoiceChatCommands implements CommandExecutor, TabCompleter { return null; } } -} \ No newline at end of file +} diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/config/ServerConfig.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/config/ServerConfig.java index a0c97b7..31c0054 100644 --- a/source/bukkit/src/main/java/de/maxhenkel/voicechat/config/ServerConfig.java +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/config/ServerConfig.java @@ -2,7 +2,6 @@ package de.maxhenkel.voicechat.config; import de.maxhenkel.configbuilder.ConfigBuilder; import de.maxhenkel.configbuilder.entry.ConfigEntry; -import de.maxhenkel.voicechat.BuildConstants; import de.maxhenkel.voicechat.Voicechat; import de.maxhenkel.voicechat.voice.common.AudioUtils; @@ -28,7 +27,7 @@ public class ServerConfig { public ConfigEntry threadedServerSupport; public ServerConfig(ConfigBuilder builder) { - builder.header(String.format("%s server config v%s", BuildConstants.PLUGIN_NAME, Voicechat.INSTANCE.getDescription().getVersion())); + builder.header("AsminVoiceChat (Simple Voice Chat) server config v1.0.2"); voiceChatPort = builder .integerEntry("port", 24454, -1, 65535, @@ -126,4 +125,4 @@ public class ServerConfig { VOIP, AUDIO, RESTRICTED_LOWDELAY; } -} \ No newline at end of file +} diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/NetManager.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/NetManager.java index da80ef5..c48cd89 100644 --- a/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/NetManager.java +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/NetManager.java @@ -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 > void registerOutgoingPacket(Class 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); } @@ -129,4 +140,4 @@ public class NetManager implements Listener { Voicechat.compatibility.sendStatusMessage(player, key, args); } -} \ No newline at end of file +} diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/ProtocolChannelCompatibility.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/ProtocolChannelCompatibility.java new file mode 100644 index 0000000..a14617c --- /dev/null +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/net/ProtocolChannelCompatibility.java @@ -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; + } + } +} diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/PlayerStateManager.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/PlayerStateManager.java index 2cc4d1f..04098c4 100644 --- a/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/PlayerStateManager.java +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/PlayerStateManager.java @@ -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 stateList = new ArrayList<>(states.size()); for (PlayerState state : states.values()) { diff --git a/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.java b/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.java index 44bd8da..a690da9 100644 --- a/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.java +++ b/source/bukkit/src/main/java/de/maxhenkel/voicechat/voice/server/ServerVoiceEvents.java @@ -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(), diff --git a/tests/ProtocolCryptoSelfTest.java b/tests/ProtocolCryptoSelfTest.java index ea68a28..78bccd4 100644 --- a/tests/ProtocolCryptoSelfTest.java +++ b/tests/ProtocolCryptoSelfTest.java @@ -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) {