From a8e8442ffc88b91435949cbe9d6d9ba4df4b8367 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 11 Jan 2025 12:25:38 -0600 Subject: [PATCH 1/7] Prevent updates with active contracts in CampaignXmlParser Added logic to block campaign updates if active contracts exist, ensuring users complete them before proceeding. Displayed an error dialog to inform users of the issue and maintain version integrity. --- .../mekhq/campaign/io/CampaignXmlParser.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index fe783cdf51..c4c34d51a3 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -29,6 +29,7 @@ import megamek.common.icons.Camouflage; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.logging.MMLogger; +import mekhq.MHQConstants; import mekhq.MekHQ; import mekhq.NullEntityException; import mekhq.Utilities; @@ -73,6 +74,7 @@ import org.apache.commons.lang3.StringUtils; import org.w3c.dom.*; +import javax.swing.*; import javax.xml.parsers.DocumentBuilder; import java.io.*; import java.time.LocalDate; @@ -581,6 +583,38 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { } }); + if (MHQConstants.VERSION.isHigherThan(version)) { + List contracts = retVal.getAtBContracts(); + boolean hasActiveContract = !contracts.isEmpty(); + + if (!hasActiveContract) { + LocalDate today = retVal.getLocalDate(); + + for (AtBContract contract : retVal.getAtBContracts()) { + // This catches any contracts that have been accepted, but haven't yet started + if (contract.getStartDate().isAfter(today)) { + hasActiveContract = true; + break; + } + } + } + + if (hasActiveContract) { + String message = String.format( + "This campaign has an active contract." + + "\n\nPlease complete the contract before updating to version %s.", + MHQConstants.VERSION); + + // Ensure dialog displays on the Event Dispatch Thread (EDT) + JOptionPane.showMessageDialog( + null, + message, + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + logger.info("Load of campaign file complete!"); return retVal; From c8b8bef0b39e992686eae1cdba6494e82fbd2c3d Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 11 Jan 2025 12:27:53 -0600 Subject: [PATCH 2/7] Refactored active contract warning logic into a new method Extracted the logic for displaying a warning for active or future contracts into a dedicated method, improving readability and maintainability. Updated corresponding code to use the new method for better encapsulation and reduced duplication. --- .../mekhq/campaign/io/CampaignXmlParser.java | 72 +++++++++++-------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index c4c34d51a3..ff1ec1d718 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -583,36 +583,10 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { } }); - if (MHQConstants.VERSION.isHigherThan(version)) { - List contracts = retVal.getAtBContracts(); - boolean hasActiveContract = !contracts.isEmpty(); - - if (!hasActiveContract) { - LocalDate today = retVal.getLocalDate(); - for (AtBContract contract : retVal.getAtBContracts()) { - // This catches any contracts that have been accepted, but haven't yet started - if (contract.getStartDate().isAfter(today)) { - hasActiveContract = true; - break; - } - } - } - if (hasActiveContract) { - String message = String.format( - "This campaign has an active contract." + - "\n\nPlease complete the contract before updating to version %s.", - MHQConstants.VERSION); - - // Ensure dialog displays on the Event Dispatch Thread (EDT) - JOptionPane.showMessageDialog( - null, - message, - "Error", - JOptionPane.ERROR_MESSAGE - ); - } + if (MHQConstants.VERSION.isHigherThan(version)) { + triggerActiveContractWarning(retVal); } logger.info("Load of campaign file complete!"); @@ -620,6 +594,48 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { return retVal; } + /** + * Displays a warning dialog if the given {@code Campaign} has an active or future contract. + *

+ * This method checks the campaign's list of contracts to determine if there are any currently active + * or accepted contracts with a start date in the future. If such a contract exists, it shows an error + * dialog informing the user that the campaign cannot proceed until the contract is completed. + *

+ * + * @param campaign The {@link Campaign} object representing the current campaign, + * which contains information about the contracts and local date. + */ + private static void triggerActiveContractWarning(Campaign campaign) { + List contracts = campaign.getAtBContracts(); + boolean hasActiveContract = !contracts.isEmpty(); + + if (!hasActiveContract) { + LocalDate today = campaign.getLocalDate(); + + for (AtBContract contract : campaign.getAtBContracts()) { + // This catches any contracts that have been accepted, but haven't yet started + if (contract.getStartDate().isAfter(today)) { + hasActiveContract = true; + break; + } + } + } + + if (hasActiveContract) { + String message = String.format( + "This campaign has an active contract." + + "\n\nPlease complete the contract before updating to version %s.", + MHQConstants.VERSION); + + JOptionPane.showMessageDialog( + null, + message, + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + /** * This will fixup unit-tech problems seen in some save games, such as techs * having been double-assigned or being assigned to mothballed units. From 6d9133e79498ba90a051d6a5a16140ad2c155579 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sat, 11 Jan 2025 12:33:12 -0600 Subject: [PATCH 3/7] Refactored active contract check in trigger warning Reorganized the active contract logic to directly use `getActiveAtBContracts()` for clarity and efficiency. Moved the contracts list retrieval to conditional logic to avoid redundant calls. --- MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index ff1ec1d718..5e4361b2e5 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -606,13 +606,13 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { * which contains information about the contracts and local date. */ private static void triggerActiveContractWarning(Campaign campaign) { - List contracts = campaign.getAtBContracts(); - boolean hasActiveContract = !contracts.isEmpty(); + boolean hasActiveContract = !campaign.getActiveAtBContracts().isEmpty(); if (!hasActiveContract) { + List contracts = campaign.getAtBContracts(); LocalDate today = campaign.getLocalDate(); - for (AtBContract contract : campaign.getAtBContracts()) { + for (AtBContract contract : contracts) { // This catches any contracts that have been accepted, but haven't yet started if (contract.getStartDate().isAfter(today)) { hasActiveContract = true; From 87d319d6b4ba6819629658b7d2e5b367f7f66a70 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Sun, 12 Jan 2025 11:25:35 -0600 Subject: [PATCH 4/7] Add warning for active contracts during version updates Refactored logic to display a warning for active or future contracts into a new `ActiveContractWarning` class. Integrated the new warning mechanism into `CampaignXmlParser` and revised resource properties to include localized messages. Removed redundant code handling contract warnings directly in `CampaignXmlParser`. --- .../resources/mekhq/resources/GUI.properties | 7 ++- MekHQ/src/mekhq/campaign/Campaign.java | 47 ++++++++++++++++ .../mekhq/campaign/io/CampaignXmlParser.java | 52 ++---------------- .../gui/dialog/ActiveContractWarning.java | 55 +++++++++++++++++++ 4 files changed, 113 insertions(+), 48 deletions(-) create mode 100644 MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index fc12362c29..2eb9f2d017 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -1556,4 +1556,9 @@ AutoResolveMethod.PRINCESS.toolTipText=Princess plays the game for you. AutoResolveMethod.ABSTRACT_COMBAT.text=Abstract Combat Auto Resolution AutoResolveMethod.ABSTRACT_COMBAT.toolTipText=ACAR, a fast simulation using a subset of the abstract combat system rules AutoResolveMethod.promptForAutoResolveMethod.text=Select the method to use for auto resolving the scenario. -AutoResolveMethod.promptForAutoResolveMethod.title=Auto Resolve Method \ No newline at end of file +AutoResolveMethod.promptForAutoResolveMethod.title=Auto Resolve Method + +ActiveContractWarning.title=Active Contract Warning +ActiveContractWarning.message="This campaign has an active contract.\ + \n\ + \nPlease complete the contract before updating to version %s." \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index dfa601762f..ba22489c29 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -1209,6 +1209,53 @@ public List 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 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 getActiveAtBContracts() { return getActiveAtBContracts(false); } diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index 5e4361b2e5..8d617c5b2f 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2018-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * @@ -68,13 +68,13 @@ import mekhq.campaign.universe.PlanetarySystem.PlanetarySystemEvent; import mekhq.campaign.universe.Systems; import mekhq.campaign.universe.fameAndInfamy.FameAndInfamyController; +import mekhq.gui.dialog.ActiveContractWarning; import mekhq.io.idReferenceClasses.PersonIdReference; import mekhq.module.atb.AtBEventProcessor; import mekhq.utilities.MHQXMLUtility; import org.apache.commons.lang3.StringUtils; import org.w3c.dom.*; -import javax.swing.*; import javax.xml.parsers.DocumentBuilder; import java.io.*; import java.time.LocalDate; @@ -583,10 +583,10 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { } }); - - if (MHQConstants.VERSION.isHigherThan(version)) { - triggerActiveContractWarning(retVal); + if (retVal.hasActiveAtBContract(true)) { + new ActiveContractWarning(); + } } logger.info("Load of campaign file complete!"); @@ -594,48 +594,6 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { return retVal; } - /** - * Displays a warning dialog if the given {@code Campaign} has an active or future contract. - *

- * This method checks the campaign's list of contracts to determine if there are any currently active - * or accepted contracts with a start date in the future. If such a contract exists, it shows an error - * dialog informing the user that the campaign cannot proceed until the contract is completed. - *

- * - * @param campaign The {@link Campaign} object representing the current campaign, - * which contains information about the contracts and local date. - */ - private static void triggerActiveContractWarning(Campaign campaign) { - boolean hasActiveContract = !campaign.getActiveAtBContracts().isEmpty(); - - if (!hasActiveContract) { - List contracts = campaign.getAtBContracts(); - LocalDate today = campaign.getLocalDate(); - - for (AtBContract contract : contracts) { - // This catches any contracts that have been accepted, but haven't yet started - if (contract.getStartDate().isAfter(today)) { - hasActiveContract = true; - break; - } - } - } - - if (hasActiveContract) { - String message = String.format( - "This campaign has an active contract." + - "\n\nPlease complete the contract before updating to version %s.", - MHQConstants.VERSION); - - JOptionPane.showMessageDialog( - null, - message, - "Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - /** * This will fixup unit-tech problems seen in some save games, such as techs * having been double-assigned or being assigned to mothballed units. diff --git a/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java b/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java new file mode 100644 index 0000000000..87500860b5 --- /dev/null +++ b/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MekHQ. + * + * MegaMek 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 3 of the License, or + * (at your option) any later version. + * + * MegaMek 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. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package mekhq.gui.dialog; + +import mekhq.MHQConstants; +import mekhq.MekHQ; + +import javax.swing.*; +import java.util.ResourceBundle; + +public class ActiveContractWarning { + private final String RESOURCE_KEY = "mekhq.resources.GUI"; + private final transient ResourceBundle resources = + ResourceBundle.getBundle(RESOURCE_KEY, MekHQ.getMHQOptions().getLocale()); + + /** + * Displays a warning dialog to the user indicating that the campaign has + * an active contract. The warning informs the user to complete the active + * contract before updating to the specified version of the application. + *

+ * The displayed message includes the current application version retrieved + * from {@code MHQConstants.VERSION}. + *

+ * The dialog uses an error message icon and is displayed in a modal popup. + * + * @see JOptionPane#showMessageDialog(java.awt.Component, Object, String, int) + */ + public ActiveContractWarning() { + String message = String.format(resources.getString("ActiveContractWarning.message"), + MHQConstants.VERSION); + + JOptionPane.showMessageDialog( + null, + message, + resources.getString("ActiveContractWarning.title"), + JOptionPane.ERROR_MESSAGE + ); + } +} From d8b3041be3afcccde1f937d75c17a8f96edd3785 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 15 Jan 2025 14:10:11 -0600 Subject: [PATCH 5/7] Refactored campaign version checks and removed ActiveContractWarning. Replaced ActiveContractWarning with a more robust version compatibility check during campaign loading. Unified dialog functionality into CampaignHasProblemOnLoad to handle load issues more effectively. Refactored redundant code, such as getSpeakerIcon, into a common utility to improve maintainability. --- .../CampaignHasProblemOnLoad.properties | 31 +++++ .../resources/mekhq/resources/GUI.properties | 7 +- MekHQ/src/mekhq/campaign/Campaign.java | 10 ++ MekHQ/src/mekhq/campaign/CampaignFactory.java | 72 ++++++++++- .../mekhq/campaign/io/CampaignXmlParser.java | 15 +-- .../baseComponents/AbstractMHQNagDialog.java | 2 +- .../baseComponents/MHQDialogImmersive.java | 22 +++- .../gui/dialog/ActiveContractWarning.java | 55 --------- .../gui/dialog/CampaignHasProblemOnLoad.java | 113 ++++++++++++++++++ .../gui/dialog/ContractAutomationDialog.java | 2 +- .../DialogAbandonedConvoy.java | 2 +- .../DialogContractStart.java | 2 +- .../resupplyAndCaches/DialogInterception.java | 2 +- .../resupplyAndCaches/DialogItinerary.java | 2 +- .../DialogPlayerConvoyOption.java | 2 +- .../DialogResupplyFocus.java | 2 +- .../DialogRoleplayEvent.java | 2 +- .../ResupplyDialogUtilities.java | 19 --- .../gui/stratcon/StratconScenarioWizard.java | 2 +- 19 files changed, 259 insertions(+), 105 deletions(-) create mode 100644 MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties delete mode 100644 MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java create mode 100644 MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java diff --git a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties new file mode 100644 index 0000000000..c3bc5f7650 --- /dev/null +++ b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties @@ -0,0 +1,31 @@ +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 never 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=

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.

\ +
    \ +
  1. Load your campaign.
  2. \ +
  3. Save and close it.
  4. \ +
  5. Repeat for the next Milestone.
  6. \ +
\ +

After catching up with all Milestones, you can safely upgrade to the latest development\ + \ version.

\ +

Warning: The 'continue regardless' button is included to help development and testing\ + \ and is not intended for general use.

\ +

The MekHQ team will not offer assistance if you ignore this warning.

+ +ACTIVE_OR_FUTURE_CONTRACT.message=

%s, our command and control software license doesn't support\ + \ this action.

\ +

We can continue, but it will void the warranty. +ACTIVE_OR_FUTURE_CONTRACT.ooc=

A lot of information is initialized when a contract is created,\ + \ changing versions mid-contract is not supported.

\ +

Warning: The 'continue regardless' button is included to help development and testing\ + \ and is not intended for general use.

\ +

The MekHQ team will not offer assistance if you ignore this warning.

\ No newline at end of file diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties index 2eb9f2d017..fc12362c29 100644 --- a/MekHQ/resources/mekhq/resources/GUI.properties +++ b/MekHQ/resources/mekhq/resources/GUI.properties @@ -1556,9 +1556,4 @@ AutoResolveMethod.PRINCESS.toolTipText=Princess plays the game for you. AutoResolveMethod.ABSTRACT_COMBAT.text=Abstract Combat Auto Resolution AutoResolveMethod.ABSTRACT_COMBAT.toolTipText=ACAR, a fast simulation using a subset of the abstract combat system rules AutoResolveMethod.promptForAutoResolveMethod.text=Select the method to use for auto resolving the scenario. -AutoResolveMethod.promptForAutoResolveMethod.title=Auto Resolve Method - -ActiveContractWarning.title=Active Contract Warning -ActiveContractWarning.message="This campaign has an active contract.\ - \n\ - \nPlease complete the contract before updating to version %s." \ No newline at end of file +AutoResolveMethod.promptForAutoResolveMethod.title=Auto Resolve Method \ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java index eec3c44fae..330ba62d39 100644 --- a/MekHQ/src/mekhq/campaign/Campaign.java +++ b/MekHQ/src/mekhq/campaign/Campaign.java @@ -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; @@ -179,6 +180,7 @@ public class Campaign implements ITechManager { public static final String REPORT_LINEBREAK = "

"; 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 @@ -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; } diff --git a/MekHQ/src/mekhq/campaign/CampaignFactory.java b/MekHQ/src/mekhq/campaign/CampaignFactory.java index 59f0afacc5..6d1e09121a 100644 --- a/MekHQ/src/mekhq/campaign/CampaignFactory.java +++ b/MekHQ/src/mekhq/campaign/CampaignFactory.java @@ -18,16 +18,22 @@ */ 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; import java.io.InputStream; import java.util.zip.GZIPInputStream; +import static mekhq.campaign.CampaignFactory.CampaignProblemType.NONE; + /** * Defines a factory API that enables {@link Campaign} instances to be created * from its detected format. @@ -35,6 +41,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. */ @@ -64,7 +77,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); @@ -79,7 +92,62 @@ 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 a given campaign for potential loading problems and handles user choices based on + * detected issues. + * + *

This method performs the following checks:

+ *
    + *
  • Ensures the campaign version is compatible with the current application version.
  • + *
  • Checks whether the campaign version is supported compared to the last milestone version.
  • + *
  • Checks whether the campaign has active or future AtB (Against the Bot) contracts.
  • + *
+ * + *

If any issues are found, a dialog prompts the user to decide whether to proceed or not. + * If the user chooses to abort, the method returns {@code null}; otherwise, it returns the + * original campaign object.

+ * + * @param campaign the {@link Campaign} object to validate for loading issues + * @return the given {@link Campaign} if no critical issues are detected or the user chooses to proceed; + * {@code null} if the user opts to abort loading due to critical issues + */ + private static Campaign checkForLoadProblems(Campaign campaign) { + CampaignProblemType problemType = CampaignProblemType.NONE; + + final Version mhqVersion = MHQConstants.VERSION; + final Version lastMilestone = MHQConstants.LAST_MILESTONE; + final Version campaignVersion = campaign.getVersion(); + + if (campaignVersion.isHigherThan(mhqVersion)) { + problemType = CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION; + } + + if (campaignVersion.isLowerThan(lastMilestone)) { + problemType = CampaignProblemType.CANT_LOAD_FROM_OLDER_VERSION; + } + + if (!campaign.hasActiveAtBContract(true)) { + problemType = CampaignProblemType.ACTIVE_OR_FUTURE_CONTRACT; + } + + if (problemType != NONE) { + CampaignHasProblemOnLoad problemDialog = new CampaignHasProblemOnLoad(campaign, problemType); + + if (problemDialog.getDialogChoice() == 0) { + return null; + } + } + + return campaign; } private byte[] readHeader(InputStream is) throws IOException { diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java index 748d7ceb8d..ca473d6b90 100644 --- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java +++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java @@ -29,7 +29,6 @@ import megamek.common.icons.Camouflage; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.logging.MMLogger; -import mekhq.MHQConstants; import mekhq.MekHQ; import mekhq.NullEntityException; import mekhq.Utilities; @@ -62,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; @@ -71,7 +68,6 @@ import mekhq.campaign.universe.PlanetarySystem.PlanetarySystemEvent; import mekhq.campaign.universe.Systems; import mekhq.campaign.universe.fameAndInfamy.FameAndInfamyController; -import mekhq.gui.dialog.ActiveContractWarning; import mekhq.io.idReferenceClasses.PersonIdReference; import mekhq.module.atb.AtBEventProcessor; import mekhq.utilities.MHQXMLUtility; @@ -84,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; @@ -146,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 @@ -244,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")) { @@ -596,12 +593,6 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException { } }); - if (MHQConstants.VERSION.isHigherThan(version)) { - if (retVal.hasActiveAtBContract(true)) { - new ActiveContractWarning(); - } - } - logger.info("Load of campaign file complete!"); return retVal; diff --git a/MekHQ/src/mekhq/gui/baseComponents/AbstractMHQNagDialog.java b/MekHQ/src/mekhq/gui/baseComponents/AbstractMHQNagDialog.java index de5e08a6b8..9f03c8a3a4 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/AbstractMHQNagDialog.java +++ b/MekHQ/src/mekhq/gui/baseComponents/AbstractMHQNagDialog.java @@ -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; /** diff --git a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java index fa47da707f..7544492c0f 100644 --- a/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java +++ b/MekHQ/src/mekhq/gui/baseComponents/MHQDialogImmersive.java @@ -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 { @@ -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. *

diff --git a/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java b/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java deleted file mode 100644 index 87500860b5..0000000000 --- a/MekHQ/src/mekhq/gui/dialog/ActiveContractWarning.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. - * - * This file is part of MekHQ. - * - * MegaMek 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 3 of the License, or - * (at your option) any later version. - * - * MegaMek 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. - * - * You should have received a copy of the GNU General Public License - * along with MegaMek. If not, see . - */ -package mekhq.gui.dialog; - -import mekhq.MHQConstants; -import mekhq.MekHQ; - -import javax.swing.*; -import java.util.ResourceBundle; - -public class ActiveContractWarning { - private final String RESOURCE_KEY = "mekhq.resources.GUI"; - private final transient ResourceBundle resources = - ResourceBundle.getBundle(RESOURCE_KEY, MekHQ.getMHQOptions().getLocale()); - - /** - * Displays a warning dialog to the user indicating that the campaign has - * an active contract. The warning informs the user to complete the active - * contract before updating to the specified version of the application. - *

- * The displayed message includes the current application version retrieved - * from {@code MHQConstants.VERSION}. - *

- * The dialog uses an error message icon and is displayed in a modal popup. - * - * @see JOptionPane#showMessageDialog(java.awt.Component, Object, String, int) - */ - public ActiveContractWarning() { - String message = String.format(resources.getString("ActiveContractWarning.message"), - MHQConstants.VERSION); - - JOptionPane.showMessageDialog( - null, - message, - resources.getString("ActiveContractWarning.title"), - JOptionPane.ERROR_MESSAGE - ); - } -} diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java new file mode 100644 index 0000000000..e5b62857ec --- /dev/null +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -0,0 +1,113 @@ +package mekhq.gui.dialog; + +import megamek.common.annotations.Nullable; +import mekhq.MekHQ; +import mekhq.campaign.Campaign; +import mekhq.campaign.CampaignFactory.CampaignProblemType; +import mekhq.campaign.personnel.Person; +import mekhq.gui.baseComponents.MHQDialogImmersive; + +import java.util.List; +import java.util.ResourceBundle; + +import static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND; + +/** + * Dialog to inform and handle campaign-loading problems within MekHQ. + * + *

This dialog prompts the user with both in-character and out-of-character messages, + * providing actionable options if any issues arise during campaign load, such as version + * incompatibility or missing contracts.

+ * + *

The dialog presents two options: "Cancel" to abort loading the campaign or + * "Continue Regardless" to proceed despite the detected issues. It dynamically generates + * text based on the problem type and campaign information.

+ */ +public class CampaignHasProblemOnLoad extends MHQDialogImmersive { + private static final String BUNDLE_KEY = "mekhq.resources.CampaignHasProblemOnLoad"; + private static final ResourceBundle resources = ResourceBundle.getBundle( + BUNDLE_KEY, MekHQ.getMHQOptions().getLocale()); + + /** + * Constructs the dialog to handle campaign load problems. + * + *

The dialog is built using localized messages for both + * in-character and out-of-character messages, following the detected + * problem type and campaign details. It also sets up predefined + * buttons for user interaction.

+ * + * @param campaign the {@link Campaign} for which the load problem dialog is presented + * @param problemType the {@link CampaignProblemType} specifying the nature of the load problem + */ + public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) { + super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign, problemType), + createButtons(), createOutOfCharacterMessage(problemType), 0, + null, null, null); + } + + /** + * Generates the list of buttons for the dialog. + * + *

Buttons include:

+ *
    + *
  • "Cancel" button to stop loading the campaign.
  • + *
  • "Continue" button to proceed with loading the campaign despite the problems.
  • + *
+ * + * @return a {@link List} of {@link ButtonLabelTooltipPair} objects representing the dialog's buttons + */ + private static List createButtons() { + ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair( + resources.getString("cancel.button"), null); + + ButtonLabelTooltipPair btnContinue = new ButtonLabelTooltipPair( + resources.getString("continue.button"), null); + + return List.of(btnCancel, btnContinue); + } + + /** + * Retrieves the speaker for in-character dialog. + * + *

The speaker is determined as the senior administrator for the campaign + * with the "Command" specialization. If no such administrator is found, {@code null} + * is returned.

+ * + * @param campaign the {@link Campaign} whose senior administrator is to be retrieved + * @return a {@link Person} representing the senior administrator, or {@code null} if none exists + */ + private static @Nullable Person getSpeaker(Campaign campaign) { + return campaign.getSeniorAdminPerson(COMMAND); + } + + /** + * Creates the in-character message dynamically based on the problem type. + * + *

This message is localized and assembled using resource bundles, with campaign-specific + * information such as the commander's address.

+ * + * @param campaign the {@link Campaign} for which the in-character message is generated + * @param problemType the {@link CampaignProblemType} specifying the nature of the load problem + * @return a localized {@link String} containing the in-character message + */ + private static String createInCharacterMessage(Campaign campaign, CampaignProblemType problemType) { + String typeKey = problemType.toString(); + String commanderAddress = campaign.getCommanderAddress(false); + + return String.format(resources.getString(typeKey + ".message"), commanderAddress); + } + + /** + * Creates the out-of-character message dynamically based on the problem type. + * + *

This message is localized and is more technical or process-oriented, + * explaining the detected issues in plain terms.

+ * + * @param problemType the {@link CampaignProblemType} specifying the nature of the load problem + * @return a localized {@link String} containing the out-of-character message + */ + private static String createOutOfCharacterMessage(CampaignProblemType problemType) { + String typeKey = problemType.toString(); + return resources.getString(typeKey + ".message"); + } +} diff --git a/MekHQ/src/mekhq/gui/dialog/ContractAutomationDialog.java b/MekHQ/src/mekhq/gui/dialog/ContractAutomationDialog.java index 23e3bc6add..eeafff216e 100644 --- a/MekHQ/src/mekhq/gui/dialog/ContractAutomationDialog.java +++ b/MekHQ/src/mekhq/gui/dialog/ContractAutomationDialog.java @@ -28,7 +28,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; public class ContractAutomationDialog extends JDialog { diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java index 93b10207bc..e714cec0e1 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogAbandonedConvoy.java @@ -32,7 +32,7 @@ import static megamek.common.Compute.randomInt; 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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java index 3fcae938a8..b8f50045dd 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogContractStart.java @@ -35,7 +35,7 @@ import static mekhq.campaign.mission.resupplyAndCaches.Resupply.isProhibitedUnitType; import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.estimateCargoRequirements; 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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java index ed1b92441b..9085d306a8 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogInterception.java @@ -35,7 +35,7 @@ import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.forceContainsOnlyAerialForces; import static mekhq.campaign.mission.resupplyAndCaches.ResupplyUtilities.forceContainsOnlyVTOLForces; 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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java index ee74c75812..f5143e95c3 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java @@ -45,10 +45,10 @@ import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_LOOT; import static mekhq.campaign.mission.resupplyAndCaches.Resupply.ResupplyType.RESUPPLY_SMUGGLER; import static mekhq.campaign.universe.Factions.getFactionLogo; +import static mekhq.gui.baseComponents.MHQDialogImmersive.getSpeakerIcon; import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.createPartsReport; import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.formatColumnData; import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getEnemyFactionReference; -import static mekhq.gui.dialog.resupplyAndCaches.ResupplyDialogUtilities.getSpeakerIcon; import static mekhq.utilities.ImageUtilities.scaleImageIconToWidth; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java index d212104f95..efa53c36f9 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogPlayerConvoyOption.java @@ -30,7 +30,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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java index d9c22b6122..9c6d881ede 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogResupplyFocus.java @@ -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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java index 2c6bec7d1f..3aed64a44c 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogRoleplayEvent.java @@ -29,7 +29,7 @@ import java.util.UUID; 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; /** diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java index cf03d426e5..570582df3b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java @@ -18,7 +18,6 @@ */ package mekhq.gui.dialog.resupplyAndCaches; -import megamek.common.annotations.Nullable; import mekhq.campaign.Campaign; import mekhq.campaign.mission.AtBContract; import mekhq.campaign.mission.resupplyAndCaches.Resupply; @@ -27,10 +26,8 @@ import mekhq.campaign.parts.MekLocation; import mekhq.campaign.parts.Part; import mekhq.campaign.parts.equipment.AmmoBin; -import mekhq.campaign.personnel.Person; import mekhq.campaign.universe.Faction; -import javax.swing.*; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -55,22 +52,6 @@ * presentation and interaction contexts tied to resupply scenarios.

*/ public class ResupplyDialogUtilities { - /** - * 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. - * @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(Campaign campaign, @Nullable Person speaker) { - if (speaker == null) { - return campaign.getCampaignFactionIcon(); - } - - return speaker.getPortrait().getImageIcon(); - } - /** * Generates a detailed report of the parts available in the convoy contents of the given resupply mission. * diff --git a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java index 98c10117ce..8382498e0f 100644 --- a/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java +++ b/MekHQ/src/mekhq/gui/stratcon/StratconScenarioWizard.java @@ -53,7 +53,7 @@ import static mekhq.campaign.stratcon.StratconScenario.ScenarioState.PRIMARY_FORCES_COMMITTED; import static mekhq.campaign.stratcon.StratconScenario.ScenarioState.REINFORCEMENTS_COMMITTED; 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; /** From bf5fd5df1a26b01a1144031df7aebb23cb2b463b Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 15 Jan 2025 14:27:56 -0600 Subject: [PATCH 6/7] Refined campaign loading error handling Updated error dialogs to adapt button options based on problem type, added detailed handling for unsupported version issues, and refactored code for cleaner logic. This improves user guidance and streamlines the campaign loading process. --- .../CampaignHasProblemOnLoad.properties | 15 ++-- MekHQ/src/mekhq/campaign/CampaignFactory.java | 70 ++++++++++++------- .../gui/dialog/CampaignHasProblemOnLoad.java | 25 +++++-- 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties index c3bc5f7650..4d6e97f92f 100644 --- a/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties +++ b/MekHQ/resources/mekhq/resources/CampaignHasProblemOnLoad.properties @@ -10,22 +10,27 @@ CANT_LOAD_FROM_OLDER_VERSION.message=%s, we seem to be having a problem with our CANT_LOAD_FROM_OLDER_VERSION.ooc=

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.

\ -
    \ -
  1. Load your campaign.
  2. \ -
  3. Save and close it.
  4. \ -
  5. Repeat for the next Milestone.
  6. \ -
\ +
\ +

1. Load your campaign in the next Milestone.

\ +

2. Save and close it.

\ +

3. Repeat for the next Milestone.

\ +
\

After catching up with all Milestones, you can safely upgrade to the latest development\ \ version.

\ +
\

Warning: The 'continue regardless' button is included to help development and testing\ \ and is not intended for general use.

\ +
\

The MekHQ team will not offer assistance if you ignore this warning.

ACTIVE_OR_FUTURE_CONTRACT.message=

%s, our command and control software license doesn't support\ \ this action.

\ +
\

We can continue, but it will void the warranty. ACTIVE_OR_FUTURE_CONTRACT.ooc=

A lot of information is initialized when a contract is created,\ \ changing versions mid-contract is not supported.

\ +
\

Warning: The 'continue regardless' button is included to help development and testing\ \ and is not intended for general use.

\ +
\

The MekHQ team will not offer assistance if you ignore this warning.

\ No newline at end of file diff --git a/MekHQ/src/mekhq/campaign/CampaignFactory.java b/MekHQ/src/mekhq/campaign/CampaignFactory.java index 6d1e09121a..f4a08ff6de 100644 --- a/MekHQ/src/mekhq/campaign/CampaignFactory.java +++ b/MekHQ/src/mekhq/campaign/CampaignFactory.java @@ -32,8 +32,6 @@ import java.io.InputStream; import java.util.zip.GZIPInputStream; -import static mekhq.campaign.CampaignFactory.CampaignProblemType.NONE; - /** * Defines a factory API that enables {@link Campaign} instances to be created * from its detected format. @@ -102,54 +100,74 @@ public static CampaignFactory newInstance(MekHQ app) { } /** - * Validates a given campaign for potential loading problems and handles user choices based on - * detected issues. + * Validates the campaign for loading issues and presents the user with dialogs for each problem encountered. * - *

This method performs the following checks:

+ *

This method sequentially checks for three potential problems while loading the campaign:

*
    - *
  • Ensures the campaign version is compatible with the current application version.
  • - *
  • Checks whether the campaign version is supported compared to the last milestone version.
  • - *
  • Checks whether the campaign has active or future AtB (Against the Bot) contracts.
  • + *
  • If the campaign version is newer than the application's version.
  • + *
  • If the campaign version is older than the last supported milestone version.
  • + *
  • If the campaign has active or future AtB contracts.
  • *
* - *

If any issues are found, a dialog prompts the user to decide whether to proceed or not. - * If the user chooses to abort, the method returns {@code null}; otherwise, it returns the - * original campaign object.

+ *

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.

* - * @param campaign the {@link Campaign} object to validate for loading issues - * @return the given {@link Campaign} if no critical issues are detected or the user chooses to proceed; - * {@code null} if the user opts to abort loading due to critical issues + * @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) { - CampaignProblemType problemType = CampaignProblemType.NONE; - 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)) { - problemType = CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION; + 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)) { - problemType = CampaignProblemType.CANT_LOAD_FROM_OLDER_VERSION; - } - - if (!campaign.hasActiveAtBContract(true)) { - problemType = CampaignProblemType.ACTIVE_OR_FUTURE_CONTRACT; + if (triggerProblemDialog(campaign, CampaignProblemType.CANT_LOAD_FROM_OLDER_VERSION)) { + return null; + } } - if (problemType != NONE) { - CampaignHasProblemOnLoad problemDialog = new CampaignHasProblemOnLoad(campaign, problemType); - - if (problemDialog.getDialogChoice() == 0) { + // 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. + * + *

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}.

+ * + * @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 { is.mark(4); byte[] header = new byte[2]; diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java index e5b62857ec..57e03041e6 100644 --- a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -11,6 +11,7 @@ import java.util.ResourceBundle; import static mekhq.campaign.Campaign.AdministratorSpecialization.COMMAND; +import static mekhq.campaign.CampaignFactory.CampaignProblemType.CANT_LOAD_FROM_NEWER_VERSION; /** * Dialog to inform and handle campaign-loading problems within MekHQ. @@ -41,29 +42,39 @@ public class CampaignHasProblemOnLoad extends MHQDialogImmersive { */ public CampaignHasProblemOnLoad(Campaign campaign, CampaignProblemType problemType) { super(campaign, getSpeaker(campaign), null, createInCharacterMessage(campaign, problemType), - createButtons(), createOutOfCharacterMessage(problemType), 0, + createButtons(problemType), createOutOfCharacterMessage(problemType), 0, null, null, null); } /** - * Generates the list of buttons for the dialog. + * Generates the list of buttons for the dialog based on the specified problem type. * *

Buttons include:

*
    - *
  • "Cancel" button to stop loading the campaign.
  • - *
  • "Continue" button to proceed with loading the campaign despite the problems.
  • + *
  • "Cancel" button: Stops loading the campaign in all scenarios.
  • + *
  • "Continue" button: Allows proceeding with loading the campaign, provided the problem type permits it.
  • *
* + *

If the problem type is {@code CANT_LOAD_FROM_NEWER_VERSION}, only the "Cancel" button is available + * because proceeding is not allowed for this issue. For other problem types, both "Cancel" and "Continue" + * buttons are included in the dialog.

+ * + * @param problemType the {@link CampaignProblemType} specifying the nature of the issue, which determines + * the buttons to display * @return a {@link List} of {@link ButtonLabelTooltipPair} objects representing the dialog's buttons */ - private static List createButtons() { + private static List createButtons(CampaignProblemType problemType) { ButtonLabelTooltipPair btnCancel = new ButtonLabelTooltipPair( resources.getString("cancel.button"), null); ButtonLabelTooltipPair btnContinue = new ButtonLabelTooltipPair( resources.getString("continue.button"), null); - return List.of(btnCancel, btnContinue); + if (problemType == CANT_LOAD_FROM_NEWER_VERSION) { + return List.of(btnCancel); + } else { + return List.of(btnCancel, btnContinue); + } } /** @@ -108,6 +119,6 @@ private static String createInCharacterMessage(Campaign campaign, CampaignProble */ private static String createOutOfCharacterMessage(CampaignProblemType problemType) { String typeKey = problemType.toString(); - return resources.getString(typeKey + ".message"); + return resources.getString(typeKey + ".ooc"); } } From edd5b668863f53f0a5f92205682074769360e093 Mon Sep 17 00:00:00 2001 From: IllianiCBT Date: Wed, 15 Jan 2025 14:50:12 -0600 Subject: [PATCH 7/7] Update copyright years to include 2025 Updated copyright headers across multiple files to extend coverage to 2025. This change ensures compliance with proper attribution and copyright practices for the MegaMek and MekHQ project files. --- MekHQ/src/mekhq/campaign/CampaignFactory.java | 2 +- .../gui/dialog/CampaignHasProblemOnLoad.java | 18 ++++++++++++++++++ .../resupplyAndCaches/DialogItinerary.java | 2 +- .../ResupplyDialogUtilities.java | 2 +- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/MekHQ/src/mekhq/campaign/CampaignFactory.java b/MekHQ/src/mekhq/campaign/CampaignFactory.java index f4a08ff6de..4e4e57b7ca 100644 --- a/MekHQ/src/mekhq/campaign/CampaignFactory.java +++ b/MekHQ/src/mekhq/campaign/CampaignFactory.java @@ -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. * diff --git a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java index 57e03041e6..85a5b9939e 100644 --- a/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java +++ b/MekHQ/src/mekhq/gui/dialog/CampaignHasProblemOnLoad.java @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2025 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MekHQ. + * + * MekHQ 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 3 of the License, or + * (at your option) any later version. + * + * MekHQ 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. + * + * You should have received a copy of the GNU General Public License + * along with MekHQ. If not, see . + */ package mekhq.gui.dialog; import megamek.common.annotations.Nullable; diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java index f5143e95c3..d0b7c4aa85 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/DialogItinerary.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. * diff --git a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java index 570582df3b..75aa53c08b 100644 --- a/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java +++ b/MekHQ/src/mekhq/gui/dialog/resupplyAndCaches/ResupplyDialogUtilities.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 - The MegaMek Team. All Rights Reserved. + * Copyright (c) 2024-2025 - The MegaMek Team. All Rights Reserved. * * This file is part of MekHQ. *