diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 2dde4aae4da..a0600fea0e5 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -14,16 +14,7 @@ */ package megamek.common; -import java.awt.Image; -import java.math.BigInteger; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - import megamek.MMConstants; import megamek.client.bot.princess.FireControl; import megamek.client.ui.Base64Image; @@ -32,27 +23,11 @@ import megamek.client.ui.swing.calculationReport.DummyCalculationReport; import megamek.codeUtilities.StringUtility; import megamek.common.MovePath.MoveStepType; -import megamek.common.actions.AbstractAttackAction; -import megamek.common.actions.ChargeAttackAction; -import megamek.common.actions.DfaAttackAction; -import megamek.common.actions.DisplacementAttackAction; -import megamek.common.actions.EntityAction; -import megamek.common.actions.PushAttackAction; -import megamek.common.actions.TeleMissileAttackAction; -import megamek.common.actions.WeaponAttackAction; +import megamek.common.actions.*; import megamek.common.annotations.Nullable; import megamek.common.battlevalue.BVCalculator; -import megamek.common.enums.AimingMode; -import megamek.common.enums.BasementType; -import megamek.common.enums.BuildingType; -import megamek.common.enums.GamePhase; -import megamek.common.enums.MPBoosters; -import megamek.common.enums.WeaponSortOrder; -import megamek.common.equipment.AmmoMounted; -import megamek.common.equipment.ArmorType; -import megamek.common.equipment.BombMounted; -import megamek.common.equipment.MiscMounted; -import megamek.common.equipment.WeaponMounted; +import megamek.common.enums.*; +import megamek.common.equipment.*; import megamek.common.event.GameEntityChangeEvent; import megamek.common.force.Force; import megamek.common.hexarea.HexArea; @@ -64,13 +39,7 @@ import megamek.common.planetaryconditions.Wind; import megamek.common.preference.PreferenceManager; import megamek.common.util.DiscordFormat; -import megamek.common.weapons.AlamoMissileWeapon; -import megamek.common.weapons.AltitudeBombAttack; -import megamek.common.weapons.CapitalMissileBearingsOnlyHandler; -import megamek.common.weapons.DiveBombAttack; -import megamek.common.weapons.SpaceBombAttack; -import megamek.common.weapons.Weapon; -import megamek.common.weapons.WeaponHandler; +import megamek.common.weapons.*; import megamek.common.weapons.bayweapons.AR10BayWeapon; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.common.weapons.bayweapons.CapitalMissileBayWeapon; @@ -80,6 +49,15 @@ import megamek.logging.MMLogger; import megamek.utilities.xml.MMXMLUtility; +import java.awt.*; +import java.math.BigInteger; +import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + /** * Entity is a master class for basically anything on the board except terrain. */ @@ -91,6 +69,16 @@ public abstract class Entity extends TurnOrdered implements Transporter, Targeta private static final long serialVersionUID = 1430806396279853295L; + public enum InvalidSourceBuildReason { + UNIT_OLDER_THAN_EQUIPMENT_INTRO_YEAR, + NOT_ENOUGH_SLOT_COUNT, + UNIT_OVERWEIGHT, + INVALID_OR_OUTDATED_BUILD, + INCOMPLETE_BUILD, + INVALID_ENGINE, + INVALID_CREW, + } + public static final int DOES_NOT_TRACK_HEAT = 999; public static final int UNLIMITED_JUMP_DOWN = 999; @@ -397,6 +385,7 @@ public abstract class Entity extends TurnOrdered implements Transporter, Targeta private List passedThroughFacing = new ArrayList<>(); + private List invalidSourceBuildReasons = new ArrayList<>(); /** * Stores the player selected hex ground to air targeting. * For ground to air, distance to target for the ground unit is determined @@ -15840,4 +15829,21 @@ public void removeFleeZone() { fleeZone = HexArea.EMPTY_AREA; hasFleeZone = false; } + + public void setInvalidSourceBuildReasons(List invalidSourceBuildReasons) { + this.invalidSourceBuildReasons = invalidSourceBuildReasons; + } + + public List getInvalidSourceBuildReasons() { + return invalidSourceBuildReasons; + } + + public boolean canonUnitWithInvalidBuild() { + if (this.isCanon() && mulId > -1) + { + return !this.getInvalidSourceBuildReasons().isEmpty(); + } + return false; + } + } diff --git a/megamek/src/megamek/common/loaders/BLKFile.java b/megamek/src/megamek/common/loaders/BLKFile.java index bb3943dff05..42a61e59cff 100644 --- a/megamek/src/megamek/common/loaders/BLKFile.java +++ b/megamek/src/megamek/common/loaders/BLKFile.java @@ -14,16 +14,6 @@ */ package megamek.common.loaders; -import java.io.File; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.Vector; -import java.util.stream.Collectors; - import megamek.common.*; import megamek.common.InfantryBay.PlatoonType; import megamek.common.equipment.AmmoMounted; @@ -36,8 +26,14 @@ import megamek.common.weapons.InfantryAttack; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.common.weapons.infantry.InfantryWeapon; +import megamek.logging.MMLogger; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; public class BLKFile { + private static final MMLogger logger = MMLogger.create(BLKFile.class); BuildingBlock dataFile; @@ -122,11 +118,30 @@ protected void setBasicEntityData(Entity entity) throws EntityLoadingException { entity.setIcon(dataFile.getDataAsString("icon")[0]); } + if (dataFile.exists("invalidSourceBuildReasons")) { + loadInvalidSourceBuildReasons(entity); + } + setTechLevel(entity); setFluff(entity); checkManualBV(entity); } + protected void loadInvalidSourceBuildReasons(Entity entity) { + String[] reasons = dataFile.getDataAsString("invalidSourceBuildReasons"); + var invalidSourceBuildReasons = new ArrayList(); + if (reasons != null) { + for (var reason : reasons) { + try { + invalidSourceBuildReasons.add(Entity.InvalidSourceBuildReason.valueOf(reason)); + } catch (Exception e) { + logger.warn("Unable to load reason {}", reason); + } + } + } + entity.setInvalidSourceBuildReasons(invalidSourceBuildReasons); + } + protected void loadQuirks(Entity entity) throws EntityLoadingException { try { List quirks = new ArrayList<>(); @@ -291,21 +306,35 @@ protected void loadSVArmor(Entity sv) throws EntityLoadingException { TechConstants.isClan(dataFile.getDataAsInt(sv.getLocationName(i) + "_armor_tech")[0])); sv.setArmorType(armor.getArmorType(), i); sv.setBARRating(armor.getBAR(), i); - sv.setArmorTechLevel(armor.getStaticTechLevel().getCompoundTechLevel(false), i); + sv.setArmorTechLevel(armor.getStaticTechLevel().getCompoundTechLevel(sv.isClan()), i); } } else { if (dataFile.exists("barrating")) { megamek.common.equipment.ArmorType armor = ArmorType.svArmor(dataFile.getDataAsInt("barrating")[0]); - sv.setArmorType(armor.getArmorType()); + if (dataFile.exists("armor_type")) { + sv.setArmorType(dataFile.getDataAsInt("armor_type")[0]); + } else { + sv.setArmorType(armor.getArmorType()); + } + if (dataFile.exists("armor_tech_level")) { + sv.setArmorTechLevel(dataFile.getDataAsInt("armor_tech_level")[0]); + } else { + sv.setArmorTechLevel(armor.getStaticTechLevel().getCompoundTechLevel(sv.isClan())); + } sv.setBARRating(armor.getBAR()); - sv.setArmorTechLevel(armor.getStaticTechLevel().getCompoundTechLevel(false)); } else { if (dataFile.exists("armor_type")) { sv.setArmorType(dataFile.getDataAsInt("armor_type")[0]); } else { throw new EntityLoadingException("could not find armor_type block."); } + if (dataFile.exists("armor_tech_level")) { + sv.setArmorTechLevel(dataFile.getDataAsInt("armor_tech_level")[0]); + } else { + sv.setArmorTechLevel(sv.getStaticTechLevel().getCompoundTechLevel(sv.isClan())); + } } + if (dataFile.exists("armor_tech")) { sv.setArmorTechRating(dataFile.getDataAsInt("armor_tech")[0]); } else if (dataFile.exists("armor_tech_rating")) { @@ -754,14 +783,15 @@ public static BuildingBlock getBlock(Entity t) throws EntitySavingException { if (!t.hasPatchworkArmor() && (t.getArmorType(1) != ArmorType.T_ARMOR_UNKNOWN)) { blk.writeBlockData("armor_type", t.getArmorType(1)); blk.writeBlockData("armor_tech_rating", t.getArmorTechRating()); + blk.writeBlockData("armor_tech_level", t.getArmorTechLevel(1)); } else if (t.hasPatchworkArmor()) { blk.writeBlockData("armor_type", EquipmentType.T_ARMOR_PATCHWORK); for (int i = 1; i < t.locations(); i++) { ArmorType armor = ArmorType.forEntity(t, i); - blk.writeBlockData(t.getLocationName(i) + "_armor_type", t.getArmorType(i)); - blk.writeBlockData(t.getLocationName(i) + "_armor_tech", - TechConstants.getTechName(t.getArmorTechLevel(i))); + blk.writeBlockData(t.getLocationName(i) + "_armor_type", armor.getArmorType()); + blk.writeBlockData(t.getLocationName(i) + "_armor_tech", TechConstants.getTechName(t.getArmorTechLevel(i))); + blk.writeBlockData(t.getLocationName(i) + "_armor_tech_rating", armor.getTechRating()); if (armor.hasFlag(MiscType.F_SUPPORT_VEE_BAR_ARMOR)) { blk.writeBlockData(t.getLocationName(i) + "_barrating", armor.getBAR()); } @@ -1116,6 +1146,9 @@ public static BuildingBlock getBlock(Entity t) throws EntitySavingException { if (t.getFluff().hasEmbeddedFluffImage()) { blk.writeBlockData("fluffimage", t.getFluff().getBase64FluffImage().getBase64String()); } + if (t.canonUnitWithInvalidBuild()) { + blk.writeBlockData("invalidSourceBuildReasons", t.getInvalidSourceBuildReasons().stream().map(Enum::name).toList()); + } return blk; } diff --git a/megamek/src/megamek/common/loaders/BLKTankFile.java b/megamek/src/megamek/common/loaders/BLKTankFile.java index ce9cf5803c7..32fca1da055 100644 --- a/megamek/src/megamek/common/loaders/BLKTankFile.java +++ b/megamek/src/megamek/common/loaders/BLKTankFile.java @@ -21,6 +21,7 @@ import megamek.common.FuelType; import megamek.common.SuperHeavyTank; import megamek.common.Tank; +import megamek.common.equipment.ArmorType; import megamek.common.util.BuildingBlock; import megamek.logging.MMLogger; @@ -192,25 +193,7 @@ public Entity getEntity() throws EntityLoadingException { t.initializeArmor(fullArmor[x], x); } - boolean patchworkArmor = false; - if (dataFile.exists("armor_type")) { - if (dataFile.getDataAsInt("armor_type")[0] == EquipmentType.T_ARMOR_PATCHWORK) { - patchworkArmor = true; - } else { - t.setArmorType(dataFile.getDataAsInt("armor_type")[0]); - } - } else { - t.setArmorType(EquipmentType.T_ARMOR_STANDARD); - } - if (!patchworkArmor && dataFile.exists("armor_tech")) { - t.setArmorTechLevel(dataFile.getDataAsInt("armor_tech")[0]); - } - if (patchworkArmor) { - for (int i = 1; i < t.locations(); i++) { - t.setArmorType(dataFile.getDataAsInt(t.getLocationName(i) + "_armor_type")[0], i); - t.setArmorTechLevel(dataFile.getDataAsInt(t.getLocationName(i) + "_armor_type")[0], i); - } - } + setupArmorTypeAndTechLevel(t); t.autoSetInternal(); @@ -286,4 +269,38 @@ public Entity getEntity() throws EntityLoadingException { loadQuirks(t); return t; } + + private void setupArmorTypeAndTechLevel(Tank t) { + boolean patchworkArmor = false; + if (dataFile.exists("armor_type")) { + if (dataFile.getDataAsInt("armor_type")[0] == EquipmentType.T_ARMOR_PATCHWORK) { + patchworkArmor = true; + } else { + var armorTypeId = dataFile.getDataAsInt("armor_type")[0]; + t.setArmorType(armorTypeId); + var armorType = ArmorType.of(armorTypeId, t.isClan()); + var techLevel = -1; + if (dataFile.exists("armor_tech_level")) { + techLevel = dataFile.getDataAsInt("armor_tech_level")[0]; + } else { + techLevel = armorType.getStaticTechLevel().getCompoundTechLevel(t.isClan()); + } + t.setArmorTechLevel(techLevel); + } + } else { + t.setArmorType(EquipmentType.T_ARMOR_STANDARD); + } + if (!patchworkArmor && dataFile.exists("armor_tech")) { + t.setArmorTechLevel(dataFile.getDataAsInt("armor_tech")[0]); + } + if (patchworkArmor) { + for (int i = 1; i < t.locations(); i++) { + var armorTypeId = dataFile.getDataAsInt(t.getLocationName(i) + "_armor_type")[0]; + var armorType = ArmorType.of(armorTypeId, t.isClan()); + t.setArmorType(armorTypeId, i); + var techLevel = armorType.getStaticTechLevel().getCompoundTechLevel(t.isClan()); + t.setArmorTechLevel(techLevel, i); + } + } + } } diff --git a/megamek/src/megamek/common/verifier/TestAdvancedAerospace.java b/megamek/src/megamek/common/verifier/TestAdvancedAerospace.java index 030084529aa..ebfb0b54f4a 100644 --- a/megamek/src/megamek/common/verifier/TestAdvancedAerospace.java +++ b/megamek/src/megamek/common/verifier/TestAdvancedAerospace.java @@ -291,7 +291,7 @@ public static int minimumBaseCrew(Jumpship vessel) { /** * One gunner is required for each capital weapon and each six standard scale * weapons, rounding up - * + * * @return The vessel's minimum gunner requirements. */ public static int requiredGunners(Jumpship vessel) { @@ -622,7 +622,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; @@ -648,7 +648,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct &= correctGravDecks(buff); correct &= correctBays(buff); correct &= correctCriticals(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; @@ -865,7 +865,7 @@ public boolean hasIllegalEquipmentCombinations(StringBuffer buff) { /** * Checks that the unit meets minimum crew and quarters requirements. - * + * * @param buffer Where to write messages explaining failures. * @return true if the crew data is valid. */ @@ -901,7 +901,7 @@ public boolean correctCrew(StringBuffer buffer) { /** * Checks that the unit does not exceed the maximum number or size of gravity * decks. - * + * * @param buffer Where to write messages explaining failures. * @return true if the crew data is valid. */ diff --git a/megamek/src/megamek/common/verifier/TestAero.java b/megamek/src/megamek/common/verifier/TestAero.java index c9c6de8c3b5..0cf2b9f33dc 100644 --- a/megamek/src/megamek/common/verifier/TestAero.java +++ b/megamek/src/megamek/common/verifier/TestAero.java @@ -711,11 +711,12 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; } + if (!engine.engineValid) { buff.append(engine.problem.toString()).append("\n\n"); correct = false; @@ -749,7 +750,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct &= !hasIllegalEquipmentCombinations(buff); correct &= !hasMismatchedLateralWeapons(buff); correct &= correctHeatSinks(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; @@ -757,7 +758,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { /** * Checks that the weapon loads in the wings match each other. - * + * * @param buff The buffer that contains the collected error messages. * @return Whether the lateral weapons are mismatched. */ @@ -1236,7 +1237,7 @@ public static int minimumBaseCrew(Aero aero) { /** * One gunner is required for each capital weapon and each six standard scale * weapons, rounding up - * + * * @return The vessel's minimum gunner requirements. */ public static int requiredGunners(Aero aero) { diff --git a/megamek/src/megamek/common/verifier/TestBattleArmor.java b/megamek/src/megamek/common/verifier/TestBattleArmor.java index 0b5b3e8e386..cc76d6d5c15 100644 --- a/megamek/src/megamek/common/verifier/TestBattleArmor.java +++ b/megamek/src/megamek/common/verifier/TestBattleArmor.java @@ -1034,7 +1034,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; @@ -1062,7 +1062,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct &= correctManipulators(buff); correct &= correctMovement(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; diff --git a/megamek/src/megamek/common/verifier/TestEntity.java b/megamek/src/megamek/common/verifier/TestEntity.java index 8de948b2da0..3c9a24e27fb 100755 --- a/megamek/src/megamek/common/verifier/TestEntity.java +++ b/megamek/src/megamek/common/verifier/TestEntity.java @@ -247,7 +247,7 @@ public boolean ignoreFailedEquip(String name) { @Override public boolean showIncorrectIntroYear() { - return options.showIncorrectIntroYear(); + return !ignoreEquipmentIntroYear() && options.showIncorrectIntroYear(); } @Override @@ -257,7 +257,7 @@ public int getIntroYearMargin() { @Override public boolean skip() { - return options.skip(); + return !skipBuildValidation() && options.skip(); } @Override @@ -1808,4 +1808,25 @@ public int totalCritSlotCount() { } return slotCount; } + + boolean ignoreSlotCount() { + var entity = getEntity(); + return entity.getInvalidSourceBuildReasons().contains(Entity.InvalidSourceBuildReason.NOT_ENOUGH_SLOT_COUNT); + } + + boolean ignoreEquipmentIntroYear() { + var entity = getEntity(); + return entity.getInvalidSourceBuildReasons().contains(Entity.InvalidSourceBuildReason.UNIT_OLDER_THAN_EQUIPMENT_INTRO_YEAR); + } + + boolean allowOverweightConstruction() { + var entity = getEntity(); + return entity.getInvalidSourceBuildReasons().contains(Entity.InvalidSourceBuildReason.UNIT_OVERWEIGHT); + } + + boolean skipBuildValidation() { + var entity = getEntity(); + return entity.getInvalidSourceBuildReasons().contains(Entity.InvalidSourceBuildReason.INVALID_OR_OUTDATED_BUILD); + } + } // End class TestEntity diff --git a/megamek/src/megamek/common/verifier/TestInfantry.java b/megamek/src/megamek/common/verifier/TestInfantry.java index 77a4ba1b8c7..80c583eea26 100644 --- a/megamek/src/megamek/common/verifier/TestInfantry.java +++ b/megamek/src/megamek/common/verifier/TestInfantry.java @@ -145,12 +145,8 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - - // We currently have many unit introduction dates that are too early for their gear or anti-mek attacks - // enable this when dates have been straightened -// if (showIncorrectIntroYear() && hasIncorrectIntroYear(buff)) { -// correct = false; -// } + // Infantry has too many problems with intro date for its equipments therefore we are not testing the + // year of introduction of the equipments. int max = maxSecondaryWeapons(inf); if (inf.getSecondaryWeaponsPerSquad() > max) { @@ -201,7 +197,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { buff.append("Infantry may not have more than one armor kit!\n"); correct = false; } - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; diff --git a/megamek/src/megamek/common/verifier/TestMek.java b/megamek/src/megamek/common/verifier/TestMek.java index a360c788c16..6b51996de95 100755 --- a/megamek/src/megamek/common/verifier/TestMek.java +++ b/megamek/src/megamek/common/verifier/TestMek.java @@ -698,7 +698,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; @@ -740,7 +740,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct = correct && checkMiscSpreadAllocation(mek, misc, buff); } correct = correct && correctMovement(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; diff --git a/megamek/src/megamek/common/verifier/TestProtoMek.java b/megamek/src/megamek/common/verifier/TestProtoMek.java index 0385fe36ff2..91e36d5e7f8 100644 --- a/megamek/src/megamek/common/verifier/TestProtoMek.java +++ b/megamek/src/megamek/common/verifier/TestProtoMek.java @@ -228,7 +228,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return correct; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; @@ -253,7 +253,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct = false; } correct = correct && correctMovement(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; diff --git a/megamek/src/megamek/common/verifier/TestSmallCraft.java b/megamek/src/megamek/common/verifier/TestSmallCraft.java index 71d60775f8b..084ff1f2d0c 100644 --- a/megamek/src/megamek/common/verifier/TestSmallCraft.java +++ b/megamek/src/megamek/common/verifier/TestSmallCraft.java @@ -498,7 +498,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (skip()) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()); correct = false; @@ -522,7 +522,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct &= correctHeatSinks(buff); correct &= correctCrew(buff); correct &= correctCriticals(buff); - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; @@ -684,7 +684,7 @@ public boolean hasIllegalEquipmentCombinations(StringBuffer buff) { /** * Checks that the unit meets minimum crew and quarters requirements. - * + * * @param buffer Where to write messages explaining failures. * @return true if the crew data is valid. */ diff --git a/megamek/src/megamek/common/verifier/TestSupportVehicle.java b/megamek/src/megamek/common/verifier/TestSupportVehicle.java index 4e0f8775af3..d4d81b3f0fb 100644 --- a/megamek/src/megamek/common/verifier/TestSupportVehicle.java +++ b/megamek/src/megamek/common/verifier/TestSupportVehicle.java @@ -992,7 +992,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { return true; } - if (!correctWeight(buff)) { + if (!allowOverweightConstruction() && !correctWeight(buff)) { buff.insert(0, printTechLevel() + printShortMovement()); buff.append(printWeightCalculation()).append("\n"); correct = false; @@ -1015,7 +1015,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct = false; } - if (occupiedSlotCount() > totalSlotCount()) { + if (!ignoreSlotCount() && (occupiedSlotCount() > totalSlotCount())) { buff.append("Not enough item slots available! Using "); buff.append(Math.abs(occupiedSlotCount() - totalSlotCount())); buff.append(" slot(s) too many.\n"); @@ -1210,7 +1210,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { correct = false; } - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } diff --git a/megamek/src/megamek/common/verifier/TestTank.java b/megamek/src/megamek/common/verifier/TestTank.java index 9a569837d4f..4777272a084 100755 --- a/megamek/src/megamek/common/verifier/TestTank.java +++ b/megamek/src/megamek/common/verifier/TestTank.java @@ -391,7 +391,7 @@ public boolean correctEntity(StringBuffer buff, int ammoTechLvl) { if (!correctCriticals(buff)) { correct = false; } - if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN)) { + if (getEntity().hasQuirk(OptionsConstants.QUIRK_NEG_ILLEGAL_DESIGN) || getEntity().canonUnitWithInvalidBuild()) { correct = true; } return correct; diff --git a/megamek/unittests/megamek/common/loaders/BulkUnitFileTest.java b/megamek/unittests/megamek/common/loaders/BulkUnitFileTest.java new file mode 100644 index 00000000000..d6833dfe202 --- /dev/null +++ b/megamek/unittests/megamek/common/loaders/BulkUnitFileTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + */ + +package megamek.common.loaders; + +import megamek.common.*; +import megamek.common.equipment.ArmorType; +import megamek.common.verifier.*; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@Disabled +public class BulkUnitFileTest { + + @BeforeAll + public static void initializeStuff() { + MekFileParser.initCanonUnitNames(); + EquipmentType.initializeTypes(); + AmmoType.initializeTypes(); + ArmorType.initializeTypes(); + WeaponType.initializeTypes(); + MiscType.initializeTypes(); + BombType.initializeTypes(); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("allBlkFiles") + void loadVerifySaveVerifyBlks(File file) throws EntitySavingException, IOException { + checkEntityFile(file); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("allMtfFiles") + void loadVerifySaveVerifyMtfs(File file) throws EntitySavingException, IOException { + checkEntityFile(file); + } + + void checkEntityFile(File file) throws EntitySavingException, IOException { + Entity entity = loadUnit(file); + var validation = verify(entity); + // This print is to make sure you are looking at the file you expected to be looking at + System.out.println(file.getAbsoluteFile()); + assertEquals(UnitValidation.VALID, validation.state(), + "The unit is invalid:\n\t" + entity.getDisplayName() + "\n" + validation.report()); + + var suffix = entity instanceof Mek ? ".mtf" : ".blk"; + var tmpFile = File.createTempFile(file.getName(), suffix); + tmpFile.deleteOnExit(); + + if (persistUnit(tmpFile, entity)) { + Entity repersistedEntity = loadUnit(tmpFile); + var reValidation = verify(repersistedEntity); + assertEquals(UnitValidation.VALID, reValidation.state(), + "The unit is invalid after repersisting:\n\t" + tmpFile + "\n\t" + entity.getDisplayName() + "\n" + reValidation.report()); + assertEquals(reValidation.state(), validation.state()); + } + } + + @Test + void loadVerifySaveVerifySpecificFile() throws EntitySavingException, IOException { + var file = new File("data/mekfiles/vehicles/3085u/Cutting Edge/Zugvogel Omni Support Aircraft D.blk"); + checkEntityFile(file); + } + + public static List allMtfFiles() { + try (Stream paths = Files.walk(Paths.get("data/mekfiles"))) { + return paths + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".mtf")) + .map(Path::toFile) + .toList(); + } catch (IOException e) { + // do nothing + } + return List.of(); + } + + public static List allBlkFiles() { + try (Stream paths = Files.walk(Paths.get("data/mekfiles"))) { + return paths + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".blk")) + .map(Path::toFile) + .toList(); + } catch (IOException e) { + // do nothing + } + return List.of(); + } + + private Entity loadUnit(File file) { + try { + MekFileParser mfp = new MekFileParser(file); + return mfp.getEntity(); + } catch (Exception ex) { + fail(ex.getMessage()); + } + throw new IllegalStateException("Should not reach here"); + } + + public static TestEntity getEntityVerifier(Entity unit) { + EntityVerifier entityVerifier = EntityVerifier.getInstance(new File( + "data/mekfiles/UnitVerifierOptions.xml")); + TestEntity testEntity = null; + + if (unit.hasETypeFlag(Entity.ETYPE_MEK)) { + testEntity = new TestMek((Mek) unit, entityVerifier.mekOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_PROTOMEK)) { + testEntity = new TestProtoMek((ProtoMek) unit, entityVerifier.protomekOption, null); + } else if (unit.isSupportVehicle()) { + testEntity = new TestSupportVehicle(unit, entityVerifier.tankOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_TANK)) { + testEntity = new TestTank((Tank) unit, entityVerifier.tankOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_SMALL_CRAFT)) { + testEntity = new TestSmallCraft((SmallCraft) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_JUMPSHIP)) { + testEntity = new TestAdvancedAerospace((Jumpship) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_AERO)) { + testEntity = new TestAero((Aero) unit, entityVerifier.aeroOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_BATTLEARMOR)) { + testEntity = new TestBattleArmor((BattleArmor) unit, entityVerifier.baOption, null); + } else if (unit.hasETypeFlag(Entity.ETYPE_INFANTRY)) { + testEntity = new TestInfantry((Infantry) unit, entityVerifier.infOption, null); + } + return testEntity; + } + + private enum UnitValidation { + VALID, + INVALID; + public static UnitValidation of(boolean valid) { + return valid ? VALID : INVALID; + } + } + + private record Validation(UnitValidation state, String report) { } + + private static Validation verify(Entity unit) { + var sb = new StringBuffer(); + var testEntity = getEntityVerifier(unit); + + var succeeded = testEntity.correctEntity(sb, unit.getTechLevel()); + + var validation = new Validation(UnitValidation.of(succeeded), sb.toString()); + + return validation; + } + + public static boolean persistUnit(File outFile, Entity entity) throws EntitySavingException { + if (entity instanceof Mek) { + try (BufferedWriter out = new BufferedWriter(new FileWriter(outFile))) { + out.write(((Mek) entity).getMtf()); + } catch (Exception e) { + System.out.println(e.getMessage()); + return false; + } + return true; + } + + BLKFile.encode(outFile, entity); + return true; + } + +}