diff --git a/README.md b/README.md index 43daac6..05f3831 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,27 @@ ATProto-backed campaign game. using only public API, so tracking new MegaMek releases stays a recompile rather than a rebase. +## Patches + +MegaMek patches live in `patches/patchset/` as **unified diffs**, applied with +`-p1` against a pristine source tree. Never as copied source files: a copied file +silently discards upstream changes to that file on the next release, whereas a +diff conflicts loudly. + +`patches/build-patched-jar.sh` copies only the touched files out of +`megamek-0.51.0/` into a scratch dir, applies the patchset there, compiles just +those files, and injects the classes into a copy of `MegaMek.jar`. The MegaMek +source and release trees are never modified. (Injection rather than classpath +shadowing is required because `MegaMek.jar` is sealed.) + +Currently applied - verified to be the *only* difference from the stock jar: + +- `0001-sound-guard-optional-master-gain-control.patch` - MegaMek assumes the + optional `MASTER_GAIN` Clip control exists; under Webswing's audio mixer it + does not, and the client dies at startup with an uncaught NPE. Upstreamable. + +`patches/archive/` holds patches that were tried and dropped; see its README. + ## Layout - `harness/src/bench/` - benchmark + diagnostic harness diff --git a/patches/archive/multitenancy/README.md b/patches/archive/multitenancy/README.md new file mode 100644 index 0000000..a4ab45f --- /dev/null +++ b/patches/archive/multitenancy/README.md @@ -0,0 +1,21 @@ +# Archived: multi-tenancy patch (NOT applied) + +These are kept for reference only. They are **not** compiled into +`MegaMek-patched.jar`. + +They gave `AbstractGameManager` a reference to its owning `Server` instead of +reaching for the static `Server.getServerInstance()`, which points at the most +recently constructed Server and silently misroutes packets once a second game +exists in the same JVM. + +Dropped because: + +1. The architecture settled on **one game per container** (Fargate task), so two + Servers never share a JVM and the static is harmless. +2. The patch was **necessary but not sufficient** anyway - patched multi-game + runs still showed cross-game interference (round counters regressing, and a + 2-game run silently wedging). Other `getServerInstance()` callers remain in + `Compute`, `WeaponHandler`, `ACWeapon` and `TWGameManager`. + +To reproduce the density experiment, restore these to `patches/src/` and add +them back to `patches/build-patched-jar.sh`. diff --git a/patches/archive/multitenancy/multitenancy.patch b/patches/archive/multitenancy/multitenancy.patch new file mode 100644 index 0000000..4535f8f --- /dev/null +++ b/patches/archive/multitenancy/multitenancy.patch @@ -0,0 +1,98 @@ +--- a/megamek/src/megamek/server/Server.java ++++ b/megamek/src/megamek/server/Server.java +@@ -455,6 +455,14 @@ + 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() { +--- a/megamek/src/megamek/server/AbstractGameManager.java ++++ b/megamek/src/megamek/server/AbstractGameManager.java +@@ -48,6 +48,27 @@ + 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);
+@@ -60,7 +81,7 @@
+ */
+ @Override
+ public void send(Packet packet) {
+- Server.getServerInstance().send(packet);
++ server().send(packet);
+ }
+
+ /**
+@@ -70,7 +91,7 @@
+ */
+ @Override
+ public void send(int connId, Packet p) {
+- Server.getServerInstance().send(connId, p);
++ server().send(connId, p);
+ }
+
+ @Override
+@@ -203,7 +224,7 @@
+ * @see #transmitAllPlayerUpdates()
+ */
+ protected void transmitPlayerUpdate(Player player) {
+- Server.getServerInstance().transmitPlayerUpdate(player);
++ server().transmitPlayerUpdate(player);
+ }
+
+ /**
+@@ -241,19 +262,19 @@
+ }
+
+ public void sendChat(String origin, String message) {
+- Server.getServerInstance().sendChat(origin, message);
++ server().sendChat(origin, message);
+ }
+
+ public void sendChat(int connId, String origin, String message) {
+- Server.getServerInstance().sendChat(connId, origin, message);
++ server().sendChat(connId, origin, message);
+ }
+
+ public void sendServerChat(String message) {
+- Server.getServerInstance().sendServerChat(message);
++ server().sendServerChat(message);
+ }
+
+ public void sendServerChat(int connId, String message) {
+- Server.getServerInstance().sendServerChat(connId, message);
++ server().sendServerChat(connId, message);
+ }
+
+ /**
diff --git a/patches/build-patched-jar.sh b/patches/build-patched-jar.sh
index ab6e45b..37d2c5f 100755
--- a/patches/build-patched-jar.sh
+++ b/patches/build-patched-jar.sh
@@ -1,38 +1,73 @@
#!/usr/bin/env bash
-# Build a patched MegaMek.jar carrying the multi-tenancy fix.
+# Build MegaMek-patched.jar by applying patches/patchset/*.patch to a pristine
+# MegaMek source tree, compiling only the touched files, and injecting the
+# resulting classes into a copy of the release jar.
#
-# 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.
+# Patches are stored as unified diffs, not as copied source files. A copied file
+# silently discards any upstream change to that file on the next MegaMek release;
+# a diff either applies cleanly or fails loudly, which is the behaviour we want
+# when tracking upstream.
#
-# The pristine release tree is never modified; output is MegaMek-patched.jar.
+# Nothing here mutates the MegaMek source or release tree - the sources are
+# copied to a scratch dir first. Output is MegaMek-patched.jar.
+#
+# MegaMek.jar sets "Sealed: true", so patched classes cannot simply be placed
+# earlier on the classpath; the JVM refuses to load a sealed package from a
+# second codebase. Injecting them back into a copy of the jar keeps the seal
+# intact.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MM_HOME="${MM_HOME:-$ROOT/MegaMek-0.51.00}"
+MM_SRC="${MM_SRC:-$ROOT/megamek-0.51.0}"
JAVA_HOME="${JAVA_HOME:-$ROOT/jdk-21.0.12+8}"
+[ -d "$MM_SRC" ] || { echo "ERROR: MegaMek source tree missing at $MM_SRC (run scripts/fetch-deps.sh)" >&2; exit 1; }
+command -v patch >/dev/null || { echo "ERROR: 'patch' not found" >&2; exit 1; }
+
CP="$MM_HOME/MegaMek.jar"
for jar in "$MM_HOME"/lib/*.jar; do CP="$CP:$jar"; done
+WORK="$ROOT/patches/work"
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 \
- "$ROOT"/patches/src/megamek/client/ui/clientGUI/audio/Sound.java
+rm -rf "$WORK" "$OUT"
+mkdir -p "$WORK" "$OUT"
+
+shopt -s nullglob
+PATCHES=( "$ROOT"/patches/patchset/*.patch )
+shopt -u nullglob
+if [ ${#PATCHES[@]} -eq 0 ]; then
+ echo "No patches in patches/patchset/ - nothing to do."
+ exit 0
+fi
+
+# Copy only the files each patch touches, preserving their tree layout, so the
+# patches apply with -p1 exactly as they would against a fresh checkout.
+TOUCHED=()
+for p in "${PATCHES[@]}"; do
+ while read -r rel; do
+ [ -z "$rel" ] && continue
+ src="$MM_SRC/$rel"
+ [ -f "$src" ] || { echo "ERROR: $rel not found in $MM_SRC (patch stale?)" >&2; exit 1; }
+ mkdir -p "$WORK/$(dirname "$rel")"
+ cp "$src" "$WORK/$rel"
+ TOUCHED+=( "$WORK/$rel" )
+ done < <(grep '^+++ b/' "$p" | sed 's|^+++ b/||')
+done
+
+for p in "${PATCHES[@]}"; do
+ echo "applying $(basename "$p")"
+ patch -p1 -d "$WORK" --forward --silent < "$p" \
+ || { echo "ERROR: failed to apply $(basename "$p")" >&2; exit 1; }
+done
+
+echo "compiling ${#TOUCHED[@]} patched file(s)..."
+"$JAVA_HOME/bin/javac" -nowarn -classpath "$CP" -d "$OUT" "${TOUCHED[@]}"
PATCHED_JAR="$ROOT/MegaMek-patched.jar"
cp "$MM_HOME/MegaMek.jar" "$PATCHED_JAR"
-echo "injecting $(find "$OUT" -name '*.class' | wc -l) recompiled classes..."
+echo "injecting $(find "$OUT" -name '*.class' | wc -l) recompiled class(es)..."
"$JAVA_HOME/bin/jar" --update --file "$PATCHED_JAR" -C "$OUT" megamek
echo "built -> $PATCHED_JAR"
diff --git a/patches/patchset/0001-sound-guard-optional-master-gain-control.patch b/patches/patchset/0001-sound-guard-optional-master-gain-control.patch
new file mode 100644
index 0000000..82e1db9
--- /dev/null
+++ b/patches/patchset/0001-sound-guard-optional-master-gain-control.patch
@@ -0,0 +1,28 @@
+--- a/megamek/src/megamek/client/ui/clientGUI/audio/Sound.java
++++ b/megamek/src/megamek/client/ui/clientGUI/audio/Sound.java
+@@ -67,8 +67,24 @@
+ }
+
+ if (clip != null) {
++ // MASTER_GAIN is optional. A Clip implementation is free not to support
++ // it, and getControl() then returns null - so this must be checked rather
++ // than assumed. Webswing's audio mixer is one such implementation, and
++ // without this guard MegaMek dies at startup with an uncaught NPE before
++ // the client is usable at all.
++ if (!clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
++ return;
++ }
+ FloatControl gainControl = (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
+- gainControl.setValue(20f * (float) Math.log10(volume));
++ if (gainControl == null) {
++ return;
++ }
++ // log10(0) is -Infinity; clamp to the control's own floor instead.
++ float gain = (volume <= 0f)
++ ? gainControl.getMinimum()
++ : 20f * (float) Math.log10(volume);
++ gainControl.setValue(Math.max(gainControl.getMinimum(),
++ Math.min(gainControl.getMaximum(), gain)));
+ }
+ }
+ }
diff --git a/patches/src/megamek/server/AbstractGameManager.java b/patches/src/megamek/server/AbstractGameManager.java
deleted file mode 100644
index f7ec30d..0000000
--- a/patches/src/megamek/server/AbstractGameManager.java
+++ /dev/null
@@ -1,316 +0,0 @@
-/*
- * 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 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
deleted file mode 100644
index c89a3d8..0000000
--- a/patches/src/megamek/server/Server.java
+++ /dev/null
@@ -1,1556 +0,0 @@
-/*
- * 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 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 CollectionString 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;
- Setint 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;
- }
- Listint 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/patches/src/megamek/client/ui/clientGUI/audio/Sound.java b/patches/work/megamek/src/megamek/client/ui/clientGUI/audio/Sound.java
similarity index 100%
rename from patches/src/megamek/client/ui/clientGUI/audio/Sound.java
rename to patches/work/megamek/src/megamek/client/ui/clientGUI/audio/Sound.java