From bd73b51f6296e974212ba64d020ed74abca28fe0 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Tue, 14 Jan 2025 00:56:31 -0300 Subject: [PATCH 1/5] feat: fixing problems with force creation and formation setup, fixed problem with loading from MUL into simulation, now looking into other small problems --- .../client/acs-report-messages.properties | 17 +- .../ui/dialogs/AutoResolveProgressDialog.java | 25 ++- .../helpDialogs/AbstractHelpDialog.java | 34 ++++ megamek/src/megamek/common/BattleArmor.java | 8 +- megamek/src/megamek/common/Entity.java | 2 +- .../megamek/common/autoresolve/Resolver.java | 3 +- .../autoresolve/acar/SimulationContext.java | 19 +- .../autoresolve/acar/SimulationManager.java | 8 +- .../handler/StandardUnitAttackHandler.java | 2 +- .../acar/handler/WithdrawActionHandler.java | 5 + .../autoresolve/acar/phase/EndPhase.java | 8 +- .../autoresolve/acar/phase/FiringPhase.java | 11 +- .../autoresolve/acar/phase/MovementPhase.java | 51 +----- .../acar/report/AttackReporter.java | 4 +- .../acar/report/DummyAttackReporter.java | 2 +- .../acar/report/DummyEndPhaseReporter.java | 9 +- .../acar/report/DummyWithdrawReporter.java | 10 ++ .../acar/report/EndPhaseReporter.java | 17 +- .../acar/report/EntityNameReportEntry.java | 1 + .../acar/report/FormationReportEntry.java | 8 +- .../acar/report/HtmlGameLogger.java | 7 +- .../acar/report/IAttackReporter.java | 2 +- .../acar/report/IEndPhaseReporter.java | 6 +- .../acar/report/IWithdrawReporter.java | 2 + .../report/RecoveringNerveActionReporter.java | 2 +- .../report/StartingScenarioPhaseReporter.java | 62 ++++--- .../acar/report/UnitReportEntry.java | 22 ++- .../acar/report/VictoryPhaseReporter.java | 3 +- .../acar/report/WithdrawReporter.java | 19 +- .../autoresolve/component/Formation.java | 59 ++++++ .../converter/BalancedConsolidateForces.java | 1 + .../converter/EntityToFormationConverter.java | 10 +- .../converter/ForceConsolidation.java | 170 +++++++++++------- .../converter/ForceToFormationConverter.java | 51 ++++-- .../converter/LanceToFormationConverter.java | 112 ++++++++++++ .../autoresolve/converter/MMSetupForces.java | 40 +++-- .../SingleElementConsolidateForces.java | 1 + .../converter/UseCurrentForces.java | 82 +++++++++ .../converter/UseLancesAsFormations.java | 77 ++++++++ .../damage/InfantryDamageApplier.java | 6 +- megamek/src/megamek/common/force/Forces.java | 4 + .../megamek/common/util/CollectionUtil.java | 26 ++- megamek/src/megamek/common/util/Counter.java | 138 ++++++++++++++ .../src/megamek/server/ServerLobbyHelper.java | 33 ++-- 44 files changed, 924 insertions(+), 255 deletions(-) create mode 100644 megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java create mode 100644 megamek/src/megamek/common/autoresolve/converter/UseCurrentForces.java create mode 100644 megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java create mode 100644 megamek/src/megamek/common/util/Counter.java diff --git a/megamek/i18n/megamek/client/acs-report-messages.properties b/megamek/i18n/megamek/client/acs-report-messages.properties index 288b6d49ea3..f00d17b10d8 100644 --- a/megamek/i18n/megamek/client/acs-report-messages.properties +++ b/megamek/i18n/megamek/client/acs-report-messages.properties @@ -16,8 +16,10 @@ ################################################################################ acar.header=

Abstract Combat Auto Resolution

acar.header.startingScenario=

\u25ce Starting Scenario Phase

-acar.header.teamFormations=Team {0} Formations: -acar.startingScenario.numberOfUnits={0} has {1} units. +acar.header.teamFormationsHeader=Team {0} Formations +acar.header.teamFormations={0} has {1} formations +acar.startingScenario.formation.numberOfUnits={0} has {1} units. +acar.startingScenario.unit.numberOfElements={0} has {1} elements. acar.startingScenario.unitStats={0}, Armor: {1}, Structure: {2}, Crew: {3}, Hits: {4} acar.shortBlurb=short blurb @@ -99,14 +101,13 @@ acar.firingPhase.noInternalDamage=Took no internal damage. ## End Phase ################################################################################ acar.endPhase.header=

\u25cf End Phase

-acar.endPhase.destroyedHeader=

Destroyed Units

