Fix language and update group members on 2.5 clients
Build AsminVoicechat / build-and-release (push) Successful in 2m24s

This commit is contained in:
asminme committed 2026-10-24 21:59:10 +02:00
1 parent 71396a86a0
commit f5429ee4c6
13 files changed
+90 -13

No files matched your search

+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
run: | run: |
latest="$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)" latest="$(git tag --list 'v[0-9]*' --sort=-version:refname | head -n 1)"
if [[ -z "$latest" ]]; then if [[ -z "$latest" ]]; then
version="1.0.0" version="1.0.3"
else else
current="${latest#v}" current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current" IFS='.' read -r major minor patch <<< "$current"
+2
View File
@@ -2,3 +2,5 @@
build/ build/
*.class *.class
*.log *.log
/LOCAL_AI_NOTES.md
AsminVoiceChat-*.jar
Binary file not shown.
+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 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 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 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 are passed through the existing 2.6 voice routing. State messages are sent on
per-client cipher selection and compatibility-version acceptance. 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 The Bukkit plugin source and API source are in `source/bukkit/` and
`source/api/`. The cipher and compatibility changes are in the regular source `source/api/`. The cipher and compatibility changes are in the regular source
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
} }
group = 'de.maxhenkel.voicechat' group = 'de.maxhenkel.voicechat'
version = findProperty('buildVersion') ?: '1.0.0' version = findProperty('buildVersion') ?: '1.0.3'
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
+1 -1
View File
@@ -1,5 +1,5 @@
name: AsminVoicechat name: AsminVoicechat
version: 1.0.0 version: 1.0.3
main: de.maxhenkel.voicechat.Voicechat main: de.maxhenkel.voicechat.Voicechat
api-version: '1.13' api-version: '1.13'
prefix: AsminVoicechat prefix: AsminVoicechat
@@ -112,6 +112,8 @@ public class VoiceChatCommands implements CommandExecutor, TabCompleter {
return true; return true;
} }
int clientCompatibilityVersion = Voicechat.SERVER.getClientCompatibilityVersion(player.getUniqueId());
ClientConnection clientConnection = Voicechat.SERVER.getServer().getConnections().get(player.getUniqueId()); ClientConnection clientConnection = Voicechat.SERVER.getServer().getConnections().get(player.getUniqueId());
if (clientConnection == null) { if (clientConnection == null) {
@@ -130,6 +132,8 @@ public class VoiceChatCommands implements CommandExecutor, TabCompleter {
} else { } else {
NetManager.sendMessage(commandSender, "message.voicechat.ping_received_attempt", String.valueOf(pingMilliseconds), String.valueOf(attempts)); 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 @Override
@@ -2,7 +2,6 @@ package de.maxhenkel.voicechat.config;
import de.maxhenkel.configbuilder.ConfigBuilder; import de.maxhenkel.configbuilder.ConfigBuilder;
import de.maxhenkel.configbuilder.entry.ConfigEntry; import de.maxhenkel.configbuilder.entry.ConfigEntry;
import de.maxhenkel.voicechat.BuildConstants;
import de.maxhenkel.voicechat.Voicechat; import de.maxhenkel.voicechat.Voicechat;
import de.maxhenkel.voicechat.voice.common.AudioUtils; import de.maxhenkel.voicechat.voice.common.AudioUtils;
@@ -28,7 +27,7 @@ public class ServerConfig {
public ConfigEntry<Boolean> threadedServerSupport; public ConfigEntry<Boolean> threadedServerSupport;
public ServerConfig(ConfigBuilder builder) { 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 voiceChatPort = builder
.integerEntry("port", 24454, -1, 65535, .integerEntry("port", 24454, -1, 65535,
@@ -38,6 +38,9 @@ public class NetManager implements Listener {
registerOutgoingPacket(JoinedGroupPacket.class); registerOutgoingPacket(JoinedGroupPacket.class);
registerOutgoingPacket(AddCategoryPacket.class); registerOutgoingPacket(AddCategoryPacket.class);
registerOutgoingPacket(RemoveCategoryPacket.class); registerOutgoingPacket(RemoveCategoryPacket.class);
registerOutgoingChannel(ProtocolChannelCompatibility.LEGACY_PLAYER_STATE);
registerOutgoingChannel(ProtocolChannelCompatibility.LEGACY_PLAYER_STATES);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to register voice chat packets", 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 { private <T extends Packet<?>> void registerOutgoingPacket(Class<T> packetClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String id = packetClass.getDeclaredConstructor().newInstance().getID().toString(); String id = packetClass.getDeclaredConstructor().newInstance().getID().toString();
registerOutgoingChannel(id);
}
private void registerOutgoingChannel(String id) {
packets.add(id); packets.add(id);
Bukkit.getMessenger().registerOutgoingPluginChannel(Voicechat.INSTANCE, id); Bukkit.getMessenger().registerOutgoingPluginChannel(Voicechat.INSTANCE, id);
} }
@@ -114,7 +121,11 @@ public class NetManager implements Listener {
p.toBytes(buf); p.toBytes(buf);
byte[] bytes = new byte[buf.readableBytes()]; byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes); 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) { } catch (Exception e) {
Voicechat.LOGGER.error("Failed to send packet to '{}'", player.getName(), 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) { public void onPlayerHide(PlayerHideEvent event) {
RemovePlayerStatePacket packet = new RemovePlayerStatePacket(event.getHiddenPlayer().getUniqueId()); sendRemoveState(event.getObserver(), event.getHiddenPlayer());
NetManager.sendToClient(event.getObserver(), packet);
Voicechat.LOGGER.debug("Removing state of {} for {}", event.getHiddenPlayer().getName(), event.getObserver().getName()); 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) { public void broadcastRemoveState(Player stateOwner) {
RemovePlayerStatePacket packet = new RemovePlayerStatePacket(stateOwner.getUniqueId());
for (Player receiver : Voicechat.INSTANCE.getServer().getOnlinePlayers()) { for (Player receiver : Voicechat.INSTANCE.getServer().getOnlinePlayers()) {
NetManager.sendToClient(receiver, packet); sendRemoveState(receiver, stateOwner);
} }
// Send the default disconnected state to the API when disconnecting // Send the default disconnected state to the API when disconnecting
PluginManager.instance().onPlayerStateChanged(defaultDisconnectedState(stateOwner)); 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) { public void onPlayerCompatibilityCheckSucceeded(Player player) {
List<PlayerState> stateList = new ArrayList<>(states.size()); List<PlayerState> stateList = new ArrayList<>(states.size());
for (PlayerState state : states.values()) { for (PlayerState state : states.values()) {
@@ -83,6 +83,10 @@ public class ServerVoiceEvents implements Listener {
return isSupportedCompatibilityVersion(clientCompatibilities.getOrDefault(playerUuid, -1)); return isSupportedCompatibilityVersion(clientCompatibilities.getOrDefault(playerUuid, -1));
} }
public int getClientCompatibilityVersion(UUID playerUuid) {
return clientCompatibilities.getOrDefault(playerUuid, -1);
}
public static void sendIncompatibleMessage(Player player, int clientCompatibilityVersion) { public static void sendIncompatibleMessage(Player player, int clientCompatibilityVersion) {
if (clientCompatibilityVersion <= 6) { if (clientCompatibilityVersion <= 6) {
player.sendMessage(String.format(Voicechat.TRANSLATIONS.voicechatNotCompatibleMessage.get(), 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.voice.common.Secret;
import de.maxhenkel.voicechat.net.ProtocolChannelCompatibility;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
@@ -9,6 +10,8 @@ import javax.crypto.spec.SecretKeySpec;
public class ProtocolCryptoSelfTest { public class ProtocolCryptoSelfTest {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
testStateChannelCompatibility();
byte[] key = new byte[16]; byte[] key = new byte[16];
for (int i = 0; i < key.length; i++) key[i] = (byte) (i * 13 + 7); for (int i = 0; i < key.length; i++) key[i] = (byte) (i * 13 + 7);
UUID legacyKey = uuidFromBytes(key); UUID legacyKey = uuidFromBytes(key);
@@ -34,7 +37,23 @@ public class ProtocolCryptoSelfTest {
throw new AssertionError("Hybrid server AES-GCM round-trip failed"); 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) { private static UUID uuidFromBytes(byte[] bytes) {