diff --git a/.gitignore b/.gitignore
index 4c0f0e8..d0ae912 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,5 @@ harness/lib/
# Runtime noise
logs/
*.log
+MegaMek-patched.jar
+patches/out/
diff --git a/harness/build.sh b/harness/build.sh
index c9e531b..0183868 100755
--- a/harness/build.sh
+++ b/harness/build.sh
@@ -22,6 +22,6 @@ mkdir -p "$OUT"
-classpath "$CP" \
-d "$OUT" \
-Xlint:-options \
- "$ROOT"/harness/src/bench/*.java
+ "$ROOT"/harness/src/bench/*.java "$ROOT"/harness/src/spike/*.java
echo "built -> $OUT"
diff --git a/harness/run.sh b/harness/run.sh
index bf4cf48..dfda643 100755
--- a/harness/run.sh
+++ b/harness/run.sh
@@ -13,7 +13,9 @@ MM_HOME="${MM_HOME:-$ROOT/MegaMek-0.51.00}"
JAVA_HOME="${JAVA_HOME:-$ROOT/jdk-21.0.12+8}"
HEAP="${HEAP:-6g}"
-CP="$ROOT/harness/out:$MM_HOME/MegaMek.jar"
+# MM_JAR=../MegaMek-patched.jar selects the multi-tenancy-patched build.
+MM_JAR="${MM_JAR:-$MM_HOME/MegaMek.jar}"
+CP="$ROOT/harness/out:$MM_JAR"
for jar in "$MM_HOME"/lib/*.jar; do CP="$CP:$jar"; done
cd "$MM_HOME"
diff --git a/harness/src/bench/IsolationCheck.java b/harness/src/bench/IsolationCheck.java
index 9d55e20..c89424c 100644
--- a/harness/src/bench/IsolationCheck.java
+++ b/harness/src/bench/IsolationCheck.java
@@ -102,6 +102,23 @@ public final class IsolationCheck {
System.out.printf(" server B game == game B: %s%n", serverB.getGame() == gameB);
System.out.printf(" servers see different games: %s%n", serverA.getGame() != serverB.getGame());
+ // 5. The blocker. Server's constructor ends with `serverInstance = this`, and
+ // AbstractGameManager.send() routes every outbound packet through
+ // Server.getServerInstance(). So the moment a second Server exists, the first
+ // game's manager starts transmitting to the second game's connections.
+ // Isolation of the *data* (checks 1-3) is real; isolation of the *transport*
+ // is not.
+ Server current = Server.getServerInstance();
+ System.out.println();
+ System.out.printf("[5] Server.getServerInstance() == serverB (newest): %s%n", current == serverB);
+ System.out.printf(" Server.getServerInstance() == serverA (first): %s%n", current == serverA);
+ System.out.printf(" -> game A's manager would send packets to %s%n",
+ current == serverB ? "SERVER B ** WRONG GAME **" : "server A");
+ System.out.printf(" verdict: %s%n",
+ current == serverA
+ ? "ok"
+ : "FAIL - concurrent games in one JVM corrupt each other's transport");
+
serverA.die();
serverB.die();
System.exit(0);
diff --git a/harness/src/spike/TextRenderer.java b/harness/src/spike/TextRenderer.java
new file mode 100644
index 0000000..fe71593
--- /dev/null
+++ b/harness/src/spike/TextRenderer.java
@@ -0,0 +1,206 @@
+package spike;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+import megamek.common.Player;
+import megamek.common.board.Board;
+import megamek.common.board.Coords;
+import megamek.common.game.Game;
+import megamek.common.units.Entity;
+import megamek.common.units.Terrains;
+
+/**
+ * Renders live game state as text.
+ *
+ *
This is a deliberate stand-in for the eventual web client's data path. A GIF of the
+ * board is not enough to play from - it shows terrain and unit positions but none of the
+ * information the Swing client puts in its side panels: per-location armor, heat, whether
+ * a unit has already moved, what the last round's dice actually did. Everything rendered
+ * here comes off the client's own {@link Game} object, which is exactly what a browser
+ * client would be fed over a websocket.
+ *
+ *
Rendered from a viewing player's perspective so that double-blind filtering, when it
+ * matters, is honoured by construction rather than bolted on.
+ */
+public final class TextRenderer {
+
+ private TextRenderer() {
+ }
+
+ /** MegaMek displays coordinates 1-indexed as a 4-digit column/row pair. */
+ private static String coordLabel(Coords c) {
+ if (c == null) {
+ return "----";
+ }
+ return String.format("%02d%02d", c.getX() + 1, c.getY() + 1);
+ }
+
+ private static final String[] FACING = { "N", "NE", "SE", "S", "SW", "NW" };
+
+ private static String facing(int f) {
+ return (f >= 0 && f < FACING.length) ? FACING[f] : "?";
+ }
+
+ /** One character per hex, picking the most tactically relevant feature present. */
+ private static char terrainChar(Board board, int x, int y) {
+ var hex = board.getHex(x, y);
+ if (hex == null) {
+ return ' ';
+ }
+ if (hex.containsTerrain(Terrains.BUILDING)) {
+ return '#';
+ }
+ if (hex.containsTerrain(Terrains.WATER)) {
+ return '~';
+ }
+ if (hex.containsTerrain(Terrains.WOODS)) {
+ return hex.terrainLevel(Terrains.WOODS) >= 2 ? 'A' : '^';
+ }
+ if (hex.containsTerrain(Terrains.JUNGLE)) {
+ return 'J';
+ }
+ if (hex.containsTerrain(Terrains.RUBBLE)) {
+ return ':';
+ }
+ if (hex.containsTerrain(Terrains.ROUGH)) {
+ return '%';
+ }
+ if (hex.containsTerrain(Terrains.SWAMP)) {
+ return 's';
+ }
+ if (hex.containsTerrain(Terrains.ROAD)) {
+ return '=';
+ }
+ return '.';
+ }
+
+ public static String render(Game game, Player viewer) {
+ StringBuilder sb = new StringBuilder(8192);
+
+ sb.append("=== ROUND ").append(game.getCurrentRound())
+ .append(" | PHASE ").append(game.getPhase());
+ if (viewer != null) {
+ sb.append(" | VIEW ").append(viewer.getName());
+ }
+ sb.append(" ===\n\n");
+
+ List entities = new ArrayList<>(game.getEntitiesVector());
+ entities.sort(Comparator.comparingInt(Entity::getOwnerId).thenComparingInt(Entity::getId));
+
+ sb.append(renderBoard(game, entities));
+ sb.append('\n');
+ sb.append(renderUnits(game, entities));
+
+ return sb.toString();
+ }
+
+ private static String renderBoard(Game game, List entities) {
+ Board board = game.getBoard(0);
+ if (board == null) {
+ return "(no board)\n";
+ }
+ int width = board.getWidth();
+ int height = board.getHeight();
+
+ // Units are overlaid as digits keyed to the unit table below, so the map and
+ // the stat block can be read together without guessing which blip is which.
+ char[][] overlay = new char[height][width];
+ for (int i = 0; i < entities.size(); i++) {
+ Entity e = entities.get(i);
+ Coords pos = e.getPosition();
+ if (pos == null || e.isDestroyed()) {
+ continue;
+ }
+ if (pos.getY() >= 0 && pos.getY() < height && pos.getX() >= 0 && pos.getX() < width) {
+ overlay[pos.getY()][pos.getX()] = (char) ('1' + (i % 9));
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("BOARD ").append(width).append('x').append(height)
+ .append(" . clear ^ light woods A heavy woods ~ water % rough")
+ .append(" # building = road : rubble\n");
+ sb.append(" ");
+ for (int x = 0; x < width; x++) {
+ sb.append(String.format("%-2d", (x + 1) % 100));
+ }
+ sb.append('\n');
+
+ for (int y = 0; y < height; y++) {
+ sb.append(String.format(" %2d ", y + 1));
+ // Odd columns sit half a hex lower; indent them so the offset grid reads
+ // roughly like the real map instead of a rectangular lie.
+ for (int x = 0; x < width; x++) {
+ char c = overlay[y][x] != 0 ? overlay[y][x] : terrainChar(board, x, y);
+ sb.append(c).append(x % 2 == 0 ? ' ' : ' ');
+ }
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ private static String renderUnits(Game game, List entities) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("UNITS\n");
+ sb.append(String.format(" %-3s %-4s %-12s %-24s %-5s %-4s %-4s %-5s %-11s %-9s %s%n",
+ "key", "id", "owner", "unit", "pos", "face", "elev", "heat", "armor", "internal", "status"));
+
+ for (int i = 0; i < entities.size(); i++) {
+ Entity e = entities.get(i);
+ String status;
+ if (e.isDestroyed()) {
+ status = "DESTROYED";
+ } else if (e.isDone()) {
+ status = "done";
+ } else {
+ status = "active";
+ }
+
+ sb.append(String.format(" %-3s %-4d %-12s %-24s %-5s %-4s %-4d %-5s %-11s %-9s %s%n",
+ String.valueOf((char) ('1' + (i % 9))),
+ e.getId(),
+ truncate(ownerName(game, e), 12),
+ truncate(e.getShortName(), 24),
+ coordLabel(e.getPosition()),
+ facing(e.getFacing()),
+ e.getElevation(),
+ e.getHeat() + "/" + e.getHeatCapacity(),
+ e.getTotalArmor() + "/" + e.getTotalOArmor(),
+ e.getTotalInternal() + "/" + e.getTotalOInternal(),
+ status));
+ }
+
+ // Per-location armor is the detail that decides whether you twist, run or
+ // stand and shoot, so it cannot be dropped from a playable view.
+ sb.append("\nARMOR BY LOCATION (current/original, internal in parens)\n");
+ for (int i = 0; i < entities.size(); i++) {
+ Entity e = entities.get(i);
+ if (e.isDestroyed()) {
+ continue;
+ }
+ sb.append(String.format(" [%s] %-24s ", (char) ('1' + (i % 9)), truncate(e.getShortName(), 24)));
+ for (int loc = 0; loc < e.locations(); loc++) {
+ sb.append(e.getLocationAbbr(loc)).append(' ')
+ .append(e.getArmor(loc)).append('/').append(e.getOArmor(loc))
+ .append('(').append(e.getInternal(loc)).append(')')
+ .append(" ");
+ }
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ private static String ownerName(Game game, Entity e) {
+ Player p = game.getPlayer(e.getOwnerId());
+ return p == null ? "?" : p.getName();
+ }
+
+ private static String truncate(String s, int n) {
+ if (s == null) {
+ return "";
+ }
+ return s.length() <= n ? s : s.substring(0, n - 1) + "~";
+ }
+}
diff --git a/harness/src/spike/WatchGame.java b/harness/src/spike/WatchGame.java
new file mode 100644
index 0000000..6233916
--- /dev/null
+++ b/harness/src/spike/WatchGame.java
@@ -0,0 +1,185 @@
+package spike;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import megamek.client.Client;
+import megamek.client.bot.princess.Princess;
+import megamek.common.Player;
+import megamek.common.compute.Compute;
+import megamek.common.enums.GamePhase;
+import megamek.common.event.GameListenerAdapter;
+import megamek.common.event.GamePhaseChangeEvent;
+import megamek.common.event.GameReportEvent;
+import megamek.common.game.Game;
+import megamek.common.game.IGame;
+import megamek.common.loaders.MekSummaryCache;
+import megamek.common.scenario.Scenario;
+import megamek.common.scenario.ScenarioLoader;
+import megamek.server.Server;
+import megamek.server.totalWarfare.TWGameManager;
+
+/**
+ * Runs a bot-vs-bot scenario with an extra observer client attached, dumping full
+ * text state on every phase change.
+ *
+ * Purpose is twofold. It gives a human-or-agent-readable view of a live match without
+ * a GUI, and - more importantly - it exercises the exact seam the real product needs: an
+ * ordinary {@link Client} subscribed via {@link megamek.common.event.GameListener},
+ * projecting engine state into a transport-friendly form. Princess proves upstream keeps
+ * that seam working; this proves it is rich enough to render from.
+ *
+ *
Usage: {@code WatchGame [scenarioPath] [outputDir]}
+ */
+public final class WatchGame {
+
+ private static final String LOCALHOST = "127.0.0.1";
+ private static final String OBSERVER_NAME = "Observer";
+
+ public static void main(String[] args) throws Exception {
+ String scenarioPath = args.length > 0
+ ? args[0]
+ : "data/scenarios/TrainingScenarios/1-FirstRun.mms";
+ Path outDir = Path.of(args.length > 1 ? args[1] : "watch-out");
+ int maxRounds = args.length > 2 ? Integer.parseInt(args[2]) : 12;
+ Files.createDirectories(outDir);
+
+ Compute.d6();
+ MekSummaryCache cache = MekSummaryCache.getInstance();
+ while (!cache.isInitialized()) {
+ Thread.sleep(250L);
+ }
+
+ ScenarioLoader loader = new ScenarioLoader(new File(scenarioPath));
+ Scenario scenario = loader.load();
+ IGame game = scenario.createGame();
+
+ TWGameManager gameManager = new TWGameManager();
+ int port = 8900;
+ Server server = new Server("", port, gameManager);
+ server.setGame(game);
+ scenario.applyDamage(gameManager);
+ gameManager.calculatePlayerInitialCounts();
+
+ List bots = new ArrayList<>();
+ for (Player player : game.getPlayersList()) {
+ Princess bot = new Princess(player.getName(), LOCALHOST, port);
+ bot.startPrecognition();
+ bot.connect();
+ bots.add(bot);
+ }
+
+ // The observer joins as an extra player and is then flagged as an observer
+ // server-side, so it never owns a turn and never stalls the game.
+ Client observer = new Client(OBSERVER_NAME, LOCALHOST, port);
+ AtomicInteger dumpCount = new AtomicInteger();
+ StringBuilder reportLog = new StringBuilder();
+
+ observer.getGame().addGameListener(new GameListenerAdapter() {
+ @Override
+ public void gamePhaseChange(GamePhaseChangeEvent e) {
+ dump(observer, outDir, dumpCount.incrementAndGet(), reportLog);
+ }
+
+ @Override
+ public void gameReport(GameReportEvent e) {
+ if (e.getReport() != null && !e.getReport().isBlank()) {
+ reportLog.append(e.getReport()).append('\n');
+ }
+ }
+ });
+ observer.connect();
+
+ waitForConnected(bots, observer);
+
+ for (Player p : server.getGame().getPlayersList()) {
+ if (OBSERVER_NAME.equals(p.getName())) {
+ p.setObserver(true);
+ }
+ }
+
+ for (Princess bot : bots) {
+ long deadline = System.currentTimeMillis() + 30_000L;
+ while (System.currentTimeMillis() < deadline
+ && (bot.getLocalPlayer() == null
+ || bot.getGame().getEntitiesOwnedBy(bot.getLocalPlayer()) == 0)) {
+ Thread.sleep(100L);
+ }
+ }
+
+ for (Princess bot : bots) {
+ bot.sendDone(true);
+ }
+ observer.sendDone(true);
+
+ long deadline = System.currentTimeMillis() + 900_000L;
+ while (System.currentTimeMillis() < deadline) {
+ Game g = observer.getGame();
+ if (g.getPhase() == GamePhase.VICTORY || g.getCurrentRound() >= maxRounds) {
+ break;
+ }
+ Thread.sleep(1000L);
+ }
+
+ dump(observer, outDir, dumpCount.incrementAndGet(), reportLog);
+ System.out.printf("wrote %d state dumps to %s%n", dumpCount.get(), outDir.toAbsolutePath());
+
+ server.die();
+ System.exit(0);
+ }
+
+ private static void waitForConnected(List bots, Client observer) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + 60_000L;
+ while (System.currentTimeMillis() < deadline) {
+ boolean all = observer.isConnected();
+ for (Princess bot : bots) {
+ all &= bot.isConnected();
+ }
+ if (all) {
+ return;
+ }
+ Thread.sleep(100L);
+ }
+ }
+
+ private static void dump(Client observer, Path outDir, int seq, StringBuilder reportLog) {
+ try {
+ Game g = observer.getGame();
+ String body = TextRenderer.render(g, observer.getLocalPlayer());
+
+ StringBuilder sb = new StringBuilder(body);
+ sb.append("\nLAST PHASE REPORT\n");
+ String phaseReport = observer.phaseReport;
+ sb.append(phaseReport == null || phaseReport.isBlank()
+ ? " (none)\n"
+ : indent(stripHtml(phaseReport)));
+
+ String name = String.format("%03d-r%02d-%s.txt", seq, g.getCurrentRound(), g.getPhase());
+ Files.writeString(outDir.resolve(name), sb.toString(), StandardCharsets.UTF_8);
+ System.out.printf(" dump %s (round %d, %s)%n", name, g.getCurrentRound(), g.getPhase());
+ } catch (Exception e) {
+ System.err.println("dump failed: " + e.getMessage());
+ }
+ }
+
+ /** Reports arrive with HTML markup intended for the Swing report pane. */
+ private static String stripHtml(String s) {
+ return s.replaceAll("
", "\n")
+ .replaceAll("<[^>]+>", "")
+ .replaceAll("\n{3,}", "\n\n")
+ .trim();
+ }
+
+ private static String indent(String s) {
+ StringBuilder sb = new StringBuilder();
+ for (String line : s.split("\n")) {
+ sb.append(" ").append(line).append('\n');
+ }
+ return sb.toString();
+ }
+}
diff --git a/patches/build-patched-jar.sh b/patches/build-patched-jar.sh
new file mode 100755
index 0000000..3c2c8c6
--- /dev/null
+++ b/patches/build-patched-jar.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+# Build a patched MegaMek.jar carrying the multi-tenancy fix.
+#
+# MegaMek.jar sets "Sealed: true", so a replacement megamek.server.* class cannot
+# simply be placed earlier on the classpath - the JVM refuses to load a sealed
+# package from a second codebase. Injecting the recompiled classes back into a
+# copy of the jar keeps the seal intact.
+#
+# The pristine release tree is never modified; output is MegaMek-patched.jar.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+MM_HOME="${MM_HOME:-$ROOT/MegaMek-0.51.00}"
+JAVA_HOME="${JAVA_HOME:-$ROOT/jdk-21.0.12+8}"
+
+CP="$MM_HOME/MegaMek.jar"
+for jar in "$MM_HOME"/lib/*.jar; do CP="$CP:$jar"; done
+
+OUT="$ROOT/patches/out"
+rm -rf "$OUT"
+mkdir -p "$OUT"
+
+echo "compiling patched sources..."
+"$JAVA_HOME/bin/javac" \
+ -nowarn \
+ -classpath "$CP" \
+ -d "$OUT" \
+ "$ROOT"/patches/src/megamek/server/Server.java \
+ "$ROOT"/patches/src/megamek/server/AbstractGameManager.java
+
+PATCHED_JAR="$ROOT/MegaMek-patched.jar"
+cp "$MM_HOME/MegaMek.jar" "$PATCHED_JAR"
+
+echo "injecting $(find "$OUT" -name '*.class' | wc -l) recompiled classes..."
+"$JAVA_HOME/bin/jar" --update --file "$PATCHED_JAR" -C "$OUT" megamek
+
+echo "built -> $PATCHED_JAR"
diff --git a/patches/src/megamek/server/AbstractGameManager.java b/patches/src/megamek/server/AbstractGameManager.java
new file mode 100644
index 0000000..f7ec30d
--- /dev/null
+++ b/patches/src/megamek/server/AbstractGameManager.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2024-2025 The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License (GPL),
+ * version 3 or (at your option) any later version,
+ * as published by the Free Software Foundation.
+ *
+ * MegaMek 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 General Public License for more details.
+ *
+ * A copy of the GPL should have been included with this project;
+ * if not, see .
+ *
+ * NOTICE: The MegaMek organization is a non-profit group of volunteers
+ * creating free software for the BattleTech community.
+ *
+ * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
+ * of The Topps Company, Inc. All Rights Reserved.
+ *
+ * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
+ * InMediaRes Productions, LLC.
+ *
+ * MechWarrior Copyright Microsoft Corporation. MegaMek was created under
+ * Microsoft's "Game Content Usage Rules"
+ * and it is not endorsed by or
+ * affiliated with Microsoft.
+ */
+
+package megamek.server;
+
+import megamek.common.Player;
+import megamek.common.enums.GamePhase;
+import megamek.common.game.IGame;
+import megamek.common.net.enums.PacketCommand;
+import megamek.common.net.packets.InvalidPacketDataException;
+import megamek.common.net.packets.Packet;
+import megamek.common.options.OptionsConstants;
+import megamek.common.preference.PreferenceManager;
+import megamek.common.util.StringUtil;
+import megamek.logging.MMLogger;
+import megamek.server.trigger.TriggerSituation;
+
+public abstract class AbstractGameManager implements IGameManager {
+ private static final MMLogger logger = MMLogger.create(AbstractGameManager.class);
+
+ /**
+ * MULTI-TENANCY PATCH: the Server that owns this manager.
+ *
+ * Previously every method below reached for {@link Server#getServerInstance()},
+ * a static pointing at the most recently constructed Server. With one game per JVM
+ * that is always the right answer. With two, the older game silently starts
+ * transmitting to the newer game's connections and both wedge.
+ *
+ *
Falls back to the static when unset so any construction path that does not go
+ * through the Server constructor behaves exactly as before.
+ */
+ private Server owningServer;
+
+ void setOwningServer(Server server) {
+ this.owningServer = server;
+ }
+
+ protected Server server() {
+ return owningServer != null ? owningServer : Server.getServerInstance();
+ }
+
+ protected final GameManagerPacketHelper packetHelper = new GameManagerPacketHelper(this);
+ protected final GameManagerSaveHelper saveHandler = new GameManagerSaveHelper(this);
+ protected final AutosaveService autoSaveService = new AutosaveService(this);
+ protected final GameManagerScriptedEventHelper scriptedEventHelper = new GameManagerScriptedEventHelper(this);
+
+ /**
+ * Sends the given packet to all connections (all connected Clients = players).
+ *
+ * @see Server#send(Packet)
+ */
+ @Override
+ public void send(Packet packet) {
+ server().send(packet);
+ }
+
+ /**
+ * Sends the given packet to the given connection (= player ID).
+ *
+ * @see Server#send(int, Packet)
+ */
+ @Override
+ public void send(int connId, Packet p) {
+ server().send(connId, p);
+ }
+
+ @Override
+ public void handlePacket(int connId, Packet packet) {
+ if (packet.command() == PacketCommand.PLAYER_READY) {
+ try {
+ receivePlayerDone(packet, connId);
+ if (getGame().getPlayer(connId) != null) {
+ send(packetHelper.createPlayerDonePacket(connId));
+ checkReady();
+ }
+ } catch (InvalidPacketDataException e) {
+ logger.error("Invalid packet data:", e);
+ }
+ }
+ }
+
+ /**
+ * Ends this phase and moves on to the next.
+ */
+ protected abstract void endCurrentPhase();
+
+ /**
+ * Do anything we need to work through the current phase, such as give a turn to the first player to play.
+ */
+ protected abstract void executeCurrentPhase();
+
+ /**
+ * Prepares for the game's current phase. This typically involves resetting the states of units in the game and
+ * making sure the clients have the information they need for the new phase.
+ */
+ protected abstract void prepareForCurrentPhase();
+
+ /**
+ * Switches to the given new Phase and preforms preparation, checks if it should be skipped and executes it.
+ */
+ public final void changePhase(GamePhase newPhase) {
+ if (getGame().getPhase().isExchange() || getGame().getPhase().isStartingScenario()) {
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.GAME_START);
+ }
+ if (newPhase.isInitiative()) {
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.ROUND_START);
+ }
+ if (getGame().getPhase().isEnd()) {
+ // the endreport phase cannot be used here as it may be skipped
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.ROUND_END);
+ }
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.PHASE_END);
+
+ getGame().setLastPhase(getGame().getPhase());
+ getGame().setPhase(newPhase);
+
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.PHASE_START);
+ if (getGame().getPhase().isVictory()) {
+ scriptedEventHelper.processScriptedEvents(TriggerSituation.GAME_END);
+ }
+ prepareForCurrentPhase();
+
+ if (getGame().shouldSkipCurrentPhase()) {
+ endCurrentPhase();
+ } else {
+ // tell the players about the new phase
+ sendPhaseChange();
+ executeCurrentPhase();
+ }
+ }
+
+ protected void sendPhaseChange() {
+ send(packetHelper.createPhaseChangePacket());
+ }
+
+ /**
+ * Called when a player declares that they are "done". By default, this method advances to the next phase, if
+ *
+ * - all non-ghost, non-observer players are done,
+ *
+ * - the present phase does not use turns (e.g. if it's a report phase), and
+ *
+ * - we are not in an empty lobby (= no units at all).
+ *
+ * In other circumstances, ending the current phase is triggered elsewhere. Note that specifically, ghost players
+ * are not checked for their status here so the game can advance through non-turn (report) phases even with ghost
+ * players.
+ */
+ protected void checkReady() {
+ for (Player player : getGame().getPlayersList()) {
+ if (!player.isGhost() && !player.isObserver() && !player.isDone()) {
+ return;
+ }
+ }
+
+ if (!getGame().getPhase().usesTurns() && !isEmptyLobby()) {
+ endCurrentPhase();
+ }
+ }
+
+ /**
+ * Sends out the player ready stats for all players to all connections
+ */
+ protected void transmitAllPlayerDones() {
+ getGame().getPlayersList().forEach(player -> send(packetHelper.createPlayerDonePacket(player.getId())));
+ }
+
+ /**
+ * @return True when the game is in the lobby phase and is empty (no units present).
+ */
+ protected boolean isEmptyLobby() {
+ return getGame().getPhase().isLounge() && getGame().getInGameObjects().isEmpty();
+ }
+
+ /**
+ * Sets a player's ready status as received from the Client. This method does not perform any follow-up actions.
+ */
+ private void receivePlayerDone(Packet packet, int connIndex) throws InvalidPacketDataException {
+ boolean ready = packet.getBooleanValue(0);
+ Player player = getGame().getPlayer(connIndex);
+ if (null != player) {
+ player.setDone(ready);
+ } else {
+ logger.error("Tried to set done status of non-existent player!");
+ }
+ }
+
+ /**
+ * Sends out the player object to all players. Private info of the given player is redacted before being sent to
+ * other players.
+ *
+ * @param player The player whose information is to be shared
+ *
+ * @see #transmitAllPlayerUpdates()
+ */
+ protected void transmitPlayerUpdate(Player player) {
+ server().transmitPlayerUpdate(player);
+ }
+
+ /**
+ * Shares all player objects with all players. Private info is redacted before being sent to other players.
+ *
+ * @see #transmitPlayerUpdate(Player)
+ */
+ public void transmitAllPlayerUpdates() {
+ getGame().getPlayersList().forEach(this::transmitPlayerUpdate);
+ }
+
+ /**
+ * Performs an automatic save (does not check the autosave settings - the autosave will simply be done). Depending
+ * on the settings, the "autosave" filename is appended with a timestamp and/or a chat message is sent announcing
+ * the autosave.
+ */
+ public void autoSave() {
+ String fileName = "autosave";
+
+ if (PreferenceManager.getClientPreferences().stampFilenames()) {
+ fileName = StringUtil.addDateTimeStamp(fileName);
+ }
+
+ saveGame(fileName, getGame().getOptions().booleanOption(OptionsConstants.BASE_AUTOSAVE_MSG));
+ }
+
+ @Override
+ public void saveGame(String fileName, boolean sendChat) {
+ saveHandler.saveGame(fileName, sendChat);
+ }
+
+ @Override
+ public void sendSaveGame(int connId, String sFile, String sLocalPath) {
+ saveHandler.sendSaveGame(connId, sFile, sLocalPath);
+ }
+
+ public void sendChat(String origin, String message) {
+ server().sendChat(origin, message);
+ }
+
+ public void sendChat(int connId, String origin, String message) {
+ server().sendChat(connId, origin, message);
+ }
+
+ public void sendServerChat(String message) {
+ server().sendServerChat(message);
+ }
+
+ public void sendServerChat(int connId, String message) {
+ server().sendServerChat(connId, message);
+ }
+
+ /**
+ * Sends the current list of player turns as stored in the game's turn list to the Clients.
+ *
+ * @see IGame#getTurnsList()
+ */
+ public void sendCurrentTurns() {
+ send(packetHelper.createTurnListPacket());
+ }
+
+ /**
+ * Increment's the server's game round and send it to all the clients
+ */
+ public void incrementAndSendGameRound() {
+ getGame().incrementCurrentRound();
+ send(packetHelper.createCurrentRoundNumberPacket());
+ }
+
+ public GameManagerPacketHelper getPacketHelper() {
+ return packetHelper;
+ }
+
+ public AutosaveService getAutoSaveService() {
+ return autoSaveService;
+ }
+
+ /**
+ * Sends out a notification message indicating that a ghost player's turn may be skipped with the /skip command.
+ *
+ * @param ghost the Player who is ghosted. This value must not be null.
+ */
+ protected void sendGhostSkipMessage(Player ghost) {
+ String message = String.format(
+ "Player '%s' is disconnected. You may skip their current turn with the /skip command.",
+ ghost.getName());
+ sendServerChat(message);
+ }
+}
diff --git a/patches/src/megamek/server/Server.java b/patches/src/megamek/server/Server.java
new file mode 100644
index 0000000..c89a3d8
--- /dev/null
+++ b/patches/src/megamek/server/Server.java
@@ -0,0 +1,1556 @@
+/*
+ * Copyright (c) 2000-2005 - Ben Mazur (bmazur@sev.org)
+ * Copyright (c) 2013 - Edward Cullen (eddy@obsessedcomputers.co.uk)
+ * Copyright (C) 2002-2025 The MegaMek Team. All Rights Reserved.
+ *
+ * This file is part of MegaMek.
+ *
+ * MegaMek is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License (GPL),
+ * version 3 or (at your option) any later version,
+ * as published by the Free Software Foundation.
+ *
+ * MegaMek 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 General Public License for more details.
+ *
+ * A copy of the GPL should have been included with this project;
+ * if not, see .
+ *
+ * NOTICE: The MegaMek organization is a non-profit group of volunteers
+ * creating free software for the BattleTech community.
+ *
+ * MechWarrior, BattleMech, `Mech and AeroTech are registered trademarks
+ * of The Topps Company, Inc. All Rights Reserved.
+ *
+ * Catalyst Game Labs and the Catalyst Game Labs logo are trademarks of
+ * InMediaRes Productions, LLC.
+ *
+ * MechWarrior Copyright Microsoft Corporation. MegaMek was created under
+ * Microsoft's "Game Content Usage Rules"
+ * and it is not endorsed by or
+ * affiliated with Microsoft.
+ */
+package megamek.server;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import java.util.zip.GZIPInputStream;
+
+import com.thoughtworks.xstream.XStream;
+import megamek.MMConstants;
+import megamek.SuiteConstants;
+import megamek.Version;
+import megamek.client.ui.util.PlayerColour;
+import megamek.codeUtilities.StringUtility;
+import megamek.common.Player;
+import megamek.common.annotations.Nullable;
+import megamek.common.board.Board;
+import megamek.common.commandLine.AbstractCommandLineParser.ParseException;
+import megamek.common.game.Game;
+import megamek.common.game.IGame;
+import megamek.common.icons.Camouflage;
+import megamek.common.loaders.MapSettings;
+import megamek.common.net.connections.AbstractConnection;
+import megamek.common.net.enums.PacketCommand;
+import megamek.common.net.events.DisconnectedEvent;
+import megamek.common.net.events.PacketReceivedEvent;
+import megamek.common.net.factories.ConnectionFactory;
+import megamek.common.net.listeners.ConnectionListener;
+import megamek.common.net.packets.InvalidPacketDataException;
+import megamek.common.net.packets.Packet;
+import megamek.common.options.OptionsConstants;
+import megamek.common.preference.PreferenceManager;
+import megamek.common.rolls.Roll;
+import megamek.common.util.EmailService;
+import megamek.common.util.SerializationHelper;
+import megamek.logging.MMLogger;
+import megamek.server.commands.ServerCommand;
+
+/**
+ * @author Ben Mazur
+ */
+public class Server implements Runnable {
+ // server setup
+ private final String password;
+
+ private final IGameManager gameManager;
+
+ private final String metaServerUrl;
+
+ private final ServerSocket serverSocket;
+
+ private final String messageOfTheDay;
+
+ private final EmailService mailer;
+
+ private static final MMLogger LOGGER = MMLogger.create(Server.class);
+
+ private final ReentrantLock GAME_LOCK = new ReentrantLock();
+
+ public static class ReceivedPacket {
+ private int connectionId;
+ private Packet packet;
+
+ public ReceivedPacket(int cid, Packet p) {
+ setPacket(p);
+ setConnectionId(cid);
+ }
+
+ public int getConnectionId() {
+ return connectionId;
+ }
+
+ public void setConnectionId(int connectionId) {
+ this.connectionId = connectionId;
+ }
+
+ public Packet getPacket() {
+ return packet;
+ }
+
+ public void setPacket(Packet packet) {
+ this.packet = packet;
+ }
+ }
+
+ private class PacketPump implements Runnable {
+ boolean shouldStop;
+
+ PacketPump() {
+ shouldStop = false;
+ }
+
+ void signalEnd() {
+ shouldStop = true;
+ }
+
+ @Override
+ public void run() {
+ while (!shouldStop) {
+ while (!packetQueue.isEmpty()) {
+ ReceivedPacket rp = packetQueue.poll();
+ synchronized (serverLock) {
+ handle(rp.getConnectionId(), rp.getPacket());
+ }
+ }
+
+ try {
+ synchronized (packetQueue) {
+ packetQueue.wait();
+ }
+ } catch (InterruptedException ignored) {
+ // If we are interrupted, just keep going, generally this happens after we are signalled to stop.
+ }
+ }
+ }
+ }
+
+ // commands
+ private final Map commandsHash = new ConcurrentHashMap<>();
+
+ // game info
+ private final List connections = new CopyOnWriteArrayList<>();
+
+ private final Map connectionHandlers = new ConcurrentHashMap<>();
+
+ private final ConcurrentLinkedQueue packetQueue = new ConcurrentLinkedQueue<>();
+
+ private final boolean dedicated;
+
+ private final List connectionsPending = new CopyOnWriteArrayList<>();
+
+ private final Map connectionIds = new ConcurrentHashMap<>();
+
+ private int connectionCounter;
+
+ // listens for and connects players
+ private Thread connector;
+
+ private final PacketPump packetPump;
+ private Thread packetPumpThread;
+
+ private final Timer watchdogTimer = new Timer("Watchdog Timer");
+
+ private static Server serverInstance = null;
+
+ private String serverAccessKey = null;
+
+ private Timer serverBrowserUpdateTimer = null;
+
+ /**
+ * Used to ensure only one thread at a time is accessing this particular instance of the server.
+ */
+ private final Object serverLock = new Object();
+
+ public static final String ORIGIN = "***Server";
+
+ // Easter eggs. Happy April Fool's Day!!
+ private static final String DUNE_CALL = "They tried and failed?";
+
+ private static final String DUNE_RESPONSE = "They tried and died!";
+
+ private static final String STAR_WARS_CALL = "I'd just as soon kiss a Wookiee.";
+
+ private static final String STAR_WARS_RESPONSE = "I can arrange that!";
+
+ private static final String INVADER_ZIM_CALL = "What does the G stand for?";
+
+ private static final String INVADER_ZIM_RESPONSE = "I don't know.";
+
+ private static final String WARGAMES_CALL = "Shall we play a game?";
+
+ private static final String WARGAMES_RESPONSE = "Let's play global thermonuclear war.";
+
+ public static final int SERVER_CONN = Integer.MIN_VALUE;
+
+ private final ConnectionListener connectionListener = new ConnectionListener() {
+
+ private boolean isPaused = false;
+ private final List pausedWaitingList = new ArrayList<>();
+
+ /**
+ * Called when it is sensed that a connection has terminated.
+ */
+ @Override
+ public void disconnected(DisconnectedEvent e) {
+ AbstractConnection conn = e.getConnection();
+
+ // write something in the log
+ LOGGER.info("s: connect {} disconnected.", conn.getId());
+
+ connections.remove(conn);
+ synchronized (serverLock) {
+ connectionsPending.remove(conn);
+ connectionIds.remove(conn.getId());
+ ConnectionHandler ch = connectionHandlers.get(conn.getId());
+ if (ch != null) {
+ ch.signalStop();
+ connectionHandlers.remove(conn.getId());
+ }
+ }
+
+ // if there's a player for this connection, remove it too
+ Player player = getPlayer(conn.getId());
+ if (null != player) {
+ Server.this.disconnected(player);
+ }
+ }
+
+ @Override
+ public void packetReceived(PacketReceivedEvent e) {
+ ReceivedPacket rp = new ReceivedPacket(e.getConnection().getId(), e.getPacket());
+ switch (e.getPacket().command()) {
+ case CLIENT_FEEDBACK_REQUEST:
+ // Handled CFR packets specially
+ gameManager.handleCfrPacket(rp);
+ break;
+ case CLOSE_CONNECTION:
+ case CLIENT_NAME:
+ case CLIENT_VERSIONS:
+ case CHAT:
+ // Some packets should be handled immediately
+ handle(rp.getConnectionId(), rp.getPacket());
+ break;
+ case PAUSE:
+ if (!isPaused) {
+ LOGGER.info("Pause packet received - pausing packet handling");
+ sendServerChat("Game is paused.");
+ }
+ isPaused = true;
+ break;
+ case UNPAUSE:
+ if (isPaused) {
+ LOGGER.info("Unpause packet received - resuming packet handling");
+ sendServerChat("Game is resumed.");
+ }
+ isPaused = false;
+ synchronized (packetQueue) {
+ packetQueue.addAll(pausedWaitingList);
+ packetQueue.notifyAll();
+ }
+ pausedWaitingList.clear();
+ break;
+ default:
+ if (isPaused) {
+ pausedWaitingList.add(rp);
+ } else {
+ synchronized (packetQueue) {
+ packetQueue.add(rp);
+ packetQueue.notifyAll();
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * @param serverAddress IP or Domain Name Of Server
+ *
+ * @return valid hostName
+ *
+ * @throws ParseException for null or empty serverAddress
+ */
+ public static String validateServerAddress(String serverAddress) throws ParseException {
+ if ((serverAddress == null) || serverAddress.isBlank()) {
+ String message = "serverAddress must not be null or empty";
+ LOGGER.error(message);
+ throw new ParseException(message);
+ } else {
+ return serverAddress.trim();
+ }
+ }
+
+ /**
+ * @param playerName throw ParseException if null or empty
+ *
+ * @return valid playerName
+ */
+ public static String validatePlayerName(String playerName) throws ParseException {
+ if (playerName == null) {
+ String message = "playerName must not be null";
+ LOGGER.error(message);
+ throw new ParseException(message);
+ } else if (playerName.isBlank()) {
+ String message = "playerName must not be empty string";
+ LOGGER.error(message);
+ throw new ParseException(message);
+ } else {
+ return playerName.trim();
+ }
+ }
+
+ /**
+ * @param password Password to set for server.
+ *
+ * @return valid password or null if no password or password is blank string
+ */
+ public static @Nullable String validatePassword(@Nullable String password) {
+ return StringUtility.isNullOrBlank(password) ? null : password.trim();
+ }
+
+ /**
+ * Checks a String against the server password
+ *
+ * @param password The password provided by the user.
+ *
+ * @return true if the user-supplied data matches the server password or no password is set.
+ */
+ public boolean passwordMatches(Object password) {
+ return StringUtility.isNullOrBlank(this.password) || this.password.equals(password);
+ }
+
+ /**
+ * @param port if outside the established range of #{@see MMConstants#MIN_PORT} and #{@see MMConstants#MAX_PORT}
+ * return #{@see MMConstants#DEFAULT_PORT} or the passed in port.
+ *
+ * @return valid port number
+ */
+ public static int validatePort(int port) {
+ if (port < MMConstants.MIN_PORT || port > MMConstants.MAX_PORT) {
+ return MMConstants.DEFAULT_PORT;
+ }
+
+ return port;
+ }
+
+ public Server(@Nullable String password, int port, IGameManager gameManager) throws IOException {
+ this(password, port, gameManager, false, "", null, false);
+ }
+
+ public Server(@Nullable String password, int port, IGameManager gameManager, boolean registerWithServerBrowser,
+ @Nullable String metaServerUrl) throws IOException {
+ this(password, port, gameManager, registerWithServerBrowser, metaServerUrl, null, false);
+ }
+
+ /**
+ * Construct a new GameHost and begin listening for incoming clients.
+ *
+ * @param password the String that is set as a password
+ * @param port the int value that specifies the port that is used
+ * @param gameManager the {@link IGameManager} instance for this server instance.
+ * @param registerWithServerBrowser a boolean indicating whether we should register with the master
+ * server browser on ...
+ * @param mailer an email service instance to use for sending round reports.
+ * @param dedicated set to true if this server is started from a GUI-less context
+ */
+ public Server(@Nullable String password, int port, IGameManager gameManager, boolean registerWithServerBrowser,
+ @Nullable String metaServerUrl, @Nullable EmailService mailer, boolean dedicated) throws IOException {
+ this.metaServerUrl = StringUtility.isNullOrBlank(metaServerUrl) ? null : metaServerUrl;
+ this.password = StringUtility.isNullOrBlank(password) ? null : password;
+ this.gameManager = gameManager;
+ this.mailer = mailer;
+ this.dedicated = dedicated;
+
+ // initialize server socket
+ serverSocket = new ServerSocket(port);
+
+ messageOfTheDay = createMOTD();
+
+ // display server start text
+ LOGGER.info("s: starting a new server...");
+
+ try {
+ String host = InetAddress.getLocalHost().getHostName();
+ LOGGER.info("s: hostname = '{}' port = {}", host, serverSocket.getLocalPort());
+
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+ for (InetAddress address : addresses) {
+ LOGGER.info("s: hosting on address = {}", address.getHostAddress());
+ }
+ } catch (Exception ignored) {
+ }
+
+ if (password != null) {
+ LOGGER.info("s: password is set (redacted)");
+ }
+
+ for (ServerCommand command : gameManager.getCommandList(this)) {
+ registerCommand(command);
+ }
+
+ packetPump = new PacketPump();
+ packetPumpThread = new Thread(packetPump, "Packet Pump");
+ packetPumpThread.start();
+
+ if (registerWithServerBrowser) {
+ if (!StringUtility.isNullOrBlank(metaServerUrl)) {
+ final TimerTask register = new TimerTask() {
+ @Override
+ public void run() {
+ registerWithServerBrowser(true, Server.getServerInstance().metaServerUrl);
+ }
+ };
+ serverBrowserUpdateTimer = new Timer("Server Browser Register Timer", true);
+ serverBrowserUpdateTimer.schedule(register, 1, 40000);
+ } else {
+ LOGGER.error("Invalid URL for server browser {}", this.metaServerUrl);
+ }
+ }
+
+ // Fully initialized, now accept connections
+ connector = new Thread(this, "Connection Listener");
+ connector.start();
+
+ serverInstance = this;
+
+ // MULTI-TENANCY PATCH: give the manager a direct reference to its own server.
+ // The static serverInstance still exists (nothing else is disturbed), but a
+ // manager that knows its owner no longer has to guess via the static, which
+ // always points at the most recently constructed Server.
+ if (gameManager instanceof AbstractGameManager abstractGameManager) {
+ abstractGameManager.setOwningServer(this);
+ }
+ }
+
+ public IGameManager getGameManager() {
+ return gameManager;
+ }
+
+ /**
+ * Sets the game for this server. Restores any transient fields, and sets all players as ghosts. This should only be
+ * called during server initialization before any players have connected.
+ */
+ public void setGame(IGame g) {
+ gameManager.setGame(g);
+ }
+
+ public IGame getGame() {
+ return gameManager.getGame();
+ }
+
+ public EmailService getEmailService() {
+ return mailer;
+ }
+
+ /**
+ * Make a default message o' the day containing the version string, and if it was found, the build timestamp
+ */
+ private String createMOTD() {
+ return "Welcome to MegaMek. Server is running version " + SuiteConstants.VERSION;
+ }
+
+ /**
+ * @return true if the server has a password
+ */
+ public boolean isPassworded() {
+ return password != null;
+ }
+
+ /**
+ * @return true if the password matches
+ */
+ public boolean isPassword(Object guess) {
+ return password.equals(guess);
+ }
+
+ /**
+ * Registers a new command in the server command table
+ */
+ private void registerCommand(ServerCommand command) {
+ commandsHash.put(command.getName(), command);
+ }
+
+ /**
+ * Returns the command associated with the specified name
+ */
+ public ServerCommand getCommand(String name) {
+ return commandsHash.get(name);
+ }
+
+ /**
+ * @return true run from a GUI-less context
+ */
+ public boolean getDedicated() {
+ return dedicated;
+ }
+
+ /**
+ * Shuts down the server.
+ */
+ public void die() {
+ watchdogTimer.cancel();
+
+ // kill thread accepting new connections
+ connector = null;
+ packetPump.signalEnd();
+ packetPumpThread.interrupt();
+ packetPumpThread = null;
+
+ // close socket
+ try {
+ serverSocket.close();
+ } catch (Exception ignored) {
+ }
+
+ // kill pending connections
+ connectionsPending.forEach(AbstractConnection::close);
+ connectionsPending.clear();
+
+ // Send "kill" commands to all connections
+ // This WILL handle the connection end on both sides
+ send(new Packet(PacketCommand.CLOSE_CONNECTION));
+ connectionIds.clear();
+
+ // Shutdown Email
+ if (mailer != null) {
+ mailer.shutdown();
+ }
+
+ // Unregister Server Browser Setup
+ if (serverBrowserUpdateTimer != null) {
+ serverBrowserUpdateTimer.cancel();
+ }
+
+ if ((metaServerUrl != null) && (!metaServerUrl.isBlank())) {
+ registerWithServerBrowser(false, metaServerUrl);
+ }
+ }
+
+ /**
+ * Returns an enumeration of all the command names
+ */
+ public Collection getAllCommandNames() {
+ return commandsHash.keySet();
+ }
+
+ /**
+ * Sent when a client attempts to connect.
+ */
+ void clientVersionCheck(int cn) {
+ sendToPending(cn, new Packet(PacketCommand.SERVER_VERSION_CHECK));
+ }
+
+ /**
+ * Returns a free connection id.
+ */
+ public int getFreeConnectionId() {
+ while ((getPendingConnection(connectionCounter) != null) ||
+ (getConnection(connectionCounter) != null) ||
+ (getPlayer(connectionCounter) != null)) {
+ connectionCounter++;
+ }
+ return connectionCounter;
+ }
+
+ /**
+ * Returns a free entity id. Perhaps this should be in Game instead.
+ */
+ @Deprecated(since = "0.51.0", forRemoval = true)
+ public int getFreeEntityId() {
+ return getGame().getNextEntityId();
+ }
+
+ /**
+ * Allow the player to set whatever parameters he is able to
+ */
+ private void receivePlayerInfo(Packet packet, int connId) throws InvalidPacketDataException {
+ Player player = packet.getPlayer(0);
+
+ if (player == null) {
+ return;
+ }
+
+ Player gamePlayer = getGame().getPlayer(connId);
+ if (null != gamePlayer) {
+ gamePlayer.setColour(player.getColour());
+ gamePlayer.setStartingPos(player.getStartingPos());
+ gamePlayer.setStartWidth(player.getStartWidth());
+ gamePlayer.setStartOffset(player.getStartOffset());
+ gamePlayer.setStartingAnyNWx(player.getStartingAnyNWx());
+ gamePlayer.setStartingAnyNWy(player.getStartingAnyNWy());
+ gamePlayer.setStartingAnySEx(player.getStartingAnySEx());
+ gamePlayer.setStartingAnySEy(player.getStartingAnySEy());
+ gamePlayer.setTeam(player.getTeam());
+ gamePlayer.setCamouflage(player.getCamouflage().clone());
+ gamePlayer.setNbrMFConventional(player.getNbrMFConventional());
+ gamePlayer.setNbrMFCommand(player.getNbrMFCommand());
+ gamePlayer.setNbrMFVibra(player.getNbrMFVibra());
+ gamePlayer.setNbrMFActive(player.getNbrMFActive());
+ gamePlayer.setNbrMFInferno(player.getNbrMFInferno());
+ gamePlayer.setNbrMFEMP(player.getNbrMFEMP());
+ if (gamePlayer.getConstantInitBonus() != player.getConstantInitBonus()) {
+ sendServerChat("Player " +
+ gamePlayer.getName() +
+ " changed their initiative bonus from " +
+ gamePlayer.getConstantInitBonus() +
+ " to " +
+ player.getConstantInitBonus() +
+ '.');
+ }
+ gamePlayer.setConstantInitBonus(player.getConstantInitBonus());
+ gamePlayer.setEmail(player.getEmail());
+ gamePlayer.setGroundObjectsToPlace(new ArrayList<>(player.getGroundObjectsToPlace()));
+ }
+ }
+
+ /**
+ * Correct a duplicate player name
+ *
+ * @param oldName the String old player name, that is a duplicate
+ *
+ * @return the String new player name
+ */
+ private String correctDupeName(String oldName) {
+ for (Player player : getGame().getPlayersList()) {
+ if (player.getName().equals(oldName)) {
+ // We need to correct it.
+ String newName = oldName;
+ int dupNum;
+ try {
+ dupNum = Integer.parseInt(oldName.substring(oldName.lastIndexOf('.') + 1));
+ dupNum++;
+ newName = oldName.substring(0, oldName.lastIndexOf('.'));
+ } catch (Exception e) {
+ // If this fails, we don't care much. Just assume it's the first time for this name.
+ dupNum = 2;
+ }
+ newName = newName.concat(".").concat(Integer.toString(dupNum));
+ return correctDupeName(newName);
+ }
+ }
+ return oldName;
+ }
+
+ private boolean receivePlayerVersion(Packet packet, int connId) throws InvalidPacketDataException {
+ final Version version = (Version) packet.getObject(0);
+
+ if (!SuiteConstants.VERSION.is(version)) {
+ final String message = String.format("Client/Server Version Mismatch -- Client: %s, Server: %s",
+ version,
+ SuiteConstants.VERSION);
+ LOGGER.error(message);
+
+ final Player player = getPlayer(connId);
+ sendServerChat(String.format("For %s, Server reports:%s%s",
+ ((player == null) ? "unknown player" : player.getName()),
+ System.lineSeparator(),
+ message));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Receives a player name, sent from a pending connection, and connects that connection.
+ */
+ private void receivePlayerName(Packet packet, int connId) throws InvalidPacketDataException {
+ final AbstractConnection conn = getPendingConnection(connId);
+ String name = packet.getStringValue(0);
+ boolean isBot = packet.getBooleanValue(1);
+ boolean returning = false;
+ String message;
+
+ // this had better be from a pending connection
+ if (conn == null) {
+ LOGGER.warn("Got a client name from a non-pending connection");
+ return;
+ }
+
+ // check if they're connecting with the same name as a ghost player
+ for (Player player : getGame().getPlayersList()) {
+ if (player.getName().equals(name) && player.isGhost()) {
+ returning = true;
+ player.setGhost(false);
+ player.setBot(isBot);
+ // switch id
+ connId = player.getId();
+ conn.setId(connId);
+ }
+ }
+
+ if (!returning) {
+ // Check to avoid duplicate names...
+ name = correctDupeName(name);
+ sendToPending(connId, new Packet(PacketCommand.SERVER_CORRECT_NAME, name));
+ }
+
+ // right, switch the connection into the "active" bin
+ connectionsPending.remove(conn);
+ connections.add(conn);
+ connectionIds.put(conn.getId(), conn);
+
+ // add and validate the player info
+ if (!returning) {
+ addNewPlayer(connId, name, isBot);
+ }
+
+ // if it is not the lounge phase, this player becomes an observer
+ Player player = getPlayer(connId);
+ if (!getGame().getPhase().isLounge() && (null != player) && (getGame().getEntitiesOwnedBy(player) < 1)) {
+ player.setObserver(true);
+ }
+
+ // send the player the messageOfTheDay
+ sendServerChat(connId, messageOfTheDay);
+
+ // send info that the player has connected
+ transmitPlayerConnect(player);
+
+ // tell them their local playerId
+ send(connId, new Packet(PacketCommand.LOCAL_PN, connId));
+
+ // send current game info
+ sendCurrentInfo(connId);
+
+ final boolean showIPAddressesInChat = PreferenceManager.getClientPreferences().getShowIPAddressesInChat();
+
+ try {
+ InetAddress[] addresses = InetAddress.getAllByName(InetAddress.getLocalHost().getHostName());
+ for (InetAddress address : addresses) {
+ message = String.format("s: Machine IP %s", address.getHostAddress());
+ LOGGER.info(message);
+
+ if (showIPAddressesInChat) {
+ sendServerChat(connId, message);
+ }
+ }
+ } catch (Exception ignored) {
+ }
+
+ message = String.format("s: Listening on port %d", serverSocket.getLocalPort());
+ LOGGER.info(message);
+
+ if (showIPAddressesInChat) {
+ // Send the port we're listening on. Only useful for the player on the server machine to check.
+ sendServerChat(connId, message);
+ }
+
+ // Get the player *again*, because they may have disconnected.
+ player = getPlayer(connId);
+ if (null != player) {
+ String who = String.format("%s connected from %s", player.getName(), getClient(connId).getInetAddress());
+ message = String.format("s: player #%d, %s", connId, who);
+ LOGGER.info(message);
+
+ if (showIPAddressesInChat) {
+ sendServerChat(who);
+ }
+ } // Found the player
+ }
+
+ /**
+ * Sends a player the info they need to look at the current phase. This is triggered when a player first connects to
+ * the server.
+ */
+ public void sendCurrentInfo(int connId) {
+ transmitPlayerConnect(getClient(connId));
+ gameManager.sendCurrentInfo(connId);
+ }
+
+ /**
+ * Adds a new player to the game
+ */
+ private Player addNewPlayer(int connId, String name, boolean isBot) {
+ int team = getTeam(isBot);
+ Player newPlayer = new Player(connId, name);
+ newPlayer.setBot(isBot);
+ PlayerColour colour = newPlayer.getColour();
+ final PlayerColour[] colours = PlayerColour.values();
+
+ for (Player player : getGame().getPlayersList()) {
+ if (player.getId() == newPlayer.getId()) {
+ continue;
+ }
+
+ if ((player.getColour() == colour) && (colours.length > (colour.ordinal() + 1))) {
+ colour = colours[colour.ordinal() + 1];
+ }
+ }
+
+ newPlayer.setColour(colour);
+ newPlayer.setCamouflage(new Camouflage(Camouflage.COLOUR_CAMOUFLAGE, colour.name()));
+ newPlayer.setTeam(Math.min(team, 5));
+ getGame().addPlayer(connId, newPlayer);
+ validatePlayerInfo(connId);
+ return newPlayer;
+ }
+
+ private int getTeam(boolean isBot) {
+ int team = Player.TEAM_UNASSIGNED;
+
+ if (getGame().getPhase().isLounge()) {
+ team = Player.TEAM_NONE;
+ final var gOpts = getGame().getOptions();
+ if (isBot || !gOpts.booleanOption(OptionsConstants.BASE_SET_DEFAULT_TEAM_1)) {
+ for (Player p : getGame().getPlayersList()) {
+ if (p.getTeam() > team) {
+ team = p.getTeam();
+ }
+ }
+ team++;
+ } else {
+ team = 1;
+ }
+
+ }
+
+ return team;
+ }
+
+ /**
+ * Validates the player info.
+ */
+ public void validatePlayerInfo(int playerId) {
+ final Player player = getPlayer(playerId);
+
+ if (player != null) {
+ // TODO : check for duplicate or reserved names
+
+ // Colour Assignment
+ final PlayerColour[] playerColours = PlayerColour.values();
+ boolean allUsed = true;
+ Set colourUtilization = new HashSet<>();
+ for (Player otherPlayer : getGame().getPlayersList()) {
+ if (otherPlayer.getId() != playerId) {
+ colourUtilization.add(otherPlayer.getColour());
+ } else {
+ allUsed = false;
+ }
+ }
+
+ if (!allUsed && colourUtilization.contains(player.getColour())) {
+ for (PlayerColour colour : playerColours) {
+ if (!colourUtilization.contains(colour)) {
+ player.setColour(colour);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when it's been determined that an actual player disconnected. Notifies the other players and does the
+ * appropriate housekeeping.
+ */
+ void disconnected(Player player) {
+ // Only do this if nobody is trying to save a game currently
+ GAME_LOCK.lock();
+ try {
+ gameManager.disconnect(player);
+ } finally {
+ GAME_LOCK.unlock();
+ }
+ }
+
+ public void resetGame() {
+ gameManager.resetGame();
+ }
+
+ /**
+ * send a packet to the connection tells it load a locally saved game
+ *
+ * @param connId The int connection id to send to
+ * @param sFile The String filename to use
+ */
+ public void sendLoadGame(int connId, String sFile) {
+ String sFinalFile = sFile;
+ if (!sFinalFile.endsWith(MMConstants.SAVE_FILE_EXT) && !sFinalFile.endsWith(MMConstants.SAVE_FILE_GZ_EXT)) {
+ sFinalFile = sFile + MMConstants.SAVE_FILE_EXT;
+ }
+ if (!sFinalFile.endsWith(".gz")) {
+ sFinalFile = sFinalFile + ".gz";
+ }
+ send(connId, new Packet(PacketCommand.LOAD_SAVEGAME, sFinalFile));
+ }
+
+ /**
+ * load the game
+ *
+ * @param f The File to load
+ *
+ * @return A boolean value whether the loading was successful
+ */
+ public boolean loadGame(File f) {
+ return loadGame(f, true);
+ }
+
+ /**
+ * load the game
+ *
+ * @param f The File to load
+ * @param sendInfo Determines whether the connections should be updated with current info. This may be false if some
+ * reconnection remapping needs to be done first.
+ *
+ * @return A boolean value whether the loading was successful
+ */
+ public boolean loadGame(File f, boolean sendInfo) {
+ String message = String.format("s: Loading saved game file '%s'", f.getAbsolutePath());
+ LOGGER.info(message);
+
+ Game newGame;
+ try (InputStream is = new FileInputStream(f)) {
+ InputStream gzi;
+
+ if (f.getName().toLowerCase().endsWith(".gz")) {
+ gzi = new GZIPInputStream(is);
+ } else {
+ gzi = is;
+ }
+
+ XStream xStream = SerializationHelper.getLoadSaveGameXStream();
+ newGame = (Game) xStream.fromXML(gzi);
+ } catch (Exception e) {
+ message = String.format("Unable to load file: %s", f);
+ LOGGER.error(e, message);
+ return false;
+ }
+
+ setGame(newGame);
+
+ // Saves created before mapName tracking was added land here with Board.BOARD_NAME_UNNAMED.
+ // Reconstruct the name from the saved MapSettings so the Ruler title and other UI surfaces
+ // identify the map. Only fills in when the board has the placeholder; saves with an explicit
+ // name keep theirs.
+ backfillBoardNameFromMapSettings(newGame);
+
+ if (!sendInfo) {
+ return true;
+ }
+
+ // update all the clients with the new game info
+ for (AbstractConnection conn : connections) {
+ sendCurrentInfo(conn.getId());
+ }
+ return true;
+ }
+
+ /**
+ * If the deserialized game's board has the placeholder map name (saves predate the name fix),
+ * reconstructs a display name from the saved MapSettings' selected board file names. The actual
+ * board hex data is unchanged; only the displayable name is filled in.
+ */
+ private static void backfillBoardNameFromMapSettings(Game game) {
+ Board board = game.getBoard();
+ if (board == null || !Board.BOARD_NAME_UNNAMED.equals(board.getBoardName())) {
+ return;
+ }
+ MapSettings ms = game.getMapSettings();
+ if (ms == null) {
+ return;
+ }
+ List selected = ms.getBoardsSelectedVector();
+ if (selected == null || selected.isEmpty()) {
+ return;
+ }
+ LinkedHashSet names = new LinkedHashSet<>();
+ for (String raw : selected) {
+ if (raw == null) {
+ continue;
+ }
+ String name = raw;
+ if (name.startsWith(Board.BOARD_REQUEST_ROTATION)) {
+ name = name.substring(Board.BOARD_REQUEST_ROTATION.length());
+ }
+ // Skip placeholders that don't correspond to an on-disk file
+ if (MapSettings.BOARD_GENERATED.equals(name)
+ || MapSettings.BOARD_RANDOM.equals(name)
+ || name.startsWith(MapSettings.BOARD_SURPRISE)) {
+ continue;
+ }
+ if (!name.isBlank()) {
+ names.add(name);
+ }
+ }
+ if (names.isEmpty()) {
+ return;
+ }
+ String reconstructed = names.size() == 1 ? names.iterator().next() : String.join(" + ", names);
+ board.setMapName(reconstructed);
+ }
+
+ public void saveGame(String fileName) {
+ GAME_LOCK.lock();
+ try {
+ gameManager.saveGame(fileName);
+ } finally {
+ GAME_LOCK.unlock();
+ }
+ }
+
+ public void sendSaveGame(int connId, String fileName, String localPath) {
+ GAME_LOCK.lock();
+ try {
+ gameManager.sendSaveGame(connId, fileName, localPath);
+ } finally {
+ GAME_LOCK.unlock();
+ }
+ }
+
+ /**
+ * When the load command is used, there is a list of already connected players which have assigned names and player
+ * id numbers with the id numbers matching the connection numbers. When a new game is loaded, this mapping may need
+ * to be updated. This method takes a map of player names to their current ids, and uses the list of players to
+ * figure out what the current ids should change to.
+ *
+ * @param nameToIdMap This maps a player name to the current connection ID
+ * @param idToNameMap This maps a current conn ID to a player name, and is just the inverse mapping from
+ * nameToIdMap
+ */
+ public void remapConnIds(Map nameToIdMap, Map idToNameMap) {
+ // Keeps track of connections without Ids
+ List unassignedConnections = new ArrayList<>();
+
+ // Keep track of which ids are used
+ Set usedPlayerIds = new HashSet<>();
+ Set currentPlayerNames = new HashSet<>();
+
+ for (Player p : getGame().getPlayersList()) {
+ currentPlayerNames.add(p.getName());
+ }
+
+ // Map the old connection ID to new value
+ Map connIdRemapping = new HashMap<>();
+ for (Player p : getGame().getPlayersList()) {
+ // Check to see if this player was already connected
+ Integer oldId = nameToIdMap.get(p.getName());
+ if ((oldId != null) && (oldId != p.getId())) {
+ connIdRemapping.put(oldId, p.getId());
+ }
+
+ // If the old and new Ids match, make sure we remove ghost status
+ if ((oldId != null) && (oldId == p.getId())) {
+ p.setGhost(false);
+ }
+
+ // Check to see if this player's ID is taken
+ String oldName = idToNameMap.get(p.getId());
+ if ((oldName != null) && !oldName.equals(p.getName())) {
+ // If this name doesn't belong to a current player, unassign it
+ if (!currentPlayerNames.contains(oldName)) {
+ unassignedConnections.add(connectionIds.get(p.getId()));
+ // Make sure we don't add this to unassigned connections twice
+ connectionIds.remove(p.getId());
+ }
+ // If it does belong to a current player, it'll get handled
+ // when that player comes up
+ }
+ // Keep track of what Ids are used
+ usedPlayerIds.add(p.getId());
+ }
+
+ // Remap old connection Ids to new ones
+ for (Entry currentConnect : connIdRemapping.entrySet()) {
+ Integer newId = currentConnect.getValue();
+ AbstractConnection conn = connectionIds.get(currentConnect.getKey());
+ conn.setId(newId);
+
+ // If this ID is used, make sure we reassign that connection
+ if (connectionIds.containsKey(newId)) {
+ unassignedConnections.add(connectionIds.get(newId));
+ }
+ // Map the new ID
+ connectionIds.put(newId, conn);
+
+ getGame().getPlayer(newId).setGhost(false);
+ send(newId, new Packet(PacketCommand.LOCAL_PN, newId));
+ }
+
+ // It's possible we have players not in the saved game, add 'em
+ for (AbstractConnection conn : unassignedConnections) {
+ int newId = 0;
+
+ while (usedPlayerIds.contains(newId)) {
+ newId++;
+ }
+
+ String name = idToNameMap.get(conn.getId());
+ conn.setId(newId);
+ Player newPlayer = addNewPlayer(newId, name, false);
+ newPlayer.setObserver(true);
+ connectionIds.put(newId, conn);
+ send(newId, new Packet(PacketCommand.LOCAL_PN, newId));
+ }
+
+ // Ensure all clients are up-to-date on player info
+ transmitAllPlayerUpdates();
+ }
+
+ /**
+ * Shortcut to game.getPlayer(id)
+ */
+ public Player getPlayer(int id) {
+ return getGame().getPlayer(id);
+ }
+
+ /**
+ * a shorter name for getConnection()
+ */
+ private AbstractConnection getClient(int connId) {
+ return getConnection(connId);
+ }
+
+ /**
+ * Executes the process on each active connection.
+ *
+ * @param process The process to execute.
+ */
+ public void forEachConnection(Consumer process) {
+ connections.forEach(process);
+ }
+
+ /**
+ * Returns a connection, indexed by id
+ */
+ public AbstractConnection getConnection(int connId) {
+ return connectionIds.get(connId);
+ }
+
+ /**
+ * Returns a pending connection
+ */
+ public @Nullable AbstractConnection getPendingConnection(int connId) {
+ for (AbstractConnection conn : connectionsPending) {
+ if (conn.getId() == connId) {
+ return conn;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sends out all player info to the specified connection
+ */
+ private void transmitPlayerConnect(AbstractConnection connection) {
+ for (Player player : getGame().getPlayersList()) {
+ var connectionId = connection.getId();
+ connection.send(createPlayerConnectPacket(player, player.getId() != connectionId));
+ }
+ }
+
+ /**
+ * Sends out player info to all connections
+ */
+ private void transmitPlayerConnect(Player player) {
+ for (var connection : connections) {
+ var playerId = player.getId();
+ connection.send(createPlayerConnectPacket(player, playerId != connection.getId()));
+ }
+ }
+
+ /**
+ * Creates a packet informing that the player has connected
+ */
+ private Packet createPlayerConnectPacket(Player player, boolean isPrivate) {
+ var playerId = player.getId();
+ var destPlayer = player;
+ if (isPrivate) {
+ // Sending the player's data to another player's
+ // connection, need to redact any private data
+ destPlayer = player.copy();
+ destPlayer.redactPrivateData();
+ }
+ return new Packet(PacketCommand.PLAYER_ADD, playerId, destPlayer);
+ }
+
+ /**
+ * Sends out player info updates for a player to all connections
+ */
+ void transmitPlayerUpdate(Player player) {
+ for (var connection : connections) {
+ var playerId = player.getId();
+ var destPlayer = player;
+
+ if (playerId != connection.getId()) {
+ // Sending the player's data to another player's
+ // connection, need to redact any private data
+ destPlayer = player.copy();
+ destPlayer.redactPrivateData();
+ }
+ connection.send(new Packet(PacketCommand.PLAYER_UPDATE, playerId, destPlayer));
+ }
+ }
+
+ /**
+ * Sends out the player info updates for all players to all connections
+ */
+ private void transmitAllPlayerUpdates() {
+ for (Player player : getGame().getPlayersList()) {
+ transmitPlayerUpdate(player);
+ }
+ }
+
+ /**
+ * Player can request its own change of team
+ *
+ * @param teamID target team id
+ * @param player player requesting the change
+ */
+ public void requestTeamChangeForPlayer(int teamID, Player player) {
+ gameManager.requestTeamChangeForPlayer(teamID, player);
+ }
+
+ public void requestGameMaster(Player player) {
+ gameManager.requestGameMaster(player);
+ }
+
+ public static String formatChatMessage(String origin, String message) {
+ return origin + ": " + message;
+ }
+
+ /**
+ * Transmits a chat message to all players
+ */
+ public void sendChat(int connId, String origin, String message) {
+ send(connId, new Packet(PacketCommand.CHAT, formatChatMessage(origin, message)));
+ }
+
+ /**
+ * Transmits a chat message to all players
+ */
+ public void sendChat(String origin, String message) {
+ send(new Packet(PacketCommand.CHAT, formatChatMessage(origin, message)));
+ }
+
+ public void sendServerChat(int connId, String message) {
+ sendChat(connId, ORIGIN, message);
+ }
+
+ public void sendServerChat(String message) {
+ sendChat(ORIGIN, message);
+ }
+
+ /**
+ * Sends the given packet to all connections (all connected Clients = players).
+ */
+ void send(Packet packet) {
+ connections.stream().filter(Objects::nonNull).forEach(connection -> connection.send(packet));
+ }
+
+ /**
+ * Sends the given packet to the given connection (= player ID) if it is not null. Does nothing otherwise.
+ */
+ public void send(int connId, Packet packet) {
+ AbstractConnection connection = getClient(connId);
+
+ if (connection != null) {
+ connection.send(packet);
+ }
+ }
+
+ /**
+ * Send a packet to a pending connection
+ */
+ private void sendToPending(int connId, Packet packet) {
+ AbstractConnection pendingConn = getPendingConnection(connId);
+
+ if (pendingConn != null) {
+ pendingConn.send(packet);
+ }
+ // What should we do if we've lost this client?
+ // For now, nothing.
+ }
+
+ /**
+ * Process an in-game command
+ */
+ private void processCommand(int connId, String commandString) {
+ // all tokens are read as strings; if they're numbers, string-ize 'em.
+ // replaced the tokenizer with the split function.
+ String[] args = commandString.split("\\s+");
+
+ // figure out which command this is
+ String commandName = args[0].substring(1);
+
+ // process it
+ ServerCommand command = getCommand(commandName);
+ if (command != null) {
+ command.run(connId, args);
+ } else {
+ sendServerChat(connId, "Command not recognized. Type /help for a list of commands.");
+ }
+ }
+
+ /**
+ * Process a packet from a connection.
+ *
+ * @param connId - the int ID the connection that received the packet.
+ * @param packet - the Packet to be processed.
+ */
+ protected void handle(int connId, Packet packet) {
+ Player player = getGame().getPlayer(connId);
+
+ // Check player. Please note, the connection may be pending.
+ if ((null == player) && (null == getPendingConnection(connId))) {
+ String message = String.format("Server does not recognize player at connection %d", connId);
+ LOGGER.error(message);
+ return;
+ }
+
+ if (packet == null) {
+ LOGGER.error("Got null packet");
+ return;
+ }
+
+ // act on it
+ try {
+ switch (packet.command()) {
+ case CLIENT_VERSIONS:
+ final boolean valid = receivePlayerVersion(packet, connId);
+ if (valid) {
+ sendToPending(connId, new Packet(PacketCommand.SERVER_GREETING));
+ } else {
+ sendToPending(connId, new Packet(PacketCommand.ILLEGAL_CLIENT_VERSION, SuiteConstants.VERSION));
+ getPendingConnection(connId).close();
+ }
+ break;
+ case CLOSE_CONNECTION:
+ // We have a client going down!
+ AbstractConnection c = getConnection(connId);
+ if (c != null) {
+ c.close();
+ }
+ break;
+ case CLIENT_NAME:
+ receivePlayerName(packet, connId);
+ break;
+ case PLAYER_UPDATE:
+ receivePlayerInfo(packet, connId);
+ validatePlayerInfo(connId);
+ transmitPlayerUpdate(getPlayer(connId));
+ break;
+ case CHAT:
+ String chat = packet.getStringValue(0);
+ if (chat.startsWith("/")) {
+ processCommand(connId, chat);
+ } else if (packet.data().length > 1) {
+ connId = packet.getIntValue(1);
+ if (connId == Player.PLAYER_NONE) {
+ sendServerChat(chat);
+ } else {
+ sendServerChat(connId, chat);
+ }
+ } else {
+ if (player != null) {
+ sendChat(player.getName(), chat);
+ }
+ }
+
+ // Easter eggs. Happy April Fool's Day!!
+ if (DUNE_CALL.equalsIgnoreCase(chat)) {
+ sendServerChat(DUNE_RESPONSE);
+ } else if (STAR_WARS_CALL.equalsIgnoreCase(chat)) {
+ sendServerChat(STAR_WARS_RESPONSE);
+ } else if (INVADER_ZIM_CALL.equalsIgnoreCase(chat)) {
+ sendServerChat(INVADER_ZIM_RESPONSE);
+ } else if (WARGAMES_CALL.equalsIgnoreCase(chat)) {
+ wargamesResponse();
+ }
+
+ break;
+ case LOAD_GAME:
+ try {
+ sendServerChat(getPlayer(connId).getName() + " loaded a new game.");
+ setGame((Game) packet.getObject(0));
+ for (AbstractConnection conn : connections) {
+ sendCurrentInfo(conn.getId());
+ }
+ } catch (Exception e) {
+ LOGGER.error(e, "Error loading save game sent from client");
+ }
+ break;
+ default:
+ // We don't want to change the game state while other threads may be in the game
+ GAME_LOCK.lock();
+ try {
+ gameManager.handlePacket(connId, packet);
+ } finally {
+ GAME_LOCK.unlock();
+ }
+ }
+ } catch (InvalidPacketDataException e) {
+ LOGGER.error("Invalid packet data:", e);
+ }
+ }
+
+ private void wargamesResponse() {
+ sendServerChat(WARGAMES_RESPONSE);
+ }
+
+ /**
+ * Listen for incoming clients.
+ */
+ @Override
+ public void run() {
+ String message;
+ Thread currentThread = Thread.currentThread();
+ LOGGER.info("s: listening for clients...");
+ while (connector == currentThread) {
+ try {
+ Socket s = serverSocket.accept();
+ synchronized (serverLock) {
+ int id = getFreeConnectionId();
+ message = String.format("s: accepting player connection #%d...", id);
+ LOGGER.info(message);
+
+ AbstractConnection c = ConnectionFactory.getInstance().createServerConnection(s, id);
+ c.addConnectionListener(connectionListener);
+ c.open();
+ connectionsPending.add(c);
+ ConnectionHandler ch = new ConnectionHandler(c);
+ Thread newConnThread = new Thread(ch, "Connection " + id);
+ newConnThread.start();
+ connectionHandlers.put(id, ch);
+
+ clientVersionCheck(id);
+ ConnectionWatchdog w = new ConnectionWatchdog(this, id);
+ watchdogTimer.schedule(w, 1000, 500);
+ }
+ } catch (Exception ignored) {
+
+ }
+ }
+ }
+
+ /**
+ * @return a String representing the hostname
+ */
+ public String getHost() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (Exception ex) {
+ LOGGER.error(ex, "Get Host exception");
+ return "";
+ }
+ }
+
+ /**
+ * @return the int this server is listening on
+ */
+ public int getPort() {
+ return serverSocket.getLocalPort();
+ }
+
+ /**
+ * @return the current server instance. This may be null if a server has not been started
+ */
+ public static @Nullable Server getServerInstance() {
+ return serverInstance;
+ }
+
+ private void registerWithServerBrowser(boolean register, String urlString) {
+ try {
+ URL url = new URL(urlString);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setDoOutput(true);
+ conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+
+ OutputStream os = conn.getOutputStream();
+ DataOutputStream dos = new DataOutputStream(os);
+ StringBuilder content = new StringBuilder("port=" +
+ URLEncoder.encode(Integer.toString(serverSocket.getLocalPort()),
+ StandardCharsets.UTF_8));
+ if (register) {
+ for (AbstractConnection abstractConnection : connections) {
+ content.append("&players[]=").append(getPlayer(abstractConnection.getId()).getName());
+ }
+
+ if (!getGame().getPhase().isLounge() && !getGame().getPhase().isUnknown()) {
+ content.append("&close=yes");
+ }
+
+ content.append("&version=").append(SuiteConstants.VERSION);
+
+ if (isPassworded()) {
+ content.append("&pw=yes");
+ }
+ } else {
+ content.append("&delete=yes");
+ }
+
+ if (serverAccessKey != null) {
+ content.append("&key=").append(serverAccessKey);
+ }
+
+ dos.writeBytes(content.toString());
+ dos.flush();
+
+ InputStream is = conn.getInputStream();
+ InputStreamReader isr = new InputStreamReader(is);
+ BufferedReader br = new BufferedReader(isr);
+ String line;
+ if (conn.getResponseCode() == 200) {
+ while ((line = br.readLine()) != null) {
+ if (serverAccessKey == null) {
+ serverAccessKey = line;
+ }
+ }
+ }
+ } catch (Exception ignored) {
+
+ }
+ }
+
+ /**
+ * Adds a roll report to the GameManager's current pending report list.
+ *
+ * @param roll The roll to add
+ */
+ public void reportRoll(Roll roll) {
+ gameManager.addReport(getGame().getNewReport(1230).addRoll(roll));
+ }
+}
diff --git a/results/patched8.csv b/results/patched8.csv
new file mode 100644
index 0000000..446ca46
--- /dev/null
+++ b/results/patched8.csv
@@ -0,0 +1,31 @@
+elapsedSec,label,gamesStarted,gamesLive,totalRounds,rssKb,heapUsedKb,heapCommittedKb,nonHeapUsedKb,metaspaceKb,liveHeapKb
+0.8,jvm-start,0,0,0,48276,1980,16384,3232,1213,2061
+2.9,unit-cache-loaded,0,0,0,333520,92156,307200,38950,25487,92156
+4.5,settling-0,1,1,0,570452,161352,307200,74693,44313,-1
+5.5,settling-0,1,1,1,597136,251390,307200,83668,45509,-1
+6.5,settling-0,1,1,1,618932,258578,307200,89115,46291,-1
+7.5,settling-0,1,1,1,664900,210413,307200,91267,46325,-1
+8.5,settling-0,1,1,2,736596,138843,368640,94758,47486,-1
+9.5,settling-0,1,1,2,756344,205495,368640,96662,47551,-1
+10.5,settling-0,1,1,2,836008,299562,442368,97480,47560,-1
+11.5,settling-0,1,1,2,836636,379890,442368,96774,47632,-1
+12.5,settling-0,1,1,3,839436,319019,442368,99164,47786,-1
+13.4,settled-game-1,1,1,3,845768,376630,442368,76597,47611,380016
+14.9,settling-1,2,2,4,1301324,548427,884736,79583,47738,-1
+15.9,settling-1,2,2,4,1281912,298409,884736,80142,47780,-1
+16.9,settling-1,2,2,6,1298284,694110,884736,81463,47888,-1
+17.9,settling-1,2,2,6,1298804,514508,884736,81870,47895,-1
+18.9,settling-1,2,2,6,1298840,445865,884736,82193,47905,-1
+19.9,settling-1,2,2,6,1299316,392427,884736,81960,47944,-1
+20.9,settling-1,2,2,7,1299560,651182,884736,83255,47977,-1
+21.9,settling-1,2,2,7,1348528,702077,884736,86970,48481,-1
+22.9,settling-1,2,2,8,1377356,375625,884736,88603,48522,-1
+23.8,settled-game-2,2,2,8,1143264,389865,643072,77671,48590,395145
+25.3,settling-2,3,3,9,1369040,440810,991232,82712,48727,-1
+26.3,settling-2,3,3,10,1536644,639599,991232,84087,48764,-1
+27.3,settling-2,3,3,11,1537072,752695,991232,85919,48915,-1
+28.3,settling-2,3,3,11,1537064,341778,991232,84915,48919,-1
+29.3,settling-2,3,3,12,1538668,665717,991232,85864,48994,-1
+30.3,settling-2,3,3,14,1543808,689700,991232,88308,49894,-1
+31.3,settling-2,3,3,15,1544596,814842,991232,88748,49948,-1
+32.3,settling-2,3,3,16,1544704,669312,991232,89225,49979,-1
diff --git a/results/staggered8.csv b/results/staggered8.csv
index 7e0edc7..c616e55 100644
--- a/results/staggered8.csv
+++ b/results/staggered8.csv
@@ -18,3 +18,213 @@ elapsedSec,label,gamesStarted,gamesLive,totalRounds,rssKb,heapUsedKb,heapCommitt
18.9,settling-1,2,2,5,1075328,313845,696320,79615,47931,-1
19.9,settling-1,2,2,5,1075356,533628,696320,79652,47936,-1
20.9,settling-1,2,2,6,1076796,441359,696320,80060,47994,-1
+21.7,settled-game-2,2,2,6,1007072,292737,602112,80715,48060,293767
+23.5,settling-2,3,3,7,1041044,286572,622592,80817,48161,-1
+24.5,settling-2,3,3,7,1044116,246957,622592,80451,48162,-1
+25.5,settling-2,3,3,8,1046012,353156,622592,82020,48265,-1
+26.5,settling-2,3,3,8,1072640,262242,622592,83079,48753,-1
+27.5,settling-2,3,3,8,1101728,256394,622592,84739,48756,-1
+28.5,settling-2,3,3,8,1103480,307582,622592,81842,48767,-1
+29.5,settling-2,3,3,9,1125812,268958,622592,85738,48833,-1
+30.4,settled-game-3,3,3,9,1132220,330165,622592,82194,48835,333360
+31.9,settling-3,4,4,9,1158672,373028,647168,83008,48888,-1
+32.9,settling-3,4,4,9,1158536,373028,647168,83008,48888,-1
+33.9,settling-3,4,4,9,1158536,373028,647168,83008,48888,-1
+34.9,settling-3,4,4,9,1158484,373028,647168,82446,48888,-1
+35.9,settling-3,4,4,9,1104936,373028,647168,82446,48888,-1
+36.9,settling-3,4,4,9,1104932,373028,647168,81890,48888,-1
+37.9,settling-3,4,4,9,1104932,373028,647168,81890,48888,-1
+38.9,settling-3,4,4,9,1104932,373028,647168,81890,48888,-1
+39.9,settling-3,4,4,9,1104932,377124,647168,81890,48888,-1
+40.9,settling-3,4,4,9,1104932,377124,647168,81890,48888,-1
+41.9,settling-3,4,4,9,1104932,377124,647168,81890,48888,-1
+42.9,settling-3,4,4,9,1104932,377124,647168,81890,48888,-1
+43.9,settling-3,4,4,9,1104932,377124,647168,81894,48888,-1
+44.9,settling-3,4,4,9,1104932,377124,647168,81895,48888,-1
+45.9,settling-3,4,4,9,1104932,377124,647168,81897,48888,-1
+46.9,settling-3,4,4,9,1104936,377124,647168,81909,48892,-1
+47.9,settling-3,4,4,9,1104940,377124,647168,81916,48894,-1
+48.9,settling-3,4,4,9,1104940,377124,647168,81916,48894,-1
+49.9,settling-3,4,4,9,1104940,377124,647168,81916,48894,-1
+50.9,settling-3,4,4,9,1104940,377124,647168,81920,48895,-1
+51.9,settling-3,4,4,9,1104940,377124,647168,81921,48895,-1
+52.9,settling-3,4,4,9,1104940,377124,647168,81930,48895,-1
+53.9,settling-3,4,4,9,1104940,377124,647168,81930,48895,-1
+54.9,settling-3,4,4,9,1104940,377124,647168,81930,48895,-1
+55.9,settling-3,4,4,9,1104940,377124,647168,81931,48895,-1
+56.9,settling-3,4,4,9,1104940,377124,647168,81932,48895,-1
+57.9,settling-3,4,4,9,1104940,377124,647168,81935,48895,-1
+58.9,settling-3,4,4,9,1104940,377124,647168,81935,48895,-1
+59.9,settling-3,4,4,9,1104940,377124,647168,81936,48895,-1
+60.9,settling-3,4,4,9,1104940,377124,647168,81936,48895,-1
+62.0,settling-3,4,4,9,1104940,377124,647168,81936,48895,-1
+63.0,settling-3,4,4,9,1104940,377124,647168,81937,48895,-1
+64.0,settling-3,4,4,9,1104940,377124,647168,81937,48895,-1
+65.0,settling-3,4,4,9,1104940,377124,647168,81937,48895,-1
+66.0,settling-3,4,4,9,1104940,377124,647168,81937,48896,-1
+67.0,settling-3,4,4,9,1104940,377124,647168,81943,48896,-1
+68.0,settling-3,4,4,9,1104940,377124,647168,81945,48897,-1
+69.0,settling-3,4,4,9,1104940,377124,647168,81949,48897,-1
+70.0,settling-3,4,4,9,1104940,377124,647168,81955,48897,-1
+71.0,settling-3,4,4,9,1104940,377124,647168,81955,48897,-1
+72.0,settling-3,4,4,9,1104940,377124,647168,81955,48897,-1
+73.0,settling-3,4,4,9,1104940,377124,647168,81960,48898,-1
+74.0,settling-3,4,4,9,1104944,377124,647168,81960,48900,-1
+75.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+76.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+77.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+78.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+79.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+80.0,settling-3,4,4,9,1104952,377124,647168,81992,48908,-1
+81.0,settling-3,4,4,9,1104952,377124,647168,81992,48909,-1
+82.0,settling-3,4,4,9,1104956,377124,647168,82007,48912,-1
+83.0,settling-3,4,4,9,1104956,377124,647168,82010,48912,-1
+84.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+85.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+86.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+87.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+88.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+89.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+90.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+91.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+92.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+93.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+94.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+95.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+96.0,settling-3,4,4,9,1104960,377124,647168,82019,48913,-1
+97.0,settling-3,4,4,9,1104960,377124,647168,82019,48915,-1
+98.0,settling-3,4,4,9,1104960,377124,647168,82052,48915,-1
+99.0,settling-3,4,4,9,1104960,377124,647168,82052,48915,-1
+100.0,settling-3,4,4,9,1104960,377124,647168,82054,48916,-1
+101.0,settling-3,4,4,9,1104964,377124,647168,82063,48917,-1
+102.0,settling-3,4,4,9,1104964,377124,647168,82063,48917,-1
+103.0,settling-3,4,4,9,1104964,377124,647168,82066,48917,-1
+104.0,settling-3,4,4,9,1104964,377124,647168,82067,48917,-1
+105.0,settling-3,4,4,9,1104964,377124,647168,82067,48917,-1
+106.0,settling-3,4,4,9,1104964,381220,647168,82067,48917,-1
+107.0,settling-3,4,4,9,1104964,381220,647168,82067,48917,-1
+108.0,settling-3,4,4,9,1104964,381220,647168,82067,48917,-1
+109.0,settling-3,4,4,9,1104964,381220,647168,82067,48917,-1
+110.0,settling-3,4,4,9,1104964,381220,647168,82074,48917,-1
+111.0,settling-3,4,4,9,1104964,381220,647168,82074,48917,-1
+112.0,settling-3,4,4,9,1104964,381220,647168,82074,48917,-1
+113.0,settling-3,4,4,9,1104964,381220,647168,82074,48917,-1
+114.0,settling-3,4,4,9,1104964,381220,647168,82074,48917,-1
+115.0,settling-3,4,4,9,1104964,381220,647168,82075,48917,-1
+116.0,settling-3,4,4,9,1104964,381220,647168,82081,48917,-1
+117.0,settling-3,4,4,9,1104964,381220,647168,82081,48917,-1
+118.0,settling-3,4,4,9,1104964,381220,647168,82082,48917,-1
+119.0,settling-3,4,4,9,1104964,381220,647168,82082,48917,-1
+120.0,settling-3,4,4,9,1104964,381220,647168,82082,48919,-1
+121.0,settling-3,4,4,9,1104972,381220,647168,82114,48928,-1
+122.0,settling-3,4,4,9,1104972,381220,647168,82115,48928,-1
+123.0,settling-3,4,4,9,1104976,381220,647168,82126,48930,-1
+124.0,settling-3,4,4,9,1104976,381220,647168,82126,48931,-1
+125.0,settling-3,4,4,9,1104980,381220,647168,82151,48935,-1
+126.0,settling-3,4,4,9,1104980,381220,647168,82151,48937,-1
+127.0,settling-3,4,4,9,1104992,381220,647168,82195,48948,-1
+128.0,settling-3,4,4,9,1104992,381220,647168,82195,48948,-1
+129.0,settling-3,4,4,9,1104992,381220,647168,82195,48948,-1
+130.0,settling-3,4,4,9,1104996,381220,647168,82211,48952,-1
+131.0,settling-3,4,4,9,1105000,381220,647168,82228,48955,-1
+132.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+133.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+134.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+135.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+136.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+137.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+138.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+139.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+140.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+141.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+142.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+143.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+144.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+145.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+146.0,settling-3,4,4,9,1105000,381220,647168,82230,48955,-1
+147.0,settling-3,4,4,9,1105000,381220,647168,82231,48955,-1
+148.0,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+149.0,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+150.0,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+151.0,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+152.0,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+153.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+154.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+155.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+156.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+157.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+158.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+159.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+160.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+161.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+162.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+163.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+164.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+165.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+166.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+167.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+168.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+169.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+170.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+171.1,settling-3,4,4,9,1105000,381220,647168,82233,48955,-1
+172.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+173.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+174.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+175.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+176.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+177.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+178.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+179.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+180.1,settling-3,4,4,9,1105000,385316,647168,82233,48955,-1
+181.1,settling-3,4,4,9,1105156,385316,647168,82233,48955,-1
+182.1,settling-3,4,4,9,1105156,385316,647168,82233,48955,-1
+183.1,settling-3,4,4,9,1105156,385316,647168,82233,48955,-1
+184.1,settling-3,4,4,9,1105156,385316,647168,82233,48955,-1
+185.1,settling-3,4,4,9,1105156,385316,647168,82233,48955,-1
+186.1,settling-3,4,4,9,1105156,385316,647168,82235,48955,-1
+187.1,settling-3,4,4,9,1105156,385316,647168,82235,48955,-1
+188.1,settling-3,4,4,9,1105156,385316,647168,82235,48955,-1
+189.1,settling-3,4,4,9,1105156,385316,647168,82235,48955,-1
+190.1,settling-3,4,4,9,1105156,385316,647168,82243,48956,-1
+191.1,settling-3,4,4,9,1105168,385316,647168,82841,48965,-1
+192.1,settling-3,4,4,9,1105168,385316,647168,82841,48965,-1
+193.1,settling-3,4,4,9,1105168,385316,647168,82841,48965,-1
+194.1,settling-3,4,4,9,1105168,385316,647168,82841,48965,-1
+195.1,settling-3,4,4,9,1105168,385316,647168,82848,48966,-1
+196.1,settling-3,4,4,9,1105168,385316,647168,82848,48966,-1
+197.1,settling-3,4,4,9,1105168,385316,647168,82294,48966,-1
+198.1,settling-3,4,4,9,1105168,385316,647168,82294,48966,-1
+199.1,settling-3,4,4,9,1105168,385316,647168,82294,48966,-1
+200.1,settling-3,4,4,9,1105168,385316,647168,82294,48966,-1
+201.1,settling-3,4,4,9,1105168,385316,647168,82302,48968,-1
+202.1,settling-3,4,4,9,1105168,385316,647168,82302,48968,-1
+203.1,settling-3,4,4,9,1105168,385316,647168,82302,48968,-1
+204.1,settling-3,4,4,9,1105168,385316,647168,82302,48968,-1
+205.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+206.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+207.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+208.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+209.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+210.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+211.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+212.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+213.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+214.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+215.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+216.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+217.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+218.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+219.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+220.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+221.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+222.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+223.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+224.1,settling-3,4,4,9,1105168,389412,647168,82305,48968,-1
+225.1,settling-3,4,4,9,1105168,389412,647168,82305,48970,-1
+226.1,settling-3,4,4,9,1105172,389412,647168,82315,48971,-1
+227.1,settling-3,4,4,9,1105172,389412,647168,82315,48971,-1
+228.1,settling-3,4,4,9,1105172,389412,647168,82315,48971,-1
+229.1,settling-3,4,4,9,1105172,389412,647168,82319,48972,-1
+230.1,settling-3,4,4,9,1105176,389412,647168,82334,48976,-1
+231.1,settling-3,4,4,9,1105176,389412,647168,82340,48977,-1
+232.1,settling-3,4,4,9,1105176,389412,647168,82340,48977,-1
diff --git a/results/stall-jstack.txt b/results/stall-jstack.txt
new file mode 100644
index 0000000..bb53d3b
--- /dev/null
+++ b/results/stall-jstack.txt
@@ -0,0 +1,735 @@
+2026-07-30 14:35:35
+Full thread dump OpenJDK 64-Bit Server VM (21.0.12+8-LTS mixed mode, sharing):
+
+Threads class SMR info:
+_java_thread_list=0x00007678d8001fb0, length=50, elements={
+0x000076797002cd20, 0x0000767970139000, 0x000076797013a570, 0x000076797013bd00,
+0x000076797013d340, 0x000076797013e8e0, 0x00007679701404a0, 0x0000767970141b60,
+0x0000767970182760, 0x0000767970184f90, 0x00007678a00dbfc0, 0x0000767970987160,
+0x00007679709a3a00, 0x00007679709a4890, 0x00007679709b3990, 0x00007679709a7a90,
+0x00007678c41512c0, 0x00007679709a9ac0, 0x00007679709b9e40, 0x00007678c4154d70,
+0x000076785820db10, 0x00007678581b4a70, 0x00007679709a8b50, 0x0000767970119eb0,
+0x000076797011a960, 0x000076797011b4a0, 0x00007679709c1320, 0x0000767830001760,
+0x00007679709bdda0, 0x00007679709bedd0, 0x00007678300bd300, 0x000076797048f850,
+0x00007679704900f0, 0x00007679709bfc90, 0x00007679709c03a0, 0x00007679704958d0,
+0x0000767814005380, 0x00007679709bb1b0, 0x00007679709bbe00, 0x0000767814001620,
+0x00007679709bce10, 0x0000767970494090, 0x0000767970494b10, 0x00007679704e45f0,
+0x0000767970491c70, 0x00007677ec001090, 0x0000767970492c30, 0x000076797046c3f0,
+0x00007677ec0024b0, 0x00007678d8000f60
+}
+
+"main" #1 [188702] prio=5 os_prio=0 cpu=1427.67ms elapsed=180.52s tid=0x000076797002cd20 nid=188702 waiting on condition [0x00007679767fe000]
+ java.lang.Thread.State: TIMED_WAITING (sleeping)
+ at java.lang.Thread.sleep0(java.base@21.0.12/Native Method)
+ at java.lang.Thread.sleep(java.base@21.0.12/Thread.java:509)
+ at bench.MMBench.waitForSettle(MMBench.java:221)
+ at bench.MMBench.main(MMBench.java:121)
+
+"Reference Handler" #9 [188710] daemon prio=10 os_prio=0 cpu=3.90ms elapsed=180.50s tid=0x0000767970139000 nid=188710 waiting on condition [0x0000767950bfe000]
+ java.lang.Thread.State: RUNNABLE
+ at java.lang.ref.Reference.waitForReferencePendingList(java.base@21.0.12/Native Method)
+ at java.lang.ref.Reference.processPendingReferences(java.base@21.0.12/Reference.java:246)
+ at java.lang.ref.Reference$ReferenceHandler.run(java.base@21.0.12/Reference.java:208)
+
+"Finalizer" #10 [188711] daemon prio=8 os_prio=0 cpu=0.16ms elapsed=180.50s tid=0x000076797013a570 nid=188711 in Object.wait() [0x0000767950afe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on <0x0000000680001a28> (a java.lang.ref.NativeReferenceQueue$Lock)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at java.lang.ref.NativeReferenceQueue.await(java.base@21.0.12/NativeReferenceQueue.java:48)
+ at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.12/ReferenceQueue.java:158)
+ at java.lang.ref.NativeReferenceQueue.remove(java.base@21.0.12/NativeReferenceQueue.java:89)
+ - locked <0x0000000680001a28> (a java.lang.ref.NativeReferenceQueue$Lock)
+ at java.lang.ref.Finalizer$FinalizerThread.run(java.base@21.0.12/Finalizer.java:173)
+
+"Signal Dispatcher" #11 [188712] daemon prio=9 os_prio=0 cpu=0.26ms elapsed=180.50s tid=0x000076797013bd00 nid=188712 waiting on condition [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+
+"Service Thread" #12 [188713] daemon prio=9 os_prio=0 cpu=22.29ms elapsed=180.50s tid=0x000076797013d340 nid=188713 runnable [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+
+"Monitor Deflation Thread" #13 [188714] daemon prio=9 os_prio=0 cpu=37.58ms elapsed=180.50s tid=0x000076797013e8e0 nid=188714 runnable [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+
+"C2 CompilerThread0" #14 [188715] daemon prio=9 os_prio=0 cpu=11162.84ms elapsed=180.50s tid=0x00007679701404a0 nid=188715 waiting on condition [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+ No compile task
+
+"C1 CompilerThread0" #22 [188716] daemon prio=9 os_prio=0 cpu=1575.20ms elapsed=180.50s tid=0x0000767970141b60 nid=188716 waiting on condition [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+ No compile task
+
+"Notification Thread" #26 [188720] daemon prio=9 os_prio=0 cpu=0.05ms elapsed=180.50s tid=0x0000767970182760 nid=188720 runnable [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+
+"Common-Cleaner" #27 [188721] daemon prio=8 os_prio=0 cpu=5.80ms elapsed=180.50s tid=0x0000767970184f90 nid=188721 waiting on condition [0x0000767941ffe000]
+ java.lang.Thread.State: TIMED_WAITING (parking)
+ at jdk.internal.misc.Unsafe.park(java.base@21.0.12/Native Method)
+ - parking to wait for <0x00000006800389c8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
+ at java.util.concurrent.locks.LockSupport.parkNanos(java.base@21.0.12/LockSupport.java:269)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.12/AbstractQueuedSynchronizer.java:1886)
+ at java.lang.ref.ReferenceQueue.await(java.base@21.0.12/ReferenceQueue.java:71)
+ at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.12/ReferenceQueue.java:143)
+ at java.lang.ref.ReferenceQueue.remove(java.base@21.0.12/ReferenceQueue.java:218)
+ at jdk.internal.ref.CleanerImpl.run(java.base@21.0.12/CleanerImpl.java:140)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+ at jdk.internal.misc.InnocuousThread.run(java.base@21.0.12/InnocuousThread.java:186)
+
+"Timer-0" #33 [188732] daemon prio=4 os_prio=0 cpu=2.49ms elapsed=179.28s tid=0x00007678a00dbfc0 nid=188732 in Object.wait() [0x00007679415fc000]
+ java.lang.Thread.State: TIMED_WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.util.TimerThread.mainLoop(java.base@21.0.12/Timer.java:563)
+ - locked <0x0000000686260508> (a java.util.TaskQueue)
+ at java.util.TimerThread.run(java.base@21.0.12/Timer.java:516)
+
+"Watchdog Timer" #47 [188749] prio=5 os_prio=0 cpu=0.31ms elapsed=177.27s tid=0x0000767970987160 nid=188749 in Object.wait() [0x00007679419fc000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at java.util.TimerThread.mainLoop(java.base@21.0.12/Timer.java:537)
+ - locked <0x0000000686a04038> (a java.util.TaskQueue)
+ at java.util.TimerThread.run(java.base@21.0.12/Timer.java:516)
+
+"Packet Pump" #48 [188750] prio=5 os_prio=0 cpu=1646.64ms elapsed=177.25s tid=0x00007679709a3a00 nid=188750 in Object.wait() [0x00007679417fc000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.server.Server$PacketPump.run(Server.java:161)
+ - locked <0x0000000686a040c8> (a java.util.concurrent.ConcurrentLinkedQueue)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection Listener" #49 [188751] prio=5 os_prio=0 cpu=3.18ms elapsed=177.25s tid=0x00007679709a4890 nid=188751 runnable [0x000076795022e000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.Net.accept(java.base@21.0.12/Native Method)
+ at sun.nio.ch.NioSocketImpl.accept(java.base@21.0.12/NioSocketImpl.java:748)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:698)
+ at java.net.ServerSocket.platformImplAccept(java.base@21.0.12/ServerSocket.java:663)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:639)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:585)
+ at java.net.ServerSocket.accept(java.base@21.0.12/ServerSocket.java:543)
+ at megamek.server.Server.run(Server.java:1436)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeA)" #50 [188752] prio=5 os_prio=0 cpu=885.09ms elapsed=177.16s tid=0x00007679709b3990 nid=188752 in Object.wait() [0x00007679416fc000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x00000006869e43d0> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeA" #51 [188753] prio=5 os_prio=0 cpu=638.23ms elapsed=177.15s tid=0x00007679709a7a90 nid=188753 runnable [0x00007679406cf000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x00000006869f4778> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #52 [188754] prio=5 os_prio=0 cpu=105.00ms elapsed=177.15s tid=0x00007678c41512c0 nid=188754 runnable [0x0000767941afc000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x0000000686a14300> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeB)" #54 [188755] prio=5 os_prio=0 cpu=978.63ms elapsed=177.12s tid=0x00007679709a9ac0 nid=188755 in Object.wait() [0x0000767940361000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x0000000686a24188> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeB" #55 [188756] prio=5 os_prio=0 cpu=600.54ms elapsed=177.12s tid=0x00007679709b9e40 nid=188756 runnable [0x0000767940261000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x0000000686a54428> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 3" #56 [188757] prio=5 os_prio=0 cpu=95.89ms elapsed=177.12s tid=0x00007678c4154d70 nid=188757 runnable [0x0000767940161000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x0000000686a745b8> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Java2D Disposer" #57 [188758] daemon prio=10 os_prio=0 cpu=0.65ms elapsed=177.12s tid=0x000076785820db10 nid=188758 waiting on condition [0x00007678eeffe000]
+ java.lang.Thread.State: WAITING (parking)
+ at jdk.internal.misc.Unsafe.park(java.base@21.0.12/Native Method)
+ - parking to wait for <0x0000000686a24238> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
+ at java.util.concurrent.locks.LockSupport.park(java.base@21.0.12/LockSupport.java:371)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@21.0.12/AbstractQueuedSynchronizer.java:519)
+ at java.util.concurrent.ForkJoinPool.unmanagedBlock(java.base@21.0.12/ForkJoinPool.java:3780)
+ at java.util.concurrent.ForkJoinPool.managedBlock(java.base@21.0.12/ForkJoinPool.java:3725)
+ at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(java.base@21.0.12/AbstractQueuedSynchronizer.java:1746)
+ at java.lang.ref.ReferenceQueue.await(java.base@21.0.12/ReferenceQueue.java:67)
+ at java.lang.ref.ReferenceQueue.remove0(java.base@21.0.12/ReferenceQueue.java:158)
+ at java.lang.ref.ReferenceQueue.remove(java.base@21.0.12/ReferenceQueue.java:234)
+ at sun.java2d.Disposer.run(java.desktop@21.0.12/Disposer.java:145)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"AWT-XAWT" #58 [188759] daemon prio=6 os_prio=0 cpu=38.10ms elapsed=177.12s tid=0x00007678581b4a70 nid=188759 runnable [0x00007678eeefe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.awt.X11.XToolkit.waitForEvents(java.desktop@21.0.12/Native Method)
+ at sun.awt.X11.XToolkit.run(java.desktop@21.0.12/XToolkit.java:687)
+ at sun.awt.X11.XToolkit.run(java.desktop@21.0.12/XToolkit.java:651)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Watchdog Timer" #97 [188878] prio=5 os_prio=0 cpu=0.33ms elapsed=165.78s tid=0x00007679709a8b50 nid=188878 in Object.wait() [0x00007678ee7fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at java.util.TimerThread.mainLoop(java.base@21.0.12/Timer.java:537)
+ - locked <0x00000006880015b8> (a java.util.TaskQueue)
+ at java.util.TimerThread.run(java.base@21.0.12/Timer.java:516)
+
+"Packet Pump" #98 [188879] prio=5 os_prio=0 cpu=1045.73ms elapsed=165.78s tid=0x0000767970119eb0 nid=188879 in Object.wait() [0x00007678eeafe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.server.Server$PacketPump.run(Server.java:161)
+ - locked <0x000000068811c3d8> (a java.util.concurrent.ConcurrentLinkedQueue)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection Listener" #99 [188880] prio=5 os_prio=0 cpu=1.06ms elapsed=165.78s tid=0x000076797011a960 nid=188880 runnable [0x00007678ee9fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.Net.accept(java.base@21.0.12/Native Method)
+ at sun.nio.ch.NioSocketImpl.accept(java.base@21.0.12/NioSocketImpl.java:748)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:698)
+ at java.net.ServerSocket.platformImplAccept(java.base@21.0.12/ServerSocket.java:663)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:639)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:585)
+ at java.net.ServerSocket.accept(java.base@21.0.12/ServerSocket.java:543)
+ at megamek.server.Server.run(Server.java:1436)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeA)" #100 [188881] prio=5 os_prio=0 cpu=530.19ms elapsed=165.75s tid=0x000076797011b4a0 nid=188881 in Object.wait() [0x00007678ee6fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x00000006880016a8> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeA" #101 [188882] prio=5 os_prio=0 cpu=472.86ms elapsed=165.75s tid=0x00007679709c1320 nid=188882 runnable [0x00007678ee5fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x00000006880018b0> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #102 [188883] prio=5 os_prio=0 cpu=44.54ms elapsed=165.75s tid=0x0000767830001760 nid=188883 runnable [0x00007678ee4fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x00000006853850d0> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeB)" #103 [188884] prio=5 os_prio=0 cpu=619.79ms elapsed=165.72s tid=0x00007679709bdda0 nid=188884 in Object.wait() [0x00007678ee3fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x00000006853e21c8> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeB" #104 [188885] prio=5 os_prio=0 cpu=474.74ms elapsed=165.71s tid=0x00007679709bedd0 nid=188885 runnable [0x00007678ee2fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x00000006853f23e0> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #105 [188886] prio=5 os_prio=0 cpu=43.52ms elapsed=165.71s tid=0x00007678300bd300 nid=188886 runnable [0x00007678ee1fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x0000000689c76b18> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Watchdog Timer" #134 [188965] prio=5 os_prio=0 cpu=0.38ms elapsed=158.28s tid=0x000076797048f850 nid=188965 in Object.wait() [0x0000767941cfd000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at java.util.TimerThread.mainLoop(java.base@21.0.12/Timer.java:537)
+ - locked <0x000000068aa6e090> (a java.util.TaskQueue)
+ at java.util.TimerThread.run(java.base@21.0.12/Timer.java:516)
+
+"Packet Pump" #135 [188966] prio=5 os_prio=0 cpu=1109.61ms elapsed=158.28s tid=0x00007679704900f0 nid=188966 in Object.wait() [0x00007678ee8fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.server.Server$PacketPump.run(Server.java:161)
+ - locked <0x000000068aaa6690> (a java.util.concurrent.ConcurrentLinkedQueue)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection Listener" #136 [188967] prio=5 os_prio=0 cpu=1.68ms elapsed=158.28s tid=0x00007679709bfc90 nid=188967 runnable [0x00007678ee0fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.Net.accept(java.base@21.0.12/Native Method)
+ at sun.nio.ch.NioSocketImpl.accept(java.base@21.0.12/NioSocketImpl.java:748)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:698)
+ at java.net.ServerSocket.platformImplAccept(java.base@21.0.12/ServerSocket.java:663)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:639)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:585)
+ at java.net.ServerSocket.accept(java.base@21.0.12/ServerSocket.java:543)
+ at megamek.server.Server.run(Server.java:1436)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeA)" #137 [188968] prio=5 os_prio=0 cpu=813.96ms elapsed=158.24s tid=0x00007679709c03a0 nid=188968 in Object.wait() [0x00007678edffe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x000000068ab0b400> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:469)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeA" #138 [188969] prio=5 os_prio=0 cpu=517.13ms elapsed=158.24s tid=0x00007679704958d0 nid=188969 runnable [0x00007678edefe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068ab29b20> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #139 [188970] prio=5 os_prio=0 cpu=56.29ms elapsed=158.24s tid=0x0000767814005380 nid=188970 runnable [0x00007678eddfe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068aa3cae0> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeB)" #140 [188971] prio=5 os_prio=0 cpu=712.99ms elapsed=158.17s tid=0x00007679709bb1b0 nid=188971 in Object.wait() [0x00007678edcfe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x000000068b80aab8> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeB" #141 [188972] prio=5 os_prio=0 cpu=512.32ms elapsed=158.17s tid=0x00007679709bbe00 nid=188972 runnable [0x00007678edbfe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068b83fb98> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #142 [188973] prio=5 os_prio=0 cpu=82.17ms elapsed=158.03s tid=0x0000767814001620 nid=188973 runnable [0x00007679418fc000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068b86ba20> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Watchdog Timer" #173 [189017] prio=5 os_prio=0 cpu=0.39ms elapsed=149.75s tid=0x00007679709bce10 nid=189017 in Object.wait() [0x000076795032e000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at java.util.TimerThread.mainLoop(java.base@21.0.12/Timer.java:537)
+ - locked <0x000000068d3ab9a8> (a java.util.TaskQueue)
+ at java.util.TimerThread.run(java.base@21.0.12/Timer.java:516)
+
+"Packet Pump" #174 [189018] prio=5 os_prio=0 cpu=180.91ms elapsed=149.75s tid=0x0000767970494090 nid=189018 in Object.wait() [0x00007678eecfe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.server.Server$PacketPump.run(Server.java:161)
+ - locked <0x000000068d000078> (a java.util.concurrent.ConcurrentLinkedQueue)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection Listener" #175 [189019] prio=5 os_prio=0 cpu=1.63ms elapsed=149.75s tid=0x0000767970494b10 nid=189019 runnable [0x00007678ed6fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.Net.accept(java.base@21.0.12/Native Method)
+ at sun.nio.ch.NioSocketImpl.accept(java.base@21.0.12/NioSocketImpl.java:748)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:698)
+ at java.net.ServerSocket.platformImplAccept(java.base@21.0.12/ServerSocket.java:663)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:639)
+ at java.net.ServerSocket.implAccept(java.base@21.0.12/ServerSocket.java:585)
+ at java.net.ServerSocket.accept(java.base@21.0.12/ServerSocket.java:543)
+ at megamek.server.Server.run(Server.java:1436)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeA)" #176 [189020] prio=5 os_prio=0 cpu=0.66ms elapsed=149.71s tid=0x00007679704e45f0 nid=189020 in Object.wait() [0x00007678ed5fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x000000068d325608> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeA" #177 [189021] prio=5 os_prio=0 cpu=98.43ms elapsed=149.71s tid=0x0000767970491c70 nid=189021 runnable [0x00007678ed4fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068d2af3d8> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #178 [189022] prio=5 os_prio=0 cpu=22.98ms elapsed=149.71s tid=0x00007677ec001090 nid=189022 runnable [0x00007678ed3fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068d4101e0> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Princess-precognition (TraineeB)" #179 [189023] prio=5 os_prio=0 cpu=0.66ms elapsed=149.67s tid=0x0000767970492c30 nid=189023 in Object.wait() [0x00007678ed2fe000]
+ java.lang.Thread.State: WAITING (on object monitor)
+ at java.lang.Object.wait0(java.base@21.0.12/Native Method)
+ - waiting on
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:366)
+ at java.lang.Object.wait(java.base@21.0.12/Object.java:339)
+ at megamek.client.bot.princess.Precognition.waitForUnpause(Precognition.java:497)
+ - locked <0x000000068d086410> (a megamek.client.bot.princess.Precognition)
+ at megamek.client.bot.princess.Precognition.run(Precognition.java:471)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Client Connection, Player TraineeB" #180 [189024] prio=5 os_prio=0 cpu=90.95ms elapsed=149.67s tid=0x000076797046c3f0 nid=189024 runnable [0x00007678ed1fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068d229040> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.client.AbstractClient.updateConnection(AbstractClient.java:135)
+ at megamek.client.AbstractClient$ConnectionHandler.run(AbstractClient.java:630)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Connection 2" #181 [189025] prio=5 os_prio=0 cpu=21.71ms elapsed=149.67s tid=0x00007677ec0024b0 nid=189025 runnable [0x00007678ed0fe000]
+ java.lang.Thread.State: RUNNABLE
+ at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.12/Native Method)
+ at sun.nio.ch.SocketDispatcher.read(java.base@21.0.12/SocketDispatcher.java:48)
+ at sun.nio.ch.NioSocketImpl.tryRead(java.base@21.0.12/NioSocketImpl.java:256)
+ at sun.nio.ch.NioSocketImpl.implRead(java.base@21.0.12/NioSocketImpl.java:307)
+ at sun.nio.ch.NioSocketImpl.read(java.base@21.0.12/NioSocketImpl.java:346)
+ at sun.nio.ch.NioSocketImpl$1.read(java.base@21.0.12/NioSocketImpl.java:796)
+ at java.net.Socket$SocketInputStream.read(java.base@21.0.12/Socket.java:1099)
+ at java.io.BufferedInputStream.fill(java.base@21.0.12/BufferedInputStream.java:291)
+ at java.io.BufferedInputStream.implRead(java.base@21.0.12/BufferedInputStream.java:325)
+ at java.io.BufferedInputStream.read(java.base@21.0.12/BufferedInputStream.java:312)
+ at java.io.DataInputStream.readUnsignedByte(java.base@21.0.12/DataInputStream.java:295)
+ at java.io.DataInputStream.readBoolean(java.base@21.0.12/DataInputStream.java:255)
+ at megamek.common.net.connections.DataStreamConnection.readNetworkPacket(DataStreamConnection.java:97)
+ - locked <0x000000068d1a2ca8> (a java.io.DataInputStream)
+ at megamek.common.net.connections.AbstractConnection.update(AbstractConnection.java:316)
+ at megamek.server.ConnectionHandler.run(ConnectionHandler.java:67)
+ at java.lang.Thread.runWith(java.base@21.0.12/Thread.java:1596)
+ at java.lang.Thread.run(java.base@21.0.12/Thread.java:1583)
+
+"Attach Listener" #185 [189624] daemon prio=9 os_prio=0 cpu=0.33ms elapsed=0.10s tid=0x00007678d8000f60 nid=189624 waiting on condition [0x0000000000000000]
+ java.lang.Thread.State: RUNNABLE
+
+"G1 Conc#2" os_prio=0 cpu=471.83ms elapsed=179.04s tid=0x0000767944001830 nid=188744 runnable
+
+"G1 Conc#1" os_prio=0 cpu=467.15ms elapsed=179.04s tid=0x0000767944000d00 nid=188743 runnable
+
+"GC Thread#12" os_prio=0 cpu=462.21ms elapsed=179.04s tid=0x00007678e8018180 nid=188742 runnable
+
+"GC Thread#11" os_prio=0 cpu=435.91ms elapsed=179.04s tid=0x00007678e8017690 nid=188741 runnable
+
+"GC Thread#10" os_prio=0 cpu=425.09ms elapsed=179.05s tid=0x00007678e8016ba0 nid=188740 runnable
+
+"GC Thread#9" os_prio=0 cpu=434.80ms elapsed=179.05s tid=0x00007678e80160b0 nid=188739 runnable
+
+"GC Thread#8" os_prio=0 cpu=447.01ms elapsed=179.05s tid=0x00007678e80155c0 nid=188738 runnable
+
+"GC Thread#7" os_prio=0 cpu=450.89ms elapsed=179.05s tid=0x00007678e8014ad0 nid=188737 runnable
+
+"GC Thread#6" os_prio=0 cpu=426.82ms elapsed=179.05s tid=0x00007678e8014030 nid=188736 runnable
+
+"GC Thread#5" os_prio=0 cpu=467.39ms elapsed=179.05s tid=0x00007678e800b670 nid=188735 runnable
+
+"GC Thread#4" os_prio=0 cpu=434.03ms elapsed=179.05s tid=0x00007678e800abd0 nid=188734 runnable
+
+"GC Thread#3" os_prio=0 cpu=475.01ms elapsed=179.05s tid=0x00007678e800a240 nid=188733 runnable
+
+"GC Thread#2" os_prio=0 cpu=454.38ms elapsed=179.59s tid=0x00007678e8009840 nid=188725 runnable
+
+"GC Thread#1" os_prio=0 cpu=490.35ms elapsed=180.47s tid=0x00007678e8001170 nid=188722 runnable
+
+"VM Thread" os_prio=0 cpu=114.91ms elapsed=180.51s tid=0x000076797012fc10 nid=188709 runnable
+
+"VM Periodic Task Thread" os_prio=0 cpu=204.91ms elapsed=180.51s tid=0x000076797011d820 nid=188708 waiting on condition
+
+"G1 Service" os_prio=0 cpu=24.66ms elapsed=180.52s tid=0x000076797010c890 nid=188707 runnable
+
+"G1 Refine#0" os_prio=0 cpu=236.80ms elapsed=180.52s tid=0x000076797010b8d0 nid=188706 runnable
+
+"G1 Conc#0" os_prio=0 cpu=464.41ms elapsed=180.52s tid=0x0000767970063cc0 nid=188705 runnable
+
+"G1 Main Marker" os_prio=0 cpu=2.81ms elapsed=180.52s tid=0x0000767970062d20 nid=188704 runnable
+
+"GC Thread#0" os_prio=0 cpu=419.85ms elapsed=180.52s tid=0x00007679700556e0 nid=188703 runnable
+
+JNI global refs: 88, weak refs: 0
+