From 3a7e1a27ac79f89ea41692c3d43ff376a24d87e3 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 5 Dec 2022 13:44:06 -0500 Subject: [PATCH] 4201: Preventing NPE when Loading a Save Game Without Previously Launching a Game --- .../i18n/megamek/client/messages.properties | 2 +- megamek/src/megamek/Version.java | 10 ++ .../megamek/client/ui/swing/MegaMekGUI.java | 152 +++++++++++++++--- megamek/src/megamek/common/Game.java | 9 +- 4 files changed, 142 insertions(+), 31 deletions(-) diff --git a/megamek/i18n/megamek/client/messages.properties b/megamek/i18n/megamek/client/messages.properties index 272747b8bbc..1cdc8974ba9 100644 --- a/megamek/i18n/megamek/client/messages.properties +++ b/megamek/i18n/megamek/client/messages.properties @@ -3714,6 +3714,7 @@ MegaMek.MapEditor.label=Map Editor MegaMek.SaveGameDialog.title=Select saved game MegaMek.LoadGameAlert.title=Load a Game MegaMek.LoadGameAlert.message=Error: unable to load game file "%s" +MegaMek.LoadGameMissingVersion.message=Error: MegaMek does not support version migration, and thus cannot load a save file missing a version into %s. MegaMek.LoadGameIncorrectVersion.message=Error: MegaMek does not support version migration, and thus cannot load a save file from %s into %s. MegaMek.HostGameAlert.title=Host a Game MegaMek.HostDialog.title=Start Game @@ -3725,7 +3726,6 @@ MegaMek.ScenarioDialog.title=Set Scenario Players MegaMek.ScenarioDialog.me=Me MegaMek.ScenarioDialog.otherh=Other Human MegaMek.ScenarioDialog.bot=Princess -MegaMek.ScenarioDialog.otherbot=Test Bot (Do not use) MegaMek.ScenarioDialog.Camo=Camo MegaMek.SkinEditor.label=Skin Editor MegaMek.NoCamoBtn=No Camo diff --git a/megamek/src/megamek/Version.java b/megamek/src/megamek/Version.java index 9e49554fa3f..cb9004184d3 100644 --- a/megamek/src/megamek/Version.java +++ b/megamek/src/megamek/Version.java @@ -26,6 +26,7 @@ import javax.swing.*; import java.io.PrintWriter; import java.io.Serializable; +import java.util.Objects; /** * This is used for versioning, and to track the current Version the suite is running at. @@ -55,6 +56,15 @@ public Version(final @Nullable String text) { this(); fillFromText(text); } + + public Version(final String release, final String major, final String minor, + final String snapshot) throws NumberFormatException { + this(); + setRelease(Integer.parseInt(release)); + setMajor(Integer.parseInt(major)); + setMinor(Integer.parseInt(minor)); + setSnapshot(Boolean.parseBoolean(snapshot)); + } //endregion Constructors //region Getters diff --git a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java index 341aa7f4548..142d596306d 100644 --- a/megamek/src/megamek/client/ui/swing/MegaMekGUI.java +++ b/megamek/src/megamek/client/ui/swing/MegaMekGUI.java @@ -16,9 +16,9 @@ */ package megamek.client.ui.swing; -import com.thoughtworks.xstream.XStream; import megamek.MMConstants; import megamek.MegaMek; +import megamek.Version; import megamek.client.Client; import megamek.client.bot.BotClient; import megamek.client.bot.TestBot; @@ -49,15 +49,20 @@ import megamek.common.preference.PreferenceManager; import megamek.common.util.EmailService; import megamek.common.util.ImageUtil; -import megamek.common.util.SerializationHelper; import megamek.common.util.fileUtils.MegaMekFile; import megamek.server.GameManager; import megamek.server.ScenarioLoader; import megamek.server.Server; +import megamek.utilities.xml.MMXMLUtility; import org.apache.logging.log4j.LogManager; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import javax.swing.*; import javax.swing.filechooser.FileFilter; +import javax.xml.parsers.DocumentBuilder; import java.awt.*; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; @@ -540,30 +545,47 @@ public String getDescription() { return; } - // extract game data before starting to check and get player names - Game newGame; - try (InputStream is = new FileInputStream(fc.getSelectedFile()); InputStream gzi = new GZIPInputStream(is)) { - XStream xstream = SerializationHelper.getXStream(); - newGame = (Game) xstream.fromXML(gzi); - } catch (Exception e) { - LogManager.getLogger().error("Unable to load file: " + fc.getSelectedFile().getAbsolutePath(), e); - JOptionPane.showMessageDialog(frame, Messages.getFormattedString("MegaMek.LoadGameAlert.message", fc.getSelectedFile().getAbsolutePath()), - Messages.getString("MegaMek.LoadGameAlert.title"), JOptionPane.ERROR_MESSAGE); - return; - } - - if (!MMConstants.VERSION.is(newGame.getVersion())) { - final String message = String.format(Messages.getString("MegaMek.LoadGameIncorrectVersion.message"), - newGame.getVersion(), MMConstants.VERSION); - JOptionPane.showMessageDialog(frame, message, - Messages.getString("MegaMek.LoadGameAlert.title"), JOptionPane.ERROR_MESSAGE); - LogManager.getLogger().error(message); - return; - } + final Vector playerNames = new Vector<>(); + + // Handrolled extraction, as we require Server initialization to use XStream and don't need + // the additional overhead of initializing everything twice + try (InputStream is = new FileInputStream(fc.getSelectedFile()); + InputStream gzi = new GZIPInputStream(is)) { + // Using factory get an instance of document builder + final DocumentBuilder documentBuilder = MMXMLUtility.newSafeDocumentBuilder(); + // Parse using builder to get DOM representation of the XML file + final Document xmlDocument = documentBuilder.parse(gzi); + + final Element gameElement = xmlDocument.getDocumentElement(); + gameElement.normalize(); + + final NodeList nl = gameElement.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + final Node n = nl.item(i); + if (n.getNodeType() != Node.ELEMENT_NODE) { + continue; + } - Vector playerNames = new Vector<>(); - for (Player player : newGame.getPlayersVector()) { - playerNames.add(player.getName()); + switch (n.getNodeName()) { + case "version": + if (!validateSaveVersion(n)) { + return; + } + break; + case "players": + parsePlayerNames(n, playerNames); + break; + default: + break; + } + } + } catch (Exception ex) { + LogManager.getLogger().error("Unable to load file: " + fc.getSelectedFile().getAbsolutePath(), ex); + JOptionPane.showMessageDialog(frame, + Messages.getFormattedString("MegaMek.LoadGameAlert.message", + fc.getSelectedFile().getAbsolutePath()), + Messages.getString("MegaMek.LoadGameAlert.title"), + JOptionPane.ERROR_MESSAGE); } HostDialog hd = new HostDialog(frame, playerNames); @@ -577,6 +599,86 @@ public String getDescription() { hd.isRegister(), hd.isRegister() ? hd.getMetaserver() : "", null, fc.getSelectedFile(), hd.getPlayerName()); } + + private boolean validateSaveVersion(final Node n) { + if (!n.hasChildNodes()) { + final String message = String.format( + Messages.getString("MegaMek.LoadGameMissingVersion.message"), + MMConstants.VERSION); + JOptionPane.showMessageDialog(frame, message, + Messages.getString("MegaMek.LoadGameAlert.title"), + JOptionPane.ERROR_MESSAGE); + LogManager.getLogger().error(message); + } + final NodeList nl = n.getChildNodes(); + String release = null; + String major = null; + String minor = null; + String snapshot = null; + for (int i = 0; i < nl.getLength(); i++) { + final Node n2 = nl.item(i); + if (n2.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + switch (n2.getNodeName()) { + case "release": + release = n2.getTextContent(); + break; + case "major": + major = n2.getTextContent(); + break; + case "minor": + minor = n2.getTextContent(); + break; + case "snapshot": + snapshot = n2.getTextContent(); + break; + default: + break; + } + } + + final Version version = new Version(release, major, minor, snapshot); + if (MMConstants.VERSION.is(version)) { + return true; + } else { + final String message = String.format( + Messages.getString("MegaMek.LoadGameIncorrectVersion.message"), + version, MMConstants.VERSION); + JOptionPane.showMessageDialog(frame, message, + Messages.getString("MegaMek.LoadGameAlert.title"), JOptionPane.ERROR_MESSAGE); + LogManager.getLogger().error(message); + return false; + } + } + + private void parsePlayerNames(final Node n, final Vector playerNames) { + if (!n.hasChildNodes()) { + return; + } + + final NodeList nl = n.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + final Node n2 = nl.item(i); + if ((n2.getNodeType() != Node.ELEMENT_NODE) || !n2.hasChildNodes() + || !"megamek.common.Player".equals(n2.getNodeName())) { + continue; + } + + final NodeList nl2 = n2.getChildNodes(); + for (int j = 0; j < nl2.getLength(); j++) { + final Node n3 = nl2.item(j); + if (n3.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + + if ("name".equals(n3.getNodeName())) { + playerNames.add(n3.getTextContent()); + } + } + } + } /** Developer Utility: Loads "quicksave.sav.gz" with the last used connection settings. */ public void quickLoadGame() { diff --git a/megamek/src/megamek/common/Game.java b/megamek/src/megamek/common/Game.java index 5c7e94012f0..7ac1de83e04 100644 --- a/megamek/src/megamek/common/Game.java +++ b/megamek/src/megamek/common/Game.java @@ -57,6 +57,10 @@ public class Game implements IGame, Serializable { */ public final Version version = MMConstants.VERSION; + private Vector players = new Vector<>(); + private Hashtable playerIds = new Hashtable<>(); + private Vector teams = new Vector<>(); + private GameOptions options = new GameOptions(); private Board board = new Board(); @@ -71,11 +75,6 @@ public class Game implements IGame, Serializable { */ private Vector vOutOfGame = new Vector<>(); - private Vector players = new Vector<>(); - private Vector teams = new Vector<>(); - - private Hashtable playerIds = new Hashtable<>(); - private final Map> entityPosLookup = new HashMap<>(); /**