1 Commits
Author SHA1 Message Date
asminme 8bde5778e2 pl language fix
Build AsminVoicechat / build-and-release (push) Successful in 2m25s
2026-10-24 15:58:28 +02:00
13 changed files with 84 additions and 125 deletions

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.3" version="1.0.1"
else else
current="${latest#v}" current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current" IFS='.' read -r major minor patch <<< "$current"
-1
View File
@@ -3,4 +3,3 @@ build/
*.class *.class
*.log *.log
/LOCAL_AI_NOTES.md /LOCAL_AI_NOTES.md
AsminVoiceChat-*.jar
Binary file not shown.
+2 -6
View File
@@ -10,12 +10,8 @@ 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. State messages are sent on are passed through the existing 2.6 voice routing. The fork changes only the
the channel names expected by each client generation, so 2.5 clients receive per-client cipher selection and compatibility-version acceptance.
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.3' version = findProperty('buildVersion') ?: '1.0.1'
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.3 version: 1.0.1
main: de.maxhenkel.voicechat.Voicechat main: de.maxhenkel.voicechat.Voicechat
api-version: '1.13' api-version: '1.13'
prefix: AsminVoicechat prefix: AsminVoicechat
@@ -112,8 +112,6 @@ 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) {
@@ -132,8 +130,6 @@ 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
@@ -27,97 +27,133 @@ public class ServerConfig {
public ConfigEntry<Boolean> threadedServerSupport; public ConfigEntry<Boolean> threadedServerSupport;
public ServerConfig(ConfigBuilder builder) { public ServerConfig(ConfigBuilder builder) {
builder.header("AsminVoiceChat (Simple Voice Chat) server config v1.0.2"); builder.header(String.format("AsminVoiceChat (Simple Voice Chat) server config v%s", Voicechat.INSTANCE.getDescription().getVersion()));
voiceChatPort = builder voiceChatPort = builder
.integerEntry("port", 24454, -1, 65535, .integerEntry("port", 24454, -1, 65535,
"The port number to use for the voice chat communication.", "EN: The port number to use for the voice chat communication.",
"Audio packets are always transmitted via the UDP protocol on the port number", "PL: Port używany do komunikacji czatu głosowego.",
"specified here, independently of other networking used for the game server.", "EN: Audio packets are always transmitted via the UDP protocol on the port number",
"Set this to '-1' to use the same port number that is used by the Minecraft server.", "PL: Pakiety audio są zawsze przesyłane przez UDP na porcie wskazanym tutaj,",
"However, it is strongly recommended NOT to use the same port number because UDP on", "EN: specified here, independently of other networking used for the game server.",
"it is also used by default for the server query. Doing so may crash the 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!"
); );
voiceChatBindAddress = builder voiceChatBindAddress = builder
.stringEntry("bind_address", "", .stringEntry("bind_address", "",
"The server IP address to bind the voice chat to", "EN: The server IP address to bind the voice chat to",
"Leave blank to use the 'server-ip' property from the 'server.properties' config file", "PL: Adres IP serwera, na którym ma nasłuchiwać czat głosowy.",
"To bind to the wildcard IP address, use '*'" "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 '*'."
); );
voiceChatDistance = builder voiceChatDistance = builder
.doubleEntry("max_voice_distance", 48D, 1D, 1_000_000D, .doubleEntry("max_voice_distance", 48D, 1D, 1_000_000D,
"The distance to which the voice can be heard" "EN: The distance to which the voice can be heard",
"PL: Maksymalna odległość, z której słychać głos."
); );
whisperDistance = builder whisperDistance = builder
.doubleEntry("whisper_distance", 24D, 1D, 1_000_000D, .doubleEntry("whisper_distance", 24D, 1D, 1_000_000D,
"The distance to which the voice can be heard when whispering" "EN: The distance to which the voice can be heard when whispering",
"PL: Maksymalna odległość, z której słychać szept."
); );
voiceChatCodec = builder voiceChatCodec = builder
.enumEntry("codec", Codec.VOIP, .enumEntry("codec", Codec.VOIP,
"The Opus codec", "EN: The Opus codec",
"Valid values are 'VOIP', 'AUDIO', and 'RESTRICTED_LOWDELAY'" "PL: Używany kodek Opus.",
"EN: Valid values are 'VOIP', 'AUDIO', and 'RESTRICTED_LOWDELAY'",
"PL: Dozwolone wartości: 'VOIP', 'AUDIO' oraz 'RESTRICTED_LOWDELAY'."
); );
voiceChatMtuSize = builder voiceChatMtuSize = builder
.integerEntry("mtu_size", AudioUtils.MAX_OPUS_PAYLOAD_SIZE, 512, 2048, .integerEntry("mtu_size", AudioUtils.MAX_OPUS_PAYLOAD_SIZE, 512, 2048,
"The maximum size that audio packets are allowed to have (in bytes)", "EN: The maximum size that audio packets are allowed to have (in bytes)",
"Setting this to lower values might cause issues" "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."
); );
tcpRateLimit = builder tcpRateLimit = builder
.integerEntry("tcp_rate_limit", 16, -1, 1024, .integerEntry("tcp_rate_limit", 16, -1, 1024,
"The maximum number of packets a player can send per second", "EN: 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", "PL: Maksymalna liczba pakietów, które gracz może wysłać w ciągu sekundy.",
"This only applies to voice chat packets that are sent through Minecrafts networking", "EN: Set this to -1 to disable the rate limit - This must be greater than 0 in all other cases",
"This affects actions like opening/joining/leaving voice chat groups or general state changes like disabling/enabling the voice chat" "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."
); );
keepAlive = builder keepAlive = builder
.integerEntry("keep_alive", 1000, 1000, Integer.MAX_VALUE, .integerEntry("keep_alive", 1000, 1000, Integer.MAX_VALUE,
"The frequency at which keep-alive packets are sent (in milliseconds)", "EN: The frequency at which keep-alive packets are sent (in milliseconds)",
"Setting this to a higher value may result in timeouts" "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."
); );
groupsEnabled = builder groupsEnabled = builder
.booleanEntry("enable_groups", true, .booleanEntry("enable_groups", true,
"If group chats are allowed" "EN: If group chats are allowed",
"PL: Czy rozmowy grupowe są dozwolone."
); );
voiceHost = builder voiceHost = builder
.stringEntry("voice_host", "", .stringEntry("voice_host", "",
"The hostname that clients should use to connect to the voice chat", "EN: The hostname that clients should use to connect to the voice chat",
"This may also include a port, e.g. 'example.com:24454'", "PL: Nazwa hosta, której klienci używają do łączenia się z voice chatem.",
"Do NOT change this value if you don't know what you're doing" "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."
); );
allowRecording = builder allowRecording = builder
.booleanEntry("allow_recording", true, .booleanEntry("allow_recording", true,
"If players are allowed to record the voice chat audio" "EN: If players are allowed to record the voice chat audio",
"PL: Czy gracze mogą nagrywać dźwięk z voice chatu."
); );
spectatorInteraction = builder spectatorInteraction = builder
.booleanEntry("spectator_interaction", false, .booleanEntry("spectator_interaction", false,
"If spectators are allowed to talk to other players" "EN: If spectators are allowed to talk to other players",
"PL: Czy widzowie mogą rozmawiać z innymi graczami."
); );
spectatorPlayerPossession = builder spectatorPlayerPossession = builder
.booleanEntry("spectator_player_possession", false, .booleanEntry("spectator_player_possession", false,
"If spectators can talk to players they are spectating" "EN: If spectators can talk to players they are spectating",
"PL: Czy widz może rozmawiać z graczem, którego obserwuje."
); );
forceVoiceChat = builder forceVoiceChat = builder
.booleanEntry("force_voice_chat", false, .booleanEntry("force_voice_chat", false,
"If players without the voice chat mod should be kicked from the server" "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."
); );
loginTimeout = builder loginTimeout = builder
.integerEntry("login_timeout", 10_000, 100, Integer.MAX_VALUE, .integerEntry("login_timeout", 10_000, 100, Integer.MAX_VALUE,
"The amount of time the server should wait to check if a player has the mod installed (in milliseconds)", "EN: 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'" "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'."
); );
broadcastRange = builder broadcastRange = builder
.doubleEntry("broadcast_range", -1D, -1D, Double.MAX_VALUE, .doubleEntry("broadcast_range", -1D, -1D, Double.MAX_VALUE,
"The range in which the voice chat should broadcast audio", "EN: The range in which the voice chat should broadcast audio",
"A value less than 0 means 'max_voice_distance'" "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'."
); );
allowPings = builder allowPings = builder
.booleanEntry("allow_pings", true, .booleanEntry("allow_pings", true,
"If the voice chat server should reply to external pings" "EN: If the voice chat server should reply to external pings",
"PL: Czy serwer voice chatu ma odpowiadać na zewnętrzne pingi."
); );
threadedServerSupport = builder threadedServerSupport = builder
.booleanEntry("threaded_server_support", true, .booleanEntry("threaded_server_support", true,
"If the voice chat should support servers that run multiple threads, like Folia", "EN: 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" "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."
); );
} }
@@ -38,9 +38,6 @@ 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);
} }
@@ -103,10 +100,6 @@ 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);
} }
@@ -121,11 +114,7 @@ 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);
int compatibilityVersion = Voicechat.SERVER.getClientCompatibilityVersion(player.getUniqueId()); player.sendPluginMessage(Voicechat.INSTANCE, p.getID().toString(), bytes);
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);
} }
@@ -1,27 +0,0 @@
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,7 +56,8 @@ public class PlayerStateManager implements Listener {
} }
public void onPlayerHide(PlayerHideEvent event) { public void onPlayerHide(PlayerHideEvent event) {
sendRemoveState(event.getObserver(), event.getHiddenPlayer()); RemovePlayerStatePacket packet = new RemovePlayerStatePacket(event.getHiddenPlayer().getUniqueId());
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());
} }
@@ -82,22 +83,14 @@ 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()) {
sendRemoveState(receiver, stateOwner); NetManager.sendToClient(receiver, packet);
} }
// 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,10 +83,6 @@ 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(),
+1 -20
View File
@@ -1,5 +1,4 @@
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;
@@ -10,8 +9,6 @@ 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);
@@ -37,23 +34,7 @@ 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: legacy state-channel mapping, 100 CBC interop cycles and 100 GCM round-trips"); System.out.println("PASS: 100 CBC client/server 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) {