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 +