Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further Improvements to Cargo Container Checks; added Unit Tests #5954

Merged
merged 2 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions MekHQ/src/mekhq/campaign/unit/Unit.java
Original file line number Diff line number Diff line change
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;
}
}
}