Beginning to work on audio stuff now. Lots of changes.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package com.github.manevolent.ts3j.audio;
|
||||
|
||||
public enum AudioEncoding {
|
||||
OPUS,
|
||||
SPEEX,
|
||||
PCM
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.manevolent.ts3j.audio;
|
||||
|
||||
public interface Microphone {
|
||||
default boolean isMuted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean isReady();
|
||||
|
||||
default AudioEncoding getEncoding() {
|
||||
return AudioEncoding.PCM;
|
||||
}
|
||||
|
||||
byte[] provide();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.github.manevolent.ts3j.event;
|
||||
|
||||
/*
|
||||
* #%L
|
||||
* TeamSpeak 3 Java API
|
||||
* %%
|
||||
* Copyright (C) 2014 Bert De Geyter
|
||||
* %%
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ClientChannelGroupChangedEvent extends BaseEvent {
|
||||
|
||||
public ClientChannelGroupChangedEvent(Map<String, String> map) {
|
||||
super(map);
|
||||
}
|
||||
|
||||
public int getClientId() {
|
||||
return getInt("clid");
|
||||
}
|
||||
|
||||
public int getChannelGroupId() {
|
||||
return getInt("cgid");
|
||||
}
|
||||
|
||||
public int getChannelId() {
|
||||
return getInt("cid");
|
||||
}
|
||||
|
||||
public int getInheritedChannelId() {
|
||||
return getInt("cgi");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(TS3Listener listener) {
|
||||
listener.onClientChannelGroupChanged(this);
|
||||
}
|
||||
}
|
||||
@@ -57,4 +57,7 @@ public interface TS3Listener {
|
||||
void onServerGroupList(ServerGroupListEvent e);
|
||||
|
||||
void onClientNeededPermissions(ClientNeededPermissionsEvent e);
|
||||
|
||||
void onClientChannelGroupChanged(ClientChannelGroupChangedEvent e);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.github.manevolent.ts3j.protocol;
|
||||
|
||||
public class RingQueue {
|
||||
private static final int
|
||||
NOT_SET = 0x00,
|
||||
SET = 0x01,
|
||||
OUT_OF_WINDOW = 0x00,
|
||||
IN_WINDOW = 0x10;
|
||||
|
||||
private boolean[] ringBufferSet;
|
||||
|
||||
private final int initialBufferSize = 16;
|
||||
private int currentStart, currentLength;
|
||||
|
||||
private int mappedBaseOffset;
|
||||
private final int mappedMod;
|
||||
private int generation;
|
||||
|
||||
private final int maxBufferSize;
|
||||
|
||||
public RingQueue(int maxBufferSize, int mod) {
|
||||
if (maxBufferSize == -1)
|
||||
{
|
||||
this.maxBufferSize = (mod / 2) - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (maxBufferSize >= mod)
|
||||
throw new IllegalArgumentException("Modulo must be bigger than buffer size");
|
||||
|
||||
this.maxBufferSize = maxBufferSize;
|
||||
}
|
||||
|
||||
int setBufferSize = Math.min(initialBufferSize, maxBufferSize);
|
||||
ringBufferSet = new boolean[setBufferSize];
|
||||
mappedMod = mod;
|
||||
|
||||
clear();
|
||||
}
|
||||
|
||||
private void bufferSet(int index) {
|
||||
bufferExtend(index);
|
||||
int local = indexToLocal(index);
|
||||
int newLength = local - currentStart + 1 + (local >= currentStart ? 0 : ringBufferSet.length);
|
||||
currentLength = Math.max(currentLength, newLength);
|
||||
ringBufferSet[local] = true;
|
||||
}
|
||||
|
||||
private boolean stateGet(int index) {
|
||||
bufferExtend(index);
|
||||
int local = indexToLocal(index);
|
||||
return ringBufferSet[local];
|
||||
}
|
||||
|
||||
private void bufferPop() {
|
||||
ringBufferSet[currentStart] = false;
|
||||
|
||||
currentStart = (currentStart + 1) % ringBufferSet.length;
|
||||
currentLength --;
|
||||
}
|
||||
|
||||
private void bufferExtend(int index) {
|
||||
if (index < ringBufferSet.length)
|
||||
return;
|
||||
if (index >= maxBufferSize)
|
||||
throw new IllegalArgumentException("the index does not fit into the maximal buffer size");
|
||||
int extendto = index < ringBufferSet.length * 2
|
||||
? Math.min(ringBufferSet.length * 2, maxBufferSize)
|
||||
: Math.min(index + ringBufferSet.length, maxBufferSize);
|
||||
|
||||
Object[] extRingBuffer = new Object[extendto];
|
||||
boolean[] extRingBufferSet = new boolean[extendto];
|
||||
|
||||
System.arraycopy(ringBufferSet, currentStart, extRingBufferSet, 0, ringBufferSet.length - currentStart);
|
||||
System.arraycopy(ringBufferSet, 0, extRingBufferSet, ringBufferSet.length - currentStart, currentStart);
|
||||
|
||||
currentStart = 0;
|
||||
ringBufferSet = extRingBufferSet;
|
||||
}
|
||||
|
||||
private int indexToLocal(int index) {
|
||||
return (currentStart + index) % ringBufferSet.length;
|
||||
}
|
||||
|
||||
public void clear()
|
||||
{
|
||||
currentStart = 0;
|
||||
currentLength = 0;
|
||||
for (int i = 0; i < ringBufferSet.length; i ++) ringBufferSet[i] = false;
|
||||
mappedBaseOffset = 0;
|
||||
generation = 0;
|
||||
}
|
||||
|
||||
public void set(int mappedValue) {
|
||||
int index = mappedToIndex(mappedValue);
|
||||
|
||||
if (isSetIndex(index) != ItemSetStatus.IN_WINDOW_NOT_SET)
|
||||
throw new IllegalStateException("object cannot be set");
|
||||
|
||||
bufferSet(index);
|
||||
}
|
||||
|
||||
private int mappedToIndex(int mappedValue) {
|
||||
if (mappedValue >= mappedMod)
|
||||
throw new ArrayIndexOutOfBoundsException("mappedValue");
|
||||
|
||||
if (isNextGen(mappedValue)) {
|
||||
return (mappedValue + mappedMod) - mappedBaseOffset;
|
||||
} else {
|
||||
return mappedValue - mappedBaseOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNextGen(int mappedValue) {
|
||||
return mappedBaseOffset > mappedMod - maxBufferSize && mappedValue < maxBufferSize;
|
||||
}
|
||||
|
||||
public int getGeneration(int mappedValue) {
|
||||
return generation + (isNextGen(mappedValue) ? 1 : 0);
|
||||
}
|
||||
|
||||
private ItemSetStatus isSetIndex(int index) {
|
||||
if (index < 0)
|
||||
return ItemSetStatus.OUT_OF_WINDOW_SET;
|
||||
else if (index > currentLength && index < maxBufferSize)
|
||||
return ItemSetStatus.IN_WINDOW_NOT_SET;
|
||||
else if (index >= maxBufferSize)
|
||||
return ItemSetStatus.OUT_OF_WINDOW_NOT_SET;
|
||||
|
||||
return stateGet(index) ? ItemSetStatus.IN_WINDOW_SET : ItemSetStatus.IN_WINDOW_NOT_SET;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return currentLength;
|
||||
}
|
||||
|
||||
public boolean isSet(int packetId) {
|
||||
return isSetIndex(packetId) == ItemSetStatus.IN_WINDOW_SET;
|
||||
}
|
||||
|
||||
public enum ItemSetStatus {
|
||||
OUT_OF_WINDOW_NOT_SET(0x00),
|
||||
OUT_OF_WINDOW_SET(0x01),
|
||||
IN_WINDOW_NOT_SET(0x10),
|
||||
IN_WINDOW_SET(0x11);
|
||||
|
||||
private final int index;
|
||||
|
||||
ItemSetStatus(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public static ItemSetStatus fromId(int index) {
|
||||
for (ItemSetStatus value : values())
|
||||
if (value.getIndex() == index) return value;
|
||||
|
||||
throw new IllegalArgumentException("invalid index: " + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.github.manevolent.ts3j.protocol.header.HeaderFlag;
|
||||
import com.github.manevolent.ts3j.protocol.header.PacketHeader;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class PacketBody5Pong extends PacketBody {
|
||||
private int packetId;
|
||||
|
||||
+6
-3
@@ -141,6 +141,8 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
|
||||
}
|
||||
} else if (packet.getBody() instanceof PacketBody2Command) {
|
||||
Ts3Debugging.debug(((PacketBody2Command) packet.getBody()).getText());
|
||||
|
||||
SingleCommand command = ((PacketBody2Command) packet.getBody()).parse().simplifyOne();
|
||||
|
||||
if (command.getName().equalsIgnoreCase("initivexpand2")) {
|
||||
@@ -186,18 +188,19 @@ public class LocalClientHandlerConnecting extends LocalClientHandler {
|
||||
|
||||
getClient().setSecureParameters(Ts3Crypt.cryptoInit2(license, alphaBytes, beta, keyPair.getValue()));
|
||||
|
||||
alphaBytes = null;
|
||||
|
||||
getClient().writePacket(new PacketBody2Command(
|
||||
ProtocolRole.CLIENT,
|
||||
new SingleCommand(
|
||||
"clientinit",
|
||||
ProtocolRole.CLIENT,
|
||||
new CommandSingleParameter("client_nickname", getClient().getNickname()),
|
||||
new CommandSingleParameter("client_version", "3.1.8 [Build: 1516614607]"),
|
||||
new CommandSingleParameter("client_version", "3.?.? [Build: 5680278000]"),
|
||||
new CommandSingleParameter("client_platform", "Windows"),
|
||||
new CommandSingleParameter(
|
||||
"client_version_sign",
|
||||
"LJ5q+KWT4KwBX7oR/9j9A12hBrq5ds5ony99" +
|
||||
"f9kepNmqFskhT7gfB51bAJNgAMOzXVCeaItNmc10F2wUNktqCw=="
|
||||
"DX5NIYLvfJEUjuIbCidnoeozxIDRRkpq3I9vVMBmE9L2qnekOoBzSenkzsg2lC9CMv8K5hkEzhr2TYUYSwUXCg=="
|
||||
),
|
||||
new CommandSingleParameter("client_input_hardware", "1"),
|
||||
new CommandSingleParameter("client_output_hardware", "1"),
|
||||
|
||||
+3
-6
@@ -119,7 +119,8 @@ public class PacketTransformation {
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen) {
|
||||
public byte[] decrypt(PacketHeader header, ByteBuffer buffer, int dataLen)
|
||||
throws InvalidCipherTextException {
|
||||
Pair<byte[], byte[]> parameters = computeParameters(header);
|
||||
|
||||
byte[] headerWithoutMac = new byte[header.getSize() - MAC_LEN];
|
||||
@@ -142,11 +143,7 @@ public class PacketTransformation {
|
||||
|
||||
len = cipher.processBytes(buffer.array(), header.getSize(), dataLen, result, 0);
|
||||
len += cipher.processBytes(buffer.array(), 0, MAC_LEN, result, len);
|
||||
try {
|
||||
len += cipher.doFinal(result, len);
|
||||
} catch (InvalidCipherTextException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
len += cipher.doFinal(result, len);
|
||||
|
||||
if (len != dataLen)
|
||||
throw new IllegalArgumentException(len + " != " + dataLen);
|
||||
|
||||
+218
-69
@@ -4,6 +4,7 @@ import com.github.manevolent.ts3j.command.*;
|
||||
import com.github.manevolent.ts3j.identity.Identity;
|
||||
import com.github.manevolent.ts3j.protocol.NetworkPacket;
|
||||
import com.github.manevolent.ts3j.protocol.Packet;
|
||||
import com.github.manevolent.ts3j.protocol.RingQueue;
|
||||
import com.github.manevolent.ts3j.protocol.SocketRole;
|
||||
import com.github.manevolent.ts3j.protocol.client.ClientConnectionState;
|
||||
import com.github.manevolent.ts3j.protocol.header.ClientPacketHeader;
|
||||
@@ -19,9 +20,11 @@ import com.github.manevolent.ts3j.util.QuickLZ;
|
||||
import com.github.manevolent.ts3j.util.Ts3Crypt;
|
||||
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||
import javafx.util.Pair;
|
||||
import org.bouncycastle.crypto.InvalidCipherTextException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.*;
|
||||
@@ -39,8 +42,10 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
private final Map<PacketBodyType, Reassembly> reassemblyQueue = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> clientOptions = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, PacketResponse> sendQueue = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, PacketResponse> sendQueueLow = new ConcurrentHashMap<>();
|
||||
|
||||
private final Map<PacketBodyType, PacketCounter> localSendCounter = new ConcurrentHashMap<>();
|
||||
private final Map<PacketBodyType, PacketCounter> remoteSendCounter = new ConcurrentHashMap<>();
|
||||
private final Map<PacketBodyType, RingQueue> remoteQueue = new ConcurrentHashMap<>();
|
||||
|
||||
private CommandProcessor commandProcessor;
|
||||
|
||||
@@ -53,6 +58,8 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
private int clientId = 0;
|
||||
private int packetId = 0;
|
||||
|
||||
private long lastPing;
|
||||
|
||||
private final NetworkReader networkReader = new NetworkReader();
|
||||
private final NetworkHandler networkHandler = new NetworkHandler();
|
||||
private Thread networkThread = null;
|
||||
@@ -75,14 +82,15 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
reassemblyQueue.put(bodyType, new Reassembly());
|
||||
|
||||
// Initialize counters
|
||||
for (PacketBodyType bodyType : PacketBodyType.values())
|
||||
for (PacketBodyType bodyType : PacketBodyType.values()) {
|
||||
if (bodyType == PacketBodyType.INIT1) {
|
||||
localSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero());
|
||||
remoteSendCounter.put(PacketBodyType.INIT1, new PacketCounterZero());
|
||||
localSendCounter.put(bodyType, new PacketCounterZero());
|
||||
} else {
|
||||
localSendCounter.put(bodyType, new PacketCounterFull());
|
||||
remoteSendCounter.put(bodyType, new PacketCounterFull(-1));
|
||||
localSendCounter.put(bodyType, new PacketCounterFull(true));
|
||||
}
|
||||
|
||||
remoteQueue.put(bodyType, new RingQueue(100, 65536));
|
||||
}
|
||||
}
|
||||
|
||||
protected void start() {
|
||||
@@ -104,7 +112,7 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
setTransformation(new PacketTransformation(parameters.getIvStruct(), parameters.getFakeSignature()));
|
||||
}
|
||||
|
||||
protected abstract NetworkPacket readNetworkPacket() throws IOException;
|
||||
protected abstract NetworkPacket readNetworkPacket(int timeout) throws IOException;
|
||||
|
||||
protected abstract void writeNetworkPacket(NetworkPacket packet) throws IOException;
|
||||
|
||||
@@ -166,21 +174,93 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
return connectionState == ClientConnectionState.CONNECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
||||
private PacketHeader newOutgoingHeader() {
|
||||
PacketHeader header;
|
||||
|
||||
try {
|
||||
header = body.getRole().getHeaderClass().newInstance();
|
||||
header = getRole().getOut().getHeaderClass().newInstance();
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writePacket(PacketBody body) throws IOException, TimeoutException {
|
||||
// Construct a network packet
|
||||
Packet packet = new Packet(body.getRole());
|
||||
packet.setHeader(header);
|
||||
packet.setBody(body);
|
||||
|
||||
// Generate header
|
||||
PacketHeader header = newOutgoingHeader();
|
||||
|
||||
// Ensure type matches
|
||||
header.setType(packet.getBody().getType());
|
||||
|
||||
// Set header values
|
||||
body.setHeaderValues(header);
|
||||
|
||||
if (header instanceof ClientPacketHeader)
|
||||
((ClientPacketHeader) header).setClientId(clientId);
|
||||
|
||||
// Count packet
|
||||
Pair<Integer, Integer> counter;
|
||||
|
||||
switch (header.getType()) {
|
||||
case ACK:
|
||||
counter = new Pair<>(
|
||||
((PacketBody6Ack)packet.getBody()).getPacketId(),
|
||||
remoteQueue.get(PacketBodyType.ACK).getGeneration(
|
||||
((PacketBody6Ack) packet.getBody()).getPacketId()
|
||||
)
|
||||
);
|
||||
break;
|
||||
case ACK_LOW:
|
||||
counter = new Pair<>(
|
||||
((PacketBody7AckLow)packet.getBody()).getPacketId(),
|
||||
remoteQueue.get(PacketBodyType.ACK_LOW).getGeneration(
|
||||
((PacketBody7AckLow) packet.getBody()).getPacketId()
|
||||
)
|
||||
);
|
||||
break;
|
||||
case PONG:
|
||||
counter = new Pair<>(
|
||||
((PacketBody5Pong)packet.getBody()).getPacketId(),
|
||||
remoteQueue.get(PacketBodyType.PONG).getGeneration(
|
||||
((PacketBody5Pong) packet.getBody()).getPacketId()
|
||||
)
|
||||
);
|
||||
break;
|
||||
/*case PING:
|
||||
counter = localSendCounter.get(PacketBodyType.PING).current();
|
||||
break;*/
|
||||
case INIT1:
|
||||
counter = new Pair<>(101, 0);
|
||||
break;
|
||||
default:
|
||||
counter = localSendCounter.get(header.getType()).next();
|
||||
break;
|
||||
}
|
||||
|
||||
header.setPacketId(counter.getKey());
|
||||
header.setGeneration(counter.getValue());
|
||||
|
||||
switch (header.getType()) {
|
||||
case INIT1:
|
||||
case PONG:
|
||||
case PING:
|
||||
// setHeaderValues will do this, too
|
||||
header.setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||
break;
|
||||
case COMMAND:
|
||||
case COMMAND_LOW:
|
||||
header.setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
|
||||
break;
|
||||
}
|
||||
|
||||
packet.setHeader(header);
|
||||
|
||||
writePacket(packet);
|
||||
}
|
||||
|
||||
@@ -193,30 +273,6 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
packet.getHeader().getRole().name() + " != " +
|
||||
getRole().getOut().name());
|
||||
|
||||
// Ensure type matches
|
||||
packet.getHeader().setType(packet.getBody().getType());
|
||||
|
||||
// Set header values
|
||||
packet.getBody().setHeaderValues(packet.getHeader());
|
||||
|
||||
Pair<Integer, Integer> counterResult = localSendCounter.get(packet.getHeader().getType()).next();
|
||||
if (packet.getHeader() instanceof ClientPacketHeader)
|
||||
((ClientPacketHeader) packet.getHeader()).setClientId(clientId);
|
||||
|
||||
packet.getHeader().setPacketId(counterResult.getKey());
|
||||
packet.getHeader().setGeneration(counterResult.getValue());
|
||||
|
||||
switch (packet.getHeader().getType()) {
|
||||
case INIT1:
|
||||
packet.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||
packet.getHeader().setPacketId((short)101);
|
||||
break;
|
||||
case COMMAND:
|
||||
case COMMAND_LOW:
|
||||
packet.getHeader().setPacketFlag(HeaderFlag.NEW_PROTOCOL, true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (packet.getSize() > 500) {
|
||||
int totalSize = packet.getBody().getSize();
|
||||
|
||||
@@ -274,6 +330,10 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
ByteBuffer outputBuffer;
|
||||
|
||||
if (!packet.getHeader().getPacketFlag(HeaderFlag.UNENCRYPTED)) {
|
||||
Ts3Debugging.debug("[PROTOCOL] ENCRYPT " +
|
||||
packet.getHeader().getType().name() + " generation=" +
|
||||
packet.getHeader().getGeneration());
|
||||
|
||||
outputBuffer = getTransformation().encrypt(packet);
|
||||
} else {
|
||||
if (packet.getHeader().getType() == PacketBodyType.INIT1) {
|
||||
@@ -315,7 +375,7 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
writeNetworkPacket(networkPacket);
|
||||
|
||||
// If necessary, fulfill promise for the packet acknowledgement or throw TimeoutException
|
||||
if (response != null) {
|
||||
if (response != null && networkPacket.getHeader().getType() != PacketBodyType.PING) {
|
||||
while (response.getRetries() < response.getMaxTries()) {
|
||||
try {
|
||||
if (getState() == ClientConnectionState.DISCONNECTED)
|
||||
@@ -344,6 +404,11 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
private Packet readPacketIntl(NetworkPacket networkPacket) throws IOException {
|
||||
if (networkPacket.getHeader() == null) throw new NullPointerException("header");
|
||||
|
||||
RingQueue ring = remoteQueue.get(networkPacket.getHeader().getType());
|
||||
int currentGeneration = ring == null ? 0 : ring.getGeneration(networkPacket.getHeader().getPacketId());
|
||||
|
||||
networkPacket.getHeader().setGeneration(currentGeneration);
|
||||
|
||||
Packet packet = new Packet(networkPacket.getHeader().getRole());
|
||||
packet.setHeader(networkPacket.getHeader());
|
||||
|
||||
@@ -366,18 +431,25 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
" is unencrypted, but must be encrypted"
|
||||
);
|
||||
|
||||
ByteBuffer packetBuffer =
|
||||
(ByteBuffer) networkPacket.getBuffer().position(networkPacket.getHeader().getSize());
|
||||
ByteBuffer packetBuffer = networkPacket.getBuffer();
|
||||
|
||||
// Decrypt
|
||||
if (encrypted) {
|
||||
packetBuffer = ByteBuffer.wrap(
|
||||
getTransformation().decrypt(
|
||||
networkPacket.getHeader(),
|
||||
packetBuffer,
|
||||
networkPacket.getDatagram().getLength() - networkPacket.getHeader().getSize()
|
||||
)
|
||||
);
|
||||
Ts3Debugging.debug("[PROTOCOL] DECRYPT " + networkPacket.getHeader().getType().name()
|
||||
+ " generation=" + currentGeneration);
|
||||
|
||||
try {
|
||||
packetBuffer = ByteBuffer.wrap(
|
||||
getTransformation().decrypt(
|
||||
networkPacket.getHeader(),
|
||||
packetBuffer,
|
||||
networkPacket.getDatagram().getLength() -
|
||||
networkPacket.getHeader().getSize()
|
||||
)
|
||||
).order(ByteOrder.BIG_ENDIAN);
|
||||
} catch (InvalidCipherTextException e) {
|
||||
throw new IOException("failed to decrypt " + networkPacket.getHeader().getType().name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fragment handling
|
||||
@@ -399,7 +471,9 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
if (packet.getBody() instanceof PacketBodyCompressed) {
|
||||
byte[] decompressed = QuickLZ.decompress(((PacketBodyCompressed) packet.getBody()).getCompressed());
|
||||
|
||||
packetBuffer = ByteBuffer.wrap(decompressed);
|
||||
packetBuffer = ByteBuffer
|
||||
.wrap(decompressed)
|
||||
.order(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
packet = new Packet(packet.getRole(), packet.getHeader());
|
||||
|
||||
@@ -413,43 +487,89 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
public Packet readPacket() throws IOException, TimeoutException {
|
||||
public Packet readPacket() throws
|
||||
IOException,
|
||||
TimeoutException {
|
||||
NetworkPacket networkPacket;
|
||||
long timeout;
|
||||
|
||||
while (isReading()) {
|
||||
Packet packet = readPacketIntl(readNetworkPacket());
|
||||
try {
|
||||
timeout = getState() == ClientConnectionState.CONNECTED ?
|
||||
Math.min(1000L, 1000L - (System.currentTimeMillis() - lastPing)) :
|
||||
Integer.MAX_VALUE;
|
||||
|
||||
if (timeout <= 0) throw new SocketTimeoutException();
|
||||
|
||||
networkPacket = readNetworkPacket((int) timeout);
|
||||
} catch (SocketTimeoutException e) {
|
||||
if (getState() == ClientConnectionState.CONNECTED) {
|
||||
writePacket(new PacketBody4Ping(getRole().getOut()));
|
||||
lastPing = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Packet packet = readPacketIntl(networkPacket);
|
||||
|
||||
// Find if the packet must be acknowledged
|
||||
PacketBodyType ackType = packet.getHeader().getType().getAcknolwedgedBy();
|
||||
PacketBodyType ackType = packet
|
||||
.getHeader()
|
||||
.getType()
|
||||
.getAcknolwedgedBy();
|
||||
|
||||
if (ackType != null) {
|
||||
switch (ackType) {
|
||||
case ACK:
|
||||
Ts3Debugging.debug("[PROTOCOL] Acknowledge " + packet + " " + packet.getHeader().getType().name()
|
||||
+ " id=" + packet.getHeader().getPacketId());
|
||||
writePacket(new PacketBody6Ack(getRole().getOut(), packet.getHeader().getPacketId()));
|
||||
break;
|
||||
case ACK_LOW:
|
||||
writePacket(new PacketBody7AckLow(getRole().getOut(), packet.getHeader().getPacketId()));
|
||||
break;
|
||||
case PONG:
|
||||
writePacket(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId()));
|
||||
Packet pong = new Packet(
|
||||
getRole().getOut(),
|
||||
newOutgoingHeader()
|
||||
);
|
||||
|
||||
if (pong.getHeader() instanceof ClientPacketHeader)
|
||||
((ClientPacketHeader) pong.getHeader()).setClientId(getClientId());
|
||||
|
||||
pong.getHeader().setPacketFlag(HeaderFlag.UNENCRYPTED, true);
|
||||
pong.getHeader().setPacketId(packet.getHeader().getPacketId());
|
||||
pong.setBody(new PacketBody5Pong(getRole().getOut(), packet.getHeader().getPacketId()));
|
||||
|
||||
writePacket(pong);
|
||||
|
||||
continue; // Don't pass pings to the parent
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.getHeader().getType().canResend() &&
|
||||
packet.getHeader().getType() != PacketBodyType.INIT1) {
|
||||
// Find if we already acknowledged this packet
|
||||
RingQueue ring = remoteQueue.get(packet.getHeader().getType());
|
||||
if (ring.isSet(packet.getHeader().getPacketId())) {
|
||||
Ts3Debugging.debug("[PROTOCOL] Ignoring handling resent packet: " +
|
||||
packet + " " +
|
||||
packet.getHeader().getType().name() + " id=" +
|
||||
packet.getHeader().getPacketId());
|
||||
|
||||
continue; // Drop the packet
|
||||
} else {
|
||||
ring.set(packet.getHeader().getPacketId());
|
||||
}
|
||||
}
|
||||
|
||||
// Find if the packet must be reassembled
|
||||
if (packet.getHeader().getType().isSplittable()) {
|
||||
packet = reassemblyQueue.get(packet.getHeader().getType()).put(packet);
|
||||
if (packet == null) continue;
|
||||
}
|
||||
|
||||
if (packet.getHeader().getType().canResend() && packet.getHeader().getType() != PacketBodyType.INIT1)
|
||||
// Find if we already acknowledged this packet
|
||||
if (!remoteSendCounter
|
||||
.get(packet.getHeader().getType())
|
||||
.setPacketId(packet.getHeader().getPacketId())) {
|
||||
Ts3Debugging.debug("[PROTOCOL] Ignoring handling resent packet: " +
|
||||
packet.getHeader().getType().name() + " id=" +
|
||||
packet.getHeader().getPacketId());
|
||||
continue; // Drop the packet
|
||||
}
|
||||
|
||||
// Find if the packet is itself an acknowledgement
|
||||
boolean handle;
|
||||
PacketResponse response;
|
||||
@@ -459,7 +579,11 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
handle = false;
|
||||
break;
|
||||
case ACK_LOW:
|
||||
response = sendQueue.get(((PacketBody7AckLow)packet.getBody()).getPacketId());
|
||||
response = sendQueueLow.get(((PacketBody7AckLow)packet.getBody()).getPacketId());
|
||||
handle = false;
|
||||
break;
|
||||
case PONG:
|
||||
response = null;
|
||||
handle = false;
|
||||
break;
|
||||
default:
|
||||
@@ -680,6 +804,11 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
int getPacketId();
|
||||
boolean setPacketId(int packetId);
|
||||
int getGenerationId();
|
||||
void setGenerationId(int i);
|
||||
|
||||
default Pair<Integer,Integer> current() {
|
||||
return new Pair<>(getPacketId(), getGenerationId());
|
||||
}
|
||||
}
|
||||
|
||||
private class PacketCounterZero implements PacketCounter {
|
||||
@@ -702,18 +831,26 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
public int getGenerationId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGenerationId(int i) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private class PacketCounterFull implements PacketCounter {
|
||||
private final Object sendLock = new Object();
|
||||
private final boolean counting;
|
||||
private int packetId = 0;
|
||||
private int generationId = 0;
|
||||
|
||||
public PacketCounterFull() {
|
||||
|
||||
public PacketCounterFull(boolean counting) {
|
||||
this.counting = counting;
|
||||
}
|
||||
|
||||
public PacketCounterFull(int start) {
|
||||
public PacketCounterFull(int start, boolean counting) {
|
||||
this.counting = counting;
|
||||
|
||||
setPacketId(start);
|
||||
}
|
||||
|
||||
@@ -727,8 +864,8 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
if (this.packetId == packetId) return false;
|
||||
|
||||
if (packetId >= 65536) {
|
||||
packetId = 0;
|
||||
generationId++;
|
||||
this.packetId = 0; // wrap around
|
||||
if (isCounting()) setGenerationId(getGenerationId() + 1);
|
||||
} else {
|
||||
this.packetId = packetId;
|
||||
}
|
||||
@@ -741,13 +878,25 @@ public abstract class AbstractTeamspeakClientSocket
|
||||
return generationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGenerationId(int i) {
|
||||
synchronized (sendLock) {
|
||||
Ts3Debugging.debug("setGenerationId: " + i);
|
||||
this.generationId = i;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> next() {
|
||||
synchronized (sendLock) {
|
||||
setPacketId(packetId + 1);
|
||||
return new Pair<>(packetId, generationId);
|
||||
setPacketId(getPacketId() + 1);
|
||||
return current();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCounting() {
|
||||
return counting;
|
||||
}
|
||||
}
|
||||
|
||||
private class Reassembly {
|
||||
|
||||
+13
-7
@@ -16,10 +16,7 @@ import com.github.manevolent.ts3j.protocol.packet.PacketBody2Command;
|
||||
import com.github.manevolent.ts3j.util.Ts3Debugging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.*;
|
||||
@@ -67,10 +64,12 @@ public class LocalTeamspeakClientSocket
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NetworkPacket readNetworkPacket() throws IOException {
|
||||
socket.receive(packet);
|
||||
protected NetworkPacket readNetworkPacket(int timeout) throws
|
||||
IOException,
|
||||
SocketTimeoutException {
|
||||
|
||||
Ts3Debugging.debug("[NETWORK] READ len=" + packet.getLength() + " from " + packet.getSocketAddress());
|
||||
socket.setSoTimeout(timeout);
|
||||
socket.receive(packet);
|
||||
|
||||
PacketHeader header;
|
||||
|
||||
@@ -86,6 +85,11 @@ public class LocalTeamspeakClientSocket
|
||||
|
||||
header.read(buffer);
|
||||
|
||||
Ts3Debugging.debug("[NETWORK] READ" +
|
||||
" id=" + header.getPacketId() +
|
||||
" len=" + packet.getLength() +
|
||||
" from " + packet.getSocketAddress());
|
||||
|
||||
return new NetworkPacket(
|
||||
packet,
|
||||
header,
|
||||
@@ -325,6 +329,8 @@ public class LocalTeamspeakClientSocket
|
||||
public void process(AbstractTeamspeakClientSocket client, SingleCommand singleCommand)
|
||||
throws CommandProcessException {
|
||||
try {
|
||||
setClientId(Integer.parseInt(singleCommand.get("aclid").getValue()));
|
||||
|
||||
setState(ClientConnectionState.CONNECTED);
|
||||
} catch (Exception e) {
|
||||
throw new CommandProcessException(e);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of Discord4J.
|
||||
*
|
||||
* Discord4J is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Discord4J is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Discord4J. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.github.manevolent.ts3j.util;
|
||||
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
/**
|
||||
* Custom Thread that executes the passed task every specified period.
|
||||
*
|
||||
* Time of execution is held in account when calculating the time to sleep until the next execution.
|
||||
*/
|
||||
public class HighPrecisionRecurrentTask extends Thread {
|
||||
|
||||
private final int periodInNanos;
|
||||
private final int spinningTimeNanos;
|
||||
private final Runnable target;
|
||||
private volatile boolean stop;
|
||||
|
||||
/**
|
||||
* @param periodInMilis The period in milliseconds of the task execution.
|
||||
* @param percentageOfSleepSpinning The percentage of total sleeping that should be done spinning to increase accuracy.
|
||||
* @param target The task to run.
|
||||
*/
|
||||
public HighPrecisionRecurrentTask(int periodInMilis, float percentageOfSleepSpinning, Runnable target) {
|
||||
super("TS3J AudioThread");
|
||||
if (percentageOfSleepSpinning < 0 || percentageOfSleepSpinning > 1)
|
||||
throw new IllegalArgumentException("percentageOfSleepSpinning < 0 | percentageOfSleepSpinning > 1");
|
||||
this.periodInNanos = periodInMilis * 1_000_000;
|
||||
this.spinningTimeNanos = (int) (periodInNanos * percentageOfSleepSpinning);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public boolean isStop() {
|
||||
return stop;
|
||||
}
|
||||
|
||||
public void setStop(boolean stop) {
|
||||
this.stop = stop;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings(value = "empty-statement")
|
||||
public void run() {
|
||||
long nextTarget = 0;
|
||||
while (!stop) {
|
||||
long now = System.nanoTime();
|
||||
target.run();
|
||||
long total = System.nanoTime() - now;
|
||||
nextTarget = now + periodInNanos - total;
|
||||
sleepFor(periodInNanos - total - spinningTimeNanos);
|
||||
while (nextTarget > System.nanoTime()) {
|
||||
; //consume cycles
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void sleepFor(long nanos) {
|
||||
if (nanos > 0) {
|
||||
long elapsed = 0;
|
||||
while (elapsed < nanos) {
|
||||
long t0 = System.nanoTime();
|
||||
LockSupport.parkNanos(nanos - elapsed);
|
||||
elapsed += System.nanoTime() - t0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public final class QuickLZ
|
||||
int level = (source[0] >>> 2) & 0x3;
|
||||
|
||||
if (level != 1 && level != 3)
|
||||
throw new RuntimeException("Java version only supports level 1 and 3");
|
||||
return source;
|
||||
|
||||
if ((source[0] & 1) != 1)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
package com.github.manevolent.ts3j.util;
|
||||
|
||||
public final class Ts3Debugging {
|
||||
private static boolean enabled;
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public static void setEnabled(boolean enabled) {
|
||||
Ts3Debugging.enabled = enabled;
|
||||
}
|
||||
|
||||
public static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
@@ -25,19 +35,23 @@ public final class Ts3Debugging {
|
||||
}
|
||||
|
||||
public static void info(String message) {
|
||||
System.err.println("[INFO] " + message);
|
||||
if (isEnabled()) {
|
||||
System.err.println("[INFO] " + message);
|
||||
}
|
||||
}
|
||||
|
||||
public static void debug(Object message) {
|
||||
System.err.println("[DEBUG] " + message.toString());
|
||||
if (isEnabled()) System.err.println("[DEBUG] " + message.toString());
|
||||
}
|
||||
|
||||
public static void debug(byte[] message) {
|
||||
debug(getHex(message));
|
||||
if (isEnabled()) debug(getHex(message));
|
||||
}
|
||||
|
||||
public static void debug(String message, Throwable ex) {
|
||||
System.err.println("[DEBUG] " + message);
|
||||
ex.printStackTrace();
|
||||
if (isEnabled()) {
|
||||
System.err.println("[DEBUG] " + message);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user