Skip to content

Commit 104bf12

Browse files
authored
Merge pull request #6603 from IllianiBird/onBoarding
Fix #6596: Added Off-Board Deployment Validation and Improved Generation Safety
2 parents 8288af2 + 992a0b3 commit 104bf12

File tree

2 files changed

+130
-32
lines changed

2 files changed

+130
-32
lines changed

MekHQ/src/mekhq/campaign/mission/AtBDynamicScenarioFactory.java

+99-29
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import static megamek.common.Compute.d6;
3535
import static megamek.common.Compute.randomInt;
3636
import static megamek.common.UnitType.*;
37+
import static megamek.common.WeaponType.CLASS_ARTILLERY;
3738
import static megamek.common.planetaryconditions.Atmosphere.THIN;
3839
import static megamek.common.planetaryconditions.Wind.TORNADO_F4;
3940
import static mekhq.campaign.force.CombatTeam.getStandardForceSize;
@@ -68,6 +69,7 @@
6869
import megamek.common.containers.MunitionTree;
6970
import megamek.common.enums.Gender;
7071
import megamek.common.enums.SkillLevel;
72+
import megamek.common.equipment.WeaponMounted;
7173
import megamek.common.icons.Camouflage;
7274
import megamek.common.planetaryconditions.Atmosphere;
7375
import megamek.common.planetaryconditions.Wind;
@@ -1062,6 +1064,12 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac
10621064
if (unidentifiedThirdPartyPresent) {
10631065
generatedForce.setCamouflage(AtBContract.pickRandomCamouflage(currentDate.getYear(), factionCode));
10641066
}
1067+
1068+
boolean isDeployOffBoard = forceTemplate.getDeployOffboard();
1069+
if (isDeployOffBoard) {
1070+
validateOffBoardCapabilities(generatedEntities);
1071+
}
1072+
10651073
scenario.addBotForce(generatedForce, forceTemplate, campaign);
10661074

10671075
if (!contract.isBatchallAccepted()) {
@@ -1232,6 +1240,46 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac
12321240
return generatedLanceCount;
12331241
}
12341242

