2 Commits
Author SHA1 Message Date
asminme 1f58d472e7 update readme.md (no build)
Build AsminVoicechat / build-and-release (push) Canceled after 7s
2026-10-25 01:31:32 +02:00
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 99 additions and 13 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.0"
version="1.0.3"
else
current="${latest#v}"
IFS='.' read -r major minor patch <<< "$current"
+2
View File
@@ -2,3 +2,5 @@
build/
*.class
*.log
/LOCAL_AI_NOTES.md
AsminVoiceChat-*.jar
Binary file not shown.
+15 -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
@@ -40,6 +44,15 @@ DMCA is filed. If you feel strongly about the fork, no offense intended, get in
touch and we can try to work it out. Formal abuse reports can be sent to
[abuse@asmin.me](mailto:abuse@asmin.me).
## Testing AsminVoicechat
If you'd like to see how AsminVoicechat works in action, you're welcome to join my Minecraft server, [ClassicMC.top](https://classicmc.top).
**Server address:** `classicmc.top`
**Supported Minecraft versions:** 1.20.4 – 1.21.11
The server runs the configured version of AsminVoicechat, which is the same version available in this repository. Feel free to join and test the plugin in a live environment to see how it works in practice.
Copyright (c) 2026 Asmin Studios (asmin.me) for the AsminVoicechat changes.
All rights reserved for those changes. Upstream and bundled third-party rights
and notices remain with their respective owners.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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
@@ -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<Boolean> 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,
@@ -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) {