Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Strategic Formations #5268

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 50 additions & 33 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
import java.util.Map.Entry;
import java.util.stream.Collectors;

import static mekhq.campaign.force.StrategicFormation.recalculateStrategicFormations;
import static mekhq.campaign.market.contractMarket.ContractAutomation.performAutomatedActivation;
import static mekhq.campaign.personnel.backgrounds.BackgroundsController.randomMercenaryCompanyNameGenerator;
import static mekhq.campaign.personnel.education.EducationController.getAcademy;
Expand Down Expand Up @@ -201,7 +202,7 @@ public class Campaign implements ITechManager {

// hierarchically structured Force object to define TO&E
private Force forces;
private final Hashtable<Integer, StrategicFormation> strategicFormations; // AtB
private Hashtable<Integer, StrategicFormation> strategicFormations; // AtB

private Faction faction;
private int techFactionCode;
Expand Down Expand Up @@ -271,7 +272,7 @@ public class Campaign implements ITechManager {
private final CampaignSummary campaignSummary;
private final Quartermaster quartermaster;
private StoryArc storyArc;
private FameAndInfamyController fameAndInfamy;
private final FameAndInfamyController fameAndInfamy;
private BehaviorSettings autoResolveBehaviorSettings;
private List<Unit> automatedMothballUnits;

Expand Down Expand Up @@ -454,15 +455,45 @@ public List<Force> getAllForces() {
return new ArrayList<>(forceIds.values());
}

public void importLance(StrategicFormation l) {
strategicFormations.put(l.getForceId(), l);
public void importStrategicFormation(StrategicFormation strategicFormation) {
strategicFormations.put(strategicFormation.getForceId(), strategicFormation);
}

public void setStrategicFormations(final Hashtable<Integer, StrategicFormation> strategicFormations) {
this.strategicFormations = strategicFormations;
}

public Hashtable<Integer, StrategicFormation> getStrategicFormations() {
// Here we sanitize the list, ensuring ineligible formations have been removed before
// returning the hashtable. In theory, this shouldn't be necessary, however, having this
// sanitizing step should remove the need for isEligible() checks whenever we fetch the
// hashtable.
List<Integer> formationsToSanitize = new ArrayList<>();
for (StrategicFormation strategicFormation : strategicFormations.values()) {
if (!strategicFormation.isEligible(this)) {
formationsToSanitize.add(strategicFormation.getForceId());
try {
Force force = getForce(strategicFormation.getForceId());
force.setStrategicFormation(false);
} catch (Exception ex) {
// We're not too worried if we can't find the associated Force,
// as this just means it has been deleted at some point and not removed correctly.
}
}
}

for (int id : formationsToSanitize) {
strategicFormations.remove(id);
}

return strategicFormations;
}

public ArrayList<StrategicFormation> getStrategicFormationList() {
// This call allows us to utilize the self-sanitizing feature of getStrategicFormations(),
// without needing to directly include the code here, too.
strategicFormations = getStrategicFormations();

return strategicFormations.values().stream()
.filter(l -> forceIds.containsKey(l.getForceId()))
.collect(Collectors.toCollection(ArrayList::new));
Expand Down Expand Up @@ -909,13 +940,11 @@ public void addForce(Force force, Force superForce) {
forceIds.put(id, force);
lastForceId = id;

if (campaignOptions.isUseAtB() && !force.getUnits().isEmpty()) {
if (null == strategicFormations.get(id)) {
strategicFormations.put(id, new StrategicFormation(force.getId(), this));
}
}

force.updateCommander(this);

if (campaignOptions.isUseAtB()) {
recalculateStrategicFormations(this);
}
}

public void moveForce(Force force, Force superForce) {
Expand Down Expand Up @@ -1008,25 +1037,18 @@ public void addUnitToForce(@Nullable Unit u, int id) {
}

if (campaignOptions.isUseAtB()) {
if ((null != prevForce) && prevForce.getUnits().isEmpty()) {
strategicFormations.remove(prevForce.getId());
}

if ((null == strategicFormations.get(id)) && (null != force)) {
strategicFormations.put(id, new StrategicFormation(force.getId(), this));
}
recalculateStrategicFormations(this);
}
}

/**
* Adds force and all its subforces to the AtB lance table
*/
private void addAllLances(Force force) {
if (force.isStrategicFormation()) {
strategicFormations.put(force.getId(), new StrategicFormation(force.getId(), this));
}
for (Force f : force.getSubForces()) {
addAllLances(f);
private void addAllStrategicFormations(Force force) {
recalculateStrategicFormations(this);

for (Force subForce : force.getSubForces()) {
addAllStrategicFormations(subForce);
}
}

Expand Down Expand Up @@ -4318,6 +4340,7 @@ public void processNewDayUnits() {
private void processNewDayForces() {
// update formation levels
Force.populateFormationLevelsFromOrigin(this);
recalculateStrategicFormations(this);

// Update the force icons based on the end-of-day unit status if desired
if (MekHQ.getMHQOptions().getNewDayForceIconOperationalStatus()) {
Expand Down Expand Up @@ -4989,17 +5012,13 @@ public void removeForce(Force force) {
}
}
}
MekHQ.triggerEvent(new OrganizationChangedEvent(this, force));

// also remove this force's id from any scenarios
if (force.isDeployed()) {
Scenario s = getScenario(force.getScenarioId());
s.removeForce(fid);
}

if (campaignOptions.isUseAtB()) {
strategicFormations.remove(fid);
}

if (null != force.getParentForce()) {
force.getParentForce().removeSubForce(fid);
}
Expand All @@ -5013,10 +5032,8 @@ public void removeForce(Force force) {
}
}

ArrayList<Force> subs = new ArrayList<>(force.getSubForces());
for (Force sub : subs) {
removeForce(sub);
MekHQ.triggerEvent(new OrganizationChangedEvent(this, sub));
if (campaignOptions.isUseAtB()) {
recalculateStrategicFormations(this);
}
}

Expand Down Expand Up @@ -8305,7 +8322,7 @@ public void initAtB(boolean newCampaign) {
}
}

addAllLances(this.forces);
addAllStrategicFormations(this.forces);

// Determine whether or not there is an active contract
setHasActiveContract();
Expand Down
20 changes: 9 additions & 11 deletions MekHQ/src/mekhq/campaign/force/Force.java
Original file line number Diff line number Diff line change
Expand Up @@ -389,15 +389,17 @@ public Vector<UUID> getUnits() {
*/
public Vector<UUID> getAllUnits(boolean combatForcesOnly) {
Vector<UUID> allUnits;

if (combatForcesOnly && !isCombatForce()) {
allUnits = new Vector<>();
} else {
allUnits = new Vector<>(units);
}

for (Force f : subForces) {
allUnits.addAll(f.getAllUnits(combatForcesOnly));
for (Force force : subForces) {
allUnits.addAll(force.getAllUnits(combatForcesOnly));
}

return allUnits;
}

Expand Down Expand Up @@ -841,11 +843,7 @@ public int hashCode() {
public int getTotalBV(Campaign campaign, boolean forceStandardBattleValue) {
int bvTotal = 0;

for (Force subforce : getSubForces()) {
bvTotal += subforce.getTotalBV(campaign, forceStandardBattleValue);
}

for (UUID unitId : getUnits()) {
for (UUID unitId : getAllUnits(false)) {
// no idea how this would happen, but sometimes a unit in a forces unit ID list
// has an invalid ID?
if (campaign.getUnit(unitId) == null) {
Expand Down Expand Up @@ -904,16 +902,16 @@ public int getTotalUnitCount(Campaign campaign, boolean isClanBidding) {
* Calculates the unit type most represented in this force
* and all subforces.
*
* @param c Working campaign
* @param campaign Working campaign
* @return Majority unit type.
*/
public int getPrimaryUnitType(Campaign c) {
public int getPrimaryUnitType(Campaign campaign) {
Map<Integer, Integer> unitTypeBuckets = new TreeMap<>();
int biggestBucketID = -1;
int biggestBucketCount = 0;

for (UUID id : getUnits()) {
int unitType = c.getUnit(id).getEntity().getUnitType();
for (UUID id : getAllUnits(false)) {
int unitType = campaign.getUnit(id).getEntity().getUnitType();

unitTypeBuckets.merge(unitType, 1, Integer::sum);

Expand Down
76 changes: 73 additions & 3 deletions MekHQ/src/mekhq/campaign/force/StrategicFormation.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Lance.java
*
* Copyright (c) 2011 - Carl Spain. All rights reserved.
* Copyright (c) 2020 - The MegaMek Team. All Rights Reserved.
* Copyright (c) 2020-2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MekHQ.
*
Expand All @@ -23,7 +23,9 @@

import megamek.common.*;
import megamek.logging.MMLogger;
import mekhq.MekHQ;
import mekhq.campaign.Campaign;
import mekhq.campaign.event.OrganizationChangedEvent;
import mekhq.campaign.mission.AtBContract;
import mekhq.campaign.mission.AtBScenario;
import mekhq.campaign.mission.atb.AtBScenarioFactory;
Expand All @@ -39,6 +41,7 @@

import java.io.PrintWriter;
import java.time.LocalDate;
import java.util.Hashtable;
import java.util.List;
import java.util.UUID;

Expand Down Expand Up @@ -263,8 +266,7 @@ public boolean isEligible(Campaign campaign) {
}

/*
* Check that the number of units and weight are within the limits
* and that the force contains at least one ground unit.
* Check that the number of units and weight are within the limits.
*/
if (campaign.getCampaignOptions().isLimitLanceNumUnits()) {
int size = getSize(campaign);
Expand Down Expand Up @@ -323,6 +325,15 @@ size > getStdLanceSize(campaign.getFaction()) + 2) {
return false;
}
}

List<Force> parentForces = force.getAllParents();

for (Force parentForce : parentForces) {
if (parentForce.isStrategicFormation()) {
force.setStrategicFormation(false);
return false;
}
}
}

force.setStrategicFormation(hasGround);
Expand Down Expand Up @@ -600,4 +611,63 @@ public static double calculateTotalWeight(Campaign campaign, int forceId) {

return weight;
}

/**
* This static method updates the strategic formations across the campaign.
* It starts at the top level force, and calculates the strategic formations for each sub-force.
* It keeps only the eligible strategic formations and imports them into the campaign.
* After every formation is processed, an 'OrganizationChangedEvent' is triggered by that force.
*
* @param campaign the current campaign.
*/
public static void recalculateStrategicFormations(Campaign campaign) {
IllianiCBT marked this conversation as resolved.
Show resolved Hide resolved
campaign.setStrategicFormations(new Hashtable<>());
for (Force force : campaign.getAllForces()) {
force.setStrategicFormation(false);
}

Force originNode = campaign.getForce(0);

StrategicFormation strategicFormation = new StrategicFormation(0, campaign);
boolean isEligible = strategicFormation.isEligible(campaign);

if (isEligible) {
originNode.setStrategicFormation(true);
campaign.importStrategicFormation(strategicFormation);
}

MekHQ.triggerEvent(new OrganizationChangedEvent(originNode));

recalculateSubForceStrategicStatus(campaign, originNode);
}

/**
* This method is used to update the strategic formations for the campaign working downwards
* from a specified node, through all of its sub-forces.
* It creates a new {@link StrategicFormation} for each sub-force and checks its eligibility.
* Eligible formations are imported into the campaign, and the strategic formation status of
* the respective force is set to {@code true}.
* After every force is processed, an 'OrganizationChangedEvent' is triggered.
* This function runs recursively on each sub-force, effectively traversing the complete TO&E.
*
* @param campaign the current {@link Campaign}.
* @param workingNode the {@link Force} node from which the method starts working down through
* all its sub-forces.
*/
private static void recalculateSubForceStrategicStatus(Campaign campaign, Force workingNode) {
for (Force force : workingNode.getSubForces()) {
StrategicFormation strategicFormation = new StrategicFormation(force.getId(), campaign);

boolean isEligible = strategicFormation.isEligible(campaign);

if (isEligible) {
campaign.importStrategicFormation(strategicFormation);
force.setStrategicFormation(true);
}

MekHQ.triggerEvent(new OrganizationChangedEvent(force));

recalculateSubForceStrategicStatus(campaign, force);
}
}
}
4 changes: 3 additions & 1 deletion MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import java.util.*;
import java.util.Map.Entry;

import static mekhq.campaign.force.StrategicFormation.recalculateStrategicFormations;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;


Expand Down Expand Up @@ -793,7 +794,7 @@ private static void processStrategicFormationNodes(Campaign campaign, Node worki
StrategicFormation strategicFormation = StrategicFormation.generateInstanceFromXML(wn2);

if (strategicFormation != null) {
campaign.importLance(strategicFormation);
campaign.importStrategicFormation(strategicFormation);
}
}
}
Expand Down Expand Up @@ -854,6 +855,7 @@ private static void processForces(Campaign retVal, Node wn, Version version) {
}
}

recalculateStrategicFormations(retVal);
logger.info("Load of Force Organization complete!");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,10 @@ public static int generateForce(AtBDynamicScenario scenario, AtBContract contrac
int forceBV = 0;
double forceMultiplier = forceTemplate.getForceMultiplier();

if (forceMultiplier == 0) {
forceMultiplier = 1;
}

if (forceMultiplier != 1) {
logger.info(String.format("Force BV Multiplier: %s (from scenario template)", forceMultiplier));
}
Expand Down
6 changes: 1 addition & 5 deletions MekHQ/src/mekhq/campaign/stratcon/StratconRulesManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -1438,7 +1438,7 @@ public static List<Integer> getAvailableForceIDs(int unitType, Campaign campaign
// assemble a set of all force IDs that are currently assigned to tracks that are not this one
Set<Integer> forcesInTracks = campaign.getActiveAtBContracts().stream()
.flatMap(contract -> contract.getStratconCampaignState().getTracks().stream())
.filter(track -> (track != currentTrack) || !reinforcements)
.filter(track -> (!Objects.equals(track, currentTrack)) || !reinforcements)
.flatMap(track -> track.getAssignedForceCoords().keySet().stream())
.collect(Collectors.toSet());

Expand All @@ -1449,10 +1449,6 @@ public static List<Integer> getAvailableForceIDs(int unitType, Campaign campaign
}

for (StrategicFormation formation : campaign.getStrategicFormations().values()) {
if (!formation.isEligible(campaign)) {
continue;
}

Force force = campaign.getForce(formation.getForceId());

if (force == null) {
Expand Down
Loading