Skip to content

Commit

Permalink
4201: Preventing NPE when Loading a Save Game Without Previously Laun…
Browse files Browse the repository at this point in the history
…ching a Game
  • Loading branch information
Windchild292 committed Dec 5, 2022
1 parent 7a229a9 commit 3a7e1a2
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 31 deletions.
2 changes: 1 addition & 1 deletion megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
10 changes: 10 additions & 0 deletions megamek/src/megamek/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
152 changes: 127 additions & 25 deletions megamek/src/megamek/client/ui/swing/MegaMekGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> 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<String> 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);
Expand All @@ -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<String> 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() {
Expand Down
9 changes: 4 additions & 5 deletions megamek/src/megamek/common/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public class Game implements IGame, Serializable {
*/
public final Version version = MMConstants.VERSION;

private Vector<Player> players = new Vector<>();
private Hashtable<Integer, Player> playerIds = new Hashtable<>();
private Vector<Team> teams = new Vector<>();

private GameOptions options = new GameOptions();

private Board board = new Board();
Expand All @@ -71,11 +75,6 @@ public class Game implements IGame, Serializable {
*/
private Vector<Entity> vOutOfGame = new Vector<>();

private Vector<Player> players = new Vector<>();
private Vector<Team> teams = new Vector<>();

private Hashtable<Integer, Player> playerIds = new Hashtable<>();

private final Map<Coords, HashSet<Integer>> entityPosLookup = new HashMap<>();

/**
Expand Down

0 comments on commit 3a7e1a2

Please sign in to comment.