mekhq.Utilities.java Source code

Java tutorial

Introduction

Here is the source code for mekhq.Utilities.java

Source

/*
 * Utilities.java
 *
 * Copyright (c) 2009 Jay Lawson <jaylawson39 at yahoo.com>. 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;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.Vector;
import java.util.stream.Collectors;

import javax.swing.JTable;
import javax.swing.table.TableModel;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDateTime;

import megamek.common.Aero;
import megamek.common.AmmoType;
import megamek.common.BattleArmor;
import megamek.common.Compute;
import megamek.common.ConvFighter;
import megamek.common.Coords;
import megamek.common.Crew;
import megamek.common.CriticalSlot;
import megamek.common.Entity;
import megamek.common.EquipmentType;
import megamek.common.ITechnology;
import megamek.common.Infantry;
import megamek.common.Jumpship;
import megamek.common.LandAirMech;
import megamek.common.Mech;
import megamek.common.MechSummary;
import megamek.common.MechSummaryCache;
import megamek.common.Mounted;
import megamek.common.Protomech;
import megamek.common.SmallCraft;
import megamek.common.Tank;
import megamek.common.TechConstants;
import megamek.common.UnitType;
import megamek.common.VTOL;
import megamek.common.logging.LogLevel;
import megamek.common.options.IOption;
import megamek.common.options.OptionsConstants;
import megamek.common.util.EncodeControl;
import mekhq.campaign.Campaign;
import mekhq.campaign.CampaignOptions;
import mekhq.campaign.parts.Part;
import mekhq.campaign.parts.equipment.AmmoBin;
import mekhq.campaign.parts.equipment.BattleArmorEquipmentPart;
import mekhq.campaign.parts.equipment.EquipmentPart;
import mekhq.campaign.parts.equipment.MissingEquipmentPart;
import mekhq.campaign.personnel.Person;
import mekhq.campaign.personnel.SkillType;
import mekhq.campaign.unit.CrewType;
import mekhq.campaign.unit.Unit;
import mekhq.campaign.unit.UnitTechProgression;

/**
 *
 * @author Jay Lawson <jaylawson39 at yahoo.com>
 */
public class Utilities {
    private static final int MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24;

    private static ResourceBundle resourceMap = ResourceBundle.getBundle("mekhq.resources.Utilities", //$NON-NLS-1$
            new EncodeControl());

    // A couple of arrays for use in the getLevelName() method
    private static int[] arabicNumbers = { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1 };
    private static String[] romanNumerals = "M,CM,D,CD,C,XC,L,XL,X,IX,V,IV,I".split(","); //$NON-NLS-1$ //$NON-NLS-2$

    public static int roll3d6() {
        Vector<Integer> rolls = new Vector<Integer>();
        rolls.add(Compute.d6());
        rolls.add(Compute.d6());
        rolls.add(Compute.d6());
        Collections.sort(rolls);
        return (rolls.elementAt(0) + rolls.elementAt(1));
    }

    /*
     * Roll a certain number of dice with a certain number of faces
     */
    public static int dice(int num, int faces) {
        int result = 0;

        // Roll however many dice as necessary
        for (int i = 0; i < num; i++) {
            result += Compute.randomInt(faces) + 1;
        }

        return result;
    }

    /**
     * Get a random element out of a collection, with equal probability.
     * <p>
     * This is the same as calling the following code, only plays nicely with
     * all collections (including ones like Set which don't implement RandomAccess)
     * and deals gracefully with empty collections.
     * <pre>
     * collection.get(Compute.randomInt(collection.size());
     * </pre>
     * 
     * @return <i>null</i> if the collection itself is null or empty;
     * can return <i>null</i> if the collection contains <i>null</i> items.
     * 
     */
    public static <T> T getRandomItem(Collection<? extends T> collection) {
        if ((null == collection) || collection.isEmpty()) {
            return null;
        }
        int index = Compute.randomInt(collection.size());
        Iterator<? extends T> iterator = collection.iterator();
        for (int i = 0; i < index; ++i) {
            iterator.next();
        }
        return iterator.next();
    }

    /**
     * Get a random element out of a list, with equal probability.
     * <p>
     * This is the same as calling the following code,
     * only deals gracefully with empty lists.
     * <pre>
     * list.get(Compute.randomInt(list.size());
     * </pre>
     * 
     * @return <i>null</i> if the list itself is null or empty;
     * can return <i>null</i> if the list contains <i>null</i> items.
     * 
     */
    public static <T> T getRandomItem(List<? extends T> list) {
        if ((null == list) || list.isEmpty()) {
            return null;
        }
        int index = Compute.randomInt(list.size());
        return list.get(index);
    }

    /**
     * @return linear interpolation value between min and max
     */
    public static double lerp(double min, double max, double f) {
        // The order of operations is important here, to not lose precision
        return min * (1.0 - f) + max * f;
    }

    /**
     * @return linear interpolation value between min and max, rounded to the nearest integer
     */
    public static int lerp(int min, int max, double f) {
        // The order of operations is important here, to not lose precision
        return (int) Math.round(min * (1.0 - f) + max * f);
    }

    /**
     * @return linear interpolation value between min and max, rounded to the nearest coordinate
     * <p>
     * For theory behind the method used, see: http://www.redblobgames.com/grids/hexagons/
     */
    public static Coords lerp(Coords min, Coords max, double f) {
        int minX = min.getX();
        int minZ = min.getY() - (min.getX() - (min.getX() & 1)) / 2;
        int minY = -minX - minZ;
        int maxX = max.getX();
        int maxZ = max.getY() - (max.getX() - (max.getX() & 1)) / 2;
        int maxY = -maxX - maxZ;
        double lerpX = lerp((double) minX, (double) maxX, f);
        double lerpY = lerp((double) minY, (double) maxY, f);
        double lerpZ = lerp((double) minZ, (double) maxZ, f);
        int resultX = (int) Math.round(lerpX);
        int resultY = (int) Math.round(lerpY);
        int resultZ = (int) Math.round(lerpZ);
        double diffX = Math.abs(resultX * 1.0 - lerpX);
        double diffY = Math.abs(resultY * 1.0 - lerpY);
        double diffZ = Math.abs(resultZ * 1.0 - lerpZ);
        if ((diffX > diffY) && (diffX > diffZ)) {
            resultX = -resultY - resultZ;
        } else if (diffY > diffZ) {
            resultY = -resultX - resultZ;
        } else {
            resultZ = -resultX - resultY;
        }
        return new Coords(resultX, resultZ + (resultX - (resultX & 1)) / 2);
    }

    /**
     * The method is returns the same as a call to the following code:
     * <pre>T result = (null != getFirst()) ? getFirst() : getSecond();</pre>
     * ... with the major difference that getFirst() and getSecond() get evaluated exactly once.
     * <p>
     * This means that it doesn't matter if getFirst() is relatively expensive to evaluate
     * or has side effects. It also means that getSecond() gets evaluated <i>regardless</i> if
     * it is needed or not. Since Java guarantees the order of evaluation for arguments to be
     * the same as the order in which they appear (JSR 15.7.4), this makes it more suitable
     * for re-playable procedural generation and similar method calls with side effects.
     * 
     * @return the first argument if it's not <i>null</i>, else the second argument
     */
    public static <T> T nonNull(T first, T second) {
        return (null != first) ? first : second;
    }

