/* * RATManager.java * * Copyright (c) 2016 Carl Spain. All rights reserved. * * This file is part of MekHQ. * * MekHQ 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. * * MekHQ 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 MekHQ. If not, see <http://www.gnu.org/licenses/>. */ package mekhq.campaign.universe; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import megamek.client.RandomUnitGenerator; import megamek.common.Compute; import megamek.common.EntityMovementMode; import megamek.common.EntityWeightClass; import megamek.common.MechSummary; import megamek.common.UnitType; import megamek.common.event.Subscribe; import mekhq.MekHQ; import mekhq.campaign.event.OptionsChangedEvent; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Provides a front end to RandomUnitGenerator that allows the user to generate units * based on criteria such as faction, unit type, and weight class. May be restricted to * a certain subset of all available RATs. * * @author Neoancient * */ public class RATManager implements IUnitGenerator { private static final String ALT_FACTION = "data/universe/altfactions.xml"; private static final String RATINFO_DIR = "data/universe/ratdata"; // allRATs.get(collectionName).get(era); eras are sorted from latest to earliest private Map<String,LinkedHashMap<Integer,List<RAT>>> allRATs; private ArrayList<String> selectedCollections; private Map<String,List<String>> altFactions; private static Map<String,List<Integer>> allCollections = null; private static Map<String,String> fileNames = new HashMap<>(); private boolean canIgnoreEra = false; public RATManager() { allRATs = new HashMap<>(); selectedCollections = new ArrayList<>(); loadAltFactions(); MekHQ.registerHandler(this); } @Subscribe public void updateRATconfig(OptionsChangedEvent ev) { canIgnoreEra = ev.getOptions().canIgnoreRatEra(); setSelectedRATs(ev.getOptions().getRATs()); } /** * Replaces selected RAT collections with new list * @param selected List of RAT collection names */ public void setSelectedRATs(List<String> selected) { selectedCollections.clear(); for (String col : selected) { addRAT(col); } } /** * Replaces selected RAT collections with new list * @param selected Array of RAT collection names */ public void setSelectedRATs(String[] selected) { selectedCollections.clear(); for (String col : selected) { addRAT(col); } } /** * Append RAT collection to list of selected RATs * @param collection Name of RAT collection to add */ private void addRAT(String collection) { if (allRATs.containsKey(collection) || loadCollection(collection)) { selectedCollections.add(collection); } } /** * Remove RAT collection from list of selected RATs * @param collection Name of RAT collection to remove */ public void removeRAT(String collection) { selectedCollections.remove(collection); } public void setIgnoreRatEra(boolean ignore) { canIgnoreEra = ignore; } private boolean loadCollection(String name) { if (!fileNames.containsKey(name)) { MekHQ.logError("RAT collection " + name + " not found in " + RATINFO_DIR); return false; } /* Need RUG to be loaded for validation */ while (!RandomUnitGenerator.getInstance().isInitialized()) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } File f = new File(RATINFO_DIR, fileNames.get(name)); FileInputStream fis = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document xmlDoc = null; DocumentBuilder db; try { fis = new FileInputStream(f); db = dbf.newDocumentBuilder(); xmlDoc = db.parse(fis); fis.close(); } catch (Exception ex) { ex.printStackTrace(); MekHQ.logError("While loading RAT info from " + f.getName() + ": " + ex.getMessage()); return false; } Element elem = xmlDoc.getDocumentElement(); NodeList nl = elem.getChildNodes(); elem.normalize(); if (elem.getAttributes().getNamedItem("source") != null) { name = elem.getAttributes().getNamedItem("source").getTextContent(); allRATs.put(name, new LinkedHashMap<Integer,List<RAT>>()); List<Integer> eras = allCollections.get(name); for (int e = eras.size() - 1; e >= 0; e--) { allRATs.get(name).put(eras.get(e), new ArrayList<RAT>()); } for (int i = 0; i < nl.getLength(); i++) { Node eraNode = nl.item(i); if (eraNode.getNodeName().equalsIgnoreCase("era")) { String year = eraNode.getAttributes().getNamedItem("year").getTextContent(); if (year != null) { try { int era = Integer.parseInt(year); allRATs.get(name).put(era, new ArrayList<RAT>()); parseEraNode(eraNode, name, era); } catch (NumberFormatException ex) { MekHQ.logError("Could not parse year " + year + " in " + name); } } else { MekHQ.logError("year attribute not found for era in RAT collection " + name); } } } return allRATs.get(name).size() > 0; } else { MekHQ.logError("source attribute not found for RAT data in " + f.getName()); return false; } } private void parseEraNode(Node eraNode, String name, int era) { Set<String> allRatNames = RandomUnitGenerator.getInstance().getRatMap().keySet(); NodeList nl = eraNode.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node ratNode = nl.item(i); if (ratNode.getNodeName().equals("rat")) { RAT rat = RAT.createFromXml(ratNode); if (rat != null && allRatNames.contains(rat.ratName)) { allRATs.get(name).get(era).add(rat); } } } } /** * Loads a list of alternate factions to check when the desired one cannot be found in a given * RAT before checking generic ones. */ private void loadAltFactions() { altFactions = new HashMap<>(); File f = new File(ALT_FACTION); FileInputStream fis = null; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document xmlDoc = null; DocumentBuilder db; try { fis = new FileInputStream(f); db = dbf.newDocumentBuilder(); xmlDoc = db.parse(fis); fis.close(); } catch (Exception ex) { ex.printStackTrace(); MekHQ.logError("While loading altFactions: " + ex.getMessage()); } Element elem = xmlDoc.getDocumentElement(); NodeList nl = elem.getChildNodes(); elem.normalize(); for (int i = 0; i < nl.getLength(); i++) { Node node = nl.item(i); if (node.getNodeName().equals("faction") && node.getAttributes().getNamedItem("key") != null) { String key = node.getAttributes().getNamedItem("key").getTextContent(); altFactions.putIfAbsent(key, new ArrayList<String>()); for (String alt : node.getTextContent().split(",")) { altFactions.get(key).add(alt); } } } } /** * * @return A map of all collections available with a list of eras included */ public static Map<String,List<Integer>> getAllRATCollections() { if (allCollections == null) { populateCollectionNames(); } return allCollections; } /** * Scans ratdata directory for list of available RATs that can be used by CampaignOptions * to provide a list. */ public static void populateCollectionNames() { allCollections = new HashMap<>(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document xmlDoc = null; DocumentBuilder db; File dir = new File(RATINFO_DIR); FileInputStream fis = null; if (!dir.isDirectory()) { MekHQ.logError("Ratinfo directory not found"); return; } for (File f : dir.listFiles()) { if (f.getName().endsWith(".xml")) { try { fis = new FileInputStream(f); db = dbf.newDocumentBuilder(); xmlDoc = db.parse(fis); fis.close(); } catch (Exception ex) { ex.printStackTrace(); MekHQ.logError("While loading RAT info from " + f.getName() + ": " + ex.getMessage()); } Element elem = xmlDoc.getDocumentElement(); NodeList nl = elem.getChildNodes(); elem.normalize(); String name = null; ArrayList<Integer> eras = new ArrayList<>(); if (elem.getAttributes().getNamedItem("source") != null) { name = elem.getAttributes().getNamedItem("source").getTextContent(); for (int j = 0; j < nl.getLength(); j++) { Node eraNode = nl.item(j); if (eraNode.getNodeName().equalsIgnoreCase("era")) { String year = eraNode.getAttributes().getNamedItem("year").getTextContent(); if (year != null) { try { eras.add(Integer.parseInt(year)); } catch (NumberFormatException ex) { MekHQ.logError("Could not parse year " + year + " in " + f.getName()); } } else { MekHQ.logError("year attribute not found for era in " + f.getName()); } } } fileNames.put(name, f.getName()); Collections.sort(eras); allCollections.put(name, eras); } else { MekHQ.logError("source attribute not found for RAT data in " + f.getName()); } } } } private RAT findRAT(String faction, int unitType, int weightClass, int year, int quality) { List<String> factionList = factionTree(faction); for (String collectionName : selectedCollections) { Map<Integer,List<RAT>> collection = allRATs.get(collectionName); if (collection == null) { continue; } for (int era : collection.keySet()) { if (era > year) { continue; } for (String f : factionList) { Optional<RAT> match = collection.get(era).stream() .filter(rat -> rat.matches(f, unitType, weightClass, quality)) .findFirst(); if (match.isPresent()) { return match.get(); } } } } if (canIgnoreEra) { for (String collectionName : selectedCollections) { Map<Integer,List<RAT>> collection = allRATs.get(collectionName); if (collection == null) { continue; } List<Integer> eras = new ArrayList<>(collection.keySet()); Collections.reverse(eras); for (int era : eras) { for (String f : factionList) { Optional<RAT> match = collection.get(era).stream() .filter(rat -> rat.matches(f, unitType, weightClass, quality)) .findFirst(); if (match.isPresent()) { return match.get(); } } } } } return null; } private List<String> factionTree(String faction) { List<String> retVal = new ArrayList<>(); retVal.add(faction); if (faction.contains(".")) { faction = faction.split("\\.")[0]; retVal.add(faction); } if (altFactions.containsKey(faction)) { List<String> alts = new ArrayList<>(); alts.addAll(altFactions.get(faction)); while (alts.size() > 0) { int index = Compute.randomInt(alts.size()); retVal.add(alts.get(index)); alts.remove(index); } } Faction f = Faction.getFaction(faction); if (f.isPeriphery()) { retVal.add("Periphery"); } retVal.add(f.isClan()? "Clan" : "General"); return retVal; } /* (non-Javadoc) * @see mekhq.campaign.universe.IUnitGenerator#isSupportedUnitType(int) */ @Override public boolean isSupportedUnitType(int unitType) { return unitType == UnitType.MEK || unitType == UnitType.TANK || unitType == UnitType.AERO || unitType == UnitType.DROPSHIP || unitType == UnitType.INFANTRY || unitType == UnitType.BATTLE_ARMOR || unitType == UnitType.PROTOMEK; } /* (non-Javadoc) * @see mekhq.campaign.universe.IUnitGenerator#generate(java.lang.String, int, int, int, int) */ @Override public MechSummary generate(String faction, int unitType, int weightClass, int year, int quality) { return generate(faction, unitType, weightClass, year, quality, null); } /* (non-Javadoc) * @see mekhq.campaign.universe.IUnitGenerator#generate(java.lang.String, int, int, int, int, java.util.function.Predicate) */ @Override public MechSummary generate(String faction, int unitType, int weightClass, int year, int quality, Predicate<MechSummary> filter) { List<MechSummary> list = generate(1, faction, unitType, weightClass, year, quality, filter); if (list.size() > 0) { return list.get(0); } return null; } @Override public MechSummary generate(String faction, int unitType, int weightClass, int year, int quality, Collection<EntityMovementMode> movementModes, Predicate<MechSummary> filter) { List<MechSummary> list = generate(1, faction, unitType, weightClass, year, quality, movementModes, filter); if (list.size() > 0) { return list.get(0); } return null; } /* (non-Javadoc) * @see mekhq.campaign.universe.IUnitGenerator#generate(int, java.lang.String, int, int, int, int) */ @Override public List<MechSummary> generate(int count, String faction, int unitType, int weightClass, int year, int quality) { return generate(count, faction, unitType, weightClass, year, quality, null); } /* (non-Javadoc) * @see mekhq.campaign.universe.IUnitGenerator#generate(int, java.lang.String, int, int, int, int, java.util.function.Predicate) */ @Override public List<MechSummary> generate(int count, String faction, int unitType, int weightClass, int year, int quality, Predicate<MechSummary> filter) { RAT rat = findRAT(faction, unitType, weightClass, year, quality); if (rat != null) { if (unitType == UnitType.TANK) { filter = filter.and(ms -> ms.getUnitType().equals("Tank")); } else if (unitType == UnitType.VTOL) { filter = filter.and(ms -> ms.getUnitType().equals("VTOL")); } return RandomUnitGenerator.getInstance().generate(count, rat.ratName, filter); } return new ArrayList<MechSummary>(); } @Override public List<MechSummary> generate(int count, String faction, int unitType, int weightClass, int year, int quality, Collection<EntityMovementMode> movementModes, Predicate<MechSummary> filter) { RAT rat = findRAT(faction, unitType, weightClass, year, quality); if (rat != null) { if (!movementModes.isEmpty()) { Predicate<MechSummary> moveFilter = ms -> movementModes.contains(EntityMovementMode.getMode(ms.getUnitSubType())); if (filter == null) { filter = moveFilter; } else { filter = filter.and(moveFilter); } } return RandomUnitGenerator.getInstance().generate(count, rat.ratName, filter); } return new ArrayList<MechSummary>(); } private static class RAT { String ratName = null; HashSet<String> factions = new HashSet<>(); HashSet<Integer> unitTypes = new HashSet<>(); HashSet<Integer> weightClasses = new HashSet<>(); HashSet<Integer> ratings = new HashSet<>(); public boolean matches(String faction, int unitType, int weightClass, int quality) { return (factions.contains(faction) || factions.isEmpty()) && (unitTypes.contains(unitType) || unitTypes.isEmpty()) && (weightClasses.contains(weightClass) || weightClasses.isEmpty() || weightClass < 0) && (ratings.contains(quality) || ratings.isEmpty()); } public static RAT createFromXml(Node node) { RAT retVal = new RAT(); if (node.getAttributes().getNamedItem("name") == null) { MekHQ.logError("name attribute missing"); return null; } retVal.ratName = node.getAttributes().getNamedItem("name").getTextContent(); NodeList nl = node.getChildNodes(); for (int i = 0; i < nl.getLength(); i++) { Node wn = nl.item(i); switch (wn.getNodeName()) { case "factions": if (wn.getTextContent().length() > 0) { retVal.factions.addAll(Arrays.asList(wn.getTextContent().split(","))); } break; case "unitTypes": for (String ut : wn.getTextContent().split(",")) { switch(ut) { case "Mek": retVal.unitTypes.add(UnitType.MEK); break; case "Tank": retVal.unitTypes.add(UnitType.TANK); break; case "BattleArmor": retVal.unitTypes.add(UnitType.BATTLE_ARMOR); break; case "Infantry": retVal.unitTypes.add(UnitType.INFANTRY); break; case "ProtoMek": retVal.unitTypes.add(UnitType.PROTOMEK); break; case "VTOL": retVal.unitTypes.add(UnitType.VTOL); break; case "Naval": retVal.unitTypes.add(UnitType.NAVAL); break; case "Gun Emplacement": retVal.unitTypes.add(UnitType.GUN_EMPLACEMENT); break; case "Conventional Fighter": retVal.unitTypes.add(UnitType.CONV_FIGHTER); break; case "Aero": retVal.unitTypes.add(UnitType.AERO); break; case "Small Craft": retVal.unitTypes.add(UnitType.SMALL_CRAFT); break; case "Dropship": retVal.unitTypes.add(UnitType.DROPSHIP); break; case "Jumpship": retVal.unitTypes.add(UnitType.JUMPSHIP); break; case "Warship": retVal.unitTypes.add(UnitType.WARSHIP); break; case "Space Station": retVal.unitTypes.add(UnitType.SPACE_STATION); break; } } break; case "weightClasses": for (String wc : wn.getTextContent().split(",")) { switch(wc) { case "UL": retVal.weightClasses.add(EntityWeightClass.WEIGHT_ULTRA_LIGHT); break; case "L": retVal.weightClasses.add(EntityWeightClass.WEIGHT_LIGHT); break; case "M": retVal.weightClasses.add(EntityWeightClass.WEIGHT_MEDIUM); break; case "H": retVal.weightClasses.add(EntityWeightClass.WEIGHT_HEAVY); break; case "A": retVal.weightClasses.add(EntityWeightClass.WEIGHT_ASSAULT); break; case "SH": case "C": retVal.weightClasses.add(EntityWeightClass.WEIGHT_SUPER_HEAVY); break; } } break; case "ratings": for (String r : wn.getTextContent().split(",")) { switch(r) { case "A": case "Keshik": case "K": retVal.ratings.add(4); break; case "B": case "FL": retVal.ratings.add(3); break; case "C": case "SL": case "2L": retVal.ratings.add(2); break; case "D": case "Sol": case "Solahma": retVal.ratings.add(1); break; case "F": case "PG": case "PGC": retVal.ratings.add(0); break; } } break; } } return retVal; } } }