Update networking to include udp reset on drop packet

This commit is contained in:
Retera 2021-06-28 23:39:30 -04:00
parent 83fc753a0c
commit 08fd06a882
19 changed files with 322 additions and 42 deletions

View File

@ -53,7 +53,7 @@ import com.etheller.warsmash.viewer5.handlers.w3x.ui.MeleeUI;
public class WarsmashGdxMapScreen implements InputProcessor, Screen {
public static final boolean ENABLE_AUDIO = true;
private static final boolean ENABLE_MUSIC = true;
private static final boolean ENABLE_MUSIC = false;
private final War3MapViewer viewer;
private final Rectangle tempRect = new Rectangle();

View File

@ -21,4 +21,6 @@ public interface ClientToServerListener {
void finishedTurn(SocketAddress sourceAddress, int gameTurnTick);
void framesSkipped(int nFramesSkipped);
}

View File

@ -9,4 +9,5 @@ public class ClientToServerProtocol {
public static final int UNIT_CANCEL_TRAINING = 5;
public static final int FINISHED_TURN = 6;
public static final int JOIN_GAME = 7;
public static final int FRAMES_SKIPPED = 8;
}

View File

@ -5,6 +5,8 @@ public interface GameTurnManager {
void turnCompleted(int gameTurnTick);
void framesSkipped(float skippedCount);
GameTurnManager PAUSED = new GameTurnManager() {
@Override
public int getLatestCompletedTurn() {
@ -15,6 +17,10 @@ public interface GameTurnManager {
public void turnCompleted(final int gameTurnTick) {
System.err.println("got turnCompleted(" + gameTurnTick + ") while paused !!");
}
@Override
public void framesSkipped(final float skippedCount) {
}
};
GameTurnManager LOCAL = new GameTurnManager() {
@ -26,5 +32,10 @@ public interface GameTurnManager {
@Override
public void turnCompleted(final int gameTurnTick) {
}
@Override
public void framesSkipped(final float skippedCount) {
}
};
}

View File

@ -7,13 +7,13 @@ import java.util.HashMap;
import java.util.Map;
import com.badlogic.gdx.Gdx;
import com.etheller.warsmash.networking.udp.UdpClient;
import com.etheller.warsmash.networking.udp.OrderedUdpClient;
import com.etheller.warsmash.util.WarsmashConstants;
import com.etheller.warsmash.viewer5.handlers.w3x.War3MapViewer;
import com.etheller.warsmash.viewer5.handlers.w3x.simulation.players.CPlayerUnitOrderExecutor;
public class WarsmashClient implements ServerToClientListener, GameTurnManager {
private final UdpClient udpClient;
private final OrderedUdpClient udpClient;
private final War3MapViewer game;
private final Map<Integer, CPlayerUnitOrderExecutor> indexToExecutor = new HashMap<>();
private int latestCompletedTurn = -1;
@ -21,7 +21,8 @@ public class WarsmashClient implements ServerToClientListener, GameTurnManager {
public WarsmashClient(final InetAddress serverAddress, final War3MapViewer game)
throws UnknownHostException, IOException {
this.udpClient = new UdpClient(serverAddress, WarsmashConstants.PORT_NUMBER, new WarsmashClientParser(this));
this.udpClient = new OrderedUdpClient(serverAddress, WarsmashConstants.PORT_NUMBER,
new WarsmashClientParser(this));
this.game = game;
this.writer = new WarsmashClientWriter(this.udpClient);
}
@ -131,12 +132,14 @@ public class WarsmashClient implements ServerToClientListener, GameTurnManager {
}
@Override
public int getLatestCompletedTurn() {
return this.latestCompletedTurn;
public void framesSkipped(final float skippedCount) {
this.writer.framesSkipped((int) skippedCount);
this.writer.send();
}
public UdpClient getUdpClient() {
return this.udpClient;
@Override
public int getLatestCompletedTurn() {
return this.latestCompletedTurn;
}
public WarsmashClientWriter getWriter() {

View File

@ -2,15 +2,20 @@ package com.etheller.warsmash.networking;
import java.nio.ByteBuffer;
import com.etheller.warsmash.networking.udp.UdpClientListener;
import com.etheller.warsmash.networking.udp.OrderedUdpClientListener;
public class WarsmashClientParser implements UdpClientListener {
public class WarsmashClientParser implements OrderedUdpClientListener {
private final ServerToClientListener listener;
public WarsmashClientParser(final ServerToClientListener listener) {
this.listener = listener;
}
@Override
public void cantReplay(final int seqNo) {
throw new IllegalStateException("Cant replay seqNo=" + seqNo + " !");
}
@Override
public void parse(final ByteBuffer buffer) {
final int initialLimit = buffer.limit();
@ -18,8 +23,9 @@ public class WarsmashClientParser implements UdpClientListener {
while (buffer.hasRemaining()) {
final int length = buffer.getInt();
if (length > buffer.remaining()) {
throw new IllegalStateException(
"Got mismatched protocol length " + length + " > " + buffer.remaining() + "!!");
// this packet is junk to us, so we will skip and continue (drop system will
// handle it)
System.err.println("Got mismatched protocol length " + length + " > " + buffer.remaining() + "!!");
}
final int protocol = buffer.getInt();
switch (protocol) {

View File

@ -4,13 +4,13 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.etheller.warsmash.networking.udp.UdpClient;
import com.etheller.warsmash.networking.udp.OrderedUdpClient;
public class WarsmashClientWriter {
private final UdpClient client;
private final OrderedUdpClient client;
private final ByteBuffer sendBuffer = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN);
public WarsmashClientWriter(final UdpClient client) {
public WarsmashClientWriter(final OrderedUdpClient client) {
this.client = client;
}
@ -79,6 +79,13 @@ public class WarsmashClientWriter {
this.sendBuffer.putInt(gameTurnTick);
}
public void framesSkipped(final int skippedCount) {
this.sendBuffer.clear();
this.sendBuffer.putInt(4 + 4);
this.sendBuffer.putInt(ClientToServerProtocol.FRAMES_SKIPPED);
this.sendBuffer.putInt(skippedCount);
}
public void joinGame() {
this.sendBuffer.clear();
this.sendBuffer.putInt(4);
@ -87,7 +94,6 @@ public class WarsmashClientWriter {
public void send() {
this.sendBuffer.flip();
System.out.println("CLIENT WRITER calling send() on " + this.sendBuffer.remaining() + " bytes");
try {
this.client.send(this.sendBuffer);
}

View File

@ -10,11 +10,11 @@ import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import com.etheller.warsmash.networking.udp.UdpServer;
import com.etheller.warsmash.networking.udp.OrderedUdpServer;
import com.etheller.warsmash.util.WarsmashConstants;
public class WarsmashServer implements ClientToServerListener {
private final UdpServer udpServer;
private final OrderedUdpServer udpServer;
private final Map<SocketAddress, Integer> socketAddressToPlayerIndex = new HashMap<>();
private final Set<SocketAddress> clientsAwaitingTurnFinished = new HashSet<>();
private final List<Runnable> turnActions = new ArrayList<>();
@ -23,7 +23,7 @@ public class WarsmashServer implements ClientToServerListener {
private boolean gameStarted = false;
public WarsmashServer() throws IOException {
this.udpServer = new UdpServer(WarsmashConstants.PORT_NUMBER, new WarsmashServerParser(this));
this.udpServer = new OrderedUdpServer(WarsmashConstants.PORT_NUMBER, new WarsmashServerParser(this));
this.writer = new WarsmashServerWriter(this.udpServer, this.socketAddressToPlayerIndex.keySet());
}
@ -139,7 +139,7 @@ public class WarsmashServer implements ClientToServerListener {
@Override
public void finishedTurn(final SocketAddress sourceAddress, final int gameTurnTick) {
System.out.println("finishedTurn(" + gameTurnTick + ") from " + sourceAddress);
// System.out.println("finishedTurn(" + gameTurnTick + ") from " + sourceAddress);
if (!this.gameStarted) {
throw new IllegalStateException(
"Client should not send us finishedTurn() message when game has not started!");
@ -160,6 +160,11 @@ public class WarsmashServer implements ClientToServerListener {
}
}
@Override
public void framesSkipped(final int nFramesSkipped) {
// dont care for now
}
public static void main(final String[] args) {
try {
final WarsmashServer server = new WarsmashServer();

View File

@ -4,9 +4,9 @@ import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import com.etheller.warsmash.networking.udp.UdpServerListener;
import com.etheller.warsmash.networking.udp.OrderedUdpServerListener;
public class WarsmashServerParser implements UdpServerListener {
public class WarsmashServerParser implements OrderedUdpServerListener {
private final ClientToServerListener listener;
@ -21,8 +21,10 @@ public class WarsmashServerParser implements UdpServerListener {
while (buffer.hasRemaining()) {
final int length = buffer.getInt();
if (length > buffer.remaining()) {
throw new IllegalStateException(
"Got mismatched protocol length " + length + " > " + buffer.remaining() + "!!");
// this packet is junk to us, so we will skip and continue (drop system will
// handle it)
System.err.println("Got mismatched protocol length " + length + " > " + buffer.remaining() + "!!");
break;
}
final int protocol = buffer.getInt();
switch (protocol) {
@ -81,6 +83,11 @@ public class WarsmashServerParser implements UdpServerListener {
this.listener.joinGame(sourceAddress);
break;
}
case ClientToServerProtocol.FRAMES_SKIPPED: {
final int nFramesSkipped = buffer.getInt();
this.listener.framesSkipped(nFramesSkipped);
break;
}
default:
System.err.println("Got unknown protocol: " + protocol);
@ -92,4 +99,9 @@ public class WarsmashServerParser implements UdpServerListener {
buffer.position(initialLimit);
}
}
@Override
public void cantReplay(final SocketAddress sourceAddress, final int seqNo) {
throw new IllegalStateException("Cant replay " + seqNo + " to " + sourceAddress + " !");
}
}

View File

@ -6,14 +6,14 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Set;
import com.etheller.warsmash.networking.udp.UdpServer;
import com.etheller.warsmash.networking.udp.OrderedUdpServer;
public class WarsmashServerWriter implements ServerToClientListener {
private final UdpServer server;
private final OrderedUdpServer server;
private final ByteBuffer sendBuffer = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN);
private final Set<SocketAddress> allKnownAddressesToSend;
public WarsmashServerWriter(final UdpServer server, final Set<SocketAddress> allKnownAddressesToSend) {
public WarsmashServerWriter(final OrderedUdpServer server, final Set<SocketAddress> allKnownAddressesToSend) {
this.server = server;
this.allKnownAddressesToSend = allKnownAddressesToSend;
}

View File

@ -0,0 +1,31 @@
package com.etheller.warsmash.networking.udp;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
public class OrderedUdpClient extends OrderedUdpCommuncation implements Runnable {
private final UdpClient udpClient;
public OrderedUdpClient(final InetAddress serverAddress, final int portNumber,
final OrderedUdpClientListener listener) throws UnknownHostException, IOException {
super(listener);
this.udpClient = new UdpClient(serverAddress, portNumber, this);
}
@Override
protected void trySend(final ByteBuffer data) {
try {
this.udpClient.send(data);
}
catch (final IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
this.udpClient.run();
}
}

View File

@ -0,0 +1,5 @@
package com.etheller.warsmash.networking.udp;
public interface OrderedUdpClientListener extends UdpClientListener {
void cantReplay(int seqNo);
}

View File

@ -0,0 +1,107 @@
package com.etheller.warsmash.networking.udp;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
public abstract class OrderedUdpCommuncation implements UdpClientListener {
private static final int MAX_STORED_SENT_DATA_SIZE = 10000;
private static final int ORDERED_UDP_MESSAGE = 'M';
private static final int ORDERED_UDP_REPLAY_REQUEST = 'R';
private final Map<Integer, ByteBuffer> seqNoToDataSent;
private final Map<Integer, ByteBuffer> seqNoToDataReceived;
private final Queue<Integer> seqNosStored;
private int nextSendSeqNo;
private int nextReceiveSeqNo;
private final OrderedUdpClientListener delegate;
private final ByteBuffer sendBuffer;
public OrderedUdpCommuncation(final OrderedUdpClientListener delegate) {
this.seqNoToDataSent = new HashMap<>();
this.seqNoToDataReceived = new HashMap<>();
this.seqNosStored = new ArrayDeque<>();
this.sendBuffer = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN);
this.delegate = delegate;
}
public void send(final ByteBuffer data) throws IOException {
final ByteBuffer writeBuffer = ByteBuffer.allocate(1024).order(ByteOrder.BIG_ENDIAN);
writeBuffer.clear();
final Integer seqNo = this.nextSendSeqNo; // only autobox once, would be ideal to not box at all
writeBuffer.putInt(ORDERED_UDP_MESSAGE);
writeBuffer.putInt(seqNo);
writeBuffer.put(data);
writeBuffer.flip();
final int position = writeBuffer.position();
trySend(writeBuffer);
writeBuffer.position(position);
if (this.seqNosStored.size() == MAX_STORED_SENT_DATA_SIZE) {
this.seqNoToDataSent.remove(this.seqNosStored.poll());
}
this.seqNosStored.offer(seqNo);
this.seqNoToDataSent.put(seqNo, writeBuffer);
this.nextSendSeqNo++;
}
// it's udp so we're just trying, we don't really know if it'll drop or not
protected abstract void trySend(final ByteBuffer data);
@Override
public void parse(ByteBuffer readBuffer) {
final int messageType = readBuffer.getInt();
switch (messageType) {
case ORDERED_UDP_MESSAGE: {
final int serverSeqNo = readBuffer.getInt();
if (serverSeqNo > this.nextReceiveSeqNo) {
// ahead, need to queue it and request replay
for (int i = this.nextReceiveSeqNo; i < serverSeqNo; i++) {
requestReplay(i);
}
final ByteBuffer queuedReceivedData = ByteBuffer.allocate(readBuffer.remaining())
.order(ByteOrder.BIG_ENDIAN);
queuedReceivedData.put(readBuffer);
this.seqNoToDataReceived.put(serverSeqNo, queuedReceivedData);
}
else if (serverSeqNo < this.nextReceiveSeqNo) {
// dup, ignore
}
else {
// exactly equal
do {
this.delegate.parse(readBuffer);
this.nextReceiveSeqNo++;
}
while ((readBuffer = this.seqNoToDataReceived.remove(this.nextReceiveSeqNo)) != null);
}
break;
}
case ORDERED_UDP_REPLAY_REQUEST:
final int requestedSeqNo = readBuffer.getInt();
final ByteBuffer replayData = this.seqNoToDataSent.get(requestedSeqNo);
if (replayData == null) {
this.delegate.cantReplay(requestedSeqNo);
}
else {
System.err.println("Replay requested for packet with seqNo=" + requestedSeqNo + ", we will send it!");
final int position = replayData.position();
trySend(replayData);
replayData.position(position);
}
break;
}
}
private void requestReplay(final int i) {
this.sendBuffer.clear();
this.sendBuffer.putInt(ORDERED_UDP_REPLAY_REQUEST);
this.sendBuffer.putInt(i);
this.sendBuffer.flip();
trySend(this.sendBuffer);
}
}

View File

@ -0,0 +1,81 @@
package com.etheller.warsmash.networking.udp;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
public class OrderedUdpServer implements UdpServerListener, Runnable {
private final OrderedUdpServerListener listener;
private final UdpServer udpServer;
private final Map<SocketAddress, OrderedKnownClient> addrToClient = new HashMap<>();
public OrderedUdpServer(final int port, final OrderedUdpServerListener listener) throws IOException {
this.listener = listener;
this.udpServer = new UdpServer(port, this);
}
@Override
public void parse(final SocketAddress sourceAddress, final ByteBuffer buffer) {
getClient(sourceAddress).parse(buffer);
}
public void send(final SocketAddress destination, final ByteBuffer buffer) throws IOException {
getClient(destination).send(buffer);
}
private OrderedKnownClient getClient(final SocketAddress sourceAddress) {
OrderedKnownClient orderedKnownClient = this.addrToClient.get(sourceAddress);
if (orderedKnownClient == null) {
orderedKnownClient = new OrderedKnownClient(sourceAddress, new OrderedAddressedSender(sourceAddress));
this.addrToClient.put(sourceAddress, orderedKnownClient);
}
return orderedKnownClient;
}
private class OrderedKnownClient extends OrderedUdpCommuncation {
private final SocketAddress sourceAddress;
public OrderedKnownClient(final SocketAddress sourceAddress, final OrderedUdpClientListener listener) {
super(listener);
this.sourceAddress = sourceAddress;
}
@Override
protected void trySend(final ByteBuffer data) {
try {
OrderedUdpServer.this.udpServer.send(this.sourceAddress, data);
}
catch (final IOException e) {
throw new RuntimeException(e);
}
}
}
private class OrderedAddressedSender implements OrderedUdpClientListener {
private final SocketAddress sourceAddress;
public OrderedAddressedSender(final SocketAddress sourceAddress) {
this.sourceAddress = sourceAddress;
}
@Override
public void parse(final ByteBuffer buffer) {
OrderedUdpServer.this.listener.parse(this.sourceAddress, buffer);
}
@Override
public void cantReplay(final int seqNo) {
OrderedUdpServer.this.listener.cantReplay(this.sourceAddress, seqNo);
}
}
@Override
public void run() {
this.udpServer.run();
}
}

View File

@ -0,0 +1,7 @@
package com.etheller.warsmash.networking.udp;
import java.net.SocketAddress;
public interface OrderedUdpServerListener extends UdpServerListener {
void cantReplay(SocketAddress sourceAddress, int seqNo);
}

View File

@ -12,7 +12,7 @@ import com.etheller.warsmash.util.WarsmashConstants;
public class UdpClient implements Runnable {
private final DatagramChannel channel;
private final ByteBuffer buffer;
private final ByteBuffer readBuffer;
private boolean running;
private final UdpClientListener clientListener;
@ -20,9 +20,9 @@ public class UdpClient implements Runnable {
throws UnknownHostException, IOException {
this.channel = DatagramChannel.open().connect(new InetSocketAddress(serverAddress, portNumber));
this.channel.configureBlocking(true);
this.buffer = ByteBuffer.allocate(1024);
this.readBuffer = ByteBuffer.allocate(1024);
this.clientListener = clientListener;
this.buffer.order(ByteOrder.BIG_ENDIAN);
this.readBuffer.order(ByteOrder.BIG_ENDIAN);
}
public void send(final ByteBuffer data) throws IOException {
@ -38,10 +38,10 @@ public class UdpClient implements Runnable {
this.running = true;
while (this.running) {
try {
this.buffer.clear();
this.channel.receive(this.buffer);
this.buffer.flip();
this.clientListener.parse(this.buffer);
this.readBuffer.clear();
this.channel.receive(this.readBuffer);
this.readBuffer.flip();
this.clientListener.parse(this.readBuffer);
}
catch (final IOException e) {
System.err.println("Error reading from channel:");

View File

@ -35,10 +35,6 @@ public class UdpServer implements Runnable {
this.channel.send(buffer, destination);
}
public void send(final ByteBuffer buffer) throws IOException {
this.channel.write(buffer);
}
@Override
public void run() {
this.running = true;

View File

@ -683,7 +683,8 @@ public class War3MapViewer extends AbstractMdxModelViewer {
final UnitSound constructingBuilding = War3MapViewer.this.uiSounds
.getSound(War3MapViewer.this.gameUI.getSkinField("JobDoneSound"));
final RenderUnit renderUnit = War3MapViewer.this.unitToRenderPeer.get(constructedStructure);
if (constructingBuilding != null) {
if ((constructingBuilding != null) && (renderUnit.getSimulationUnit()
.getPlayerIndex() == War3MapViewer.this.localPlayerIndex)) {
constructingBuilding.play(War3MapViewer.this.worldScene.audioContext,
constructedStructure.getX(), constructedStructure.getY(), renderUnit.getZ());
}
@ -1127,13 +1128,14 @@ public class War3MapViewer extends AbstractMdxModelViewer {
final int playerIndex = unit.getPlayer();
final int customTeamColor = unit.getCustomTeamColor();
final float unitAngle = unit.getAngle();
int editorConfigHitPointPercent = unit.getHitpoints();
final int editorConfigHitPointPercent = unit.getHitpoints();
final CUnit unitCreated = createNewUnit(modifications, unitId, unitX, unitY, unitZ, playerIndex,
customTeamColor, unitAngle);
if(unitCreated != null) {
if (unitCreated != null) {
if (editorConfigHitPointPercent > 0) {
unitCreated.setLife(simulation, unitCreated.getMaximumLife() * (editorConfigHitPointPercent / 100f));
unitCreated.setLife(this.simulation,
unitCreated.getMaximumLife() * (editorConfigHitPointPercent / 100f));
}
if (unit.getGoldAmount() != 0) {
unitCreated.setGold(unit.getGoldAmount());
@ -1495,6 +1497,10 @@ public class War3MapViewer extends AbstractMdxModelViewer {
this.gameTurnManager.turnCompleted(this.simulation.getGameTurnTick());
}
else {
if (this.updateTime > (WarsmashConstants.SIMULATION_STEP_TIME * 3)) {
this.gameTurnManager.framesSkipped(this.updateTime / WarsmashConstants.SIMULATION_STEP_TIME);
this.updateTime = 0;
}
break;
}
}

View File

@ -2646,6 +2646,7 @@ public class MeleeUI implements CUnitStateListener, CommandButtonListener, Comma
rallied |= ability instanceof CAbilityRally;
attacked |= ability instanceof CAbilityAttack;
ordered = true;
break;
}
}