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);
}
/**