From 5afc3ee733690830d40dd94cac2e8d63de7ad874 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Thu, 28 Nov 2024 15:28:38 -0700 Subject: [PATCH 01/26] Basic cleanup for readability. Adjust ratings adjustment math when dealing with searching multiple parent factions to avoid messy truncated integers. --- .../client/ratgenerator/RATGenerator.java | 96 ++++++++++++------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index d03e5287d3b..3eafa1e2284 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -137,18 +137,32 @@ public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, St return null; } + /** + * Return the availability rating for a given chassis. If the specific faction is not + * directly listed, the parents of the provided faction are used as a lookup. If multiple + * parent factions are provided, all of them are calculated and then averaged. + * @param era year for era + * @param unit string with chassis name + * @param fRec faction data + * @param year year to test + * @return An availability rating object + */ public @Nullable AvailabilityRating findChassisAvailabilityRecord(int era, String unit, FactionRecord fRec, int year) { + if (fRec == null) { return null; } + AvailabilityRating retVal = null; if (chassisIndex.containsKey(era) && chassisIndex.get(era).containsKey(unit)) { + if (chassisIndex.get(era).get(unit).containsKey(fRec.getKey())) { retVal = chassisIndex.get(era).get(unit).get(fRec.getKey()); } else if (fRec.getParentFactions().size() == 1) { retVal = findChassisAvailabilityRecord(era, unit, fRec.getParentFactions().get(0), year); } else if (!fRec.getParentFactions().isEmpty()) { + ArrayList list = new ArrayList<>(); for (String alt : fRec.getParentFactions()) { AvailabilityRating ar = findChassisAvailabilityRecord(era, unit, alt, year); @@ -157,6 +171,7 @@ public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, St } } retVal = mergeFactionAvailability(fRec.getKey(), list); + } else { retVal = chassisIndex.get(era).get(unit).get("General"); } @@ -377,34 +392,35 @@ public boolean eraIsLoaded(int era) { } /** - * Used for a faction with multiple parent factions (e.g. FC == FS + LA) to find - * the average - * availability among the parents. Based on average weight rather than av - * rating. + * Used for a faction with multiple parent factions (e.g. FC == FS + LA) + * to find the average availability among the parents. Based on average + * weight rather than av rating. * - * @param faction The faction code to use for the new AvailabilityRecord - * @param list A list of ARs for the various parent factions - * @return A new AR with the average availability code from the various - * factions. + * @param faction The faction code to use for the new AvailabilityRecord + * @param avList A list of ratings from the various parent factions + * @return A new availability rating with the average value from the various factions. */ - private AvailabilityRating mergeFactionAvailability(String faction, List list) { - if (list.isEmpty()) { + private AvailabilityRating mergeFactionAvailability(String faction, + List avList) { + + if (avList.isEmpty()) { return null; } + double totalWt = 0; int totalAdj = 0; - for (AvailabilityRating ar : list) { + + for (AvailabilityRating ar : avList) { totalWt += AvailabilityRating.calcWeight(ar.availability); totalAdj += ar.ratingAdjustment; } - AvailabilityRating retVal = list.get(0).makeCopy(faction); + AvailabilityRating retVal = avList.get(0).makeCopy(faction); - retVal.availability = (int) (AvailabilityRating.calcAvRating(totalWt / list.size())); - if (totalAdj < 0) { - retVal.ratingAdjustment = (totalAdj - 1) / list.size(); - } else { - retVal.ratingAdjustment = (totalAdj + 1) / list.size(); + retVal.availability = (int) (AvailabilityRating.calcAvRating(totalWt / avList.size())); + if (totalAdj != 0) { + retVal.ratingAdjustment = totalAdj > 0 ? 1 : -1; } + return retVal; } @@ -438,11 +454,31 @@ private Double interpolate(Number av1, Number av2, int year1, int year2, int now + (av2.doubleValue() - av1.doubleValue()) * (now - year1) / (year2 - year1); } - public List generateTable(FactionRecord fRec, int unitType, int year, - String rating, Collection weightClasses, int networkMask, - Collection movementModes, - Collection roles, int roleStrictness, - FactionRecord user) { + /** + * Generate a frequency table for random selection of units, given a range of parameters + * @param fRec + * @param unitType + * @param year + * @param rating + * @param weightClasses + * @param networkMask + * @param movementModes + * @param roles + * @param roleStrictness + * @param user + * @return + */ + public List generateTable(FactionRecord fRec, + int unitType, + int year, + String rating, + Collection weightClasses, + int networkMask, + Collection movementModes, + Collection roles, + int roleStrictness, + FactionRecord user) { + HashMap unitWeights = new HashMap<>(); HashMap salvageWeights = new HashMap<>(); @@ -465,16 +501,13 @@ public List generateTable(FactionRecord fRec, int unitType } /* - * Adjustments for unit rating require knowing both how many ratings are - * available + * Adjustments for unit rating require knowing both how many ratings are available * to the faction and where the rating falls within the whole. If a faction does * not have designated rating levels, it inherits those of the parent faction; - * if there are multiple parent factions the first match is used. Some very - * minor - * or generic factions do not use rating adjustments, indicated by a rating - * level - * of -1. A faction that has one rating level is a special case that always has - * the indicated rating within the parent faction's system. + * if there are multiple parent factions the first match is used. + * Some very minor or generic factions do not use rating adjustments, indicated by + * a rating level of -1. A faction that has one rating level is a special case that + * always has the indicated rating within the parent faction's system. */ int ratingLevel = -1; @@ -507,8 +540,7 @@ public List generateTable(FactionRecord fRec, int unitType continue; } - AvailabilityRating ar = findChassisAvailabilityRecord(early, - chassisKey, fRec, year); + AvailabilityRating ar = findChassisAvailabilityRecord(early, chassisKey, fRec, year); if (ar == null) { continue; } From cccd446b0fd5f5920df33c1c28ec7503ceee6d43 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Thu, 28 Nov 2024 16:37:38 -0700 Subject: [PATCH 02/26] Readability improvements --- .../client/ratgenerator/RATGenerator.java | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 3eafa1e2284..7ec9347a5fc 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -47,10 +47,8 @@ /** * Generates a random assignment table (RAT) dynamically based on a variety of - * criteria, - * including faction, era, unit type, weight class, equipment rating, faction - * subcommand, vehicle - * movement mode, and mission role. + * criteria, including faction, era, unit type, weight class, equipment rating, + * faction subcommand, vehicle movement mode, and mission role. * * @author Neoancient */ @@ -123,8 +121,10 @@ public void reloadFromDir(File dir) { rg.getEraSet().forEach(e -> rg.loadEra(e, dir)); } - public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, String faction, - int year) { + public AvailabilityRating findChassisAvailabilityRecord(int era, + String unit, + String faction, + int year) { if (factions.containsKey(faction)) { return findChassisAvailabilityRecord(era, unit, factions.get(faction), year); } @@ -145,7 +145,7 @@ public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, St * @param unit string with chassis name * @param fRec faction data * @param year year to test - * @return An availability rating object + * @return chassis availability rating, relative to other chassis in a collection */ public @Nullable AvailabilityRating findChassisAvailabilityRecord(int era, String unit, FactionRecord fRec, int year) { @@ -184,8 +184,9 @@ public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, St return null; } - public @Nullable AvailabilityRating findModelAvailabilityRecord(int era, String unit, - String faction) { + public @Nullable AvailabilityRating findModelAvailabilityRecord(int era, + String unit, + String faction) { if (factions.containsKey(faction)) { return findModelAvailabilityRecord(era, unit, factions.get(faction)); } else if (modelIndex.containsKey(era) && modelIndex.get(era).containsKey(unit)) { @@ -195,31 +196,50 @@ public AvailabilityRating findChassisAvailabilityRecord(int era, String unit, St } } - public @Nullable AvailabilityRating findModelAvailabilityRecord(int era, String unit, - @Nullable FactionRecord fRec) { + /** + * Generate the availability rating for a specific model + * @param era era designation + * @param unit string full chassis-model name + * @param fRec faction data + * @return the availability value relative to other models of the same chassis + */ + public @Nullable AvailabilityRating findModelAvailabilityRecord(int era, + String unit, + @Nullable FactionRecord fRec) { + if (null == models.get(unit)) { logger.error("Trying to find record for unknown model " + unit); return null; } else if ((fRec == null) || models.get(unit).factionIsExcluded(fRec)) { return null; } + if (modelIndex.containsKey(era) && modelIndex.get(era).containsKey(unit)) { + + // If the provided faction is directly specified, return its availability if (modelIndex.get(era).get(unit).containsKey(fRec.getKey())) { return modelIndex.get(era).get(unit).get(fRec.getKey()); } + // If the provided faction has a single parent, return its availability if (fRec.getParentFactions().size() == 1) { return findModelAvailabilityRecord(era, unit, fRec.getParentFactions().get(0)); } else if (!fRec.getParentFactions().isEmpty()) { - ArrayList list = new ArrayList<>(); - for (String alt : fRec.getParentFactions()) { - AvailabilityRating ar = findModelAvailabilityRecord(era, unit, alt); + + // If neither the faction nor a direct parent is directly specified and + // multiple parent factions are available, calculate an average between them + ArrayList avList = new ArrayList<>(); + for (String curParent : fRec.getParentFactions()) { + AvailabilityRating ar = findModelAvailabilityRecord(era, unit, curParent); if (ar != null) { - list.add(ar); + avList.add(ar); } } - return mergeFactionAvailability(fRec.getKey(), list); + return mergeFactionAvailability(fRec.getKey(), avList); + } + + // As a fallback, check for General availability return modelIndex.get(era).get(unit).get("General"); } @@ -587,11 +607,10 @@ public List generateTable(FactionRecord fRec, } // If there is more than one weight class and the faction record (or parent) - // indicates a - // certain distribution of weight classes, adjust the weight value to conform to - // the given - // ratio. + // indicates a certain distribution of weight classes, adjust the weight value + // to conform to the given ratio. if (weightClasses.size() > 1) { + // Get standard weight class distribution for faction ArrayList wcd = fRec.getWeightDistribution(early, unitType); @@ -704,6 +723,7 @@ public List generateTable(FactionRecord fRec, retVal.add(new TableEntry(wt, mRec.getMekSummary())); } } + return retVal; } @@ -831,8 +851,7 @@ private void adjustForRating(FactionRecord fRec, int unitType, int year, int rat } // For non-Clan factions, the amount of salvage from Clan factions is part of - // the overall - // Clan percentage. + // the overall Clan percentage. if (!fRec.isClan() && (pctClan != null) && (totalClan > 0)) { double clanSalvage = salvageWeights.keySet().stream().filter(FactionRecord::isClan) .mapToDouble(salvageWeights::get).sum(); @@ -917,8 +936,8 @@ private synchronized void initialize(File dir) { /** * If the year is equal to one of the era marks, it loads that era. If it is - * between two, it - * loads eras on both sides. Otherwise, just load the closest era. + * between two, it loads eras on both sides. Otherwise, just load the + * closest era. */ public void loadYear(final int year) { if (getEraSet().isEmpty()) { @@ -1054,10 +1073,8 @@ private synchronized void loadEra(int era, File dir) { /** * Creates model and chassis records for all units that don't already have - * entries. This should - * only be called after all availability records are loaded, otherwise they will - * be overwritten. - * + * entries. This should only be called after all availability records are + * loaded, otherwise they will be overwritten. * Used for editing. */ public void initRemainingUnits() { From 2074199688d4c97bdd58010dc314b384f0566312 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 29 Nov 2024 09:38:16 -0700 Subject: [PATCH 03/26] Generate model list using basic filters. Create new method for getting total model weight which includes +/- dynamic adjustment and role-based adjustment. --- .../client/ratgenerator/ChassisRecord.java | 120 ++++++++++++++++-- .../client/ratgenerator/RATGenerator.java | 6 + 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index 883b3e53e46..531944625b3 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -13,11 +13,9 @@ */ package megamek.client.ratgenerator; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; +import java.util.*; +import megamek.common.EntityMovementMode; import megamek.logging.MMLogger; /** @@ -55,6 +53,108 @@ public List getSortedModels() { } + /** + * Generate a list of models for this chassis based on certain criteria. + * Early prototypes may be available one year before official introduction. + * @param exactYear game year, model must be less than one year away + * @param movementModes movement mode types, may be null or empty + * @param networkMask specific C3 network equipment + * @return set of models which pass the filter requirements, may be empty + */ + public HashSet getFilteredModels(int exactYear, + Collection movementModes, + int networkMask) { + + HashSet filteredModels = new HashSet<>(); + + for (ModelRecord curModel : models) { + // Introduction date check - exclude anything more than a year in the future + if (curModel.introYear > exactYear + 1) { + continue; + } + + // Movement mode check + if (movementModes != null && !movementModes.isEmpty()) { + if (!movementModes.contains(curModel.getMovementMode())) { + continue; + } + } + + // C3 network equipment check + if ((networkMask & curModel.getNetworkMask()) != networkMask) { + continue; + } + + filteredModels.add(curModel); + } + + return filteredModels; + } + + /** + * Total the weights of all models for this chassis, including modifiers for + * +/- dynamic adjustment, intro year adjustment, and role selection. + * @param validModels models to add up + * @param era year for era, defined by availability data files + * @param exactYear current year in game + * @param fRec faction data + * @param roles roles selected for generation, may be null or empty + * @param roleStrictness positive number, higher applies heavier role adjustments + * @param equipRating equipment rating to generate for + * @param numRatingLevels how many rating levels are present + * @return sum of calculated weights of all models of this chassis + */ + public double totalModelWeight(HashSet validModels, + int era, + int exactYear, + FactionRecord fRec, + Collection roles, + int roleStrictness, + int equipRating, + int numRatingLevels) { + + RATGenerator ratGen = RATGenerator.getInstance(); + double retVal = 0; + double adjRating; + Number roleRating; + + // For each model + for (ModelRecord curModel : validModels) { + + if (curModel.factionIsExcluded(fRec)) { + continue; + } + + // Get the availability rating for the provided faction and year, + // skip processing if not available + AvailabilityRating avRating = ratGen.findModelAvailabilityRecord(era, + curModel.getKey(), fRec); + if (avRating == null || avRating.getAvailability() <= 0) { + continue; + } + + // Adjust availability for +/- dynamic and intro year + adjRating = calcAvailability(avRating, equipRating, numRatingLevels, exactYear); + if (adjRating <= 0) { + continue; + } + + // Adjust availability for roles. Method may return null as a filtering mechanism. + roleRating = MissionRole.adjustAvailabilityByRole(adjRating, + roles, curModel, exactYear, roleStrictness); + + if (roleRating == null || roleRating.doubleValue() <= 0) { + continue; + } + + // Calculate the weight and add it to the total + retVal += AvailabilityRating.calcWeight(roleRating.doubleValue()); + + } + + return retVal; + } + public int totalModelWeight(int era, String fKey) { FactionRecord fRec = RATGenerator.getInstance().getFaction(fKey); if (fRec == null) { @@ -66,13 +166,13 @@ public int totalModelWeight(int era, String fKey) { public int totalModelWeight(int era, FactionRecord fRec) { int retVal = 0; - RATGenerator rg = RATGenerator.getInstance(); + RATGenerator ratGen = RATGenerator.getInstance(); - for (ModelRecord mr : models) { - AvailabilityRating ar = rg.findModelAvailabilityRecord(era, - mr.getKey(), fRec); - if (ar != null) { - retVal += AvailabilityRating.calcWeight(ar.getAvailability()); + for (ModelRecord curModel : models) { + AvailabilityRating avRating = ratGen.findModelAvailabilityRecord(era, + curModel.getKey(), fRec); + if (avRating != null) { + retVal += AvailabilityRating.calcWeight(avRating.getAvailability()); } } diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 7ec9347a5fc..0937cd7afe8 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -569,6 +569,12 @@ public List generateTable(FactionRecord fRec, cRec.calcAvailability(ar, ratingLevel, numRatingLevels, late), Math.max(early, cRec.getIntroYear()), late, year); if (cAv > 0) { + + // Apply basic filters to models before summing the total weight + HashSet validModels = cRec.getFilteredModels(year, movementModes,networkMask); + + double testWeight = cRec.totalModelWeight(validModels, early, year, cRec.isOmni() ? user : fRec, roles, roleStrictness, ratingLevel, numRatingLevels); + double totalModelWeight = cRec.totalModelWeight(early, cRec.isOmni() ? user : fRec); for (ModelRecord mRec : cRec.getModels()) { From 92afe428d7fd3ff35c5d61b3bcaab2bdb27f7928 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 29 Nov 2024 10:19:58 -0700 Subject: [PATCH 04/26] Clean up dynamic availability adjustment for intro year, adding proper offsets pre-production models and initial production limits --- .../ratgenerator/AbstractUnitRecord.java | 23 ++++++++++++------- .../client/ratgenerator/ChassisRecord.java | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/AbstractUnitRecord.java b/megamek/src/megamek/client/ratgenerator/AbstractUnitRecord.java index 27f4cdff6e6..ed5a9e7820e 100644 --- a/megamek/src/megamek/client/ratgenerator/AbstractUnitRecord.java +++ b/megamek/src/megamek/client/ratgenerator/AbstractUnitRecord.java @@ -40,23 +40,30 @@ public AbstractUnitRecord(String chassis) { } /** - * Adjusts availability rating for the first couple years after introduction. + * Adjusts availability rating for +/- dynamic. Also reduces availability by + * introduction year, with 1 year before heavily reduced for pre-production + * prototypes and first year slightly reduced for working out initial + * production. * - * @param ar The AvailabilityRecord for the chassis or model. - * @param rating The force equipment rating. + * @param avRating The AvailabilityRecord for the chassis or model. + * @param equipRating The force equipment rating. * @param ratingLevels The number of equipment rating levels used by the faction. - * @param year The game year + * @param year The game year * @return The adjusted availability rating. */ - public int calcAvailability(AvailabilityRating ar, int rating, int ratingLevels, int year) { - int retVal = ar.adjustForRating(rating, ratingLevels); + public int calcAvailability(AvailabilityRating avRating, int equipRating, int ratingLevels, int year) { + int retVal = avRating.adjustForRating(equipRating, ratingLevels); - if (introYear == year) { + // Pre-production prototypes are heavily reduced + if (year == introYear - 1) { retVal -= 2; } - if (introYear == year + 1) { + + // Initial production year is slightly reduced + if (year == introYear) { retVal -= 1; } + return Math.max(retVal, 0); } diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index 531944625b3..9bb1471ed0a 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -134,7 +134,7 @@ public double totalModelWeight(HashSet validModels, } // Adjust availability for +/- dynamic and intro year - adjRating = calcAvailability(avRating, equipRating, numRatingLevels, exactYear); + adjRating = curModel.calcAvailability(avRating, equipRating, numRatingLevels, exactYear); if (adjRating <= 0) { continue; } From 6a58e58f67ac1e6680b35d056dc161e7acd7ebfe Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 29 Nov 2024 11:17:49 -0700 Subject: [PATCH 05/26] Fix math for +/- dynamic availability calculation --- .../ratgenerator/AvailabilityRating.java | 23 +++++++++++++++---- .../client/ratgenerator/RATGenerator.java | 2 ++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/AvailabilityRating.java b/megamek/src/megamek/client/ratgenerator/AvailabilityRating.java index b228c0774a7..400b2359c01 100644 --- a/megamek/src/megamek/client/ratgenerator/AvailabilityRating.java +++ b/megamek/src/megamek/client/ratgenerator/AvailabilityRating.java @@ -123,13 +123,26 @@ public int getAvailability() { return availability; } - public int adjustForRating(int rating, int numLevels) { - if (rating < 0 || ratingAdjustment == 0) { + /** + * Adjust availability rating for the dynamic +/- value, which is based on + * equipment quality rating. The (+) will reduce availability for commands + * with a lower rating, while the (-) will reduce availability for commands + * with a higher rating. + * @param equipRating zero-based index based on {@code numLevels} of rating to check + * @param numLevels number of equipment levels available, typically 5 (A/B/C/D/F) + * @return integer, may be negative + */ + public int adjustForRating(int equipRating, int numLevels) { + if (ratingAdjustment == 0 || equipRating < 0) { return availability; - } else if (ratingAdjustment < 0) { - return availability - rating; + } + + if (ratingAdjustment > 0) { + // (+) adjustment, reduce availability as equipment rating decreases + return availability - (numLevels - 1 - equipRating); } else { - return availability - (numLevels - rating); + // (-) adjustment, reduce availability as equipment rating increases + return availability - equipRating; } } diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 0937cd7afe8..29b969c2870 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -560,6 +560,8 @@ public List generateTable(FactionRecord fRec, continue; } + // TODO: add weight class filtering here + AvailabilityRating ar = findChassisAvailabilityRecord(early, chassisKey, fRec, year); if (ar == null) { continue; From a71ea5f5503bb265ad9f8df449caf01c3fa3a9f0 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 29 Nov 2024 20:14:15 -0700 Subject: [PATCH 06/26] Add interpolation for when game year is between era years --- .../client/ratgenerator/ChassisRecord.java | 33 +++++++++++++++---- .../client/ratgenerator/RATGenerator.java | 10 +++++- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index 9bb1471ed0a..e124a94a248 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -56,7 +56,7 @@ public List getSortedModels() { /** * Generate a list of models for this chassis based on certain criteria. * Early prototypes may be available one year before official introduction. - * @param exactYear game year, model must be less than one year away + * @param exactYear game year * @param movementModes movement mode types, may be null or empty * @param networkMask specific C3 network equipment * @return set of models which pass the filter requirements, may be empty @@ -68,7 +68,7 @@ public HashSet getFilteredModels(int exactYear, HashSet filteredModels = new HashSet<>(); for (ModelRecord curModel : models) { - // Introduction date check - exclude anything more than a year in the future + // Introduction date should be at most 1 year away for pre-production prototypes if (curModel.introYear > exactYear + 1) { continue; } @@ -93,10 +93,12 @@ public HashSet getFilteredModels(int exactYear, /** * Total the weights of all models for this chassis, including modifiers for - * +/- dynamic adjustment, intro year adjustment, and role selection. + * +/- dynamic adjustment, intro year adjustment, interpolation, and role + * modifications. * @param validModels models to add up - * @param era year for era, defined by availability data files + * @param era year for current era * @param exactYear current year in game + * @param nextEra start date of next era after the current one * @param fRec faction data * @param roles roles selected for generation, may be null or empty * @param roleStrictness positive number, higher applies heavier role adjustments @@ -107,6 +109,7 @@ public HashSet getFilteredModels(int exactYear, public double totalModelWeight(HashSet validModels, int era, int exactYear, + int nextEra, FactionRecord fRec, Collection roles, int roleStrictness, @@ -114,8 +117,10 @@ public double totalModelWeight(HashSet validModels, int numRatingLevels) { RATGenerator ratGen = RATGenerator.getInstance(); + AvailabilityRating avRating, nextAvRating; double retVal = 0; double adjRating; + double nextRating; Number roleRating; // For each model @@ -127,8 +132,7 @@ public double totalModelWeight(HashSet validModels, // Get the availability rating for the provided faction and year, // skip processing if not available - AvailabilityRating avRating = ratGen.findModelAvailabilityRecord(era, - curModel.getKey(), fRec); + avRating = ratGen.findModelAvailabilityRecord(era, curModel.getKey(), fRec); if (avRating == null || avRating.getAvailability() <= 0) { continue; } @@ -139,6 +143,23 @@ public double totalModelWeight(HashSet validModels, continue; } + // If required, interpolate availability between era start or intro date + // (whichever is later), and start of next era + if (exactYear > era && era != nextEra) { + nextAvRating = ratGen.findModelAvailabilityRecord(nextEra, + curModel.getKey(), fRec); + + if (nextAvRating != null) { + + int interpolationStart = Math.max(curModel.introYear, era); + nextRating = curModel.calcAvailability(nextAvRating, equipRating, numRatingLevels, nextEra); + if (adjRating != nextRating) { + adjRating = adjRating + (nextRating - adjRating) * (exactYear - interpolationStart) / (nextEra - interpolationStart); + } + } + + } + // Adjust availability for roles. Method may return null as a filtering mechanism. roleRating = MissionRole.adjustAvailabilityByRole(adjRating, roles, curModel, exactYear, roleStrictness); diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 29b969c2870..624710ebd4b 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -575,7 +575,15 @@ public List generateTable(FactionRecord fRec, // Apply basic filters to models before summing the total weight HashSet validModels = cRec.getFilteredModels(year, movementModes,networkMask); - double testWeight = cRec.totalModelWeight(validModels, early, year, cRec.isOmni() ? user : fRec, roles, roleStrictness, ratingLevel, numRatingLevels); + double testWeight = cRec.totalModelWeight(validModels, + early, + year, + late, + cRec.isOmni() ? user : fRec, + roles, + roleStrictness, + ratingLevel, + numRatingLevels); double totalModelWeight = cRec.totalModelWeight(early, cRec.isOmni() ? user : fRec); From 56bc56db0ec8486afabecfc6384c206b57a1f04c Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 29 Nov 2024 21:07:37 -0700 Subject: [PATCH 07/26] Cache individual model weights so they don't have to be calculated twice --- .../client/ratgenerator/ChassisRecord.java | 12 ++++++++++-- .../client/ratgenerator/RATGenerator.java | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index e124a94a248..0bc04837049 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -114,7 +114,8 @@ public double totalModelWeight(HashSet validModels, Collection roles, int roleStrictness, int equipRating, - int numRatingLevels) { + int numRatingLevels, + HashMap weightData) { RATGenerator ratGen = RATGenerator.getInstance(); AvailabilityRating avRating, nextAvRating; @@ -123,6 +124,10 @@ public double totalModelWeight(HashSet validModels, double nextRating; Number roleRating; + // Clear any pre-existing weighting data - this should only cover + // the current set of models + weightData.clear(); + // For each model for (ModelRecord curModel : validModels) { @@ -169,8 +174,11 @@ public double totalModelWeight(HashSet validModels, } // Calculate the weight and add it to the total - retVal += AvailabilityRating.calcWeight(roleRating.doubleValue()); + adjRating = AvailabilityRating.calcWeight(roleRating.doubleValue()); + retVal += adjRating; + // Cache the final availability rating + weightData.put(curModel.getKey(), adjRating); } return retVal; diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 624710ebd4b..bf012e3ecf5 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -575,6 +575,8 @@ public List generateTable(FactionRecord fRec, // Apply basic filters to models before summing the total weight HashSet validModels = cRec.getFilteredModels(year, movementModes,networkMask); + HashMap modelWeights = new HashMap<>(); + double testWeight = cRec.totalModelWeight(validModels, early, year, @@ -583,7 +585,20 @@ public List generateTable(FactionRecord fRec, roles, roleStrictness, ratingLevel, - numRatingLevels); + numRatingLevels, + modelWeights); + + for (ModelRecord curModel : validModels) { + if (!modelWeights.containsKey(curModel.getKey())) { + continue; + } + + // Overall availability is the odds of the chassis multiplied + // by the odds of the model. Note that the chassis weight total + // is calculated later after accounting for salvage. + double curWeight = AvailabilityRating.calcWeight(cAv) * modelWeights.get(curModel.getKey()) / testWeight; + + } double totalModelWeight = cRec.totalModelWeight(early, cRec.isOmni() ? user : fRec); From d3e64c262914be54ea6012cf14ab1265b8160e3a Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sat, 30 Nov 2024 09:52:47 -0700 Subject: [PATCH 08/26] Add preliminary chassis weight class filter and weight class filter to model filter --- .../client/ratgenerator/ChassisRecord.java | 7 +++++++ .../client/ratgenerator/RATGenerator.java | 17 ++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index 0bc04837049..4f1ff764801 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -57,11 +57,13 @@ public List getSortedModels() { * Generate a list of models for this chassis based on certain criteria. * Early prototypes may be available one year before official introduction. * @param exactYear game year + * @param validWeightClasses restrict weight class to one or more classes * @param movementModes movement mode types, may be null or empty * @param networkMask specific C3 network equipment * @return set of models which pass the filter requirements, may be empty */ public HashSet getFilteredModels(int exactYear, + Collection validWeightClasses, Collection movementModes, int networkMask) { @@ -73,6 +75,11 @@ public HashSet getFilteredModels(int exactYear, continue; } + // Weight class check + if (validWeightClasses != null && !validWeightClasses.isEmpty() && !validWeightClasses.contains(curModel.getWeightClass())) { + continue; + } + // Movement mode check if (movementModes != null && !movementModes.isEmpty()) { if (!movementModes.contains(curModel.getMovementMode())) { diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index bf012e3ecf5..4778cd059bd 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -541,6 +541,7 @@ public List generateTable(FactionRecord fRec, ratingLevel = factionRatings.indexOf(rating); } + // Iterate through all available chassis for (String chassisKey : chassisIndex.get(early).keySet()) { ChassisRecord cRec = chassis.get(chassisKey); if (cRec == null) { @@ -548,11 +549,12 @@ public List generateTable(FactionRecord fRec, continue; } + // Handle ChassisRecords saved as "AERO" units as ASFs for now if (Arrays.asList(UnitType.AERO, UnitType.AEROSPACEFIGHTER).contains(cRec.getUnitType())) { - // Handle ChassisRecords saved as "AERO" units as ASFs for now cRec.setUnitType(UnitType.AEROSPACEFIGHTER); } + // Only return VTOLs when specifically requesting the unit type if (cRec.getUnitType() != unitType && !(unitType == UnitType.TANK && cRec.getUnitType() == UnitType.VTOL @@ -560,7 +562,15 @@ public List generateTable(FactionRecord fRec, continue; } - // TODO: add weight class filtering here + // Preliminary filtering by weight class. Most units that have a weight + // class are the same for all models although a few outliers exist, so + // just look for the first. + if (weightClasses != null && !weightClasses.isEmpty()) { + boolean validChassis = Arrays.stream(cRec.getModels().stream().mapToInt(ModelRecord::getWeightClass).toArray()).anyMatch(weightClasses::contains); + if (!validChassis) { + continue; + } + } AvailabilityRating ar = findChassisAvailabilityRecord(early, chassisKey, fRec, year); if (ar == null) { @@ -573,7 +583,8 @@ public List generateTable(FactionRecord fRec, if (cAv > 0) { // Apply basic filters to models before summing the total weight - HashSet validModels = cRec.getFilteredModels(year, movementModes,networkMask); + HashSet validModels = cRec.getFilteredModels(year, + weightClasses, movementModes, networkMask); HashMap modelWeights = new HashMap<>(); From 148943d413f351bfaf348bbeccad1a3979c1cc9c Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sat, 30 Nov 2024 10:24:41 -0700 Subject: [PATCH 09/26] Replace model weight calculations with new process --- .../client/ratgenerator/RATGenerator.java | 52 +++++-------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 4778cd059bd..3bf6e4540ba 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -588,7 +588,7 @@ public List generateTable(FactionRecord fRec, HashMap modelWeights = new HashMap<>(); - double testWeight = cRec.totalModelWeight(validModels, + double totalWeight = cRec.totalModelWeight(validModels, early, year, late, @@ -599,48 +599,24 @@ public List generateTable(FactionRecord fRec, numRatingLevels, modelWeights); - for (ModelRecord curModel : validModels) { - if (!modelWeights.containsKey(curModel.getKey())) { - continue; - } + if (totalWeight > 0 && !modelWeights.isEmpty()) { + for (ModelRecord curModel : validModels) { + if (!modelWeights.containsKey(curModel.getKey())) { + continue; + } - // Overall availability is the odds of the chassis multiplied - // by the odds of the model. Note that the chassis weight total - // is calculated later after accounting for salvage. - double curWeight = AvailabilityRating.calcWeight(cAv) * modelWeights.get(curModel.getKey()) / testWeight; + // Overall availability is the odds of the chassis multiplied + // by the odds of the model. Note that the chassis weight total + // is factored later after accounting for salvage. + double curWeight = AvailabilityRating.calcWeight(cAv) * modelWeights.get(curModel.getKey()) / totalWeight; - } - - double totalModelWeight = cRec.totalModelWeight(early, - cRec.isOmni() ? user : fRec); - for (ModelRecord mRec : cRec.getModels()) { - if (mRec.getIntroYear() > year - || (!weightClasses.isEmpty() - && !weightClasses.contains(mRec.getWeightClass())) - || (networkMask & mRec.getNetworkMask()) != networkMask) { - continue; - } - - if (!movementModes.isEmpty() && !movementModes.contains(mRec.getMovementMode())) { - continue; - } - ar = findModelAvailabilityRecord(early, mRec.getKey(), fRec); - if ((ar == null) || (ar.getAvailability() == 0)) { - continue; - } - double mAv = mRec.calcAvailability(ar, ratingLevel, numRatingLevels, early); - mAv = interpolate(mAv, - mRec.calcAvailability(ar, ratingLevel, numRatingLevels, late), - Math.max(early, mRec.getIntroYear()), late, year); - Double adjMAv = MissionRole.adjustAvailabilityByRole(mAv, roles, mRec, year, roleStrictness); - if (adjMAv != null) { - double mWt = AvailabilityRating.calcWeight(adjMAv) / totalModelWeight - * AvailabilityRating.calcWeight(cAv); - if (mWt > 0) { - unitWeights.put(mRec, mWt); + // Add the random selection weight for this specific model to the tracker + if (curWeight > 0) { + unitWeights.put(curModel, curWeight); } } } + } } From 3a91cf64e68f665cef1aea99ad0663df356ebcf6 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sat, 30 Nov 2024 10:36:11 -0700 Subject: [PATCH 10/26] Readability improvements --- .../client/ratgenerator/RATGenerator.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 3bf6e4540ba..c8dcf6d1938 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -475,18 +475,18 @@ private Double interpolate(Number av1, Number av2, int year1, int year2, int now } /** - * Generate a frequency table for random selection of units, given a range of parameters - * @param fRec - * @param unitType - * @param year - * @param rating - * @param weightClasses - * @param networkMask - * @param movementModes - * @param roles - * @param roleStrictness - * @param user - * @return + * Generate random selection table entries, given a range of parameters + * @param fRec faction data for selecting units + * @param unitType type of unit (Mek, conventional infantry, etc.) + * @param year current game year + * @param rating equipment rating, typically A/B/C/D/F with A best and F worst + * @param weightClasses which weight classes to select, empty or null for all + * @param networkMask type of C3 system required, 0 for none + * @param movementModes which movement types to select, empty or null for all + * @param roles apply force generator roles when calculating random selection weights + * @param roleStrictness how strictly to apply roles, 0 (none) or higher (more) + * @param user used with OmniMek and salvage balancing + * @return list of entries suitable for building a random generation table, may be empty */ public List generateTable(FactionRecord fRec, int unitType, @@ -508,16 +508,16 @@ public List generateTable(FactionRecord fRec, fRec = new FactionRecord(); } - Integer early = eraSet.floor(year); - if (early == null) { - early = eraSet.first(); + Integer currentEra = eraSet.floor(year); + if (currentEra == null) { + currentEra = eraSet.first(); } - Integer late = null; + Integer nextEra = null; if (!eraSet.contains(year)) { - late = eraSet.ceiling(year); + nextEra = eraSet.ceiling(year); } - if (late == null) { - late = early; + if (nextEra == null) { + nextEra = currentEra; } /* @@ -542,7 +542,7 @@ public List generateTable(FactionRecord fRec, } // Iterate through all available chassis - for (String chassisKey : chassisIndex.get(early).keySet()) { + for (String chassisKey : chassisIndex.get(currentEra).keySet()) { ChassisRecord cRec = chassis.get(chassisKey); if (cRec == null) { logger.error("Could not locate chassis " + chassisKey); @@ -572,14 +572,14 @@ public List generateTable(FactionRecord fRec, } } - AvailabilityRating ar = findChassisAvailabilityRecord(early, chassisKey, fRec, year); + AvailabilityRating ar = findChassisAvailabilityRecord(currentEra, chassisKey, fRec, year); if (ar == null) { continue; } - double cAv = cRec.calcAvailability(ar, ratingLevel, numRatingLevels, early); + double cAv = cRec.calcAvailability(ar, ratingLevel, numRatingLevels, currentEra); cAv = interpolate(cAv, - cRec.calcAvailability(ar, ratingLevel, numRatingLevels, late), - Math.max(early, cRec.getIntroYear()), late, year); + cRec.calcAvailability(ar, ratingLevel, numRatingLevels, nextEra), + Math.max(currentEra, cRec.getIntroYear()), nextEra, year); if (cAv > 0) { // Apply basic filters to models before summing the total weight @@ -589,9 +589,9 @@ public List generateTable(FactionRecord fRec, HashMap modelWeights = new HashMap<>(); double totalWeight = cRec.totalModelWeight(validModels, - early, + currentEra, year, - late, + nextEra, cRec.isOmni() ? user : fRec, roles, roleStrictness, @@ -630,7 +630,7 @@ public List generateTable(FactionRecord fRec, if (weightClasses.size() > 1) { // Get standard weight class distribution for faction - ArrayList wcd = fRec.getWeightDistribution(early, unitType); + ArrayList wcd = fRec.getWeightDistribution(currentEra, unitType); if ((wcd != null) && !wcd.isEmpty()) { // Ultra-light and superheavy are too rare to warrant their own values and for @@ -670,25 +670,25 @@ public List generateTable(FactionRecord fRec, double total = unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); - if (fRec.getPctSalvage(early) != null) { + if (fRec.getPctSalvage(currentEra) != null) { HashMap salvageEntries = new HashMap<>(); - for (Entry entry : fRec.getSalvage(early).entrySet()) { + for (Entry entry : fRec.getSalvage(currentEra).entrySet()) { salvageEntries.put(entry.getKey(), interpolate(entry.getValue(), - fRec.getSalvage(late).get(entry.getKey()), - early, late, year)); + fRec.getSalvage(nextEra).get(entry.getKey()), + currentEra, nextEra, year)); } - if (!late.equals(early)) { - for (Entry entry : fRec.getSalvage(late).entrySet()) { + if (!nextEra.equals(currentEra)) { + for (Entry entry : fRec.getSalvage(nextEra).entrySet()) { if (!salvageEntries.containsKey(entry.getKey())) { salvageEntries.put(entry.getKey(), interpolate(0.0, - entry.getValue(), early, late, year)); + entry.getValue(), currentEra, nextEra, year)); } } } - double salvage = fRec.getPctSalvage(early); + double salvage = fRec.getPctSalvage(currentEra); if (salvage >= 100) { salvage = total; unitWeights.clear(); @@ -711,7 +711,7 @@ public List generateTable(FactionRecord fRec, } if (ratingLevel >= 0) { - adjustForRating(fRec, unitType, year, ratingLevel, unitWeights, salvageWeights, early, late); + adjustForRating(fRec, unitType, year, ratingLevel, unitWeights, salvageWeights, currentEra, nextEra); } // Increase weights if necessary to keep smallest from rounding down to zero From a8232df11a9299ac4d3f78711533b97888effc69 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sat, 30 Nov 2024 17:15:35 -0700 Subject: [PATCH 11/26] Improve chassis processing, skipping units not introduced yet plus better interpolation processing --- .../client/ratgenerator/RATGenerator.java | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index c8dcf6d1938..f4ea6a07763 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -549,6 +549,11 @@ public List generateTable(FactionRecord fRec, continue; } + // Pre-production prototypes may show up one year before official introduction + if (cRec.introYear > year + 1) { + continue; + } + // Handle ChassisRecords saved as "AERO" units as ASFs for now if (Arrays.asList(UnitType.AERO, UnitType.AEROSPACEFIGHTER).contains(cRec.getUnitType())) { cRec.setUnitType(UnitType.AEROSPACEFIGHTER); @@ -566,21 +571,56 @@ public List generateTable(FactionRecord fRec, // class are the same for all models although a few outliers exist, so // just look for the first. if (weightClasses != null && !weightClasses.isEmpty()) { - boolean validChassis = Arrays.stream(cRec.getModels().stream().mapToInt(ModelRecord::getWeightClass).toArray()).anyMatch(weightClasses::contains); + boolean validChassis = Arrays.stream(cRec.getModels(). + stream(). + mapToInt(ModelRecord::getWeightClass). + toArray()). + anyMatch(weightClasses::contains); if (!validChassis) { continue; } } - AvailabilityRating ar = findChassisAvailabilityRecord(currentEra, chassisKey, fRec, year); - if (ar == null) { + AvailabilityRating chassisAvRating = findChassisAvailabilityRecord(currentEra, + chassisKey, fRec, year); + if (chassisAvRating == null) { continue; } - double cAv = cRec.calcAvailability(ar, ratingLevel, numRatingLevels, currentEra); - cAv = interpolate(cAv, - cRec.calcAvailability(ar, ratingLevel, numRatingLevels, nextEra), - Math.max(currentEra, cRec.getIntroYear()), nextEra, year); - if (cAv > 0) { + + double chassisAdjRating; + + // If necessary, interpolate chassis availability between era values + if (year != currentEra && year != nextEra) { + + // Find the chassis availability at the start of the era, or at + // intro date, including dynamic modifiers + chassisAdjRating = cRec.calcAvailability(chassisAvRating, + ratingLevel, + numRatingLevels, + Math.max(currentEra, Math.min(year, cRec.introYear))); + + AvailabilityRating chassisNextAvRating = findChassisAvailabilityRecord(nextEra, chassisKey, fRec, nextEra); + + double chassisNextAdj = 0.0; + if (chassisNextAvRating != null) { + chassisNextAdj = cRec.calcAvailability(chassisNextAvRating, ratingLevel, numRatingLevels, nextEra); + } + + if (chassisAdjRating != chassisNextAdj) { + chassisAdjRating = interpolate( + chassisAdjRating, + chassisNextAdj, + Math.max(currentEra, Math.min(year, cRec.introYear)), nextEra, year); + } + + } else { + // Find the chassis availability taking into account +/- dynamic + // modifiers and introduction year + chassisAdjRating = cRec.calcAvailability(chassisAvRating, + ratingLevel, numRatingLevels, year); + } + + if (chassisAdjRating > 0) { // Apply basic filters to models before summing the total weight HashSet validModels = cRec.getFilteredModels(year, @@ -608,7 +648,7 @@ public List generateTable(FactionRecord fRec, // Overall availability is the odds of the chassis multiplied // by the odds of the model. Note that the chassis weight total // is factored later after accounting for salvage. - double curWeight = AvailabilityRating.calcWeight(cAv) * modelWeights.get(curModel.getKey()) / totalWeight; + double curWeight = AvailabilityRating.calcWeight(chassisAdjRating) * modelWeights.get(curModel.getKey()) / totalWeight; // Add the random selection weight for this specific model to the tracker if (curWeight > 0) { From a5c5ec0498a3cc2b1b695a486acae240ba74177a Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 00:16:48 -0700 Subject: [PATCH 12/26] Improve model interpolation, including phase out/model not present in next era --- .../client/ratgenerator/ChassisRecord.java | 38 +++++++++++-------- .../client/ratgenerator/RATGenerator.java | 9 +++-- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index 4f1ff764801..d68e65b614f 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -103,7 +103,7 @@ public HashSet getFilteredModels(int exactYear, * +/- dynamic adjustment, intro year adjustment, interpolation, and role * modifications. * @param validModels models to add up - * @param era year for current era + * @param currentEra year for current era * @param exactYear current year in game * @param nextEra start date of next era after the current one * @param fRec faction data @@ -114,7 +114,7 @@ public HashSet getFilteredModels(int exactYear, * @return sum of calculated weights of all models of this chassis */ public double totalModelWeight(HashSet validModels, - int era, + int currentEra, int exactYear, int nextEra, FactionRecord fRec, @@ -144,32 +144,40 @@ public double totalModelWeight(HashSet validModels, // Get the availability rating for the provided faction and year, // skip processing if not available - avRating = ratGen.findModelAvailabilityRecord(era, curModel.getKey(), fRec); + avRating = ratGen.findModelAvailabilityRecord(currentEra, curModel.getKey(), fRec); if (avRating == null || avRating.getAvailability() <= 0) { continue; } - // Adjust availability for +/- dynamic and intro year - adjRating = curModel.calcAvailability(avRating, equipRating, numRatingLevels, exactYear); - if (adjRating <= 0) { - continue; - } - // If required, interpolate availability between era start or intro date // (whichever is later), and start of next era - if (exactYear > era && era != nextEra) { + if (exactYear > currentEra && currentEra != nextEra) { nextAvRating = ratGen.findModelAvailabilityRecord(nextEra, curModel.getKey(), fRec); + int interpolationStart = Math.max(currentEra, Math.min(exactYear, curModel.introYear)); + + adjRating = curModel.calcAvailability(avRating, + equipRating, numRatingLevels, interpolationStart); + + nextRating = 0.0; if (nextAvRating != null) { + nextRating = curModel.calcAvailability(nextAvRating, + equipRating, numRatingLevels, nextEra); + } - int interpolationStart = Math.max(curModel.introYear, era); - nextRating = curModel.calcAvailability(nextAvRating, equipRating, numRatingLevels, nextEra); - if (adjRating != nextRating) { - adjRating = adjRating + (nextRating - adjRating) * (exactYear - interpolationStart) / (nextEra - interpolationStart); - } + if (adjRating != nextRating) { + adjRating = adjRating + + (nextRating - adjRating) * (exactYear - interpolationStart) / (nextEra - interpolationStart); } + } else { + // Adjust availability for +/- dynamic and intro year + adjRating = curModel.calcAvailability(avRating, equipRating, numRatingLevels, exactYear); + } + + if (adjRating <= 0) { + continue; } // Adjust availability for roles. Method may return null as a filtering mechanism. diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index f4ea6a07763..e67dee63043 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -591,15 +591,16 @@ public List generateTable(FactionRecord fRec, // If necessary, interpolate chassis availability between era values if (year != currentEra && year != nextEra) { + AvailabilityRating chassisNextAvRating = findChassisAvailabilityRecord(nextEra, chassisKey, fRec, nextEra); // Find the chassis availability at the start of the era, or at // intro date, including dynamic modifiers + int interpolationStart = Math.max(currentEra, Math.min(year, cRec.introYear)); chassisAdjRating = cRec.calcAvailability(chassisAvRating, ratingLevel, numRatingLevels, - Math.max(currentEra, Math.min(year, cRec.introYear))); + interpolationStart); - AvailabilityRating chassisNextAvRating = findChassisAvailabilityRecord(nextEra, chassisKey, fRec, nextEra); double chassisNextAdj = 0.0; if (chassisNextAvRating != null) { @@ -610,7 +611,9 @@ public List generateTable(FactionRecord fRec, chassisAdjRating = interpolate( chassisAdjRating, chassisNextAdj, - Math.max(currentEra, Math.min(year, cRec.introYear)), nextEra, year); + interpolationStart, + nextEra, + year); } } else { From e29077ac0531cd8ea538298dd2e1d9951a219372 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 10:21:25 -0700 Subject: [PATCH 13/26] Change original total model weight calculation to natively work with doubles instead of in, due to 2^(n/2) generating non-integers on odd values of n --- megamek/src/megamek/client/ratgenerator/ChassisRecord.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java index d68e65b614f..a3909cac3fe 100644 --- a/megamek/src/megamek/client/ratgenerator/ChassisRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ChassisRecord.java @@ -199,7 +199,7 @@ public double totalModelWeight(HashSet validModels, return retVal; } - public int totalModelWeight(int era, String fKey) { + public double totalModelWeight(int era, String fKey) { FactionRecord fRec = RATGenerator.getInstance().getFaction(fKey); if (fRec == null) { logger.warn("Attempt to find totalModelWeight for non-existent faction " + fKey); @@ -208,8 +208,8 @@ public int totalModelWeight(int era, String fKey) { return totalModelWeight(era, fRec); } - public int totalModelWeight(int era, FactionRecord fRec) { - int retVal = 0; + public double totalModelWeight(int era, FactionRecord fRec) { + double retVal = 0; RATGenerator ratGen = RATGenerator.getInstance(); for (ModelRecord curModel : models) { From 95aca30e79ea4c87145254bc51ae8683f392fbdf Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 10:25:15 -0700 Subject: [PATCH 14/26] Null protection for weight class redistribution section check --- megamek/src/megamek/client/ratgenerator/RATGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index e67dee63043..99518513d38 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -670,7 +670,7 @@ public List generateTable(FactionRecord fRec, // If there is more than one weight class and the faction record (or parent) // indicates a certain distribution of weight classes, adjust the weight value // to conform to the given ratio. - if (weightClasses.size() > 1) { + if (weightClasses != null && weightClasses.size() > 1) { // Get standard weight class distribution for faction ArrayList wcd = fRec.getWeightDistribution(currentEra, unitType); From 2255135ede3a64844a0d82c6acbedd530ac91778 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 10:51:47 -0700 Subject: [PATCH 15/26] Readability improvements to faction weight distribution adjustments --- .../client/ratgenerator/RATGenerator.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 99518513d38..8e905a82c57 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -667,34 +667,37 @@ public List generateTable(FactionRecord fRec, return new ArrayList<>(); } - // If there is more than one weight class and the faction record (or parent) - // indicates a certain distribution of weight classes, adjust the weight value - // to conform to the given ratio. + // If there is more than one weight class being generated and the faction record + // (or parent) indicates a certain distribution of weight classes, adjust the + // generated unit weights conform to the given ratio. if (weightClasses != null && weightClasses.size() > 1) { // Get standard weight class distribution for faction - ArrayList wcd = fRec.getWeightDistribution(currentEra, unitType); + ArrayList weightClassDistribution = fRec.getWeightDistribution(currentEra, + unitType); - if ((wcd != null) && !wcd.isEmpty()) { + if ((weightClassDistribution != null) && !weightClassDistribution.isEmpty()) { // Ultra-light and superheavy are too rare to warrant their own values and for // weight class distribution purposes are grouped with light and assault, // respectively. final int[] wcdIndex = { 0, 0, 1, 2, 3, 3 }; - // Find the totals of the weight for the generated table - double totalMRWeight = unitWeights.values().stream() + // Find the totals of the weights for the generated table + double totalTableWeight = unitWeights.values().stream() .mapToDouble(Double::doubleValue) .sum(); - // Find the sum of the weight distribution values for all weight classes in use. + // Find the sum of the weight distribution values for each weight + // class being called for int totalWCDWeights = weightClasses.stream() - .filter(wc -> wcdIndex[wc] < wcd.size()) - .mapToInt(wc -> wcd.get(wcdIndex[wc])) + .filter(wc -> wcdIndex[wc] < weightClassDistribution.size()) + .mapToInt(wc -> weightClassDistribution.get(wcdIndex[wc])) .sum(); if (totalWCDWeights > 0) { // Group all the models of the generated table by weight class. Function grouper = mr -> wcdIndex[mr.getWeightClass()]; - Map> weightGroups = unitWeights.keySet().stream() - .collect(Collectors.groupingBy(grouper)); + Map> weightGroups = unitWeights. + keySet(). + stream().collect(Collectors.groupingBy(grouper)); // Go through the weight class groups and adjust the table weights so the total // of each group corresponds to the distribution for this faction. @@ -703,7 +706,7 @@ public List generateTable(FactionRecord fRec, .mapToDouble(unitWeights::get) .sum(); if (totalWeight > 0) { - double adj = totalMRWeight * wcd.get(i) / (totalWeight * totalWCDWeights); + double adj = totalTableWeight * weightClassDistribution.get(i) / (totalWeight * totalWCDWeights); weightGroups.get(i).forEach(mr -> unitWeights.merge(mr, adj, (x, y) -> x * y)); } } From 21711b13efb1a1b24198aa61b3670b903157fa17 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 12:20:39 -0700 Subject: [PATCH 16/26] Readability improvements to salvage percentage calculations. Fix application of salvage percentage to conventional math. --- .../client/ratgenerator/RATGenerator.java | 50 +++++++++++++------ 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 8e905a82c57..48ea522387f 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -500,7 +500,6 @@ public List generateTable(FactionRecord fRec, FactionRecord user) { HashMap unitWeights = new HashMap<>(); - HashMap salvageWeights = new HashMap<>(); loadYear(year); @@ -714,44 +713,65 @@ public List generateTable(FactionRecord fRec, } } - double total = unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); - + // If there are salvage percentages defined for the generating faction + HashMap salvageWeights = new HashMap<>(); if (fRec.getPctSalvage(currentEra) != null) { HashMap salvageEntries = new HashMap<>(); + + // If current year is directly on an era data point take it, otherwise + // interpolate between current era and next era values. for (Entry entry : fRec.getSalvage(currentEra).entrySet()) { salvageEntries.put(entry.getKey(), + currentEra == year ? + entry.getValue() : interpolate(entry.getValue(), - fRec.getSalvage(nextEra).get(entry.getKey()), - currentEra, nextEra, year)); + fRec.getSalvage(nextEra).get(entry.getKey()), + currentEra, + nextEra, + year)); } + // Add salvage from the next era that is not already present if (!nextEra.equals(currentEra)) { for (Entry entry : fRec.getSalvage(nextEra).entrySet()) { if (!salvageEntries.containsKey(entry.getKey())) { - salvageEntries.put(entry.getKey(), interpolate(0.0, - entry.getValue(), currentEra, nextEra, year)); + salvageEntries.put(entry.getKey(), + interpolate(0.0, + entry.getValue(), + currentEra, + nextEra, + year)); } } } - double salvage = fRec.getPctSalvage(currentEra); - if (salvage >= 100) { - salvage = total; + // Use the total salvage percentage from the faction data to get the total + // weight of all salvage entries, from the current overall table weight. + // If a salvage percentage of 100 percent is specified (unlikely, but possible) + // then clear the existing table and regenerate everything again based purely + // on salvage. + double totalTableWeight = unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); + double overallSalvage = fRec.getPctSalvage(currentEra); + if (overallSalvage >= 100) { + overallSalvage = totalTableWeight; unitWeights.clear(); } else { - salvage = salvage * total / (100 - salvage); + overallSalvage = totalTableWeight * overallSalvage / 100.0; } + + // Break down the total salvage weight by relative weights of each + // provided salvage faction double totalFactionWeight = salvageEntries.values().stream() .mapToDouble(Double::doubleValue) .sum(); for (String fKey : salvageEntries.keySet()) { FactionRecord salvageFaction = factions.get(fKey); if (salvageFaction == null) { - logger.debug("Could not locate faction " + fKey - + " for " + fRec.getKey() + " salvage"); + logger.debug("Could not locate faction {} for {} salvage.", + fKey, fRec.getKey()); } else { - double wt = salvage * salvageEntries.get(fKey) / totalFactionWeight; - salvageWeights.put(salvageFaction, wt); + double factionSalvageWeight = overallSalvage * salvageEntries.get(fKey) / totalFactionWeight; + salvageWeights.put(salvageFaction, factionSalvageWeight); } } } From 6bda293817f41bbd379cf47dda426ac8d29cc6be Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 14:27:01 -0700 Subject: [PATCH 17/26] Readability improvements to percentage adjustments --- .../client/ratgenerator/RATGenerator.java | 181 +++++++++++------- 1 file changed, 112 insertions(+), 69 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 48ea522387f..a35ae9510ee 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -776,11 +776,14 @@ public List generateTable(FactionRecord fRec, } } + // Adjust weights of standard table entries and salvage entries for established + // percentages of Omni-units, base Clan tech, and Star League/advanced tech if (ratingLevel >= 0) { adjustForRating(fRec, unitType, year, ratingLevel, unitWeights, salvageWeights, currentEra, nextEra); } - // Increase weights if necessary to keep smallest from rounding down to zero + // Incorporate the salvage entries with the unit entries. Then re-calculate + // weights as necessary to keep the range of values between 0 and 1000. double adj = 1.0; DoubleSummaryStatistics stats = Stream .concat(salvageWeights.values().stream(), unitWeights.values().stream()) @@ -811,54 +814,82 @@ public List generateTable(FactionRecord fRec, return retVal; } - private void adjustForRating(FactionRecord fRec, int unitType, int year, int rating, - Map unitWeights, - Map salvageWeights, Integer early, - Integer late) { - double total = 0.0; - double totalOmni = 0.0; - double totalClan = 0.0; - double totalSL = 0.0; + /** + * Adjust weighted random selection value based on percentage values for + * Omni-units, Clan-tech units, and Star League/advanced tech units from + * faction data. The {@code unitWeights} and {@code salvageWeights} parameters + * are modified rather than returning a single unified set. + * @param fRec faction used to generate units + * @param unitType type of unit being generated + * @param year current game year + * @param rating equipment rating based on available range, typically F (0)/D/C/B/A (4) + * @param unitWeights random frequency rates (entries for the table), excluding salvage + * @param salvageWeights random frequency rates of salvaged units by faction + * @param currentEra current era + * @param nextEra next era + */ + private void adjustForRating(FactionRecord fRec, + int unitType, + int year, + int rating, + Map unitWeights, + Map salvageWeights, + Integer currentEra, + Integer nextEra) { + + double totalWeight = 0.0; + double totalOmniWeight = 0.0; + double totalClanWeight = 0.0; + double totalSLWeight = 0.0; + + // Total the unit weight of all selected units, plus get totals + // of all Omni-units, base Clan-tech units, and Star League/advanced + // tech units for (Entry entry : unitWeights.entrySet()) { - total += entry.getValue(); + totalWeight += entry.getValue(); if (entry.getKey().isOmni()) { - totalOmni += entry.getValue(); + totalOmniWeight += entry.getValue(); } if (entry.getKey().isClan()) { - totalClan += entry.getValue(); + totalClanWeight += entry.getValue(); } else if (entry.getKey().isSL()) { - totalSL += entry.getValue(); + totalSLWeight += entry.getValue(); } } + Double pctOmni = null; Double pctNonOmni = null; Double pctSL = null; Double pctClan = null; Double pctOther = null; + + // Get the desired percentages from faction data, and interpolate between + // eras if needed if (unitType == UnitType.MEK) { - pctOmni = interpolate(fRec.findPctTech(TechCategory.OMNI, early, rating), - fRec.findPctTech(TechCategory.OMNI, late, rating), early, late, year); - pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN, early, rating), - fRec.findPctTech(TechCategory.CLAN, late, rating), early, late, year); - pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED, early, rating), - fRec.findPctTech(TechCategory.IS_ADVANCED, late, rating), early, late, year); + pctOmni = interpolate(fRec.findPctTech(TechCategory.OMNI, currentEra, rating), + fRec.findPctTech(TechCategory.OMNI, nextEra, rating), currentEra, nextEra, year); + pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN, currentEra, rating), + fRec.findPctTech(TechCategory.CLAN, nextEra, rating), currentEra, nextEra, year); + pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED, currentEra, rating), + fRec.findPctTech(TechCategory.IS_ADVANCED, nextEra, rating), currentEra, nextEra, year); } if (unitType == UnitType.AEROSPACEFIGHTER) { - pctOmni = interpolate(fRec.findPctTech(TechCategory.OMNI_AERO, early, rating), - fRec.findPctTech(TechCategory.OMNI_AERO, late, rating), early, late, year); - pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN_AERO, early, rating), - fRec.findPctTech(TechCategory.CLAN_AERO, late, rating), early, late, year); - pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED_AERO, early, rating), - fRec.findPctTech(TechCategory.IS_ADVANCED_AERO, late, rating), early, late, year); + pctOmni = interpolate(fRec.findPctTech(TechCategory.OMNI_AERO, currentEra, rating), + fRec.findPctTech(TechCategory.OMNI_AERO, nextEra, rating), currentEra, nextEra, year); + pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN_AERO, currentEra, rating), + fRec.findPctTech(TechCategory.CLAN_AERO, nextEra, rating), currentEra, nextEra, year); + pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED_AERO, currentEra, rating), + fRec.findPctTech(TechCategory.IS_ADVANCED_AERO, nextEra, rating), currentEra, nextEra, year); } if (unitType == UnitType.TANK || unitType == UnitType.VTOL) { - pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN_VEE, early, rating), - fRec.findPctTech(TechCategory.CLAN_VEE, late, rating), early, late, year); - pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, early, rating), - fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, late, rating), early, late, year); + pctClan = interpolate(fRec.findPctTech(TechCategory.CLAN_VEE, currentEra, rating), + fRec.findPctTech(TechCategory.CLAN_VEE, nextEra, rating), currentEra, nextEra, year); + pctSL = interpolate(fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, currentEra, rating), + fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, nextEra, rating), currentEra, nextEra, year); } - /* Adjust for lack of precision in post-FM:Updates extrapolations */ + // Use margin values from the faction data to adjust for lack of precision + // in post-FM:Updates era extrapolations if (pctSL != null || pctClan != null) { pctOther = 100.0; if (pctSL != null) { @@ -868,11 +899,11 @@ private void adjustForRating(FactionRecord fRec, int unitType, int year, int rat if (pctClan != null) { pctOther -= pctClan; } - Double techMargin = interpolate(fRec.getTechMargin(early), fRec.getTechMargin(late), - early, late, year); + Double techMargin = interpolate(fRec.getTechMargin(currentEra), + fRec.getTechMargin(nextEra), currentEra, nextEra, year); if (techMargin != null && techMargin > 0) { if (pctClan != null) { - double pct = 100.0 * totalClan / total; + double pct = 100.0 * totalClanWeight / totalWeight; if (pct < pctClan - techMargin) { pctClan -= techMargin; } else if (pct > pctClan + techMargin) { @@ -881,7 +912,7 @@ private void adjustForRating(FactionRecord fRec, int unitType, int year, int rat } if (pctSL != null) { - double pct = 100.0 * totalSL / total; + double pct = 100.0 * totalSLWeight / totalWeight; if (pct < pctSL - techMargin) { pctSL -= techMargin; } else if (pct > pctSL + techMargin) { @@ -890,22 +921,22 @@ private void adjustForRating(FactionRecord fRec, int unitType, int year, int rat } } - Double upgradeMargin = interpolate(fRec.getUpgradeMargin(early), - fRec.getUpgradeMargin(late), early, late, year); + Double upgradeMargin = interpolate(fRec.getUpgradeMargin(currentEra), + fRec.getUpgradeMargin(nextEra), currentEra, nextEra, year); if ((upgradeMargin != null) && (upgradeMargin > 0)) { - double pct = 100.0 * (total - totalClan - totalSL) / total; + double pct = 100.0 * (totalWeight - totalClanWeight - totalSLWeight) / totalWeight; if (pct < pctOther - upgradeMargin) { pctOther -= upgradeMargin; } else if (pct > pctOther + upgradeMargin) { pctOther += upgradeMargin; } - /* - * If clan, sl, and other are all adjusted, the values probably - * don't add up to 100, which is fine unless the upgradeMargin is - * <= techMargin. Then pctOther is more certain, and we adjust - * the values of clan and sl to keep the value of "other" equal to - * a percentage. - */ + + // If Clan-tech, Star League tech, and Other are all adjusted, the + // values probably don't add up to 100. This is acceptable unless + // the upgradeMargin is less than techMargin. Then pctOther is more + // certain, and we adjust the values of Clan-tech and Star League + // tech to keep the value of Other equal to a percentage. + if (techMargin != null) { if (upgradeMargin <= techMargin) { if (pctClan == null || pctClan == 0) { @@ -918,58 +949,70 @@ private void adjustForRating(FactionRecord fRec, int unitType, int year, int rat } } } + } } + + // Adjust Omni-unit percentage by margin values from faction data if (pctOmni != null) { - Double omniMargin = interpolate(fRec.getOmniMargin(early), fRec.getOmniMargin(late), - early, late, year); - if ((omniMargin != null) && (omniMargin > 0)) { - double pct = 100.0 * totalOmni / total; + Double omniMargin = interpolate(fRec.getOmniMargin(currentEra), fRec.getOmniMargin(nextEra), + currentEra, nextEra, year); + + if (omniMargin != null && omniMargin > 0) { + double pct = 100.0 * totalOmniWeight / totalWeight; if (pct < pctOmni - omniMargin) { pctOmni -= omniMargin; } else if (pct > pctOmni + omniMargin) { pctOmni += omniMargin; } } + pctNonOmni = 100.0 - pctOmni; } // For non-Clan factions, the amount of salvage from Clan factions is part of // the overall Clan percentage. - if (!fRec.isClan() && (pctClan != null) && (totalClan > 0)) { - double clanSalvage = salvageWeights.keySet().stream().filter(FactionRecord::isClan) - .mapToDouble(salvageWeights::get).sum(); - total += clanSalvage; - totalClan += clanSalvage; + if (!fRec.isClan() && (pctClan != null) && (totalClanWeight > 0)) { + double clanSalvage = salvageWeights. + keySet(). + stream(). + filter(FactionRecord::isClan). + mapToDouble(salvageWeights::get). + sum(); + + totalWeight += clanSalvage; + totalClanWeight += clanSalvage; for (FactionRecord fr : salvageWeights.keySet()) { if (fr.isClan()) { - salvageWeights.put(fr, salvageWeights.get(fr) - * (pctClan / 100.0) * (total / totalClan)); + salvageWeights.put(fr, salvageWeights.get(fr) * (pctClan / 100.0) * (totalWeight / totalClanWeight)); } } + } - double totalOther = total - totalClan - totalSL; + + // Anything not base Clan or Star League/advanced tech is Other/basic tech + double totalOther = totalWeight - totalClanWeight - totalSLWeight; for (ModelRecord mRec : unitWeights.keySet()) { - if (pctOmni != null && mRec.isOmni() && totalOmni < total) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctOmni / 100.0) * (total / totalOmni)); + if (pctOmni != null && mRec.isOmni() && totalOmniWeight < totalWeight) { + unitWeights.put(mRec, unitWeights.get(mRec) * (pctOmni / 100.0) * (totalWeight / totalOmniWeight)); } - if (pctNonOmni != null && !mRec.isOmni() && totalOmni > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctNonOmni / 100.0) * (total / (total - totalOmni))); + if (pctNonOmni != null && !mRec.isOmni() && totalOmniWeight > 0) { + unitWeights.put(mRec, unitWeights.get(mRec) * (pctNonOmni / 100.0) * (totalWeight / (totalWeight - totalOmniWeight))); } - if (pctSL != null && mRec.isSL() - && totalSL > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctSL / 100.0) * (total / totalSL)); + if (pctSL != null && mRec.isSL() && totalSLWeight > 0) { + unitWeights.put(mRec, unitWeights.get(mRec) * (pctSL / 100.0) * (totalWeight / totalSLWeight)); } - if (pctClan != null && mRec.isClan() - && totalClan > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctClan / 100.0) * (total / totalClan)); + if (pctClan != null && mRec.isClan() && totalClanWeight > 0) { + unitWeights.put(mRec, unitWeights.get(mRec) * (pctClan / 100.0) * (totalWeight / totalClanWeight)); } if (pctOther != null && pctOther > 0 && !mRec.isClan() && !mRec.isSL()) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctOther / 100.0) - * (total / totalOther)); + unitWeights.put(mRec, unitWeights.get(mRec) * (pctOther / 100.0) * (totalWeight / totalOther)); } } - double multiplier = total / unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); + + // With modifications, check the new total weight against the original value and + // use it as a multiplier to + double multiplier = totalWeight / unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); for (ModelRecord mRec : unitWeights.keySet()) { unitWeights.merge(mRec, multiplier, (a, b) -> a * b); } From 24e98d4e82c2352826440ee526ca5cc88c424c05 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Sun, 1 Dec 2024 16:16:24 -0700 Subject: [PATCH 18/26] Add unit type and role filtering to C/SL/O adjustment call --- .../client/ratgenerator/RATGenerator.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index a35ae9510ee..e02671eb539 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -777,8 +777,20 @@ public List generateTable(FactionRecord fRec, } // Adjust weights of standard table entries and salvage entries for established - // percentages of Omni-units, base Clan tech, and Star League/advanced tech - if (ratingLevel >= 0) { + // percentages of Omni-units, base Clan tech, and Star League/advanced tech. + // Only do this for Omni-capable unit types, which also covers those which are + // commonly fitted with Clan or Star League/advanced technology. + // Do not re-balance conventional infantry, battle armor, large craft, or other + // unit types. Also skip combat support and civilian specialist roles. + if (ratingLevel >= 0 && + (unitType == UnitType.MEK || + unitType == UnitType.AEROSPACEFIGHTER || + unitType == UnitType.TANK || + unitType == UnitType.VTOL) && + (roles == null || + (!roles.contains(MissionRole.SUPPORT) || + !roles.contains(MissionRole.CIVILIAN) || + !roles.contains(MissionRole.ENGINEER)))){ adjustForRating(fRec, unitType, year, ratingLevel, unitWeights, salvageWeights, currentEra, nextEra); } @@ -1010,8 +1022,8 @@ private void adjustForRating(FactionRecord fRec, } } - // With modifications, check the new total weight against the original value and - // use it as a multiplier to + // After modifications, check the new total weight against the original value and + // use it as a multiplier to compensate double multiplier = totalWeight / unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); for (ModelRecord mRec : unitWeights.keySet()) { unitWeights.merge(mRec, multiplier, (a, b) -> a * b); From 019a85df0018d57691da121ef83ddfbed991ec7b Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Mon, 2 Dec 2024 20:25:10 -0700 Subject: [PATCH 19/26] Integrate chassis total weight into individual weights. Update math for re-balancing Omni-unit proportions. --- .../client/ratgenerator/RATGenerator.java | 122 ++++++++++++++---- 1 file changed, 97 insertions(+), 25 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index e02671eb539..ce23d47ef94 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -72,6 +72,12 @@ public class RATGenerator { private ArrayList listeners; + /** + * Minimum difference between actual percentage and desired percentage of Omni-units + * before weights are re-balanced + */ + private static double MIN_OMNI_DIFFERENCE = 2.5; + protected RATGenerator() { models = new HashMap<>(); chassis = new HashMap<>(); @@ -541,27 +547,28 @@ public List generateTable(FactionRecord fRec, } // Iterate through all available chassis + double chassisWeightTotal = 0.0; for (String chassisKey : chassisIndex.get(currentEra).keySet()) { - ChassisRecord cRec = chassis.get(chassisKey); - if (cRec == null) { - logger.error("Could not locate chassis " + chassisKey); + ChassisRecord curChassis = chassis.get(chassisKey); + if (curChassis == null) { + logger.error("Could not locate chassis {}", chassisKey); continue; } // Pre-production prototypes may show up one year before official introduction - if (cRec.introYear > year + 1) { + if (curChassis.introYear > year + 1) { continue; } // Handle ChassisRecords saved as "AERO" units as ASFs for now - if (Arrays.asList(UnitType.AERO, UnitType.AEROSPACEFIGHTER).contains(cRec.getUnitType())) { - cRec.setUnitType(UnitType.AEROSPACEFIGHTER); + if (Arrays.asList(UnitType.AERO, UnitType.AEROSPACEFIGHTER).contains(curChassis.getUnitType())) { + curChassis.setUnitType(UnitType.AEROSPACEFIGHTER); } // Only return VTOLs when specifically requesting the unit type - if (cRec.getUnitType() != unitType && + if (curChassis.getUnitType() != unitType && !(unitType == UnitType.TANK - && cRec.getUnitType() == UnitType.VTOL + && curChassis.getUnitType() == UnitType.VTOL && movementModes.contains(EntityMovementMode.VTOL))) { continue; } @@ -570,7 +577,7 @@ public List generateTable(FactionRecord fRec, // class are the same for all models although a few outliers exist, so // just look for the first. if (weightClasses != null && !weightClasses.isEmpty()) { - boolean validChassis = Arrays.stream(cRec.getModels(). + boolean validChassis = Arrays.stream(curChassis.getModels(). stream(). mapToInt(ModelRecord::getWeightClass). toArray()). @@ -594,8 +601,8 @@ public List generateTable(FactionRecord fRec, // Find the chassis availability at the start of the era, or at // intro date, including dynamic modifiers - int interpolationStart = Math.max(currentEra, Math.min(year, cRec.introYear)); - chassisAdjRating = cRec.calcAvailability(chassisAvRating, + int interpolationStart = Math.max(currentEra, Math.min(year, curChassis.introYear)); + chassisAdjRating = curChassis.calcAvailability(chassisAvRating, ratingLevel, numRatingLevels, interpolationStart); @@ -603,7 +610,7 @@ public List generateTable(FactionRecord fRec, double chassisNextAdj = 0.0; if (chassisNextAvRating != null) { - chassisNextAdj = cRec.calcAvailability(chassisNextAvRating, ratingLevel, numRatingLevels, nextEra); + chassisNextAdj = curChassis.calcAvailability(chassisNextAvRating, ratingLevel, numRatingLevels, nextEra); } if (chassisAdjRating != chassisNextAdj) { @@ -618,23 +625,23 @@ public List generateTable(FactionRecord fRec, } else { // Find the chassis availability taking into account +/- dynamic // modifiers and introduction year - chassisAdjRating = cRec.calcAvailability(chassisAvRating, + chassisAdjRating = curChassis.calcAvailability(chassisAvRating, ratingLevel, numRatingLevels, year); } if (chassisAdjRating > 0) { // Apply basic filters to models before summing the total weight - HashSet validModels = cRec.getFilteredModels(year, + HashSet validModels = curChassis.getFilteredModels(year, weightClasses, movementModes, networkMask); HashMap modelWeights = new HashMap<>(); - double totalWeight = cRec.totalModelWeight(validModels, + double totalWeight = curChassis.totalModelWeight(validModels, currentEra, year, nextEra, - cRec.isOmni() ? user : fRec, + curChassis.isOmni() ? user : fRec, roles, roleStrictness, ratingLevel, @@ -642,6 +649,8 @@ public List generateTable(FactionRecord fRec, modelWeights); if (totalWeight > 0 && !modelWeights.isEmpty()) { + double chassisWeight = AvailabilityRating.calcWeight(chassisAdjRating); + boolean hasModels = false; for (ModelRecord curModel : validModels) { if (!modelWeights.containsKey(curModel.getKey())) { continue; @@ -649,23 +658,33 @@ public List generateTable(FactionRecord fRec, // Overall availability is the odds of the chassis multiplied // by the odds of the model. Note that the chassis weight total - // is factored later after accounting for salvage. - double curWeight = AvailabilityRating.calcWeight(chassisAdjRating) * modelWeights.get(curModel.getKey()) / totalWeight; + // is factored later after all chassis are processed. + double curWeight = chassisWeight * modelWeights.get(curModel.getKey()) / totalWeight; // Add the random selection weight for this specific model to the tracker if (curWeight > 0) { unitWeights.put(curModel, curWeight); + hasModels = true; } } + + if (hasModels) { + chassisWeightTotal += chassisWeight; + } } } } - if (unitWeights.isEmpty()) { + if (unitWeights.isEmpty() || chassisWeightTotal == 0.0) { return new ArrayList<>(); } + // Factor chassis total into every weight + for (ModelRecord curModel : unitWeights.keySet()) { + unitWeights.merge(curModel, chassisWeightTotal, (a, b) -> 100.0 * a / b); + } + // If there is more than one weight class being generated and the faction record // (or parent) indicates a certain distribution of weight classes, adjust the // generated unit weights conform to the given ratio. @@ -982,6 +1001,59 @@ private void adjustForRating(FactionRecord fRec, pctNonOmni = 100.0 - pctOmni; } + // Get the difference between the ideal Omni level and the current one + // and proportionally assign it to the Omni and non-Omni groups + if (pctOmni == null) { + pctOmni = 0.0; + } + + double omniPctDifference = pctOmni - (100.0 * totalOmniWeight / totalWeight); + + // If there are not enough or too many Omni-units based on the faction data, + // re-balance all the weights to bring them back into line. If the faction + // data specifies Omni-units but none are present, nothing can be done. + double totalWeightPostMod = 0.0; + if (Math.abs(omniPctDifference) > MIN_OMNI_DIFFERENCE && totalOmniWeight > 0.0 && pctOmni >= 0.0) { + + // Non-omni adjustment needs to be opposite sign of Omni percentage adjustment + // i.e. if Omni needs increasing, non-Omni gets decreased; if Omni needs reducing, + // non-Omni needs increasing. + double totalNonOmniWeight = (omniPctDifference > 0 ? 1.0 : -1.0) * (totalOmniWeight - totalWeight); + + // Apply the weight modifications proportionally, using the unit weight relative + // to the weight total of either Omni or non-Omni as appropriate + double curWeight = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (curModel.isOmni()) { + if (pctOmni > 0.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalOmniWeight; + } else { + curWeight = 0.0; + } + } else { + if (pctOmni < 100.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalNonOmniWeight; + } else { + curWeight = 0.0; + } + } + unitWeights.put(curModel, curWeight); + + totalWeightPostMod += curWeight; + } + + } else { + totalWeightPostMod = totalWeight; + } + + + + + + + // For non-Clan factions, the amount of salvage from Clan factions is part of // the overall Clan percentage. if (!fRec.isClan() && (pctClan != null) && (totalClanWeight > 0)) { @@ -1005,12 +1077,12 @@ private void adjustForRating(FactionRecord fRec, // Anything not base Clan or Star League/advanced tech is Other/basic tech double totalOther = totalWeight - totalClanWeight - totalSLWeight; for (ModelRecord mRec : unitWeights.keySet()) { - if (pctOmni != null && mRec.isOmni() && totalOmniWeight < totalWeight) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctOmni / 100.0) * (totalWeight / totalOmniWeight)); - } - if (pctNonOmni != null && !mRec.isOmni() && totalOmniWeight > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctNonOmni / 100.0) * (totalWeight / (totalWeight - totalOmniWeight))); - } +// if (pctOmni != null && mRec.isOmni() && totalOmniWeight < totalWeight) { +// unitWeights.put(mRec, unitWeights.get(mRec) * (pctOmni / 100.0) * (totalWeight / totalOmniWeight)); +// } +// if (pctNonOmni != null && !mRec.isOmni() && totalOmniWeight > 0) { +// unitWeights.put(mRec, unitWeights.get(mRec) * (pctNonOmni / 100.0) * (totalWeight / (totalWeight - totalOmniWeight))); +// } if (pctSL != null && mRec.isSL() && totalSLWeight > 0) { unitWeights.put(mRec, unitWeights.get(mRec) * (pctSL / 100.0) * (totalWeight / totalSLWeight)); } From ce87b8b02232fb796a47f00d93cd2f9f62ecba11 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Tue, 3 Dec 2024 15:59:59 -0700 Subject: [PATCH 20/26] Update calculations for Clan/SL proportions. Not entirely successful due to complexity of managing multiple factors, including salvage. --- .../client/ratgenerator/RATGenerator.java | 265 ++++++++++++------ 1 file changed, 172 insertions(+), 93 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index ce23d47ef94..1a0ddf1f366 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -78,6 +78,17 @@ public class RATGenerator { */ private static double MIN_OMNI_DIFFERENCE = 2.5; + /** + * Minimum difference between actual percentage and desired percentage of base-Clan + * units before weights are re-balanced + */ + private static double MIN_CLAN_DIFFERENCE = 5.0; + /** + * Minimum difference between actual percentage and desired percentage of Star League + * and advanced IS tech units before weights are rebalanced + */ + private static double MIN_SL_DIFFERENCE = 5.0; + protected RATGenerator() { models = new HashMap<>(); chassis = new HashMap<>(); @@ -895,7 +906,8 @@ private void adjustForRating(FactionRecord fRec, Double pctOther = null; // Get the desired percentages from faction data, and interpolate between - // eras if needed + // eras if needed. + // Note that vehicles do not re-balance based on Omni/non-Omni ratios. if (unitType == UnitType.MEK) { pctOmni = interpolate(fRec.findPctTech(TechCategory.OMNI, currentEra, rating), fRec.findPctTech(TechCategory.OMNI, nextEra, rating), currentEra, nextEra, year); @@ -919,6 +931,99 @@ private void adjustForRating(FactionRecord fRec, fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, nextEra, rating), currentEra, nextEra, year); } + // Clan factions generally only field Clan-tech Omni-units, so the desired + // percentage of Omni-units should always be lower than the percentage of base + // Clan units. Trying to do otherwise creates unexpected results. + if (fRec.isClan() && (pctOmni != null && pctClan != null) && pctOmni > pctClan) { + logger.warn("Clan faction {} Clan/SL/Omni rating has higher Omni ({}) than Clan ({}) value in era {}.", fRec.getKey(), pctOmni, pctClan, currentEra); + } + + // Adjust Omni-unit percentage by margin values from faction data + if (pctOmni != null) { + Double omniMargin = interpolate(fRec.getOmniMargin(currentEra), fRec.getOmniMargin(nextEra), + currentEra, nextEra, year); + + if (omniMargin != null && omniMargin > 0) { + double pct = 100.0 * totalOmniWeight / totalWeight; + if (pct < pctOmni - omniMargin) { + pctOmni -= omniMargin; + } else if (pct > pctOmni + omniMargin) { + pctOmni += omniMargin; + } + } + + pctNonOmni = 100.0 - pctOmni; + } + + // Get the difference between the ideal Omni level and the current one + // and proportionally assign it to the Omni and non-Omni groups + if (pctOmni == null) { + pctOmni = 0.0; + } + double omniPctDifference = pctOmni - (100.0 * totalOmniWeight / totalWeight); + + // If there are not enough or too many Omni-units based on the faction data, + // re-balance all the weights to bring them back into line. If the faction + // data specifies Omni-units but none are present, nothing can be done. + double totalWeightPostMod = 0.0; + double totalClanOmniWeight = 0.0; + double totalSLOmniWeight = 0.0; + if (Math.abs(omniPctDifference) > MIN_OMNI_DIFFERENCE && totalOmniWeight > 0.0 && pctOmni >= 0.0) { + + // Non-omni adjustment needs to be opposite sign of Omni percentage adjustment + // i.e. if Omni needs increasing, non-Omni gets decreased; if Omni needs reducing, + // non-Omni needs increasing. + double totalNonOmniWeight = (omniPctDifference > 0 ? 1.0 : -1.0) * (totalOmniWeight - totalWeight); + + // Apply the weight modifications proportionally, using the unit weight relative + // to the weight total of either Omni or non-Omni as appropriate + double curWeight; + totalClanWeight = 0.0; + totalSLWeight = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (curModel.isOmni()) { + if (pctOmni > 0.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalOmniWeight; + } else { + curWeight = 0.0; + } + } else { + if (pctOmni < 100.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalNonOmniWeight; + } else { + curWeight = 0.0; + } + } + unitWeights.put(curModel, curWeight); + + totalWeightPostMod += curWeight; + + // Re-calculate total weights of Clan and SL/advanced units for + // potential re-balancing. Track Omni-units separately so the weights + // we just set are kept. + if (curModel.isClan()) { + totalClanWeight += curWeight; + if (curModel.isOmni()) { + totalClanOmniWeight += curWeight; + } + } + if (curModel.isSL()) { + totalSLWeight += curWeight; + if (curModel.isOmni()) { + totalSLOmniWeight += curWeight; + } + } + + } + + } + + if (totalWeightPostMod > 0.0) { + totalWeight = totalWeightPostMod; + } + // Use margin values from the faction data to adjust for lack of precision // in post-FM:Updates era extrapolations if (pctSL != null || pctClan != null) { @@ -953,7 +1058,7 @@ private void adjustForRating(FactionRecord fRec, } Double upgradeMargin = interpolate(fRec.getUpgradeMargin(currentEra), - fRec.getUpgradeMargin(nextEra), currentEra, nextEra, year); + fRec.getUpgradeMargin(nextEra), currentEra, nextEra, year); if ((upgradeMargin != null) && (upgradeMargin > 0)) { double pct = 100.0 * (totalWeight - totalClanWeight - totalSLWeight) / totalWeight; if (pct < pctOther - upgradeMargin) { @@ -984,114 +1089,88 @@ private void adjustForRating(FactionRecord fRec, } } - // Adjust Omni-unit percentage by margin values from faction data - if (pctOmni != null) { - Double omniMargin = interpolate(fRec.getOmniMargin(currentEra), fRec.getOmniMargin(nextEra), - currentEra, nextEra, year); - if (omniMargin != null && omniMargin > 0) { - double pct = 100.0 * totalOmniWeight / totalWeight; - if (pct < pctOmni - omniMargin) { - pctOmni -= omniMargin; - } else if (pct > pctOmni + omniMargin) { - pctOmni += omniMargin; - } - } + // If there are not enough or too many base Clan or Star League/advanced + // IS tech units based on the faction data, re-balance the weights to bring + // them back into line. - pctNonOmni = 100.0 - pctOmni; + if (pctClan == null) { + pctClan = 0.0; } - - // Get the difference between the ideal Omni level and the current one - // and proportionally assign it to the Omni and non-Omni groups - if (pctOmni == null) { - pctOmni = 0.0; + if (pctSL == null) { + pctSL = 0.0; + } + if (pctOther == null) { + pctOther = 0.0; } - double omniPctDifference = pctOmni - (100.0 * totalOmniWeight / totalWeight); - - // If there are not enough or too many Omni-units based on the faction data, - // re-balance all the weights to bring them back into line. If the faction - // data specifies Omni-units but none are present, nothing can be done. - double totalWeightPostMod = 0.0; - if (Math.abs(omniPctDifference) > MIN_OMNI_DIFFERENCE && totalOmniWeight > 0.0 && pctOmni >= 0.0) { - - // Non-omni adjustment needs to be opposite sign of Omni percentage adjustment - // i.e. if Omni needs increasing, non-Omni gets decreased; if Omni needs reducing, - // non-Omni needs increasing. - double totalNonOmniWeight = (omniPctDifference > 0 ? 1.0 : -1.0) * (totalOmniWeight - totalWeight); - - // Apply the weight modifications proportionally, using the unit weight relative - // to the weight total of either Omni or non-Omni as appropriate - double curWeight = 0.0; - for (ModelRecord curModel : unitWeights.keySet()) { - curWeight = unitWeights.get(curModel); - - if (curModel.isOmni()) { - if (pctOmni > 0.0) { - curWeight = curWeight + curWeight * omniPctDifference / totalOmniWeight; - } else { - curWeight = 0.0; - } - } else { - if (pctOmni < 100.0) { - curWeight = curWeight + curWeight * omniPctDifference / totalNonOmniWeight; - } else { - curWeight = 0.0; - } - } - unitWeights.put(curModel, curWeight); - - totalWeightPostMod += curWeight; - } - - } else { - totalWeightPostMod = totalWeight; - } + double salvageTotalWeight = salvageWeights.keySet().stream().mapToDouble(salvageWeights::get).sum(); + // Non-Clan factions consider salvage from Clan factions as Clan units, + // so include that weight in the total weight of Clan units + double clanSalvageWeight = 0.0; + if (!fRec.isClan()) { + clanSalvageWeight = salvageWeights. + keySet(). + stream(). + filter(FactionRecord::isClan). + mapToDouble(salvageWeights::get). + sum(); + } + double clanPctDifference = pctClan - (100.0 * (totalClanWeight + clanSalvageWeight) / (totalWeight + salvageTotalWeight)); + double slPctDifference = pctSL - (100.0 * totalSLWeight / (totalWeight + salvageTotalWeight)); + if ((Math.abs(clanPctDifference) > MIN_CLAN_DIFFERENCE && totalClanWeight > 0.0) || + (Math.abs(slPctDifference) > MIN_SL_DIFFERENCE && totalSLWeight > 0.0)) { + // Adjust percentages to limit changes to non-Omni units + clanPctDifference = pctClan - (100.0 * (totalClanWeight + clanSalvageWeight - totalClanOmniWeight) / (totalWeight + salvageTotalWeight)); + boolean clanFlag = Math.abs(clanPctDifference) > MIN_CLAN_DIFFERENCE; + slPctDifference = pctSL - (100.0 * (totalSLWeight - totalSLOmniWeight) / (totalWeight + salvageTotalWeight)); + boolean slFlag = Math.abs(slPctDifference) > MIN_SL_DIFFERENCE; + // Non-Clan and non-SL/advanced units are adjusted to make up the balance + double totalOtherWeight = (totalWeight + salvageTotalWeight) - + (totalClanWeight + clanSalvageWeight - totalClanOmniWeight) - + (totalSLWeight - totalSLOmniWeight); + double otherDifference = pctOther - (100.0 * totalOtherWeight / (totalWeight + salvageTotalWeight)); - // For non-Clan factions, the amount of salvage from Clan factions is part of - // the overall Clan percentage. - if (!fRec.isClan() && (pctClan != null) && (totalClanWeight > 0)) { - double clanSalvage = salvageWeights. - keySet(). - stream(). - filter(FactionRecord::isClan). - mapToDouble(salvageWeights::get). - sum(); + // Apply the weight modifications proportionally, using the unit weight + // relative to the weight total of either Clan or SL/advanced as + // appropriate. + // Omni-units have already been balanced, so limit adjustments to non-Omnis. + double curWeight; + totalWeightPostMod = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); - totalWeight += clanSalvage; - totalClanWeight += clanSalvage; - for (FactionRecord fr : salvageWeights.keySet()) { - if (fr.isClan()) { - salvageWeights.put(fr, salvageWeights.get(fr) * (pctClan / 100.0) * (totalWeight / totalClanWeight)); + if (curModel.isClan()) { + if (!curModel.isOmni() && clanFlag && totalClanWeight > 0.0) { + if (pctClan > 0.0) { + curWeight = curWeight + curWeight * clanPctDifference / totalClanWeight; + } else { + curWeight = 0.0; + } + } + } else if (curModel.isSL()) { + if (!curModel.isOmni() && slFlag && totalSLWeight > 0.0) { + if (pctSL > 0.0) { + curWeight = curWeight + curWeight * slPctDifference / totalSLWeight; + } else { + curWeight = 0.0; + } + } + } else if (!curModel.isOmni() && totalOtherWeight > 0.0) { + curWeight = curWeight - curWeight * otherDifference / totalOtherWeight; } - } - } + unitWeights.put(curModel, curWeight); + totalWeightPostMod += curWeight; - // Anything not base Clan or Star League/advanced tech is Other/basic tech - double totalOther = totalWeight - totalClanWeight - totalSLWeight; - for (ModelRecord mRec : unitWeights.keySet()) { -// if (pctOmni != null && mRec.isOmni() && totalOmniWeight < totalWeight) { -// unitWeights.put(mRec, unitWeights.get(mRec) * (pctOmni / 100.0) * (totalWeight / totalOmniWeight)); -// } -// if (pctNonOmni != null && !mRec.isOmni() && totalOmniWeight > 0) { -// unitWeights.put(mRec, unitWeights.get(mRec) * (pctNonOmni / 100.0) * (totalWeight / (totalWeight - totalOmniWeight))); -// } - if (pctSL != null && mRec.isSL() && totalSLWeight > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctSL / 100.0) * (totalWeight / totalSLWeight)); - } - if (pctClan != null && mRec.isClan() && totalClanWeight > 0) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctClan / 100.0) * (totalWeight / totalClanWeight)); - } - if (pctOther != null && pctOther > 0 && !mRec.isClan() && !mRec.isSL()) { - unitWeights.put(mRec, unitWeights.get(mRec) * (pctOther / 100.0) * (totalWeight / totalOther)); } + } // After modifications, check the new total weight against the original value and From 286f8d2c3f758ed0af469272a5a4fcf15bfec400 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Wed, 4 Dec 2024 15:50:45 -0700 Subject: [PATCH 21/26] Rebuild C/SL/O calculations for better results. Include logging for potential conflicting data as well as results that are out of line with provided data. --- .../client/ratgenerator/RATGenerator.java | 435 ++++++++++++------ 1 file changed, 287 insertions(+), 148 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 1a0ddf1f366..1851ef60b65 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -20,6 +20,7 @@ import java.io.FileNotFoundException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; import java.util.*; import java.util.Map.Entry; import java.util.function.Function; @@ -74,20 +75,20 @@ public class RATGenerator { /** * Minimum difference between actual percentage and desired percentage of Omni-units - * before weights are re-balanced + * that will trigger re-balancing */ private static double MIN_OMNI_DIFFERENCE = 2.5; /** * Minimum difference between actual percentage and desired percentage of base-Clan - * units before weights are re-balanced + * units that will trigger re-balancing */ - private static double MIN_CLAN_DIFFERENCE = 5.0; + private static double MIN_CLAN_DIFFERENCE = 2.5; /** * Minimum difference between actual percentage and desired percentage of Star League - * and advanced IS tech units before weights are rebalanced + * and advanced IS tech units that will trigger re-balancing */ - private static double MIN_SL_DIFFERENCE = 5.0; + private static double MIN_SL_DIFFERENCE = 2.5; protected RATGenerator() { models = new HashMap<>(); @@ -883,6 +884,7 @@ private void adjustForRating(FactionRecord fRec, double totalOmniWeight = 0.0; double totalClanWeight = 0.0; double totalSLWeight = 0.0; + double totalOtherWeight = 0.0; // Total the unit weight of all selected units, plus get totals // of all Omni-units, base Clan-tech units, and Star League/advanced @@ -892,15 +894,12 @@ private void adjustForRating(FactionRecord fRec, if (entry.getKey().isOmni()) { totalOmniWeight += entry.getValue(); } - if (entry.getKey().isClan()) { - totalClanWeight += entry.getValue(); - } else if (entry.getKey().isSL()) { - totalSLWeight += entry.getValue(); + if (!entry.getKey().isClan() && !entry.getKey().isSL()) { + totalOtherWeight += entry.getValue(); } } Double pctOmni = null; - Double pctNonOmni = null; Double pctSL = null; Double pctClan = null; Double pctOther = null; @@ -931,11 +930,21 @@ private void adjustForRating(FactionRecord fRec, fRec.findPctTech(TechCategory.IS_ADVANCED_VEE, nextEra, rating), currentEra, nextEra, year); } - // Clan factions generally only field Clan-tech Omni-units, so the desired - // percentage of Omni-units should always be lower than the percentage of base - // Clan units. Trying to do otherwise creates unexpected results. - if (fRec.isClan() && (pctOmni != null && pctClan != null) && pctOmni > pctClan) { - logger.warn("Clan faction {} Clan/SL/Omni rating has higher Omni ({}) than Clan ({}) value in era {}.", fRec.getKey(), pctOmni, pctClan, currentEra); + // Omni percentage should never be higher than Clan percentage for + // Clan factions, and never higher than Clan plus SL for IS factions. + // This may lead to unexpected results. + if (fRec.isClan()) { + if (pctOmni != null && pctClan != null && pctOmni > pctClan) { + logger.warn("Clan faction {} Clan/SL/Omni rating has" + + " higher Omni ({}) than Clan ({}) value in era {}.", + fRec.getKey(), pctOmni, pctClan, currentEra); + } + } else { + if (pctOmni != null && pctClan != null && pctSL != null && pctOmni > pctClan + pctSL) { + logger.warn("Non-Clan faction {} Clan/SL/Omni rating has" + + " higher Omni ({}) than Clan ({}) + SL ({}) value in era {}.", + fRec.getKey(), pctOmni, pctClan, pctSL, currentEra); + } } // Adjust Omni-unit percentage by margin values from faction data @@ -952,80 +961,80 @@ private void adjustForRating(FactionRecord fRec, } } - pctNonOmni = 100.0 - pctOmni; } - // Get the difference between the ideal Omni level and the current one - // and proportionally assign it to the Omni and non-Omni groups - if (pctOmni == null) { - pctOmni = 0.0; - } - double omniPctDifference = pctOmni - (100.0 * totalOmniWeight / totalWeight); - - // If there are not enough or too many Omni-units based on the faction data, - // re-balance all the weights to bring them back into line. If the faction - // data specifies Omni-units but none are present, nothing can be done. double totalWeightPostMod = 0.0; - double totalClanOmniWeight = 0.0; - double totalSLOmniWeight = 0.0; - if (Math.abs(omniPctDifference) > MIN_OMNI_DIFFERENCE && totalOmniWeight > 0.0 && pctOmni >= 0.0) { - - // Non-omni adjustment needs to be opposite sign of Omni percentage adjustment - // i.e. if Omni needs increasing, non-Omni gets decreased; if Omni needs reducing, - // non-Omni needs increasing. - double totalNonOmniWeight = (omniPctDifference > 0 ? 1.0 : -1.0) * (totalOmniWeight - totalWeight); - - // Apply the weight modifications proportionally, using the unit weight relative - // to the weight total of either Omni or non-Omni as appropriate - double curWeight; - totalClanWeight = 0.0; - totalSLWeight = 0.0; - for (ModelRecord curModel : unitWeights.keySet()) { - curWeight = unitWeights.get(curModel); - - if (curModel.isOmni()) { - if (pctOmni > 0.0) { - curWeight = curWeight + curWeight * omniPctDifference / totalOmniWeight; - } else { - curWeight = 0.0; - } - } else { - if (pctOmni < 100.0) { - curWeight = curWeight + curWeight * omniPctDifference / totalNonOmniWeight; - } else { - curWeight = 0.0; - } - } - unitWeights.put(curModel, curWeight); - - totalWeightPostMod += curWeight; - - // Re-calculate total weights of Clan and SL/advanced units for - // potential re-balancing. Track Omni-units separately so the weights - // we just set are kept. - if (curModel.isClan()) { - totalClanWeight += curWeight; - if (curModel.isOmni()) { - totalClanOmniWeight += curWeight; - } - } - if (curModel.isSL()) { - totalSLWeight += curWeight; - if (curModel.isOmni()) { - totalSLOmniWeight += curWeight; - } - } - - } - - } - - if (totalWeightPostMod > 0.0) { - totalWeight = totalWeightPostMod; + // Only balance Meks and aerospace for Omni/non-Omni ratios + if ((unitType == UnitType.MEK || unitType == UnitType.AEROSPACEFIGHTER) && pctOmni != null) { + + // Get the difference between the ideal Omni level and the current one + double omniPctDifference = pctOmni - (100.0 * totalOmniWeight / totalWeight); + + // If there are not enough or too many Omni-units based on the faction data, + // re-balance all the weights to bring them back into line. If the faction + // data specifies Omni-units but none are present, nothing can be done. + if (Math.abs(omniPctDifference) > MIN_OMNI_DIFFERENCE && totalOmniWeight > 0.0 && pctOmni >= 0.0) { + + // Total weight of non-Omni units. Sign is deliberately inverted + // so weights of non-Omni units are moved in opposite direction from + // Omni units. + double totalNonOmniWeight = totalOmniWeight - totalWeight; + + // Apply the weight modifications proportionally, using the unit weight relative + // to the weight total of either Omni or non-Omni as appropriate + double curWeight; + totalClanWeight = 0.0; + totalSLWeight = 0.0; + totalOtherWeight = 0.0; + double totalOmniWeightPostMod = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (curModel.isOmni()) { + if (pctOmni > 0.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalOmniWeight; + } else { + curWeight = 0.0; + } + } else { + if (pctOmni < 100.0) { + curWeight = curWeight + curWeight * omniPctDifference / totalNonOmniWeight; + } else { + curWeight = 0.0; + } + } + + unitWeights.put(curModel, curWeight); + totalWeightPostMod += curWeight; + + // Re-calculate total weights of the various categories + if (curModel.isClan()) { + totalClanWeight += curWeight; + } else if (curModel.isSL()) { + totalSLWeight += curWeight; + } else { + totalOtherWeight += curWeight; + } + if (curModel.isOmni()) { + totalOmniWeightPostMod += curWeight; + } + + } + + if (totalOmniWeightPostMod > 0.0) { + totalOmniWeight = totalOmniWeightPostMod; + } + + } + + if (totalWeightPostMod > 0.0) { + totalWeight = totalWeightPostMod; + } + } // Use margin values from the faction data to adjust for lack of precision - // in post-FM:Updates era extrapolations + // in post-FM:Updates era extrapolations of Clan and Star League ratios if (pctSL != null || pctClan != null) { pctOther = 100.0; if (pctSL != null) { @@ -1089,96 +1098,226 @@ private void adjustForRating(FactionRecord fRec, } } + // Re-balance Star League/advanced IS tech designs against Clan and + // low-tech units + if (pctSL != null) { - // If there are not enough or too many base Clan or Star League/advanced - // IS tech units based on the faction data, re-balance the weights to bring - // them back into line. + double slPctDifference = pctSL - (100.0 * totalSLWeight / totalWeight); + if (Math.abs(slPctDifference) > MIN_SL_DIFFERENCE && totalSLWeight > 0.0) { - if (pctClan == null) { - pctClan = 0.0; - } - if (pctSL == null) { - pctSL = 0.0; - } - if (pctOther == null) { - pctOther = 0.0; - } + // Total weight of non-Star League/advanced units. Sign is deliberately + // inverted so weights of non-SL units are moved in opposite direction from + // SL units. + double totalNonSLWeight = totalSLWeight - totalWeight; + double totalSLWeightPostMod = 0.0; + + // Apply the weight modifications proportionally, using the unit weight relative + // to the weight total of either Clan or Other as appropriate + double curWeight; + totalClanWeight = 0.0; + totalOtherWeight = 0.0; + totalOmniWeight = 0.0; + totalWeightPostMod = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (curModel.isSL()) { + if (pctSL > 0.0) { + curWeight = curWeight + curWeight * slPctDifference / totalSLWeight; + } else { + curWeight = 0.0; + } + } else { + if (pctSL < 100.0) { + curWeight = curWeight + curWeight * slPctDifference / totalNonSLWeight; + } else { + curWeight = 0.0; + } + } + + unitWeights.put(curModel, curWeight); + totalWeightPostMod += curWeight; + + // Re-calculate total weights of the various categories + if (curModel.isClan()) { + totalClanWeight += curWeight; + } else if (curModel.isSL()) { + totalSLWeightPostMod += curWeight; + } else { + totalOtherWeight += curWeight; + } + if (curModel.isOmni()) { + totalOmniWeight += curWeight; + } + + } + + if (totalWeightPostMod > 0.0) { + totalWeight = totalWeightPostMod; + } + if (totalSLWeightPostMod > 0.0) { + totalSLWeight = totalSLWeightPostMod; + } + + } - double salvageTotalWeight = salvageWeights.keySet().stream().mapToDouble(salvageWeights::get).sum(); + } - // Non-Clan factions consider salvage from Clan factions as Clan units, - // so include that weight in the total weight of Clan units + // Re-balance Clan designs against SL and low-tech units to match + // faction data. double clanSalvageWeight = 0.0; - if (!fRec.isClan()) { - clanSalvageWeight = salvageWeights. - keySet(). - stream(). - filter(FactionRecord::isClan). - mapToDouble(salvageWeights::get). - sum(); - } - - double clanPctDifference = pctClan - (100.0 * (totalClanWeight + clanSalvageWeight) / (totalWeight + salvageTotalWeight)); - double slPctDifference = pctSL - (100.0 * totalSLWeight / (totalWeight + salvageTotalWeight)); - - if ((Math.abs(clanPctDifference) > MIN_CLAN_DIFFERENCE && totalClanWeight > 0.0) || - (Math.abs(slPctDifference) > MIN_SL_DIFFERENCE && totalSLWeight > 0.0)) { - - // Adjust percentages to limit changes to non-Omni units - clanPctDifference = pctClan - (100.0 * (totalClanWeight + clanSalvageWeight - totalClanOmniWeight) / (totalWeight + salvageTotalWeight)); - boolean clanFlag = Math.abs(clanPctDifference) > MIN_CLAN_DIFFERENCE; - - slPctDifference = pctSL - (100.0 * (totalSLWeight - totalSLOmniWeight) / (totalWeight + salvageTotalWeight)); - boolean slFlag = Math.abs(slPctDifference) > MIN_SL_DIFFERENCE; - - // Non-Clan and non-SL/advanced units are adjusted to make up the balance - double totalOtherWeight = (totalWeight + salvageTotalWeight) - - (totalClanWeight + clanSalvageWeight - totalClanOmniWeight) - - (totalSLWeight - totalSLOmniWeight); - double otherDifference = pctOther - (100.0 * totalOtherWeight / (totalWeight + salvageTotalWeight)); - - // Apply the weight modifications proportionally, using the unit weight - // relative to the weight total of either Clan or SL/advanced as - // appropriate. - // Omni-units have already been balanced, so limit adjustments to non-Omnis. - double curWeight; - totalWeightPostMod = 0.0; - for (ModelRecord curModel : unitWeights.keySet()) { - curWeight = unitWeights.get(curModel); + if (pctClan != null) { - if (curModel.isClan()) { - if (!curModel.isOmni() && clanFlag && totalClanWeight > 0.0) { + // Get total weights of provided salvage for inclusion in Clan re-balancing. + if (!fRec.isClan()) { + clanSalvageWeight = salvageWeights. + keySet(). + stream(). + filter(FactionRecord::isClan). + mapToDouble(salvageWeights::get). + sum(); + } + + double clanPctDifference = pctClan - (100.0 * Math.min(totalWeight, totalClanWeight + clanSalvageWeight) / totalWeight); + + if (Math.abs(clanPctDifference) > MIN_CLAN_DIFFERENCE && totalClanWeight > 0.0) { + + // Total weight of non-Clan units, including salvage if appropriate. + // Sign is deliberately inverted so weights of non-Clan units are moved + // in opposite direction from Clan units. + double totalNonClanWeight = Math.min(totalWeight, totalClanWeight + clanSalvageWeight) - totalWeight; + + // Apply the weight modifications proportionally, using the unit weight relative + // to the weight total of either Star League or Other as appropriate + double curWeight; + totalSLWeight = 0.0; + totalOtherWeight = 0.0; + totalOmniWeight = 0.0; + totalWeightPostMod = 0.0; + double totalClanWeightPostMod = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (curModel.isClan()) { if (pctClan > 0.0) { curWeight = curWeight + curWeight * clanPctDifference / totalClanWeight; } else { curWeight = 0.0; } - } - } else if (curModel.isSL()) { - if (!curModel.isOmni() && slFlag && totalSLWeight > 0.0) { - if (pctSL > 0.0) { - curWeight = curWeight + curWeight * slPctDifference / totalSLWeight; + } else { + if (pctClan < 100.0) { + curWeight = curWeight + curWeight * clanPctDifference / totalNonClanWeight; } else { curWeight = 0.0; } } - } else if (!curModel.isOmni() && totalOtherWeight > 0.0) { - curWeight = curWeight - curWeight * otherDifference / totalOtherWeight; + + unitWeights.put(curModel, curWeight); + totalWeightPostMod += curWeight; + + // Re-calculate total weights of the various categories + if (curModel.isClan()) { + totalClanWeightPostMod += curWeight; + } else if (curModel.isSL()) { + totalSLWeight += curWeight; + } else { + totalOtherWeight += curWeight; + } + if (curModel.isOmni()) { + totalOmniWeight += curWeight; + } + + } + + if (totalWeightPostMod > 0.0) { + totalWeight = totalWeightPostMod; + } + if (totalClanWeightPostMod > 0.0) { + totalClanWeight = totalClanWeightPostMod; + } + + } + + } + + // If Clan and Star League/advanced IS percentages leave no allowance + // for Other/low-tech then remove them using weight of 0.0 + + if (pctSL != null && pctClan != null && + (pctOther == 0.0 || pctSL + pctClan >= 100.0)) { + + double pctOtherDifference = pctOther - 100.0 * totalOtherWeight / totalWeight; + double totalAdvancedWeight = totalOtherWeight - totalWeight; + + double curWeight; + totalOmniWeight = 0.0; + totalClanWeight = 0.0; + totalSLWeight = 0.0; + totalOtherWeight = 0.0; + totalWeightPostMod = 0.0; + for (ModelRecord curModel : unitWeights.keySet()) { + curWeight = unitWeights.get(curModel); + + if (!curModel.isSL() && !curModel.isClan() && curWeight > 0.0) { + curWeight = 0.0; + } else { + curWeight = curWeight + curWeight * pctOtherDifference / totalAdvancedWeight; } unitWeights.put(curModel, curWeight); totalWeightPostMod += curWeight; + // Re-calculate total weights of the various categories + if (curModel.isClan()) { + totalClanWeight += curWeight; + } else if (curModel.isSL()) { + totalSLWeight += curWeight; + } else { + totalOtherWeight += curWeight; + } + if (curModel.isOmni()) { + totalOmniWeight += curWeight; + } } + if (totalWeightPostMod > 0.0) { + totalWeight = totalWeightPostMod; + } } - // After modifications, check the new total weight against the original value and - // use it as a multiplier to compensate - double multiplier = totalWeight / unitWeights.values().stream().mapToDouble(Double::doubleValue).sum(); - for (ModelRecord mRec : unitWeights.keySet()) { - unitWeights.merge(mRec, multiplier, (a, b) -> a * b); + // Check each of the weight totals against the faction specified percentages + // and log any that are significantly different + DecimalFormat pctFormatter = new DecimalFormat("#.##"); + if (pctOmni != null && + Math.abs(pctOmni - (100.0 * totalOmniWeight / totalWeight)) > MIN_OMNI_DIFFERENCE) { + logger.info("Faction {} Omni percentage ({}) differs significantly from" + + " faction C/SL/O data ({}) in year {}.", + fRec.getKey(), + pctFormatter.format(100.0 * totalOmniWeight / totalWeight), + pctOmni, + year); + } + if (pctSL != null && + Math.abs(pctSL - (100.0 * totalSLWeight / totalWeight)) > MIN_SL_DIFFERENCE) { + logger.info("Faction {} Star League/advanced IS percentage ({}) differs" + + " significantly from faction C/SL/O data ({}) in year {}.", + fRec.getKey(), + pctFormatter.format(100.0 * totalSLWeight / totalWeight), + pctSL, + year); + } + if (pctClan != null && + Math.abs(pctClan - + (100.0 * Math.min(totalWeight, totalClanWeight + clanSalvageWeight) / totalWeight) + ) > MIN_CLAN_DIFFERENCE) { + logger.info("Faction {} Clan percentage ({}) differs significantly from" + + " faction C/SL/O data ({}) in year {}.", + fRec.getKey(), + pctFormatter.format(100.0 * Math.min(totalWeight, totalClanWeight + clanSalvageWeight) / totalWeight), + pctClan, + year); } + } public void dispose() { From 0d7f2125e8a042950cbd967083559956fca011cc Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Wed, 4 Dec 2024 21:04:36 -0700 Subject: [PATCH 22/26] Minor updates --- megamek/src/megamek/client/ratgenerator/RATGenerator.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 1851ef60b65..955c9364ea6 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -894,7 +894,9 @@ private void adjustForRating(FactionRecord fRec, if (entry.getKey().isOmni()) { totalOmniWeight += entry.getValue(); } - if (!entry.getKey().isClan() && !entry.getKey().isSL()) { + if (entry.getKey().isSL()) { + totalSLWeight += entry.getValue(); + } else if (!entry.getKey().isClan()) { totalOtherWeight += entry.getValue(); } } @@ -1109,7 +1111,6 @@ private void adjustForRating(FactionRecord fRec, // inverted so weights of non-SL units are moved in opposite direction from // SL units. double totalNonSLWeight = totalSLWeight - totalWeight; - double totalSLWeightPostMod = 0.0; // Apply the weight modifications proportionally, using the unit weight relative // to the weight total of either Clan or Other as appropriate @@ -1118,6 +1119,7 @@ private void adjustForRating(FactionRecord fRec, totalOtherWeight = 0.0; totalOmniWeight = 0.0; totalWeightPostMod = 0.0; + double totalSLWeightPostMod = 0.0; for (ModelRecord curModel : unitWeights.keySet()) { curWeight = unitWeights.get(curModel); @@ -1168,7 +1170,7 @@ private void adjustForRating(FactionRecord fRec, double clanSalvageWeight = 0.0; if (pctClan != null) { - // Get total weights of provided salvage for inclusion in Clan re-balancing. + // Non-Clan factions count salvage weights from Clan factions as Clan tech if (!fRec.isClan()) { clanSalvageWeight = salvageWeights. keySet(). From 1dde064c0c380be8315aca14c36e74a5b69e668b Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Thu, 5 Dec 2024 08:47:45 -0700 Subject: [PATCH 23/26] Improve handling of base IS mixed tech units. Some minor readability cleanup. --- .../client/ratgenerator/ModelRecord.java | 113 +++++++++--------- .../client/ratgenerator/RATGenerator.java | 14 +-- 2 files changed, 62 insertions(+), 65 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ModelRecord.java b/megamek/src/megamek/client/ratgenerator/ModelRecord.java index 76f0bec604c..de039d0690f 100644 --- a/megamek/src/megamek/client/ratgenerator/ModelRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ModelRecord.java @@ -48,8 +48,8 @@ /** * Specific unit variants; analyzes equipment to determine suitability for - * certain types - * of missions in addition to what is formally declared in the data files. + * certain types of missions in addition to what is formally declared in the + * data files. * * @author Neoancient */ @@ -75,6 +75,7 @@ public class ModelRecord extends AbstractUnitRecord { private boolean primitive; private boolean retrotech; private boolean starLeague; + private boolean mixedTech; private int weightClass; private EntityMovementMode movementMode; @@ -146,6 +147,21 @@ public boolean isClan() { return clan; } + /** + * @return true, if unit is base IS tech and mounts Clan tech equipment + */ + public boolean isMixedTech() { + return mixedTech; + } + + /** + * @return true, if unit is either base Clan tech, or is base IS tech and + * mounts Clan tech equipment + */ + public boolean isMixedOrClanTech() { + return clan || mixedTech; + } + /** * Unit contains at least some primitive technology, without any advanced tech. * Testing is not extensive, there may be units that are not properly flagged. @@ -159,9 +175,8 @@ public boolean isPrimitive() { /** * Unit consists of at least some primitive technology and some advanced/Star - * League/Clan - * technology. Testing is not extensive, there may be units that are not - * properly flagged. + * League/Clan technology. Testing is not extensive, there may be units that + * are not properly flagged. * * @return true, if unit contains both primitive and advanced tech */ @@ -222,17 +237,12 @@ public double getArtilleryProportion() { /** * Proportion of total weapons BV that is capable of attacking targets at longer - * ranges. - * Units with values of 0.75 or higher are mostly armed with weapons that can - * hit targets at - * 15+ hexes, have a minimum range, and potentially fire indirectly in ground - * combat; or - * reach long/extreme range in air or space combat. + * ranges. Units with values of 0.75 or higher are mostly armed with weapons + * that can hit targets at 15+ hexes, have a minimum range, and potentially fire + * indirectly in ground combat; or reach long/extreme range in air or space combat. * Complementary to getSRProportion() - where one is high and the other is low, - * the unit is - * specialized for that range bracket. If both values are similar the unit is - * well balanced - * between long and short ranged capabilities. + * the unit is specialized for that range bracket. If both values are similar + * the unit is well balanced between long and short ranged capabilities. * TODO: rename for consistency and clarity * * @return between zero (none) and 1.0 (all weapons) @@ -243,17 +253,12 @@ public double getLongRange() { /** * Proportion of total weapons BV that is limited to attacking targets at close - * range. - * Units with values of 0.75 or higher are mostly armed with weapons that have a - * long range - * of less than 12 hexes and do not have a minimum range in ground combat, or - * are limited to - * short range in air/space combat. + * range. Units with values of 0.75 or higher are mostly armed with weapons that + * have a long range of less than 12 hexes and do not have a minimum range in + * ground combat, or are limited to short range in air/space combat. * Complementary to getLongRange() - where one is high and the other is low, the - * unit is - * specialized for that range bracket. If both values are similar the unit is - * well balanced - * between long and short ranged capabilities. + * unit is specialized for that range bracket. If both values are similar the + * unit is well balanced between long and short ranged capabilities. * * @return between zero (none) and 1.0 (all weapons) */ @@ -391,8 +396,7 @@ public boolean getAntiMek() { /** * Checks the equipment carried by this unit and summarizes it in a variety of - * easy to access - * data + * easy to access data * * @param unitData Data for unit */ @@ -492,12 +496,10 @@ private void analyzeModel(MekSummary unitData) { for (int i = 0; i < unitData.getEquipmentNames().size(); i++) { // EquipmentType.get is throwing an NPE intermittently, and the only possibility - // I can see - // is that there is a null equipment name. + // I can see is that there is a null equipment name. if (null == unitData.getEquipmentNames().get(i)) { - logger.error( - "RATGenerator ModelRecord encountered null equipment name in MekSummary for " - + unitData.getName() + ", index " + i); + logger.error("RATGenerator ModelRecord encountered null equipment name" + + " in MekSummary for {}, index {}", unitData.getName(), i); continue; } EquipmentType eq = EquipmentType.get(unitData.getEquipmentNames().get(i)); @@ -510,6 +512,11 @@ private void analyzeModel(MekSummary unitData) { losTech = true; } + // If this is Clan tech on an IS base unit, set the mixed tech flag + if (!mixedTech && !clan && eq.isClan()) { + mixedTech = true; + } + if (eq instanceof WeaponType) { // Flag units that are capable of making anti-Mek attacks. Don't bother making @@ -552,23 +559,20 @@ private void analyzeModel(MekSummary unitData) { } // Check for use against airborne targets. Ignore small craft, DropShips, and - // other - // large space craft. + // other large spacecraft. if (unitType < UnitType.SMALL_CRAFT) { flakBV += getFlakBVModifier((WeaponType) eq) * eq.getBV(null) * unitData.getEquipmentQuantities().get(i); } // Check for artillery weapons. Ignore aerospace fighters, small craft, and - // large - // space craft. + // large spacecraft. if (unitType <= UnitType.CONV_FIGHTER && eq instanceof ArtilleryWeapon) { artilleryBV += eq.getBV(null) * unitData.getEquipmentQuantities().get(i); } // Don't check incendiary weapons for conventional infantry, fixed wing - // aircraft, - // and space-going units + // aircraft, and space-going units if (!incendiary && (unitType < UnitType.CONV_FIGHTER && unitType != UnitType.INFANTRY)) { if (eq instanceof FlamerWeapon || @@ -596,9 +600,8 @@ private void analyzeModel(MekSummary unitData) { } // Total up BV for weapons that require ammo. Streak-type missile systems get a - // discount. Ignore small craft, DropShips, and large space craft. Ignore - // infantry - // weapons except for field guns. + // discount. Ignore small craft, DropShips, and large spacecraft. Ignore + // infantry weapons except for field guns. if (unitType < UnitType.SMALL_CRAFT && ((WeaponType) eq).getAmmoType() > AmmoType.T_NA && !(eq instanceof InfantryWeapon)) { @@ -620,15 +623,14 @@ private void analyzeModel(MekSummary unitData) { } // Total up BV for weapons capable of attacking at the longest ranges or using - // indirect fire. Ignore small craft, DropShips, and other space craft. + // indirect fire. Ignore small craft, DropShips, and other spacecraft. if (unitType < UnitType.SMALL_CRAFT) { longRangeBV += getLongRangeModifier((WeaponType) eq) * eq.getBV(null) * unitData.getEquipmentQuantities().get(i); } // Total up BV of weapons suitable for attacking at close range. Ignore small - // craft, - // DropShips, and other space craft. Also skip anti-Mek attacks. + // craft, DropShips, and other spacecraft. Also skip anti-Mek attacks. if (unitType < UnitType.SMALL_CRAFT) { shortRangeBV += getShortRangeModifier((WeaponType) eq) * eq.getBV(null) * unitData.getEquipmentQuantities().get(i); @@ -696,9 +698,8 @@ private void analyzeModel(MekSummary unitData) { } // Calculate BV proportions for all ground units, VTOL, blue water naval, gun - // emplacements - // and fixed wing aircraft. Exclude Small craft, DropShips, and large space - // craft. + // emplacements and fixed wing aircraft. Exclude Small craft, DropShips, and + // large spacecraft. if (unitType <= UnitType.AEROSPACEFIGHTER) { if (totalWeaponBV > 0) { flakBVProportion = flakBV / totalWeaponBV; @@ -722,9 +723,8 @@ private void analyzeModel(MekSummary unitData) { /** * Units are considered primitive if they have no advanced tech and at least - * some primitive - * tech. The check is not exhaustive so some niche units may not be flagged - * properly. + * some primitive tech. The check is not exhaustive so some niche units may + * not be flagged properly. * * @param unitData Unit data * @return true if unit has primitive tech and no advanced tech @@ -824,10 +824,9 @@ private boolean isUnitPrimitive(MekSummary unitData) { /** * Checks that unit has at least one piece of primitive tech for basic unit - * components such as - * engine, frame, and armor. The check is not extensive so some units may - * include a niche item - * even though they are not flagged as such. + * components such as engine, frame, and armor. The check is not extensive + * so some units may include a niche item even though they are not flagged + * as such. * * @param unitData Unit data * @return true if unit contains primitive basic equipment @@ -880,8 +879,7 @@ private boolean unitHasPrimitiveTech(MekSummary unitData) { /** * Check if unit is built with advanced technology. This only checks the basic - * components, - * not any mounted equipment such as weapons. + * components, not any mounted equipment such as weapons. * * @param unitData unit data * @param starLeagueOnly true to only check original Star League tech - XL @@ -989,8 +987,7 @@ private double getFlakBVModifier(WeaponType checkWeapon) { double ineffective = 0.0; // Use a limited version for checking air-to-air capability, including potential - // for - // thresholding heavily armored targets + // for thresholding heavily armored targets if (unitType == UnitType.CONV_FIGHTER || unitType == UnitType.AEROSPACEFIGHTER) { if (checkWeapon.getAmmoType() == AmmoType.T_AC_LBX || checkWeapon.getAmmoType() == AmmoType.T_HAG || diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 955c9364ea6..047278fd1d5 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -896,7 +896,7 @@ private void adjustForRating(FactionRecord fRec, } if (entry.getKey().isSL()) { totalSLWeight += entry.getValue(); - } else if (!entry.getKey().isClan()) { + } else if (!entry.getKey().isMixedOrClanTech()) { totalOtherWeight += entry.getValue(); } } @@ -1010,7 +1010,7 @@ private void adjustForRating(FactionRecord fRec, totalWeightPostMod += curWeight; // Re-calculate total weights of the various categories - if (curModel.isClan()) { + if (curModel.isMixedOrClanTech()) { totalClanWeight += curWeight; } else if (curModel.isSL()) { totalSLWeight += curWeight; @@ -1141,7 +1141,7 @@ private void adjustForRating(FactionRecord fRec, totalWeightPostMod += curWeight; // Re-calculate total weights of the various categories - if (curModel.isClan()) { + if (curModel.isMixedOrClanTech()) { totalClanWeight += curWeight; } else if (curModel.isSL()) { totalSLWeightPostMod += curWeight; @@ -1200,7 +1200,7 @@ private void adjustForRating(FactionRecord fRec, for (ModelRecord curModel : unitWeights.keySet()) { curWeight = unitWeights.get(curModel); - if (curModel.isClan()) { + if (curModel.isMixedOrClanTech()) { if (pctClan > 0.0) { curWeight = curWeight + curWeight * clanPctDifference / totalClanWeight; } else { @@ -1218,7 +1218,7 @@ private void adjustForRating(FactionRecord fRec, totalWeightPostMod += curWeight; // Re-calculate total weights of the various categories - if (curModel.isClan()) { + if (curModel.isMixedOrClanTech()) { totalClanWeightPostMod += curWeight; } else if (curModel.isSL()) { totalSLWeight += curWeight; @@ -1260,7 +1260,7 @@ private void adjustForRating(FactionRecord fRec, for (ModelRecord curModel : unitWeights.keySet()) { curWeight = unitWeights.get(curModel); - if (!curModel.isSL() && !curModel.isClan() && curWeight > 0.0) { + if (!curModel.isSL() && !curModel.isMixedOrClanTech() && curWeight > 0.0) { curWeight = 0.0; } else { curWeight = curWeight + curWeight * pctOtherDifference / totalAdvancedWeight; @@ -1270,7 +1270,7 @@ private void adjustForRating(FactionRecord fRec, totalWeightPostMod += curWeight; // Re-calculate total weights of the various categories - if (curModel.isClan()) { + if (curModel.isMixedOrClanTech()) { totalClanWeight += curWeight; } else if (curModel.isSL()) { totalSLWeight += curWeight; From 4533fd07c5feeda48867251a62a2ce89eaeaa37a Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Thu, 5 Dec 2024 09:28:46 -0700 Subject: [PATCH 24/26] Add unit type to C/SL/O info log entries --- .../src/megamek/client/ratgenerator/RATGenerator.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index 047278fd1d5..af7ca9bc031 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -1292,18 +1292,20 @@ private void adjustForRating(FactionRecord fRec, DecimalFormat pctFormatter = new DecimalFormat("#.##"); if (pctOmni != null && Math.abs(pctOmni - (100.0 * totalOmniWeight / totalWeight)) > MIN_OMNI_DIFFERENCE) { - logger.info("Faction {} Omni percentage ({}) differs significantly from" + + logger.info("Faction {} {} Omni percentage ({}) differs significantly from" + " faction C/SL/O data ({}) in year {}.", fRec.getKey(), + UnitType.getTypeName(unitType), pctFormatter.format(100.0 * totalOmniWeight / totalWeight), pctOmni, year); } if (pctSL != null && Math.abs(pctSL - (100.0 * totalSLWeight / totalWeight)) > MIN_SL_DIFFERENCE) { - logger.info("Faction {} Star League/advanced IS percentage ({}) differs" + + logger.info("Faction {} {} Star League/advanced IS percentage ({}) differs" + " significantly from faction C/SL/O data ({}) in year {}.", fRec.getKey(), + UnitType.getTypeName(unitType), pctFormatter.format(100.0 * totalSLWeight / totalWeight), pctSL, year); @@ -1312,9 +1314,10 @@ private void adjustForRating(FactionRecord fRec, Math.abs(pctClan - (100.0 * Math.min(totalWeight, totalClanWeight + clanSalvageWeight) / totalWeight) ) > MIN_CLAN_DIFFERENCE) { - logger.info("Faction {} Clan percentage ({}) differs significantly from" + + logger.info("Faction {} {} Clan percentage ({}) differs significantly from" + " faction C/SL/O data ({}) in year {}.", fRec.getKey(), + UnitType.getTypeName(unitType), pctFormatter.format(100.0 * Math.min(totalWeight, totalClanWeight + clanSalvageWeight) / totalWeight), pctClan, year); From def6259ec4e4465fee2425b142d370c835533b44 Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Thu, 5 Dec 2024 09:52:59 -0700 Subject: [PATCH 25/26] Fix typos in CJF C/SL/R values found through testing --- megamek/data/forcegenerator/3058.xml | 4 ++-- megamek/data/forcegenerator/3060.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/megamek/data/forcegenerator/3058.xml b/megamek/data/forcegenerator/3058.xml index 0fc56d3e64b..8282135ba29 100644 --- a/megamek/data/forcegenerator/3058.xml +++ b/megamek/data/forcegenerator/3058.xml @@ -129,8 +129,8 @@ 25,25,53,90,100 - 53,53,80,46,100 - 47,47,20,54,0 + 53,53,80,96,100 + 47,47,20,4,0 0,0,20,20,20 100,100,80,80,80 CHH:0,CSR:0,CIH:0,CI:0,CSV:2,CFM:0,CCO:0,CGS:0,CSA:0,CDS:0,CW:6,LA:5,CNC:0,CSJ:0,CGB:0 diff --git a/megamek/data/forcegenerator/3060.xml b/megamek/data/forcegenerator/3060.xml index 7bf50c3fa8a..55f8b57c860 100644 --- a/megamek/data/forcegenerator/3060.xml +++ b/megamek/data/forcegenerator/3060.xml @@ -119,8 +119,8 @@ 25,25,55,90,100 - 55,55,80,10,100 - 45,45,20,90,0 + 55,55,80,90,100 + 45,45,20,10,0 0,0,20,20,20 100,100,80,80,80 CW:10,LA:2,CSV:4 From cae45e0767ee4321c330d1a445a9a336f6c9410c Mon Sep 17 00:00:00 2001 From: SuperStucco Date: Fri, 13 Dec 2024 17:35:04 -0700 Subject: [PATCH 26/26] Updates from review comments --- megamek/src/megamek/client/ratgenerator/ModelRecord.java | 2 +- megamek/src/megamek/client/ratgenerator/RATGenerator.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/megamek/src/megamek/client/ratgenerator/ModelRecord.java b/megamek/src/megamek/client/ratgenerator/ModelRecord.java index de039d0690f..62c666479eb 100644 --- a/megamek/src/megamek/client/ratgenerator/ModelRecord.java +++ b/megamek/src/megamek/client/ratgenerator/ModelRecord.java @@ -150,7 +150,7 @@ public boolean isClan() { /** * @return true, if unit is base IS tech and mounts Clan tech equipment */ - public boolean isMixedTech() { + public boolean isMixedISTech() { return mixedTech; } diff --git a/megamek/src/megamek/client/ratgenerator/RATGenerator.java b/megamek/src/megamek/client/ratgenerator/RATGenerator.java index af7ca9bc031..1c0148bef0d 100644 --- a/megamek/src/megamek/client/ratgenerator/RATGenerator.java +++ b/megamek/src/megamek/client/ratgenerator/RATGenerator.java @@ -589,10 +589,10 @@ public List generateTable(FactionRecord fRec, // class are the same for all models although a few outliers exist, so // just look for the first. if (weightClasses != null && !weightClasses.isEmpty()) { - boolean validChassis = Arrays.stream(curChassis.getModels(). + boolean validChassis = curChassis. + getModels(). stream(). mapToInt(ModelRecord::getWeightClass). - toArray()). anyMatch(weightClasses::contains); if (!validChassis) { continue;