Skip to content

Commit ddc6a2b

Browse files
committed
refactor(server): delegate replay saving to ReplayListener
1 parent 34c746f commit ddc6a2b

File tree

6 files changed

+72
-42
lines changed

6 files changed

+72
-42
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package sc.framework
2+
3+
import sc.networking.XStreamProvider.Companion.loadPluginXStream
4+
import java.io.IOException
5+
import java.io.Writer
6+
7+
class ReplayListener<T>(private val history: MutableList<T> = ArrayList()) {
8+
9+
fun addMessage(message: T) =
10+
history.add(message)
11+
12+
/** Write replay of game to a writer. */
13+
@Throws(IOException::class)
14+
fun saveReplay(writer: Writer) {
15+
val xStream = loadPluginXStream()
16+
writer.write("<protocol>\n")
17+
for (element in history) {
18+
// TODO do we need to save RoomPackets?
19+
writer.write("${xStream.toXML(element)}\n")
20+
writer.flush()
21+
}
22+
writer.write("</protocol>")
23+
writer.close()
24+
}
25+
}

sdk/src/server-api/sc/protocol/room/MementoMessage.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import sc.framework.plugins.IPerspectiveProvider
1010
data class MementoMessage(
1111
val state: IGameState,
1212
@XStreamOmitField private val perspective: Any?
13-
): ObservableRoomMessage, IPerspectiveProvider {
13+
): ObservableRoomMessage, IPerspectiveProvider, Cloneable {
1414
override fun getPerspective() = perspective
15-
}
15+
public override fun clone() = MementoMessage(state.clone(), perspective)
16+
}

server/src/sc/server/Configuration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public static void set(final String key, final String value) {
8181
properties.setProperty(key, value);
8282
}
8383

