From 5de4dd15fce4ad9f1dfd753d229d74075aa7dec4 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 20 Dec 2024 11:59:35 -0600 Subject: [PATCH 1/3] Refactored resupply logic and streamlined cargo calculations. Simplified resupply calculations by removing lance-based drop counts and introduced weight multipliers for specific items. Improved convoy item allocation by addressing edge cases, randomizing selection, and ensuring efficient distribution of cargo capacity. Added enhanced logging and insured consistent handling of null cases. --- MekHQ/src/mekhq/campaign/Campaign.java | 4 +- .../mekhq/campaign/mission/AtBContract.java | 2 +- .../GenerateResupplyContents.java | 37 +++++++--- .../resupplyAndCaches/PerformResupply.java | 74 +++++++++++-------- .../mission/resupplyAndCaches/Resupply.java | 2 +- .../resupplyAndCaches/ResupplyUtilities.java | 10 +-- .../resupplyAndCaches/DialogItinerary.java | 10 +++ .../stratcon/CampaignManagementDialog.java | 4 +- .../src/mekhq/gui/view/MissionViewPanel.java | 2 - 9 files changed, 87 insertions(+), 58 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index 496f0e517b..955077c898 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -4102,11 +4102,9 @@ private void processResupply(AtBContract contract) { boolean isGuerrilla = contract.getContractType().isGuerrillaWarfare(); if (!isGuerrilla || Compute.d6(1) > 4) { - int dropCount = (int) round((double) contract.getRequiredLances() / 3); - ResupplyType resupplyType = isGuerrilla ? ResupplyType.RESUPPLY_SMUGGLER : ResupplyType.RESUPPLY_NORMAL; Resupply resupply = new Resupply(this, contract, resupplyType); - performResupply(resupply, contract, dropCount); + performResupply(resupply, contract); } } diff --git a/MekHQ/src/mekhq/campaign/mission/AtBContract.java b/MekHQ/src/mekhq/campaign/mission/AtBContract.java index 91b3fc85ef..ceed87feff 100644 --- a/MekHQ/src/mekhq/campaign/mission/AtBContract.java +++ b/MekHQ/src/mekhq/campaign/mission/AtBContract.java @@ -906,7 +906,7 @@ public void checkEvents(Campaign campaign) { if (doBonusRoll(campaign)) { campaign.addReport("Bonus: Captured Supplies"); Resupply resupply = new Resupply(campaign, this, ResupplyType.RESUPPLY_LOOT); - performResupply(resupply, this, 1); + performResupply(resupply, this); } break; diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java index 054430243d..83bc4b8a41 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/GenerateResupplyContents.java @@ -21,6 +21,7 @@ import megamek.codeUtilities.ObjectUtility; import megamek.common.Compute; import megamek.common.annotations.Nullable; +import megamek.logging.MMLogger; import mekhq.campaign.Campaign; import mekhq.campaign.finances.Money; import mekhq.campaign.mission.AtBContract; @@ -44,6 +45,8 @@ * item value constraints. */ public class GenerateResupplyContents { + private static final MMLogger logger = MMLogger.create(GenerateResupplyContents.class); + private static final Money HIGH_VALUE_ITEM = Money.of(250000); /** @@ -66,6 +69,10 @@ public enum DropType { * @param usePlayerConvoys Indicates whether player convoy cargo capacity should be applied. */ static void getResupplyContents(Resupply resupply, DropType dropType, boolean usePlayerConvoys) { + // Ammo and Armor are delivered in batches of 5, so we need to make sure to multiply their + // weight by five when picking these items. + final int WEIGHT_MULTIPLIER = dropType == DropType.DROP_TYPE_PARTS ? 1 : 5; + double targetCargoTonnage = resupply.getTargetCargoTonnage(); if (usePlayerConvoys) { final int targetCargoTonnagePlayerConvoy = resupply.getTargetCargoTonnagePlayerConvoy(); @@ -79,22 +86,25 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us final int negotiatorSkill = resupply.getNegotiatorSkill(); - List resupplyContents = resupply.getConvoyContents(); - List droppedItems = new ArrayList<>(); - double runningTotal = 0; - double targetValue = switch (dropType) { + double availableSpace = switch (dropType) { case DROP_TYPE_PARTS -> targetCargoTonnage * resupply.getFocusParts(); case DROP_TYPE_ARMOR -> targetCargoTonnage * resupply.getFocusArmor(); case DROP_TYPE_AMMO -> targetCargoTonnage * resupply.getFocusAmmo(); }; - if (targetValue == 0) { + if (availableSpace == 0) { return; } - while (runningTotal < targetValue) { + List relevantPartsPool = switch(dropType) { + case DROP_TYPE_PARTS -> partsPool; + case DROP_TYPE_ARMOR -> armorPool; + case DROP_TYPE_AMMO -> ammoBinPool; + }; + + while ((availableSpace > 0) && (!relevantPartsPool.isEmpty())) { Part potentialPart = switch(dropType) { case DROP_TYPE_PARTS -> getRandomDrop(partsPool, negotiatorSkill); case DROP_TYPE_ARMOR -> getRandomDrop(armorPool, negotiatorSkill); @@ -105,8 +115,9 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us // Even if the pool isn't empty, it's highly unlikely we'll get a successful pull on // future iterations, so we end generation early. if (potentialPart == null) { - resupplyContents.addAll(droppedItems); - resupply.setConvoyContents(resupplyContents); + resupply.getConvoyContents().addAll(droppedItems); + calculateConvoyWorth(resupply); + logger.info("Encountered null part while getting resupply contents. Aborting early."); return; } @@ -136,13 +147,15 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us case DROP_TYPE_AMMO -> ammoBinPool.remove(potentialPart); } - runningTotal += potentialPart.getTonnage(); - droppedItems.add(potentialPart); + availableSpace -= potentialPart.getTonnage() * WEIGHT_MULTIPLIER; + + if (availableSpace >= 0) { + droppedItems.add(potentialPart); + } } } - resupplyContents.addAll(droppedItems); - resupply.setConvoyContents(resupplyContents); + resupply.getConvoyContents().addAll(droppedItems); calculateConvoyWorth(resupply); } diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java index 68b04b7e65..9176a2b2d8 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/PerformResupply.java @@ -77,6 +77,26 @@ public class PerformResupply { private static final MMLogger logger = MMLogger.create(PerformResupply.class); + /** + * Initiates the resupply process for a specified campaign and active contract. + * + *

This method provides a simplified entry point to the resupply workflow, using a default value + * of 1 for the supply drop count. It delegates to the overloaded method + * {@link #performResupply(Resupply, AtBContract, int)} for the main execution of the resupply + * process, encompassing supply generation, convoy interaction, and delivery confirmation.

+ * + *

This entry point is typically used when the exact number of supply drops is not specified or + * defaults to a single drop per invocation.

+ * + * @param resupply the {@link Resupply} instance containing information about the resupply operation, + * such as the supplies to be delivered, convoy setup, and context-specific rules. + * @param contract the {@link AtBContract} representing the current contract, which provides the + * operational context for the resupply, including permissions and restrictions. + */ + public static void performResupply(Resupply resupply, AtBContract contract) { + performResupply(resupply, contract, 1); + } + /** * Executes the resupply process for a specified campaign, contract, and supply drop count. * This method coordinates supply allocation, convoy interaction, potential for interception, @@ -142,6 +162,16 @@ public static void performResupply(Resupply resupply, AtBContract contract, int getResupplyContents(resupply, DROP_TYPE_PARTS, isUsePlayerConvoy); } + resupply.setConvoyContents(resupply.getConvoyContents()); + + double totalTonnage = 0; + for (Part part : resupply.getConvoyContents()) { + totalTonnage += part.getTonnage() * (part instanceof Armor || part instanceof AmmoBin ? 5 : 1); + } + + logger.info("totalTonnage: " + totalTonnage); + + // This shouldn't occur, but we include it as insurance. if (resupply.getConvoyContents().isEmpty()) { campaign.addReport(String.format(resources.getString("convoyUnsuccessful.text"), @@ -222,7 +252,9 @@ public static void makeSmugglerDelivery(Resupply resupply) { * @param resupply the {@link Resupply} instance containing convoy and mission-specific data. */ public static void loadPlayerConvoys(Resupply resupply) { - final Campaign campaign = resupply.getCampaign(); + // Ammo and Armor are delivered in batches of 5, so we need to make sure to multiply their + // weight by five when picking these items. + final int WEIGHT_MULTIPLIER = 5; final Map playerConvoys = resupply.getPlayerConvoys(); // Sort the player's available convoys according to cargo space, largest -> smallest @@ -235,10 +267,8 @@ public static void loadPlayerConvoys(Resupply resupply) { sortedConvoys.add(entry.getKey()); } - // Sort the available parts according to weight final List convoyContents = resupply.getConvoyContents(); - convoyContents.sort((part1, part2) -> - Double.compare(part2.getTonnage(), part1.getTonnage())); + Collections.shuffle(convoyContents); // Distribute parts across the convoys for (Force convoy : sortedConvoys) { @@ -249,40 +279,22 @@ public static void loadPlayerConvoys(Resupply resupply) { Double cargoCapacity = playerConvoys.get(convoy); List convoyItems = new ArrayList<>(); - // It is technically possible to end up with one or more items that won't fit, - // we need an early break to avoid situations where we infinite loop due to a - // particularly large item. - while (!convoyContents.isEmpty() && cargoCapacity > 0) { - boolean partAdded = false; // Ensure at least one part is removed per iteration - - // Iterate over parts to find what can fit - Iterator iterator = convoyContents.iterator(); - while (iterator.hasNext()) { - Part part = iterator.next(); - if (cargoCapacity - part.getTonnage() >= 0) { - convoyItems.add(part); - cargoCapacity -= part.getTonnage(); - iterator.remove(); // Remove directly during iteration - partAdded = true; - } + for (Part part : convoyContents) { + double tonnage = part.getTonnage(); + + if (part instanceof AmmoBin || part instanceof Armor) { + tonnage *= WEIGHT_MULTIPLIER; } - // Break the loop if no part was added in this iteration to avoid infinite looping - if (!partAdded) { - break; + if (cargoCapacity - tonnage >= 0) { + convoyItems.add(part); + cargoCapacity -= tonnage; } } + convoyContents.removeAll(convoyItems); processConvoy(resupply, convoyItems, convoy); } - - if (!convoyContents.isEmpty()) { - campaign.addReport(String.format(resources.getString("convoyInsufficientSize.text"))); - - for (Part part : convoyContents) { - campaign.addReport("- " + part.getName()); - } - } } /** diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 9874c46837..d775b87fc2 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -104,8 +104,8 @@ public Resupply(Campaign campaign, AtBContract contract, ResupplyType resupplyTy focusArmor = 0.25; focusParts = 0.5; - buildPartsPools(collectParts()); calculateNegotiationSkill(); + buildPartsPools(collectParts()); calculatePlayerConvoyValues(); convoyContents = new ArrayList<>(); diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java index 380eb1eada..ccf016c7bf 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/ResupplyUtilities.java @@ -30,7 +30,7 @@ import java.util.UUID; -import static java.lang.Math.round; +import static java.lang.Math.ceil; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.CARGO_MULTIPLIER; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.calculateTargetCargoTonnage; import static mekhq.campaign.personnel.enums.PersonnelStatus.KIA; @@ -128,13 +128,13 @@ private static void decideCrewMemberFate(Campaign campaign, Person person) { /** * Estimates the total cargo requirements for a resupply operation based on the campaign - * and the associated contract details. + * and the associated contract details. These cargo requirements are specifically modified for + * player-owned convoys. * *

This estimation is calculated as follows: *

    *
  • Determines the target cargo tonnage using the {@link Campaign} and {@link AtBContract} data.
  • *
  • Applies a cargo multiplier defined in {@link Resupply#CARGO_MULTIPLIER}.
  • - *
  • Accounts for the required number of supply drops, assuming one drop per three lances.
  • *
* * @param campaign the {@link Campaign} instance to calculate cargo requirements for. @@ -142,8 +142,6 @@ private static void decideCrewMemberFate(Campaign campaign, Person person) { * @return the estimated cargo requirement in tons. */ public static int estimateCargoRequirements(Campaign campaign, AtBContract contract) { - final double dropCount = (double) contract.getRequiredLances() / 3; - - return (int) round(calculateTargetCargoTonnage(campaign, contract) * CARGO_MULTIPLIER * dropCount); + return (int) ceil(calculateTargetCargoTonnage(campaign, contract) * CARGO_MULTIPLIER); } } diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java index 41f539dbe0..56f27ff821 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java @@ -24,6 +24,7 @@ import mekhq.campaign.mission.enums.AtBMoraleLevel; import mekhq.campaign.mission.resupplyAndCaches.Resupply; import mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType; +import mekhq.campaign.parts.Part; import mekhq.campaign.personnel.Person; import mekhq.campaign.personnel.enums.PersonnelRole; @@ -168,6 +169,15 @@ public static void itineraryDialog(Resupply resupply) { } else { if (resupply.getUsePlayerConvoy()) { loadPlayerConvoys(resupply); + + final List convoyContents = resupply.getConvoyContents(); + if (!convoyContents.isEmpty()) { + campaign.addReport(String.format(resources.getString("convoyInsufficientSize.text"))); + + for (Part part : convoyContents) { + campaign.addReport("- " + part.getName()); + } + } } else { processConvoy(resupply, resupply.getConvoyContents(), null); } diff --git a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java index e5e8bd63da..7e84bb120a 100644 --- a/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java +++ b/MekHQ/src/mekhq/gui/stratcon/CampaignManagementDialog.java @@ -164,7 +164,7 @@ private void requestResupply(ActionEvent event) { } else { AtBContract contract = currentCampaignState.getContract(); Resupply resupply = new Resupply(campaign, contract, ResupplyType.RESUPPLY_NORMAL); - performResupply(resupply, contract, 1); + performResupply(resupply, contract); } btnRequestResupply.setEnabled(currentCampaignState.getSupportPoints() > 0); @@ -207,7 +207,7 @@ public void supplyDropDialog() { AtBContract contract = currentCampaignState.getContract(); Resupply resupply = new Resupply(campaign, contract, ResupplyType.RESUPPLY_NORMAL); - performResupply(resupply, contract, 1); + performResupply(resupply, contract); currentCampaignState.useSupportPoints((int) numberModel.getValue()); }); diff --git a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java index 6ef6d2a579..9e980cae6d 100644 --- a/MekHQ/src/mekhq/gui/view/MissionViewPanel.java +++ b/MekHQ/src/mekhq/gui/view/MissionViewPanel.java @@ -986,7 +986,6 @@ public void mouseClicked(MouseEvent e) { if (campaign.getCampaignOptions().isUseStratCon()) { lblCargoRequirement.setName("lblCargoRequirement"); lblCargoRequirement.setText(resourceMap.getString("lblCargoRequirement.text")); - lblCargoRequirement.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText())); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = y; @@ -996,7 +995,6 @@ public void mouseClicked(MouseEvent e) { txtCargoRequirement.setName("txtCargoRequirement"); txtCargoRequirement.setText(estimateCargoRequirements(campaign, contract) + "t"); - txtCargoRequirement.setToolTipText(wordWrap(contract.getMoraleLevel().getToolTipText())); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = y++; From 2538af0b52e3607044a92fb95af09804efb0bbef Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 20 Dec 2024 12:16:29 -0600 Subject: [PATCH 2/3] Add check for MotiveSystem in resupply eligibility Implemented a new method to verify if parts are instances of `MotiveSystem` and included it in the resupply check logic. This ensures MotiveSystem components are properly assessed for eligibility during resupply. --- .../campaign/mission/resupplyAndCaches/Resupply.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java index 9874c46837..ddf96ed34f 100644 --- a/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java +++ b/MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java @@ -566,6 +566,7 @@ private boolean isIneligiblePart(Part part, Unit unit) { return checkExclusionList(part) || checkMekLocation(part, unit) || checkTankLocation(part) + || checkMotiveSystem(part) || checkTransporter(part); } @@ -589,6 +590,16 @@ private boolean checkExclusionList(Part part) { return false; } + /** + * Checks if the given part is an instance of {@code MotiveSystem}. + * + * @param part the {@link Part} to be checked. + * @return {@code true} if the part is a {@link MotiveSystem}, {@code false} otherwise. + */ + private boolean checkMotiveSystem(Part part) { + return part instanceof MotiveSystem; + } + /** * Checks if a part belonging to a 'Mek' unit is eligible for resupply, based on its location * or whether the unit is considered extinct. For example, parts located in the center torso From d5e77fadd3db114c33d26cfb2650978ae944ebd8 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Fri, 20 Dec 2024 12:32:11 -0600 Subject: [PATCH 3/3] Clarified speaker names in resupply dialog interfaces. Updated speaker name logic in `DialogInterception` and `DialogRoleplayEvent` to include convoy names for improved context. This ensures clearer identification of involved entities during dialogs. --- .../gui/dialog/resupplyAndCaches/DialogInterception.java | 4 ++-- .../gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java index 9114218c31..23f2601d73 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java @@ -91,13 +91,13 @@ public static void dialogInterception(Resupply resupply, @Nullable Force targetC String speakerName; if (speaker != null) { - speakerName = speaker.getFullTitle(); + speakerName = speaker.getFullTitle() + " - " + targetConvoy.getName(); } else { if (targetConvoy == null) { speakerName = String.format(resources.getString("dialogBorderConvoySpeakerDefault.text"), contract.getEmployerName(campaign.getGameYear())); } else { - speakerName = campaign.getName(); + speakerName = targetConvoy.getName(); } } diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java index da75bfacc4..b3f0a89e9b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java @@ -92,7 +92,7 @@ public static void dialogConvoyRoleplayEvent(Campaign campaign, Force playerConv String speakerName; if (speaker != null) { - speakerName = speaker.getFullTitle(); + speakerName = speaker.getFullTitle() + " - " + playerConvoy.getName(); } else { speakerName = playerConvoy.getName(); }