Skip to content

Commit

Permalink
feat(sdk): simplify replay loader
Browse files Browse the repository at this point in the history
No need to be a full-blown XStreamClient
  • Loading branch information
xeruf committed Jun 5, 2024
1 parent ff9fc40 commit 4973344
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 81 deletions.
65 changes: 65 additions & 0 deletions sdk/src/main/server-api/sc/framework/ReplayLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package sc.framework

import org.slf4j.LoggerFactory
import sc.api.plugins.IGameState
import sc.networking.XStreamProvider
import sc.protocol.room.MementoMessage
import sc.protocol.room.RoomPacket
import sc.shared.GameResult
import java.io.EOFException
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.zip.GZIPInputStream

/**
* Loads game information from any XML file (usually a replay).
*
* Only usable once!
*/
class ReplayLoader(inputStream: InputStream) {
constructor(file: File): this(
if(file.extension == "gz")
GZIPInputStream(file.inputStream())
else
file.inputStream()
)

val stream = XStreamProvider.loadPluginXStream().createObjectInputStream(inputStream)

fun loadHistory(exitCondition: (IGameState) -> Boolean = { false }): Pair<List<IGameState>, GameResult?> {
val history: MutableList<IGameState> = ArrayList(50)
var result: GameResult? = null
try {
while(true) {
val message = stream.readObject()
logger.trace("Adding packet to replay: {}", message)
if(message !is RoomPacket)
throw IOException("Can't extract replay from $message")
when(val msg = message.data) {
is MementoMessage -> {
history.add(msg.state)
if(exitCondition(msg.state))
break
}

is GameResult -> result = msg
else -> logger.warn("Unknown message in replay: {}", msg)
}
}
} catch(e: EOFException) {
logger.info("Replay fully loaded")
} finally {
stream.close()
}
return history to result
}

fun getTurn(turn: Int): IGameState =
loadHistory { it.turn >= turn }.first.last().takeIf { it.turn >= turn }
?: throw NoSuchElementException("No GameState of turn $turn")

companion object {
private val logger = LoggerFactory.getLogger(this::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ public class FileSystemInterface implements INetworkInterface {
private final InputStream inputStream;
private final OutputStream outputStream;

public FileSystemInterface(File file) throws FileNotFoundException {
this.inputStream = new FileInputStream(file);
this.outputStream = new NullOutputStream(true);
}

public FileSystemInterface(InputStream in) {
this.inputStream = in;
this.outputStream = new NullOutputStream(true);
}

public FileSystemInterface(File file) throws FileNotFoundException {
this(new FileInputStream(file));
}

@Override
public void close() throws IOException {
this.inputStream.close();
Expand Down
55 changes: 0 additions & 55 deletions sdk/src/main/server-api/sc/networking/clients/GameLoaderClient.kt

This file was deleted.

14 changes: 6 additions & 8 deletions sdk/src/main/server-api/sc/networking/clients/XStreamClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,6 @@ public void receiveThread() {
throw new ClassNotFoundException("Received object of unknown class " + object.getClass().getName());
}
}
} catch (EOFException e) {
logger.info("End of input reached, disconnecting {}", this);
logger.trace("Disconnected with", e);
} catch (IOException e) {
// The other side closed the connection.
// It is better when the other side sends a CloseConnection message beforehand,
Expand Down Expand Up @@ -191,8 +188,10 @@ protected synchronized void sendObject(Object packet) {
this.out.writeObject(packet);
this.out.flush();
} catch (XStreamException e) {
stopReceiver();
handleDisconnect(DisconnectCause.PROTOCOL_ERROR, e);
} catch (IOException e) {
stopReceiver();
handleDisconnect(DisconnectCause.LOST_CONNECTION, e);
}
}
Expand All @@ -203,7 +202,7 @@ protected final void handleDisconnect(DisconnectCause cause) {
try {
close();
} catch (Exception e) {
logger.error("Failed to close", e);
logger.error("Failed to close {}", this, e);
}

onDisconnected(cause);
Expand Down Expand Up @@ -233,6 +232,7 @@ public void stop() {
// this side caused disconnect, notify other side
if (!isClosed())
send(new CloseConnection());
stopReceiver();
handleDisconnect(DisconnectCause.INITIATED_DISCONNECT);
}

Expand All @@ -254,8 +254,6 @@ public synchronized void close() {
if (!isClosed()) {
this.closed = true;

stopReceiver();

try {
if (this.out != null)
this.out.close();
Expand All @@ -269,10 +267,10 @@ public synchronized void close() {
try {
this.networkInterface.close();
} catch (Exception e) {
logger.warn("Failed to close " + networkInterface, e);
logger.warn("Failed to close {} for {}", networkInterface, this, e);
}
} else {
logger.warn("Attempted to close an already closed stream");
logger.warn("Attempted to close the already closed {}", this);
}
}

Expand Down
9 changes: 3 additions & 6 deletions server/src/main/java/sc/server/gaming/GameRoomManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
import sc.api.plugins.IGameState;
import sc.api.plugins.exceptions.GameRoomException;
import sc.api.plugins.exceptions.RescuableClientException;
import sc.networking.InvalidScoreDefinitionException;
import sc.networking.clients.GameLoaderClient;
import sc.framework.ReplayLoader;
import sc.protocol.requests.PrepareGameRequest;
import sc.protocol.responses.GamePreparedResponse;
import sc.protocol.responses.RoomWasJoinedEvent;
import sc.server.Configuration;
import sc.server.network.Client;
import sc.shared.*;
import sc.shared.SlotDescriptor;

import java.io.File;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.*;

/**
Expand Down Expand Up @@ -63,7 +60,7 @@ public synchronized GameRoom createGameRoom(String gameType) {
}

logger.info("Loading game from file '{}' at turn {}", gameFile, turn);
game = plugin.createGameFromState(new GameLoaderClient(gameFile).getTurn(turn));
game = plugin.createGameFromState(new ReplayLoader(gameFile).getTurn(turn));
} else {
game = plugin.createGame();
}
Expand Down
14 changes: 7 additions & 7 deletions server/src/test/java/sc/server/gaming/GameLoaderTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package sc.server.gaming
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.*
import sc.networking.clients.GameLoaderClient
import sc.framework.ReplayLoader
import sc.server.plugins.TestGameState
import java.io.File
import java.util.zip.GZIPOutputStream
Expand All @@ -18,15 +18,15 @@ class GameLoaderTest: FunSpec({
}
val state = TestGameState()
listOf(
"String" to GameLoaderClient(minimalReplay.byteInputStream()),
"GZip File" to GameLoaderClient(tmpfile)
"String" to { ReplayLoader(minimalReplay.byteInputStream()) },
"GZip File" to { ReplayLoader(tmpfile) }
).forEach { (clue, client) ->
test(clue) {
client.getHistory() shouldBe listOf(state)
client.getTurn(0) shouldBe state
client.getTurn(-1) shouldBe state
client().loadHistory().first shouldBe listOf(state)
client().getTurn(0) shouldBe state
client().getTurn(-1) shouldBe state
shouldThrow<NoSuchElementException> {
client.getTurn(1)
client().getTurn(1) shouldBe state
}
}
}
Expand Down

0 comments on commit 4973344

Please sign in to comment.