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 extends T> 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) {