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

Warnings on Campaign Load #5757

Merged
merged 8 commits into from
Jan 16, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
cancel.button=Cancel
continue.button=Continue Regardless

CANT_LOAD_FROM_NEWER_VERSION.message=%s, we seem to be having a problem with our command and control\
\ software. Checking the data, it looks like we might have a version mismatch.
CANT_LOAD_FROM_NEWER_VERSION.ooc=A campaign can <b>never</b> be loaded into an older version.

CANT_LOAD_FROM_OLDER_VERSION.message=%s, we seem to be having a problem with our command and control\
\ software. Checking the data, it looks like we still need to update our systems.
CANT_LOAD_FROM_OLDER_VERSION.ooc=<p>To avoid file corruption and ensure a smooth experience, load\
\ and save your campaign in each Milestone released after the version your campaign was last\
\ saved in.</p>\
<br>\
<p>1. Load your campaign in the next Milestone.</p>\
<p>2. Save and close it.</p>\
<p>3. Repeat for the next Milestone.</p>\
<br>\
<p>After catching up with all Milestones, you can safely upgrade to the latest development\
\ version.</p>\
<br>\
<p><b>Warning:</b> The 'continue regardless' button is included to help development and testing\
\ and is <i>not</i> intended for general use.</p>\
<br>\
<p>The MekHQ team will <i>not</i> offer assistance if you ignore this warning.</p>

ACTIVE_OR_FUTURE_CONTRACT.message=<p>%s, our command and control software license doesn't support\
\ this action.</p>\
<br>\
<p>We can continue, but it will void the warranty.
ACTIVE_OR_FUTURE_CONTRACT.ooc=<p>A lot of information is initialized when a contract is created,\
\ changing versions mid-contract is not supported.</p>\
<br>\
<p><b>Warning:</b> The 'continue regardless' button is included to help development and testing\
\ and is <i>not</i> intended for general use.</p>\
<br>\
<p>The MekHQ team will <i>not</i> offer assistance if you ignore this warning.</p>
57 changes: 57 additions & 0 deletions MekHQ/src/mekhq/campaign/Campaign.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
*/
package mekhq.campaign;