1243+
/**
1244+
* Validates whether a force contains the required artillery weapons to deploy off-board, adjusting deployment
1245+
* settings if necessary.
1246+
*
1247+
* <p>This method iterates through the generated entities in a force, checking if they have at least one weapon
1248+
* classified as artillery. If any entity in the force doesn't have an artillery weapon, the entire force is moved
1249+
* on-board, as forces cannot be split between on-board and off-board deployments.</p>
1250+
*
1251+
* @param generatedEntities A list of {@link Entity} objects representing the units generated for the force. Each
1252+
* entity and its weapon list will be checked for artillery capability.
1253+
*
1254+
* @author Illiani
1255+
* @since 0.50.05
1256+
*/
1257+
public static void validateOffBoardCapabilities(List<Entity> generatedEntities) {
1258+
for (Entity entity : generatedEntities) {
1259+
boolean hasArtillery = false;
1260+
for (WeaponMounted weapon : entity.getTotalWeaponList()) {
1261+
WeaponType type = weapon.getType();
1262+
1263+
if (type == null) {
1264+
continue;
1265+
}
1266+
1267+
int attackClass = type.getAtClass();
1268+
1269+
if (attackClass == CLASS_ARTILLERY) {
1270+
hasArtillery = true;
1271+
break;
1272+
}
1273+
}
1274+
1275+
if (!hasArtillery) {
1276+
logger.info("{} was meant to deploy off board, but they don't have an artillery weapon." +
1277+
" Moving them on board.", entity.getDisplayName());
1278+
entity.setOffBoard(0, OffBoardDirection.NONE);
1279+
}
1280+
}
1281+
}
1282+
12351283
/**
12361284
* Retrieves the {@link StratconTrackState} associated with the given {@link AtBDynamicScenario}.
12371285
* <p>
@@ -2041,13 +2089,15 @@ public static Entity getInfantryEntity(UnitGeneratorParameters params, SkillLeve
20412089
MekSummary unitData = campaign.getUnitGenerator().generate(params);
20422090

20432091
if (unitData == null) {
2044-
20452092
// If XCT troops were requested but none were found, generate without the role
20462093
if (useTempXCT && params.getMissionRoles().contains(XCT)) {
20472094
noXCTParams = params.clone();
2048-
noXCTParams.getMissionRoles().remove(XCT);
2049-
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2050-
temporaryXCT = true;
2095+
2096+
if (noXCTParams != null) {
2097+
noXCTParams.getMissionRoles().remove(XCT);
2098+
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2099+
temporaryXCT = true;
2100+
}
20512101
}
20522102
if (unitData == null) {
20532103
if (!params.getMissionRoles().isEmpty()) {
@@ -2212,16 +2262,21 @@ private static List<Entity> fillTransport(AtBScenario scenario, Entity transport
22122262
while (remainingCount > 0) {
22132263

22142264
// Set base random generation parameters
2265+
logger.info("Generating infantry bay for params {}", params);
22152266
UnitGeneratorParameters newParams = params.clone();
2216-
newParams.clearMovementModes();
2217-
newParams.setWeightClass(AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED);
2267+
if (newParams == null) {
2268+
logger.info("newParams is null");
2269+
} else {
2270+
newParams.clearMovementModes();
2271+
newParams.setWeightClass(AtBDynamicScenarioFactory.UNIT_WEIGHT_UNSPECIFIED);
2272+
}
22182273

22192274
Entity transportedUnit = null;
22202275
Entity mechanizedBAUnit = null;
22212276

22222277
// If a roll against the battle armor target number succeeds, try to generate a
22232278
// battle armor unit first
2224-
if (d6(2) >= infantryToBAUpgradeTNs[params.getQuality()]) {
2279+
if (newParams != null && d6(2) >= infantryToBAUpgradeTNs[params.getQuality()]) {
22252280
newParams.setMissionRoles(requiredRoles.getOrDefault(BATTLE_ARMOR, new HashSet<>()));
22262281
transportedUnit = generateTransportedBAUnit(newParams, bayCapacity, skill, false, campaign);
22272282

@@ -2238,7 +2293,7 @@ private static List<Entity> fillTransport(AtBScenario scenario, Entity transport
22382293

22392294
// If a battle armor unit wasn't generated and conditions permit, try generating
22402295
// conventional infantry. Generate air assault infantry for VTOL transports.
2241-
if (transportedUnit == null && allowInfantry) {
2296+
if (newParams != null && transportedUnit == null && allowInfantry) {
22422297
newParams.setMissionRoles(requiredRoles.getOrDefault(INFANTRY, new HashSet<>()));
22432298
if (transport.getUnitType() == VTOL && !newParams.getMissionRoles().contains(XCT)) {
22442299
UnitGeneratorParameters paratrooperParams = newParams.clone();
@@ -2303,10 +2358,15 @@ private static List<Entity> fillTransport(AtBScenario scenario, Entity transport
23032358
*
23042359
* @return Generated infantry unit, or null if one cannot be generated
23052360
*/
2306-
private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters params, double bayCapacity,
2361+
private static @Nullable Entity generateTransportedInfantryUnit(UnitGeneratorParameters params, double bayCapacity,
23072362
SkillLevel skill, boolean useTempXCT, Campaign campaign) {
23082363

23092364
UnitGeneratorParameters newParams = params.clone();
2365+
if (newParams == null) {
2366+
logger.warn("newParams is null");
2367+
return null;
2368+
}
2369+
23102370
newParams.setUnitType(INFANTRY);
23112371
MekSummary unitData;
23122372
boolean temporaryXCT = false;
@@ -2317,7 +2377,6 @@ private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters pa
23172377
// which may
23182378
// include other types
23192379
if (bayCapacity <= IUnitGenerator.FOOT_PLATOON_INFANTRY_WEIGHT) {
2320-
23212380
if (newParams.getMissionRoles().contains(PARATROOPER)) {
23222381
newParams.setMovementModes(IUnitGenerator.ALL_INFANTRY_MODES);
23232382
} else {
@@ -2331,9 +2390,12 @@ private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters pa
23312390
// If XCT troops were requested but none were found, generate without the role
23322391
if (useTempXCT && newParams.getMissionRoles().contains(XCT)) {
23332392
noXCTParams = newParams.clone();
2334-
noXCTParams.getMissionRoles().remove(XCT);
2335-
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2336-
temporaryXCT = true;
2393+
2394+
if (noXCTParams != null) {
2395+
noXCTParams.getMissionRoles().remove(XCT);
2396+
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2397+
temporaryXCT = true;
2398+
}
23372399
}
23382400
if (unitData == null) {
23392401
return null;
@@ -2362,9 +2424,12 @@ private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters pa
23622424
// If XCT troops were requested but none were found, generate without the role
23632425
if (useTempXCT && newParams.getMissionRoles().contains(XCT)) {
23642426
noXCTParams = newParams.clone();
2365-
noXCTParams.getMissionRoles().remove(XCT);
2366-
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2367-
temporaryXCT = true;
2427+
2428+
if (noXCTParams != null) {
2429+
noXCTParams.getMissionRoles().remove(XCT);
2430+
unitData = campaign.getUnitGenerator().generate(noXCTParams);
2431+
temporaryXCT = true;
2432+
}
23682433
}
23692434
if (unitData == null) {
23702435
return null;
@@ -2394,7 +2459,7 @@ private static Entity generateTransportedInfantryUnit(UnitGeneratorParameters pa
23942459
*
23952460
* @return Generated battle armor entity with crew, null if one cannot be generated
23962461
*/
2397-
private static Entity generateTransportedBAUnit(UnitGeneratorParameters params, double bayCapacity,
2462+
private static @Nullable Entity generateTransportedBAUnit(UnitGeneratorParameters params, double bayCapacity,
23982463
SkillLevel skill, boolean retryAsMechanized, Campaign campaign) {
23992464

24002465
// Ensure a proposed non-mechanized carrier has enough bay space
@@ -2403,23 +2468,24 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params,
24032468
}
24042469

24052470
UnitGeneratorParameters newParams = params.clone();
2406-
newParams.setUnitType(BATTLE_ARMOR);
2407-
2408-
newParams.getMovementModes().addAll(IUnitGenerator.ALL_BATTLE_ARMOR_MODES);
2409-
2410-
// Set the parameters to filter out types that are too heavy for the provided
2411-
// bay space, or those that cannot use mechanized BA travel
2412-
if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT) {
2413-
newParams.setFilter(inf -> inf.getTons() <= bayCapacity);
2414-
} else {
2415-
newParams.addMissionRole(MECHANIZED_BA);
2471+
if (newParams != null) {
2472+
newParams.setUnitType(BATTLE_ARMOR);
2473+
newParams.getMovementModes().addAll(IUnitGenerator.ALL_BATTLE_ARMOR_MODES);
2474+
2475+
// Set the parameters to filter out types that are too heavy for the provided
2476+
// bay space, or those that cannot use mechanized BA travel
2477+
if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT) {
2478+
newParams.setFilter(inf -> inf.getTons() <= bayCapacity);
2479+
} else {
2480+
newParams.addMissionRole(MECHANIZED_BA);
2481+
}
24162482
}
24172483

24182484
MekSummary unitData = campaign.getUnitGenerator().generate(newParams);
24192485

24202486
// If generating for an internal bay fails, try again as mechanized if the flag is set
24212487
if (unitData == null) {
2422-
if (bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT && retryAsMechanized) {
2488+
if (newParams != null && bayCapacity != IUnitGenerator.NO_WEIGHT_LIMIT && retryAsMechanized) {
24232489
newParams.setFilter(null);
24242490
newParams.addMissionRole((MECHANIZED_BA));
24252491
unitData = campaign.getUnitGenerator().generate(newParams);
@@ -2430,7 +2496,11 @@ private static Entity generateTransportedBAUnit(UnitGeneratorParameters params,
24302496
}
24312497

24322498
// Add an appropriate crew
2433-
return createEntityWithCrew(newParams.getFaction(), skill, campaign, unitData);
2499+
if (newParams != null && unitData != null) {
2500+
return createEntityWithCrew(newParams.getFaction(), skill, campaign, unitData);
2501+
} else {
2502+
return null;
2503+
}
24342504
}
24352505

24362506
/**

MekHQ/src/mekhq/campaign/universe/UnitGeneratorParameters.java

+31-3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
import megamek.client.ratgenerator.Parameters;
3939
import megamek.common.EntityMovementMode;
4040
import megamek.common.MekSummary;
41+
import megamek.common.annotations.Nullable;
42+
import megamek.logging.MMLogger;
4143
import mekhq.campaign.mission.AtBDynamicScenarioFactory;
4244

4345
/**
@@ -46,7 +48,9 @@
4648
*
4749
* @author NickAragua
4850
*/
49-
public class UnitGeneratorParameters {
51+
public class UnitGeneratorParameters implements Cloneable {
52+
private static final MMLogger logger = MMLogger.create(UnitGeneratorParameters.class);
53+
5054
private String faction;
5155
private int unitType;
5256
private int weightClass;
@@ -65,7 +69,7 @@ public UnitGeneratorParameters() {
6569
* Thorough deep clone of this generator parameters object.
6670
*/
6771
@Override
68-
public UnitGeneratorParameters clone() {
72+
public @Nullable UnitGeneratorParameters clone() {
6973
try {
7074
UnitGeneratorParameters unitGeneratorParameters = (UnitGeneratorParameters) super.clone();
7175
unitGeneratorParameters.setFaction(faction);
@@ -79,12 +83,14 @@ public UnitGeneratorParameters clone() {
7983

8084
unitGeneratorParameters.setMovementModes(newModes);
8185

82-
for (MissionRole missionRole : missionRoles) {
86+
// We need a separate copy of the missionRoles collection to avoid concurrent modification
87+
for (MissionRole missionRole : new ArrayList<>(missionRoles)) {
8388
unitGeneratorParameters.addMissionRole(missionRole);
8489
}
8590

8691
return unitGeneratorParameters;
8792
} catch (CloneNotSupportedException e) {
93+
logger.error("Failed to clone UnitGeneratorParameters. State of the object: {}", this, e);
8894
return null;
8995
}
9096
}
@@ -194,4 +200,26 @@ public Predicate<MekSummary> getFilter() {
194200
public void setFilter(Predicate<MekSummary> filter) {
195201
this.filter = filter;
196202
}
203+
204+
@Override
205+
public String toString() {
206+
return "UnitGeneratorParameters{" +
207+
"faction=" +
208+
faction +
209+
", unitType=" +
210+
unitType +
211+
", weightClass=" +
212+
weightClass +
213+
", year=" +
214+
year +
215+
", quality=" +
216+
quality +
217+
", filter=" +
218+
filter +
219+
", movementModes=" +
220+
movementModes +
221+
", missionRoles=" +
222+
missionRoles +
223+
'}';
224+
}
197225
}

0 commit comments

Comments
 (0)