diff --git a/MekHQ/src/mekhq/campaign/unit/Unit.java b/MekHQ/src/mekhq/campaign/unit/Unit.java
index 180cde522f..fd75dd2381 100644
--- a/MekHQ/src/mekhq/campaign/unit/Unit.java
+++ b/MekHQ/src/mekhq/campaign/unit/Unit.java
@@ -1496,27 +1496,26 @@ public Money getSellValue() {
}
/**
- * Computes the total cargo capacity of the entity, accounting for transport bays
- * and mounted equipment designated for cargo, only if the entity is fully crewed.
+ * Calculates the total cargo capacity of the entity, considering the usable capacities of
+ * transport bays and mounted equipment designated for cargo. The calculation is performed only
+ * if the entity is fully crewed.
*
- *
The total cargo capacity is the sum of the following:
+ * The total cargo capacity is derived from the following:
*
- * - The usable capacities of transport bays that are instances of {@link CargoBay},
- * {@link RefrigeratedCargoBay}, or {@link InsulatedCargoBay}, adjusted for damage.
- * - The tonnage of mounted equipment marked as cargo (via the {@code F_CARGO} flag),
- * provided the equipment is operable and located in valid entity sections.
+ * - The usable capacities of transport bays ({@link CargoBay}, {@link RefrigeratedCargoBay},
+ * or {@link InsulatedCargoBay}), adjusted for existing damage.
+ * - The tonnage of mounted equipment tagged with the {@code F_CARGO} flag, provided
+ * the equipment is operable and located in non-destroyed sections of the entity.
*
*
- * Important Considerations:
+ * Special Conditions:
*
- * - The method returns a cargo capacity of zero if the entity is not fully crewed.
- * - Capabilities of transport bays or mounted equipment that are damaged beyond operability
- * or located in destroyed sections are not included in the calculation.
- * - The computation assumes no external conditions affect the equipment or bays beyond the
- * immediate considerations of damage and operability.
+ * - The method returns {@code 0.0} if the entity is not fully crewed.
+ * - Bays or mounted equipment damaged beyond usability are excluded from the total.
+ * - Only equipment in valid (non-destroyed) sections of the entity are considered.
*
*
- * @return The total cargo capacity of the entity if it is fully crewed; otherwise, {@code 0.0}.
+ * @return The total cargo capacity of the entity if fully crewed; otherwise, {@code 0.0}.
*/
public double getCargoCapacity() {
if (!isFullyCrewed()) {
@@ -1552,7 +1551,8 @@ public double getCargoCapacity() {
if (mounted.getType().hasFlag(F_CARGO)) {
// isOperable doesn't check if the mounted location still exists, so we check for
// that first.
- if ((entity.getInternal(mounted.getLocation()) != 0) && (mounted.isOperable())) {
+ if (!mounted.getEntity().isLocationBad(mounted.getLocation())
+ && (mounted.isOperable())) {
capacity += mounted.getTonnage();
}
}
diff --git a/MekHQ/unittests/mekhq/campaign/unit/GetCargoCapacityTest.java b/MekHQ/unittests/mekhq/campaign/unit/GetCargoCapacityTest.java
new file mode 100644
index 0000000000..9d2c19d60a
--- /dev/null
+++ b/MekHQ/unittests/mekhq/campaign/unit/GetCargoCapacityTest.java
@@ -0,0 +1,309 @@
+package mekhq.campaign.unit;
+
+import megamek.common.*;
+import megamek.common.icons.Portrait;
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.campaign.CampaignOptions;
+import mekhq.campaign.personnel.Person;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import static megamek.common.MiscType.F_CARGO;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * CargoCapacityTest is a test suite for verifying the cargo capacity calculations of different
+ * entity types. It ensures that factoring accurately computes the cargo capacity in the state of
+ * transport bays, mounted equipment, and damage to specific locations.
+ *
+ * The test primarily validates conditions where:
+ *
+ * - All systems are operational.
+ * - Damage is applied to specific locations or bays.
+ * - All systems are destroyed.
+ *
+ */
+
+class CargoCapacityTest {
+ MMLogger logger = MMLogger.create(CargoCapacityTest.class);
+
+ private Campaign mockCampaign;
+ private CampaignOptions mockCampaignOptions;
+
+ private final String CARGO_MEK = "Buster BC XV-M-B HaulerMech MOD";
+ private final CargoUnit cargoMek = new CargoUnit(CARGO_MEK, 0, 2);
+
+ private final String CARGO_DROP_SHIP = "Hoshiryokou (Tug Boat)";
+ private final CargoUnit cargoDropShip = new CargoUnit(CARGO_DROP_SHIP, 7, 100);
+
+ private final String CARGO_FIGHTER = "Caravan Heavy Transport";
+ private final CargoUnit cargoFighter = new CargoUnit(CARGO_FIGHTER, 60, 0);
+
+ private final String CARGO_TANK = "Prime Mover (LRM)";
+ private final CargoUnit cargoTank = new CargoUnit(CARGO_TANK, 0, 20);
+
+ @BeforeEach
+ public void setup() {
+ mockCampaign = mock(Campaign.class);
+ mockCampaignOptions = mock(CampaignOptions.class);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoMek() {
+ Entity entity = createEntity(cargoMek.name);
+ testCargoTotal(entity, cargoMek.getTotalCargoCapacity());
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoMekKilledLocations() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ testCargoTotal(entity, cargoMek.bayCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoMekKilledBays() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killBays(entity);
+ testCargoTotal(entity, cargoMek.otherCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoMekKillEverything() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ killBays(entity);
+ testCargoTotal(entity, 0);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoDropShip() {
+ Entity entity = createEntity(cargoDropShip.name);
+ testCargoTotal(entity, cargoDropShip.getTotalCargoCapacity());
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoDropShipKilledLocations() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ testCargoTotal(entity, cargoMek.bayCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoDropShipKilledBays() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killBays(entity);
+ testCargoTotal(entity, cargoMek.otherCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoDropShipKillEverything() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ killBays(entity);
+ testCargoTotal(entity, 0);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoFighter() {
+ Entity entity = createEntity(cargoFighter.name);
+ testCargoTotal(entity, cargoFighter.getTotalCargoCapacity());
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoFighterKilledLocations() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ testCargoTotal(entity, cargoMek.bayCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoFighterKilledBays() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killBays(entity);
+ testCargoTotal(entity, cargoMek.otherCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoFighterKillEverything() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ killBays(entity);
+ testCargoTotal(entity, 0);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoTank() {
+ Entity entity = createEntity(cargoTank.name);
+ testCargoTotal(entity, cargoTank.getTotalCargoCapacity());
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoTankKilledLocations() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ testCargoTotal(entity, cargoMek.bayCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoTankKilledBays() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killBays(entity);
+ testCargoTotal(entity, cargoMek.otherCargoCapacity);
+ }
+
+ @Test
+ public void testCargoCapacityOfCargoTankKillEverything() {
+ Entity entity = createEntity(cargoMek.name);
+ assertNotNull(entity);
+ killCargoLocations(entity);
+ killBays(entity);
+ testCargoTotal(entity, 0);
+ }
+
+ /**
+ * Creates an {@link Entity} from the given unit name by retrieving its information from the
+ * cache.
+ *
+ * If the unit cannot be found or loaded, appropriate error logging occurs, and {@code null}
+ * is returned.
+ *
+ *
+ * @param unitName The name of the unit to retrieve and parse.
+ * @return The {@link Entity} representing the unit, or {@code null} if the unit cannot be loaded.
+ */
+ private Entity createEntity(String unitName) {
+ MekSummary mekSummary = MekSummaryCache.getInstance().getMek(unitName);
+ if (mekSummary == null) {
+ logger.error("Cannot find entry for {}", unitName);
+ return null;
+ }
+
+ MekFileParser mekFileParser;
+
+ try {
+ mekFileParser = new MekFileParser(mekSummary.getSourceFile(), mekSummary.getEntryName());
+ } catch (Exception ex) {
+ logger.error("Unable to load unit: {}", mekSummary.getEntryName(), ex);
+ return null;
+ }
+
+ return mekFileParser.getEntity();
+ }
+
+ /**
+ * Verifies the calculated cargo capacity of a given entity against an expected value.
+ *
+ * To ensure accurate calculations, this method creates and fully crews a {@link Unit} entity,
+ * then compares the reported cargo capacity to the expected value.
+ *
+ * Mock {@link Person} crew members are added to satisfy crewing requirements.
+ * Drivers, gunners, and vessel crew are set up as needed by the entity being tested.
+ *
+ * @param entity The {@link Entity} whose cargo capacity is to be tested.
+ * @param expectedCargoTotal The expected total cargo capacity for the provided entity.
+ */
+ private void testCargoTotal(Entity entity, double expectedCargoTotal) {
+ Unit unit = new Unit(entity, mockCampaign);
+
+ while (!unit.isFullyCrewed()) {
+ Person crewMember = mock(Person.class);
+ when(mockCampaign.getCampaignOptions()).thenReturn(mockCampaignOptions);
+ when(crewMember.getPortrait()).thenReturn(mock(Portrait.class));
+ when(crewMember.getId()).thenReturn(mock(UUID.class));
+
+ if (unit.getTotalDriverNeeds() > 0) {
+ unit.addDriver(crewMember);
+ continue;
+ }
+
+ if (unit.getTotalGunnerNeeds() > 0) {
+ unit.addGunner(crewMember);
+ continue;
+ }
+
+ if (unit.getTotalCrewNeeds() > 0) {
+ unit.addVesselCrew(crewMember);
+ }
+ }
+
+ double cargoCapacity = unit.getCargoCapacity();
+ assertEquals(expectedCargoTotal, cargoCapacity);
+ }
+
+ /**
+ * Simulates the destruction of all transport location-based systems for the provided entity.
+ *
+ * This method iterates through all mounted equipment on the entity, identifies those with
+ * the {@code F_CARGO} flag, and marks their locations as destroyed.
+ *
+ * @param entity The {@link Entity} whose transport systems are to be simulated as destroyed.
+ */
+ private void killCargoLocations(Entity entity) {
+ for (Mounted> mounted : entity.getMisc()) {
+ if (mounted.getType().hasFlag(F_CARGO)) {
+ if (entity.getInternal(mounted.getLocation()) != IArmorState.ARMOR_NA) {
+ entity.setInternal(IArmorState.ARMOR_DESTROYED, mounted.getLocation());
+ }
+ }
+ }
+ }
+
+ /**
+ * Simulates the destruction of all transport bays for the provided entity.
+ *
+ * This method identifies and marks all bay-related systems with the {@code F_CARGO} flag as
+ * destroyed.
+ *
+ * @param entity The {@link Entity} whose bays are to be simulated as destroyed.
+ */
+ private void killBays(Entity entity) {
+ for (Mounted> mounted : entity.getMisc()) {
+ if (mounted.getType().hasFlag(F_CARGO)) {
+ if (entity.getInternal(mounted.getLocation()) == IArmorState.ARMOR_NA) {
+ mounted.setDestroyed(true);
+ }
+ }
+ }
+ }
+
+ /**
+ * CargoRecord is an immutable data class representing information about the cargo capacity of
+ * an entity. It contains the name of the entity, the cargo capacity from transport bays, the
+ * cargo capacity from other mounted equipment, and a utility method to calculate the total
+ * cargo capacity.
+ *
+ * @param name The name of the entity associated with the cargo.
+ * @param bayCargoCapacity The cargo capacity contributed by transport bays.
+ * @param otherCargoCapacity The cargo capacity contributed by other mounted equipment.
+ */
+ public record CargoUnit(String name, double bayCargoCapacity, double otherCargoCapacity) {
+
+ /**
+ * Calculates the total cargo capacity as the sum of {@code bayCargoCapacity} and
+ * {@code otherCargoCapacity}.
+ *
+ * @return The total cargo capacity of the entity.
+ */
+ public double getTotalCargoCapacity() {
+ return bayCargoCapacity + otherCargoCapacity;
+ }
+ }
+}