1 Commits
Author SHA1 Message Date
asminme f5429ee4c6 Fix language and update group members on 2.5 clients
Build AsminVoicechat / build-and-release (push) Successful in 2m24s
2026-10-24 21:59:10 +02:00
13 changed files with 125 additions and 84 deletions

No files matched your search

+1 -1
View File
@@ -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.1"
version="1.0.3"
else
current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current"
+1
View File
@@ -3,3 +3,4 @@ build/
*.class
*.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
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
+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
}
group = 'de.maxhenkel.voicechat'
version = findProperty('buildVersion') ?: '1.0.1'
version = findProperty('buildVersion') ?: '1.0.3'
java {
sourceCompatibility = JavaVersion.VERSION_1_8
+1 -1
View File
@@ -1,5 +1,5 @@
name: AsminVoicechat
version: 1.0.1
version: 1.0.3
main: de.maxhenkel.voicechat.Voicechat
api-version: '1.13'
prefix: AsminVoicechat
@@ -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
@@ -27,133 +27,97 @@ public class ServerConfig {
public ConfigEntry<Boolean> threadedServerSupport;
public ServerConfig(ConfigBuilder builder) {
builder.header(String.format("AsminVoiceChat (Simple Voice Chat) server config v%s", Voicechat.INSTANCE.getDescription().getVersion()));
builder.header("AsminVoiceChat (Simple Voice Chat) server config v1.0.2");
voiceChatPort = builder
.integerEntry("port", 24454, -1, 65535,
"EN: The port number to use for the voice chat communication.",
"PL: Port używany do komunikacji czatu głosowego.",
"EN: Audio packets are always transmitted via the UDP protocol on the port number",
"PL: Pakiety audio są zawsze przesyłane przez UDP na porcie wskazanym tutaj,",
"EN: specified here, independently of other networking used for the game server.",
"PL: niezależnie od pozostałej komunikacji sieciowej serwera gry.",
"EN: Set this to '-1' to use the same port number that is used by the Minecraft server.",
"PL: Ustaw '-1', aby użyć tego samego portu co serwer Minecraft.",
"EN: However, it is strongly recommended NOT to use the same port number because UDP on",
"PL: Zdecydowanie zaleca się użycie innego portu, ponieważ UDP na tym porcie jest domyślnie używane także przez zapytania serwera.",
"EN: it is also used by default for the server query. Doing so may crash the server!",
"PL: Użycie tego samego portu może spowodować awarię serwera!"
"The port number to use for the voice chat communication.",
"Audio packets are always transmitted via the UDP protocol on the port number",
"specified here, independently of other networking used for the game server.",
"Set this to '-1' to use the same port number that is used by the Minecraft server.",
"However, it is strongly recommended NOT to use the same port number because UDP on",
"it is also used by default for the server query. Doing so may crash the server!"
);
voiceChatBindAddress = builder
.stringEntry("bind_address", "",
"EN: The server IP address to bind the voice chat to",
"PL: Adres IP serwera, na którym ma nasłuchiwać czat głosowy.",
"EN: Leave blank to use the 'server-ip' property from the 'server.properties' config file",
"PL: Pozostaw puste, aby użyć wartości 'server-ip' z pliku 'server.properties'.",
"EN: To bind to the wildcard IP address, use '*'",
"PL: Aby nasłuchiwać na wszystkich interfejsach sieciowych, wpisz '*'."
"The server IP address to bind the voice chat to",
"Leave blank to use the 'server-ip' property from the 'server.properties' config file",
"To bind to the wildcard IP address, use '*'"
);
voiceChatDistance = builder
.doubleEntry("max_voice_distance", 48D, 1D, 1_000_000D,
"EN: The distance to which the voice can be heard",
"PL: Maksymalna odległość, z której słychać głos."
"The distance to which the voice can be heard"
);
whisperDistance = builder
.doubleEntry("whisper_distance", 24D, 1D, 1_000_000D,
"EN: The distance to which the voice can be heard when whispering",
"PL: Maksymalna odległość, z której słychać szept."
"The distance to which the voice can be heard when whispering"
);
voiceChatCodec = builder
.enumEntry("codec", Codec.VOIP,
"EN: The Opus codec",
"PL: Używany kodek Opus.",
"EN: Valid values are 'VOIP', 'AUDIO', and 'RESTRICTED_LOWDELAY'",
"PL: Dozwolone wartości: 'VOIP', 'AUDIO' oraz 'RESTRICTED_LOWDELAY'."
"The Opus codec",
"Valid values are 'VOIP', 'AUDIO', and 'RESTRICTED_LOWDELAY'"
);
voiceChatMtuSize = builder
.integerEntry("mtu_size", AudioUtils.MAX_OPUS_PAYLOAD_SIZE, 512, 2048,
"EN: The maximum size that audio packets are allowed to have (in bytes)",
"PL: Maksymalny rozmiar pakietu audio w bajtach.",
"EN: Setting this to lower values might cause issues",
"PL: Ustawienie mniejszej wartości może powodować problemy."
"The maximum size that audio packets are allowed to have (in bytes)",
"Setting this to lower values might cause issues"
);
tcpRateLimit = builder
.integerEntry("tcp_rate_limit", 16, -1, 1024,
"EN: The maximum number of packets a player can send per second",
"PL: Maksymalna liczba pakietów, które gracz może wysłać w ciągu sekundy.",
"EN: Set this to -1 to disable the rate limit - This must be greater than 0 in all other cases",
"PL: Ustaw '-1', aby wyłączyć limit. W pozostałych przypadkach wartość musi być większa od 0.",
"EN: This only applies to voice chat packets that are sent through Minecrafts networking",
"PL: Limit dotyczy tylko pakietów voice chatu przesyłanych przez sieć Minecrafta.",
"EN: This affects actions like opening/joining/leaving voice chat groups or general state changes like disabling/enabling the voice chat",
"PL: Obejmuje m.in. otwieranie, dołączanie i opuszczanie grup oraz włączanie lub wyłączanie voice chatu."
"The maximum number of packets a player can send per second",
"Set this to -1 to disable the rate limit - This must be greater than 0 in all other cases",
"This only applies to voice chat packets that are sent through Minecrafts networking",
"This affects actions like opening/joining/leaving voice chat groups or general state changes like disabling/enabling the voice chat"
);
keepAlive = builder
.integerEntry("keep_alive", 1000, 1000, Integer.MAX_VALUE,
"EN: The frequency at which keep-alive packets are sent (in milliseconds)",
"PL: Częstotliwość wysyłania pakietów keep-alive w milisekundach.",
"EN: Setting this to a higher value may result in timeouts",
"PL: Ustawienie większej wartości może powodować przekroczenia czasu połączenia."
"The frequency at which keep-alive packets are sent (in milliseconds)",
"Setting this to a higher value may result in timeouts"
);
groupsEnabled = builder
.booleanEntry("enable_groups", true,
"EN: If group chats are allowed",
"PL: Czy rozmowy grupowe są dozwolone."
"If group chats are allowed"
);
voiceHost = builder
.stringEntry("voice_host", "",
"EN: The hostname that clients should use to connect to the voice chat",
"PL: Nazwa hosta, której klienci używają do łączenia się z voice chatem.",
"EN: This may also include a port, e.g. 'example.com:24454'",
"PL: Możesz podać również port, np. 'example.com:24454'.",
"EN: Do NOT change this value if you don't know what you're doing",
"PL: Nie zmieniaj tej wartości, jeśli nie wiesz, do czego służy."
"The hostname that clients should use to connect to the voice chat",
"This may also include a port, e.g. 'example.com:24454'",
"Do NOT change this value if you don't know what you're doing"
);
allowRecording = builder
.booleanEntry("allow_recording", true,
"EN: If players are allowed to record the voice chat audio",
"PL: Czy gracze mogą nagrywać dźwięk z voice chatu."
"If players are allowed to record the voice chat audio"
);
spectatorInteraction = builder
.booleanEntry("spectator_interaction", false,
"EN: If spectators are allowed to talk to other players",
"PL: Czy widzowie mogą rozmawiać z innymi graczami."
"If spectators are allowed to talk to other players"
);
spectatorPlayerPossession = builder
.booleanEntry("spectator_player_possession", false,
"EN: If spectators can talk to players they are spectating",
"PL: Czy widz może rozmawiać z graczem, którego obserwuje."
"If spectators can talk to players they are spectating"
);
forceVoiceChat = builder
.booleanEntry("force_voice_chat", false,
"EN: If players without the voice chat mod should be kicked from the server",
"PL: Czy gracze bez moda voice chatu mają być wyrzucani z serwera."
"If players without the voice chat mod should be kicked from the server"
);
loginTimeout = builder
.integerEntry("login_timeout", 10_000, 100, Integer.MAX_VALUE,
"EN: The amount of time the server should wait to check if a player has the mod installed (in milliseconds)",
"PL: Czas oczekiwania na sprawdzenie, czy gracz ma zainstalowany mod, w milisekundach.",
"EN: Only relevant when 'force_voice_chat' is set to 'true'",
"PL: Używane tylko wtedy, gdy 'force_voice_chat' ma wartość 'true'."
"The amount of time the server should wait to check if a player has the mod installed (in milliseconds)",
"Only relevant when 'force_voice_chat' is set to 'true'"
);
broadcastRange = builder
.doubleEntry("broadcast_range", -1D, -1D, Double.MAX_VALUE,
"EN: The range in which the voice chat should broadcast audio",
"PL: Zasięg, w którym voice chat rozsyła dźwięk.",
"EN: A value less than 0 means 'max_voice_distance'",
"PL: Wartość mniejsza od 0 oznacza użycie 'max_voice_distance'."
"The range in which the voice chat should broadcast audio",
"A value less than 0 means 'max_voice_distance'"
);
allowPings = builder
.booleanEntry("allow_pings", true,
"EN: If the voice chat server should reply to external pings",
"PL: Czy serwer voice chatu ma odpowiadać na zewnętrzne pingi."
"If the voice chat server should reply to external pings"
);
threadedServerSupport = builder
.booleanEntry("threaded_server_support", true,
"EN: If the voice chat should support servers that run multiple threads, like Folia",
"PL: Czy voice chat ma obsługiwać serwery wielowątkowe, takie jak Folia.",
"EN: Disabling this may slightly improve performance, but can cause issues on servers that don't run everything on a single thread",
"PL: Wyłączenie może nieznacznie poprawić wydajność, ale powodować problemy na serwerach, które nie wykonują wszystkiego na jednym wątku."
"If the voice chat should support servers that run multiple threads, like Folia",
"Disabling this may slightly improve performance, but can cause issues on servers that don't run everything on a single thread"
);
}
@@ -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) {