Skip to content

Commit

Permalink
Merge branch 'master' into rjh-additional-files-to-ignore
Browse files Browse the repository at this point in the history
  • Loading branch information
rjhancock authored Feb 2, 2025
2 parents b13d25b + cf2ebad commit b42ca37
Show file tree
Hide file tree
Showing 3 changed files with 327 additions and 19 deletions.
3 changes: 1 addition & 2 deletions MekHQ/docs/history.txt
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,7 @@ MEKHQ VERSION HISTORY:
+ PR #5931: Refactored Part Handling in Resupply Module
+ Fix #5932: Added check for injuries that are not permanent before going on leave
+ PR #5946: Implemented Linked Scenarios for Interceptions


+ Fix #5951: Fixed Cargo Capacity Calculation in For Cargo Items in Body

0.50.02 (2024-12-30 2130 UTC)
+ Fix #846, #5185: Fix BattleArmor customization/refit overweight check.
Expand Down
34 changes: 17 additions & 17 deletions MekHQ/src/mekhq/campaign/unit/Unit.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;
import static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;
import static java.lang.Math.max;
import static megamek.common.MiscType.F_CARGO;
import static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;
import static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;
import static mekhq.campaign.parts.enums.PartQuality.*;
import static mekhq.campaign.unit.enums.TransporterType.*;

Expand Down Expand Up @@ -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.
*
* <p>The total cargo capacity is the sum of the following:</p>
* <p>The total cargo capacity is derived from the following:</p>
* <ul>
* <li>The usable capacities of transport bays that are instances of {@link CargoBay},
* {@link RefrigeratedCargoBay}, or {@link InsulatedCargoBay}, adjusted for damage.</li>
* <li>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.</li>
* <li>The usable capacities of transport bays ({@link CargoBay}, {@link RefrigeratedCargoBay},
* or {@link InsulatedCargoBay}), adjusted for existing damage.</li>
* <li>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.</li>
* </ul>
*
* <p><strong>Important Considerations:</strong></p>
* <p><strong>Special Conditions:</strong></p>
* <ul>
* <li>The method returns a cargo capacity of zero if the entity is not fully crewed.</li>
* <li>Capabilities of transport bays or mounted equipment that are damaged beyond operability
* or located in destroyed sections are not included in the calculation.</li>
* <li>The computation assumes no external conditions affect the equipment or bays beyond the
* immediate considerations of damage and operability.</li>
* <li>The method returns {@code 0.0} if the entity is not fully crewed.</li>
* <li>Bays or mounted equipment damaged beyond usability are excluded from the total.</li>
* <li>Only equipment in valid (non-destroyed) sections of the entity are considered.</li>
* </ul>
*
* @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()) {
Expand Down Expand Up @@ -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();
}
}
Expand Down
309 changes: 309 additions & 0 deletions MekHQ/unittests/mekhq/campaign/unit/GetCargoCapacityTest.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The test primarily validates conditions where:</p>
* <ul>
* <li>All systems are operational.</li>
* <li>Damage is applied to specific locations or bays.</li>
* <li>All systems are destroyed.</li>
* </ul>
*/

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.
*
* <p>If the unit cannot be found or loaded, appropriate error logging occurs, and {@code null}
* is returned.
* </p>
*
* @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.
*
* <p>To ensure accurate calculations, this method creates and fully crews a {@link Unit} entity,
* then compares the reported cargo capacity to the expected value.</p>
*
* <p>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.</p>
*
* @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.
*
* <p>This method iterates through all mounted equipment on the entity, identifies those with
* the {@code F_CARGO} flag, and marks their locations as destroyed.</p>
*
* @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.
*
* <p>This method identifies and marks all bay-related systems with the {@code F_CARGO} flag as
* destroyed.</p>
*
* @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;
}
}
}

0 comments on commit b42ca37

Please sign in to comment.