Skip to content

Commit

Permalink
Merge pull request #5675 from IllianiCBT/resupply_bugFixes
Browse files Browse the repository at this point in the history
Multiple Resupply Bug Fixes
  • Loading branch information
HammerGS authored Jan 7, 2025
2 parents 52c93a7 + 81076ea commit 5a38816
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 47 deletions.
6 changes: 3 additions & 3 deletions MekHQ/resources/mekhq/resources/Resupply.properties
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ usePlayerConvoyOptional.text=%s, our employer has a delivery ready for us but is
<br>\
<br>This <b>enhanced</b> delivery requires an estimated <b>%s</b> tons of cargo space across all\
\ convoys. However, as this is an estimate final tonnage may vary. We currently have a total of\
\ <b>%s</b> available space across <b>%s</b> convoy%s. Damaged or partially vehicles are not\
\ considered available.\
\ <b>%s</b> available space across <b>%s</b> convoy%s. Damaged or partially crewed vehicles are\
\ not considered available.\
<br>\
<br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\
\ units and personnel will be lost.\
Expand All @@ -81,7 +81,7 @@ usePlayerConvoyForced.text=%s, our employer has a delivery ready for us, but we
<br>\
<br>This delivery requires an estimated <b>%s</b> tons of cargo space across all convoys. However,\
\ as this is an estimate final tonnage may vary. We currently have a total of <b>%s</b> available\
\ space across <b>%s</b> convoy%s. Damaged or partially vehicles are not considered available.\
\ space across <b>%s</b> convoy%s. Damaged or partially crewed vehicles are not considered available.\
<br>\
<br>Be aware that this can be a risky job and if we fail to defend any intercepted convoys all\
\ units and personnel will be lost.\
Expand Down
2 changes: 0 additions & 2 deletions MekHQ/src/mekhq/campaign/market/procurement/Procurement.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ public List<Part> makeProcurementChecks(List<Part> parts, boolean useHardExtinct
}
}

logger.info(successfulParts);

return successfulParts;
}

Expand Down
10 changes: 10 additions & 0 deletions MekHQ/src/mekhq/campaign/mission/Loot.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
import mekhq.campaign.finances.Money;
import mekhq.campaign.finances.enums.TransactionType;
import mekhq.campaign.mission.enums.ScenarioType;
import mekhq.campaign.parts.Armor;
import mekhq.campaign.parts.Part;
import mekhq.campaign.parts.enums.PartQuality;
import mekhq.campaign.parts.equipment.AmmoBin;
import mekhq.campaign.rating.IUnitRating;
import mekhq.campaign.unit.Unit;
import mekhq.utilities.MHQXMLUtility;
Expand All @@ -45,6 +47,8 @@
import java.util.*;

import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;
import static mekhq.utilities.ReportingUtilities.CLOSING_SPAN_TAG;
import static mekhq.utilities.ReportingUtilities.spanOpeningWithCustomColor;

