From e0895dd2307535987f3f57cac3195569822fc8b2 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Mon, 7 Apr 2025 12:58:37 -0500 Subject: [PATCH] Ensured StratCon Campaigns Always Generate At Least One Track - Refactored logic to extract reusable helper methods `getScenarioOdds` and `getDeploymentTime`, improving code clarity and maintainability. - Introduced fallback mechanism to guarantee a minimum of one track in StratCon Areas of Operation, preventing cases where zero tracks are generated. - Added `getTrackCount` method in `StratconCampaignState` to replace direct use of `getTracks().size()`. - Corrected potential division by zero errors when distributing track objects by ensuring a valid track count. --- .../stratcon/StratconCampaignState.java | 60 +++++++------ .../stratcon/StratconContractInitializer.java | 88 +++++++++++++++---- 2 files changed, 103 insertions(+), 45 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java index 1d73d4105af..47f8dd00e1e 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconCampaignState.java @@ -27,6 +27,12 @@ */ package mekhq.campaign.stratcon; +import java.io.PrintWriter; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import javax.xml.namespace.QName; + import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.Marshaller; @@ -44,12 +50,6 @@ import mekhq.campaign.mission.AtBScenario; import org.w3c.dom.Node; -import javax.xml.namespace.QName; -import java.io.PrintWriter; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; - /** * Contract-level state object for a StratCon campaign. * @@ -117,6 +117,10 @@ public List getTracks() { return tracks; } + public int getTrackCount() { + return tracks.size(); + } + public void addTrack(StratconTrackState track) { tracks.add(track); } @@ -144,13 +148,13 @@ public int getSupportPoints() { * Modifies the current support points by the specified amount. * *

- * This method increases or decreases the support points by the given number. - * It adds the value of {@code change} to the existing support points total. - * This can be used to reflect changes due to various gameplay events or actions. + * This method increases or decreases the support points by the given number. It adds the value of {@code change} to + * the existing support points total. This can be used to reflect changes due to various gameplay events or + * actions. *

* - * @param change The amount to adjust the support points by. Positive values will - * increase the support points, while negative values will decrease them. + * @param change The amount to adjust the support points by. Positive values will increase the support points, while + * negative values will decrease them. */ public void changeSupportPoints(int change) { supportPoints += change; @@ -210,10 +214,11 @@ public void useSupportPoints(int decrement) { } /** - * Convenience/speed method of determining whether or not a force with the given - * ID has been deployed to a track in this campaign. + * Convenience/speed method of determining whether or not a force with the given ID has been deployed to a track in + * this campaign. * * @param forceID the force ID to check + * * @return Deployed or not. */ public boolean isForceDeployedHere(int forceID) { @@ -227,8 +232,7 @@ public boolean isForceDeployedHere(int forceID) { } /** - * Removes the scenario with the given campaign scenario ID from any tracks - * where it's present + * Removes the scenario with the given campaign scenario ID from any tracks where it's present */ public void removeStratconScenario(int scenarioID) { for (StratconTrackState trackState : tracks) { @@ -240,23 +244,23 @@ public void removeStratconScenario(int scenarioID) { * Retrieves the {@link StratconScenario} associated with a given {@link AtBScenario}. * *

- * This method searches through all {@link StratconTrackState} objects in the {@link StratconCampaignState} - * to find the first {@link StratconScenario} whose backing scenario matches the specified {@link AtBScenario}. - * If no such scenario is found, it returns {@code null}. + * This method searches through all {@link StratconTrackState} objects in the {@link StratconCampaignState} to find + * the first {@link StratconScenario} whose backing scenario matches the specified {@link AtBScenario}. If no such + * scenario is found, it returns {@code null}. *

* * Usage: *

- * Use this method to easily fetch the {@link StratconScenario} associated with the provided - * {@link AtBScenario}. + * Use this method to easily fetch the {@link StratconScenario} associated with the provided {@link AtBScenario}. *

* * @param campaign The {@link Campaign} containing the data to search through. * @param scenario The {@link AtBScenario} to find the corresponding {@link StratconScenario} for. + * * @return The matching {@link StratconScenario}, or {@code null} if no corresponding scenario is found. */ public static @Nullable StratconScenario getStratconScenarioFromAtBScenario(Campaign campaign, - AtBScenario scenario) { + AtBScenario scenario) { AtBContract contract = scenario.getContract(campaign); if (contract == null) { return null; @@ -279,8 +283,7 @@ public void removeStratconScenario(int scenarioID) { } /** - * Serialize this instance of a campaign state to a PrintWriter - * Omits initial xml declaration + * Serialize this instance of a campaign state to a PrintWriter Omits initial xml declaration * * @param pw The destination print writer */ @@ -288,7 +291,8 @@ public void Serialize(PrintWriter pw) { try { JAXBContext context = JAXBContext.newInstance(StratconCampaignState.class); JAXBElement stateElement = new JAXBElement<>(new QName(ROOT_XML_ELEMENT_NAME), - StratconCampaignState.class, this); + StratconCampaignState.class, + this); Marshaller m = context.createMarshaller(); m.setProperty(Marshaller.JAXB_FRAGMENT, true); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); @@ -299,10 +303,10 @@ public void Serialize(PrintWriter pw) { } /** - * Attempt to deserialize an instance of a Campaign State from the passed-in XML - * Node + * Attempt to deserialize an instance of a Campaign State from the passed-in XML Node * * @param xmlNode The node with the campaign state + * * @return Possibly an instance of a StratconCampaignState */ public static StratconCampaignState Deserialize(Node xmlNode) { @@ -331,8 +335,8 @@ public static StratconCampaignState Deserialize(Node xmlNode) { } /** - * This adapter provides a way to convert between a LocalDate and the ISO-8601 string - * representation of the date that is used for XML marshaling and unmarshalling in JAXB. + * This adapter provides a way to convert between a LocalDate and the ISO-8601 string representation of the date + * that is used for XML marshaling and unmarshalling in JAXB. */ public static class LocalDateAdapter extends XmlAdapter { @Override diff --git a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java index 6c042738b7f..43fe3c575da 100644 --- a/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java +++ b/MekHQ/src/mekhq/campaign/stratcon/StratconContractInitializer.java @@ -65,7 +65,8 @@ public class StratconContractInitializer { /** * Initializes the campaign state given a contract, campaign and contract definition */ - public static void initializeCampaignState(AtBContract contract, Campaign campaign, StratconContractDefinition contractDefinition) { + public static void initializeCampaignState(AtBContract contract, Campaign campaign, + StratconContractDefinition contractDefinition) { StratconCampaignState campaignState = new StratconCampaignState(contract); campaignState.setBriefingText(contractDefinition.getBriefing() + "
" + @@ -91,10 +92,8 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai int planetaryTemperature = campaign.getLocation().getPlanet().getTemperature(campaign.getLocalDate()); for (int x = 0; x < maximumTrackIndex; x++) { - int scenarioOdds = contractDefinition.getScenarioOdds() - .get(Compute.randomInt(contractDefinition.getScenarioOdds().size())); - int deploymentTime = contractDefinition.getDeploymentTimes() - .get(Compute.randomInt(contractDefinition.getDeploymentTimes().size())); + int scenarioOdds = getScenarioOdds(contractDefinition); + int deploymentTime = getDeploymentTime(contractDefinition); StratconTrackState track = initializeTrackState(NUM_LANCES_PER_TRACK, scenarioOdds, @@ -109,16 +108,24 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai // required lances. int oddLanceCount = contract.getRequiredCombatTeams() % NUM_LANCES_PER_TRACK; if (oddLanceCount > 0) { - int scenarioOdds = contractDefinition.getScenarioOdds() - .get(Compute.randomInt(contractDefinition.getScenarioOdds().size())); - int deploymentTime = contractDefinition.getDeploymentTimes() - .get(Compute.randomInt(contractDefinition.getDeploymentTimes().size())); + int scenarioOdds = getScenarioOdds(contractDefinition); + int deploymentTime = getDeploymentTime(contractDefinition); StratconTrackState track = initializeTrackState(oddLanceCount, scenarioOdds, deploymentTime, planetaryTemperature); - track.setDisplayableName(String.format("Sector %d", campaignState.getTracks().size())); + track.setDisplayableName(String.format("Sector %d", campaignState.getTrackCount())); + campaignState.addTrack(track); + } + + // Last chance generation, to ensure we never generate a StratCon map with 0 tracks + if (campaignState.getTrackCount() == 0) { + int scenarioOdds = getScenarioOdds(contractDefinition); + int deploymentTime = getDeploymentTime(contractDefinition); + + StratconTrackState track = initializeTrackState(1, scenarioOdds, deploymentTime, planetaryTemperature); + track.setDisplayableName(String.format("Sector %d", campaignState.getTrackCount())); campaignState.addTrack(track); } @@ -129,7 +136,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai (int) Math.max(1, -objectiveParams.objectiveCount * contract.getRequiredCombatTeams()); - List trackObjects = trackObjectDistribution(objectiveCount, campaignState.getTracks().size()); + List trackObjects = trackObjectDistribution(objectiveCount, campaignState.getTrackCount()); for (int x = 0; x < trackObjects.size(); x++) { int numObjects = trackObjects.get(x); @@ -197,7 +204,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai (int) (-contractDefinition.getAlliedFacilityCount() * contract.getRequiredCombatTeams()); - List trackObjects = trackObjectDistribution(facilityCount, campaignState.getTracks().size()); + List trackObjects = trackObjectDistribution(facilityCount, campaignState.getTrackCount()); for (int x = 0; x < trackObjects.size(); x++) { int numObjects = trackObjects.get(x); @@ -214,7 +221,7 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai (int) contractDefinition.getHostileFacilityCount() : (int) (-contractDefinition.getHostileFacilityCount() * contract.getRequiredCombatTeams()); - trackObjects = trackObjectDistribution(facilityCount, campaignState.getTracks().size()); + trackObjects = trackObjectDistribution(facilityCount, campaignState.getTrackCount()); for (int x = 0; x < trackObjects.size(); x++) { int numObjects = trackObjects.get(x); @@ -283,10 +290,50 @@ public static void initializeCampaignState(AtBContract contract, Campaign campai // now we're done } + /** + * Retrieves a random deployment time from the provided {@link StratconContractDefinition}. + * + *

The deployment time is selected randomly from the list of deployment times in the + * given {@code StratconContractDefinition}.

+ * + * @param contractDefinition the contract definition containing deployment time options + * + * @return a randomly selected deployment time + * + * @throws IllegalArgumentException if the list of deployment times is empty + * @throws NullPointerException if {@code contractDefinition} or its deployment times list is null + * @author Illiani + * @since 0.50.05 + */ + private static int getDeploymentTime(StratconContractDefinition contractDefinition) { + return contractDefinition.getDeploymentTimes() + .get(Compute.randomInt(contractDefinition.getDeploymentTimes().size())); + } + + /** + * Retrieves a random scenario odds value from the provided {@link StratconContractDefinition}. + * + *

The scenario odds are selected randomly from the list of scenario odds in the + * given {@code StratconContractDefinition}.

+ * + * @param contractDefinition the contract definition containing scenario odds options + * + * @return a randomly selected scenario odds value + * + * @throws IllegalArgumentException if the list of scenario odds is empty + * @throws NullPointerException if {@code contractDefinition} or its scenario odds list is null + * @author Illiani + * @since 0.50.05 + */ + private static int getScenarioOdds(StratconContractDefinition contractDefinition) { + return contractDefinition.getScenarioOdds().get(Compute.randomInt(contractDefinition.getScenarioOdds().size())); + } + /** * Set up initial state of a track, dimensions are based on number of assigned lances. */ - public static StratconTrackState initializeTrackState(int numLances, int scenarioOdds, int deploymentTime, int planetaryTemp) { + public static StratconTrackState initializeTrackState(int numLances, int scenarioOdds, int deploymentTime, + int planetaryTemp) { // to initialize a track, // 1. we set the # of required lances // 2. set the track size to a total of numlances * 28 hexes, a rectangle that is @@ -325,6 +372,9 @@ public static StratconTrackState initializeTrackState(int numLances, int scenari * Generates an array list representing the number of objects to place in a given number of tracks. */ private static List trackObjectDistribution(int numObjects, int numTracks) { + // This ensures we're not at risk of dividing by 0 + numTracks = Math.max(1, numTracks); + List retVal = new ArrayList<>(); int leftOver = numObjects % numTracks; @@ -350,7 +400,8 @@ private static List trackObjectDistribution(int numObjects, int numTrac * Avoids places with existing facilities and scenarios, capable of taking facility sub set and setting strategic * objective flag. */ - private static void initializeTrackFacilities(StratconTrackState trackState, int numFacilities, ForceAlignment owner, boolean strategicObjective, List modifiers) { + private static void initializeTrackFacilities(StratconTrackState trackState, int numFacilities, + ForceAlignment owner, boolean strategicObjective, List modifiers) { int trackSize = trackState.getWidth() * trackState.getHeight(); @@ -424,7 +475,9 @@ private static void initializeTrackFacilities(StratconTrackState trackState, int * @param objectiveModifiers a list of optional {@link String} modifiers to apply to the generated scenarios; can be * {@code null} if no modifiers are required */ - private static void initializeObjectiveScenarios(Campaign campaign, AtBContract contract, StratconTrackState trackState, int numScenarios, List objectiveScenarios, List objectiveModifiers) { + private static void initializeObjectiveScenarios(Campaign campaign, AtBContract contract, + StratconTrackState trackState, int numScenarios, List objectiveScenarios, + List objectiveModifiers) { // pick scenario from subset // place it on the map somewhere nothing else has been placed yet // if it's a facility scenario, place the facility @@ -551,7 +604,8 @@ private static void initializeObjectiveScenarios(Campaign campaign, AtBContract * @return a {@link StratconCoords} object representing the location of a suitable, unoccupied coordinate, or * {@code null} if no valid coordinates are available */ - public static @Nullable StratconCoords getUnoccupiedCoords(StratconTrackState trackState, boolean allowPlayerFacilities, boolean allowPlayerForces, boolean emphasizeStrategicTargets) { + public static @Nullable StratconCoords getUnoccupiedCoords(StratconTrackState trackState, + boolean allowPlayerFacilities, boolean allowPlayerForces, boolean emphasizeStrategicTargets) { final int trackHeight = trackState.getHeight(); final int trackWidth = trackState.getWidth();