    /**
     * For details and caveats, see the two-argument method.
     * 
     * @return the first non-<i>null</i> argument, else <i>null</i> if all are <i>null</i>
     */
    @SafeVarargs
    public static <T> T nonNull(T first, T second, T... others) {
        if (null != first) {
            return first;
        }
        if (null != second) {
            return second;
        }
        T result = others[0];
        int index = 1;
        while ((null == result) && (index < others.length)) {
            result = others[index];
            ++index;
        }
        return result;
    }

    public static ArrayList<AmmoType> getMunitionsFor(Entity entity, AmmoType cur_atype, int techLvl) {
        ArrayList<AmmoType> atypes = new ArrayList<AmmoType>();
        for (AmmoType atype : AmmoType.getMunitionsFor(cur_atype.getAmmoType())) {
            //this is an abbreviated version of setupMunitions in the CustomMechDialog
            //TODO: clan/IS limitations?

            if ((entity instanceof Aero) && !atype.canAeroUse()) {
                continue;
            }

            int lvl = atype.getTechLevel(entity.getTechLevelYear());
            if (lvl < 0) {
                lvl = 0;
            }
            if (techLvl < Utilities.getSimpleTechLevel(lvl)) {
                continue;
            }
            if (TechConstants.isClan(cur_atype.getTechLevel(entity.getTechLevelYear())) != TechConstants
                    .isClan(lvl)) {
                continue;
            }

            // Only Protos can use Proto-specific ammo
            if (atype.hasFlag(AmmoType.F_PROTOMECH) && !(entity instanceof Protomech)) {
                continue;
            }

            // When dealing with machine guns, Protos can only
            // use proto-specific machine gun ammo
            if ((entity instanceof Protomech) && atype.hasFlag(AmmoType.F_MG)
                    && !atype.hasFlag(AmmoType.F_PROTOMECH)) {
                continue;
            }

            if (atype.hasFlag(AmmoType.F_NUCLEAR) && atype.hasFlag(AmmoType.F_CAP_MISSILE)
                    && !entity.getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_AT2_NUKES)) {
                continue;
            }

            // Battle Armor ammo can't be selected at all.
            // All other ammo types need to match on rack size and tech.
            if ((atype.getRackSize() == cur_atype.getRackSize())
                    && (atype.hasFlag(AmmoType.F_BATTLEARMOR) == cur_atype.hasFlag(AmmoType.F_BATTLEARMOR))
                    && (atype.hasFlag(AmmoType.F_ENCUMBERING) == cur_atype.hasFlag(AmmoType.F_ENCUMBERING))
                    && ((atype.getTonnage(entity) == cur_atype.getTonnage(entity))
                            || atype.hasFlag(AmmoType.F_CAP_MISSILE))) {
                atypes.add(atype);
            }
        }
        return atypes;
    }

    public static boolean compareMounted(Mounted a, Mounted b) {
        if (!a.getType().equals(b.getType()))
            return false;
        if (!a.getClass().equals(b.getClass()))
            return false;
        if (!a.getName().equals(b.getName()))
            return false;
        if (a.getLocation() != b.getLocation())
            return false;
        return true;
    }

    public static String getCurrencyString(long value) {
        NumberFormat numberFormat = DecimalFormat.getIntegerInstance();
        String text = numberFormat.format(value) + " C-Bills";
        return text;
    }

    /**
     * Returns the last file modified in a directory and all subdirectories
     * that conforms to a FilenameFilter
     * @param dir
     * @param filter
     * @return
     */
    public static File lastFileModified(String dir, FilenameFilter filter) {
        File fl = new File(dir);
        File[] files = fl.listFiles(filter);
        long lastMod = Long.MIN_VALUE;
        File choice = null;
        for (File file : files) {
            if (file.lastModified() > lastMod) {
                choice = file;
                lastMod = file.lastModified();
            }
        }
        //ok now we need to recursively search any subdirectories, so see if they
        //contain more recent files
        files = fl.listFiles();
        for (File file : files) {
            if (!file.isDirectory()) {
                continue;
            }
            File subFile = lastFileModified(file.getPath(), filter);
            if (null != subFile && subFile.lastModified() > lastMod) {
                choice = subFile;
                lastMod = subFile.lastModified();
            }
        }
        return choice;
    }

    public static File[] getAllFiles(String dir, FilenameFilter filter) {
        File fl = new File(dir);
        File[] files = fl.listFiles(filter);
        return files;
    }

    public static ArrayList<String> getAllVariants(Entity en, Campaign campaign) {
        final String METHOD_NAME = "getAllVariants(Entity, Campaign)"; // $NON-NLS-1$
        CampaignOptions options = campaign.getCampaignOptions();
        ArrayList<String> variants = new ArrayList<String>();
        for (MechSummary summary : MechSummaryCache.getInstance().getAllMechs()) {
            // If this isn't the same chassis, is our current unit, or is a different weight we continue
            if (!en.getChassis().equalsIgnoreCase(summary.getChassis())
                    || en.getModel().equalsIgnoreCase(summary.getModel()) || summary.getTons() != en.getWeight()
                    || !summary.getUnitType().equals(UnitType.determineUnitType(en))) {
                continue;
            }
            // If we only allow canon units and this isn't canon we continue
            if (!summary.isCanon() && options.allowCanonRefitOnly()) {
                continue;
            }
            // If the unit doesn't meet the tech filter criteria we continue
            ITechnology techProg = UnitTechProgression.getProgression(summary, campaign.getTechFaction(), true);
            if (null == techProg) {
                // This should never happen unless there was an exception thrown when calculating the progression.
                // In such a case we will log it and take the least restrictive action, which is to let it through.
                MekHQ.getLogger().log(Utilities.class, METHOD_NAME, LogLevel.WARNING,
                        "Could not determine tech progression for " + summary.getName() // $NON-NLS-1$
                                + ", including among available refits."); // $NON-NLS-1$
            } else if (!campaign.isLegal(techProg)) {
                continue;
            }
            // Otherwise, we can offer it for selection
            variants.add(summary.getModel());
        }
        return variants;
    }

    public static boolean isOmniVariant(Entity entity1, Entity entity2) {
        if (!entity1.isOmni() || !entity2.isOmni()) {
            return false;
        }
        if (entity1.getWeight() != entity2.getWeight()) {
            return false;
        }
        if (entity1.getClass() != entity2.getClass()) {
            return false;
        }
        if (entity1.getEngine().getRating() != entity2.getEngine().getRating()
                || entity1.getEngine().getEngineType() != entity2.getEngine().getEngineType()
                || entity1.getEngine().getFlags() != entity2.getEngine().getFlags()) {
            return false;
        }
        if (entity1.getStructureType() != entity2.getStructureType()) {
            return false;
        }
        if (entity1 instanceof Mech) {
            if (((Mech) entity1).getCockpitType() != ((Mech) entity2).getCockpitType()) {
                return false;
            }
            if (((Mech) entity1).getGyroType() != ((Mech) entity2).getGyroType()) {
                return false;
            }
        }
        if (entity1 instanceof Aero) {
            if (((Aero) entity1).getCockpitType() != ((Aero) entity2).getCockpitType()) {
                return false;
            }
        }
        if (entity1 instanceof Tank) {
            if (entity1.getMovementMode() != entity2.getMovementMode()) {
                return false;
            }
        }
        List<EquipmentType> fixedEquipment = new ArrayList<>();
        for (int loc = 0; loc < entity1.locations(); loc++) {
            if (entity1.getArmorType(loc) != entity2.getArmorType(loc)
                    || entity1.getOArmor(loc) != entity2.getOArmor(loc)) {
                return false;
            }
            fixedEquipment.clear();
            //Go through the base entity and make a list of all fixed equipment in this location.
            for (int slot = 0; slot < entity1.getNumberOfCriticals(loc); slot++) {
                CriticalSlot crit = entity1.getCritical(loc, slot);
                if (null != crit && crit.getType() == CriticalSlot.TYPE_EQUIPMENT && null != crit.getMount()) {
                    if (!crit.getMount().isOmniPodMounted()) {
                        fixedEquipment.add(crit.getMount().getType());
                        if (null != crit.getMount2()) {
                            fixedEquipment.add(crit.getMount2().getType());
                        }
                    }
                }
            }
            //Go through the critical slots in this location for the second entity and remove all fixed
            //equipment from the list. If not found or something is left over, there is a fixed equipment difference.
            for (int slot = 0; slot < entity2.getNumberOfCriticals(loc); slot++) {
                CriticalSlot crit = entity1.getCritical(loc, slot);
                if (null != crit && crit.getType() == CriticalSlot.TYPE_EQUIPMENT && null != crit.getMount()) {
                    if (!crit.getMount().isOmniPodMounted()) {
                        if (!fixedEquipment.remove(crit.getMount().getType())) {
                            return false;
                        }
                        if (null != crit.getMount2() && !fixedEquipment.remove(crit.getMount2().getType())) {
                            return false;
                        }
                    }
                }
            }
            if (!fixedEquipment.isEmpty()) {
                return false;
            }
        }
        return true;
    }

    public static int generateExpLevel(int bonus) {
        int roll = Compute.d6(2) + bonus;
        if (roll < 2) {
            return SkillType.EXP_ULTRA_GREEN;
        }
        if (roll < 6) {
            return SkillType.EXP_GREEN;
        } else if (roll < 10) {
            return SkillType.EXP_REGULAR;
        } else if (roll < 12) {
            return SkillType.EXP_VETERAN;
        } else {
            return SkillType.EXP_ELITE;
        }
    }

    public static Person findCommander(Entity entity, ArrayList<Person> vesselCrew, ArrayList<Person> gunners,
            ArrayList<Person> drivers, Person navigator) {
        //take first by rank
        //if rank is tied, take gunners over drivers
        //if two of the same type are tie rank, take the first one
        int bestRank = -1;
        Person commander = null;
        for (Person p : vesselCrew) {
            if (null != p && p.getRankNumeric() > bestRank) {
                commander = p;
                bestRank = p.getRankNumeric();
            }
        }
        for (Person p : gunners) {
            if (p.getRankNumeric() > bestRank) {
                commander = p;
                bestRank = p.getRankNumeric();
            }
        }
        for (Person p : drivers) {
            if (null != p && p.getRankNumeric() > bestRank) {
                commander = p;
                bestRank = p.getRankNumeric();
            }
        }
        if (navigator != null) {
            if (null != navigator && navigator.getRankNumeric() > bestRank) {
                commander = navigator;
                bestRank = navigator.getRankNumeric();
            }
        }
        return commander;
    }

    /*
     * Simple utility function to take a specified number and randomize it a little bit
     * roll 1d6 results in:
     * 1: target - 2
     * 2: target - 1
     * 3 & 4: target
     * 5: target + 1
     * 6: target + 2
     */
    public static int randomSkillFromTarget(int target) {
        int dice = Compute.d6();
        if (dice == 1) {
            target -= 2;
        } else if (dice == 2) {
            target -= 1;
        } else if (dice == 5) {
            target += 1;
        } else if (dice == 6) {
            target += 2;
        }
        return Math.max(target, 0);
    }

    /*
     * If an infantry platoon or vehicle crew took damage, perform the personnel injuries
     */
    public static ArrayList<Person> doCrewInjuries(Entity e, Campaign c, ArrayList<Person> newCrew) {
        int casualties = 0;
        if (null != e && e instanceof Infantry) {
            e.applyDamage();
            casualties = newCrew.size() - ((Infantry) e).getShootingStrength();
            for (Person p : newCrew) {
                for (int i = 0; i < casualties; i++) {
                    if (Compute.d6(2) >= 7) {
                        int hits = c.getCampaignOptions().getMinimumHitsForVees();
                        if (c.getCampaignOptions().useAdvancedMedical()
                                || c.getCampaignOptions().useRandomHitsForVees()) {
                            int range = 6 - hits;
                            hits = hits + Compute.randomInt(range);
                        }
                        p.setHits(hits);
                    } else {
                        p.setHits(6);
                    }
                }
            }
        }

        return newCrew;
    }

    public static boolean isDeadCrew(Entity e) {
        if (Compute.getFullCrewSize(e) == 0 || e.getCrew().isDead()) {
            return true;
        }

        return false;
    }

    public static Map<CrewType, Collection<Person>> genRandomCrewWithCombinedSkill(Campaign c, Unit u,
            String factionCode) {
        Objects.requireNonNull(c);
        Objects.requireNonNull(u);
        Objects.requireNonNull(u.getEntity(), "Unit needs to have a valid Entity attached");
        Crew oldCrew = u.getEntity().getCrew();
        String commanderName = oldCrew.getName();
        int averageGunnery = 0;
        int averagePiloting = 0;
        List<Person> drivers = new ArrayList<Person>();
        List<Person> gunners = new ArrayList<Person>();
        List<Person> vesselCrew = new ArrayList<Person>();
        Person navigator = null;
        Person consoleCmdr = null;
        int totalGunnery = 0;
        int totalPiloting = 0;
        drivers.clear();
        gunners.clear();
        vesselCrew.clear();
        navigator = null;

        // If the entire crew is dead, we don't want to generate them.
        // Actually, we do because they might not be truly dead - this will be the case for BA for example
        // Also, the user may choose to GM make them un-dead in the resolve scenario dialog. I am disabling
        // this because it is causing problems for BA.
        /*if (isDeadCrew(unit.getEntity())) {
        return new ArrayList<Person>();
        }*/

        // Generate solo crews
        if (u.usesSoloPilot()) {
            Person p = null;
            if (u.getEntity() instanceof LandAirMech) {
                p = c.newPerson(Person.T_MECHWARRIOR, factionCode);
                p.addSkill(SkillType.S_PILOT_MECH,
                        SkillType.getType(SkillType.S_PILOT_MECH).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_MECH,
                        SkillType.getType(SkillType.S_GUN_MECH).getTarget() - oldCrew.getGunnery(), 0);
                p.addSkill(SkillType.S_PILOT_AERO,
                        SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_AERO,
                        SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(), 0);
                p.setSecondaryRole(Person.T_AERO_PILOT);
            } else if (u.getEntity() instanceof Mech) {
                p = c.newPerson(Person.T_MECHWARRIOR, factionCode);
                p.addSkill(SkillType.S_PILOT_MECH,
                        SkillType.getType(SkillType.S_PILOT_MECH).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_MECH,
                        SkillType.getType(SkillType.S_GUN_MECH).getTarget() - oldCrew.getGunnery(), 0);
            } else if (u.getEntity() instanceof Aero) {
                p = c.newPerson(Person.T_AERO_PILOT, factionCode);
                p.addSkill(SkillType.S_PILOT_AERO,
                        SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_AERO,
                        SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(), 0);
            } else if (u.getEntity() instanceof ConvFighter) {
                p = c.newPerson(Person.T_CONV_PILOT, factionCode);
                p.addSkill(SkillType.S_PILOT_JET,
                        SkillType.getType(SkillType.S_PILOT_JET).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_JET,
                        SkillType.getType(SkillType.S_GUN_JET).getTarget() - oldCrew.getPiloting(), 0);
            } else if (u.getEntity() instanceof Protomech) {
                p = c.newPerson(Person.T_PROTO_PILOT, factionCode);
                //p.addSkill(SkillType.S_PILOT_PROTO, SkillType.getType(SkillType.S_PILOT_PROTO).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_PROTO,
                        SkillType.getType(SkillType.S_GUN_PROTO).getTarget() - oldCrew.getGunnery(), 0);
            } else if (u.getEntity() instanceof VTOL) {
                p = c.newPerson(Person.T_VTOL_PILOT, factionCode);
                p.addSkill(SkillType.S_PILOT_VTOL,
                        SkillType.getType(SkillType.S_PILOT_VTOL).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_VEE,
                        SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(), 0);
            } else {
                //assume tanker if we got here
                p = c.newPerson(Person.T_GVEE_DRIVER, factionCode);
                p.addSkill(SkillType.S_PILOT_GVEE,
                        SkillType.getType(SkillType.S_PILOT_GVEE).getTarget() - oldCrew.getPiloting(), 0);
                p.addSkill(SkillType.S_GUN_VEE,
                        SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(), 0);
            }

            populateOptionsFromCrew(p, oldCrew);

            drivers.add(p);
        } else if (oldCrew.getSlotCount() > 1) {
            for (int slot = 0; slot < oldCrew.getSlotCount(); slot++) {
                Person p = null;
                if (u.getEntity() instanceof Mech) {
                    p = c.newPerson(Person.T_MECHWARRIOR, factionCode);
                    p.addSkill(SkillType.S_PILOT_MECH,
                            SkillType.getType(SkillType.S_PILOT_MECH).getTarget() - oldCrew.getPiloting(slot), 0);
                    p.addSkill(SkillType.S_GUN_MECH,
                            SkillType.getType(SkillType.S_GUN_MECH).getTarget() - oldCrew.getGunnery(slot), 0);
                } else if (u.getEntity() instanceof Aero) {
                    p = c.newPerson(Person.T_AERO_PILOT, factionCode);
                    p.addSkill(SkillType.S_PILOT_AERO,
                            SkillType.getType(SkillType.S_PILOT_AERO).getTarget() - oldCrew.getPiloting(slot), 0);
                    p.addSkill(SkillType.S_GUN_AERO,
                            SkillType.getType(SkillType.S_GUN_AERO).getTarget() - oldCrew.getGunnery(slot), 0);
                }
                if (null != p) {
                    p.setName(oldCrew.getName(slot));
                    if (!oldCrew.getExternalIdAsString().equals("-1")) {
                        p.setId(UUID.fromString(oldCrew.getExternalIdAsString(slot)));
                    }

                    populateOptionsFromCrew(p, oldCrew);
                    drivers.add(p);
                }
            }
        } else {
            // Generate drivers for multi-crewed vehicles.

            //Uggh, BA are a nightmare. The getTotalDriverNeeds will adjust for missing/destroyed suits
            //but we can't change that because lots of other stuff needs that to be right, so we will hack
            //it here to make it the starting squad size
            int driversNeeded = u.getTotalDriverNeeds();
            if (u.getEntity() instanceof BattleArmor) {
                driversNeeded = ((BattleArmor) u.getEntity()).getSquadSize();
            }
            while (drivers.size() < driversNeeded) {
                Person p = null;
                if (u.getEntity() instanceof SmallCraft || u.getEntity() instanceof Jumpship) {
                    p = c.newPerson(Person.T_SPACE_PILOT, factionCode);
                    p.addSkill(SkillType.S_PILOT_SPACE,
                            randomSkillFromTarget(
                                    SkillType.getType(SkillType.S_PILOT_SPACE).getTarget() - oldCrew.getPiloting()),
                            0);
                    totalPiloting += p.getSkill(SkillType.S_PILOT_SPACE).getFinalSkillValue();
                } else if (u.getEntity() instanceof BattleArmor) {
                    p = c.newPerson(Person.T_BA, factionCode);
                    p.addSkill(SkillType.S_GUN_BA, randomSkillFromTarget(
                            SkillType.getType(SkillType.S_GUN_BA).getTarget() - oldCrew.getGunnery()), 0);
                    totalGunnery += p.getSkill(SkillType.S_GUN_BA).getFinalSkillValue();
                } else if (u.getEntity() instanceof Infantry) {
                    p = c.newPerson(Person.T_INFANTRY, factionCode);
                    p.addSkill(SkillType.S_SMALL_ARMS,
                            randomSkillFromTarget(
                                    SkillType.getType(SkillType.S_SMALL_ARMS).getTarget() - oldCrew.getGunnery()),
                            0);
                    totalGunnery += p.getSkill(SkillType.S_SMALL_ARMS).getFinalSkillValue();
                } else if (u.getEntity() instanceof VTOL) {
                    p = c.newPerson(Person.T_VTOL_PILOT, factionCode);
                    p.addSkill(SkillType.S_PILOT_VTOL,
                            SkillType.getType(SkillType.S_PILOT_VTOL).getTarget() - oldCrew.getPiloting(), 0);
                    p.addSkill(SkillType.S_GUN_VEE,
                            SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(), 0);
                } else if (u.getEntity() instanceof Mech) {
                    p = c.newPerson(Person.T_MECHWARRIOR, factionCode);
                    p.addSkill(SkillType.S_PILOT_MECH,
                            SkillType.getType(SkillType.S_PILOT_MECH).getTarget() - oldCrew.getPiloting(), 0);
                    p.addSkill(SkillType.S_GUN_MECH,
                            SkillType.getType(SkillType.S_GUN_MECH).getTarget() - oldCrew.getGunnery(), 0);
                } else {
                    //assume tanker if we got here
                    p = c.newPerson(Person.T_GVEE_DRIVER, factionCode);
                    p.addSkill(SkillType.S_PILOT_GVEE,
                            SkillType.getType(SkillType.S_PILOT_GVEE).getTarget() - oldCrew.getPiloting(), 0);
                    p.addSkill(SkillType.S_GUN_VEE,
                            SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery(), 0);
                }

                // this will have the side effect of giving every driver on the crew
                // the SPAs from the entity's crew.
                // Not really any way around it 
                populateOptionsFromCrew(p, oldCrew);
                drivers.add(p);
            }

            // Regenerate as needed to balance
            if (drivers.size() != 0) {
                averageGunnery = (int) Math.round(((double) totalGunnery) / drivers.size());
                averagePiloting = (int) Math.round(((double) totalPiloting) / drivers.size());
                if (u.getEntity() instanceof SmallCraft || u.getEntity() instanceof Jumpship) {
                    while (averagePiloting != oldCrew.getPiloting()) {
                        totalPiloting = 0;
                        for (Person p : drivers) {
                            p.addSkill(SkillType.S_PILOT_SPACE, randomSkillFromTarget(
                                    SkillType.getType(SkillType.S_PILOT_SPACE).getTarget() - oldCrew.getPiloting()),
                                    0);
                            totalPiloting += p.getSkill(SkillType.S_PILOT_SPACE).getFinalSkillValue();
                        }
                        averagePiloting = (int) Math.round(((double) totalPiloting) / drivers.size());
                    }
                } else if (u.getEntity() instanceof BattleArmor) {
                    while (averageGunnery != oldCrew.getGunnery()) {
                        totalGunnery = 0;
                        for (Person p : drivers) {
                            p.addSkill(SkillType.S_GUN_BA, randomSkillFromTarget(
                                    SkillType.getType(SkillType.S_GUN_BA).getTarget() - oldCrew.getGunnery()), 0);
                            totalGunnery += p.getSkill(SkillType.S_GUN_BA).getFinalSkillValue();
                        }
                        averageGunnery = (int) Math.round(((double) totalGunnery) / drivers.size());
                    }
                } else if (u.getEntity() instanceof Infantry) {
                    while (averageGunnery != oldCrew.getGunnery()) {
                        totalGunnery = 0;
                        for (Person p : drivers) {
                            p.addSkill(SkillType.S_SMALL_ARMS, randomSkillFromTarget(
                                    SkillType.getType(SkillType.S_SMALL_ARMS).getTarget() - oldCrew.getGunnery()),
                                    0);
                            totalGunnery += p.getSkill(SkillType.S_SMALL_ARMS).getFinalSkillValue();
                        }
                        averageGunnery = (int) Math.round(((double) totalGunnery) / drivers.size());
                    }
                }
            }
            if (!u.usesSoldiers()) {
                // Generate gunners for multi-crew vehicles
                while (gunners.size() < u.getTotalGunnerNeeds()) {
                    Person p = null;
                    if (u.getEntity() instanceof SmallCraft || u.getEntity() instanceof Jumpship) {
                        p = c.newPerson(Person.T_SPACE_GUNNER, factionCode);
                        p.addSkill(SkillType.S_GUN_SPACE, randomSkillFromTarget(
                                SkillType.getType(SkillType.S_GUN_SPACE).getTarget() - oldCrew.getGunnery()), 0);
                        totalGunnery += p.getSkill(SkillType.S_GUN_SPACE).getFinalSkillValue();
                    } else if (u.getEntity() instanceof Mech) {
                        p = c.newPerson(Person.T_MECHWARRIOR, factionCode);
                        p.addSkill(SkillType.S_PILOT_MECH,
                                SkillType.getType(SkillType.S_PILOT_MECH).getTarget() - oldCrew.getPiloting(), 0);
                        p.addSkill(SkillType.S_GUN_MECH,
                                SkillType.getType(SkillType.S_GUN_MECH).getTarget() - oldCrew.getGunnery(), 0);
                        totalGunnery += p.getSkill(SkillType.S_GUN_MECH).getFinalSkillValue();
                    } else {
                        //assume tanker if we got here
                        p = c.newPerson(Person.T_VEE_GUNNER, factionCode);
                        p.addSkill(SkillType.S_GUN_VEE,
                                randomSkillFromTarget(
                                        SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery()),
                                0);
                        totalGunnery += p.getSkill(SkillType.S_GUN_VEE).getFinalSkillValue();
                    }

                    populateOptionsFromCrew(p, oldCrew);
                    gunners.add(p);
                }

                // Regenerate gunners as needed to balance
                if (gunners.size() != 0) {
                    averageGunnery = (int) Math.round(((double) totalGunnery) / gunners.size());
                    if (u.getEntity() instanceof Tank) {
                        while (averageGunnery != oldCrew.getGunnery()) {
                            totalGunnery = 0;
                            for (Person p : gunners) {
                                p.addSkill(SkillType.S_GUN_VEE, randomSkillFromTarget(
                                        SkillType.getType(SkillType.S_GUN_VEE).getTarget() - oldCrew.getGunnery()),
                                        0);
                                totalGunnery += p.getSkill(SkillType.S_GUN_VEE).getFinalSkillValue();
                            }
                            averageGunnery = (int) Math.round(((double) totalGunnery) / gunners.size());
                        }
                    } else if (u.getEntity() instanceof SmallCraft || u.getEntity() instanceof Jumpship) {
                        while (averageGunnery != oldCrew.getGunnery()) {
                            totalGunnery = 0;
                            for (Person p : gunners) {
                                p.addSkill(SkillType.S_GUN_SPACE,
                                        randomSkillFromTarget(SkillType.getType(SkillType.S_GUN_SPACE).getTarget()
                                                - oldCrew.getGunnery()),
                                        0);
                                totalGunnery += p.getSkill(SkillType.S_GUN_SPACE).getFinalSkillValue();
                            }
                            averageGunnery = (int) Math.round(((double) totalGunnery) / gunners.size());
                        }
                    }
                }
            }
        }
        //Multi-slot crews already have the names set. Single-slot multi-crew units need to assign the commander's name.
        boolean nameset = oldCrew.getSlotCount() > 1;
        while (vesselCrew.size() < u.getTotalCrewNeeds()) {
            Person p = c.newPerson(Person.T_SPACE_CREW, factionCode);
            if (!nameset) {
                p.setName(commanderName);
                nameset = true;
            }
            vesselCrew.add(p);
        }

        if (u.canTakeNavigator()) {
            Person p = c.newPerson(Person.T_NAVIGATOR, factionCode);
            navigator = p;
        }

        if (u.canTakeTechOfficer()) {
            Person p = c.newPerson(Person.T_VEE_GUNNER, factionCode);
            consoleCmdr = p;
        }

        for (Person p : drivers) {
            if (!nameset) {
                p.setName(commanderName);
                nameset = true;
            }
        }

        for (Person p : gunners) {
            if (!nameset) {
                p.setName(commanderName);
                nameset = true;
            }
        }

        for (Person p : vesselCrew) {
            if (!nameset) {
                p.setName(commanderName);
                nameset = true;
            }
        }

        if (null != navigator) {
            if (!nameset) {
                navigator.setName(commanderName);
                nameset = true;
            }
        }

        if (null != consoleCmdr) {
            if (!nameset) {
                consoleCmdr.setName(commanderName);
                nameset = true;
            }
        }

        // Gather the data
        Map<CrewType, Collection<Person>> result = new HashMap<>();
        if (!drivers.isEmpty()) {
            if (u.usesSoloPilot()) {
                result.put(CrewType.PILOT, drivers);
            } else if (u.usesSoldiers()) {
                result.put(CrewType.SOLDIER, drivers);
            } else {
                result.put(CrewType.DRIVER, drivers);
            }
        }
        if (!gunners.isEmpty()) {
            result.put(CrewType.GUNNER, gunners);
        }
        if (!vesselCrew.isEmpty()) {
            result.put(CrewType.VESSEL_CREW, vesselCrew);
        }
        if (null != navigator) {
            result.put(CrewType.NAVIGATOR, Collections.singletonList(navigator));
        }
        if (null != consoleCmdr) {
            result.put(CrewType.TECH_OFFICER, Collections.singletonList(consoleCmdr));
        }
        return result;
    }

    /**
     * Worker function that takes the PilotOptions (SPAs, in other words) from the given "old crew" and sets them for a person.
     * @param p The person whose SPAs to populate
     * @param oldCrew The entity the SPAs of whose crew we're importing
     */
    private static void populateOptionsFromCrew(Person p, Crew oldCrew) {
        Enumeration<IOption> optionsEnum = oldCrew.getOptions().getOptions();
        while (optionsEnum.hasMoreElements()) {
            IOption currentOption = (IOption) optionsEnum.nextElement();
            p.getOptions().getOption(currentOption.getName()).setValue(currentOption.getValue());
        }
    }

    public static int generateRandomExp() {
        int roll = Compute.randomInt(100);
        if (roll < 20) { // 20% chance of a randomized xp
            return (Compute.randomInt(8) + 1);
        } else if (roll < 40) { // 20% chance of 3 xp
            return 3;
        } else if (roll < 60) { // 20% chance of 2 xp
            return 2;
        } else if (roll < 80) { // 20% chance of 1 xp
            return 1;
        }
        return 0; // 20% chance of no xp
    }

    public static int rollSpecialAbilities(int bonus) {
        int roll = Compute.d6(2) + bonus;
        if (roll < 10) {
            return 0;
        } else if (roll < 12) {
            return 1;
        } else {
            return 2;
        }
    }

    public static boolean rollProbability(int prob) {
        return Compute.randomInt(100) <= prob;
    }

    public static int getAgeByExpLevel(int expLevel, boolean clan) {
        int baseage = 19;
        int ndice = 1;
        switch (expLevel) {
        case (SkillType.EXP_REGULAR):
            ndice = 2;
            break;
        case (SkillType.EXP_VETERAN):
            ndice = 3;
            break;
        case (SkillType.EXP_ELITE):
            ndice = 4;
            break;
        }

        int age = baseage;
        while (ndice > 0) {
            int roll = Compute.d6();
            //reroll all sixes once
            if (roll == 6) {
                roll += (Compute.d6() - 1);
            }
            if (clan) {
                roll = (int) Math.ceil(roll / 2.0);
            }
            age += roll;
            ndice--;
        }
        return age;
    }

    public static String getOptionDisplayName(IOption option) {
        String name = option.getDisplayableNameWithValue();
        name = name.replaceAll("\\(.+?\\)", ""); //$NON-NLS-1$ //$NON-NLS-2$
        if (option.getType() == IOption.CHOICE) {
            name += " - " + option.getValue(); //$NON-NLS-1$
        }
        return name;
    }

    public static String printIntegerArray(int[] array) {
        String values = ""; //$NON-NLS-1$
        for (int i = 0; i < array.length; i++) {
            values += Integer.toString(array[i]);
            if (i < (array.length - 1)) {
                values += ","; //$NON-NLS-1$
            }
        }
        return values;
    }

    public static String printDoubleArray(double[] array) {
        String values = ""; //$NON-NLS-1$
        for (int i = 0; i < array.length; i++) {
            values += Double.toString(array[i]);
            if (i < (array.length - 1)) {
                values += ","; //$NON-NLS-1$
            }
        }
        return values;
    }

    public static String printBooleanArray(boolean[] array) {
        String values = ""; //$NON-NLS-1$
        for (int i = 0; i < array.length; i++) {
            values += Boolean.toString(array[i]);
            if (i < (array.length - 1)) {
                values += ","; //$NON-NLS-1$
            }
        }
        return values;
    }

    public static int getSimpleTechLevel(int level) {
        switch (level) {
        case TechConstants.T_ALLOWED_ALL:
        case TechConstants.T_INTRO_BOXSET:
            return CampaignOptions.TECH_INTRO;
        case TechConstants.T_IS_TW_NON_BOX:
        case TechConstants.T_CLAN_TW:
        case TechConstants.T_IS_TW_ALL:
        case TechConstants.T_TW_ALL:
            return CampaignOptions.TECH_STANDARD;
        case TechConstants.T_IS_ADVANCED:
        case TechConstants.T_CLAN_ADVANCED:
            return CampaignOptions.TECH_ADVANCED;
        case TechConstants.T_IS_EXPERIMENTAL:
        case TechConstants.T_CLAN_EXPERIMENTAL:
            return CampaignOptions.TECH_EXPERIMENTAL;
        case TechConstants.T_IS_UNOFFICIAL:
        case TechConstants.T_CLAN_UNOFFICIAL:
            return CampaignOptions.TECH_UNOFFICIAL;
        case TechConstants.T_TECH_UNKNOWN:
            return CampaignOptions.TECH_UNKNOWN;
        default:
            return CampaignOptions.TECH_INTRO;
        }
    }

    //copied from http://www.roseindia.net/java/beginners/copyfile.shtml
    public static void copyfile(File inFile, File outFile) {
        try {
            InputStream in = new FileInputStream(inFile);

            //For Append the file.
            //  OutputStream out = new FileOutputStream(f2,true);

            //For Overwrite the file.
            OutputStream out = new FileOutputStream(outFile);

            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
            in.close();
            out.close();
            System.out.println("File copied."); //$NON-NLS-1$
        } catch (FileNotFoundException ex) {
            System.out.println(ex.getMessage() + " in the specified directory."); //$NON-NLS-1$
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }

    public static void unscrambleEquipmentNumbers(Unit unit) {
        //BA has one part per equipment entry per suit and may need to have trooper fields set following
        //a refit
        if (unit.getEntity() instanceof BattleArmor) {
            assignTroopersAndEquipmentNums(unit);
            return;
        }
        List<Integer> equipNums = new ArrayList<Integer>();
        for (Mounted m : unit.getEntity().getEquipment()) {
            equipNums.add(unit.getEntity().getEquipmentNum(m));
        }
        List<Part> remaining = new ArrayList<>();
        List<Part> notFound = new ArrayList<>();
        for (Part part : unit.getParts()) {
            int eqnum = -1;
            EquipmentType etype = null;
            if (part instanceof EquipmentPart) {
                eqnum = ((EquipmentPart) part).getEquipmentNum();
                etype = ((EquipmentPart) part).getType();
            } else if (part instanceof MissingEquipmentPart) {
                eqnum = ((MissingEquipmentPart) part).getEquipmentNum();
                etype = ((MissingEquipmentPart) part).getType();
            }
            if (null != etype) {
                if (equipNums.contains(eqnum) && etype.equals(unit.getEntity().getEquipment(eqnum).getType())) {
                    equipNums.remove(equipNums.indexOf(eqnum));
                } else {
                    remaining.add(part);
                }
            }
        }
        // For ammo types we want to match the same munition type if possible to avoid the possibility
        // imposing unnecessary ammo swaps.
        boolean allMunitions = false;
        Mounted m;
        while ((remaining.size() > 0) && !allMunitions) {
            for (Part part : remaining) {
                if (part instanceof EquipmentPart) {
                    EquipmentPart epart = (EquipmentPart) part;
                    int i = -1;
                    boolean found = false;
                    for (int equipNum : equipNums) {
                        i++;
                        m = unit.getEntity().getEquipment(equipNum);
                        if (!allMunitions && (part instanceof AmmoBin)
                                && (!(m.getType() instanceof AmmoType) || (((AmmoType) epart.getType())
                                        .getMunitionType() != ((AmmoType) m.getType()).getMunitionType()))) {
                            continue;
                        }
                        if (m.getType().equals(epart.getType()) && !m.isDestroyed()) {
                            epart.setEquipmentNum(equipNum);
                            found = true;
                            break;
                        }
                    }
                    if (found) {
                        equipNums.remove(i);
                    } else {
                        notFound.add(epart);
                    }
                } else if (part instanceof MissingEquipmentPart) {
                    MissingEquipmentPart epart = (MissingEquipmentPart) part;
                    int i = -1;
                    boolean found = false;
                    for (int equipNum : equipNums) {
                        i++;
                        m = unit.getEntity().getEquipment(equipNum);
                        if (!allMunitions && (part instanceof AmmoBin)
                                && (!(m.getType() instanceof AmmoType) || (((AmmoType) epart.getType())
                                        .getMunitionType() != ((AmmoType) m.getType()).getMunitionType()))) {
                            continue;
                        }
                        if (m.getType().equals(epart.getType()) && !m.isDestroyed()) {
                            epart.setEquipmentNum(equipNum);
                            found = true;
                            break;
                        }
                    }
                    if (found) {
                        equipNums.remove(i);
                    } else {
                        notFound.add(epart);
                    }
                }
            }
            allMunitions = true;
            remaining = new ArrayList<>(notFound);
            notFound.clear();
        }

        if (remaining.size() > 0) {
            StringBuilder builder = new StringBuilder();
            builder.append(String.format("Could not unscramble equipment for %s (%s)\r\n\r\n", unit.getName(),
                    unit.getId()));
            for (Part part : remaining) {
                builder.append(" - " + part.getPartName() + " equipmentNum: ");
                if (part instanceof EquipmentPart) {
                    builder.append(((EquipmentPart) part).getEquipmentNum() + "\r\n");
                } else if (part instanceof MissingEquipmentPart) {
                    builder.append(((MissingEquipmentPart) part).getEquipmentNum() + "\r\n");
                }
            }

            builder.append("\r\nAvailable (remaining) equipment:\r\n");
            for (int equipNum : equipNums) {
                m = unit.getEntity().getEquipment(equipNum);
                EquipmentType mType = m.getType();
                builder.append(String.format(" %d: %s %s\r\n", equipNum, m.getName(), mType.getName()));
            }
            MekHQ.getLogger().log(Utilities.class, "unscrambleEquipmentNumbers", LogLevel.WARNING,
                    builder.toString());
        }
    }

    public static void assignTroopersAndEquipmentNums(Unit unit) {
        if (!(unit.getEntity() instanceof BattleArmor)) {
            throw new IllegalArgumentException("Attempting to assign trooper values to parts for non-BA unit");
        }

        //Create a list that we can remove parts from as we match them
        List<EquipmentPart> tempParts = unit.getParts().stream().filter(p -> p instanceof EquipmentPart)
                .map(p -> (EquipmentPart) p).collect(Collectors.toList());

        for (Mounted m : unit.getEntity().getEquipment()) {
            final int eqNum = unit.getEntity().getEquipmentNum(m);
            //Look for parts of the same type with the equipment number already set correctly
            List<EquipmentPart> parts = tempParts.stream()
                    .filter(p -> p.getType().getInternalName().equals(m.getType().getInternalName())
                            && p.getEquipmentNum() == eqNum)
                    .collect(Collectors.toList());
            //If we don't find any, just match the internal name and set the equipment number.
            if (parts.isEmpty()) {
                parts = tempParts.stream()
                        .filter(p -> p.getType().getInternalName().equals(m.getType().getInternalName()))
                        .collect(Collectors.toList());
                parts.forEach(p -> p.setEquipmentNum(eqNum));
            }
            if (parts.stream().allMatch(p -> p instanceof BattleArmorEquipmentPart)) {
                //Try to find one for each trooper; if the Entity has multiple pieces of equipment of this
                //type this will make sure we're only setting one group to this eq number.
                Part[] perTrooper = new Part[unit.getEntity().locations() - 1];
                for (EquipmentPart p : parts) {
                    int trooper = ((BattleArmorEquipmentPart) p).getTrooper();
                    if (trooper > 0) {
                        perTrooper[trooper - 1] = p;
                    }
                }
                //Assign a part to any empty position and set the trooper field
                for (int t = 0; t < perTrooper.length; t++) {
                    if (null == perTrooper[t]) {
                        for (Part p : parts) {
                            if (((BattleArmorEquipmentPart) p).getTrooper() < 1) {
                                ((BattleArmorEquipmentPart) p).setTrooper(t + 1);
                                perTrooper[t] = p;
                                break;
                            }
                        }
                    }
                }
                //Normally there should be a part in each position, but we will leave open the possibility
                //of equipment missing equipment for some troopers in the case of modular/AP mounts or DWPs
                for (Part p : perTrooper) {
                    if (null != p) {
                        tempParts.remove(p);
                    }
                }
            } else {
                //Ammo Bin
                tempParts.removeAll(parts);
            }
        }
        //TODO: Is it necessary to update armor?
    }

    public static int getDaysBetween(Date date1, Date date2) {
        return (int) ((date2.getTime() - date1.getTime()) / MILLISECONDS_IN_DAY);
    }

    /**
     * Calculates the number of days between start and end dates, taking
     * into consideration leap years, year boundaries etc.
     *
     * @param start the start date
     * @param end the end date, must be later than the start date
     * @return the number of days between the start and end dates
     */
    public static long countDaysBetween(Date start, Date end) {
        if (end.before(start)) {
            throw new IllegalArgumentException("The end date must be later than the start date");
        }

        //reset all hours mins and secs to zero on start date
        Calendar startCal = GregorianCalendar.getInstance();
        startCal.setTime(start);
        startCal.set(Calendar.HOUR_OF_DAY, 0);
        startCal.set(Calendar.MINUTE, 0);
        startCal.set(Calendar.SECOND, 0);
        long startTime = startCal.getTimeInMillis();

        //reset all hours mins and secs to zero on end date
        Calendar endCal = GregorianCalendar.getInstance();
        endCal.setTime(end);
        endCal.set(Calendar.HOUR_OF_DAY, 0);
        endCal.set(Calendar.MINUTE, 0);
        endCal.set(Calendar.SECOND, 0);
        long endTime = endCal.getTimeInMillis();

        return (endTime - startTime) / MILLISECONDS_IN_DAY;
    }

    public static int getDiffFullYears(Date date, GregorianCalendar b) {
        GregorianCalendar a = new GregorianCalendar();
        a.setTime(date);
        int diff = b.get(GregorianCalendar.YEAR) - a.get(GregorianCalendar.YEAR);
        if (a.get(GregorianCalendar.MONTH) > b.get(GregorianCalendar.MONTH)
                || (a.get(GregorianCalendar.MONTH) == b.get(GregorianCalendar.MONTH)
                        && a.get(GregorianCalendar.DATE) > b.get(GregorianCalendar.DATE))) {
            diff--;
        }
        return diff;
    }

    public static int getDiffPartialYears(Date date, GregorianCalendar b) {
        GregorianCalendar a = new GregorianCalendar();
        a.setTime(date);
        int diff = b.get(GregorianCalendar.YEAR) - a.get(GregorianCalendar.YEAR);
        if (diff == 0 && countDaysBetween(a.getTime(), b.getTime()) > 0) {
            return 1;
        }
        return diff;
    }

    /** @return the current date as a DateTime time stamp for midnight in UTC time zone */
    public static DateTime getDateTimeDay(Calendar cal) {
        return new LocalDateTime(cal).toDateTime(DateTimeZone.UTC);
    }

    /** @return the current date as a DateTime time stamp for midnight in UTC time zone */
    public static DateTime getDateTimeDay(Date date) {
        return new LocalDateTime(date).toDateTime(DateTimeZone.UTC);
    }

    /**
     * Export a JTable to a CSV file
     * @param table
     * @param file
     * @return report
     */
    public static String exportTabletoCSV(JTable table, File file) {
        String report;
        try {
            TableModel model = table.getModel();
            BufferedWriter writer = Files.newBufferedWriter(Paths.get(file.getPath()));
            String[] columns = new String[model.getColumnCount()];
            for (int i = 0; i < model.getColumnCount(); i++) {
                columns[i] = model.getColumnName(i);
            }
            CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(columns));

            for (int i = 0; i < model.getRowCount(); i++) {
                Object[] towrite = new String[model.getColumnCount()];
                for (int j = 0; j < model.getColumnCount(); j++) {
                    // use regex to remove any HTML tags
                    towrite[j] = model.getValueAt(i, j).toString().replaceAll("\\<[^>]*>", "");
                }
                csvPrinter.printRecord(towrite);
            }

            csvPrinter.flush();
            csvPrinter.close();

            report = model.getRowCount() + " " + resourceMap.getString("RowsWritten.text");
        } catch (Exception ioe) {
            MekHQ.getLogger().log(Utilities.class, "exportTabletoCSV", LogLevel.INFO, "Error exporting JTable");
            report = "Error exporting JTable. See log for details.";
        }
        return report;
    }

    public static Vector<String> splitString(String str, String sep) {
        StringTokenizer st = new StringTokenizer(str, sep);
        Vector<String> output = new Vector<String>();
        while (st.hasMoreTokens()) {
            output.add(st.nextToken());
        }
        return output;
    }

    public static String combineString(Collection<String> vec, String sep) {
        if ((null == vec) || (null == sep)) {
            return null;
        }
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String part : vec) {
            if (first) {
                first = false;
            } else {
                sb.append(sep);
            }
            sb.append(part);
        }
        return sb.toString();
    }

    /** @return the input string with all words capitalized */
    public static String capitalize(String str) {
        if ((null == str) || str.isEmpty()) {
            return str;
        }
        final char[] buffer = str.toCharArray();
        boolean capitalizeNext = true;
        for (int i = 0; i < buffer.length; ++i) {
            final char ch = buffer[i];
            if (Character.isWhitespace(ch)) {
                capitalizeNext = true;
            } else if (capitalizeNext) {
                buffer[i] = Character.toTitleCase(ch);
                capitalizeNext = false;
            }
        }
        return new String(buffer);
    }

    public static String getRomanNumeralsFromArabicNumber(int level, boolean checkZero) {
        // If we're 0, then we just return an empty string
        if (checkZero && level == 0) {
            return ""; //$NON-NLS-1$
        }

        // Roman numeral, prepended with a space for display purposes
        String roman = " "; //$NON-NLS-1$
        int num = level + 1;

        for (int i = 0; i < arabicNumbers.length; i++) {
            while (num > arabicNumbers[i]) {
                roman += romanNumerals[i];
                num -= arabicNumbers[i];
            }
        }

        return roman;
    }

    // TODO: Optionize this to allow user to choose roman or arabic numerals
    public static int getArabicNumberFromRomanNumerals(String name) {
        // If we're 0, then we just return an empty string
        if (name.equals("")) { //$NON-NLS-1$
            return 0;
        }

        // Roman numeral, prepended with a space for display purposes
        int arabic = 0;
        String roman = name;

        for (int i = 0; i < roman.length(); i++) {
            int num = romanNumerals.toString().indexOf(roman.charAt(i));
            if (i < roman.length()) {
                int temp = romanNumerals.toString().indexOf(roman.charAt(i + 1));
                // If this is a larger number, then we need to combine them
                if (temp > num) {
                    num = temp - num;
                    i++;
                }
            }

            arabic += num;
        }

        return arabic - 1;
    }

    public static Map<String, Integer> sortMapByValue(Map<String, Integer> unsortMap, boolean highFirst) {

        // Convert Map to List
        List<Map.Entry<String, Integer>> list = new LinkedList<Map.Entry<String, Integer>>(unsortMap.entrySet());

        // Sort list with comparator, to compare the Map values
        Collections.sort(list, new Comparator<Map.Entry<String, Integer>>() {
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
                return (o1.getValue()).compareTo(o2.getValue());
            }
        });

        // Convert sorted map back to a Map
        Map<String, Integer> sortedMap = new LinkedHashMap<String, Integer>();
        if (highFirst) {
            ListIterator<Map.Entry<String, Integer>> li = list.listIterator(list.size());
            while (li.hasPrevious()) {
                Map.Entry<String, Integer> entry = li.previous();
                sortedMap.put(entry.getKey(), entry.getValue());
            }
        } else {
            for (Iterator<Map.Entry<String, Integer>> it = list.iterator(); it.hasNext();) {
                Map.Entry<String, Integer> entry = it.next();
                sortedMap.put(entry.getKey(), entry.getValue());
            }
        }

        return sortedMap;
    }

    public static boolean isLikelyCapture(Entity en) {
        //most of these conditions are now controlled better in en.canEscape, but there
        //are some additional ones we want to add
        if (!en.canEscape()) {
            return true;
        }
        return en.isDestroyed() || en.isDoomed() || en.isStalled() || en.isStuck();
    }

    /**
     * Run through the directory and call parser.parse(fis) for each XML file found. Don't recurse.
     */
    public static void parseXMLFiles(String dirName, FileParser parser) {
        parseXMLFiles(dirName, parser, false);
    }

    /**
     * Run through the directory and call parser.parse(fis) for each XML file found.
     */
    public static void parseXMLFiles(String dirName, FileParser parser, boolean recurse) {
        final String METHOD_NAME = "parseXMLFiles(String,FileParser,boolean)"; //$NON-NLS-1$

        if (null == dirName || null == parser) {
            throw new NullPointerException();
        }
        File dir = new File(dirName);
        if (dir.isDirectory()) {
            File[] files = dir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.toLowerCase(Locale.ROOT).endsWith(".xml"); //$NON-NLS-1$
                }
            });
            if (null != files && files.length > 0) {
                // Case-insensitive sorting. Yes, even on Windows. Deal with it.
                Arrays.sort(files, new Comparator<File>() {
                    @Override
                    public int compare(File f1, File f2) {
                        return f1.getPath().compareTo(f2.getPath());
                    }
                });
                // Try parsing and updating the main list, one by one
                for (File file : files) {
                    if (file.isFile()) {
                        try (FileInputStream fis = new FileInputStream(file)) {
                            parser.parse(fis);
                        } catch (Exception ex) {
                            // Ignore this file then
                            MekHQ.getLogger().log(Utilities.class, METHOD_NAME, LogLevel.ERROR,
                                    "Exception trying to parse " + file.getPath() + " - ignoring."); //$NON-NLS-1$ //$NON-NLS-2$
                            MekHQ.getLogger().log(Utilities.class, METHOD_NAME, ex);
                        }
                    }
                }
            }

            if (!recurse) {
                // We're done
                return;
            }

            // Get subdirectories too
            File[] dirs = dir.listFiles();
            if (null != dirs && dirs.length > 0) {
                Arrays.sort(dirs, new Comparator<File>() {
                    @Override
                    public int compare(File f1, File f2) {
                        return f1.getPath().compareTo(f2.getPath());
                    }
                });
                for (File subDirectory : dirs) {
                    if (subDirectory.isDirectory()) {
                        parseXMLFiles(subDirectory.getPath(), parser, recurse);
                    }
                }
            }

        }
    }
}