Expand Down Expand Up @@ -214,6 +218,12 @@ public void getLoot(Campaign campaign, Scenario scenario,
double partWeight = part.getTonnage();
partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight;

if (part instanceof AmmoBin) {
partWeight = RESUPPLY_AMMO_TONNAGE;
} else if (part instanceof Armor) {
partWeight = RESUPPLY_ARMOR_TONNAGE;
}

if (isResupply) {
if (cargo - partWeight < 0) {
abandonedParts.add("<br>- " + part.getName() + " (" + partWeight + " tons)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ 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();
Expand Down Expand Up @@ -112,7 +108,8 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us
case DROP_TYPE_AMMO -> ammoBinPool;
};

while ((availableSpace > 0) && (!relevantPartsPool.isEmpty())) {
double currentLoad = 0;
while ((currentLoad < availableSpace) && (!relevantPartsPool.isEmpty())) {
Part potentialPart = switch(dropType) {
case DROP_TYPE_PARTS -> getRandomDrop(partsPool, negotiatorSkill);
case DROP_TYPE_ARMOR -> getRandomDrop(armorPool, negotiatorSkill);
Expand Down Expand Up @@ -155,10 +152,16 @@ static void getResupplyContents(Resupply resupply, DropType dropType, boolean us
case DROP_TYPE_AMMO -> ammoBinPool.remove(potentialPart);
}

double partWeight = potentialPart.getTonnage();
partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight;
// Ammo and Armor are delivered in batches of 5t,
// so we need to make sure we're treating them as 5t no matter their actual weight.
double partWeight = 5;

if (dropType == DropType.DROP_TYPE_PARTS) {
partWeight = potentialPart.getTonnage();
partWeight = partWeight == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : partWeight;
}

availableSpace -= partWeight * WEIGHT_MULTIPLIER;
currentLoad += partWeight;
droppedItems.add(potentialPart);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@

import static mekhq.campaign.mission.enums.AtBMoraleLevel.CRITICAL;
import static mekhq.campaign.mission.enums.AtBMoraleLevel.DOMINATING;
import static mekhq.campaign.mission.enums.AtBMoraleLevel.ROUTED;
import static mekhq.campaign.mission.enums.AtBMoraleLevel.STALEMATE;
import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_AMMO;
import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_ARMOR;
import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.DropType.DROP_TYPE_PARTS;
import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.RESUPPLY_MINIMUM_PART_WEIGHT;
import static mekhq.campaign.mission.resupplyAndCaches.GenerateResupplyContents.getResupplyContents;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_CONTRACT_END;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT;
import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.forceContainsMajorityVTOLForces;
Expand Down Expand Up @@ -167,7 +168,13 @@ public static void performResupply(Resupply resupply, AtBContract contract, int

double totalTonnage = 0;
for (Part part : resupply.getConvoyContents()) {
totalTonnage += part.getTonnage() * (part instanceof Armor || part instanceof AmmoBin ? 5 : 1);
if (part instanceof AmmoBin) {
totalTonnage += RESUPPLY_AMMO_TONNAGE;
} else if (part instanceof Armor) {
totalTonnage += RESUPPLY_ARMOR_TONNAGE;
} else {
totalTonnage += part.getTonnage();
}
}

logger.info("totalTonnage: " + totalTonnage);
Expand Down Expand Up @@ -207,9 +214,9 @@ public static void makeDelivery(Resupply resupply, @Nullable List<Part> contents
for (Part part : contents) {
if (part instanceof AmmoBin) {
campaign.getQuartermaster().addAmmo(((AmmoBin) part).getType(),
((AmmoBin) part).getFullShots() * 5);
((AmmoBin) part).getFullShots() * RESUPPLY_AMMO_TONNAGE);
} else if (part instanceof Armor) {
int quantity = (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * 5);
int quantity = (int) Math.ceil(((Armor) part).getArmorPointsPerTon() * RESUPPLY_ARMOR_TONNAGE);
((Armor) part).setAmount(quantity);
campaign.getWarehouse().addPart(part, true);
} else {
Expand Down Expand Up @@ -255,7 +262,6 @@ public static void makeSmugglerDelivery(Resupply resupply) {
public static void loadPlayerConvoys(Resupply resupply) {
// 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 Campaign campaign = resupply.getCampaign();
final Map<Force, Double> playerConvoys = resupply.getPlayerConvoys();

Expand Down Expand Up @@ -285,8 +291,10 @@ public static void loadPlayerConvoys(Resupply resupply) {
double tonnage = part.getTonnage();
tonnage = tonnage == 0 ? RESUPPLY_MINIMUM_PART_WEIGHT : tonnage;

if (part instanceof AmmoBin || part instanceof Armor) {
tonnage *= WEIGHT_MULTIPLIER;
if (part instanceof AmmoBin) {
tonnage = RESUPPLY_AMMO_TONNAGE;
} else if (part instanceof Armor) {
tonnage = RESUPPLY_ARMOR_TONNAGE;
}

if (cargoCapacity - tonnage >= 0) {
Expand Down Expand Up @@ -326,18 +334,14 @@ public static void processConvoy(Resupply resupply, List<Part> convoyContents, @
// First, we need to identify whether the convoy has been intercepted.
AtBMoraleLevel morale = contract.getMoraleLevel();

// There isn't any chance of an interception if the enemy is Routed, so early-exit
if (morale.isRouted()) {
completeSuccessfulDelivery(resupply, convoyContents);
return;
}

int interceptionChance = morale.ordinal();

// There isn't any chance of an interception if the enemy is Routed, so early-exit
if (interceptionChance == ROUTED.ordinal()) {
completeSuccessfulDelivery(resupply, convoyContents);
return;
}

// This chance is modified by convoy weight, for player convoys this is easy - we just
// calculate the weight of all units in the convoy. For NPC convoys, we need to get a bit
// creative, as we have no way to determine their size prior to any interception scenario.
Expand Down
98 changes: 80 additions & 18 deletions MekHQ/src/mekhq/campaign/mission/resupplyAndCaches/Resupply.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.math.BigInteger;
import java.util.*;

import static java.lang.Math.floor;
import static java.lang.Math.min;
import static java.lang.Math.round;
import static megamek.common.MiscType.F_SPONSON_TURRET;
Expand Down Expand Up @@ -65,6 +66,8 @@ public class Resupply {
private Money convoyContentsValueCalculated;

public static final int CARGO_MULTIPLIER = 4;
public static final int RESUPPLY_AMMO_TONNAGE = 1;
public static final int RESUPPLY_ARMOR_TONNAGE = 5;

private static final MMLogger logger = MMLogger.create(Resupply.class);

Expand Down Expand Up @@ -545,9 +548,11 @@ private Map<String, PartDetails> collectParts() {
}

int dropWeight = part instanceof MissingPart ? 10 : 1;
dropWeight = (int) floor(dropWeight * getPartMultiplier(part));

PartDetails partDetails = new PartDetails(part, dropWeight);

processedParts.merge(part.toString(), partDetails, (oldValue, newValue) -> {
processedParts.merge(getPartKey(part), partDetails, (oldValue, newValue) -> {
oldValue.setWeight(oldValue.getWeight() + newValue.getWeight());
return oldValue;
});
Expand All @@ -563,6 +568,31 @@ private Map<String, PartDetails> collectParts() {
return processedParts;
}

/**
* Generates a key for the given part based on its name and tonnage.
*
* <p>The key is a combination of the part's name and its tonnage, separated by a colon.
* For specific part types such as {@link AmmoBin} and {@link Armor}, the tonnage is
* always set to a set value, regardless of the actual tonnage.</p>
*
* @param part The {@link Part} for which the key is generated. Must not be {@code null}.
* @return A unique key in the format {@code "partName:partTonnage"}, where
* {@code partName} is the name of the part and {@code partTonnage} is the
* tonnage of the part or a fixed value for {@link AmmoBin} and {@link Armor}.
*/
private static String getPartKey(Part part) {
String partName = part.getName();
double partTonnage = part.getTonnage();

if (part instanceof AmmoBin) {
partTonnage = RESUPPLY_AMMO_TONNAGE;
} else if (part instanceof Armor) {
partTonnage = RESUPPLY_ARMOR_TONNAGE;
}

return partName + ':' + partTonnage;
}

/**
* Checks if a part is ineligible for inclusion in the resupply process. Ineligibility is
* determined based on exclusion lists, unit structure compatibility, and transporter checks.
Expand Down Expand Up @@ -648,28 +678,60 @@ private boolean checkTransporter(Part part) {
}

/**
* Applies warehouse weight modifiers to the collected parts list by comparing the
* in-campaign warehouse spare parts with the current list. Removes parts from the pool
* if the warehouse already contains enough resources to offset demand.
* Adjusts the provided parts list by applying warehouse weight modifiers.
*
* <p>This method compares the in-campaign warehouse's spare parts inventory with the given
* parts list and reduces the weight (quantity) of parts in the list based on the warehouse
* stock. If the warehouse contains enough resources to fully satisfy the demand for a part,
* the part is removed from the parts list.</p>
*
* <p>The adjustments are performed as follows:
* <ul>
* <li>For each part in the warehouse:
* <ul>
* <li>The weight of the part in the part list is reduced by the quantity available
* in the warehouse.</li>
* <li>If the weight becomes zero or negative, the part is flagged for removal.</li>
* </ul>
* </li>
* <li>All flagged parts are then removed from the part list.</li>
* </ul>
* </p>
*
* @param partsList A map of part names and their respective part details to modify.
* @param partsList A map containing part identifiers (keys) and their corresponding {@link PartDetails}.
* The map will be modified to reflect the warehouse adjustments.
*/
private void applyWarehouseWeightModifiers(Map<String, PartDetails> partsList) {
// Because of how AmmoBins work, we're always considering the campaign to have 0 rounds
// of ammo in storage, we could avoid this, but I don't think it's necessary.
// Adjust based on the quantity in the warehouse
for (Part part : campaign.getWarehouse().getSpareParts()) {
PartDetails targetPart = partsList.get(part.toString());
if (targetPart != null) {
int spareCount = part.getQuantity();
double multiplier = getPartMultiplier(part);

double targetPartCount = targetPart.getWeight() * multiplier;
if ((targetPartCount - spareCount) < 1) {
partsList.remove(part.toString());
} else {
targetPart.setWeight(min(1, targetPartCount - spareCount));
}
int weight = part.getQuantity();

// We don't want empty AmmoStorage to reduce Resupply weighting
if (part instanceof AmmoStorage && (((AmmoStorage) part).getShots() == 0)) {
continue;
}

PartDetails partDetails = new PartDetails(part, weight);

partsList.merge(getPartKey(part), partDetails, (oldValue, newValue) -> {
oldValue.setWeight(oldValue.getWeight() - newValue.getWeight());
return oldValue;
});
}

// Remove any items that now have 0 (or negative) tickets left in the pool
List<String> removalList = new ArrayList<>();
for (PartDetails partDetails : partsList.values()) {
Part part = partDetails.getPart();
double weight = partDetails.getWeight();

if (weight <= 0) {
removalList.add(getPartKey(part));
}
}

for (String removalKey : removalList) {
partsList.remove(removalKey);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
import java.util.UUID;
import java.util.Vector;

import static java.lang.Math.ceil;
import static java.lang.Math.floor;
import static java.lang.Math.max;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.CARGO_MULTIPLIER;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_AMMO_TONNAGE;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.RESUPPLY_ARMOR_TONNAGE;
import static mekhq.campaign.mission.resupplyAndCaches.Resupply.calculateTargetCargoTonnage;
import static mekhq.campaign.personnel.enums.PersonnelStatus.KIA;
import static mekhq.utilities.EntityUtilities.getEntityFromUnitId;
Expand Down Expand Up @@ -143,7 +145,10 @@ private static void decideCrewMemberFate(Campaign campaign, Person person) {
*/
public static int estimateCargoRequirements(Campaign campaign, AtBContract contract) {
double targetTonnage = calculateTargetCargoTonnage(campaign, contract) * CARGO_MULTIPLIER;
return (int) Math.ceil(targetTonnage);

// Armor and ammo are always delivered in blocks, so cargo will never be less than the sum
// of those blocks
return max(RESUPPLY_AMMO_TONNAGE + RESUPPLY_ARMOR_TONNAGE, (int) Math.ceil(targetTonnage));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion MekHQ/src/mekhq/gui/view/MissionViewPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ public void mouseClicked(MouseEvent e) {
pnlStats.add(lblCargoRequirement, gridBagConstraints);

txtCargoRequirement.setName("txtCargoRequirement");
txtCargoRequirement.setText(estimateCargoRequirements(campaign, contract) + "t");
txtCargoRequirement.setText(estimateCargoRequirements(campaign, contract) + "t+");
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = y++;
Expand Down

0 comments on commit 5a38816

Please sign in to comment.