diff --git a/megamek/data/css/acarCssFile.css b/megamek/data/css/acarCssFile.css new file mode 100644 index 00000000000..d53b8456029 --- /dev/null +++ b/megamek/data/css/acarCssFile.css @@ -0,0 +1,11 @@ + @@ -65,6 +70,20 @@ protected void initialize() { } + private static String loadCssFromResources() { + var cssFile = new File("data/css/acarCssFile.css"); + if (cssFile.exists()) { + try (InputStream inputStream = Files.newInputStream(Paths.get(cssFile.toURI()))) { + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + + } catch (IOException e) { + logger.error("Error reading CSS file", e); + } + } else { + logger.error("CSS file not found " + cssFile); + } + return ""; + } /** * Creates GameLog named * diff --git a/megamek/src/megamek/common/autoresolve/acar/report/IAttackReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/IAttackReporter.java index 772d8a09db0..3ef37815685 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/IAttackReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/IAttackReporter.java @@ -21,7 +21,7 @@ import megamek.common.strategicBattleSystems.SBFUnit; public interface IAttackReporter { - void reportAttackStart(Formation attacker, int unitNumber, Formation target); + void reportAttackStart(Formation attacker, int unitNumber, Formation target, SBFUnit targetUnit); void reportCannotSucceed(String toHitDesc); diff --git a/megamek/src/megamek/common/autoresolve/acar/report/IEndPhaseReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/IEndPhaseReporter.java index f8287ee256e..4b57f9b36ff 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/IEndPhaseReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/IEndPhaseReporter.java @@ -16,10 +16,14 @@ package megamek.common.autoresolve.acar.report; import megamek.common.Entity; +import megamek.common.autoresolve.component.Formation; +import megamek.common.strategicBattleSystems.SBFUnit; public interface IEndPhaseReporter { void endPhaseHeader(); - void reportUnitDestroyed(Entity entity); + void reportUnitDestroyed(Formation formation, SBFUnit unit); + + void reportElementDestroyed(Formation formation, SBFUnit unit, Entity entity); } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/IWithdrawReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/IWithdrawReporter.java index 192c5f5600e..36ddfb07e9a 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/IWithdrawReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/IWithdrawReporter.java @@ -19,5 +19,7 @@ public interface IWithdrawReporter { + void reportStartingWithdrawForCrippled(Formation withdrawingFormation); + void reportStartingWithdrawForOrder(Formation withdrawingFormation); void reportSuccessfulWithdraw(Formation withdrawingFormation); } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java index e358bb53a46..89c9da8aab8 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java @@ -13,7 +13,6 @@ */ package megamek.common.autoresolve.acar.report; -import megamek.client.ui.swing.util.UIUtil; import megamek.common.IGame; import megamek.common.Roll; import megamek.common.autoresolve.acar.SimulationManager; @@ -22,8 +21,6 @@ import java.util.function.Consumer; -import static megamek.client.ui.swing.tooltip.SBFInGameObjectTooltip.ownerColor; - public class RecoveringNerveActionReporter implements IRecoveringNerveActionReporter { private final IGame game; @@ -44,18 +41,20 @@ public static IRecoveringNerveActionReporter create(SimulationManager manager) { @Override public void reportRecoveringNerveStart(Formation formation, int toHitValue) { reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryAttempt") - .add(new FormationReportEntry(formation.generalName(), UIUtil.hexColor(ownerColor(formation, game))).text()) - .add(toHitValue).noNL() + .add(new FormationReportEntry(formation, game).reportText()) + .add(toHitValue) ); } @Override public void reportMoraleStatusChange(Formation.MoraleStatus newMoraleStatus, Roll roll) { - reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryRoll").indent().add(new RollReportEntry(roll).reportText()).add(newMoraleStatus.name().toLowerCase())); + reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryRoll") + .indent().add(new RollReportEntry(roll).reportText()).add(newMoraleStatus.name().toLowerCase())); } @Override public void reportFailureRoll(Roll roll, SBFFormation.MoraleStatus moraleStatus) { - reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryFail").add(roll.toString()).add(moraleStatus.name().toLowerCase())); + reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryFail") + .add(roll.toString()).add(moraleStatus.name().toLowerCase())); } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/StartingScenarioPhaseReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/StartingScenarioPhaseReporter.java index d0a4d6fe075..709dc7fac3b 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/StartingScenarioPhaseReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/StartingScenarioPhaseReporter.java @@ -13,9 +13,11 @@ */ package megamek.common.autoresolve.acar.report; +import megamek.client.ui.swing.util.UIUtil; import megamek.common.Entity; import megamek.common.IGame; import megamek.common.Player; +import megamek.common.autoresolve.acar.SimulationContext; import megamek.common.autoresolve.acar.SimulationManager; import java.util.ArrayList; @@ -23,6 +25,8 @@ import java.util.List; import java.util.function.Consumer; +import static megamek.client.ui.swing.tooltip.SBFInGameObjectTooltip.ownerColor; + public class StartingScenarioPhaseReporter implements IStartingScenarioPhaseReporter { private final IGame game; @@ -63,7 +67,7 @@ public void formationsSetup(SimulationManager gameManager) { for (var team : teamMap.keySet()) { var teamPlayers = teamMap.get(team); - var teamReport = new PublicReportEntry("acar.header.teamFormations").add(team); + var teamReport = new PublicReportEntry("acar.header.teamFormationsHeader").add(team); reportConsumer.accept(teamReport); for (var player : teamPlayers) { playerFinalReport(player); @@ -72,27 +76,43 @@ public void formationsSetup(SimulationManager gameManager) { } private void playerFinalReport(Player player) { - var playerEntities = game.getInGameObjects().stream() - .filter(e -> e.getOwnerId() == player.getId()) - .filter(Entity.class::isInstance) - .map(Entity.class::cast) - .toList(); - - reportConsumer.accept(new PublicReportEntry("acar.startingScenario.numberOfUnits").add(new PlayerNameReportEntry(player).reportText()) - .add(playerEntities.size()).indent()); - - for (var entity : playerEntities) { - var armor = entity.getArmorRemainingPercent(); - if (armor < 0d) { - armor = 0d; + var formations = ((SimulationContext) game).getActiveFormations(player); + + reportConsumer.accept(new PublicReportEntry("acar.header.teamFormations") + .add(new PlayerNameReportEntry(player).reportText()) + .add(formations.size()).indent()); + + for (var formation : formations) { + var color = ownerColor(formation, game); + if (!formation.isSingleEntity()) { + reportConsumer.accept(new PublicReportEntry("acar.startingScenario.formation.numberOfUnits") + .add(new FormationReportEntry( + formation.getName(), "", UIUtil.hexColor(color)).reportText()) + .add(formation.getUnits().size()) + .indent(1)); + } + + for (var unit : formation.getUnits()) { + if (!formation.isSingleEntity()) { + reportConsumer.accept(new PublicReportEntry("acar.startingScenario.unit.numberOfElements") + .add(new UnitReportEntry(unit, ownerColor(formation, game)).reportText()) + .add(unit.getElements().size()) + .indent(2)); + } + for (var element : unit.getElements()) { + var entity = (Entity) game.getInGameObject(element.getId()).orElseThrow(); + var armor = entity.getArmorRemainingPercent(); + var internal = entity.getInternalRemainingPercent(); + var crew = entity.getCrew(); + reportConsumer.accept(new PublicReportEntry("acar.startingScenario.unitStats") + .add(new EntityNameReportEntry(entity).reportText()) + .add(String.format("%.2f%%", armor * 100)) + .add(String.format("%.2f%%", internal * 100)) + .add(crew.getName()) + .add(crew.getHits()) + .indent(3)); + } } - reportConsumer.accept(new PublicReportEntry("acar.startingScenario.unitStats") - .add(new EntityNameReportEntry(entity).reportText()) - .add(String.format("%.2f%%", armor * 100)) - .add(String.format("%.2f%%", entity.getInternalRemainingPercent() * 100)) - .add(entity.getCrew().getName()) - .add(entity.getCrew().getHits()) - .indent(2)); } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/UnitReportEntry.java b/megamek/src/megamek/common/autoresolve/acar/report/UnitReportEntry.java index d9a9374f873..b33f16a1140 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/UnitReportEntry.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/UnitReportEntry.java @@ -15,8 +15,10 @@ import megamek.client.ui.swing.util.UIUtil; import megamek.common.autoresolve.component.Formation; +import megamek.common.strategicBattleSystems.SBFUnit; import java.awt.*; +import java.util.ArrayList; public class UnitReportEntry extends PublicReportEntry { @@ -33,12 +35,24 @@ public UnitReportEntry(String formationName, String unitName, String playerColor } public UnitReportEntry(Formation formation, int unitIndex, Color color) { - this(formation.generalName(), unitName(formation, unitIndex), UIUtil.hexColor(color)); + this((formation.getEntity() == null) ? formation.getName() : null, unitName(formation, unitIndex), UIUtil.hexColor(color)); } + public UnitReportEntry(Formation formation, SBFUnit unit, Color color) { + this((formation.getEntity() == null) ? formation.getName() : null, unit.getName(), UIUtil.hexColor(color)); + } + + public UnitReportEntry(SBFUnit unit, Color color) { + this(null, unit.getName(), UIUtil.hexColor(color)); + } + + @Override protected String reportText() { - return "" + formationName + "" + ", " + if (formationName == null) { + return "" + unitName + ""; + } + return "[" + formationName + "]" + " " + "" + unitName + ""; } @@ -46,7 +60,9 @@ private static String unitName(Formation formation, int unitIndex) { if ((unitIndex < 0) || (unitIndex > formation.getUnits().size())) { throw new IllegalArgumentException("Invalid unit index"); } else { - return formation.getUnits().get(unitIndex).getName(); + var unit = formation.getUnits().get(unitIndex); + + return unit.getName(); } } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/VictoryPhaseReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/VictoryPhaseReporter.java index a570d6181e9..bec0bfe022a 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/VictoryPhaseReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/VictoryPhaseReporter.java @@ -44,6 +44,7 @@ public static IVictoryPhaseReporter create(SimulationManager manager) { @Override public void victoryHeader() { + reportConsumer.accept(new DividerEntry()); reportConsumer.accept(new ReportEntryWithAnchor("acar.endOfCombat.header", "end-of-combat").noNL()); reportConsumer.accept(new LinkEntry("acar.link.backRef", "summary-end-of-combat")); } @@ -64,7 +65,7 @@ public void victoryResult(SimulationManager gameManager) { var victoryResult = gameManager.getCurrentVictoryResult(); reportConsumer.accept(new ReportEntryWithAnchor("acar.victory.teamVictorious", "victory").add(victoryResult.getWinningTeam())); - + reportConsumer.accept(new DividerEntry()); for (var team : teamMap.keySet()) { var teamPlayers = teamMap.get(team); reportConsumer.accept(new ReportEntryWithAnchor("acar.endOfCombat.teamReportHeader", "end-team-" + team).add(team).noNL()); diff --git a/megamek/src/megamek/common/autoresolve/acar/report/WithdrawReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/WithdrawReporter.java index dceb1dc7a8f..80ff8850869 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/WithdrawReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/WithdrawReporter.java @@ -36,11 +36,26 @@ public static IWithdrawReporter create(SimulationManager manager) { return new WithdrawReporter(manager.getGame(), manager::addReport); } + @Override + public void reportStartingWithdrawForCrippled(Formation withdrawingFormation) { + reportConsumer.accept( + new PublicReportEntry("acar.endPhase.withdrawAnnouncement") + .add(new FormationReportEntry(withdrawingFormation, game).reportText()) + .add(new PublicReportEntry("acar.endPhase.withdrawMotive.crippled").reportText())); + } + + @Override + public void reportStartingWithdrawForOrder(Formation withdrawingFormation) { + reportConsumer.accept( + new PublicReportEntry("acar.endPhase.withdrawAnnouncement") + .add(new FormationReportEntry(withdrawingFormation, game).reportText()) + .add(new PublicReportEntry("acar.endPhase.withdrawMotive.metOrderCondition").reportText())); + } + @Override public void reportSuccessfulWithdraw(Formation withdrawingFormation) { reportConsumer.accept( new PublicReportEntry("acar.endPhase.withdrawSuccess") - .add(new FormationReportEntry(withdrawingFormation, game).reportText()) - .indent(2)); + .add(new FormationReportEntry(withdrawingFormation, game).reportText())); } } diff --git a/megamek/src/megamek/common/autoresolve/component/Formation.java b/megamek/src/megamek/common/autoresolve/component/Formation.java index e24dde9d7de..7fabfb86501 100644 --- a/megamek/src/megamek/common/autoresolve/component/Formation.java +++ b/megamek/src/megamek/common/autoresolve/component/Formation.java @@ -24,6 +24,9 @@ import megamek.common.strategicBattleSystems.SBFFormation; import megamek.common.strategicBattleSystems.SBFUnit; +import java.util.ArrayList; +import java.util.List; + public class Formation extends SBFFormation { @@ -39,6 +42,36 @@ public class Formation extends SBFFormation { private InitiativeRoll initiativeRoll = new InitiativeRoll(); private Role role; private Entity entity; + private List beingTargetedBy = new ArrayList<>(); + private int startingSize; + + public void addBeingTargetedBy(Formation formation) { + beingTargetedBy.add(formation.getId()); + } + + public int beingTargetByHowMany() { + return beingTargetedBy.size(); + } + + public int getStartingSize() { + return startingSize; + } + + public void setStartingSize(int startingSize) { + this.startingSize = startingSize; + } + + public int currentSize() { + int total = 0; + for (var unit : getUnits()) { + total += unit.getElements().size(); + } + return total; + } + + public boolean isSingleEntity() { + return entity != null; + } public Entity getEntity() { return entity; @@ -120,10 +153,36 @@ public void reset() { targetFormationId = Entity.NONE; engagementControl = null; highStressEpisode = false; + beingTargetedBy.clear(); getMemory().clear("range."); setDone(false); } + public String getDisplayName() { + if (getEntity() != null) { + return entity.getDisplayName() + " ID:" + entity.getId(); + } + + return getName()+ " ID:" + getId(); + } + + public List getElementNames() { + var entityNames = new ArrayList(6); + + if (getEntity() != null) { + entityNames.add(entity.getDisplayName()); + return entityNames; + } + entityNames.add(getName()); + for (var unit : getUnits()) { + for (var element : unit.getElements()) { + entityNames.add(element.getName() + " ID:" + element.getId()); + } + } + return entityNames; + } + + public int getCurrentMovement() { return getUnits().stream().mapToInt(u -> Math.max(0, u.getMovement() - u.getMpCrits())).min().orElse(0); } @@ -179,6 +238,11 @@ public boolean isCrippled() { return unitIsCrippledLatch; } + public boolean hasDamageAtRange(ASRange range) { + return getUnits().stream() + .anyMatch(u -> u.getCurrentDamage().getDamage(range).hasDamage()); + } + public void setStdDamage(ASDamageVector stdDamage) { this.stdDamage = stdDamage; } diff --git a/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java similarity index 89% rename from megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java rename to megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java index 2c07875093d..83a7ec28c5c 100644 --- a/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java @@ -15,7 +15,7 @@ import megamek.client.ui.swing.calculationReport.DummyCalculationReport; import megamek.common.Entity; -import megamek.common.ForceAssignable; +import megamek.common.GunEmplacement; import megamek.common.alphaStrike.ASDamage; import megamek.common.alphaStrike.ASDamageVector; import megamek.common.alphaStrike.ASRange; @@ -24,8 +24,6 @@ import megamek.common.autoresolve.acar.SimulationContext; import megamek.common.autoresolve.acar.role.Role; import megamek.common.autoresolve.component.Formation; -import megamek.common.force.Force; -import megamek.common.force.Forces; import megamek.common.strategicBattleSystems.BaseFormationConverter; import megamek.common.strategicBattleSystems.SBFUnit; import megamek.common.strategicBattleSystems.SBFUnitConverter; @@ -33,11 +31,11 @@ import java.util.ArrayList; -public class EntityToFormationConverter extends BaseFormationConverter { - private static final MMLogger logger = MMLogger.create(EntityToFormationConverter.class); +public class EntityAsFormation extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(EntityAsFormation.class); private final Entity entity; - public EntityToFormationConverter(Entity entity, SimulationContext game) { + public EntityAsFormation(Entity entity, SimulationContext game) { super(null, game, new Formation(), new DummyCalculationReport()); this.entity = entity; } @@ -45,6 +43,9 @@ public EntityToFormationConverter(Entity entity, SimulationContext game) { @Override public Formation convert() { var thisUnit = new ArrayList(); + if (entity instanceof GunEmplacement gun) { + gun.initializeArmor(50, 0); + } var element = ASConverter.convertAndKeepRefs(entity); if (element != null) { thisUnit.add(element); @@ -54,7 +55,7 @@ public Formation convert() { return null; } - SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, entity.getDisplayName(), report).createSbfUnit(); + SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, entity.getDisplayName() + " ID:" + entity.getId(), report).createSbfUnit(); formation.addUnit(convertedUnit); formation.setEntity(entity); formation.setRole(Role.getRole(entity.getRole())); @@ -70,6 +71,7 @@ public Formation convert() { unit.setArmor(health); unit.setCurrentArmor(health); } + formation.setStartingSize(formation.currentSize()); return formation; } diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java similarity index 73% rename from megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java rename to megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java index e5d57456d3e..5e821e54142 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java @@ -29,45 +29,51 @@ import megamek.common.strategicBattleSystems.BaseFormationConverter; import megamek.common.strategicBattleSystems.SBFUnit; import megamek.common.strategicBattleSystems.SBFUnitConverter; +import megamek.common.util.Counter; import megamek.logging.MMLogger; import java.util.ArrayList; -public class ForceToFormationConverter extends BaseFormationConverter { - private static final MMLogger logger = MMLogger.create(ForceToFormationConverter.class); +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; - public ForceToFormationConverter(Force force, SimulationContext game) { +public class EntityAsUnit extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(EntityAsUnit.class); + + public EntityAsUnit(Force force, SimulationContext game) { super(force, game, new Formation()); } @Override public Formation convert() { - var forceName = ""; Forces forces = game.getForces(); - - // default role - Role role = Role.getRole(UnitRole.SKIRMISHER); - + var player = game.getPlayer(force.getOwnerId()); + Counter counter = new Counter<>(); for (Force subforce : forces.getFullSubForces(force)) { - var thisUnit = new ArrayList(); + if (!subforce.getSubForces().isEmpty() || subforce.getEntities().isEmpty()) { + continue; + } for (ForceAssignable entity : forces.getFullEntities(subforce)) { + var thisUnit = new ArrayList(); + var unitName = "UNKNOWN"; if (entity instanceof Entity entityCast) { - forceName = entityCast.getDisplayName(); + entityCast.setOwner(player); + unitName = entityCast.getDisplayName() + " ID:" + entityCast.getId(); var element = ASConverter.convertAndKeepRefs(entityCast); if (element != null) { thisUnit.add(element); - role = Role.getRole(entityCast.getRole()); + counter.add(Role.getRole(entityCast.getRole())); } else { var msg = String.format("Could not convert entity %s to AS element", entityCast); logger.error(msg); } } + SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, unitName, report).createSbfUnit(); + formation.addUnit(convertedUnit); } - SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, subforce.getName(), report).createSbfUnit(); - formation.addUnit(convertedUnit); } - formation.setName(forceName); - formation.setRole(role); + formation.setOwnerId(force.getOwnerId()); + formation.setName(force.getName()); + formation.setRole(firstNonNull(counter.top(), Role.getRole(UnitRole.SKIRMISHER))); formation.setStdDamage(setStdDamageForFormation(formation)); for (var unit : formation.getUnits()) { var health = 0; @@ -77,6 +83,7 @@ public Formation convert() { unit.setArmor(health); unit.setCurrentArmor(health); } + formation.setStartingSize(formation.currentSize()); return formation; } diff --git a/megamek/src/megamek/common/autoresolve/converter/FlattenForces.java b/megamek/src/megamek/common/autoresolve/converter/FlattenForces.java new file mode 100644 index 00000000000..9d6a9ca611c --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/FlattenForces.java @@ -0,0 +1,72 @@ +/* + * 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.autoresolve.converter; + +import megamek.common.IGame; +import megamek.common.force.Force; +import megamek.common.force.Forces; + +import java.util.ArrayList; + +public class FlattenForces extends ForceConsolidation { + + @Override + protected int getMaxEntitiesInSubForce() { + return -1; + } + + @Override + protected int getMaxEntitiesInTopLevelForce() { + return -1; + } + + @Override + public void consolidateForces(IGame game) { + + var newTopLevelForces = new ArrayList(); + int forceId = 0; + for (var force : game.getForces().getTopLevelForces()) { + var player = game.getPlayer(force.getOwnerId()); + var team = player.getTeam(); + var hasNoSubForce = force.subForceCount() == 0; + var hasEntities = force.entityCount() > 0; + if (hasNoSubForce && hasEntities) { + forceId = transformIntoTopLevelForce(force, force, newTopLevelForces, forceId, team); + } else { + for (var subForce : game.getForces().getFullSubForces(force)) { + forceId = transformIntoTopLevelForce(force, subForce, newTopLevelForces, forceId, team); + } + } + } + game.setForces(new Forces(game)); + createForcesOnGame(game, newTopLevelForces, game.getForces()); + } + + private static int transformIntoTopLevelForce(Force force, Force subForce, ArrayList newTopLevelForces, int forceId, int team) { + var hasNoSubForce = subForce.subForceCount() == 0; + var hasEntities = subForce.entityCount() > 0; + if (hasNoSubForce && hasEntities) { + + var topLevel = new Container(forceId++, subForce.getName(), force.getName(), team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); + topLevel.subs().add( + new Container(forceId++, subForce.getName(), force.getName(), team, force.getOwnerId(), new ArrayList<>(subForce.getEntities()), new ArrayList<>()) + ); + newTopLevelForces.add(topLevel); + } + return forceId; + } + +} diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java index d31b9b9db0d..b48d7505440 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java +++ b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java @@ -20,6 +20,7 @@ import megamek.common.force.Force; import megamek.common.force.Forces; import megamek.common.icons.Camouflage; +import megamek.logging.MMLogger; import java.util.*; import java.util.stream.Collectors; @@ -30,10 +31,68 @@ * @author Luana Coppio */ public abstract class ForceConsolidation { - + private static final MMLogger logger = MMLogger.create(ForceConsolidation.class); protected abstract int getMaxEntitiesInSubForce(); protected abstract int getMaxEntitiesInTopLevelForce(); + public record Container(int uid, String name, String breadcrumb, int teamId, int playerId, List entities, List subs) { + public boolean isLeaf() { + return subs.isEmpty() && !entities.isEmpty(); + } + + public boolean isTop() { + return !subs.isEmpty() && entities.isEmpty(); + } + + @Override + public String toString() { + return new StringJoiner(", ", Container.class.getSimpleName() + "[", "]") + .add("uid=" + uid) + .add("breadcrumb='" + breadcrumb + "'") + .add("name='" + name + "'") + .add("teamId=" + teamId) + .add("playerId=" + playerId) + .add("entities=" + entities) + .add("subs=" + subs) + .toString(); + } + } + + public record ForceRepresentation(int uid, int teamId, int playerId, int[] entities, int[] subForces) { + public boolean isLeaf() { + return subForces.length == 0 && entities.length > 0; + } + + public boolean isTop() { + return subForces.length > 0 && entities.length == 0; + } + } + + /** + * Finds a cycle in the forces, if any. + * @param forces The forces to check for cycles + * @return The force that is the root of a cycle, or null if no cycle is found + */ + public static Force cycleFinder(Forces forces) { + var forcesInternalRepresentation = forces.getForcesInternalRepresentation(); + var visited = new HashSet(); + var stack = new ArrayDeque<>(forces.getTopLevelForces()); + + while (!stack.isEmpty()) { + var force = stack.pop(); + if (!visited.add(force.getId())) { + return force; + } + for (var subForceId : force.getSubForces()) { + var subForce = forcesInternalRepresentation.get(subForceId); + if (subForce != null) { + stack.push(subForce); + } + } + } + return null; + } + /** * Consolidates forces by redistributing entities and sub forces as needed. * It will balance the forces by team, ensuring that each force has a maximum of 20 entities and 4 sub forces. @@ -50,39 +109,76 @@ public void consolidateForces(IGame game) { } var representativeOwnerForForce = new HashMap>(); for (var force : forces.getAllForces()) { - representativeOwnerForForce.computeIfAbsent(teamByPlayer.get(force.getOwnerId()), k -> new ArrayList<>()).add(game.getPlayer(force.getOwnerId())); + representativeOwnerForForce.computeIfAbsent(teamByPlayer.get(force.getOwnerId()), + k -> new ArrayList<>()).add(game.getPlayer(force.getOwnerId())); } - List forceRepresentation = getForceRepresentations(forces, teamByPlayer); - var balancedConsolidateForces = balanceForces(forceRepresentation); - + var forcesRepresentation = translateForcesToForceRepresentation(forces, teamByPlayer); + var consolidatedForces = balanceForces(forcesRepresentation); clearAllForces(forces); + createForcesOnGame(game, consolidatedForces, forces); + } - for (var forceRep : balancedConsolidateForces) { - var player = representativeOwnerForForce.get(forceRep.teamId()).get(0); - var parentForceId = forces.addTopLevelForce( - new Force( - "[Team " + forceRep.teamId() + "] "+ forceNameByPlayer.get(player.getId()) + " Formation", - -1, - new Camouflage(), - player), - player); - for (var subForce : forceRep.subs()) { - var subForceId = forces.addSubForce( - new Force( - "[Team " + forceRep.teamId() + "] " + subForce.uid() + " Unit", - -1, - new Camouflage(), - player), - forces.getForce(parentForceId)); - for (var entityId : subForce.entities()) { - forces.addEntity((Entity) game.getEntityFromAllSources(entityId), subForceId); - } + protected static void createForcesOnGame( + IGame game, + List consolidatedForces, + Forces forces + ) + { + for (var forceRep : consolidatedForces) { + var player = game.getPlayer(forceRep.playerId()); + var camouflage = player.getCamouflage().clone(); + + depthFirstSearch( + game, + forceRep, + forces, + player, + camouflage, + -1); + } + } + + protected static void depthFirstSearch( + IGame game, + Container forceRep, + Forces forces, + Player player, + Camouflage camouflage, + int parentForceId + ) { + var node = new Force( + forceRep.name(), + -1, + camouflage, + player); + int newForceId; + if (parentForceId == -1) { + newForceId = forces.addTopLevelForce(node, player); + } else { + newForceId = forces.addSubForce(node, forces.getForce(parentForceId)); + } + for (var entityId : forceRep.entities()) { + var entity = (Entity) game.getEntityFromAllSources(entityId); + if (entity == null) { + logger.error("Entity id " + entityId + " not found in game, could not load at " + forceRep); + continue; } + forces.addEntity(entity, newForceId); + } + for (var subForce : forceRep.subs()) { + depthFirstSearch( + game, + subForce, + forces, + player, + camouflage, + newForceId); } + } - private void clearAllForces(Forces forces) { + protected static void clearAllForces(Forces forces) { // Remove all empty forces and sub forces after consolidation forces.deleteForces(forces.getAllForces()); } @@ -94,40 +190,15 @@ private void clearAllForces(Forces forces) { * @param teamByPlayer A map of player IDs to team IDs * @return A list of ForceRepresentations */ - private List getForceRepresentations(Forces forces, Map teamByPlayer) { - List forceRepresentations = new ArrayList<>(); + protected List translateForcesToForceRepresentation(Forces forces, Map teamByPlayer) { + List forceRepresentations = new ArrayList<>(); for (Force force : forces.getTopLevelForces()) { int[] entityIds = forces.getFullEntities(force).stream().mapToInt(ForceAssignable::getId).toArray(); - forceRepresentations.add(new BalancedConsolidateForces.ForceRepresentation(force.getId(), teamByPlayer.get(force.getOwnerId()), entityIds, new int[0])); + forceRepresentations.add(new ForceRepresentation(force.getId(), teamByPlayer.get(force.getOwnerId()), force.getOwnerId(), entityIds, new int[0])); } return forceRepresentations; } - public record Container(int uid, int teamId, int[] entities, Container[] subs) { - public boolean isLeaf() { - return subs.length == 0 && entities.length > 0; - } - - public boolean isTop() { - return subs.length > 0 && entities.length == 0; - } - - @Override - public String toString() { - return "Container(uid=" + uid + ", team=" + teamId + ", ent=" + Arrays.toString(entities) + ", subs=" + Arrays.toString(subs) + ")"; - } - } - - public record ForceRepresentation(int uid, int teamId, int[] entities, int[] subForces) { - public boolean isLeaf() { - return subForces.length == 0 && entities.length > 0; - } - - public boolean isTop() { - return subForces.length > 0 && entities.length == 0; - } - } - /** * Balances the forces by team, tries to ensure that every team has the same number of top level forces, each within the ACS parameters * of a maximum of 20 entities and 4 sub forces. It also aggregates the entities by team instead of keeping segregated by player. @@ -135,7 +206,7 @@ public boolean isTop() { * @param forces List of Forces to balance * @return List of Trees representing the balanced forces */ - public List balanceForces(List forces) { + protected List balanceForces(List forces) { Map> entitiesByTeam = new HashMap<>(); for (ForceRepresentation c : forces) { @@ -157,7 +228,7 @@ public List balanceForces(List forces) { return new ArrayList<>(balancedForces.values()); } - private void createTopLevelForTeam(Map balancedForces, int team, List allEntityIds, int topCount) { + protected void createTopLevelForTeam(Map balancedForces, int team, List allEntityIds, int topCount) { int maxId = balancedForces.keySet().stream().max(Integer::compareTo).orElse(0) + 1; int maxEntitiesPerTopLevelForce = (int) Math.min(Math.ceil((double) allEntityIds.size() / topCount), getMaxEntitiesInTopLevelForce()); @@ -175,9 +246,12 @@ private void createTopLevelForTeam(Map balancedForces, int t var subForceSize = Math.min(subListOfEntityIds.size(), start + step); Container leaf = new Container( maxId++, + null, + null, team, - subListOfEntityIds.subList(start, subForceSize).stream().mapToInt(Integer::intValue).toArray(), - new Container[0]); + -1, + subListOfEntityIds.subList(start, subForceSize).stream().toList(), + new ArrayList<>()); subForces.add(leaf); } @@ -186,12 +260,7 @@ private void createTopLevelForTeam(Map balancedForces, int t break; } - var subForcesArray = new Container[subForces.size()]; - for (int k = 0; k < subForcesArray.length; k++) { - subForcesArray[k] = subForces.get(k); - } - - Container top = new Container(maxId++, team, new int[0], subForcesArray); + Container top = new Container(maxId++, null, null, team, -1, new ArrayList<>(), subForces); balancedForces.put(top.uid(), top); } } diff --git a/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java new file mode 100644 index 00000000000..52e71ffbc6c --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java @@ -0,0 +1,87 @@ +/* + * 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.autoresolve.converter; + +import megamek.common.IGame; +import megamek.common.force.Force; +import megamek.common.force.Forces; + +import java.util.*; + +public class KeepCurrentForces extends ForceConsolidation { + @Override + protected int getMaxEntitiesInSubForce() { + return -1; + } + + @Override + protected int getMaxEntitiesInTopLevelForce() { + return -1; + } + + @Override + public void consolidateForces(IGame game) { + var newTopLevelForces = new ArrayList(); + var cycleFound = KeepCurrentForces.cycleFinder(game.getForces()); + if (cycleFound != null) { + throw new IllegalStateException("Cycle detected in forces " + cycleFound.getName() + " " + cycleFound.getId()); + } + var forcesInternalRepresentation = game.getForces().getForcesInternalRepresentation(); + + Deque queue = new ArrayDeque<>(game.getForces().getTopLevelForces()); + int forceId = 0; + var newForceMap = new HashMap(); + var entityDuplicationChecker = new HashSet(); + while (!queue.isEmpty()) { + var force = queue.poll(); + if (force == null) { + continue; + } + var parentForce = forcesInternalRepresentation.get(force.getParentId()); + var parentNode = parentForce == null ? null : newForceMap.get(parentForce.getId()); + var breadcrumb = ""; + while (parentForce != null) { + breadcrumb = parentForce.getName() + " > "; + parentForce = forcesInternalRepresentation.get(parentForce.getParentId()); + } + var player = game.getPlayer(force.getOwnerId()); + var team = player.getTeam(); + var container = new Container(forceId++, force.getName(), breadcrumb, team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); + newForceMap.put(force.getId(), container); + + for (var entityId : force.getEntities()) { + if (entityDuplicationChecker.contains(entityId)) { + throw new IllegalStateException("Entity " + entityId + " is duplicated"); + } + container.entities().add(entityId); + entityDuplicationChecker.add(entityId); + } + if (parentNode != null) { + parentNode.subs().add(container); + } else { + newTopLevelForces.add(container); + } + + for (var subForceId : force.getSubForces()) { + var subForce = forcesInternalRepresentation.get(subForceId); + queue.add(subForce); + } + } + + game.setForces(new Forces(game)); + createForcesOnGame(game, newTopLevelForces, game.getForces()); + } +} diff --git a/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java new file mode 100644 index 00000000000..bc0835640b9 --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java @@ -0,0 +1,86 @@ +/* + * 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.autoresolve.converter; + +import megamek.common.Entity; +import megamek.common.ForceAssignable; +import megamek.common.UnitRole; +import megamek.common.alphaStrike.ASDamage; +import megamek.common.alphaStrike.ASDamageVector; +import megamek.common.alphaStrike.ASRange; +import megamek.common.alphaStrike.AlphaStrikeElement; +import megamek.common.alphaStrike.conversion.ASConverter; +import megamek.common.autoresolve.acar.SimulationContext; +import megamek.common.autoresolve.acar.role.Role; +import megamek.common.autoresolve.component.Formation; +import megamek.common.force.Force; +import megamek.common.force.Forces; +import megamek.common.strategicBattleSystems.BaseFormationConverter; +import megamek.common.strategicBattleSystems.SBFUnit; +import megamek.common.strategicBattleSystems.SBFUnitConverter; +import megamek.common.util.Counter; +import megamek.logging.MMLogger; + +import java.util.ArrayList; + +import static org.apache.commons.lang3.ObjectUtils.firstNonNull; + +public class LowestForceAsUnit extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(LowestForceAsUnit.class); + + public LowestForceAsUnit(Force force, SimulationContext game) { + super(force, game, new Formation()); + } + + @Override + public Formation convert() { + Forces forces = game.getForces(); + var player = game.getPlayer(force.getOwnerId()); + Counter counter = new Counter<>(); + for (Force subforce : forces.getFullSubForces(force)) { + if (!subforce.getSubForces().isEmpty() || subforce.getEntities().isEmpty()) { + continue; + } + var thisUnit = new ArrayList(); + for (ForceAssignable entity : forces.getFullEntities(subforce)) { + if (entity instanceof Entity entityCast) { + entityCast.setOwner(player); + if (entityCast.getOwnerId() != force.getOwnerId()) { + logger.error("Entity " + entityCast + " does not belong to force " + force); + continue; + } + var element = ASConverter.convertAndKeepRefs(entityCast); + if (element != null) { + thisUnit.add(element); + counter.add(Role.getRole(entityCast.getRole())); + } else { + var msg = String.format("Could not convert entity %s to AS element", entityCast); + logger.error(msg); + } + } + } + if (!thisUnit.isEmpty()) { + SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, subforce.getName(), report).createSbfUnit(); + formation.addUnit(convertedUnit); + } + } + formation.setOwnerId(force.getOwnerId()); + formation.setName(force.getName()); + calcSbfFormationStats(); + formation.setRole(firstNonNull(counter.top(), Role.getRole(UnitRole.SKIRMISHER))); + formation.setStartingSize(formation.currentSize()); + return formation; + } + +} diff --git a/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java b/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java index 0a7140f9aa2..7fe12599031 100644 --- a/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java @@ -45,7 +45,8 @@ public void createForcesOnSimulation(SimulationContext simulation) { for (var player : game.getPlayersList()) { setupPlayer(player, game.getInGameObjects(), simulation); } - + // the forces are present in "game" and should be applied to simulation + simulation.setForces(game.getForces()); convertForcesIntoFormations(simulation); } @@ -64,21 +65,38 @@ public FailedToConvertForceToFormationException(Throwable cause) { * Convert the forces in the game to formations, this is the most important step in the setup of the game, * it converts every top level force into a single formation, and those formations are then added to the game * and used in the auto resolve in place of the original entities - * @param game The game object to convert the forces in + * @param simulationContext The simulationContext which contains the forces to be converted */ - private static void convertForcesIntoFormations(SimulationContext game) { - for(var inGameObject : game.getInGameObjects()) { + private void convertForcesIntoFormations(SimulationContext simulationContext) { + if (!simulationContext.getForces().getAllForces().isEmpty()) { + new KeepCurrentForces().consolidateForces(simulationContext); + // Check the depth of the force, according to the depth of the force, it will either be + for (var force : simulationContext.getForces().getTopLevelForces()) { + try { + var formation = new LowestForceAsUnit(force, simulationContext).convert(); + formation.setTargetFormationId(Entity.NONE); + simulationContext.addUnit(formation); + } catch (Exception e) { + Sentry.captureException(e); + throw new FailedToConvertForceToFormationException(e); + } + } + return; + } + + for (var inGameObject : simulationContext.getInGameObjects()) { try { if (inGameObject instanceof Entity entity) { - var formation = new EntityToFormationConverter(entity, game).convert(); + var formation = new EntityAsFormation(entity, simulationContext).convert(); formation.setTargetFormationId(Entity.NONE); - game.addUnit(formation); + simulationContext.addUnit(formation); } } catch (Exception e) { Sentry.captureException(e); throw new FailedToConvertForceToFormationException(e); } } + } /** @@ -134,15 +152,11 @@ private List setupPlayerForces(List inGameObjects, Player player if (Objects.isNull(entity)) { continue; } - + entity.setId(unit.getId()); entity.setExternalIdAsString(unit.getExternalIdAsString()); entity.setOwner(player); - - // If this unit is a spacecraft, set the crew size and marine size values - if (entity.isLargeCraft() || (entity.getUnitType() == UnitType.SMALL_CRAFT)) { - entity.setNCrew(unit.getNCrew()); - entity.setNMarines(unit.getNMarines()); - } + entity.setNCrew(unit.getNCrew()); + entity.setNMarines(unit.getNMarines()); // Calculate deployment round int deploymentRound = entity.getDeployRound(); entity.setDeployRound(deploymentRound); diff --git a/megamek/src/megamek/common/autoresolve/converter/SingleElementConsolidateForces.java b/megamek/src/megamek/common/autoresolve/converter/SingletonForces.java similarity index 94% rename from megamek/src/megamek/common/autoresolve/converter/SingleElementConsolidateForces.java rename to megamek/src/megamek/common/autoresolve/converter/SingletonForces.java index fbbfd757c2e..e1667d3b681 100644 --- a/megamek/src/megamek/common/autoresolve/converter/SingleElementConsolidateForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/SingletonForces.java @@ -18,7 +18,7 @@ * in a way to consolidate then into valid forces to build Formations out of them. * @author Luana Coppio */ -public class SingleElementConsolidateForces extends ForceConsolidation { +public class SingletonForces extends ForceConsolidation { public static final int MAX_ENTITIES_IN_SUB_FORCE = 1; public static final int MAX_ENTITIES_IN_TOP_LEVEL_FORCE = 1; @@ -32,5 +32,6 @@ protected int getMaxEntitiesInSubForce() { protected int getMaxEntitiesInTopLevelForce() { return MAX_ENTITIES_IN_TOP_LEVEL_FORCE; } + } diff --git a/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java b/megamek/src/megamek/common/autoresolve/converter/SortSBFValidForces.java similarity index 94% rename from megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java rename to megamek/src/megamek/common/autoresolve/converter/SortSBFValidForces.java index 4ac9cf6f633..d54090bdd2d 100644 --- a/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/SortSBFValidForces.java @@ -18,7 +18,7 @@ * in a way to consolidate then into valid forces to build Formations out of them. * @author Luana Coppio */ -public class BalancedConsolidateForces extends ForceConsolidation { +public class SortSBFValidForces extends ForceConsolidation { public static final int MAX_ENTITIES_IN_SUB_FORCE = 6; public static final int MAX_ENTITIES_IN_TOP_LEVEL_FORCE = 20; @@ -32,5 +32,6 @@ protected int getMaxEntitiesInSubForce() { protected int getMaxEntitiesInTopLevelForce() { return MAX_ENTITIES_IN_TOP_LEVEL_FORCE; } + } diff --git a/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java b/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java index 967d02c4322..61fb0f0cf78 100644 --- a/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java +++ b/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java @@ -19,7 +19,6 @@ import megamek.common.Infantry; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** @@ -66,6 +65,9 @@ public int devastateUnit() { public HitDetails damageArmor(HitDetails hitDetails) { if (entity() instanceof BattleArmor te) { var trooperId = te.getRandomTrooper(); + if (trooperId == -1) { + return hitDetails.killsCrew(); + } var currentValueArmor = te.getArmor(BattleArmor.LOC_SQUAD); var newArmorValue = Math.max(currentValueArmor - hitDetails.damageToApply(), 0); if (te.getArmor(trooperId) > 0) { diff --git a/megamek/src/megamek/common/force/Forces.java b/megamek/src/megamek/common/force/Forces.java index 7faa3e92109..486a4ae3192 100644 --- a/megamek/src/megamek/common/force/Forces.java +++ b/megamek/src/megamek/common/force/Forces.java @@ -299,6 +299,10 @@ public Player getOwner(int forceId) { return forces.get(getForceId(entity.getId())); } + public HashMap getForcesInternalRepresentation() { + return forces; + } + /** * Returns the id of the force that the provided entity is a direct part of. * E.g., If it is part of a lance in a company, the lance id will be returned. diff --git a/megamek/src/megamek/common/util/CollectionUtil.java b/megamek/src/megamek/common/util/CollectionUtil.java index eb049a20d5d..5ac9001f371 100644 --- a/megamek/src/megamek/common/util/CollectionUtil.java +++ b/megamek/src/megamek/common/util/CollectionUtil.java @@ -15,12 +15,11 @@ * * You should have received a copy of the GNU General Public License * along with MegaMek. If not, see . - */ + */ package megamek.common.util; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** Some utility methods for Collections. */ public final class CollectionUtil { @@ -29,23 +28,32 @@ private CollectionUtil() { } /** * Returns a list that is the concatenation of the provided lists. Does NOT - * do anything else (e.g. remove duplicate entries). + * do anything else (e.g. remove duplicate entries). */ public static List union(List c1, List c2) { List result = new ArrayList<>(c1); result.addAll(c2); return result; } - - /** + + /**\ + * Returns a hashmap that has the number of occurrence of each element on the list + * @param listOfElements List of elements to count. + * @return Map with the count of each element in the list. + */ + static public Counter counter(List listOfElements) { + return new Counter<>(listOfElements); + } + + /** * @return One element (not randomly chosen) of the collection or the element if it has only one. * @throws java.util.NoSuchElementException if the collection is empty. */ public static T anyOneElement(Collection collection) { return collection.stream().findFirst().orElseThrow(); } - - /** + + /** * Returns the only element of the collection. * Throws an IllegalArgument exception if the collection size is greater than 1. * Throws a NoSuchElement exception if it is empty. diff --git a/megamek/src/megamek/common/util/Counter.java b/megamek/src/megamek/common/util/Counter.java new file mode 100644 index 00000000000..90027c8f749 --- /dev/null +++ b/megamek/src/megamek/common/util/Counter.java @@ -0,0 +1,138 @@ +/* + * 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.util; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * This class counts elements + * @param + */ +public class Counter implements Collection { + + private final Map map; + + public Counter() { + this.map = new HashMap<>(); + } + + public Counter(List initialValue) { + this.map = initialValue.stream().collect(Collectors.groupingBy(s -> s, + Collectors.counting())); + } + + public T top() { + return map.entrySet().stream() + .max(Comparator.comparingLong(Map.Entry::getValue)) + .map(Map.Entry::getKey) + .orElse(null); + } + + public T bottom() { + return map.entrySet().stream() + .min(Comparator.comparingLong(Map.Entry::getValue)) + .map(Map.Entry::getKey) + .orElse(null); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return map.containsKey(o); + } + + @Override + public Iterator iterator() { + return map.keySet().iterator(); + } + + @Override + public Object[] toArray() { + return map.keySet().toArray(); + } + + @Override + public T1[] toArray(T1[] a) { + return map.keySet().toArray(a); + } + + @Override + public boolean add(T t) { + if (map.containsKey(t)) { + map.put(t, map.get(t) + 1); + } else { + map.put(t, 1L); + } + return true; + } + + @Override + public boolean remove(Object o) { + if (o instanceof Long) { + return false; + } + else { + var ret = map.remove(o); + return ret != null; + } + } + + @Override + public boolean containsAll(Collection c) { + return map.keySet().containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + for (T t : c) { + add(t); + } + return true; + } + + @Override + public boolean removeAll(Collection c) { + for (Object o : c) { + remove(o); + } + return true; + } + + @Override + public boolean retainAll(Collection c) { + for (T t : map.keySet()) { + if (!c.contains(t)) { + remove(t); + } + } + return true; + } + + @Override + public void clear() { + map.clear(); + } +} diff --git a/megamek/src/megamek/server/ServerLobbyHelper.java b/megamek/src/megamek/server/ServerLobbyHelper.java index b1c429e13e5..0aaf1d45fce 100644 --- a/megamek/src/megamek/server/ServerLobbyHelper.java +++ b/megamek/src/megamek/server/ServerLobbyHelper.java @@ -18,14 +18,6 @@ */ package megamek.server; -import static java.util.stream.Collectors.toList; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - import megamek.common.Entity; import megamek.common.ForceAssignable; import megamek.common.Game; @@ -38,6 +30,10 @@ import megamek.logging.MMLogger; import megamek.server.totalwarfare.TWGameManager; +import java.util.*; + +import static java.util.stream.Collectors.toList; + public class ServerLobbyHelper { private static final MMLogger logger = MMLogger.create(ServerLobbyHelper.class); @@ -50,11 +46,8 @@ public class ServerLobbyHelper { * if the client sending it is the owner */ static boolean isNewForceValid(Game game, Force force) { - if ((!force.isTopLevel() && !game.getForces().contains(force.getParentId())) - || (force.getChildCount() != 0)) { - return false; - } - return true; + return (force.isTopLevel() || game.getForces().contains(force.getParentId())) + && (force.getChildCount() == 0); } /** @@ -144,7 +137,8 @@ private static HashSet lobbyDisembark(Game game, Entity entity) { */ private static HashSet lobbyDisembarkOthers(Game game, Entity entity, Collection entities) { HashSet result = new HashSet<>(); - if (entity.getTransportId() != Entity.NONE) { + + if (entity != null && entity.getTransportId() != Entity.NONE) { Entity carrier = game.getEntity(entity.getTransportId()); if (carrier != null && !entities.contains(carrier)) { carrier.unload(entity); @@ -230,15 +224,14 @@ public static void receiveForceParent(Packet c, int connId, Game game, TWGameMan var changedForces = new HashSet(); if (newParentId == Force.NO_FORCE) { - forceList.stream().forEach(f -> changedForces.addAll(forces.promoteForce(forces.getForce(f.getId())))); + forceList.forEach(f -> changedForces.addAll(forces.promoteForce(forces.getForce(f.getId())))); } else { if (!forces.contains(newParentId)) { logger.warn("Tried to attach forces to non-existing parent force ID " + newParentId); return; } Force newParent = forces.getForce(newParentId); - forceList.stream() - .forEach(f -> changedForces.addAll(forces.attachForce(forces.getForce(f.getId()), newParent))); + forceList.forEach(f -> changedForces.addAll(forces.attachForce(forces.getForce(f.getId()), newParent))); } if (!changedForces.isEmpty()) { @@ -395,10 +388,10 @@ public static void correctC3Connections(Game game) { for (Entity entity : game.getEntitiesVector()) { if (entity.hasNhC3()) { String net = entity.getC3NetId(); - int id = Entity.NONE; try { - id = Integer.parseInt(net.substring(net.indexOf(".") + 1)); - if (game.getEntity(id).getOwner().isEnemyOf(entity.getOwner())) { + var id = Integer.parseInt(net.substring(net.indexOf(".") + 1)); + var netUnit = game.getEntity(id); + if (netUnit != null && netUnit.getOwner().isEnemyOf(entity.getOwner())) { entity.setC3NetIdSelf(); } } catch (Exception ignored) {