/* * 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.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; 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.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.StringTokenizer; import java.util.Vector; import java.util.stream.Collectors; import javax.swing.JTable; import javax.swing.table.TableModel; 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.Infantry; import megamek.common.Jumpship; 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.VTOL; import megamek.common.options.IOption; 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.MissingAmmoBin; 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; /** * * @author Jay Lawson <jaylawson39 at yahoo.com> */ public class Utilities { private static final int MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; // 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.getAmmoType() == AmmoType.T_MML) || (atype.getAmmoType() == AmmoType.T_ATM) || (atype.getAmmoType() == AmmoType.T_NARC) || (atype.getAmmoType() == AmmoType.T_AC_LBX))) { 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; } // 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))) { 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, int year, CampaignOptions options) { 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()) { continue; } // If we only allow canon units and this isn't canon we continue if(!summary.isCanon() && options.allowCanonRefitOnly()) { continue; } // If we're limiting by year and aren't to this unit's year yet we continue if(options.limitByYear() && summary.getYear() > year) { continue; } // If the tech level doesn't meet the game's tech level we continue if(options.getTechLevel() < Utilities.getSimpleTechLevel(summary.getType())) { 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) { 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; 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 Mech) { p = c.newPerson(Person.T_MECHWARRIOR); 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); 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); 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); //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); 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); 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); } 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); 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); 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); 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); 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); 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); } 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); 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 { //assume tanker if we got here p = c.newPerson(Person.T_VEE_GUNNER); 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(); } 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()); } } } } } boolean nameset = false; while(vesselCrew.size() < u.getTotalCrewNeeds()) { Person p = c.newPerson(Person.T_SPACE_CREW); if (!nameset) { p.setName(commanderName); nameset = true; } vesselCrew.add(p); } if(u.canTakeNavigator()) { Person p = c.newPerson(Person.T_NAVIGATOR); navigator = 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; } } // 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)); } return result; } 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; } ArrayList<Integer> equipNums = new ArrayList<Integer>(); for(Mounted m : unit.getEntity().getEquipment()) { equipNums.add(unit.getEntity().getEquipmentNum(m)); } for(Part part : unit.getParts()) { if(!(part instanceof EquipmentPart)) { continue; } EquipmentPart ep = ((EquipmentPart) part); Mounted m = unit.getEntity().getEquipment(ep.getEquipmentNum()); //Taharqa: I am not sure what was supposed to go in here, but it doesn't actually //do anything at this point and it is producing an NPE on some refits, so I am //commenting it out //if (m.getType().getInternalName().equals(ep.getType().getInternalName())) { //} if(part instanceof AmmoBin) { AmmoBin bin = (AmmoBin)part; int i = -1; boolean found = false; for(int equipNum : equipNums) { i++; m = unit.getEntity().getEquipment(equipNum); if(!(m.getType() instanceof AmmoType)) { continue; } if(m.getType().getInternalName().equals(bin.getType().getInternalName()) && ((AmmoType)m.getType()).getMunitionType() == bin.getMunitionType() && !m.isDestroyed()) { bin.setEquipmentNum(equipNum); found = true; break; } } if(found) { equipNums.remove(i); } } else if(part instanceof MissingAmmoBin) { MissingAmmoBin bin = (MissingAmmoBin)part; int i = -1; boolean found = false; for(int equipNum : equipNums) { i++; m = unit.getEntity().getEquipment(equipNum); if(!(m.getType() instanceof AmmoType)) { continue; } if(m.getType().getInternalName().equals(bin.getType().getInternalName()) && m.isDestroyed()) { bin.setEquipmentNum(equipNum); found = true; break; } } if(found) { equipNums.remove(i); } } else 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(m.getType() instanceof AmmoType) { continue; } if(m.getType().getInternalName().equals(epart.getType().getInternalName()) && !m.isDestroyed()) { epart.setEquipmentNum(equipNum); found = true; break; } } if(found) { equipNums.remove(i); } } 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(m.getType().getInternalName().equals(epart.getType().getInternalName()) && m.isDestroyed()) { epart.setEquipmentNum(equipNum); found = true; break; } } if(found) { equipNums.remove(i); } } } } 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 TSV * code derived from: * https://sites.google.com/site/teachmemrxymon/java/export-records-from-jtable-to-ms-excel * @param table * @param file */ public static void exportTabletoCSV(JTable table, File file){ try{ TableModel model = table.getModel(); FileWriter csv = new FileWriter(file); for(int i = 0; i < model.getColumnCount(); i++) { String s = model.getColumnName(i); if(null == s) { s = ""; //$NON-NLS-1$ } if (s.contains("\"")) { //$NON-NLS-1$ s = s.replace("\"", "\"\""); //$NON-NLS-1$ //$NON-NLS-2$ } s = "\"" + s + "\""; //$NON-NLS-1$ //$NON-NLS-2$ csv.write(s+","); //$NON-NLS-1$ } csv.write("\n"); //$NON-NLS-1$ for(int i=0; i< model.getRowCount(); i++) { for(int j=0; j < model.getColumnCount(); j++) { String s = model.getValueAt(i,j).toString(); if(null == s) { s = ""; //$NON-NLS-1$ } if (s.contains("\"")) { //$NON-NLS-1$ s = s.replace("\"", "\"\""); //$NON-NLS-1$ //$NON-NLS-2$ } s = "\"" + s + "\""; //$NON-NLS-1$ //$NON-NLS-2$ csv.write(s+","); //$NON-NLS-1$ } csv.write("\n"); //$NON-NLS-1$ } csv.close(); }catch(IOException e){ System.out.println(e); } } 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) { 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.logError("Exception trying to parse " + file.getPath() + " - ignoring."); //$NON-NLS-1$ //$NON-NLS-2$ MekHQ.logError(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); } } } } } }