package games.strategy.engine.data; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicReference; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import games.strategy.debug.ClientLogger; import games.strategy.engine.ClientContext; import games.strategy.engine.data.gameparser.XmlGameElementMapper; import games.strategy.engine.data.properties.BooleanProperty; import games.strategy.engine.data.properties.ColorProperty; import games.strategy.engine.data.properties.ComboProperty; import games.strategy.engine.data.properties.FileProperty; import games.strategy.engine.data.properties.GameProperties; import games.strategy.engine.data.properties.IEditableProperty; import games.strategy.engine.data.properties.NumberProperty; import games.strategy.engine.data.properties.StringProperty; import games.strategy.engine.delegate.IDelegate; import games.strategy.engine.framework.IGameLoader; import games.strategy.triplea.Constants; import games.strategy.triplea.attachments.TechAbilityAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.GenericTechAdvance; import games.strategy.triplea.delegate.TechAdvance; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.util.Tuple; import games.strategy.util.Version; public class GameParser { private static final Class<?>[] SETTER_ARGS = {String.class}; private GameData data; private final Collection<SAXParseException> errorsSAX = new ArrayList<>(); public static final String DTD_FILE_NAME = "game.dtd"; private final String mapName; public GameParser(final String mapName) { this.mapName = mapName; } /** * Parses a file into a GameData object. * * @param delayParsing * Should we only parse the game name, notes, and playerlist? Normally this should be "false", except for the * game chooser which * should use the user set preference. */ public synchronized GameData parse(final InputStream stream, final AtomicReference<String> gameName, final boolean delayParsing) throws GameParseException, SAXException, EngineVersionException, IllegalArgumentException { if (stream == null) { throw new IllegalArgumentException("Stream must be non null"); } Document doc = null; try { doc = getDocument(stream); } catch (final IOException | ParserConfigurationException e) { throw new IllegalStateException("Error parsing: " + mapName, e); } final Element root = doc.getDocumentElement(); data = new GameData(); // mandatory fields // get the name of the map parseInfo(getSingleChild("info", root)); if (gameName != null) { gameName.set(data.getGameName()); } // test minimum engine version FIRST parseMinimumEngineVersionNumber(getSingleChild("triplea", root, true)); parseGameLoader(getSingleChild("loader", root)); // if we manage to get this far, past the minimum engine version number test, AND we are still good, then check and // see if we have any // SAX errors we need to show if (!errorsSAX.isEmpty()) { for (final SAXParseException error : errorsSAX) { System.err.println("SAXParseException: game: " + (data == null ? "?" : (data.getGameName() == null ? "?" : data.getGameName())) + ", line: " + error.getLineNumber() + ", column: " + error.getColumnNumber() + ", error: " + error.getMessage()); } } parseDiceSides(getSingleChild("diceSides", root, true)); final Element playerListNode = getSingleChild("playerList", root); parsePlayerList(playerListNode); parseAlliances(playerListNode); final Node properties = getSingleChild("propertyList", root, true); if (properties != null) { parseProperties(properties); } // everything until here is needed to select a game, the rest can be parsed when a game is selected if (delayParsing) { return data; } parseMap(getSingleChild("map", root)); final Element resourceList = getSingleChild("resourceList", root, true); if (resourceList != null) { parseResources(resourceList); } final Element unitList = getSingleChild("unitList", root, true); if (unitList != null) { parseUnits(unitList); } // Parse all different relationshipTypes that are defined in the xml, for example: War, Allied, Neutral, NAP final Element relationshipTypes = getSingleChild("relationshipTypes", root, true); if (relationshipTypes != null) { parseRelationshipTypes(relationshipTypes); } final Element territoryEffectList = getSingleChild("territoryEffectList", root, true); if (territoryEffectList != null) { parseTerritoryEffects(territoryEffectList); } parseGamePlay(getSingleChild("gamePlay", root)); final Element production = getSingleChild("production", root, true); if (production != null) { parseProduction(production); } final Element technology = getSingleChild("technology", root, true); if (technology != null) { parseTechnology(technology); } else { TechAdvance.createDefaultTechAdvances(data); } final Element attachmentList = getSingleChild("attachmentList", root, true); if (attachmentList != null) { parseAttachments(attachmentList); } final Node initialization = getSingleChild("initialize", root, true); if (initialization != null) { parseInitialization(initialization); } // set & override default relationships // sets the relationship between all players and the NullPlayer to NullRelation // (with archeType War) data.getRelationshipTracker().setNullPlayerRelations(); // sets the relationship for all players with themselfs to the SelfRelation (with archeType Allied) data.getRelationshipTracker().setSelfRelations(); // set default tech attachments (comes after we parse all technologies, parse all attachments, and parse all game // options/properties) if (data.getGameLoader() instanceof games.strategy.triplea.TripleA) { checkThatAllUnitsHaveAttachments(data); TechAbilityAttachment.setDefaultTechnologyAttachments(data); } try { validate(); } catch (final Exception e) { ClientLogger.logQuietly("Error parsing: " + mapName, e); throw new GameParseException(mapName, e.getMessage()); } return data; } private void parseDiceSides(final Node diceSides) { if (diceSides == null) { data.setDiceSides(6); } else { data.setDiceSides(Integer.parseInt(((Element) diceSides).getAttribute("value"))); } } private void parseMinimumEngineVersionNumber(final Node minimumVersion) throws EngineVersionException { if (minimumVersion == null) { return; } final Version mapCompatibleWithTripleaVersion = new Version(((Element) minimumVersion).getAttribute("minimumVersion")); if (mapCompatibleWithTripleaVersion.isGreaterThan(ClientContext.engineVersion().getVersion(), true)) { throw new EngineVersionException("Trying to play a map made for a newer version of TripleA. Map named '" + data.getGameName() + "' requires at least TripleA version " + mapCompatibleWithTripleaVersion.toString()); } } private void validate() throws GameParseException { // validate unit attachments for (final UnitType u : data.getUnitTypeList()) { validateAttachments(u); } for (final Territory t : data.getMap()) { validateAttachments(t); } for (final Resource r : data.getResourceList().getResources()) { validateAttachments(r); } for (final PlayerID r : data.getPlayerList().getPlayers()) { validateAttachments(r); } for (final RelationshipType r : data.getRelationshipTypeList().getAllRelationshipTypes()) { validateAttachments(r); } for (final TerritoryEffect r : data.getTerritoryEffectList().values()) { validateAttachments(r); } for (final TechAdvance r : data.getTechnologyFrontier().getTechs()) { validateAttachments(r); } // if relationships are used, every player should have a relationship with every other player validateRelationships(); } private void validateRelationships() throws GameParseException { // for every player for (final PlayerID player : data.getPlayerList()) { // in relation to every player for (final PlayerID player2 : data.getPlayerList()) { // See if there is a relationship between them if ((data.getRelationshipTracker().getRelationshipType(player, player2) == null)) { throw new GameParseException(mapName, "No relation set for: " + player.getName() + " and " + player2.getName()); // or else throw an exception! } } } } private void validateAttachments(final Attachable attachable) throws GameParseException { for (final IAttachment a : attachable.getAttachments().values()) { a.validate(data); } } public Document getDocument(final InputStream input) throws SAXException, IOException, ParserConfigurationException { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); // get the dtd location final String dtdFile = "/games/strategy/engine/xml/" + DTD_FILE_NAME; final URL url = GameParser.class.getResource(dtdFile); if (url == null) { throw new RuntimeException("Map: " + mapName + ", " + String.format("Could not find in classpath %s", dtdFile)); } final String dtdSystem = url.toExternalForm(); final String system = dtdSystem.substring(0, dtdSystem.length() - 8); final DocumentBuilder builder = factory.newDocumentBuilder(); builder.setErrorHandler(new ErrorHandler() { @Override public void fatalError(final SAXParseException exception) { errorsSAX.add(exception); } @Override public void error(final SAXParseException exception) { errorsSAX.add(exception); } @Override public void warning(final SAXParseException exception) { errorsSAX.add(exception); } }); return builder.parse(input, system); } /** * If mustfind is true and cannot find the player an exception will be thrown. */ private PlayerID getPlayerID(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final PlayerID player = data.getPlayerList().getPlayerID(name); if (player == null && mustFind) { throw new GameParseException(mapName, "Could not find player. name:" + name); } return player; } /** * If mustfind is true and cannot find the player an exception will be thrown. * * @return a RelationshipType from the relationshipTypeList, at this point all relationshipTypes should have been * declared * @throws GameParseException * when */ private RelationshipType getRelationshipType(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final RelationshipType relation = data.getRelationshipTypeList().getRelationshipType(name); if (relation == null && mustFind) { throw new GameParseException(mapName, "Could not find relation name:" + name); } return relation; } private TerritoryEffect getTerritoryEffect(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final TerritoryEffect effect = data.getTerritoryEffectList().get(name); if (effect == null && mustFind) { throw new GameParseException(mapName, "Could not find territoryEffect name:" + name); } return effect; } /** * If mustfind is true and cannot find the productionRule an exception will be thrown. */ private ProductionRule getProductionRule(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final ProductionRule productionRule = data.getProductionRuleList().getProductionRule(name); if (productionRule == null && mustFind) { throw new GameParseException(mapName, "Could not find production rule. name:" + name); } return productionRule; } /** * If mustfind is true and cannot find the productionRule an exception will be thrown. */ private RepairRule getRepairRule(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final RepairRule repairRule = data.getRepairRuleList().getRepairRule(name); if (repairRule == null && mustFind) { throw new GameParseException(mapName, "Could not find production rule. name:" + name); } return repairRule; } /** * If mustfind is true and cannot find the territory an exception will be thrown. */ private Territory getTerritory(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final Territory territory = data.getMap().getTerritory(name); if (territory == null && mustFind) { throw new GameParseException(mapName, "Could not find territory. name:" + name); } return territory; } /** * If mustfind is true and cannot find the unitType an exception will be thrown. */ private UnitType getUnitType(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final UnitType type = data.getUnitTypeList().getUnitType(name); if (type == null && mustFind) { throw new GameParseException(mapName, "Could not find unitType. name:" + name); } return type; } /** * If mustfind is true and cannot find the technology an exception will be thrown. */ private TechAdvance getTechnology(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); TechAdvance type = data.getTechnologyFrontier().getAdvanceByName(name); if (type == null) { type = data.getTechnologyFrontier().getAdvanceByProperty(name); } if (type == null && mustFind) { throw new GameParseException(mapName, "Could not find technology. name:" + name); } return type; } /** * If mustfind is true and cannot find the Delegate an exception will be thrown. */ private IDelegate getDelegate(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final IDelegate delegate = data.getDelegateList().getDelegate(name); if (delegate == null && mustFind) { throw new GameParseException(mapName, "Could not find delegate. name:" + name); } return delegate; } /** * If mustfind is true and cannot find the Resource an exception will be thrown. */ private Resource getResource(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final Resource resource = data.getResourceList().getResource(name); if (resource == null && mustFind) { throw new GameParseException(mapName, "Could not find resource. name:" + name); } return resource; } /** * If mustfind is true and cannot find the productionRule an exception will be thrown. */ private ProductionFrontier getProductionFrontier(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final ProductionFrontier productionFrontier = data.getProductionFrontierList().getProductionFrontier(name); if (productionFrontier == null && mustFind) { throw new GameParseException(mapName, "Could not find production frontier. name:" + name); } return productionFrontier; } /** * If mustfind is true and cannot find the productionRule an exception will be thrown. */ private RepairFrontier getRepairFrontier(final Element element, final String attribute, final boolean mustFind) throws GameParseException { final String name = element.getAttribute(attribute); final RepairFrontier repairFrontier = data.getRepairFrontierList().getRepairFrontier(name); if (repairFrontier == null && mustFind) { throw new GameParseException(mapName, "Could not find production frontier. name:" + name); } return repairFrontier; } /** * Loads an instance of the given class. * Assumes a zero argument constructor. */ private Object getInstance(final String className) throws GameParseException { Object instance = null; try { final Class<?> instanceClass = Class.forName(className); instance = instanceClass.newInstance(); // a lot can go wrong, the following list is just a subset of potential pitfalls } catch (final ClassNotFoundException cnfe) { throw new GameParseException(mapName, "Class <" + className + "> could not be found."); } catch (final InstantiationException ie) { throw new GameParseException(mapName, "Class <" + className + "> could not be instantiated. ->" + ie.getMessage()); } catch (final IllegalAccessException iae) { throw new GameParseException(mapName, "Constructor could not be accessed ->" + iae.getMessage()); } return instance; } /** * Get the given child. * If there is not exactly one child throw a SAXExcpetion */ private Element getSingleChild(final String name, final Element node) throws GameParseException { return getSingleChild(name, node, false); } /** * If optional is true, will not throw an exception if there are 0 children. */ private Element getSingleChild(final String name, final Node node, final boolean optional) throws GameParseException { final List<Element> children = getChildren(name, node); // none found if (children.size() == 0) { if (optional) { return null; } throw new GameParseException(mapName, "No child called " + name); } // too many found if (children.size() > 1) { throw new GameParseException(mapName, "Too many children named " + name); } return children.get(0); } private List<Element> getChildren(final String name, final Node node) { final ArrayList<Element> found = new ArrayList<>(); final NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { final Node current = children.item(i); if (current.getNodeName().equals(name)) { found.add((Element) current); } } return found; } private List<Node> getNonTextNodesIgnoring(final Node node, final String ignore) { final List<Node> rVal = getNonTextNodes(node); final Iterator<Node> iter = rVal.iterator(); while (iter.hasNext()) { if (((Element) iter.next()).getTagName().equals(ignore)) { iter.remove(); } } return rVal; } private List<Node> getNonTextNodes(final Node node) { final ArrayList<Node> found = new ArrayList<>(); final NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); ++i) { final Node current = children.item(i); if (!(current.getNodeType() == Node.TEXT_NODE)) { found.add(current); } } return found; } private void parseInfo(final Node info) { final String gameName = ((Element) info).getAttribute("name"); data.setGameName(gameName); final String version = ((Element) info).getAttribute("version"); data.setGameVersion(new Version(version)); } private void parseGameLoader(final Node loader) throws GameParseException { final String className = ((Element) loader).getAttribute("javaClass"); final Object instance = getInstance(className); if (!(instance instanceof IGameLoader)) { throw new GameParseException(mapName, "Loader must implement IGameLoader. Class Name:" + className); } data.setGameLoader((IGameLoader) instance); } private void parseMap(final Node map) throws GameParseException { final List<Element> grids = getChildren("grid", map); parseGrids(grids); // get the Territories final List<Element> territories = getChildren("territory", map); parseTerritories(territories); final List<Element> connections = getChildren("connection", map); parseConnections(connections); } private void parseGrids(final List<Element> grids) throws GameParseException { for (final Element current : grids) { final String gridType = current.getAttribute("type"); final String name = current.getAttribute("name"); final String xs = current.getAttribute("x"); final String ys = current.getAttribute("y"); final List<Element> waterNodes = getChildren("water", current); final Set<String> water = parseGridWater(waterNodes); final String horizontalConnections = current.getAttribute("horizontal-connections"); final String verticalConnections = current.getAttribute("vertical-connections"); final String diagonalConnections = current.getAttribute("diagonal-connections"); setGrids(data, gridType, name, xs, ys, water, horizontalConnections, verticalConnections, diagonalConnections, false); } } /** * Creates and adds new territories and their connections to their map, based on a grid. */ private void setGrids(final GameData data, final String gridType, final String name, final String xs, final String ys, final Set<String> water, final String horizontalConnections, final String verticalConnections, final String diagonalConnections, final boolean addingOntoExistingMap) throws GameParseException { final GameMap map = data.getMap(); boolean horizontalConnectionsImplict; if (horizontalConnections.equals("implicit")) { horizontalConnectionsImplict = true; } else if (horizontalConnections.equals("explicit")) { horizontalConnectionsImplict = false; } else { throw new GameParseException(mapName, "horizontal-connections attribute must be either \"explicit\" or \"implicit\""); } boolean verticalConnectionsImplict; if (verticalConnections.equals("implicit")) { verticalConnectionsImplict = true; } else if (verticalConnections.equals("explicit")) { verticalConnectionsImplict = false; } else { throw new GameParseException(mapName, "vertical-connections attribute must be either \"explicit\" or \"implicit\""); } boolean diagonalConnectionsImplict; if (diagonalConnections.equals("implicit")) { diagonalConnectionsImplict = true; } else if (diagonalConnections.equals("explicit")) { diagonalConnectionsImplict = false; } else { throw new GameParseException(mapName, "diagonal-connections attribute must be either \"explicit\" or \"implicit\""); } final int x_size = Integer.valueOf(xs); int y_size; if (ys != null) { y_size = Integer.valueOf(ys); } else { y_size = 0; } map.setGridDimensions(x_size, y_size); if (gridType.equals("square")) { // Add territories for (int y = 0; y < y_size; y++) { for (int x = 0; x < x_size; x++) { boolean isWater; isWater = water.contains(x + "-" + y); final Territory newTerritory = new Territory(name + "_" + x + "_" + y, isWater, data, x, y); if (addingOntoExistingMap && map.getTerritories().contains(newTerritory)) { continue; } map.addTerritory(newTerritory); } } if (addingOntoExistingMap) { map.reorderTerritoryList(); } // Add any implicit horizontal connections if (horizontalConnectionsImplict) { for (int y = 0; y < y_size; y++) { for (int x = 0; x < x_size - 1; x++) { map.addConnection(map.getTerritoryFromCoordinates(x, y), map.getTerritoryFromCoordinates(x + 1, y)); } } } // Add any implicit vertical connections if (verticalConnectionsImplict) { for (int x = 0; x < x_size; x++) { for (int y = 0; y < y_size - 1; y++) { map.addConnection(map.getTerritoryFromCoordinates(x, y), map.getTerritoryFromCoordinates(x, y + 1)); } } } // Add any implicit acute diagonal connections if (diagonalConnectionsImplict) { for (int y = 0; y < y_size - 1; y++) { for (int x = 0; x < x_size - 1; x++) { map.addConnection(map.getTerritoryFromCoordinates(x, y), map.getTerritoryFromCoordinates(x + 1, y + 1)); } } } // Add any implicit obtuse diagonal connections if (diagonalConnectionsImplict) { for (int y = 0; y < y_size - 1; y++) { for (int x = 1; x < x_size; x++) { map.addConnection(map.getTerritoryFromCoordinates(x, y), map.getTerritoryFromCoordinates(x - 1, y + 1)); } } } // This type is a triangular grid of points and lines, used for in several rail games } else if (gridType.equals("points-and-lines")) { // Add territories for (int y = 0; y < y_size; y++) { for (int x = 0; x < x_size; x++) { final boolean isWater = false; if (!water.contains(x + "-" + y)) { final Territory newTerritory = new Territory(name + "_" + x + "_" + y, isWater, data, x, y); if (addingOntoExistingMap && map.getTerritories().contains(newTerritory)) { continue; } map.addTerritory(newTerritory); } } } if (addingOntoExistingMap) { map.reorderTerritoryList(); } // Add any implicit horizontal connections if (horizontalConnectionsImplict) { for (int y = 0; y < y_size; y++) { for (int x = 0; x < x_size - 1; x++) { final Territory from = map.getTerritoryFromCoordinates(x, y); final Territory to = map.getTerritoryFromCoordinates(x + 1, y); if (from != null && to != null) { map.addConnection(from, to); } } } } // Add any implicit acute diagonal connections if (diagonalConnectionsImplict) { for (int y = 1; y < y_size; y++) { for (int x = 0; x < x_size - 1; x++) { if (y % 4 == 0 || (y + 1) % 4 == 0) { final Territory from = map.getTerritoryFromCoordinates(x, y); final Territory to = map.getTerritoryFromCoordinates(x, y - 1); if (from != null && to != null) { map.addConnection(from, to); } } else { final Territory from = map.getTerritoryFromCoordinates(x, y); final Territory to = map.getTerritoryFromCoordinates(x + 1, y - 1); if (from != null && to != null) { map.addConnection(from, to); } } } } } // Add any implicit obtuse diagonal connections if (diagonalConnectionsImplict) { for (int y = 1; y < y_size; y++) { for (int x = 0; x < x_size - 1; x++) { if (y % 4 == 0 || (y + 1) % 4 == 0) { final Territory from = map.getTerritoryFromCoordinates(x, y); final Territory to = map.getTerritoryFromCoordinates(x - 1, y - 1); if (from != null && to != null) { map.addConnection(from, to); } } else { final Territory from = map.getTerritoryFromCoordinates(x, y); final Territory to = map.getTerritoryFromCoordinates(x, y - 1); if (from != null && to != null) { map.addConnection(from, to); } } } } } } } private Set<String> parseGridWater(final List<Element> waterNodes) { final Set<String> set = new HashSet<>(); for (final Element current : waterNodes) { final int x = Integer.valueOf(current.getAttribute("x")); final int y = Integer.valueOf(current.getAttribute("y")); set.add(x + "-" + y); } return set; } private void parseTerritories(final List<Element> territories) { final GameMap map = data.getMap(); for (final Element current : territories) { final boolean water = current.getAttribute("water").trim().equalsIgnoreCase("true"); final String name = current.getAttribute("name"); final Territory newTerritory = new Territory(name, water, data); map.addTerritory(newTerritory); } } private void parseConnections(final List<Element> connections) throws GameParseException { final GameMap map = data.getMap(); for (final Element current : connections) { final Territory t1 = getTerritory(current, "t1", true); final Territory t2 = getTerritory(current, "t2", true); map.addConnection(t1, t2); } } private void parseResources(final Element root) { final Iterator<Element> iter = getChildren("resource", root).iterator(); while (iter.hasNext()) { data.getResourceList().addResource(new Resource(iter.next().getAttribute("name"), data)); } } private void parseRelationshipTypes(final Element root) { final Iterator<Element> iter = getChildren("relationshipType", root).iterator(); while (iter.hasNext()) { data.getRelationshipTypeList().addRelationshipType(new RelationshipType(iter.next().getAttribute("name"), data)); } } private void parseTerritoryEffects(final Element root) { final Iterator<Element> iter = getChildren("territoryEffect", root).iterator(); while (iter.hasNext()) { final String name = iter.next().getAttribute("name"); data.getTerritoryEffectList().put(name, new TerritoryEffect(name, data)); } } private void parseUnits(final Element root) { final Iterator<Element> iter = getChildren("unit", root).iterator(); while (iter.hasNext()) { data.getUnitTypeList().addUnitType(new UnitType(iter.next().getAttribute("name"), data)); } } /** * @param root * root node containing the playerList. */ private void parsePlayerList(final Element root) { final PlayerList playerList = data.getPlayerList(); for (final Element current : getChildren("player", root)) { final String name = current.getAttribute("name"); // It appears the commented line ALWAYS returns false regardless of the value of current.getAttribute("optional") // boolean isOptional = Boolean.getBoolean(current.getAttribute("optional")); final boolean isOptional = current.getAttribute("optional").equals("true"); final boolean canBeDisabled = current.getAttribute("canBeDisabled").equals("true"); final PlayerID newPlayer = new PlayerID(name, isOptional, canBeDisabled, data); playerList.addPlayerID(newPlayer); } } private void parseAlliances(final Element root) throws GameParseException { final AllianceTracker allianceTracker = data.getAllianceTracker(); final Collection<PlayerID> players = data.getPlayerList().getPlayers(); for (final Element current : getChildren("alliance", root)) { final PlayerID p1 = getPlayerID(current, "player", true); final String alliance = current.getAttribute("alliance"); allianceTracker.addToAlliance(p1, alliance); } // if relationships aren't initialized based on relationshipInitialize we use the alliances to set the relationships if (getSingleChild("relationshipInitialize", root, true) == null) { final RelationshipTracker relationshipTracker = data.getRelationshipTracker(); final RelationshipTypeList relationshipTypeList = data.getRelationshipTypeList(); // iterate through all players to get known allies and enemies for (final PlayerID currentPlayer : players) { // start with all players as enemies // start with no players as allies final Set<PlayerID> allies = allianceTracker.getAllies(currentPlayer); final Set<PlayerID> enemies = new HashSet<>(players); enemies.removeAll(allies); // remove self from enemieslist (in case of free-for-all) enemies.remove(currentPlayer); // remove self from allieslist (in case you are a member of an alliance) allies.remove(currentPlayer); // At this point enemies and allies should be set for this player. for (final PlayerID alliedPLayer : allies) { relationshipTracker.setRelationship(currentPlayer, alliedPLayer, relationshipTypeList.getDefaultAlliedRelationship()); } for (final PlayerID enemyPlayer : enemies) { relationshipTracker.setRelationship(currentPlayer, enemyPlayer, relationshipTypeList.getDefaultWarRelationship()); } } } } private void parseRelationInitialize(final List<Element> relations) throws GameParseException { if (relations.size() > 0) { final RelationshipTracker tracker = data.getRelationshipTracker(); for (final Element current : relations) { final PlayerID p1 = getPlayerID(current, "player1", true); final PlayerID p2 = getPlayerID(current, "player2", true); final RelationshipType r = getRelationshipType(current, "type", true); final int roundValue = Integer.valueOf(current.getAttribute("roundValue")); tracker.setRelationship(p1, p2, r, roundValue); } } } private void parseGamePlay(final Element root) throws GameParseException { parseDelegates(getChildren("delegate", root)); parseSequence(getSingleChild("sequence", root)); parseOffset(getSingleChild("offset", root, true)); } private void parseProperties(final Node root) throws GameParseException { final Collection<String> runningList = new ArrayList<>(); final GameProperties properties = data.getProperties(); final Iterator<Element> children = getChildren("property", root).iterator(); while (children.hasNext()) { final Element current = children.next(); final String editable = current.getAttribute("editable"); final String property = current.getAttribute("name"); String value = current.getAttribute("value"); runningList.add(property); if (value == null || value.length() == 0) { final List<Element> valueChildren = getChildren("value", current); if (!valueChildren.isEmpty()) { final Element valueNode = valueChildren.get(0); if (valueNode != null) { value = valueNode.getTextContent(); } } } if (editable != null && editable.equalsIgnoreCase("true")) { parseEditableProperty(current, property, value); } else { final List<Node> children2 = getNonTextNodesIgnoring(current, "value"); if (children2.size() == 0) { // we don't know what type this property is!!, it appears like only numbers and string may be represented // without proper type // definition try { // test if it is an integer final int integer = Integer.parseInt(value); properties.set(property, integer); } catch (final NumberFormatException e) { // then it must be a string properties.set(property, value); } } else { final String type = children2.get(0).getNodeName(); if (type.equals("boolean")) { properties.set(property, Boolean.valueOf(value)); } else if (type.equals("file")) { properties.set(property, new File(value)); } else if (type.equals("number")) { int intValue = 0; if (value != null) { try { intValue = Integer.parseInt(value); } catch (final NumberFormatException e) { // value already 0 } } properties.set(property, intValue); } else { properties.set(property, value); } } } } // add properties for all triplea related maps here: if (!runningList.contains(Constants.AI_BONUS_INCOME_FLAT_RATE)) { data.getProperties() .addEditableProperty(new NumberProperty(Constants.AI_BONUS_INCOME_FLAT_RATE, null, 40, -20, 0)); } if (!runningList.contains(Constants.AI_BONUS_INCOME_PERCENTAGE)) { data.getProperties() .addEditableProperty(new NumberProperty(Constants.AI_BONUS_INCOME_PERCENTAGE, null, 200, -100, 0)); } if (!runningList.contains(Constants.AI_BONUS_ATTACK)) { data.getProperties() .addEditableProperty(new NumberProperty(Constants.AI_BONUS_ATTACK, null, data.getDiceSides(), 0, 0)); } if (!runningList.contains(Constants.AI_BONUS_DEFENSE)) { data.getProperties() .addEditableProperty(new NumberProperty(Constants.AI_BONUS_DEFENSE, null, data.getDiceSides(), 0, 0)); } } private void parseEditableProperty(final Element property, final String name, final String defaultValue) throws GameParseException { // what type final List<Node> children = getNonTextNodes(property); if (children.size() != 1) { throw new GameParseException(mapName, "Editable properties must have exactly 1 child specifying the type. Number of children found:" + children.size() + " for node:" + property.getNodeName()); } final Element child = (Element) children.get(0); final String childName = child.getNodeName(); IEditableProperty editableProperty; if (childName.equals("boolean")) { editableProperty = new BooleanProperty(name, null, Boolean.valueOf(defaultValue).booleanValue()); } else if (childName.equals("file")) { editableProperty = new FileProperty(name, null, defaultValue); } else if (childName.equals("list") || childName.equals("combo")) { final StringTokenizer tokenizer = new StringTokenizer(child.getAttribute("values"), ","); final Collection<String> values = new ArrayList<>(); while (tokenizer.hasMoreElements()) { values.add(tokenizer.nextToken()); } editableProperty = new ComboProperty<>(name, null, defaultValue, values); } else if (childName.equals("number")) { final int max = Integer.valueOf(child.getAttribute("max")).intValue(); final int min = Integer.valueOf(child.getAttribute("min")).intValue(); final int def = Integer.valueOf(defaultValue).intValue(); editableProperty = new NumberProperty(name, null, max, min, def); } else if (childName.equals("color")) { // Parse the value as a hexidecimal number final int def = Integer.valueOf(defaultValue, 16).intValue(); editableProperty = new ColorProperty(name, null, def); } else if (childName.equals("string")) { editableProperty = new StringProperty(name, null, defaultValue); } else { throw new GameParseException(mapName, "Unrecognized property type:" + childName); } data.getProperties().addEditableProperty(editableProperty); } private void parseOffset(final Node offsetAttributes) { if (offsetAttributes == null) { return; } final int roundOffset = Integer.parseInt(((Element) offsetAttributes).getAttribute("round")); data.getSequence().setRoundOffset(roundOffset); } private void parseDelegates(final List<Element> delegateList) throws GameParseException { final DelegateList delegates = data.getDelegateList(); final Iterator<Element> iterator = delegateList.iterator(); while (iterator.hasNext()) { final Element current = iterator.next(); // load the class final String className = current.getAttribute("javaClass"); XmlGameElementMapper elementMapper = new XmlGameElementMapper(); IDelegate delegate = elementMapper.getDelegate(className).orElseThrow( () -> new GameParseException(mapName, "Class <" + className + "> is not a delegate.")); final String name = current.getAttribute("name"); String displayName = current.getAttribute("display"); if (displayName == null) { displayName = name; } delegate.initialize(name, displayName); delegates.addDelegate(delegate); } } private void parseSequence(final Node sequence) throws GameParseException { parseSteps(getChildren("step", sequence)); } private void parseSteps(final List<Element> stepList) throws GameParseException { final Iterator<Element> iterator = stepList.iterator(); while (iterator.hasNext()) { final Element current = iterator.next(); final IDelegate delegate = getDelegate(current, "delegate", true); final PlayerID player = getPlayerID(current, "player", false); final String name = current.getAttribute("name"); String displayName = null; final List<Element> propertyElements = getChildren("stepProperty", current); final Properties stepProperties = pareStepProperties(propertyElements); if (current.hasAttribute("display")) { displayName = current.getAttribute("display"); } final GameStep step = new GameStep(name, displayName, player, delegate, data, stepProperties); if (current.hasAttribute("maxRunCount")) { final int runCount = Integer.parseInt(current.getAttribute("maxRunCount")); if (runCount <= 0) { throw new GameParseException(mapName, "maxRunCount must be positive"); } step.setMaxRunCount(runCount); } data.getSequence().addStep(step); } } private Properties pareStepProperties(final List<Element> properties) { final Properties rVal = new Properties(); for (final Element stepProperty : properties) { final String name = stepProperty.getAttribute("name"); final String value = stepProperty.getAttribute("value"); rVal.setProperty(name, value); } return rVal; } private void parseProduction(final Node root) throws GameParseException { parseProductionRules(getChildren("productionRule", root)); parseProductionFrontiers(getChildren("productionFrontier", root)); parsePlayerProduction(getChildren("playerProduction", root)); parseRepairRules(getChildren("repairRule", root)); parseRepairFrontiers(getChildren("repairFrontier", root)); parsePlayerRepair(getChildren("playerRepair", root)); } private void parseTechnology(final Node root) throws GameParseException { parseTechnologies(getSingleChild("technologies", root, true)); parsePlayerTech(getChildren("playerTech", root)); } private void parseProductionRules(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final String name = current.getAttribute("name"); final ProductionRule rule = new ProductionRule(name, data); parseCosts(rule, getChildren("cost", current)); parseResults(rule, getChildren("result", current)); data.getProductionRuleList().addProductionRule(rule); } } private void parseRepairRules(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final String name = current.getAttribute("name"); final RepairRule rule = new RepairRule(name, data); parseRepairCosts(rule, getChildren("cost", current)); parseRepairResults(rule, getChildren("result", current)); data.getRepairRuleList().addRepairRule(rule); } } private void parseCosts(final ProductionRule rule, final List<Element> elements) throws GameParseException { if (elements.size() == 0) { throw new GameParseException(mapName, "no costs for rule:" + rule.getName()); } for (final Element current : elements) { final Resource resource = getResource(current, "resource", true); final int quantity = Integer.parseInt(current.getAttribute("quantity")); rule.addCost(resource, quantity); } } private void parseRepairCosts(final RepairRule rule, final List<Element> elements) throws GameParseException { if (elements.size() == 0) { throw new GameParseException(mapName, "no costs for rule:" + rule.getName()); } for (final Element current : elements) { final Resource resource = getResource(current, "resource", true); final int quantity = Integer.parseInt(current.getAttribute("quantity")); rule.addCost(resource, quantity); } } private void parseResults(final ProductionRule rule, final List<Element> elements) throws GameParseException { if (elements.size() == 0) { throw new GameParseException(mapName, "no results for rule:" + rule.getName()); } for (final Element current : elements) { // must find either a resource or a unit with the given name NamedAttachable result = null; result = getResource(current, "resourceOrUnit", false); if (result == null) { result = getUnitType(current, "resourceOrUnit", false); } if (result == null) { throw new GameParseException( "mapName, Could not find resource or unit" + current.getAttribute("resourceOrUnit")); } final int quantity = Integer.parseInt(current.getAttribute("quantity")); rule.addResult(result, quantity); } } private void parseRepairResults(final RepairRule rule, final List<Element> elements) throws GameParseException { if (elements.size() == 0) { throw new GameParseException(mapName, "no results for rule:" + rule.getName()); } for (final Element current : elements) { // must find either a resource or a unit with the given name NamedAttachable result = null; result = getResource(current, "resourceOrUnit", false); if (result == null) { result = getUnitType(current, "resourceOrUnit", false); } if (result == null) { throw new GameParseException(mapName, "Could not find resource or unit" + current.getAttribute("resourceOrUnit")); } final int quantity = Integer.parseInt(current.getAttribute("quantity")); rule.addResult(result, quantity); } } private void parseProductionFrontiers(final List<Element> elements) throws GameParseException { final ProductionFrontierList frontiers = data.getProductionFrontierList(); for (final Element current : elements) { final String name = current.getAttribute("name"); final ProductionFrontier frontier = new ProductionFrontier(name, data); parseFrontierRules(getChildren("frontierRules", current), frontier); frontiers.addProductionFrontier(frontier); } } private void parseTechnologies(final Node element) { if (element == null) { return; } final TechnologyFrontier allTechs = data.getTechnologyFrontier(); parseTechs(getChildren("techname", element), allTechs); } private void parsePlayerTech(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final PlayerID player = getPlayerID(current, "player", true); final TechnologyFrontierList categories = player.getTechnologyFrontierList(); parseCategories(getChildren("category", current), categories); } } private void parseCategories(final List<Element> elements, final TechnologyFrontierList categories) throws GameParseException { for (final Element current : elements) { final TechnologyFrontier tf = new TechnologyFrontier(current.getAttribute("name"), data); parseCategoryTechs(getChildren("tech", current), tf); categories.addTechnologyFrontier(tf); } } private void parseRepairFrontiers(final List<Element> elements) throws GameParseException { final RepairFrontierList frontiers = data.getRepairFrontierList(); for (final Element current : elements) { final String name = current.getAttribute("name"); final RepairFrontier frontier = new RepairFrontier(name, data); parseRepairFrontierRules(getChildren("repairRules", current), frontier); frontiers.addRepairFrontier(frontier); } } private void parsePlayerProduction(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final PlayerID player = getPlayerID(current, "player", true); final ProductionFrontier frontier = getProductionFrontier(current, "frontier", true); player.setProductionFrontier(frontier); } } private void parsePlayerRepair(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final PlayerID player = getPlayerID(current, "player", true); final RepairFrontier repairFrontier = getRepairFrontier(current, "frontier", true); player.setRepairFrontier(repairFrontier); } } private void parseFrontierRules(final List<Element> elements, final ProductionFrontier frontier) throws GameParseException { final Iterator<Element> iter = elements.iterator(); while (iter.hasNext()) { final ProductionRule rule = getProductionRule(iter.next(), "name", true); frontier.addRule(rule); } } private void parseTechs(final List<Element> elements, final TechnologyFrontier allTechsFrontier) { for (final Element current : elements) { final String name = current.getAttribute("name"); final String tech = current.getAttribute("tech"); TechAdvance ta; if (tech.length() > 0) { ta = new GenericTechAdvance(name, TechAdvance.findDefinedAdvanceAndCreateAdvance(tech, data), data); } else { try { ta = TechAdvance.findDefinedAdvanceAndCreateAdvance(name, data); } catch (final IllegalArgumentException e) { ta = new GenericTechAdvance(name, null, data); } } allTechsFrontier.addAdvance(ta); } } private void parseCategoryTechs(final List<Element> elements, final TechnologyFrontier frontier) throws GameParseException { for (final Element current : elements) { TechAdvance ta = data.getTechnologyFrontier().getAdvanceByProperty(current.getAttribute("name")); if (ta == null) { ta = data.getTechnologyFrontier().getAdvanceByName(current.getAttribute("name")); } if (ta == null) { throw new GameParseException(mapName, "Technology not found :" + current.getAttribute("name")); } frontier.addAdvance(ta); } } private void parseRepairFrontierRules(final List<Element> elements, final RepairFrontier frontier) throws GameParseException { final Iterator<Element> iter = elements.iterator(); while (iter.hasNext()) { final RepairRule rule = getRepairRule(iter.next(), "name", true); frontier.addRule(rule); } } private void parseAttachments(final Element root) throws GameParseException { for (final Element current : getChildren("attachment", root)) { final String className = current.getAttribute("javaClass"); final Attachable attachable = findAttachment(current, current.getAttribute("type")); final String name = current.getAttribute("name"); final List<Element> options = getChildren("option", current); IAttachment attachment = new XmlGameElementMapper().getAttachment(className, name, attachable, data) .orElseThrow( () -> new GameParseException(mapName, "Attachment of type " + className + " could not be instantiated")); attachable.addAttachment(name, attachment); final ArrayList<Tuple<String, String>> attachmentOptionValues = setValues(attachment, options); // keep a list of attachment references in the order they were added data.addToAttachmentOrderAndValues(Tuple.of(attachment, attachmentOptionValues)); } } private Attachable findAttachment(final Element element, final String type) throws GameParseException { Attachable returnVal; final String name = "attachTo"; if (type.equals("unitType")) { returnVal = getUnitType(element, name, true); } else if (type.equals("territory")) { returnVal = getTerritory(element, name, true); } else if (type.equals("resource")) { returnVal = getResource(element, name, true); } else if (type.equals("territoryEffect")) { returnVal = getTerritoryEffect(element, name, true); } else if (type.equals("player")) { returnVal = getPlayerID(element, name, true); } else if (type.equals("relationship")) { returnVal = this.getRelationshipType(element, name, true); } else if (type.equals("technology")) { returnVal = getTechnology(element, name, true); } else { throw new GameParseException(mapName, "Type not found to attach to:" + type); } return returnVal; } private static String capitalizeFirstLetter(final String aString) { char first = aString.charAt(0); first = Character.toUpperCase(first); return first + aString.substring(1); } private ArrayList<Tuple<String, String>> setValues(final IAttachment attachment, final List<Element> values) throws GameParseException { final ArrayList<Tuple<String, String>> options = new ArrayList<>(); for (final Element current : values) { // find the setter String name = null; Method setter = null; try { name = current.getAttribute("name"); if (name.length() == 0) { throw new GameParseException(mapName, "Option name with 0 length"); } setter = attachment.getClass().getMethod("set" + capitalizeFirstLetter(name), SETTER_ARGS); } catch (final NoSuchMethodException nsme) { throw new GameParseException(mapName, "The following option name of " + attachment.getName() + " of class " + attachment.getClass().getName().substring(attachment.getClass().getName().lastIndexOf('.') + 1) + " are either misspelled or exist only in a future version of TripleA. Setter: " + name); } // find the value final String value = current.getAttribute("value"); final String count = current.getAttribute("count"); String itemValues; if (count.length() > 0) { itemValues = count + ":" + value; } else { itemValues = value; } // invoke try { final Object[] args = {itemValues}; setter.invoke(attachment, args); } catch (final IllegalAccessException iae) { throw new GameParseException(mapName, "Setter not public. Setter:" + name + " Class:" + attachment.getClass().getName()); } catch (final InvocationTargetException ite) { ite.getCause().printStackTrace(System.out); throw new GameParseException(mapName, "Error setting property:" + name + " cause:" + ite.getCause().getMessage()); } options.add(Tuple.of(name, itemValues)); } return options; } private void parseInitialization(final Node root) throws GameParseException { // parse territory owners final Node owner = getSingleChild("ownerInitialize", root, true); if (owner != null) { parseOwner(getChildren("territoryOwner", owner)); } // parse initial unit placement final Node unit = getSingleChild("unitInitialize", root, true); if (unit != null) { parseUnitPlacement(getChildren("unitPlacement", unit)); parseHeldUnits(getChildren("heldUnits", unit)); } // parse resources given final Node resource = getSingleChild("resourceInitialize", root, true); if (resource != null) { parseResourceInitialization(getChildren("resourceGiven", resource)); } // parse relationships final Node relationInitialize = getSingleChild("relationshipInitialize", root, true); if (relationInitialize != null) { parseRelationInitialize(getChildren("relationship", relationInitialize)); } } private void parseOwner(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final Territory territory = getTerritory(current, "territory", true); final PlayerID owner = getPlayerID(current, "owner", true); territory.setOwner(owner); // Set the original owner on startup. // TODO Look into this // The addition of this caused the automated tests to fail as TestAttachment can't be cast to TerritoryAttachment // The addition of this IF to pass the tests is wrong, but works until a better solution is found. // Kevin will look into it. if (!territory.getData().getGameName().equals("gameExample") && !territory.getData().getGameName().equals("test")) { // set the original owner final TerritoryAttachment ta = TerritoryAttachment.get(territory); if (ta != null) { // If we already have an original owner set (ie: we set it previously in the attachment using originalOwner or // occupiedTerrOf), // then we DO NOT set the original owner again. // This is how we can have a game start with territories owned by 1 faction but controlled by a 2nd faction. final PlayerID currentOwner = ta.getOriginalOwner(); if (currentOwner == null) { ta.setOriginalOwner(owner); } } } } } private void parseUnitPlacement(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final Territory territory = getTerritory(current, "territory", true); final UnitType type = getUnitType(current, "unitType", true); final String ownerString = current.getAttribute("owner"); final String hitsTakenString = current.getAttribute("hitsTaken"); final String unitDamageString = current.getAttribute("unitDamage"); final PlayerID owner; if (ownerString == null || ownerString.trim().length() == 0) { owner = PlayerID.NULL_PLAYERID; } else { owner = getPlayerID(current, "owner", false); } final int hits; if (hitsTakenString != null && hitsTakenString.trim().length() > 0) { hits = Integer.parseInt(hitsTakenString); if (hits < 0 || hits > UnitAttachment.get(type).getHitPoints() - 1) { throw new GameParseException(mapName, "hitsTaken cannot be less than zero or greater than one less than total hitpPoints"); } } else { hits = 0; } final int unitDamage; if (unitDamageString != null && unitDamageString.trim().length() > 0) { unitDamage = Integer.parseInt(unitDamageString); if (unitDamage < 0) { throw new GameParseException(mapName, "unitDamage cannot be less than zero"); } } else { unitDamage = 0; } final int quantity = Integer.parseInt(current.getAttribute("quantity")); territory.getUnits().addAllUnits(type.create(quantity, owner, false, hits, unitDamage)); } } private void parseHeldUnits(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final PlayerID player = getPlayerID(current, "player", true); final UnitType type = getUnitType(current, "unitType", true); final int quantity = Integer.parseInt(current.getAttribute("quantity")); player.getUnits().addAllUnits(type.create(quantity, player)); } } private void parseResourceInitialization(final List<Element> elements) throws GameParseException { for (final Element current : elements) { final PlayerID player = getPlayerID(current, "player", true); final Resource resource = getResource(current, "resource", true); final int quantity = Integer.parseInt(current.getAttribute("quantity")); player.getResources().addResource(resource, quantity); } } private void checkThatAllUnitsHaveAttachments(final GameData data) throws GameParseException { final Collection<UnitType> errors = new ArrayList<>(); for (final UnitType ut : data.getUnitTypeList().getAllUnitTypes()) { final UnitAttachment ua = UnitAttachment.get(ut); if (ua == null) { errors.add(ut); } } if (!errors.isEmpty()) { throw new GameParseException(mapName, data.getGameName() + " does not have unit attachments for: " + MyFormatter.defaultNamedToTextList(errors)); } } }