diff --git a/MekHQ/data/images/misc/acar_splash_hd.png b/MekHQ/data/images/misc/acar_splash_hd.png new file mode 100644 index 0000000000..2f73610390 Binary files /dev/null and b/MekHQ/data/images/misc/acar_splash_hd.png differ diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java index 19d9eb3e60..9c56ef7a99 100644 --- a/MekHQ/src/mekhq/MekHQ.java +++ b/MekHQ/src/mekhq/MekHQ.java @@ -27,6 +27,8 @@ import megamek.SuiteConstants; import megamek.client.Client; import megamek.client.bot.princess.BehaviorSettings; +import megamek.client.ui.dialogs.AutoResolveChanceDialog; +import megamek.client.ui.dialogs.AutoResolveProgressDialog; import megamek.client.ui.dialogs.AutoResolveSimulationLogDialog; import megamek.client.ui.preferences.PreferencesNode; import megamek.client.ui.preferences.SuitePreferences; @@ -37,6 +39,7 @@ import megamek.common.Board; import megamek.common.annotations.Nullable; import megamek.common.autoresolve.acar.SimulatedClient; +import megamek.common.autoresolve.converter.SingletonForces; import megamek.common.autoresolve.event.AutoResolveConcludedEvent; import megamek.common.event.*; import megamek.common.net.marshalling.SanityInputFilter; @@ -645,25 +648,25 @@ public void startAutoResolve(AtBScenario scenario, List units) { this.autosaveService.requestBeforeMissionAutosave(getCampaign()); if (getCampaign().getCampaignOptions().isAutoResolveVictoryChanceEnabled()) { - var proceed = megamek.client.ui.dialogs.AutoResolveChanceDialog + var proceed = AutoResolveChanceDialog .showDialog( - getCampaigngui().getFrame(), + campaignGUI.getFrame(), getCampaign().getCampaignOptions().getAutoResolveNumberOfScenarios(), Runtime.getRuntime().availableProcessors(), - getCampaign().getPlayer().getTeam(), - new AtBSetupForces(getCampaign(), units, scenario), + 1, + new AtBSetupForces(getCampaign(), units, scenario, new SingletonForces()), new Board(scenario.getBaseMapX(), scenario.getBaseMapY())) == JOptionPane.YES_OPTION; if (!proceed) { return; } } - var event = megamek.client.ui.dialogs.AutoResolveProgressDialog.showDialog( - getCampaigngui().getFrame(), - new AtBSetupForces(getCampaign(), units, scenario), + var event = AutoResolveProgressDialog.showDialog( + campaignGUI.getFrame(), + new AtBSetupForces(getCampaign(), units, scenario, new SingletonForces()), new Board(scenario.getBaseMapX(), scenario.getBaseMapY())); - var autoResolveBattleReport = new AutoResolveSimulationLogDialog(getCampaigngui().getFrame(), event.getLogFile()); + var autoResolveBattleReport = new AutoResolveSimulationLogDialog(campaignGUI.getFrame(), event.getLogFile()); autoResolveBattleReport.setModal(true); autoResolveBattleReport.setVisible(true); diff --git a/MekHQ/src/mekhq/campaign/CampaignFactory.java b/MekHQ/src/mekhq/campaign/CampaignFactory.java index 294024bc3a..59f0afacc5 100644 --- a/MekHQ/src/mekhq/campaign/CampaignFactory.java +++ b/MekHQ/src/mekhq/campaign/CampaignFactory.java @@ -79,7 +79,6 @@ public Campaign createCampaign(InputStream is) // ...otherwise, assume we're an XML file. CampaignXmlParser parser = new CampaignXmlParser(is, this.app); - return parser.parse(); } @@ -91,4 +90,5 @@ private byte[] readHeader(InputStream is) throws IOException { return header; } + } diff --git a/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java b/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java index e0b5bc4938..aee7980a77 100644 --- a/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java +++ b/MekHQ/src/mekhq/campaign/autoresolve/AtBSetupForces.java @@ -19,10 +19,7 @@ import megamek.common.*; import megamek.common.alphaStrike.conversion.ASConverter; import megamek.common.autoresolve.acar.SimulationContext; -import megamek.common.autoresolve.converter.ConsolidateForces; -import megamek.common.autoresolve.converter.ForceToFormationConverter; -import megamek.common.autoresolve.converter.SetupForces; -import megamek.common.autoresolve.converter.SingleElementConsolidateForces; +import megamek.common.autoresolve.converter.*; import megamek.common.copy.CrewRefBreak; import megamek.common.force.Forces; import megamek.common.options.OptionsConstants; @@ -41,6 +38,8 @@ import java.io.InputStream; import java.util.*; +import static megamek.common.force.Force.NO_FORCE; + /** * @author Luana Coppio */ @@ -50,11 +49,20 @@ public class AtBSetupForces extends SetupForces { private final Campaign campaign; private final List units; private final AtBScenario scenario; + private final ForceConsolidation forceConsolidationMethod; + + private final OrderFactory orderFactory; + + public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario, ForceConsolidation forceConsolidationMethod) { + this(campaign, units, scenario, forceConsolidationMethod, new OrderFactory(campaign, scenario)); + } - public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario) { + public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario, ForceConsolidation forceConsolidationMethod, OrderFactory orderFactory) { this.campaign = campaign; this.units = units; this.scenario = scenario; + this.forceConsolidationMethod = forceConsolidationMethod; + this.orderFactory = orderFactory; } /** @@ -64,10 +72,18 @@ public AtBSetupForces(Campaign campaign, List units, AtBScenario scenario) public void createForcesOnSimulation(SimulationContext game) { setupPlayer(game); setupBots(game); - ConsolidateForces.consolidateForces(game, new SingleElementConsolidateForces()); + forceConsolidationMethod.consolidateForces(game); convertForcesIntoFormations(game); } + @Override + public void addOrdersToForces(SimulationContext context) { + var orders = orderFactory.getOrders(); + context.getOrders().clear(); + context.getOrders().addAll(orders); + context.getOrders().resetOrders(); + } + private static class FailedToConvertForceToFormationException extends RuntimeException { public FailedToConvertForceToFormationException(Throwable cause) { super(cause); @@ -80,10 +96,10 @@ public FailedToConvertForceToFormationException(Throwable cause) { * and used in the auto resolve in place of the original entities * @param game The game object to convert the forces in */ - private static void convertForcesIntoFormations(SimulationContext game) { + private void convertForcesIntoFormations(SimulationContext game) { for(var force : game.getForces().getTopLevelForces()) { try { - var formation = new ForceToFormationConverter(force, game).convert(); + var formation = new EntityAsUnit(force, game).convert(); formation.setTargetFormationId(Entity.NONE); formation.setOwnerId(force.getOwnerId()); game.addUnit(formation); @@ -413,33 +429,14 @@ private PlanetaryConditions getPlanetaryConditions() { private void sendEntities(List entities, SimulationContext game) { Map forceMapping = new HashMap<>(); for (final Entity entity : new ArrayList<>(entities)) { - if (entity instanceof ProtoMek) { - int numPlayerProtos = game.getSelectedEntityCount(new EntitySelector() { - private final int ownerId = entity.getOwnerId(); - @Override - public boolean accept(Entity entity) { - return (entity instanceof ProtoMek) && (ownerId == entity.getOwnerId()); - } - }); - - entity.setUnitNumber((short) (numPlayerProtos / 5)); - } - - if (Entity.NONE == entity.getId()) { - entity.setId(game.getNextEntityId()); - } - - // Give the unit a spotlight, if it has the spotlight quirk - entity.setExternalSearchlight(entity.hasExternalSearchlight() - || entity.hasQuirk(OptionsConstants.QUIRK_POS_SEARCHLIGHT)); - + lastTouchesBeforeSendingEntity(game, entity); game.getPlayer(entity.getOwnerId()).changeInitialEntityCount(1); // Restore forces from MULs or other external sources from the forceString, if // any if (!entity.getForceString().isBlank()) { List forceList = Forces.parseForceString(entity); - int realId = megamek.common.force.Force.NO_FORCE; + int realId = NO_FORCE; boolean topLevel = true; for (megamek.common.force.Force force : forceList) { @@ -463,4 +460,26 @@ public boolean accept(Entity entity) { } } } + + private static void lastTouchesBeforeSendingEntity(SimulationContext game, Entity entity) { + if (entity instanceof ProtoMek) { + int numPlayerProtos = game.getSelectedEntityCount(new EntitySelector() { + private final int ownerId = entity.getOwnerId(); + @Override + public boolean accept(Entity entity) { + return (entity instanceof ProtoMek) && (ownerId == entity.getOwnerId()); + } + }); + + entity.setUnitNumber((short) (numPlayerProtos / 5)); + } + + if (Entity.NONE == entity.getId()) { + entity.setId(game.getNextEntityId()); + } + + // Give the unit a spotlight, if it has the spotlight quirk + entity.setExternalSearchlight(entity.hasExternalSearchlight() + || entity.hasQuirk(OptionsConstants.QUIRK_POS_SEARCHLIGHT)); + } } diff --git a/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java b/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java new file mode 100644 index 0000000000..b1b3e005d8 --- /dev/null +++ b/MekHQ/src/mekhq/campaign/autoresolve/OrderFactory.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2024-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 mekhq.campaign.autoresolve; + +import megamek.common.Entity; +import megamek.common.OffBoardDirection; +import megamek.common.autoresolve.acar.order.Condition; +import megamek.common.autoresolve.acar.order.Order; +import megamek.common.autoresolve.acar.order.OrderType; +import megamek.common.autoresolve.acar.order.Orders; +import megamek.logging.MMLogger; +import mekhq.campaign.Campaign; +import mekhq.campaign.mission.Scenario; +import mekhq.campaign.mission.ScenarioObjective; + +import java.util.Set; + +/** + * @author Luana Coppio + */ +public class OrderFactory { + private static final MMLogger logger = MMLogger.create(OrderFactory.class); + + private final Campaign campaign; + private final Scenario scenario; + private Orders orders; + + public OrderFactory(Campaign campaign, Scenario scenario) { + this.campaign = campaign; + this.scenario = scenario; + } + + public Orders getOrders() { + this.orders = new Orders(); + var scenarioObjectives = scenario.getScenarioObjectives(); + for (var objective : scenarioObjectives) { + createOrderFromObjective(campaign.getPlayer().getId(), objective); + } + return orders; + } + + private void addOrder(Order order) { + logger.info("Adding order {}", order); + this.orders.add(order); + } + + private void createOrderFromObjective(int ownerId, ScenarioObjective objective) { + + switch (objective.getObjectiveCriterion()) { + case Capture: // no capture, only destroy! + case Destroy: + createDestroyOrder(ownerId, objective); + break; + case Preserve: + createPreserveOrder(ownerId, objective); + break; + case ReachMapEdge: + createReachMapEdgeOrder(ownerId, objective); + break; + case ForceWithdraw: + createForceWithdrawOrder(ownerId, objective); + break; + case PreventReachMapEdge: + createPreventReachMapEdgeOrder(ownerId, objective); + break; + case Custom: + default: + // do nothing for custom bc what can be done? + break; + }; + } + // Make the enemy withdraw + private void createForceWithdrawOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_NOT_WITHDRAWING) + .withCondition(Condition.alwaysTrue()); + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) { + orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit()); + } + addOrder(orderBuilder.build()); + } + + private void createReachMapEdgeOrder(int ownerId, ScenarioObjective objective) { + if (objective.getDestinationEdge() != null && objective.getDestinationEdge() != OffBoardDirection.NONE) { + var northSide = Set.of(OffBoardDirection.NORTH, OffBoardDirection.EAST); + var orderType = northSide.contains(objective.getDestinationEdge()) ? OrderType.FLEE_NORTH : OrderType.FLEE_SOUTH; + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, orderType) + .withCondition(Condition.alwaysTrue()); + orders.add(orderBuilder.build()); + } else { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, scenario.getStartingPos() > 4 ? OrderType.FLEE_NORTH : OrderType.FLEE_SOUTH) + .withCondition(Condition.alwaysTrue()); + addOrder(orderBuilder.build()); + } + } + + private void createPreventReachMapEdgeOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET_WITHDRAWING) + .withCondition(Condition.alwaysTrue()); + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None) { + orderBuilder.withCondition(context -> context.getCurrentRound() <= objective.getTimeLimit()); + } + addOrder(orderBuilder.build()); + } + + private void createPreserveOrder(int ownerId, ScenarioObjective objective) { + if (!objective.getAssociatedForceNames().isEmpty()) { + var order = createOrderForPreserveAssociatedForces(ownerId, objective); + addOrder(order); + } + if (!objective.getAssociatedUnitIDs().isEmpty()) { + var order = createOrderForPreserveAssociatedUnits(ownerId, objective); + addOrder(order); + } + } + + private Order createOrderForPreserveAssociatedForces(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET) + .withCondition(context -> { + var attackTypeOrders = Set.of(OrderType.ATTACK_TARGET, OrderType.ATTACK_TARGET_NOT_WITHDRAWING, OrderType.ATTACK_TARGET_WITHDRAWING); + var hasOutstandingAttackOrders = context.getOrders().getOrders(ownerId).stream() + .filter(o -> attackTypeOrders.contains(o.getOrderType())).anyMatch(order -> order.isEligible(context)); + int totalUnits = 0; + var currentUnits = 0; + for (var forceName : objective.getAssociatedForceNames()) { + for (var force : context.getForces().getTopLevelForces()) { + if (force.getName().equals(forceName)) { + totalUnits += force.getEntities().size(); + for (var entityId : force.getEntities()) { + currentUnits += context.getSelectedEntityCount(entity -> entity.getId() == entityId); + } + } + } + } + + if (totalUnits == 0) { + return true; + } else { + + var timeLimit = false; + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None && objective.isTimeLimitAtMost()) { + timeLimit = context.getCurrentRound() >= objective.getTimeLimit(); + } + if (!hasOutstandingAttackOrders) { + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return currentUnits < objective.getFixedAmount() || timeLimit; + } else { + int percent = (int) ((totalUnits / (double) currentUnits) * 100); + return percent < objective.getPercentage() || timeLimit; + } + } else { + if (timeLimit) { + return context.getCurrentRound() >= objective.getTimeLimit(); + } else { + return true; + } + } + } + }); + + return orderBuilder.build(); + } + + private Order createOrderForPreserveAssociatedUnits(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.WITHDRAW_IF_CONDITION_IS_MET) + .withCondition(context -> { + var inGameObjects = context.getInGameObjects(); + var unitIds = objective.getAssociatedUnitIDs(); + var entitiesOfInterest = inGameObjects.stream().filter(Entity.class::isInstance).map(Entity.class::cast) + .filter(e -> e.getOwnerId() == ownerId).toList(); + var currentUnits = 0; + var totalUnits = unitIds.size(); + if (totalUnits == 0) { + return true; + } + for (var unit : entitiesOfInterest) { + if (unitIds.contains(unit.getExternalIdAsString())) { + currentUnits++; + } + } + var timeLimit = false; + if (objective.getTimeLimitType() != ScenarioObjective.TimeLimitType.None && objective.isTimeLimitAtMost()) { + timeLimit = context.getCurrentRound() >= objective.getTimeLimit(); + } + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return currentUnits < objective.getFixedAmount() || timeLimit; + } else { + int percent = (int) ((totalUnits / (double) currentUnits) * 100); + return percent < objective.getPercentage() || timeLimit; + } + }); + return orderBuilder.build(); + } + + private void createDestroyOrder(int ownerId, ScenarioObjective objective) { + var orderBuilder = Order.OrderBuilder.anOrder(ownerId, OrderType.ATTACK_TARGET); + if (objective.getPercentage() == 100) { + orderBuilder.withCondition(Condition.alwaysTrue()); + } else { + orderBuilder.withCondition(context -> { + var enemyPlayers = context.getPlayersList().stream().filter(p -> p.isEnemyOf(campaign.getPlayer())).toList(); + var totalUnits = 0; + var currentUnits = 0; + for (var player: enemyPlayers) { + totalUnits += context.getStartingNumberOfUnits(player.getId()); + currentUnits += context.getActiveFormations(player).size(); + } + + if (totalUnits == 0) { + return true; + } + if (objective.getAmountType().equals(ScenarioObjective.ObjectiveAmountType.Fixed)) { + return (totalUnits - currentUnits) < objective.getFixedAmount(); + } else { + var currentPercent = 100 - (int) (currentUnits / (double) totalUnits); + return (objective.getPercentage() - currentPercent) > 0; + } + }); + } + addOrder(orderBuilder.build()); + } +} diff --git a/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java b/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java index 5933f0484a..3aabde9a8d 100644 --- a/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java +++ b/MekHQ/unittests/mekhq/campaign/autoresolve/ResolverTest.java @@ -23,6 +23,7 @@ import megamek.common.*; import megamek.common.autoresolve.Resolver; import megamek.common.autoresolve.acar.SimulationOptions; +import megamek.common.autoresolve.converter.FlattenForces; import megamek.common.autoresolve.event.AutoResolveConcludedEvent; import megamek.common.enums.Gender; import megamek.common.enums.SkillLevel; @@ -48,7 +49,6 @@ import org.junit.jupiter.api.*; import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -191,16 +191,14 @@ AtBScenario createScenario(Campaign campaign) { Campaign createCampaign() { var campaign = new Campaign(); - + campaign.setName("Test Player"); var reputationController = mock(ReputationController.class); when(reputationController.getAverageSkillLevel()).thenReturn(SkillLevel.REGULAR); - campaign.setReputation(reputationController); - var force = new Force("Heroes"); - campaign.addForce(force, campaign.getForce(0)); + campaign.addForce(force, campaign.getForce(0)); return campaign; } @@ -354,7 +352,7 @@ void autoResolve(Consumer autoResolveConcludedEvent) when(botForce.getTeam()).thenReturn(2); when(botForce.getFullEntityList(any())).thenReturn(entities); - var resolver = Resolver.simulationRun(new AtBSetupForces(campaign, units, scenario), SimulationOptions.empty(), new Board(30, 30)); + var resolver = Resolver.simulationRun(new AtBSetupForces(campaign, units, scenario, new FlattenForces()), SimulationOptions.empty(), new Board(30, 30)); autoResolveConcludedEvent.accept(resolver.resolveSimulation()); }