-acar.endPhase.withdrawAnnouncement=[Withdraw] {0} attempts to withdraw under {1} conditions. -acar.endPhase.withdrawThreshold=[Withdraw] Needs {0}+ to successfully withdraw \u27e8{1}\u27e9. -acar.endPhase.withdrawRoll=[Withdraw] {0} rolled {1} for withdrawal. +acar.endPhase.withdrawAnnouncement=[Withdraw] {0} will withdraw. Reason: {1} acar.endPhase.withdrawSuccess=[Withdraw] The {0} has withdrawn from the battlefield. -acar.endPhase.withdrawFail=[Withdraw] The withdrawal attempt fails. +acar.endPhase.withdrawMotive.crippled=crippled +acar.endPhase.withdrawMotive.metOrderCondition=met order condition acar.endPhase.crewDeath=[Crew] {0} has succumbed from his wounds \u2620 acar.endPhase.crewAlive=[Crew] {0} is still alive ({1} hits) \u263a +acar.endPhase.unitDestroyed=[Destroyed] {0} was destroyed, lost elements: {1}. acar.endPhase.devastated=[Devastated] {0} has been devastated, there is nothing left of it. acar.endPhase.destroyedPilot=[Destroyed] {0} was destroyed by the pilot ejection. acar.endPhase.destroyedOffBoard=[Destroyed] {0} was destroyed after being pushed off the combat envelope. @@ -132,7 +133,7 @@ acar.morale.checkFail=[Morale] {0} fails its morale check! Morale worsens from { ## End of Combat ################################################################################ acar.endOfCombat.header=

\u25d9 End of Combat

-acar.endOfCombat.teamReportHeader=

Team {0} Report:

+acar.endOfCombat.teamReportHeader=

Team {0} Report:

acar.endOfCombat.teamRemainingUnits=

{0} has {1} units remaining.

acar.endOfCombat.teamUnitStats={0}, Armor remaining: {1}, Structure remaining: {2}, Crew: {3}, Hits: {4} acar.endOfCombat.teamUnitStatsShort={0} - Armor remaining: {1}, Structure remaining: {2} diff --git a/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java b/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java index de6c54c13a6..90fc17e6e40 100644 --- a/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java +++ b/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java @@ -179,7 +179,13 @@ public Integer doInBackground() { var result = simulateScenario(); dialog.setEvent(result); stopWatch.stop(); - + if (result == null) { + JOptionPane.showMessageDialog( + getFrame(), + "FAIL", "error", + JOptionPane.ERROR_MESSAGE); + return 0; + } var messageKey = (result.getVictoryResult().getWinningTeam() != Entity.NONE) ? "AutoResolveDialog.messageScenarioTeam" : "AutoResolveDialog.messageScenarioPlayer"; messageKey = (result.getVictoryResult().getWinningTeam() == 0 && result.getVictoryResult().getWinningPlayer() == 0) ? "AutoResolveDialog.messageScenarioDraw" : messageKey; var message = Internationalization.getFormattedText(messageKey, @@ -231,11 +237,18 @@ private AutoResolveConcludedEvent simulateScenario() { return null; })); futures.add(executor.submit(() -> { - var result = Resolver.simulationRun( - setupForces, SimulationOptions.empty(), new Board(board.getWidth(), board.getHeight())) - .resolveSimulation(); - countDownLatch.countDown(); - return result; + try { + var result = Resolver.simulationRun( + setupForces, SimulationOptions.empty(), new Board(board.getWidth(), board.getHeight())) + .resolveSimulation(); + return result; + } catch (Exception e) { + logger.error(e, e); + } finally { + countDownLatch.countDown(); + } + + return null; })); // Wait for all tasks to complete diff --git a/megamek/src/megamek/client/ui/dialogs/helpDialogs/AbstractHelpDialog.java b/megamek/src/megamek/client/ui/dialogs/helpDialogs/AbstractHelpDialog.java index 63a5cbb6732..9aa3e00eb60 100644 --- a/megamek/src/megamek/client/ui/dialogs/helpDialogs/AbstractHelpDialog.java +++ b/megamek/src/megamek/client/ui/dialogs/helpDialogs/AbstractHelpDialog.java @@ -19,11 +19,16 @@ package megamek.client.ui.dialogs.helpDialogs; import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.io.File; import javax.swing.*; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; +import javax.swing.text.AttributeSet; +import javax.swing.text.html.HTML; +import javax.swing.text.html.HTMLDocument; import megamek.client.ui.Messages; import megamek.client.ui.baseComponents.AbstractDialog; @@ -77,6 +82,35 @@ protected Container createCenterPane() { } } }); + + + // Add mouse motion listener to show tooltips for links. + pane.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + int pos = pane.viewToModel2D(e.getPoint()); + if (pos >= 0 && pane.getDocument() instanceof HTMLDocument doc) { + var elem = doc.getCharacterElement(pos); + if (elem != null) { + // The Element’s attributes may point us to a tag + var attrs = elem.getAttributes(); + Object attrsAttribute = attrs.getAttribute(HTML.Tag.A); + + if (attrsAttribute instanceof AttributeSet nAttrs) { + // Try retrieving your custom data-value attribute. + // "data-value" isn’t part of the standard HTML.Attribute enum, + // so we can use HTML.getAttributeKey("data-value"). + String dataValue = (String) nAttrs.getAttribute(HTML.getAttributeKey("data-value")); + + if (dataValue != null) { + // We found our custom attribute, so show it in the tooltip + pane.setToolTipText(dataValue); + } + } + } + } + } + }); scrollPane.getVerticalScrollBar().setUnitIncrement(16); final File helpFile = new File(getHelpFilePath()); diff --git a/megamek/src/megamek/common/BattleArmor.java b/megamek/src/megamek/common/BattleArmor.java index 422489fe3f0..67a0618e86c 100644 --- a/megamek/src/megamek/common/BattleArmor.java +++ b/megamek/src/megamek/common/BattleArmor.java @@ -1250,8 +1250,12 @@ public int getRandomTrooper() { activeTroops.add(loop); } } - int locInt = Compute.randomInt(activeTroops.size()); - return activeTroops.elementAt(locInt); + if (!activeTroops.isEmpty()) { + int locInt = Compute.randomInt(activeTroops.size()); + return activeTroops.elementAt(locInt); + } else { + return -1; + } } @Override diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index a0600fea0e5..4dfcf06d877 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -2661,7 +2661,7 @@ private String createDisplayName(int duplicateMarker) { StringBuilder builder = new StringBuilder(); builder.append(createShortName(duplicateMarker)); - if (getOwner() != null) { + if (getOwner() != null && !getOwner().getName().isBlank()) { builder.append(" (").append(getOwner().getName()).append(")"); } diff --git a/megamek/src/megamek/common/autoresolve/Resolver.java b/megamek/src/megamek/common/autoresolve/Resolver.java index 45fe2d00059..5dcd57d6987 100644 --- a/megamek/src/megamek/common/autoresolve/Resolver.java +++ b/megamek/src/megamek/common/autoresolve/Resolver.java @@ -63,8 +63,7 @@ public AutoResolveConcludedEvent resolveSimulation() { SimulationContext context = new SimulationContext(options, setupForces, board); SimulationManager simulationManager = new SimulationManager(context, suppressLog); initializeGameManager(simulationManager); - simulationManager.execute(); - return simulationManager.getConclusionEvent(); + return simulationManager.execute(); } } diff --git a/megamek/src/megamek/common/autoresolve/acar/SimulationContext.java b/megamek/src/megamek/common/autoresolve/acar/SimulationContext.java index d3dff888b90..9969c7642d8 100644 --- a/megamek/src/megamek/common/autoresolve/acar/SimulationContext.java +++ b/megamek/src/megamek/common/autoresolve/acar/SimulationContext.java @@ -1,15 +1,16 @@ /* * 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 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. * - * 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.acar; @@ -375,7 +376,7 @@ public List scriptedEvents() { } public boolean gameTimerIsExpired() { - return getRoundCount() >= 1000; + return getRoundCount() >= 20; } private int getRoundCount() { diff --git a/megamek/src/megamek/common/autoresolve/acar/SimulationManager.java b/megamek/src/megamek/common/autoresolve/acar/SimulationManager.java index 0e926f62e2b..85c43180de7 100644 --- a/megamek/src/megamek/common/autoresolve/acar/SimulationManager.java +++ b/megamek/src/megamek/common/autoresolve/acar/SimulationManager.java @@ -46,8 +46,8 @@ public class SimulationManager extends AbstractGameManager { private final List phaseHandlers = new ArrayList<>(); private final PhaseEndManager phaseEndManager; private final PhasePreparationManager phasePreparationManager; - private final ActionsProcessor actionsProcessor; - private final InitiativeHelper initiativeHelper; + private final ActionsProcessor actionsProcessor; + private final InitiativeHelper initiativeHelper; private final VictoryHelper victoryHelper; private final SimulationContext simulationContext; private final boolean suppressLog; @@ -62,12 +62,12 @@ public SimulationManager(SimulationContext simulationContext, boolean suppressLo this.victoryHelper = new VictoryHelper(this); } - public void execute() { + public AutoResolveConcludedEvent execute() { changePhase(GamePhase.STARTING_SCENARIO); while (!simulationContext.getPhase().equals(GamePhase.VICTORY)) { changePhase(GamePhase.INITIATIVE); } - + return getConclusionEvent(); } public void addPhaseHandler(PhaseHandler phaseHandler) { diff --git a/megamek/src/megamek/common/autoresolve/acar/handler/StandardUnitAttackHandler.java b/megamek/src/megamek/common/autoresolve/acar/handler/StandardUnitAttackHandler.java index a186e47545d..1b0bce58089 100644 --- a/megamek/src/megamek/common/autoresolve/acar/handler/StandardUnitAttackHandler.java +++ b/megamek/src/megamek/common/autoresolve/acar/handler/StandardUnitAttackHandler.java @@ -74,7 +74,7 @@ private void resolveAttack(Formation attacker, StandardUnitAttack attack, Format } // Start of attack report - reporter.reportAttackStart(attacker, attack.getUnitNumber(), target); + reporter.reportAttackStart(attacker, attack.getUnitNumber(), target, targetUnit); if (toHit.cannotSucceed()) { reporter.reportCannotSucceed(toHit.getDesc()); diff --git a/megamek/src/megamek/common/autoresolve/acar/handler/WithdrawActionHandler.java b/megamek/src/megamek/common/autoresolve/acar/handler/WithdrawActionHandler.java index d575eca1c2e..b81c5b766f5 100644 --- a/megamek/src/megamek/common/autoresolve/acar/handler/WithdrawActionHandler.java +++ b/megamek/src/megamek/common/autoresolve/acar/handler/WithdrawActionHandler.java @@ -47,6 +47,11 @@ public void execute() { var withdrawFormation = withdrawOpt.get(); if (!withdrawFormation.isWithdrawing()) { + if (withdrawFormation.isCrippled()) { + reporter.reportStartingWithdrawForCrippled(withdrawFormation); + } else { + reporter.reportStartingWithdrawForOrder(withdrawFormation); + } withdrawFormation.setWithdrawing(true); } diff --git a/megamek/src/megamek/common/autoresolve/acar/phase/EndPhase.java b/megamek/src/megamek/common/autoresolve/acar/phase/EndPhase.java index 2efdd6fac80..4ef3c3ddb20 100644 --- a/megamek/src/megamek/common/autoresolve/acar/phase/EndPhase.java +++ b/megamek/src/megamek/common/autoresolve/acar/phase/EndPhase.java @@ -136,6 +136,9 @@ private void forgetEverything() { private void destroyUnits(Formation formation, List destroyedUnits) { for (var unit : destroyedUnits) { + if (!formation.isSingleEntity()) { + reporter.reportUnitDestroyed(formation, unit); + } for (var element : unit.getElements()) { var entityOpt = getContext().getEntity(element.getId()); if (entityOpt.isPresent()) { @@ -143,8 +146,9 @@ private void destroyUnits(Formation formation, List destroyedUnits) { var removalConditionTable = entity.isEjectionPossible() ? REMOVAL_CONDITIONS_TABLE : REMOVAL_CONDITIONS_TABLE_NO_EJECTION; entity.setRemovalCondition(removalConditionTable.randomItem()); - - reporter.reportUnitDestroyed(entity); + if (formation.isSingleEntity()) { + reporter.reportElementDestroyed(formation, unit, entity); + } getContext().addUnitToGraveyard(entity); getContext().applyDamageToEntityFromUnit( unit, entity, EntityFinalState.fromEntityRemovalState(entity.getRemovalCondition())); diff --git a/megamek/src/megamek/common/autoresolve/acar/phase/FiringPhase.java b/megamek/src/megamek/common/autoresolve/acar/phase/FiringPhase.java index de2dc8a2ffd..4faf6f63644 100644 --- a/megamek/src/megamek/common/autoresolve/acar/phase/FiringPhase.java +++ b/megamek/src/megamek/common/autoresolve/acar/phase/FiringPhase.java @@ -13,6 +13,7 @@ */ package megamek.common.autoresolve.acar.phase; +import megamek.common.Compute; import megamek.common.alphaStrike.ASRange; import megamek.common.autoresolve.acar.SimulationManager; import megamek.common.autoresolve.acar.action.Action; @@ -78,6 +79,7 @@ private void standardUnitAttacks(AttackRecord attackRecord) { var attack = new StandardUnitAttack(actingFormation.getId(), unitIndex, target.getId(), range); attacks.add(attack); } + target.addBeingTargetedBy(actingFormation); getSimulationManager().addAttack(attacks, actingFormation); } } @@ -97,6 +99,7 @@ private List attack(Formation actingFormation) { } var ret = new ArrayList(); + ret.add(new AttackRecord(actingFormation, target.get(0), unitIds)); return ret; } @@ -183,9 +186,13 @@ private List bestTargetOrPreviousTarget(Formation actingFormation, Se Collections.shuffle(pickTarget); priorityTarget.ifPresent(formation -> pickTarget.add(0, formation)); - var iterator = targets.iterator(); - if (iterator.hasNext()) { + + var iterator = pickTarget.iterator(); + while (iterator.hasNext()) { var target = iterator.next(); + if (target.beingTargetByHowMany() > 2 && iterator.hasNext()) { + continue; + } return List.of(target); } return Collections.emptyList(); diff --git a/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java b/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java index 4cb769ba162..c5bb4939076 100644 --- a/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java +++ b/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java @@ -23,62 +23,19 @@ import megamek.common.autoresolve.acar.action.MoveToCoverAction; import megamek.common.autoresolve.acar.handler.MoveActionHandler; import megamek.common.autoresolve.acar.handler.MoveToCoverActionHandler; -import megamek.common.autoresolve.component.EngagementControl; import megamek.common.autoresolve.component.Formation; import megamek.common.autoresolve.component.FormationTurn; import megamek.common.enums.GamePhase; import megamek.common.strategicBattleSystems.SBFFormation; -import megamek.common.util.weightedMaps.WeightedDoubleMap; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; public class MovementPhase extends PhaseHandler { - private static final WeightedDoubleMap normal = WeightedDoubleMap.of( - EngagementControl.FORCED_ENGAGEMENT, 1.0, - EngagementControl.EVADE, 0.0, - EngagementControl.STANDARD, 1.0, - EngagementControl.OVERRUN, 0.5, - EngagementControl.NONE, 0.0 - ); - - private static final WeightedDoubleMap unsteady = WeightedDoubleMap.of( - EngagementControl.FORCED_ENGAGEMENT, 0.5, - EngagementControl.EVADE, 0.02, - EngagementControl.STANDARD, 1.0, - EngagementControl.OVERRUN, 0.1, - EngagementControl.NONE, 0.01 - ); - - private static final WeightedDoubleMap shaken = WeightedDoubleMap.of( - EngagementControl.FORCED_ENGAGEMENT, 0.2, - EngagementControl.EVADE, 0.1, - EngagementControl.STANDARD, 0.8, - EngagementControl.OVERRUN, 0.05, - EngagementControl.NONE, 0.01 - ); - - private static final WeightedDoubleMap broken = WeightedDoubleMap.of( - EngagementControl.FORCED_ENGAGEMENT, 0.05, - EngagementControl.EVADE, 1.0, - EngagementControl.STANDARD, 0.5, - EngagementControl.OVERRUN, 0.05, - EngagementControl.NONE, 0.3 - ); - - private static final WeightedDoubleMap routed = WeightedDoubleMap.of( - EngagementControl.NONE, 1.0 - ); - - private static final Map> engagementControlOptions = Map.of( - Formation.MoraleStatus.NORMAL, normal, - Formation.MoraleStatus.UNSTEADY, unsteady, - Formation.MoraleStatus.SHAKEN, shaken, - Formation.MoraleStatus.BROKEN, broken, - Formation.MoraleStatus.ROUTED, routed - ); - public MovementPhase(SimulationManager gameManager) { super(gameManager, GamePhase.MOVEMENT); } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/AttackReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/AttackReporter.java index 1810d24d10d..c2032973154 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/AttackReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/AttackReporter.java @@ -42,10 +42,10 @@ public static IAttackReporter create(SimulationManager manager) { } @Override - public void reportAttackStart(Formation attacker, int unitNumber, Formation target) { + public void reportAttackStart(Formation attacker, int unitNumber, Formation target, SBFUnit targetUnit) { var report = new PublicReportEntry("acar.firingPhase.attackAnnouncement"); report.add(new UnitReportEntry(attacker, unitNumber, ownerColor(attacker, game)).text()); - report.add(new FormationReportEntry(target, game).text()); + report.add(new UnitReportEntry(target, targetUnit, ownerColor(target, game)).text()); reportConsumer.accept(report); } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/DummyAttackReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/DummyAttackReporter.java index 84055e9578e..233e742d2ea 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/DummyAttackReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/DummyAttackReporter.java @@ -30,7 +30,7 @@ public static DummyAttackReporter instance() { } @Override - public void reportAttackStart(Formation attacker, int unitNumber, Formation target) { + public void reportAttackStart(Formation attacker, int unitNumber, Formation target, SBFUnit targetUnit) { } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/DummyEndPhaseReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/DummyEndPhaseReporter.java index c34db7a09b4..3719beae27e 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/DummyEndPhaseReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/DummyEndPhaseReporter.java @@ -14,6 +14,8 @@ package megamek.common.autoresolve.acar.report; import megamek.common.Entity; +import megamek.common.autoresolve.component.Formation; +import megamek.common.strategicBattleSystems.SBFUnit; public class DummyEndPhaseReporter implements IEndPhaseReporter { @@ -31,6 +33,11 @@ public void endPhaseHeader() { } @Override - public void reportUnitDestroyed(Entity entity) { + public void reportUnitDestroyed(Formation formation, SBFUnit unit) { + } + + @Override + public void reportElementDestroyed(Formation formation, SBFUnit unit, Entity entity) { + } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/DummyWithdrawReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/DummyWithdrawReporter.java index 8ca91b62549..136a1907a87 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/DummyWithdrawReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/DummyWithdrawReporter.java @@ -27,6 +27,16 @@ public static DummyWithdrawReporter instance() { } + @Override + public void reportStartingWithdrawForCrippled(Formation withdrawingFormation) { + + } + + @Override + public void reportStartingWithdrawForOrder(Formation withdrawingFormation) { + + } + @Override public void reportSuccessfulWithdraw(Formation withdrawingFormation) { diff --git a/megamek/src/megamek/common/autoresolve/acar/report/EndPhaseReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/EndPhaseReporter.java index 207a566bb0c..e605d45f2df 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/EndPhaseReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/EndPhaseReporter.java @@ -17,10 +17,14 @@ import megamek.common.IEntityRemovalConditions; import megamek.common.IGame; import megamek.common.autoresolve.acar.SimulationManager; +import megamek.common.autoresolve.component.Formation; +import megamek.common.strategicBattleSystems.SBFUnit; import java.util.Map; import java.util.function.Consumer; +import static megamek.client.ui.swing.tooltip.SBFInGameObjectTooltip.ownerColor; + public class EndPhaseReporter implements IEndPhaseReporter { private final Consumer reportConsumer; @@ -56,12 +60,21 @@ public void endPhaseHeader() { } @Override - public void reportUnitDestroyed(Entity entity) { + public void reportUnitDestroyed(Formation formation, SBFUnit unit) { + var names = unit.getElements().stream().map(e -> e.getName() + " ID:" + e.getId()).toList(); + var r = new PublicReportEntry("acar.endPhase.unitDestroyed") + .add(new UnitReportEntry(formation, unit, ownerColor(formation, game)).reportText()) + .add(String.join(", ", names)); + reportConsumer.accept(r); + } + + @Override + public void reportElementDestroyed(Formation formation, SBFUnit unit, Entity entity) { var removalCondition = entity.getRemovalCondition(); var reportId = reportIdForEachRemovalCondition.getOrDefault(removalCondition, MSG_ID_UNIT_DESTROYED_UNKNOWINGLY); var r = new PublicReportEntry(reportId) - .add(new EntityNameReportEntry(entity).reportText()); + .add(new UnitReportEntry(formation, unit, ownerColor(formation, game)).reportText()); reportConsumer.accept(r); } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/EntityNameReportEntry.java b/megamek/src/megamek/common/autoresolve/acar/report/EntityNameReportEntry.java index f72c5d60f7e..e2acf81273a 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/EntityNameReportEntry.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/EntityNameReportEntry.java @@ -16,6 +16,7 @@ import megamek.client.ui.swing.util.PlayerColour; import megamek.client.ui.swing.util.UIUtil; import megamek.common.Entity; +import megamek.common.autoresolve.component.Formation; public class EntityNameReportEntry extends PublicReportEntry { diff --git a/megamek/src/megamek/common/autoresolve/acar/report/FormationReportEntry.java b/megamek/src/megamek/common/autoresolve/acar/report/FormationReportEntry.java index 9fc472cd3eb..23b278f1dc7 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/FormationReportEntry.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/FormationReportEntry.java @@ -22,20 +22,22 @@ public class FormationReportEntry extends PublicReportEntry { private final String formationName; private final String playerColorHex; + private final String unitNames; - public FormationReportEntry(String formationName, String playerColorHex) { + public FormationReportEntry(String formationName, String unitNames, String playerColorHex) { super(null); this.formationName = formationName; + this.unitNames = unitNames; this.playerColorHex = playerColorHex; noNL(); } public FormationReportEntry(Formation formation, IGame game) { - this(formation.getName(), UIUtil.hexColor(SBFInGameObjectTooltip.ownerColor(formation, game))); + this(formation.getDisplayName(), String.join(", ", formation.getElementNames()), UIUtil.hexColor(SBFInGameObjectTooltip.ownerColor(formation, game))); } @Override protected String reportText() { - return "" + formationName + ""; + return "" + formationName + ""; } } diff --git a/megamek/src/megamek/common/autoresolve/acar/report/HtmlGameLogger.java b/megamek/src/megamek/common/autoresolve/acar/report/HtmlGameLogger.java index 9411ec41fec..9f4272fd11d 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/HtmlGameLogger.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/HtmlGameLogger.java @@ -44,8 +44,10 @@ protected void initialize() { + + + Simulation Game Log - 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..43f651a2585 100644 --- a/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java +++ b/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java @@ -44,7 +44,7 @@ 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(new FormationReportEntry(formation, game).reportText()) .add(toHitValue).noNL() ); } 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..0da01e16986 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); } diff --git a/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java b/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java index 4ac9cf6f633..bda85dce9ea 100644 --- a/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/BalancedConsolidateForces.java @@ -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/EntityToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java index 2c07875093d..15f5d365f69 100644 --- a/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.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; @@ -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/ForceConsolidation.java b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java index d31b9b9db0d..2d4e27cf2cc 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,42 @@ * @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, 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("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; + } + } + /** * 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 +83,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 +164,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 +180,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 +202,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 +220,11 @@ private void createTopLevelForTeam(Map balancedForces, int t var subForceSize = Math.min(subListOfEntityIds.size(), start + step); Container leaf = new Container( maxId++, + 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 +233,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, team, -1, new ArrayList<>(), subForces); balancedForces.put(top.uid(), top); } } diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java index e5d57456d3e..f589a0fe21c 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java @@ -13,6 +13,7 @@ */ package megamek.common.autoresolve.converter; +import megamek.client.ui.swing.calculationReport.FlexibleCalculationReport; import megamek.common.Entity; import megamek.common.ForceAssignable; import megamek.common.UnitRole; @@ -29,10 +30,13 @@ 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 ForceToFormationConverter extends BaseFormationConverter { private static final MMLogger logger = MMLogger.create(ForceToFormationConverter.class); @@ -42,41 +46,50 @@ public ForceToFormationConverter(Force force, SimulationContext game) { @Override public Formation convert() { - var forceName = ""; - Forces forces = game.getForces(); - - // default role - Role role = Role.getRole(UnitRole.SKIRMISHER); + Counter counter = new Counter<>(); + Forces forces = game.getForces(); 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) { - forceName = entityCast.getDisplayName(); + 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); - 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, subforce.getName(), report).createSbfUnit(); - formation.addUnit(convertedUnit); - } - formation.setName(forceName); - formation.setRole(role); - formation.setStdDamage(setStdDamageForFormation(formation)); - for (var unit : formation.getUnits()) { - var health = 0; - for (var element : unit.getElements()) { - health += element.getCurrentArmor() + element.getCurrentStructure(); + if (!thisUnit.isEmpty()) { + SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, subforce.getName(), report).createSbfUnit(); + formation.addUnit(convertedUnit); } - unit.setArmor(health); - unit.setCurrentArmor(health); } + formation.setOwnerId(force.getOwnerId()); + formation.setName(force.getName()); + calcSbfFormationStats(); + formation.setRole(firstNonNull(counter.top(), Role.getRole(UnitRole.SKIRMISHER))); + formation.setStdDamage(setStdDamageForFormation(formation)); +// for (var unit : formation.getUnits()) { +// var health = 0; +// for (var element : unit.getElements()) { +// health += element.getCurrentArmor() + element.getCurrentStructure(); +// } +// unit.setArmor(health); +// unit.setCurrentArmor(health); +// } + formation.setStartingSize(formation.currentSize()); return formation; } diff --git a/megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java new file mode 100644 index 00000000000..a568ac63e88 --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java @@ -0,0 +1,112 @@ +/* + * 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 LanceToFormationConverter extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(LanceToFormationConverter.class); + + public LanceToFormationConverter(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)) { + for (ForceAssignable entity : forces.getFullEntities(subforce)) { + var thisUnit = new ArrayList(); + var unitName = "UNKNOWN"; + if (entity instanceof Entity entityCast) { + entityCast.setOwner(player); + unitName = entityCast.getDisplayName() + " ID:" + entityCast.getId(); + 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); + } + } + SBFUnit convertedUnit = new SBFUnitConverter(thisUnit, unitName, report).createSbfUnit(); + formation.addUnit(convertedUnit); + } + } + 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; + for (var element : unit.getElements()) { + health += element.getCurrentArmor() + element.getCurrentStructure(); + } + unit.setArmor(health); + unit.setCurrentArmor(health); + } + formation.setStartingSize(formation.currentSize()); + return formation; + } + + private ASDamageVector setStdDamageForFormation(Formation formation) { + // Get the list of damage objects from the units in the formation + var damages = formation.getUnits().stream().map(SBFUnit::getDamage).toList(); + var size = damages.size(); + + // Initialize accumulators for the different damage types + var l = 0; + var m = 0; + var s = 0; + + // Sum up the damage values for each type + for (var damage : damages) { + l += damage.getDamage(ASRange.LONG).damage; + m += damage.getDamage(ASRange.MEDIUM).damage; + s += damage.getDamage(ASRange.SHORT).damage; + } + return new ASDamageVector( + new ASDamage(Math.ceil((double) s / size)), + new ASDamage(Math.ceil((double) m / size)), + new ASDamage(Math.ceil((double) l / size)), + null, + size, + true); + } + +} diff --git a/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java b/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java index 0a7140f9aa2..104b43ac3ae 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 UseCurrentForces().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 ForceToFormationConverter(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 EntityToFormationConverter(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/SingleElementConsolidateForces.java index fbbfd757c2e..f56ff412ece 100644 --- a/megamek/src/megamek/common/autoresolve/converter/SingleElementConsolidateForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/SingleElementConsolidateForces.java @@ -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/UseCurrentForces.java b/megamek/src/megamek/common/autoresolve/converter/UseCurrentForces.java new file mode 100644 index 00000000000..fe0012ec5d1 --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/UseCurrentForces.java @@ -0,0 +1,82 @@ +/* + * 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 UseCurrentForces extends ForceConsolidation { + @Override + protected int getMaxEntitiesInSubForce() { + return -1; + } + + @Override + protected int getMaxEntitiesInTopLevelForce() { + return -1; + } + + private record Node(int parent, Force force) {} + + @Override + public void consolidateForces(IGame game) { + + var newTopLevelForces = new ArrayList(); + var forcesInternalRepresentation = game.getForces().getForcesInternalRepresentation(); + var parents = new HashMap(); + 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 player = game.getPlayer(force.getOwnerId()); + var team = player.getTeam(); + var container = new Container(forceId++, force.getName(), 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); + } + var parentId = parents.get(force.getId()); + if (parentId != null) { + var parentNode = newForceMap.get(parentId); + parentNode.subs().add(container); + } else { + newTopLevelForces.add(container); + } + + for (var subForceId : force.getSubForces()) { + var subForce = forcesInternalRepresentation.get(subForceId); + parents.put(subForceId, force.getId()); + queue.add(subForce); + } + } + + game.setForces(new Forces(game)); + createForcesOnGame(game, newTopLevelForces, game.getForces()); + } +} diff --git a/megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java b/megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java new file mode 100644 index 00000000000..a3038bac905 --- /dev/null +++ b/megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java @@ -0,0 +1,77 @@ +/* + * 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; +import java.util.Map; + +public class UseLancesAsFormations 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 name = force.getName() + " " + subForce.getName(); + if (force.getName().equals(subForce.getName())) { + name = subForce.getName(); + } + + var topLevel = new Container(forceId++, name, team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); + topLevel.subs().add( + new Container(forceId++, name, team, force.getOwnerId(), new ArrayList<>(subForce.getEntities()), new ArrayList<>()) + ); + newTopLevelForces.add(topLevel); + } + return forceId; + } + +} diff --git a/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java b/megamek/src/megamek/common/autoresolve/damage/InfantryDamageApplier.java index 006695809fe..fef19b30414 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; /** @@ -30,7 +29,7 @@ public record InfantryDamageApplier(Infantry entity, EntityFinalState entityFina @Override public int getRandomHitLocation() { var entity = entity(); - if (entity instanceof BattleArmor ba) { + if (entity instanceof BattleArmor) { return BattleArmor.LOC_SQUAD; } return Infantry.LOC_INFANTRY; @@ -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) { From 8809502455ae68d41020d9b8852009267a9907b5 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Tue, 14 Jan 2025 12:05:32 -0300 Subject: [PATCH 2/5] feat: better naming for force conversion strategies --- .../i18n/megamek/common/messages.properties | 2 + .../ui/dialogs/AutoResolveProgressDialog.java | 17 +++---- megamek/src/megamek/common/Entity.java | 2 +- .../autoresolve/acar/phase/MovementPhase.java | 48 +++++++++++++------ ...nConverter.java => EntityAsFormation.java} | 6 +-- ...mationConverter.java => EntityAsUnit.java} | 9 ++-- ...esAsFormations.java => FlattenForces.java} | 11 ++--- .../converter/ForceConsolidation.java | 6 ++- ...rentForces.java => KeepCurrentForces.java} | 16 +++++-- ...nConverter.java => LowestForceAsUnit.java} | 13 +++-- .../autoresolve/converter/MMSetupForces.java | 6 +-- ...lidateForces.java => SingletonForces.java} | 2 +- ...ateForces.java => SortSBFValidForces.java} | 2 +- 13 files changed, 84 insertions(+), 56 deletions(-) rename megamek/src/megamek/common/autoresolve/converter/{EntityToFormationConverter.java => EntityAsFormation.java} (95%) rename megamek/src/megamek/common/autoresolve/converter/{LanceToFormationConverter.java => EntityAsUnit.java} (92%) rename megamek/src/megamek/common/autoresolve/converter/{UseLancesAsFormations.java => FlattenForces.java} (81%) rename megamek/src/megamek/common/autoresolve/converter/{UseCurrentForces.java => KeepCurrentForces.java} (81%) rename megamek/src/megamek/common/autoresolve/converter/{ForceToFormationConverter.java => LowestForceAsUnit.java} (93%) rename megamek/src/megamek/common/autoresolve/converter/{SingleElementConsolidateForces.java => SingletonForces.java} (94%) rename megamek/src/megamek/common/autoresolve/converter/{BalancedConsolidateForces.java => SortSBFValidForces.java} (94%) diff --git a/megamek/i18n/megamek/common/messages.properties b/megamek/i18n/megamek/common/messages.properties index c6dbacba580..c52edabdb07 100644 --- a/megamek/i18n/megamek/common/messages.properties +++ b/megamek/i18n/megamek/common/messages.properties @@ -772,6 +772,8 @@ AutoResolveDialog.messageScenarioPlayer=Player {0} won the scenario. AutoResolveDialog.messageScenarioDraw=The scenario ended in a draw. AutoResolveDialog.message.victory=Your forces won the scenario. Did your side control the battlefield at the end of the scenario? AutoResolveDialog.message.defeat=Your forces lost the scenario. Do you want to declare your side as controlling the battlefield at the end of the scenario? +AutoResolveDialog.messageScenarioError.text=There was an error during the execution of the simulation. If sentry isn't enabled open an issue with the developers. +AutoResolveDialog.messageScenarioError.title=Error during the simulation AutoResolveDialog.victory=Victory! AutoResolveDialog.defeat=Defeat! ResolveDialog.control.title=Control of Battlefield? diff --git a/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java b/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java index 90fc17e6e40..35d2a03f997 100644 --- a/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java +++ b/megamek/src/megamek/client/ui/dialogs/AutoResolveProgressDialog.java @@ -177,17 +177,19 @@ public Integer doInBackground() { StopWatch stopWatch = new StopWatch(); stopWatch.start(); var result = simulateScenario(); - dialog.setEvent(result); - stopWatch.stop(); if (result == null) { JOptionPane.showMessageDialog( getFrame(), - "FAIL", "error", - JOptionPane.ERROR_MESSAGE); - return 0; + Internationalization.getText("AutoResolveDialog.messageScenarioError.text"), + Internationalization.getText("AutoResolveDialog.messageScenarioError.title"), + JOptionPane.INFORMATION_MESSAGE); + return -1; } + dialog.setEvent(result); + stopWatch.stop(); + var messageKey = (result.getVictoryResult().getWinningTeam() != Entity.NONE) ? "AutoResolveDialog.messageScenarioTeam" : "AutoResolveDialog.messageScenarioPlayer"; - messageKey = (result.getVictoryResult().getWinningTeam() == 0 && result.getVictoryResult().getWinningPlayer() == 0) ? "AutoResolveDialog.messageScenarioDraw" : messageKey; + messageKey = (result.getVictoryResult().getWinningTeam() == 0 && result.getVictoryResult().getWinningPlayer() == -1) ? "AutoResolveDialog.messageScenarioDraw" : messageKey; var message = Internationalization.getFormattedText(messageKey, result.getVictoryResult().getWinningTeam(), result.getVictoryResult().getWinningPlayer()); @@ -238,10 +240,9 @@ private AutoResolveConcludedEvent simulateScenario() { })); futures.add(executor.submit(() -> { try { - var result = Resolver.simulationRun( + return Resolver.simulationRun( setupForces, SimulationOptions.empty(), new Board(board.getWidth(), board.getHeight())) .resolveSimulation(); - return result; } catch (Exception e) { logger.error(e, e); } finally { diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 4dfcf06d877..6d659ee4923 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -2661,7 +2661,7 @@ private String createDisplayName(int duplicateMarker) { StringBuilder builder = new StringBuilder(); builder.append(createShortName(duplicateMarker)); - if (getOwner() != null && !getOwner().getName().isBlank()) { + if (getOwner() != null && getOwner().getName() != null && !getOwner().getName().isBlank()) { builder.append(" (").append(getOwner().getName()).append(")"); } diff --git a/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java b/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java index c5bb4939076..cf3f772580c 100644 --- a/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java +++ b/megamek/src/megamek/common/autoresolve/acar/phase/MovementPhase.java @@ -23,6 +23,7 @@ import megamek.common.autoresolve.acar.action.MoveToCoverAction; import megamek.common.autoresolve.acar.handler.MoveActionHandler; import megamek.common.autoresolve.acar.handler.MoveToCoverActionHandler; +import megamek.common.autoresolve.acar.role.Role; import megamek.common.autoresolve.component.Formation; import megamek.common.autoresolve.component.FormationTurn; import megamek.common.enums.GamePhase; @@ -224,9 +225,9 @@ private Optional selectTarget(Formation actingFormation) { // Gather possible targets var canBeTargets = getSimulationManager().getGame().getActiveDeployedFormations().stream() - .filter(f -> actingFormation.getTargetFormationId() == Entity.NONE - || f.getId() == actingFormation.getTargetFormationId()) - .filter(SBFFormation::isDeployed) + .filter(f -> (actingFormation.getTargetFormationId() == Entity.NONE) + || (role.tailTargets() && f.getId() == actingFormation.getTargetFormationId()) + || !role.tailTargets()) .filter(f -> game.getPlayer(f.getOwnerId()).isEnemyOf(player)) .collect(Collectors.toList()); @@ -234,17 +235,9 @@ private Optional selectTarget(Formation actingFormation) { return Optional.empty(); } - if (role.targetsLastAttacker() && actingFormation.getMemory().getInt("lastAttackerId").orElse(Entity.NONE) != Entity.NONE) { - var lastAttackerId = actingFormation.getMemory().getInt("lastAttackerId").orElse(Entity.NONE); - var lastAttacker = canBeTargets.stream() - .filter(f -> f.getId() == lastAttackerId) - .findFirst(); - if (lastAttacker.isPresent()) { - var distance = actingFormation.getPosition().coords().distance(lastAttacker.get().getPosition().coords()); - if (actingFormation.getRole().preferredRange().insideRange(distance)) { - return lastAttacker; - } - } + Optional lastAttacker = getLastAttacker(actingFormation, role, canBeTargets); + if (lastAttacker.isPresent()) { + return lastAttacker; } // 2. Sort the candidates by distance (closest first, or some logic you already have) @@ -302,6 +295,33 @@ private Optional selectTarget(Formation actingFormation) { return Optional.empty(); } + private static Optional getLastAttacker(Formation actingFormation, Role role, List canBeTargets) { + if (role.targetsLastAttacker() && actingFormation.getMemory().getInt("lastAttackerId").orElse(Entity.NONE) != Entity.NONE) { + var lastAttackerId = actingFormation.getMemory().getInt("lastAttackerId").orElse(Entity.NONE); + var lastAttacker = canBeTargets.stream() + .filter(f -> f.getId() == lastAttackerId) + .findFirst(); + + if (lastAttacker.isPresent()) { + var distance = actingFormation.getPosition().coords().distance(lastAttacker.get().getPosition().coords()); + var myMove = actingFormation.getCurrentMovement(); + var targetMove = lastAttacker.get().getCurrentMovement(); + var maxDistance = distance - myMove + targetMove; + var minDistance = distance - myMove - targetMove; + + var canDamageTargetAtCurrent = actingFormation.getStdDamage().getDamage(ASRange.fromDistance(distance)).hasDamage(); + var canDamageTargetAtMin = actingFormation.getStdDamage().getDamage(ASRange.fromDistance(minDistance)).hasDamage(); + var canDamageTargetAtMax = actingFormation.getStdDamage().getDamage(ASRange.fromDistance(maxDistance)).hasDamage(); + if (canDamageTargetAtMax || canDamageTargetAtMin || canDamageTargetAtCurrent) { + // also set the unit as target + actingFormation.setTargetFormationId(lastAttackerId); + return lastAttacker; + } + } + } + return Optional.empty(); + } + private Optional selectTargetOld(Formation actingFormation) { var game = getSimulationManager().getGame(); diff --git a/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java similarity index 95% rename from megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java rename to megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java index 15f5d365f69..83a7ec28c5c 100644 --- a/megamek/src/megamek/common/autoresolve/converter/EntityToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/EntityAsFormation.java @@ -31,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; } diff --git a/megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java similarity index 92% rename from megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java rename to megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java index a568ac63e88..5e821e54142 100644 --- a/megamek/src/megamek/common/autoresolve/converter/LanceToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/EntityAsUnit.java @@ -36,10 +36,10 @@ import static org.apache.commons.lang3.ObjectUtils.firstNonNull; -public class LanceToFormationConverter extends BaseFormationConverter { - private static final MMLogger logger = MMLogger.create(LanceToFormationConverter.class); +public class EntityAsUnit extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(EntityAsUnit.class); - public LanceToFormationConverter(Force force, SimulationContext game) { + public EntityAsUnit(Force force, SimulationContext game) { super(force, game, new Formation()); } @@ -49,6 +49,9 @@ public Formation convert() { var player = game.getPlayer(force.getOwnerId()); Counter counter = new Counter<>(); for (Force subforce : forces.getFullSubForces(force)) { + if (!subforce.getSubForces().isEmpty() || subforce.getEntities().isEmpty()) { + continue; + } for (ForceAssignable entity : forces.getFullEntities(subforce)) { var thisUnit = new ArrayList(); var unitName = "UNKNOWN"; diff --git a/megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java b/megamek/src/megamek/common/autoresolve/converter/FlattenForces.java similarity index 81% rename from megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java rename to megamek/src/megamek/common/autoresolve/converter/FlattenForces.java index a3038bac905..9d6a9ca611c 100644 --- a/megamek/src/megamek/common/autoresolve/converter/UseLancesAsFormations.java +++ b/megamek/src/megamek/common/autoresolve/converter/FlattenForces.java @@ -20,9 +20,8 @@ import megamek.common.force.Forces; import java.util.ArrayList; -import java.util.Map; -public class UseLancesAsFormations extends ForceConsolidation { +public class FlattenForces extends ForceConsolidation { @Override protected int getMaxEntitiesInSubForce() { @@ -60,14 +59,10 @@ private static int transformIntoTopLevelForce(Force force, Force subForce, Array var hasNoSubForce = subForce.subForceCount() == 0; var hasEntities = subForce.entityCount() > 0; if (hasNoSubForce && hasEntities) { - var name = force.getName() + " " + subForce.getName(); - if (force.getName().equals(subForce.getName())) { - name = subForce.getName(); - } - var topLevel = new Container(forceId++, name, team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); + var topLevel = new Container(forceId++, subForce.getName(), force.getName(), team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); topLevel.subs().add( - new Container(forceId++, name, team, force.getOwnerId(), new ArrayList<>(subForce.getEntities()), new ArrayList<>()) + new Container(forceId++, subForce.getName(), force.getName(), team, force.getOwnerId(), new ArrayList<>(subForce.getEntities()), new ArrayList<>()) ); newTopLevelForces.add(topLevel); } diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java index 2d4e27cf2cc..2f10b4570e4 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java +++ b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java @@ -35,7 +35,7 @@ public abstract class ForceConsolidation { protected abstract int getMaxEntitiesInSubForce(); protected abstract int getMaxEntitiesInTopLevelForce(); - public record Container(int uid, String name, int teamId, int playerId, List entities, List subs) { + 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(); } @@ -48,6 +48,7 @@ public boolean isTop() { public String toString() { return new StringJoiner(", ", Container.class.getSimpleName() + "[", "]") .add("uid=" + uid) + .add("breadcrumb='" + breadcrumb + "'") .add("name='" + name + "'") .add("teamId=" + teamId) .add("playerId=" + playerId) @@ -221,6 +222,7 @@ protected void createTopLevelForTeam(Map balancedForces, int Container leaf = new Container( maxId++, null, + null, team, -1, subListOfEntityIds.subList(start, subForceSize).stream().toList(), @@ -233,7 +235,7 @@ protected void createTopLevelForTeam(Map balancedForces, int break; } - Container top = new Container(maxId++, null, team, -1, new ArrayList<>(), subForces); + 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/UseCurrentForces.java b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java similarity index 81% rename from megamek/src/megamek/common/autoresolve/converter/UseCurrentForces.java rename to megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java index fe0012ec5d1..d56ed07ed35 100644 --- a/megamek/src/megamek/common/autoresolve/converter/UseCurrentForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java @@ -21,7 +21,7 @@ import java.util.*; -public class UseCurrentForces extends ForceConsolidation { +public class KeepCurrentForces extends ForceConsolidation { @Override protected int getMaxEntitiesInSubForce() { return -1; @@ -49,9 +49,17 @@ public void consolidateForces(IGame game) { if (force == null) { continue; } + var parentId = parents.getOrDefault(force.getId(), -1); + var parentNode = newForceMap.get(parentId); + var breadcrumb = ""; + var breadCrumbMaker = parentNode; + while (breadCrumbMaker != null) { + breadcrumb = parentNode.breadcrumb() + " > "; + breadCrumbMaker = newForceMap.get(parents.getOrDefault(breadCrumbMaker.uid(), -1)); + } var player = game.getPlayer(force.getOwnerId()); var team = player.getTeam(); - var container = new Container(forceId++, force.getName(), team, force.getOwnerId(), new ArrayList<>(), new ArrayList<>()); + 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()) { @@ -61,9 +69,7 @@ public void consolidateForces(IGame game) { container.entities().add(entityId); entityDuplicationChecker.add(entityId); } - var parentId = parents.get(force.getId()); - if (parentId != null) { - var parentNode = newForceMap.get(parentId); + if (parentNode != null) { parentNode.subs().add(container); } else { newTopLevelForces.add(container); diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java similarity index 93% rename from megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java rename to megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java index f589a0fe21c..d273795e67f 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceToFormationConverter.java +++ b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java @@ -13,7 +13,6 @@ */ package megamek.common.autoresolve.converter; -import megamek.client.ui.swing.calculationReport.FlexibleCalculationReport; import megamek.common.Entity; import megamek.common.ForceAssignable; import megamek.common.UnitRole; @@ -37,26 +36,26 @@ import static org.apache.commons.lang3.ObjectUtils.firstNonNull; -public class ForceToFormationConverter extends BaseFormationConverter { - private static final MMLogger logger = MMLogger.create(ForceToFormationConverter.class); +public class LowestForceAsUnit extends BaseFormationConverter { + private static final MMLogger logger = MMLogger.create(LowestForceAsUnit.class); - public ForceToFormationConverter(Force force, SimulationContext game) { + public LowestForceAsUnit(Force force, SimulationContext game) { super(force, game, new Formation()); } @Override public Formation convert() { - Counter counter = new Counter<>(); - 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; diff --git a/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java b/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java index 104b43ac3ae..7fe12599031 100644 --- a/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/MMSetupForces.java @@ -69,11 +69,11 @@ public FailedToConvertForceToFormationException(Throwable cause) { */ private void convertForcesIntoFormations(SimulationContext simulationContext) { if (!simulationContext.getForces().getAllForces().isEmpty()) { - new UseCurrentForces().consolidateForces(simulationContext); + 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 ForceToFormationConverter(force, simulationContext).convert(); + var formation = new LowestForceAsUnit(force, simulationContext).convert(); formation.setTargetFormationId(Entity.NONE); simulationContext.addUnit(formation); } catch (Exception e) { @@ -87,7 +87,7 @@ private void convertForcesIntoFormations(SimulationContext simulationContext) { for (var inGameObject : simulationContext.getInGameObjects()) { try { if (inGameObject instanceof Entity entity) { - var formation = new EntityToFormationConverter(entity, simulationContext).convert(); + var formation = new EntityAsFormation(entity, simulationContext).convert(); formation.setTargetFormationId(Entity.NONE); simulationContext.addUnit(formation); } 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 f56ff412ece..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; 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 bda85dce9ea..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; From e1a57a3a428d0a0689e636f5869ef33d2b91cd19 Mon Sep 17 00:00:00 2001 From: Scoppio Date: Tue, 14 Jan 2025 18:32:18 -0300 Subject: [PATCH 3/5] fix: conflict fixed --- .../converter/ForceConsolidation.java | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java index 11ab9c35a4e..2f10b4570e4 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java +++ b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java @@ -20,7 +20,6 @@ import megamek.common.force.Force; import megamek.common.force.Forces; import megamek.common.icons.Camouflage; - import megamek.logging.MMLogger; import java.util.*; @@ -37,57 +36,6 @@ public abstract class ForceConsolidation { protected abstract int getMaxEntitiesInTopLevelForce(); public record Container(int uid, String name, String breadcrumb, int teamId, int playerId, List entities, List subs) { - List forceRepresentation = getForceRepresentations(forces, teamByPlayer); - var balancedConsolidateForces = balanceForces(forceRepresentation); - - clearAllForces(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); - } - } - } - } - - private void clearAllForces(Forces forces) { - // Remove all empty forces and sub forces after consolidation - forces.deleteForces(forces.getAllForces()); - } - - /** - * Converts the forces into a list of ForceRepresentations. It is an intermediary representation of a force, in a way that makes it very - * lightweight to manipulate and balance. It only contains the representation of the force top-level, and the list of entities in it. - * @param forces The forces to convert - * @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<>(); - 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])); - } - return forceRepresentations; - } - - public record Container(int uid, int teamId, int[] entities, Container[] subs) { public boolean isLeaf() { return subs.isEmpty() && !entities.isEmpty(); } From d93be8f939fcae4c0cf4f07748d718ec095f10de Mon Sep 17 00:00:00 2001 From: Scoppio Date: Tue, 14 Jan 2025 20:52:06 -0300 Subject: [PATCH 4/5] fix: multiple small fixes for merges, reports and nullpointers --- megamek/data/css/acarCssFile.css | 11 ++++ .../ui/dialogs/AutoResolveProgressDialog.java | 14 ++-- megamek/src/megamek/common/BattleArmor.java | 11 ++-- .../autoresolve/acar/SimulationContext.java | 3 +- .../handler/StandardUnitAttackHandler.java | 7 +- .../common/autoresolve/acar/order/Order.java | 5 ++ .../common/autoresolve/acar/order/Orders.java | 7 ++ .../autoresolve/acar/phase/MovementPhase.java | 65 ++----------------- .../acar/report/HtmlGameLogger.java | 36 ++++++---- .../report/RecoveringNerveActionReporter.java | 11 ++-- .../autoresolve/component/Formation.java | 5 ++ .../converter/KeepCurrentForces.java | 14 ++-- .../converter/LowestForceAsUnit.java | 34 ---------- 13 files changed, 88 insertions(+), 135 deletions(-) create mode 100644 megamek/data/css/acarCssFile.css 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 @@ + @@ -70,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/RecoveringNerveActionReporter.java b/megamek/src/megamek/common/autoresolve/acar/report/RecoveringNerveActionReporter.java index 43f651a2585..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; @@ -45,17 +42,19 @@ public static IRecoveringNerveActionReporter create(SimulationManager manager) { public void reportRecoveringNerveStart(Formation formation, int toHitValue) { reportConsumer.accept(new PublicReportEntry("acar.morale.recoveryAttempt") .add(new FormationReportEntry(formation, game).reportText()) - .add(toHitValue).noNL() + .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/component/Formation.java b/megamek/src/megamek/common/autoresolve/component/Formation.java index 0da01e16986..7fabfb86501 100644 --- a/megamek/src/megamek/common/autoresolve/component/Formation.java +++ b/megamek/src/megamek/common/autoresolve/component/Formation.java @@ -238,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/KeepCurrentForces.java b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java index d56ed07ed35..e1a4f36ee07 100644 --- a/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java @@ -36,10 +36,8 @@ private record Node(int parent, Force force) {} @Override public void consolidateForces(IGame game) { - var newTopLevelForces = new ArrayList(); var forcesInternalRepresentation = game.getForces().getForcesInternalRepresentation(); - var parents = new HashMap(); Deque queue = new ArrayDeque<>(game.getForces().getTopLevelForces()); int forceId = 0; var newForceMap = new HashMap(); @@ -49,13 +47,12 @@ public void consolidateForces(IGame game) { if (force == null) { continue; } - var parentId = parents.getOrDefault(force.getId(), -1); - var parentNode = newForceMap.get(parentId); + var parentForce = forcesInternalRepresentation.get(force.getParentId()); + var parentNode = parentForce == null ? null : newForceMap.get(parentForce.getId()); var breadcrumb = ""; - var breadCrumbMaker = parentNode; - while (breadCrumbMaker != null) { - breadcrumb = parentNode.breadcrumb() + " > "; - breadCrumbMaker = newForceMap.get(parents.getOrDefault(breadCrumbMaker.uid(), -1)); + while (parentForce != null) { + breadcrumb = parentForce.getName() + " > "; + parentForce = forcesInternalRepresentation.get(parentForce.getParentId()); } var player = game.getPlayer(force.getOwnerId()); var team = player.getTeam(); @@ -77,7 +74,6 @@ public void consolidateForces(IGame game) { for (var subForceId : force.getSubForces()) { var subForce = forcesInternalRepresentation.get(subForceId); - parents.put(subForceId, force.getId()); queue.add(subForce); } } diff --git a/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java index d273795e67f..bc0835640b9 100644 --- a/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java +++ b/megamek/src/megamek/common/autoresolve/converter/LowestForceAsUnit.java @@ -79,42 +79,8 @@ public Formation convert() { formation.setName(force.getName()); calcSbfFormationStats(); formation.setRole(firstNonNull(counter.top(), Role.getRole(UnitRole.SKIRMISHER))); - formation.setStdDamage(setStdDamageForFormation(formation)); -// for (var unit : formation.getUnits()) { -// var health = 0; -// for (var element : unit.getElements()) { -// health += element.getCurrentArmor() + element.getCurrentStructure(); -// } -// unit.setArmor(health); -// unit.setCurrentArmor(health); -// } formation.setStartingSize(formation.currentSize()); return formation; } - private ASDamageVector setStdDamageForFormation(Formation formation) { - // Get the list of damage objects from the units in the formation - var damages = formation.getUnits().stream().map(SBFUnit::getDamage).toList(); - var size = damages.size(); - - // Initialize accumulators for the different damage types - var l = 0; - var m = 0; - var s = 0; - - // Sum up the damage values for each type - for (var damage : damages) { - l += damage.getDamage(ASRange.LONG).damage; - m += damage.getDamage(ASRange.MEDIUM).damage; - s += damage.getDamage(ASRange.SHORT).damage; - } - return new ASDamageVector( - new ASDamage(Math.ceil((double) s / size)), - new ASDamage(Math.ceil((double) m / size)), - new ASDamage(Math.ceil((double) l / size)), - null, - size, - true); - } - } From 4c56576c4e98aa6438de7efe31507e0dd771c99c Mon Sep 17 00:00:00 2001 From: Scoppio Date: Tue, 14 Jan 2025 21:46:32 -0300 Subject: [PATCH 5/5] feat: added cycle finder to stop infinite loops while recreating the forces --- .../converter/ForceConsolidation.java | 25 +++++++++++++++++++ .../converter/KeepCurrentForces.java | 7 ++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java index 2f10b4570e4..b48d7505440 100644 --- a/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java +++ b/megamek/src/megamek/common/autoresolve/converter/ForceConsolidation.java @@ -68,6 +68,31 @@ public boolean isTop() { } } + /** + * 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. diff --git a/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java index e1a4f36ee07..52e71ffbc6c 100644 --- a/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java +++ b/megamek/src/megamek/common/autoresolve/converter/KeepCurrentForces.java @@ -32,12 +32,15 @@ protected int getMaxEntitiesInTopLevelForce() { return -1; } - private record Node(int parent, Force force) {} - @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();