import megamek.Version;
import megamek.client.bot.princess.BehaviorSettings;
import megamek.client.bot.princess.BehaviorSettingsFactory;
import megamek.client.generator.RandomGenderGenerator;
Expand Down Expand Up @@ -179,6 +180,7 @@ public class Campaign implements ITechManager {
public static final String REPORT_LINEBREAK = "<br/><br/>";

private UUID id;
private Version version; // this is dynamically populated on load and doesn't need to be saved

// we have three things to track: (1) teams, (2) units, (3) repair tasks
// we will use the same basic system (borrowed from MegaMek) for tracking
Expand Down Expand Up @@ -437,6 +439,14 @@ public UUID getId() {
return id;
}

public void setVersion(Version version) {
this.version = version;
}

public @Nullable Version getVersion() {
return version;
}

public String getName() {
return name;
}
Expand Down Expand Up @@ -1214,6 +1224,53 @@ public List<AtBContract> getAtBContracts() {
.collect(Collectors.toList());
}

/**
* Determines whether there is an active AtB (Against the Bot) contract.
* This method checks if there are contracts currently active. Optionally,
* it can also consider future contracts that have been accepted but have
* not yet started.
*
* @param includeFutureContracts a boolean indicating whether contracts that
* have been accepted but have not yet started
* should also be considered as active.
* @return {@code true} if there is any currently active AtB contract, or if
* {@code includeFutureContracts} is {@code true} and there are future
* contracts starting after the current date. Otherwise, {@code false}.
* @see #hasFutureAtBContract()
*/
public boolean hasActiveAtBContract(boolean includeFutureContracts) {
if (!getActiveAtBContracts().isEmpty()) {
return true;
}

if (includeFutureContracts) {
return hasFutureAtBContract();
}

return false;
}

/**
* Determines whether there are any future AtB (Against the Bot) contracts.
* A future contract is defined as a contract that has been accepted but
* has a start date later than the current day.
*
* @return true if there is at least one future AtB contract (accepted but
* starting after the current date). Otherwise, false.
*/
public boolean hasFutureAtBContract() {
List<AtBContract> contracts = getAtBContracts();

for (AtBContract contract : contracts) {
// This catches any contracts that have been accepted, but haven't yet started
if (contract.getStartDate().isAfter(currentDay)) {
return true;
}
}

return false;
}

public List<AtBContract> getActiveAtBContracts() {
return getActiveAtBContracts(false);
}
Expand Down
92 changes: 89 additions & 3 deletions MekHQ/src/mekhq/campaign/CampaignFactory.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018 - The MegaMek Team. All Rights Reserved.
* Copyright (c) 2018-2025 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MekHQ.
*
Expand All @@ -18,10 +18,14 @@
*/
package mekhq.campaign;

import megamek.Version;
import megamek.common.annotations.Nullable;
import mekhq.MHQConstants;
import mekhq.MekHQ;
import mekhq.NullEntityException;
import mekhq.campaign.io.CampaignXmlParseException;
import mekhq.campaign.io.CampaignXmlParser;
import mekhq.gui.dialog.CampaignHasProblemOnLoad;

import java.io.BufferedInputStream;
import java.io.IOException;
Expand All @@ -35,6 +39,13 @@
public class CampaignFactory {
private MekHQ app;

public enum CampaignProblemType {
NONE,
CANT_LOAD_FROM_NEWER_VERSION,
CANT_LOAD_FROM_OLDER_VERSION,
ACTIVE_OR_FUTURE_CONTRACT
}

/**
* Protected constructor to prevent instantiation.
*/
Expand Down Expand Up @@ -64,7 +75,7 @@ public static CampaignFactory newInstance(MekHQ app) {
* the input stream.
* @throws NullEntityException if the campaign contains a null entity
*/
public Campaign createCampaign(InputStream is)
public @Nullable Campaign createCampaign(InputStream is)
throws CampaignXmlParseException, IOException, NullEntityException {
if (!is.markSupported()) {
is = new BufferedInputStream(is);
Expand All @@ -79,7 +90,82 @@ public Campaign createCampaign(InputStream is)
// ...otherwise, assume we're an XML file.

CampaignXmlParser parser = new CampaignXmlParser(is, this.app);
return parser.parse();
Campaign campaign = parser.parse();

if (campaign == null) {
return null;
}

return checkForLoadProblems(campaign);
}

/**
* Validates the campaign for loading issues and presents the user with dialogs for each problem encountered.
*
* <p>This method sequentially checks for three potential problems while loading the campaign:</p>
* <ul>
* <li>If the campaign version is newer than the application's version.</li>
* <li>If the campaign version is older than the last supported milestone version.</li>
* <li>If the campaign has active or future AtB contracts.</li>
* </ul>
*
* <p>For each issue encountered, a dialog is displayed to the user using {@link CampaignHasProblemOnLoad}.
* The user can either cancel or proceed with loading. If the user cancels at any point, the method
* returns {@code null}. Otherwise, if no problems remain or the user chooses to proceed for all
* issues, the method returns the given {@code Campaign} object.</p>
*
* @param campaign the {@link Campaign} object to validate and load
* @return the {@link Campaign} object if the user chooses to proceed with all problems or if no
* problems are detected; {@code null} if the user chooses to cancel
*/
private static Campaign checkForLoadProblems(Campaign campaign) {
final Version mhqVersion = MHQConstants.VERSION;
final Version lastMilestone = MHQConstants.LAST_MILESTONE;
final Version campaignVersion = campaign.getVersion();

// Check if the campaign is from a newer version
if (campaignVersion.isHigherThan(mhqVersion)) {
if (triggerProblemDialog(campaign, CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION)) {
return null;
}
}

// Check if the campaign is from an older, unsupported version
if (campaignVersion.isLowerThan(lastMilestone)) {
if (triggerProblemDialog(campaign, CampaignProblemType.CANT_LOAD_FROM_OLDER_VERSION)) {
return null;
}
}

// Check if the campaign has active or future AtB contracts (only if the user is changing versions)
if (!campaignVersion.equals(mhqVersion) && campaign.hasActiveAtBContract(true)) {
if (triggerProblemDialog(campaign, CampaignProblemType.ACTIVE_OR_FUTURE_CONTRACT)) {
return null;
}
}

// All checks passed, return the campaign
return campaign;
}

/**
* Displays the {@link CampaignHasProblemOnLoad} dialog for a given problem type and returns
* whether the user cancelled the loading process.
*
* <p>The dialog informs the user about the specific problem and allows them to either
* cancel the loading process or continue despite the problem. If the user selects
* "Cancel," the method returns {@code true}. Otherwise, it returns {@code false}.</p>
*
* @param campaign the {@link Campaign} object associated with the problem
* @param problemType the {@link CampaignProblemType} specifying the current issue
* @return {@code true} if the user chose to cancel loading, {@code false} otherwise
*/
private static boolean triggerProblemDialog(Campaign campaign, CampaignProblemType problemType) {
final int USER_SELECTED_CANCEL = 0;

CampaignHasProblemOnLoad problemDialog = new CampaignHasProblemOnLoad(campaign, problemType);

return problemDialog.getDialogChoice() == USER_SELECTED_CANCEL;
}

private byte[] readHeader(InputStream is) throws IOException {
Expand Down
7 changes: 3 additions & 4 deletions MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@
import mekhq.campaign.personnel.turnoverAndRetention.RetirementDefectionTracker;
import mekhq.campaign.rating.CamOpsReputation.ReputationController;
import mekhq.campaign.storyarc.StoryArc;
import mekhq.campaign.unit.ShipTransportedUnitsSummary;
import mekhq.campaign.unit.TacticalTransportedUnitsSummary;
import mekhq.campaign.unit.Unit;
import mekhq.campaign.unit.cleanup.EquipmentUnscrambler;
import mekhq.campaign.unit.cleanup.EquipmentUnscramblerResult;
Expand All @@ -82,8 +80,6 @@
import java.util.*;
import java.util.Map.Entry;

import static mekhq.campaign.enums.CampaignTransportType.SHIP_TRANSPORT;
import static mekhq.campaign.enums.CampaignTransportType.TACTICAL_TRANSPORT;
import static mekhq.campaign.force.CombatTeam.recalculateCombatTeams;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;

Expand Down Expand Up @@ -144,6 +140,7 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException {
throw new CampaignXmlParseException(String.format("Illegal version of %s failed to parse",
campaignEle.getAttribute("version")));
}
retVal.setVersion(version);

// Indicates whether or not new units were written to disk while
// loading the Campaign file. If so, we need to kick back off loading
Expand Down Expand Up @@ -242,6 +239,8 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException {
// Okay, so what element is it?
String xn = wn.getNodeName();



if (xn.equalsIgnoreCase("campaignOptions")) {
retVal.setCampaignOptions(CampaignOptions.generateCampaignOptionsFromXml(wn, version));
} else if (xn.equalsIgnoreCase("randomSkillPreferences")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import java.util.ResourceBundle;

import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerDescription;
import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getSpeakerIcon;
import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon;
import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth;

/**
Expand Down
22 changes: 21 additions & 1 deletion MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import java.util.ResourceBundle;

import static mekhq.campaign.force.Force.FORCE_NONE;
import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getSpeakerIcon;
import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth;

public class MHQDialogImmersive extends JDialog {
Expand Down Expand Up @@ -372,6 +371,27 @@ public static StringBuilder getSpeakerDescription(Campaign campaign, Person spea
return speakerDescription;
}

/**
* Retrieves the speaker's icon for dialogs. If no speaker is supplied, the faction icon
* for the campaign is returned instead.
*
* @param campaign the {@link Campaign} instance containing the faction icon; can be
* {@code null} to use a default image.
* @param speaker the {@link Person} serving as the speaker for the dialog; can be {@code null}.
* @return an {@link ImageIcon} for the speaker's portrait, or the faction icon if the speaker is {@code null}.
*/
public static @Nullable ImageIcon getSpeakerIcon(@Nullable Campaign campaign, @Nullable Person speaker) {
if (campaign == null) {
return new ImageIcon("data/images/universe/factions/logo_mercenaries.png");
}

if (speaker == null) {
return campaign.getCampaignFactionIcon();
}

return speaker.getPortrait().getImageIcon();
}

/**
* A class to store and represent a button label and its associated tooltip.
* <p>
Expand Down
Loading
Loading