84+
public static void set(final String key, final boolean value) {
85+
properties.setProperty(key, String.valueOf(value));
86+
}
87+
8488
public static void setIfNotNull(final String key, final String value) {
8589
if (value != null) {
8690
set(key, value);

server/src/sc/server/gaming/GameRoom.java

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package sc.server.gaming;
22

3-
import com.thoughtworks.xstream.XStream;
43
import org.slf4j.Logger;
54
import org.slf4j.LoggerFactory;
65
import sc.api.plugins.IGameInstance;
@@ -12,9 +11,9 @@
1211
import sc.api.plugins.exceptions.TooManyPlayersException;
1312
import sc.api.plugins.host.IGameListener;
1413
import sc.framework.HelperMethods;
14+
import sc.framework.ReplayListener;
1515
import sc.framework.plugins.AbstractGame;
1616
import sc.framework.plugins.Player;
17-
import sc.networking.XStreamProvider;
1817
import sc.networking.clients.IClient;
1918
import sc.networking.clients.XStreamClient;
2019
import sc.protocol.ProtocolPacket;
@@ -42,9 +41,9 @@ public class GameRoom implements IGameListener {
4241
private final ScoreDefinition scoreDefinition;
4342
private final List<PlayerSlot> playerSlots = new ArrayList<>(getMaximumPlayerCount());
4443
private GameStatus status = GameStatus.CREATED;
45-
private GameResult result = null;
44+
private GameResult result;
4645
private boolean pauseRequested = false;
47-
private List<RoomMessage> history = new ArrayList<>();
46+
private final ReplayListener<RoomPacket> replayListener = Boolean.parseBoolean(Configuration.get(Configuration.SAVE_REPLAY)) ? new ReplayListener<>() : null;
4847

4948
public final IGameInstance game; // TODO make inaccessible
5049
public final List<IClient> observers = new ArrayList<>();
@@ -74,6 +73,7 @@ public synchronized void onGameOver(Map<Player, PlayerScore> results) {
7473
try {
7574
result = generateGameResult(results);
7675
logger.info("{} is over (regular={})", game, result.isRegular());
76+
saveReplayMessage(result);
7777
// save playerScore if test mode enabled
7878
if (Boolean.parseBoolean(Configuration.get(Configuration.TEST_MODE))) {
7979
List<Player> players = game.getPlayers();
@@ -84,22 +84,38 @@ public synchronized void onGameOver(Map<Player, PlayerScore> results) {
8484
logger.error("Failed to generate GameResult from " + results, t);
8585
}
8686

87-
if (Boolean.parseBoolean(Configuration.get(Configuration.SAVE_REPLAY))) {
87+
saveReplay();
88+
destroy();
89+
}
90+
91+
private void saveReplayMessage(ObservableRoomMessage message) {
92+
if (replayListener != null)
93+
replayListener.addMessage(createRoomPacket(message instanceof MementoMessage ? ((MementoMessage) message).clone() : message));
94+
}
95+
96+
/** If enabled, save the recorded replay to the default file. */
97+
public void saveReplay() {
98+
if (replayListener != null) {
8899
try {
89-
saveReplay(new BufferedWriter(new FileWriter(createReplayFile())));
100+
File file = createReplayFile();
101+
logger.debug("Saving replay to {}", file);
102+
saveReplay(new BufferedWriter(new FileWriter(file)));
90103
} catch (IOException e) {
91104
logger.error("Failed to save replay", e);
92105
}
93106
}
107+
}
94108

95-
destroy();
109+
/** Saves a replay to the writer, assuming replays have been enabled. */
110+
public void saveReplay(Writer writer) throws IOException, NullPointerException {
111+
replayListener.saveReplay(writer);
96112
}
97113

98114
public File createReplayFile() throws IOException {
99115
String fileName = HelperMethods.getReplayFilename(this.game.getPluginUUID(),
100116
playerSlots.stream().map(it -> it.getPlayer().getDisplayName()).collect(Collectors.toList()));
101117

102-
File file = new File(fileName);
118+
File file = new File(fileName).getAbsoluteFile();
103119
if(file.getParentFile().mkdirs())
104120
if(file.createNewFile())
105121
return file;
@@ -130,26 +146,6 @@ private GameResult generateGameResult(Map<Player, PlayerScore> results) {
130146
return new GameResult(scoreDefinition, scores, this.game.getWinners());
131147
}
132148

133-
/** Write replay of game to a writer. */
134-
public void saveReplay(Writer writer) throws IOException {
135-
XStream xStream = XStreamProvider.loadPluginXStream();
136-
137-
writer.write("<protocol>\n");
138-
for (RoomMessage element : history) {
139-
if (!(element instanceof IGameState))
140-
continue;
141-
IGameState state = (IGameState) element;
142-
// TODO do we need to save RoomPackets?
143-
RoomPacket roomPacket = createRoomPacket(new MementoMessage(state, null));
144-
writer.write(xStream.toXML(roomPacket) + "\n");
145-
writer.flush();
146-
}
147-
148-
writer.write(xStream.toXML(createRoomPacket(this.result)) + "\n");
149-
writer.write("</protocol>");
150-
writer.close();
151-
}
152-
153149
/** Send the given message to all Players and Observers in this room. */
154150
private void broadcast(ObservableRoomMessage message) {
155151
broadcast(createRoomPacket(message));
@@ -175,10 +171,12 @@ private void kickAllClients() {
175171
/** Send updated GameState to all players and observers. */
176172
@Override
177173
public void onStateChanged(IGameState data, boolean observersOnly) {
178-
history.add(data);
179-
observerBroadcast(new MementoMessage(data, null));
180-
if (!observersOnly)
174+
MementoMessage memento = new MementoMessage(data, null);
175+
observerBroadcast(memento);
176+
if (!observersOnly) {
181177
sendStateToPlayers(data);
178+
saveReplayMessage(memento);
179+
}
182180
}
183181

184182
/**
@@ -321,7 +319,7 @@ public synchronized void onEvent(Client source, IMove move) throws GameRoomExcep
321319
ErrorMessage errorMessage = new ErrorMessage(move, error);
322320
player.notifyListeners(errorMessage);
323321
observerBroadcast(errorMessage);
324-
history.add(errorMessage);
322+
saveReplayMessage(errorMessage);
325323
cancel();
326324
throw new GameLogicException(e.toString(), e);
327325
} catch (GameLogicException e) {

server/test/sc/server/gaming/GameRoomTest.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import io.kotest.matchers.collections.shouldHaveSize
77
import io.kotest.matchers.shouldBe
88
import org.junit.jupiter.api.assertThrows
99
import sc.protocol.requests.PrepareGameRequest
10+
import sc.server.Configuration
1011
import sc.server.helpers.StringNetworkInterface
1112
import sc.server.network.Client
1213
import sc.server.plugins.TestPlugin
@@ -20,6 +21,7 @@ class GameRoomTest: WordSpec({
2021
val client = Client(StringNetworkInterface("")).apply { start() }
2122
"A GameRoomManager" should {
2223
val manager = GameRoomManager().apply { pluginManager.loadPlugin(TestPlugin::class.java) }
24+
Configuration.set(Configuration.SAVE_REPLAY, true)
2325
"create a game when a player joins" {
2426
manager.joinOrCreateGame(client, TestPlugin.TEST_PLUGIN_UUID).playerCount shouldBe 1
2527
manager.games shouldHaveSize 1
@@ -43,16 +45,16 @@ class GameRoomTest: WordSpec({
4345
<room roomId="${room.id}">
4446
<data class="memento">
4547
<state class="sc.server.plugins.TestGameState">
48+
<turn>0</turn>
49+
<state>0</state>
50+
<currentPlayer>RED</currentPlayer>
51+
<startPlayer>RED</startPlayer>
4652
<red displayName="">
4753
<color class="sc.server.helpers.TestTeam">RED</color>
4854
</red>
4955
<blue displayName="">
5056
<color class="sc.server.helpers.TestTeam">BLUE</color>
5157
</blue>
52-
<turn>0</turn>
53-
<state>0</state>
54-
<currentPlayer>RED</currentPlayer>
55-
<startPlayer>RED</startPlayer>
5658
</state>
5759
</data>
5860
</room>

server/test/sc/server/plugins/TestGameState.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@ data class TestGameState(
88
override var turn: Int = 0,
99
var state: Int = 0,
1010
var currentPlayer: TestTeam = TestTeam.RED,
11-
val startPlayer: TestTeam = TestTeam.RED
11+
val startPlayer: TestTeam = TestTeam.RED,
12+
val red: Player = Player(TestTeam.RED),
13+
val blue: Player = Player(TestTeam.BLUE),
1214
): IGameState {
1315
override val round get() = turn / 2
14-
val red = Player(TestTeam.RED)
15-
val blue = Player(TestTeam.BLUE)
1616

1717
/** wechselt den Spieler, der aktuell an der Reihe ist anhand von `turn` */
1818
fun switchCurrentPlayer() {
1919
currentPlayer = TestTeam.values()[(turn + startPlayer.index) % 2]
2020
}
2121

22-
override fun clone() = throw NotImplementedError()
22+
override fun clone() = TestGameState(turn, state, currentPlayer, startPlayer, red, blue)
2323
}

0 commit comments

Comments
 (0)