diff --git a/.gitignore b/.gitignore
index 5546f84..c245070 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,15 @@ out/
*.eml
*.iml
.userlibraries
+
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 90df29d..c00d129 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,5 +8,26 @@
ts3j
1.0-SNAPSHOT
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.7.0
+
+ 1.8
+ 1.8
+
+
+
+
+
+
+ junit
+ junit
+ RELEASE
+ test
+
+
\ No newline at end of file
diff --git a/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java b/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java
new file mode 100644
index 0000000..f59237f
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/TeamspeakClient.java
@@ -0,0 +1,300 @@
+package com.github.manevolent.ts3j;
+
+import com.github.manevolent.ts3j.command.Command;
+import com.github.manevolent.ts3j.command.part.CommandSingleParameter;
+import com.github.manevolent.ts3j.command.response.CommandResponse;
+import com.github.manevolent.ts3j.enums.ConnectionState;
+import com.github.manevolent.ts3j.enums.PacketFlags;
+import com.github.manevolent.ts3j.enums.PacketType;
+import com.github.manevolent.ts3j.handler.TeamspeakClientHandler;
+import com.github.manevolent.ts3j.util.Ts3Logging;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.util.Date;
+import java.util.concurrent.TimeoutException;
+
+public class TeamspeakClient {
+ private final PacketReceiver packetReceiver = new PacketReceiver();
+
+ private final DatagramSocket datagramSocket;
+ private final DatagramPacket datagramPacket = new DatagramPacket(new byte[1500], 1500);
+
+ private final Object connectionStateLock = new Object();
+
+ private TeamspeakClientHandler handler;
+
+ private InetSocketAddress remote = null;
+ private ConnectionState connectionState = null;
+ private int returnCodeIndex = 0;
+
+ public TeamspeakClient(DatagramSocket datagramSocket) {
+ this.datagramSocket = datagramSocket;
+ }
+
+ public TeamspeakClient() throws SocketException {
+ this(new DatagramSocket());
+
+ setConnectionState(ConnectionState.DISCONNECTED);
+ }
+
+ protected void setConnectionState(ConnectionState state) {
+ synchronized (connectionStateLock) {
+ if (state != this.connectionState) {
+ boolean wasDisconnected = this.connectionState == ConnectionState.DISCONNECTED;
+
+ this.connectionState = state;
+
+ if (wasDisconnected) {
+ Ts3Logging.debug("Starting network receiver thread...");
+
+ Thread receiverThread = new Thread(new PacketReceiver());
+ receiverThread.setDaemon(true);
+ receiverThread.start();
+ }
+
+ if (handler != null)
+ handler.handleConnectionStateChanging(state);
+
+ if (handler == null || handler.getClass() != state.getHandlerClass()) {
+ Ts3Logging.debug("Assigning " + state.getHandlerClass().getName() + " handler...");
+
+ handler = state.createHandler(this);
+
+ try {
+ handler.onAssigned();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ connectionStateLock.notifyAll();
+ }
+ }
+ }
+
+ public boolean isConnected() {
+ return connectionState == ConnectionState.CONNECTED;
+ }
+
+ public ConnectionState getConnectionState() {
+ return connectionState;
+ }
+
+ private void joinConnectionState(ConnectionState state, long wait)
+ throws InterruptedException, TimeoutException {
+ synchronized (connectionStateLock) {
+ if (connectionState != state) {
+ connectionStateLock.wait(wait);
+
+ if (connectionState != state)
+ throw new TimeoutException("timeout waiting for " + state.name() + " state");
+ }
+ }
+ }
+
+ public CommandResponse sendCommand(Command command) throws IOException {
+ if (command.willExpectResponse() && !isConnected())
+ throw new IOException("not connected");
+
+ CommandResponse response;
+
+ if (command.willExpectResponse()) {
+ int returnCodeIndex = ++ this.returnCodeIndex;
+ command.appendParameter(new CommandSingleParameter("return_code", returnCodeIndex));
+ response = new CommandResponse(command, returnCodeIndex);
+ } else {
+ response = new CommandResponse(command, -1);
+ }
+
+ String commandString = command.toString();
+
+ response.setDispatchedTime(System.currentTimeMillis());
+
+ return response;
+ }
+
+ public void connect(InetSocketAddress remote, String password, long timeout) throws IOException {
+ try {
+ if (connectionState != ConnectionState.DISCONNECTED)
+ throw new IllegalStateException(connectionState.name());
+
+ datagramSocket.connect(remote);
+
+ setConnectionState(ConnectionState.CONNECTING);
+ joinConnectionState(ConnectionState.CONNECTED, timeout);
+ } catch (Throwable e) {
+ setConnectionState(ConnectionState.DISCONNECTED);
+
+ throw new IOException(e);
+ }
+ }
+
+ public void send(PacketType type, byte[] data) throws IOException {
+ C2SPacket packet = createOutgoingPacket(type, data);
+ }
+
+ private C2SPacket createOutgoingPacket(PacketType type, byte[] data) throws IOException {
+ C2SPacket packet = new C2SPacket();
+
+ //TODO
+ packet.setPacketId(0);
+
+ //TODO
+ packet.setGenerationId(0);
+
+ //TODO
+ packet.setClientId(0);
+
+ //TODO
+ packet.packetFlags |= 0;
+
+ switch (type) {
+ case VOICE:
+ case VOICE_WHISPER:
+
+ break;
+ case COMMAND:
+ case COMMAND_LOW:
+
+ break;
+ case PING:
+
+ break;
+ case PONG:
+
+ break;
+ case ACK:
+ case ACK_LOW:
+
+ break;
+ case INIT:
+ packet.setPacketFlags(packet.getPacketFlags() | PacketFlags.UNENCRYPTED.getIndex());
+ break;
+ }
+
+ return packet;
+ }
+
+ /**
+ * Wire packet write
+ * @param packet Packet to write
+ * @throws IOException
+ */
+ private void sendNetworkPacket(DatagramPacket packet) throws IOException {
+ Ts3Logging.debug(
+ "[NETWORK] WRITE Len=" + packet.getLength() + " to " + datagramSocket.getRemoteSocketAddress() +
+ "\n" + Ts3Logging.getHex(packet.getData())
+ );
+
+ packet.setSocketAddress(datagramSocket.getRemoteSocketAddress());
+
+ datagramSocket.send(packet);
+ }
+
+ private DatagramPacket receiveNetworkPacket() throws IOException {
+ datagramSocket.receive(datagramPacket);
+ return datagramPacket;
+ }
+
+ private void handleNetworkPacket(DatagramPacket packet) {
+ Ts3Logging.debug(
+ "[NETWORK] READ Len=" + packet.getLength() + " from " + packet.getSocketAddress() +
+ "\n" + Ts3Logging.getHex(packet.getData())
+ );
+
+ if (handler != null) handler.handleNetworkPacket(packet);
+ }
+
+ public InetSocketAddress getRemote() {
+ return remote;
+ }
+
+ private class PacketReceiver implements Runnable {
+ @Override
+ public void run() {
+ while (connectionState != ConnectionState.DISCONNECTED) {
+ try {
+ handleNetworkPacket(receiveNetworkPacket());
+ } catch (IOException e) {
+ Ts3Logging.info("Problem handling packet: " + e.getMessage());
+ }
+ }
+
+ Ts3Logging.debug("Leaving packet receiver");
+ }
+ }
+
+ private class C2SPacket {
+ private int packetId;
+ private int generationId;
+ private int clientId;
+ private int packetFlags;
+
+ private PacketType type;
+ private byte[] data;
+ private Date lastSendTime;
+
+ public PacketType getType() {
+ return type;
+ }
+
+ public void setType(PacketType type) {
+ this.type = type;
+ }
+
+ public byte[] getData() {
+ return data;
+ }
+
+ public void setData(byte[] data) {
+ this.data = data;
+ }
+
+ public Date getLastSendTime() {
+ return lastSendTime;
+ }
+
+ public int getPacketId() {
+ return packetId;
+ }
+
+ public void setPacketId(int packetId) {
+ this.packetId = packetId;
+ }
+
+ public int getGenerationId() {
+ return generationId;
+ }
+
+ public void setGenerationId(int generationId) {
+ this.generationId = generationId;
+ }
+
+ public int getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(int clientId) {
+ this.clientId = clientId;
+ }
+
+ public int getPacketFlags() {
+ return packetFlags;
+ }
+
+ public void setPacketFlags(int packetFlags) {
+ this.packetFlags = packetFlags;
+ }
+
+ /**
+ * Sends, or re-sends, a packet
+ */
+ public void send() {
+
+ }
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/command/Command.java b/src/main/java/com/github/manevolent/ts3j/command/Command.java
new file mode 100644
index 0000000..37051df
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/command/Command.java
@@ -0,0 +1,12 @@
+package com.github.manevolent.ts3j.command;
+
+import com.github.manevolent.ts3j.command.part.CommandParameter;
+
+public interface Command {
+ boolean willExpectResponse();
+
+ Iterable getParameters();
+
+ void appendParameter(CommandParameter parameter);
+
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java
new file mode 100644
index 0000000..7dd10b5
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandOption.java
@@ -0,0 +1,17 @@
+package com.github.manevolent.ts3j.command.part;
+
+import com.github.manevolent.ts3j.enums.CommandPartType;
+
+public class CommandOption implements CommandParameter {
+ private final String value;
+
+ public CommandOption(String value) {
+ this.value = " -" + value;
+ }
+
+ @Override
+ public CommandPartType getType() {
+ return CommandPartType.OPTION;
+ }
+
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java
new file mode 100644
index 0000000..1af70fc
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandParameter.java
@@ -0,0 +1,9 @@
+package com.github.manevolent.ts3j.command.part;
+
+import com.github.manevolent.ts3j.enums.CommandPartType;
+
+public interface CommandParameter {
+
+ CommandPartType getType();
+
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java
new file mode 100644
index 0000000..ef57d87
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/command/part/CommandSingleParameter.java
@@ -0,0 +1,81 @@
+package com.github.manevolent.ts3j.command.part;
+
+import com.github.manevolent.ts3j.enums.CommandPartType;
+
+public class CommandSingleParameter implements CommandParameter {
+ private final String key, value;
+
+ public CommandSingleParameter(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public CommandSingleParameter(String key, boolean value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, byte value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, short value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, int value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, long value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, float value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ public CommandSingleParameter(String key, double value) {
+ this.key = key;
+ this.value = serialize(value);
+ }
+
+ @Override
+ public CommandPartType getType() {
+ return CommandPartType.SINGLE_PARAMETER;
+ }
+
+ public static String serialize(boolean value) {
+ return value ? "1": "0";
+ }
+
+ public static String serialize(byte value) {
+ return Byte.toString(value);
+ }
+
+ public static String serialize(short value) {
+ return Short.toString(value);
+ }
+
+ public static String serialize(int value) {
+ return Integer.toString(value);
+ }
+
+ public static String serialize(long value) {
+ return Long.toString(value);
+ }
+
+ public static String serialize(float value) {
+ return Float.toString(value);
+ }
+
+ public static String serialize(double value) {
+ return Double.toString(value);
+ }
+
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java b/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java
new file mode 100644
index 0000000..3d21a87
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/command/response/CommandResponse.java
@@ -0,0 +1,47 @@
+package com.github.manevolent.ts3j.command.response;
+
+import com.github.manevolent.ts3j.command.Command;
+
+public class CommandResponse {
+ private final Object returnLock = new Object();
+ private final Command command;
+ private final int returnCode;
+ private long dispatchedTime;
+
+ public CommandResponse(Command command, int returnCode) {
+ this.command = command;
+ this.returnCode = returnCode;
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+
+ public int getReturnCode() {
+ return returnCode;
+ }
+
+ public CommandResponse join() throws InterruptedException {
+ synchronized (returnLock) {
+ returnLock.wait();
+ }
+
+ return this;
+ }
+
+ public CommandResponse join(int millis) throws InterruptedException {
+ synchronized (returnLock) {
+ returnLock.wait(millis);
+ }
+
+ return this;
+ }
+
+ public long getDispatchedTime() {
+ return dispatchedTime;
+ }
+
+ public void setDispatchedTime(long dispatchedTime) {
+ this.dispatchedTime = dispatchedTime;
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java b/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java
new file mode 100644
index 0000000..6d3f3de
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/ClientListOptions.java
@@ -0,0 +1,29 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum ClientListOptions {
+ UID(1 << 0),
+ AWAY(1 << 1),
+ VOICE(1 << 2),
+ TIMES(1 << 3),
+ GROUPS(1 << 4),
+ INFO(1 << 5),
+ ICON(1 << 6),
+ COUNTRY(1 << 7);
+
+ private final int index;
+
+ ClientListOptions(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static ClientListOptions fromId(int index) {
+ for (ClientListOptions value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java b/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java
new file mode 100644
index 0000000..69e44a7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/ClientType.java
@@ -0,0 +1,23 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum ClientType {
+ FULL(0),
+ QUERY(1);
+
+ private final int index;
+
+ ClientType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static ClientType fromId(int index) {
+ for (ClientType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java b/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java
new file mode 100644
index 0000000..3f3d246
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/CodecType.java
@@ -0,0 +1,31 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum CodecType {
+ SPEEX_NARROWBAND(0),
+ SPEEX_WIDEBAND(1),
+ SPEEX_ULTRA_WIDEBAND(2),
+ CELT_MONO(3),
+ OPUS_VOICE(4),
+ OPUS_MUSIC(5),
+
+ RAW(127)
+
+ ;
+
+ private final int index;
+
+ CodecType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static CodecType fromId(int index) {
+ for (CodecType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java b/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java
new file mode 100644
index 0000000..f5044bc
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/CommandPartType.java
@@ -0,0 +1,7 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum CommandPartType {
+ SINGLE_PARAMETER,
+ MULTI_PAREMTER,
+ OPTION;
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java b/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java
new file mode 100644
index 0000000..05390ea
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/ConnectionState.java
@@ -0,0 +1,30 @@
+
+package com.github.manevolent.ts3j.enums;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+import com.github.manevolent.ts3j.handler.*;
+
+public enum ConnectionState {
+ CONNECTING(TeamspeakClientHandlerConnecting.class),
+ NEGOTIATING(TeamspeakClientHandlerNegotiating.class),
+ CONNECTED(TeamspeakClientHandlerConnected.class),
+ DISCONNECTED(TeamspeakClientHandlerDisconnected.class);
+
+ private final Class extends TeamspeakClientHandler> clazz;
+
+ ConnectionState(Class extends TeamspeakClientHandler> clazz) {
+ this.clazz = clazz;
+ }
+
+ public Class extends TeamspeakClientHandler> getHandlerClass() {
+ return clazz;
+ }
+
+ public TeamspeakClientHandler createHandler(TeamspeakClient client) {
+ try {
+ return clazz.getConstructor(TeamspeakClient.class).newInstance(client);
+ } catch (ReflectiveOperationException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java b/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java
new file mode 100644
index 0000000..a621047
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/DisconnectReason.java
@@ -0,0 +1,35 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum DisconnectReason {
+ USER_ACTION(0),
+ USER_OR_CHANNEL_MOVED(1),
+ SUBSCRIPTION_CHANGED(2),
+ TIMEOUT(3),
+ KICKED_FROM_CHANNEL(4),
+ KICKED_FROM_SERVER(5),
+ BANNED(6),
+ SERVER_STOPPED(7),
+ LEFT_SERVER(8),
+ CHANNEL_UPDATED(9),
+ SERVER_OR_CHANNEL_EDITED(10),
+ SERVER_SHUTDOWN(11)
+
+ ;
+
+ private final int index;
+
+ DisconnectReason(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static DisconnectReason fromId(int index) {
+ for (DisconnectReason value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java
new file mode 100644
index 0000000..3506c7c
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupNamingMode.java
@@ -0,0 +1,29 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum GroupNamingMode {
+ UID(1 << 0),
+ AWAY(1 << 1),
+ VOICE(1 << 2),
+ TIMES(1 << 3),
+ GROUPS(1 << 4),
+ INFO(1 << 5),
+ ICON(1 << 6),
+ COUNTRY(1 << 7);
+
+ private final int index;
+
+ GroupNamingMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static GroupNamingMode fromId(int index) {
+ for (GroupNamingMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java
new file mode 100644
index 0000000..e950104
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupType.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum GroupType {
+ TEMPLATE(0),
+ REGULAR(1),
+ QUERY(2)
+
+ ;
+
+ private final int index;
+
+ GroupType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static GroupType fromId(int index) {
+ for (GroupType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java b/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java
new file mode 100644
index 0000000..6b63ab7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/GroupWhisperType.java
@@ -0,0 +1,29 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum GroupWhisperType {
+ ALL_CHANNELS(0),
+ CURRENT_CHANNEL(1),
+ ALL_PARENT_CHANNEL(2),
+ CHANNEL_FAMILY(3),
+ COMPLETE_CHANNEL_FAMILY(4),
+ SUBCHANNELS(5)
+
+ ;
+
+ private final int index;
+
+ GroupWhisperType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static GroupWhisperType fromId(int index) {
+ for (GroupWhisperType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java b/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java
new file mode 100644
index 0000000..3e4f106
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/HostBannerMode.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum HostBannerMode {
+ NO_ADJUST(0),
+ IGNORE_ASPECT_RATIO(1),
+ KEEP_ASPECT(2)
+
+ ;
+
+ private final int index;
+
+ HostBannerMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static HostBannerMode fromId(int index) {
+ for (HostBannerMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java b/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java
new file mode 100644
index 0000000..df3a35c
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/HostMessageMode.java
@@ -0,0 +1,27 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum HostMessageMode {
+ NONE(0),
+ LOG(1),
+ MODAL(2),
+ MODAL_QUIT(3)
+
+ ;
+
+ private final int index;
+
+ HostMessageMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static HostMessageMode fromId(int index) {
+ for (HostMessageMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java b/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java
new file mode 100644
index 0000000..7b1c878
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/KickIdentifier.java
@@ -0,0 +1,25 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum KickIdentifier {
+ CHANNEL(4),
+ SERVER(5)
+
+ ;
+
+ private final int index;
+
+ KickIdentifier(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static KickIdentifier fromId(int index) {
+ for (KickIdentifier value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java b/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java
new file mode 100644
index 0000000..2c08887
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/LicenseType.java
@@ -0,0 +1,28 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum LicenseType {
+ NO_LICENSE(0),
+ ATHP(1),
+ LAN(2),
+ NPL(3),
+ UNKNOWN(4)
+
+ ;
+
+ private final int index;
+
+ LicenseType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static LicenseType fromId(int index) {
+ for (LicenseType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java b/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java
new file mode 100644
index 0000000..9669f53
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/LogLevel.java
@@ -0,0 +1,27 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum LogLevel {
+ ERROR(1),
+ WARNING(2),
+ DEBUG(3),
+ INFO(4)
+
+ ;
+
+ private final int index;
+
+ LogLevel(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static LogLevel fromId(int index) {
+ for (LogLevel value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java
new file mode 100644
index 0000000..92c3c90
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/PacketFlags.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum PacketFlags {
+ NONE(0x0),
+ FRAGMENTED(0X10),
+ NEW_PROTOCOL(0x20),
+ COMPRESSED(0x40),
+ UNENCRYPTED(0x80);
+
+ private final int index;
+
+ PacketFlags(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static PacketFlags fromId(int index) {
+ for (PacketFlags value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java b/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java
new file mode 100644
index 0000000..23fe063
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/PacketType.java
@@ -0,0 +1,31 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum PacketType {
+ VOICE(0x0),
+ VOICE_WHISPER(0x1),
+ COMMAND(0x2),
+ COMMAND_LOW(0x3),
+ PING(0x4),
+ PONG(0x5),
+ ACK(0x6),
+ ACK_LOW(0x7),
+ INIT(0x8)
+ ;
+
+ private final int index;
+
+ PacketType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static PacketType fromId(int index) {
+ for (PacketType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java b/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java
new file mode 100644
index 0000000..96c73fc
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/PermissionGroupType.java
@@ -0,0 +1,28 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum PermissionGroupType {
+ SERVER_GROUP(0),
+ GLOBAL_CLIENT(1),
+ CHANNEL(2),
+ CHANNEL_GROUP(3),
+ CHANNEL_CLIENT(4)
+
+ ;
+
+ private final int index;
+
+ PermissionGroupType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static PermissionGroupType fromId(int index) {
+ for (PermissionGroupType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java b/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java
new file mode 100644
index 0000000..5e8aabb
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/PluginTargetMode.java
@@ -0,0 +1,27 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum PluginTargetMode {
+ CURRENT_CHANNEL(0),
+ SERVER(1),
+ CLIENT(2),
+ CURRENT_CHANNEL_SUBSCRIBED_CLIENTS(3)
+
+ ;
+
+ private final int index;
+
+ PluginTargetMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static PluginTargetMode fromId(int index) {
+ for (PluginTargetMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java b/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java
new file mode 100644
index 0000000..39789b7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/PriviledgeTokenType.java
@@ -0,0 +1,25 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum PriviledgeTokenType {
+ SERVER_GROUP(0),
+ CHANNEL_GROUP(1)
+
+ ;
+
+ private final int index;
+
+ PriviledgeTokenType(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static PriviledgeTokenType fromId(int index) {
+ for (PriviledgeTokenType value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java b/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java
new file mode 100644
index 0000000..6ff6386
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/TextMessageTargetMode.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum TextMessageTargetMode {
+ PRIVATE(1),
+ CHANNEL(2),
+ SERVER(3)
+
+ ;
+
+ private final int index;
+
+ TextMessageTargetMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static TextMessageTargetMode fromId(int index) {
+ for (TextMessageTargetMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java b/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java
new file mode 100644
index 0000000..0b05eb7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/enums/VoiceEncryptionMode.java
@@ -0,0 +1,26 @@
+package com.github.manevolent.ts3j.enums;
+
+public enum VoiceEncryptionMode {
+ INDIVIDUAL(0),
+ DISABLED(1),
+ ENABLED(2)
+
+ ;
+
+ private final int index;
+
+ VoiceEncryptionMode(int index) {
+ this.index = index;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ public static VoiceEncryptionMode fromId(int index) {
+ for (VoiceEncryptionMode value : values())
+ if (value.getIndex() == index) return value;
+
+ throw new IllegalArgumentException("invalid index: " + index);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java
new file mode 100644
index 0000000..73697a7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandler.java
@@ -0,0 +1,31 @@
+package com.github.manevolent.ts3j.handler;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+import com.github.manevolent.ts3j.enums.ConnectionState;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+
+public abstract class TeamspeakClientHandler {
+ private final TeamspeakClient client;
+
+ protected TeamspeakClientHandler(TeamspeakClient client) {
+ this.client = client;
+ }
+
+ public void handleConnectionStateChanging(ConnectionState connectionState) {
+
+ }
+
+ public void handleNetworkPacket(DatagramPacket packet) {
+
+ }
+
+ public void onAssigned() throws IOException {
+
+ }
+
+ protected TeamspeakClient getClient() {
+ return client;
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java
new file mode 100644
index 0000000..64ae81d
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnected.java
@@ -0,0 +1,9 @@
+package com.github.manevolent.ts3j.handler;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+
+public class TeamspeakClientHandlerConnected extends TeamspeakClientHandler {
+ public TeamspeakClientHandlerConnected(TeamspeakClient client) {
+ super(client);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java
new file mode 100644
index 0000000..fd62e6a
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerConnecting.java
@@ -0,0 +1,80 @@
+package com.github.manevolent.ts3j.handler;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+import com.github.manevolent.ts3j.enums.PacketType;
+import com.github.manevolent.ts3j.util.Ts3Logging;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Random;
+
+public class TeamspeakClientHandlerConnecting extends TeamspeakClientHandler {
+
+
+ public TeamspeakClientHandlerConnecting(TeamspeakClient client) {
+ super(client);
+ }
+
+ @Override
+ public void onAssigned() throws IOException {
+ Ts3Logging.debug("Connecting: sending Init1...");
+
+ processInit1();
+ }
+
+ /**
+ * Processes the first client dispatch of the Init1 packet (C2S initial handshake packet)
+ */
+ public void processInit1() throws IOException {
+ processInit1(null);
+ }
+
+ public void processInit1(byte[] data) throws IOException {
+ int versionLen = 4;
+ int initTypeLen = 1;
+ Integer type = null;
+
+ if (data != null) {
+ type = (int) data[0];
+
+ if (data.length < initTypeLen)
+ throw new IllegalArgumentException("invalid Init1 packet (too short)");
+ }
+
+ ByteBuffer buffer = null;
+
+ if (type == null) {
+ buffer = ByteBuffer.allocate(versionLen + initTypeLen + 4 + 4 + 8);
+
+ buffer.put(new byte[] { 0x09, (byte)0x83, (byte)0x8C, (byte)0xCF });
+ buffer.put((byte) 0x00); // initType
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ buffer.putInt((int) (System.currentTimeMillis() / 1000L));
+
+ Random random = new Random();
+ for (int i = 0; i < 4; i++)
+ buffer.put((byte) random.nextInt(256)); // 4byte random
+ } else {
+ switch (type) {
+ case 1:
+
+ break;
+ case 3:
+
+ break;
+ case 0x7F:
+ // 0x7F: Some strange servers do this
+ // the normal client responds by starting again
+ break;
+ default:
+ throw new IllegalArgumentException("invalid Init1 packet id: " + type);
+ }
+ }
+
+ if (buffer == null || buffer.position() <= 0)
+ throw new IllegalStateException("invalid send buffer");
+
+ getClient().send(PacketType.INIT, buffer.array());
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java
new file mode 100644
index 0000000..25549d2
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerDisconnected.java
@@ -0,0 +1,9 @@
+package com.github.manevolent.ts3j.handler;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+
+public class TeamspeakClientHandlerDisconnected extends TeamspeakClientHandler {
+ public TeamspeakClientHandlerDisconnected(TeamspeakClient client) {
+ super(client);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java
new file mode 100644
index 0000000..0b9cb38
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/handler/TeamspeakClientHandlerNegotiating.java
@@ -0,0 +1,9 @@
+package com.github.manevolent.ts3j.handler;
+
+import com.github.manevolent.ts3j.TeamspeakClient;
+
+public class TeamspeakClientHandlerNegotiating extends TeamspeakClientHandler {
+ public TeamspeakClientHandlerNegotiating(TeamspeakClient client) {
+ super(client);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java
new file mode 100644
index 0000000..603c5d7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Const.java
@@ -0,0 +1,38 @@
+package com.github.manevolent.ts3j.util;
+
+public final class Ts3Const {
+ // Common Definitions
+
+ //limited length, measured in characters
+ public static int MaxSizeChannelName = 40;
+ public static int MaxSizeVirtualserverName = 64;
+ public static int MaxSizeClientNicknameSdk = 64;
+ public static int MinSizeClientNicknameSdk = 3;
+ public static int MaxSizeReasonMessage = 80;
+
+ //limited length, measured in bytes (utf8 encoded)
+ public static int MaxSizeTextMessage = 1024;
+ public static int MaxSizeChannelTopic = 255;
+ public static int MaxSizeChannelDescription = 8192;
+ public static int MaxSizeVirtualserverWelcomeMessage = 1024;
+
+ // Rare Definitions
+
+ //limited length, measured in characters
+ public static int MaxSizeClientNickname = 30;
+ public static int MinSizeClientNickname = 3;
+ public static int MaxSizeAwayMessage = 80;
+ public static int MaxSizeGroupName = 30;
+ public static int MaxSizeTalkRequestMessage = 50;
+ public static int MaxSizeComplainMessage = 200;
+ public static int MaxSizeClientDescription = 200;
+ public static int MaxSizeHostMessage = 200;
+ public static int MaxSizeHostbuttonTooltip = 50;
+ public static int MaxSizePokeMessage = 100;
+ public static int MaxSizeOfflineMessage = 4096;
+ public static int MaxSizeOfflineMessageSubject = 200;
+
+ //limited length, measured in bytes (utf8 encoded)
+ public static int MaxSizePluginCommand = 1024 * 8;
+ public static int MaxSizeVirtualserverHostbannerGfxUrl = 2000;
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java
new file mode 100644
index 0000000..09d05c7
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3Logging.java
@@ -0,0 +1,20 @@
+package com.github.manevolent.ts3j.util;
+
+public final class Ts3Logging {
+ private static final String HEXES = "0123456789ABCDEF";
+ public static String getHex(byte[] raw) {
+ final StringBuilder hex = new StringBuilder(2 * raw.length);
+ for (final byte b : raw) {
+ hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
+ }
+ return hex.toString();
+ }
+
+ public static void info(String message) {
+ System.err.println("[INFO] " + message);
+ }
+
+ public static void debug(String message) {
+ System.err.println("[DEBUG] " + message);
+ }
+}
diff --git a/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java
new file mode 100644
index 0000000..aeafc1d
--- /dev/null
+++ b/src/main/java/com/github/manevolent/ts3j/util/Ts3String.java
@@ -0,0 +1,67 @@
+package com.github.manevolent.ts3j.util;
+
+public final class Ts3String {
+ public static String escape(String string) {
+ StringBuilder sb = new StringBuilder(string.length());
+
+ for (int i = 0; i < string.length(); i ++) {
+ switch (string.charAt(i)) {
+ case '\\': sb.append("\\\\"); break;
+ case '/': sb.append("\\/"); break;
+ case ' ': sb.append("\\s"); break;
+ case '|': sb.append("\\p"); break;
+ case '\f': sb.append("\\f"); break;
+ case '\n': sb.append("\\n"); break;
+ case '\r': sb.append("\\r"); break;
+ case '\t': sb.append("\\t"); break;
+ //case '\v': sb.append("\\v"); break;
+ default:
+ sb.append(string.charAt(i));
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public static String unescape(String string) {
+ StringBuilder sb = new StringBuilder(string.length());
+
+ for (int i = 0; i < string.length(); i ++) {
+ char c = string.charAt(i);
+ if (c == '\\') {
+ if (i++ >= string.length()) throw new IllegalArgumentException("illegal escape at end of string");
+ switch (string.charAt(i)) {
+ //case 'v': sb.append('\v'); break;
+ case 't': sb.append('\t'); break;
+ case 'r': sb.append('\r'); break;
+ case 'n': sb.append('\n'); break;
+ case 'f': sb.append('\f'); break;
+ case 'p': sb.append('|'); break;
+ case 's': sb.append(' '); break;
+ case '/': sb.append('/'); break;
+ case '\\': sb.append('\\'); break;
+ default:
+ throw new IllegalArgumentException("invalid escape code: " + c);
+ }
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public static int getTokenLength(String s) {
+ return (int) s.length() + (int) s.chars().filter(x -> isDoubleChar((char)x)).count();
+ }
+
+ public static boolean isDoubleChar(char c) {
+ return c == '\\' ||
+ c == '/' ||
+ c == ' ' ||
+ c == '|' ||
+ c == '\f' ||
+ c == '\n' ||
+ c == '\r' ||
+ c == '\t';// ||
+ //c == '\v';
+ }
+}
diff --git a/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
new file mode 100644
index 0000000..772b4f8
--- /dev/null
+++ b/src/test/java/com/github/manevolent/ts3j/ServerConnectionTest.java
@@ -0,0 +1,25 @@
+package com.github.manevolent.ts3j;
+
+import com.github.manevolent.ts3j.enums.ConnectionState;
+import junit.framework.TestCase;
+
+import java.net.InetSocketAddress;
+
+public class ServerConnectionTest extends TestCase {
+ public ServerConnectionTest(String name) {
+ super(name);
+ }
+
+ public void testParser() throws Exception {
+ TeamspeakClient client = new TeamspeakClient();
+
+ client.connect(new InetSocketAddress(
+ "voice.teamspeak.com",
+ 9987),
+ null,
+ 10000L
+ );
+
+ assertEquals(client.getConnectionState(), ConnectionState.CONNECTED);
+ }
+}