diff --git a/MekHQ/data/scenariotemplates/Convoy Escort.xml b/MekHQ/data/scenariotemplates/Convoy Escort.xml index 1fbd7b5757..a5d78ff3f7 100644 --- a/MekHQ/data/scenariotemplates/Convoy Escort.xml +++ b/MekHQ/data/scenariotemplates/Convoy Escort.xml @@ -110,8 +110,12 @@ false CIVILIAN - CARGO SUPPORT + SUPPORT + CARGO + CARGO + CARGO + APC APC diff --git a/MekHQ/data/scenariotemplates/Convoy Interdiction.xml b/MekHQ/data/scenariotemplates/Convoy Interdiction.xml index 7e231b2008..e5ec14a33d 100644 --- a/MekHQ/data/scenariotemplates/Convoy Interdiction.xml +++ b/MekHQ/data/scenariotemplates/Convoy Interdiction.xml @@ -106,8 +106,12 @@ false CIVILIAN - CARGO SUPPORT + SUPPORT + CARGO + CARGO + CARGO + APC APC diff --git a/MekHQ/data/scenariotemplates/Convoy Raid.xml b/MekHQ/data/scenariotemplates/Convoy Raid.xml index 8d9653acde..e1af103868 100644 --- a/MekHQ/data/scenariotemplates/Convoy Raid.xml +++ b/MekHQ/data/scenariotemplates/Convoy Raid.xml @@ -77,7 +77,7 @@ -1 false - -2 + 1 0 true false @@ -107,7 +107,6 @@ CIVILIAN CARGO - SUPPORT APC diff --git a/MekHQ/data/scenariotemplates/Critical Convoy Escort.xml b/MekHQ/data/scenariotemplates/Critical Convoy Escort.xml index dc428b31ab..8eabc51a26 100644 --- a/MekHQ/data/scenariotemplates/Critical Convoy Escort.xml +++ b/MekHQ/data/scenariotemplates/Critical Convoy Escort.xml @@ -110,8 +110,12 @@ false CIVILIAN - CARGO SUPPORT + SUPPORT + CARGO + CARGO + CARGO + APC APC diff --git a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java index 20f56264b9..10d37bf058 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java @@ -163,7 +163,7 @@ public static AtBDynamicScenario initializeScenarioFromTemplate(ScenarioTemplate // reinforcements to aerospace battles // space battles are even more restrictive if (template.mapParameters.getMapLocation() == MapLocation.LowAtmosphere) { - defaultReinforcements.setAllowedUnitType(ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX); + defaultReinforcements.setAllowedUnitType(SPECIAL_UNIT_TYPE_ATB_AERO_MIX); } else if (template.mapParameters.getMapLocation() == MapLocation.Space) { defaultReinforcements.setAllowedUnitType(AEROSPACEFIGHTER); } @@ -539,7 +539,7 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_MIX) { requiredRoles.put(MEK, new ArrayList<>(baseRoles)); requiredRoles.put(TANK, new ArrayList<>(baseRoles)); - } else if (forceTemplate.getAllowedUnitType() == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { + } else if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { requiredRoles.put(CONV_FIGHTER, new ArrayList<>(baseRoles)); requiredRoles.put(AEROSPACEFIGHTER, new ArrayList<>(baseRoles)); } else if (forceTemplate.getAllowedUnitType() == SPECIAL_UNIT_TYPE_ATB_CIVILIANS) { @@ -636,11 +636,11 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac // All other unit types use the force generator system to randomly select units } else { boolean allowConventionalAircraft = isPlanetOwner - && actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX + && actualUnitType == SPECIAL_UNIT_TYPE_ATB_AERO_MIX && scenario.getTemplate().mapParameters.getMapLocation() != MapLocation.Space && scenario.getAtmosphere().isDenserThan(Atmosphere.THIN); - if (actualUnitType == ScenarioForceTemplate.SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { + if (actualUnitType == SPECIAL_UNIT_TYPE_ATB_AERO_MIX) { lanceSize = getAeroLanceSize(faction); } @@ -1684,21 +1684,7 @@ public static Entity getEntity(String faction, unitData = campaign.getUnitGenerator().generate(params); } - if (unitData == null) { - if (!params.getMissionRoles().isEmpty()) { - Entity secondChanceEntity = getEntity(faction, skill, quality, unitType, weightClass, campaign); - - if (secondChanceEntity == null) { - logger.warn(String.format("Unable to randomly generate %s %s with roles: %s." + - " Second chance generation also failed.", - EntityWeightClass.getClassName(params.getWeightClass()), - getTypeName(unitType), - params.getMissionRoles().stream().map(Enum::name).collect(Collectors.joining(",")))); - } else { - return secondChanceEntity; - } - } return null; } @@ -3108,95 +3094,196 @@ private static List generateLance(String faction, SkillLevel skill, int * tactical group. */ private static List generateLance(String faction, SkillLevel skill, int quality, - List unitTypes, String weights, Map> rolesByType, - Campaign campaign, AtBScenario scenario, boolean allowsTanks) { + List unitTypes, String weights, Map> rolesByType, + Campaign campaign, AtBScenario scenario, boolean allowsTanks) { + List generatedEntities = new ArrayList<>(); // If the number of unit types and number of weight classes don't match, - // generate the lower - // of the two counts + // generate the lower of the two counts int unitTypeSize = unitTypes.size(); if (unitTypeSize > weights.length()) { logger.error( - String.format("More unit types (%d) provided than weights (%d). Truncating generated lance.", - unitTypes.size(), - weights.length())); + String.format("More unit types (%d) provided than weights (%d). Truncating generated lance.", + unitTypes.size(), + weights.length())); unitTypeSize = weights.length(); } - for (int i = 0; i < unitTypeSize; i++) { - Entity newEntity = getNewEntity(faction, skill, quality, unitTypes, weights, rolesByType, - campaign, i); + for (int unitIndex = 0; unitIndex < unitTypeSize; unitIndex++) { + Entity entity = getNewEntity(faction, skill, quality, unitTypes, weights, rolesByType, + campaign, unitIndex); - if (newEntity == null) { - logger.info(String.format("Failed to generate unit of type %s, weight %s. Beginning substitution.", - getTypeName(unitTypes.get(i)), - EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, i)))); + if (entity != null) { + generatedEntities.add(entity); + continue; + } - // If we've failed to get an entity, we start adjusting weight categories to see - // if we hit something valid. - // We start at lighter weights as, generally, they're less impactful than heavier units. - List individualType = List.of(unitTypes.get(i)); + String role = null; + Integer type = unitTypes.get(unitIndex); + if (type != null) { + Collection roleByType = rolesByType.get(type); + if (roleByType != null) { + role = roleByType.toString(); + } + } - Map> individualRole = new HashMap<>(); - individualRole.put(0, rolesByType.getOrDefault(unitTypes.get(i), List.of())); + logger.info(String.format("Failed to generate unit of type %s, weight %s, role %s." + + " Beginning substitution.", + getTypeName(unitTypes.get(unitIndex)), + EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, unitIndex)), + role != null ? "roles (" + role + ')' : "")); - List weightClasses = List.of("UL", "L", "M", "H", "A"); + entity = substituteEntity(faction, skill, quality, weights, rolesByType, + campaign, unitTypes, unitIndex, unitTypes); - for (String weight : weightClasses) { - newEntity = getNewEntity(faction, skill, quality, individualType, weight, individualRole, campaign, 0); + if (entity != null) { + generatedEntities.add(entity); + continue; + } - if (newEntity != null) { - logger.info(String.format("Substitution successful (%s)", - EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, i)))); - break; - } + // fallback unitType list container + List fallbackUnitType = unitTypes; + + // starting of error scenarios + if (unitTypes.get(unitIndex) == DROPSHIP) { + fallbackUnitType = List.of(DROPSHIP); + entity = substituteEntity(faction, skill, quality, weights, rolesByType, + campaign, unitTypes, unitIndex, fallbackUnitType); + } else if (scenario.getBoardType() == T_GROUND) { + if (allowsTanks && unitTypes.get(unitIndex) != TANK) { + logger.info("Switching unit type to Tank"); + fallbackUnitType = List.of(TANK); + + entity = substituteEntity(faction, skill, quality, weights, rolesByType, + campaign, unitTypes, unitIndex, fallbackUnitType); } - // If we still haven't got a valid entity, use hardcoded fallbacks. - if (newEntity == null) { - logger.info("Substitution unsuccessful. Using hardcoded fallbacks"); + if (unitTypes.get(unitIndex) != MEK) { + logger.info("Switching unit type to Mek"); + fallbackUnitType = List.of(MEK); - if (unitTypes.get(0) == DROPSHIP) { - newEntity = getNewEntity(faction, skill, quality, List.of(DROPSHIP), - weights, Map.of(DROPSHIP, List.of(CIVILIAN)), - campaign, 0); - - if (newEntity != null) { - logger.info("Substitution successful. Substituted with Civilian DropShip."); - } - } else { - if (scenario.getBoardType() == T_GROUND && allowsTanks) { - newEntity = getNewEntity(faction, skill, quality, List.of(TANK), - weights, null, campaign, 0); + entity = substituteEntity(faction, skill, quality, weights, rolesByType, + campaign, unitTypes, unitIndex, fallbackUnitType); + } - if (newEntity != null) { - logger.info("Substitution successful. Substituted with Tank."); - } - } else { - newEntity = getNewEntity(faction, skill, quality, List.of(AEROSPACEFIGHTER), - weights, null, campaign, 0); + // Abandon attempts to generate by role + if (entity == null) { + logger.info("Removing role requirements."); + entity = substituteEntity(faction, skill, quality, weights, null, + campaign, unitTypes, unitIndex, unitTypes); + } + } else { + if (unitTypes.get(unitIndex) != AEROSPACEFIGHTER) { + logger.info("Switching unit type to Aerospace Fighter"); + fallbackUnitType = List.of(AEROSPACEFIGHTER); - if (newEntity != null) { - logger.info("Substitution successful. Substituted with Aerospace Fighter."); - } - } - } + entity = substituteEntity(faction, skill, quality, weights, rolesByType, + campaign, unitTypes, unitIndex, fallbackUnitType); + } - if (newEntity == null) { - logger.info("Substitution unsuccessful. Abandoning attempts to generate unit."); - } + // Abandon attempts to generate by role + if (entity == null) { + logger.info("Removing role requirements."); + entity = substituteEntity(faction, skill, quality, weights, null, + campaign, unitTypes, unitIndex, fallbackUnitType); } } - if (newEntity != null) { - generatedEntities.add(newEntity); + if (entity != null) { + generatedEntities.add(entity); + } else { + logger.info("Substitution unsuccessful. Abandoning attempts to generate unit."); } } return generatedEntities; } + /** + * Attempts to substitute an entity with a fallback unit type in a series of steps. + * When the initial entity generation fails, this method makes various changes to the unit type + * and weight and tries again until it either successfully generates a new entity + * or exhausts all alternatives. + * + * @param faction The faction for the new Entity being generated + * @param skill The skill level for the new Entity being generated + * @param quality The quality for the new Entity being generated + * @param weights The weights for the unit types being considered + * @param rolesByType The roles available for each unit type + * @param campaign The campaign to which this Entity will be added + * @param unitTypes The unit types available for substitution + * @param unitIndex The index of the unit being replaced in the unitTypes list + * @param fallbackUnitType The fallback unit type to be used if normal generation steps fail + * @return The new generated Entity or null if substitution unsuccessful + */ + private static @Nullable Entity substituteEntity(String faction, SkillLevel skill, int quality, + String weights, Map> rolesByType, + Campaign campaign, List unitTypes, int unitIndex, + List fallbackUnitType) { + logger.info("Attempting to generate again"); + + Entity entity = getNewEntity(faction, skill, quality, fallbackUnitType, weights, rolesByType, + campaign, unitIndex); + + if (entity != null) { + logger.info("Substitution successful."); + return entity; + } + + logger.info("That didn't help, cycling weights."); + entity = attemptSubstitutionViaWeight(faction, skill, quality, weights, + rolesByType, campaign, unitTypes, unitIndex); + + if (entity != null) { + logger.info("Substitution successful."); + return entity; + } else { + logger.info("Unable to substitute entity."); + return null; + } + } + + /** + * Attempts to generate an Entity by substituting weight classes in a specific order. + * Starting from the lightest (`UL`) to the heaviest (`A`), each weight class is attempted + * until a valid Entity can be generated, or all weight classes have been tried. If a valid + * Entity is generated, that Entity is returned; otherwise, the method returns null. + * + * @param faction The faction for the new Entity being generated. + * @param skill The SkillLevel for the new Entity being generated. + * @param quality The quality for the new Entity being generated. + * @param weights A String representing the weights for the unit types being considered. + * @param rolesByType The roles available for each unit type. This may be null. + * @param campaign The campaign to which this Entity will be added. + * @param individualType The unit types available for substitution. + * @param unitIndex The index of the unit type being replaced in the unitTypes list. + * @return The new Entity generated or null if substitution is unsuccessful. + */ + private static @Nullable Entity attemptSubstitutionViaWeight(String faction, SkillLevel skill, + int quality, String weights, + @Nullable Map> rolesByType, + Campaign campaign, + List individualType, + int unitIndex) { + List weightClasses = List.of("UL", "L", "M", "H", "A"); + + Entity entity; + + for (String weight : weightClasses) { + entity = getNewEntity(faction, skill, quality, individualType, weight, + rolesByType, campaign, unitIndex); + + if (entity != null) { + logger.info(String.format("Substitution successful (%s)", + EntityWeightClass.getClassName(AtBConfiguration.decodeWeightStr(weights, unitIndex)))); + return entity; + } + } + + return null; + } + /** * Retrieve a new instance of Entity with the given parameters. * @@ -3207,23 +3294,41 @@ private static List generateLance(String faction, SkillLevel skill, int * @param weights the unit weights string * @param rolesByType the mapping of unit types to mission roles * @param campaign the campaign associated with the entity - * @param i the index of the unit type in the unitTypes list + * @param unitIndex the index of the unit type in the unitTypes list * * @return a new instance of Entity with the specified parameters */ private static Entity getNewEntity(String faction, SkillLevel skill, int quality, List unitTypes, String weights, @Nullable Map> rolesByType, - Campaign campaign, int i) { + Campaign campaign, int unitIndex) { Collection roles; if (rolesByType != null) { - roles = rolesByType.getOrDefault(unitTypes.get(i), new ArrayList<>()); + if (unitTypes.size() == 1) { + roles = rolesByType.getOrDefault(unitTypes.get(0), new ArrayList<>()); + } else { + roles = rolesByType.getOrDefault(unitTypes.get(unitIndex), new ArrayList<>()); + } } else { roles = null; } - return getEntity(faction, skill, quality, unitTypes.get(i), - AtBConfiguration.decodeWeightStr(weights, i), roles, campaign); + + int weight; + if (weights.length() == 1 || (weights.charAt(0) == 'U' && weights.length() == 2)) { + weight = AtBConfiguration.decodeWeightStr(weights, 0); + } else { + weight = AtBConfiguration.decodeWeightStr(weights, unitIndex); + } + + int unitType; + if (unitTypes.size() == 1) { + unitType = unitTypes.get(0); + } else { + unitType = unitTypes.get(unitIndex); + } + + return getEntity(faction, skill, quality, unitType, weight, roles, campaign); } /**