diff --git a/megamek/src/megamek/common/util/SerializationHelper.java b/megamek/src/megamek/common/util/SerializationHelper.java index 11d913dc9ca..82deb88e8dc 100644 --- a/megamek/src/megamek/common/util/SerializationHelper.java +++ b/megamek/src/megamek/common/util/SerializationHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved. * * This file is part of MegaMek. * @@ -18,12 +18,15 @@ */ package megamek.common.util; +import java.util.regex.Pattern; + import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.converters.Converter; import com.thoughtworks.xstream.converters.MarshallingContext; import com.thoughtworks.xstream.converters.UnmarshallingContext; import com.thoughtworks.xstream.io.HierarchicalStreamReader; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + import megamek.common.Coords; /** @@ -31,49 +34,69 @@ */ public class SerializationHelper { + private SerializationHelper() { + } + /** - * Factory method that produces an XStream object suitable for working with MegaMek save games + * Factory method that produces an XStream object suitable for working with + * MegaMek save games */ public static XStream getSaveGameXStream() { final XStream xStream = new XStream(); - // This will make save games much smaller by using a more efficient means of referencing - // objects in the XML graph + // This will make save games much smaller by using a more efficient means of + // referencing objects in the XML graph xStream.setMode(XStream.ID_REFERENCES); - // Setup Permissions - xStream.allowTypes(new Class[] { - megamek.client.bot.princess.BehaviorSettings.class, - megamek.common.ArtilleryTracker.ArtilleryModifier.class, - megamek.common.Board.class, - megamek.common.Coords.class, - megamek.common.CompositeTechLevel.DateRange.class, - megamek.common.CriticalSlot.class, - megamek.common.EquipmentMode.class, - megamek.common.Flare.class, - megamek.common.Game.class, - megamek.common.Hex.class, - megamek.common.Minefield.class, - megamek.common.PilotingRollData.class, - megamek.common.Player.class, - megamek.common.Sensor.class, - megamek.common.SpecialHexDisplay.class, - megamek.common.TagInfo.class, - megamek.common.TargetRollModifier.class, - megamek.common.Team.class, - megamek.common.Terrain.class, - megamek.common.Report.class, - megamek.common.force.Force.class, - megamek.server.SmokeCloud.class, - megamek.common.EntityFluff.class, - megamek.common.NarcPod.class, - megamek.common.INarcPod.class, - megamek.common.net.packets.Packet.class, - megamek.common.BoardLocation.class, - megamek.common.strategicBattleSystems.SBFMovePath.class, -// megamek.common.strategicBattleSystems.SBFUnit.class - + xStream.allowTypesByRegExp(new Pattern[] { + Pattern.compile("\\[C$"), + Pattern.compile("\\[I$"), + Pattern.compile("\\[Ljava\\.lang\\.Enum;$"), + Pattern.compile("java\\.io\\.File$"), + Pattern.compile("java\\.lang\\.Boolean$"), + Pattern.compile("java\\.lang\\.Enum$"), + Pattern.compile("java\\.lang\\.Integer$"), + Pattern.compile("java\\.lang\\.Double$"), + Pattern.compile("java\\.lang\\.Number$"), + Pattern.compile("java\\.lang\\.StringBuffer$"), + Pattern.compile("java\\.util\\.ArrayList$"), + Pattern.compile("java\\.util\\.Collections\\$SetFromMap$"), + Pattern.compile("java\\.util\\.Collections\\$UnmodifiableCollection$"), + Pattern.compile("java\\.util\\.Collections\\$UnmodifiableList$"), + Pattern.compile("java\\.util\\.concurrent\\.ConcurrentHashMap$"), + Pattern.compile("java\\.util\\.concurrent\\.ConcurrentHashMap\\$Segment$"), + Pattern.compile("java\\.util\\.concurrent\\.CopyOnWriteArrayList$"), + Pattern.compile("java\\.util\\.concurrent\\.locks\\.AbstractOwnableSynchronizer$"), + Pattern.compile("java\\.util\\.concurrent\\.locks\\.AbstractQueuedSynchronizer$"), + Pattern.compile("java\\.util\\.concurrent\\.locks\\.ReentrantLock$"), + Pattern.compile("java\\.util\\.concurrent\\.locks\\.ReentrantLock\\$NonfairSync$"), + Pattern.compile("java\\.util\\.concurrent\\.locks\\.ReentrantLock\\$Sync$"), + Pattern.compile("java\\.util\\.UUID$"), + Pattern.compile("java\\.util\\.EnumMap$"), + Pattern.compile("java\\.util\\.EnumSet.*"), + Pattern.compile("java\\.util\\.HashMap$"), + Pattern.compile("java\\.util\\.HashSet$"), + Pattern.compile("java\\.util\\.Hashtable$"), + Pattern.compile("java\\.util\\.LinkedHashSet$"), + Pattern.compile("java\\.util\\.LinkedList$"), + Pattern.compile("java\\.util\\.TreeMap$"), + Pattern.compile("java\\.util\\.TreeSet$"), + Pattern.compile("java\\.util\\.Vector$"), + Pattern.compile("\\[Ljava\\.lang\\.Object;$"), + Pattern.compile("\\[Ljava\\.lang\\.String;$"), + Pattern.compile("\\[Ljava\\.util\\.concurrent\\.ConcurrentHashMap\\$Segment;$"), + Pattern.compile("\\[\\[Lmegamek.*"), + Pattern.compile("\\[Lmegamek.*"), + Pattern.compile("megamek.*"), + Pattern.compile("\\[\\[Lmekhq.*"), + Pattern.compile("\\[Lmekhq.*"), + Pattern.compile("mekhq.*"), + Pattern.compile("\\[\\[Lmegameklab.*"), + Pattern.compile("\\[Lmegameklab.*"), + Pattern.compile("megameklab.*"), + Pattern.compile("\\[Z$") }); + xStream.allowTypeHierarchy(megamek.common.BTObject.class); xStream.allowTypeHierarchy(megamek.common.Building.class); xStream.allowTypeHierarchy(megamek.common.Crew.class); @@ -93,12 +116,13 @@ public static XStream getSaveGameXStream() { } /** - * Factory method that produces an XStream object suitable for loading MegaMek save games + * Factory method that produces an XStream object suitable for loading MegaMek + * save games */ public static XStream getLoadSaveGameXStream() { - XStream xstream = getSaveGameXStream(); + XStream xStream = getSaveGameXStream(); - xstream.registerConverter(new Converter() { + xStream.registerConverter(new Converter() { @Override public boolean canConvert(Class cls) { return (cls == Coords.class); @@ -106,8 +130,10 @@ public boolean canConvert(Class cls) { @Override public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { - int x = 0, y = 0; - boolean foundX = false, foundY = false; + int x = 0; + int y = 0; + boolean foundX = false; + boolean foundY = false; while (reader.hasMoreChildren()) { reader.moveDown(); switch (reader.getNodeName()) { @@ -134,6 +160,6 @@ public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingC } }); - return xstream; + return xStream; } } diff --git a/megamek/src/megamek/server/AbstractGameManager.java b/megamek/src/megamek/server/AbstractGameManager.java index fe9aeca84b4..3023fd0b1e0 100644 --- a/megamek/src/megamek/server/AbstractGameManager.java +++ b/megamek/src/megamek/server/AbstractGameManager.java @@ -21,13 +21,15 @@ import megamek.common.IGame; import megamek.common.Player; import megamek.common.enums.GamePhase; +import megamek.common.net.enums.PacketCommand; import megamek.common.net.packets.Packet; import megamek.common.options.OptionsConstants; import megamek.common.preference.PreferenceManager; import megamek.common.util.StringUtil; -import org.apache.logging.log4j.LogManager; +import megamek.logging.MMLogger; public abstract class AbstractGameManager implements IGameManager { + private static final MMLogger logger = MMLogger.create(AbstractGameManager.class); protected final GameManagerPacketHelper packetHelper = new GameManagerPacketHelper(this); protected final GameManagerSaveHelper saveHandler = new GameManagerSaveHelper(this); @@ -35,6 +37,7 @@ public abstract class AbstractGameManager implements IGameManager { /** * Sends the given packet to all connections (all connected Clients = players). + * * @see Server#send(Packet) */ public void send(Packet packet) { @@ -43,6 +46,7 @@ public void send(Packet packet) { /** * Sends the given packet to the given connection (= player ID). + * * @see Server#send(int, Packet) */ public void send(int connId, Packet p) { @@ -51,12 +55,10 @@ public void send(int connId, Packet p) { @Override public void handlePacket(int connId, Packet packet) { - switch (packet.getCommand()) { - case PLAYER_READY: - receivePlayerDone(packet, connId); - send(packetHelper.createPlayerDonePacket(connId)); - checkReady(); - break; + if (packet.getCommand() == PacketCommand.PLAYER_READY) { + receivePlayerDone(packet, connId); + send(packetHelper.createPlayerDonePacket(connId)); + checkReady(); } } @@ -66,20 +68,21 @@ public void handlePacket(int connId, Packet packet) { 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. + * 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. + * 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. + * Switches to the given new Phase and preforms preparation, checks if it should + * be skipped and executes it. */ public final void changePhase(GamePhase newPhase) { getGame().setLastPhase(getGame().getPhase()); @@ -97,13 +100,18 @@ public final void changePhase(GamePhase newPhase) { } /** - * 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. + * 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()) { @@ -124,14 +132,17 @@ 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). */ + /** + * @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. + * 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) { boolean ready = packet.getBooleanValue(0); @@ -139,7 +150,7 @@ private void receivePlayerDone(Packet packet, int connIndex) { if (null != player) { player.setDone(ready); } else { - LogManager.getLogger().error("Tried to set done status of non-existent player!"); + logger.error("Tried to set done status of non-existent player!"); } } @@ -155,8 +166,8 @@ protected void transmitPlayerUpdate(Player player) { } /** - * Shares all player objects with all players. Private info is redacted before being sent - * to other players. + * Shares all player objects with all players. Private info is redacted before + * being sent to other players. * * @see #transmitPlayerUpdate(Player) */ @@ -165,15 +176,18 @@ public void transmitAllPlayerUpdates() { } /** - * 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. + * 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)); } @@ -204,7 +218,9 @@ public void sendServerChat(int connId, String message) { } /** - * Sends the current list of player turns as stored in the game's turn list to the Clients. + * Sends the current list of player turns as stored in the game's turn list to + * the Clients. + * * @see IGame#getTurnsList() */ public void sendCurrentTurns() { @@ -228,14 +244,15 @@ public AutosaveService getAutoSaveService() { } /** - * Sends out a notification message indicating that a ghost player's turn may be skipped with the - * /skip command. + * 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 = "Player '" + ghost.getName() + - "' is disconnected. You may skip their current turn with the /skip command."; + 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/megamek/src/megamek/server/GameManagerSaveHelper.java b/megamek/src/megamek/server/GameManagerSaveHelper.java index 348a5ebce15..a9e3dbbdc6d 100644 --- a/megamek/src/megamek/server/GameManagerSaveHelper.java +++ b/megamek/src/megamek/server/GameManagerSaveHelper.java @@ -18,19 +18,27 @@ */ package megamek.server; -import megamek.MMConstants; -import megamek.common.net.enums.PacketCommand; -import megamek.common.net.packets.Packet; -import megamek.common.util.SerializationHelper; -import org.apache.logging.log4j.LogManager; - -import java.io.*; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPOutputStream; +import megamek.MMConstants; +import megamek.common.net.enums.PacketCommand; +import megamek.common.net.packets.Packet; +import megamek.common.util.SerializationHelper; +import megamek.logging.MMLogger; + public class GameManagerSaveHelper { + private static final MMLogger logger = MMLogger.create(GameManagerSaveHelper.class); private final AbstractGameManager gameManager; @@ -39,8 +47,8 @@ public class GameManagerSaveHelper { } /** - * Saves the game server-side. Will announce the save (or error) in chat if the given sendChat - * is true. + * Saves the game server-side. Will announce the save (or error) in chat if the + * given sendChat is true. * * @param fileName The filename to use * @param sendChat When true, the saving (or error) is announced in chat @@ -55,6 +63,7 @@ protected void saveGame(String fileName, boolean sendChat) { if (!finalFileName.endsWith(MMConstants.SAVE_FILE_EXT)) { finalFileName = fileName + MMConstants.SAVE_FILE_EXT; } + File saveGameDir = new File(MMConstants.SAVEGAME_DIR); if (!saveGameDir.exists()) { saveGameDir.mkdir(); @@ -63,11 +72,13 @@ protected void saveGame(String fileName, boolean sendChat) { finalFileName = saveGameDir + File.separator + finalFileName; try (OutputStream os = new FileOutputStream(finalFileName + ".gz"); - OutputStream gzo = new GZIPOutputStream(os); - Writer writer = new OutputStreamWriter(gzo, StandardCharsets.UTF_8)) { + OutputStream gzo = new GZIPOutputStream(os); + Writer writer = new OutputStreamWriter(gzo, StandardCharsets.UTF_8)) { SerializationHelper.getSaveGameXStream().toXML(gameManager.getGame(), writer); } catch (Exception e) { - LogManager.getLogger().error("Unable to save file: {}", finalFileName, e); + String message = String.format("Unable to save file: %s", finalFileName); + logger.error(e, message); + if (sendChat) { gameManager.sendChat("MegaMek", "Could not save the game to " + finalFileName); } @@ -81,12 +92,14 @@ protected void saveGame(String fileName, boolean sendChat) { /** * Saves the game and sends it to the specified connection * - * @param connId The int connection id to send to - * @param fileName The String filename to use - * @param localPath The String path to the file to be used on the client + * @param connId The int connection id to send to + * @param fileName The String filename to use + * @param localPath The String path to the file to be used on the + * client */ public void sendSaveGame(int connId, String fileName, String localPath) { saveGame(fileName, false); + String finalFileName = fileName; if (!finalFileName.endsWith(MMConstants.SAVE_FILE_GZ_EXT)) { if (finalFileName.endsWith(MMConstants.SAVE_FILE_EXT)) { @@ -95,8 +108,10 @@ public void sendSaveGame(int connId, String fileName, String localPath) { finalFileName = fileName + MMConstants.SAVE_FILE_GZ_EXT; } } - localPath = localPath.replaceAll("\\|", " "); + + localPath = localPath.replace("|", " "); String localFile = MMConstants.SAVEGAME_DIR + File.separator + finalFileName; + try (InputStream in = new FileInputStream(localFile); InputStream bin = new BufferedInputStream(in)) { List data = new ArrayList<>(); int input; @@ -106,7 +121,8 @@ public void sendSaveGame(int connId, String fileName, String localPath) { gameManager.send(connId, new Packet(PacketCommand.SEND_SAVEGAME, finalFileName, data, localPath)); gameManager.sendChat(connId, "***Server", "Save game has been sent to you."); } catch (Exception ex) { - LogManager.getLogger().error("Unable to load file: {}", localFile, ex); + String message = String.format("Unable to load file: %s", localFile); + logger.error(ex, message); } } } diff --git a/megamek/src/megamek/server/Server.java b/megamek/src/megamek/server/Server.java index bf871eb146f..2d6f235aaae 100644 --- a/megamek/src/megamek/server/Server.java +++ b/megamek/src/megamek/server/Server.java @@ -1,27 +1,60 @@ /* * Copyright (c) 2000-2005 - Ben Mazur (bmazur@sev.org) * Copyright (c) 2013 - Edward Cullen (eddy@obsessedcomputers.co.uk) - * Copyright (c) 2018-2022 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2018-2024 - The MegaMek Team. All Rights Reserved. * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 2 of the License, or (at your option) any later - * version. + * This file is part of MegaMek. * - * This program 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. + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . */ 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.function.Consumer; +import java.util.zip.GZIPInputStream; + import com.thoughtworks.xstream.XStream; + import megamek.MMConstants; import megamek.MegaMek; +import megamek.SuiteConstants; import megamek.Version; import megamek.client.ui.swing.util.PlayerColour; import megamek.codeUtilities.StringUtility; -import megamek.common.*; +import megamek.common.Game; +import megamek.common.IGame; +import megamek.common.Player; +import megamek.common.Roll; import megamek.common.annotations.Nullable; import megamek.common.commandline.AbstractCommandLineParser.ParseException; import megamek.common.icons.Camouflage; @@ -37,18 +70,8 @@ import megamek.common.preference.PreferenceManager; import megamek.common.util.EmailService; import megamek.common.util.SerializationHelper; +import megamek.logging.MMLogger; import megamek.server.commands.ServerCommand; -import org.apache.logging.log4j.LogManager; - -import java.io.*; -import java.net.*; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.function.Consumer; -import java.util.zip.GZIPInputStream; /** * @author Ben Mazur @@ -67,6 +90,8 @@ public class Server implements Runnable { private final EmailService mailer; + private static final MMLogger logger = MMLogger.create(Server.class); + public static class ReceivedPacket { private int connectionId; private Packet packet; @@ -192,7 +217,8 @@ public void disconnected(DisconnectedEvent e) { AbstractConnection conn = e.getConnection(); // write something in the log - LogManager.getLogger().info("s: connection " + conn.getId() + " disconnected"); + String message = String.format("s: connect %d disconnected.", conn.getId()); + logger.info(message); connections.remove(conn); synchronized (serverLock) { @@ -243,9 +269,9 @@ public void packetReceived(PacketReceivedEvent e) { */ public static String validateServerAddress(String serverAddress) throws ParseException { if ((serverAddress == null) || serverAddress.isBlank()) { - String msg = "serverAddress must not be null or empty"; - LogManager.getLogger().error(msg); - throw new ParseException(msg); + String message = "serverAddress must not be null or empty"; + logger.error(message); + throw new ParseException(message); } else { return serverAddress.trim(); } @@ -257,13 +283,13 @@ public static String validateServerAddress(String serverAddress) throws ParseExc */ public static String validatePlayerName(String playerName) throws ParseException { if (playerName == null) { - String msg = "playerName must not be null"; - LogManager.getLogger().error(msg); - throw new ParseException(msg); + String message = "playerName must not be null"; + logger.error(message); + throw new ParseException(message); } else if (playerName.isBlank()) { - String msg = "playerName must not be empty string"; - LogManager.getLogger().error(msg); - throw new ParseException(msg); + String message = "playerName must not be empty string"; + logger.error(message); + throw new ParseException(message); } else { return playerName.trim(); } @@ -281,23 +307,26 @@ public static String validatePlayerName(String playerName) throws ParseException * 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. + * @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 0 or less, will return default, if illegal number, throws ParseException + * @param port if 0 or less, will return default, if illegal number, throws + * ParseException * @return valid port number */ public static int validatePort(int port) throws ParseException { if (port <= 0) { return MMConstants.DEFAULT_PORT; } else if ((port < MMConstants.MIN_PORT) || (port > MMConstants.MAX_PORT)) { - String msg = String.format("Port number %d outside allowed range %d-%d", port, MMConstants.MIN_PORT, MMConstants.MAX_PORT); - LogManager.getLogger().error(msg); - throw new ParseException(msg); + String message = String.format("Port number %d outside allowed range %d-%d", port, MMConstants.MIN_PORT, + MMConstants.MAX_PORT); + logger.error(message); + throw new ParseException(message); } else { return port; } @@ -308,30 +337,36 @@ public Server(@Nullable String password, int port, IGameManager gameManager) thr } public Server(@Nullable String password, int port, IGameManager gameManager, - boolean registerWithServerBrowser, @Nullable String metaServerUrl) throws IOException { + 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 MegaMek.info - * @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 + * @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 https://api.megamek.org + * @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 { + 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; + String message = ""; // initialize server socket serverSocket = new ServerSocket(port); @@ -339,29 +374,24 @@ public Server(@Nullable String password, int port, IGameManager gameManager, motd = createMotd(); // display server start text - LogManager.getLogger().info("s: starting a new server..."); + logger.info("s: starting a new server..."); try { - StringBuilder sb = new StringBuilder(); String host = InetAddress.getLocalHost().getHostName(); - sb.append("s: hostname = '"); - sb.append(host); - sb.append("' port = "); - sb.append(serverSocket.getLocalPort()); - sb.append('\n'); + message = String.format("s: hostname = '%s' port = %d", host, serverSocket.getLocalPort()); + logger.info(message); + InetAddress[] addresses = InetAddress.getAllByName(host); for (InetAddress address : addresses) { - sb.append("s: hosting on address = "); - sb.append(address.getHostAddress()); - sb.append('\n'); + message = String.format("s: hosting on address = %s", address.getHostAddress()); + logger.info(message); } - - LogManager.getLogger().info(sb.toString()); } catch (Exception ignored) { - + logger.error(ignored, "Ignore Exception."); } - LogManager.getLogger().info("s: password = " + this.password); + message = String.format("s: password = %s", this.password); + logger.info(message); for (ServerCommand command : gameManager.getCommandList(this)) { registerCommand(command); @@ -382,11 +412,12 @@ public void run() { serverBrowserUpdateTimer = new Timer("Server Browser Register Timer", true); serverBrowserUpdateTimer.schedule(register, 1, 40000); } else { - LogManager.getLogger().error("Invalid URL for server browser " + this.metaServerUrl); + message = String.format("Invalid URL for server browser %s", this.metaServerUrl); + logger.error(message); } } - // Fully initialised, now accept connections + // Fully initialized, now accept connections connector = new Thread(this, "Connection Listener"); connector.start(); @@ -419,7 +450,7 @@ public EmailService getEmailService() { * it was found, the build timestamp */ private String createMotd() { - return "Welcome to MegaMek. Server is running version " + MMConstants.VERSION; + return "Welcome to MegaMek. Server is running version " + SuiteConstants.VERSION; } /** @@ -473,7 +504,7 @@ public void die() { try { serverSocket.close(); } catch (Exception ignored) { - + logger.error(ignored, "Ignored Exception"); } // kill pending connections @@ -573,8 +604,7 @@ private void receivePlayerInfo(Packet packet, int connId) { * @return the String new player name */ private String correctDupeName(String oldName) { - for (Enumeration i = getGame().getPlayers(); i.hasMoreElements(); ) { - Player player = i.nextElement(); + for (Player player : getGame().getPlayersList()) { if (player.getName().equals(oldName)) { // We need to correct it. String newName = oldName; @@ -597,10 +627,11 @@ private String correctDupeName(String oldName) { private boolean receivePlayerVersion(Packet packet, int connId) { final Version version = (Version) packet.getObject(0); - if (!MMConstants.VERSION.is(version)) { + + if (!SuiteConstants.VERSION.is(version)) { final String message = String.format("Client/Server Version Mismatch -- Client: %s, Server: %s", - version, MMConstants.VERSION); - LogManager.getLogger().error(message); + version, SuiteConstants.VERSION); + logger.error(message); final Player player = getPlayer(connId); sendServerChat(String.format("For %s, Server reports:%s%s", @@ -611,29 +642,28 @@ private boolean receivePlayerVersion(Packet packet, int connId) { final String clientChecksum = (String) packet.getObject(1); final String serverChecksum = MegaMek.getMegaMekSHA256(); - final String message; + String message = ""; // print a message indicating client doesn't have jar file if (clientChecksum == null) { message = "Client Checksum is null. Client may not have a jar file"; - LogManager.getLogger().info(message); + logger.info(message); // print message indicating server doesn't have jar file } else if (serverChecksum == null) { message = "Server Checksum is null. Server may not have a jar file"; - LogManager.getLogger().info(message); + logger.info(message); // print message indicating a client/server checksum mismatch } else if (!clientChecksum.equals(serverChecksum)) { message = String.format("Client/Server checksum mismatch. Server reports: %s, Client reports %s", serverChecksum, clientChecksum); - LogManager.getLogger().warn(message); - } else { - message = ""; + logger.warn(message); } // Now, if we need to, send message! if (message.isEmpty()) { - LogManager.getLogger().info("SUCCESS: Client/Server Version (" + version + ") and Checksum (" - + clientChecksum + ") matched"); + message = String.format("SUCCESS: Client/Server Version (%s) and Checksum (%s) matched", version, + clientChecksum); + logger.info(message); } else { Player player = getPlayer(connId); sendServerChat(String.format("For %s, Server reports:%s%s", @@ -653,25 +683,23 @@ private void receivePlayerName(Packet packet, int connId) { String name = (String) packet.getObject(0); boolean isBot = (boolean) packet.getObject(1); boolean returning = false; + String message = ""; // this had better be from a pending connection if (conn == null) { - LogManager.getLogger().warn("Got a client name from a non-pending connection"); + 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 (Enumeration i = getGame().getPlayers(); i.hasMoreElements(); ) { - Player player = i.nextElement(); - if (player.getName().equals(name)) { - if (player.isGhost()) { - returning = true; - player.setGhost(false); - player.setBot(isBot); - // switch id - connId = player.getId(); - conn.setId(connId); - } + 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); } } @@ -692,8 +720,9 @@ private void receivePlayerName(Packet packet, int connId) { // 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)) { + if (!getGame().getPhase().isLounge() && + (null != player) && + (getGame().getEntitiesOwnedBy(player) < 1)) { player.setObserver(true); } @@ -714,27 +743,33 @@ private void receivePlayerName(Packet packet, int connId) { try { InetAddress[] addresses = InetAddress.getAllByName(InetAddress.getLocalHost().getHostName()); for (InetAddress address : addresses) { - LogManager.getLogger().info("s: machine IP " + address.getHostAddress()); + message = String.format("s: Machine IP %s", address.getHostAddress()); + logger.info(message); + if (showIPAddressesInChat) { - sendServerChat(connId, "Machine IP is " + address.getHostAddress()); + sendServerChat(connId, message); } } } catch (Exception ignored) { - + logger.error(ignored, "Ignored Exception"); } - LogManager.getLogger().info("s: listening on port " + serverSocket.getLocalPort()); + 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, "Listening on port " + serverSocket.getLocalPort()); + // 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 = player.getName() + " connected from " + getClient(connId).getInetAddress(); - LogManager.getLogger().info("s: player #" + connId + ", " + who); + 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); } @@ -773,15 +808,13 @@ private Player addNewPlayer(int connId, String name, boolean isBot) { Player newPlayer = new Player(connId, name); newPlayer.setBot(isBot); PlayerColour colour = newPlayer.getColour(); - Enumeration players = getGame().getPlayers(); final PlayerColour[] colours = PlayerColour.values(); - while (players.hasMoreElements()) { - final Player p = players.nextElement(); - if (p.getId() == newPlayer.getId()) { + for (Player player : getGame().getPlayersList()) { + if (player.getId() == newPlayer.getId()) { continue; } - if ((p.getColour() == colour) && (colours.length > (colour.ordinal() + 1))) { + if ((player.getColour() == colour) && (colours.length > (colour.ordinal() + 1))) { colour = colours[colour.ordinal() + 1]; } } @@ -806,8 +839,7 @@ public void validatePlayerInfo(int playerId) { final PlayerColour[] playerColours = PlayerColour.values(); boolean allUsed = true; Set colourUtilization = new HashSet<>(); - for (Enumeration i = getGame().getPlayers(); i.hasMoreElements(); ) { - final Player otherPlayer = i.nextElement(); + for (Player otherPlayer : getGame().getPlayersList()) { if (otherPlayer.getId() != playerId) { colourUtilization.add(otherPlayer.getColour()); } else { @@ -859,7 +891,8 @@ public void sendLoadGame(int connId, String sFile) { * load the game * * @param f The File to load - * @return A boolean value whether or not the loading was successful + * @return A boolean value whether or not the loading was + * successful */ public boolean loadGame(File f) { return loadGame(f, true); @@ -870,12 +903,15 @@ public boolean loadGame(File f) { * * @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 + * current info. This may be false if some reconnection + * remapping * needs to be done first. - * @return A boolean value whether or not the loading was successful + * @return A boolean value whether or not the loading was + * successful */ public boolean loadGame(File f, boolean sendInfo) { - LogManager.getLogger().info("s: loading saved game file '" + f.getAbsolutePath() + '\''); + String message = String.format("s: Loading saved game file '%s'", f.getAbsolutePath()); + logger.info(message); Game newGame; try (InputStream is = new FileInputStream(f)) { @@ -890,7 +926,8 @@ public boolean loadGame(File f, boolean sendInfo) { XStream xstream = SerializationHelper.getLoadSaveGameXStream(); newGame = (Game) xstream.fromXML(gzi); } catch (Exception e) { - LogManager.getLogger().error("Unable to load file: " + f, e); + message = String.format("Unable to load file: %s", f); + logger.error(e, message); return false; } @@ -924,30 +961,35 @@ public void sendSaveGame(int connId, String fileName, String localPath) { * 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 + * @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 unassignedConns = new ArrayList<>(); + // Keep track of which ids are used Set usedPlayerIds = new HashSet<>(); Set currentPlayerNames = new HashSet<>(); - for (Player p : getGame().getPlayersVector()) { + + 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().getPlayersVector()) { + 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())) { @@ -965,10 +1007,11 @@ public void remapConnIds(Map nameToIdMap, Map } // Remap old connection Ids to new ones - for (Integer currConnId : connIdRemapping.keySet()) { - Integer newId = connIdRemapping.get(currConnId); - AbstractConnection conn = connectionIds.get(currConnId); + 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)) { unassignedConns.add(connectionIds.get(newId)); @@ -983,9 +1026,11 @@ public void remapConnIds(Map nameToIdMap, Map // It's possible we have players not in the saved game, add 'em for (AbstractConnection conn : unassignedConns) { int newId = 0; + while (usedPlayerIds.contains(newId)) { newId++; } + String name = idToNameMap.get(conn.getId()); conn.setId(newId); Player newPlayer = addNewPlayer(newId, name, false); @@ -1022,6 +1067,7 @@ private AbstractConnection getClient(int connId) { /** * Executes the process on each active connection. + * * @param process The process to execute. */ public void forEachConnection(Consumer process) { @@ -1051,7 +1097,7 @@ public AbstractConnection getConnection(int connId) { * Sends out all player info to the specified connection */ private void transmitPlayerConnect(AbstractConnection connection) { - for (var player : getGame().getPlayersVector()) { + for (Player player : getGame().getPlayersList()) { var connectionId = connection.getId(); connection.send(createPlayerConnectPacket(player, player.getId() != connectionId)); } @@ -1104,7 +1150,7 @@ void transmitPlayerUpdate(Player player) { * Sends out the player info updates for all players to all connections */ private void transmitAllPlayerUpdates() { - for (var player : getGame().getPlayersVector()) { + for (Player player : getGame().getPlayersList()) { transmitPlayerUpdate(player); } } @@ -1153,14 +1199,15 @@ void send(Packet packet) { } /** - * Sends the given packet to the given connection (= player ID). + * 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) { - if (getClient(connId) != null) { - getClient(connId).send(packet); + AbstractConnection connection = getClient(connId); + + if (connection != null) { + connection.send(packet); } - // What should we do if we've lost this client? - // For now, nothing. } /** @@ -1168,6 +1215,7 @@ public void send(int connId, Packet packet) { */ private void sendToPending(int connId, Packet packet) { AbstractConnection pendingConn = getPendingConnection(connId); + if (pendingConn != null) { pendingConn.send(packet); } @@ -1206,12 +1254,13 @@ 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))) { - LogManager.getLogger().error("Server does not recognize player at connection " + connId); + String message = String.format("Server does not recognize player at connection %d", connId); + logger.error(message); return; } if (packet == null) { - LogManager.getLogger().error("Got null packet"); + logger.error("Got null packet"); return; } // act on it @@ -1221,7 +1270,7 @@ protected void handle(int connId, Packet packet) { if (valid) { sendToPending(connId, new Packet(PacketCommand.SERVER_GREETING)); } else { - sendToPending(connId, new Packet(PacketCommand.ILLEGAL_CLIENT_VERSION, MMConstants.VERSION)); + sendToPending(connId, new Packet(PacketCommand.ILLEGAL_CLIENT_VERSION, SuiteConstants.VERSION)); getPendingConnection(connId).close(); } break; @@ -1273,7 +1322,7 @@ protected void handle(int connId, Packet packet) { sendCurrentInfo(conn.getId()); } } catch (Exception e) { - LogManager.getLogger().error("Error loading save game sent from client", e); + logger.error(e, "Error loading save game sent from client"); } break; default: @@ -1286,14 +1335,16 @@ protected void handle(int connId, Packet packet) { */ @Override public void run() { + String message = ""; Thread currentThread = Thread.currentThread(); - LogManager.getLogger().info("s: listening for clients..."); + logger.info("s: listening for clients..."); while (connector == currentThread) { try { Socket s = serverSocket.accept(); synchronized (serverLock) { int id = getFreeConnectionId(); - LogManager.getLogger().info("s: accepting player connection #" + id + "..."); + message = String.format("s: accepting player connection #%d...", id); + logger.info(message); AbstractConnection c = ConnectionFactory.getInstance().createServerConnection(s, id); c.addConnectionListener(connectionListener); @@ -1321,7 +1372,7 @@ public String getHost() { try { return InetAddress.getLocalHost().getHostName(); } catch (Exception ex) { - LogManager.getLogger().error("", ex); + logger.error(ex, "Get Host exception"); return ""; } } @@ -1334,7 +1385,8 @@ public int getPort() { } /** - * @return the current server instance. This may be null if a server has not been started + * @return the current server instance. This may be null if a server has not + * been started */ public static @Nullable Server getServerInstance() { return serverInstance; @@ -1346,41 +1398,44 @@ private void registerWithServerBrowser(boolean register, String urlString) { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - try (OutputStream os = conn.getOutputStream(); - DataOutputStream dos = new DataOutputStream(os)) { - String content = "port=" + URLEncoder.encode(Integer.toString(serverSocket.getLocalPort()), StandardCharsets.UTF_8); - if (register) { - for (AbstractConnection iconn : connections) { - content += "&players[]=" + getPlayer(iconn.getId()).getName(); - } - if (!getGame().getPhase().isLounge() && !getGame().getPhase().isUnknown()) { - content += "&close=yes"; - } - content += "&version=" + MMConstants.VERSION; - if (isPassworded()) { - content += "&pw=yes"; - } - } else { - content += "&delete=yes"; + OutputStream os = conn.getOutputStream(); + DataOutputStream dos = new DataOutputStream(os); + String content = "port=" + + URLEncoder.encode(Integer.toString(serverSocket.getLocalPort()), StandardCharsets.UTF_8); + if (register) { + for (AbstractConnection iconn : connections) { + content += "&players[]=" + getPlayer(iconn.getId()).getName(); + } + + if (!getGame().getPhase().isLounge() && !getGame().getPhase().isUnknown()) { + content += "&close=yes"; } - if (serverAccessKey != null) { - content += "&key=" + serverAccessKey; + content += "&version=" + SuiteConstants.VERSION; + + if (isPassworded()) { + content += "&pw=yes"; } - dos.writeBytes(content); - dos.flush(); - - try (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; - } - } + } else { + content += "&delete=yes"; + } + + if (serverAccessKey != null) { + content += "&key=" + serverAccessKey; + } + + dos.writeBytes(content); + 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; } } }