diff --git a/MekHQ/resources/mekhq/resources/CampaignGUI.properties b/MekHQ/resources/mekhq/resources/CampaignGUI.properties
index 7264303d81..8775ff3fcd 100644
--- a/MekHQ/resources/mekhq/resources/CampaignGUI.properties
+++ b/MekHQ/resources/mekhq/resources/CampaignGUI.properties
@@ -100,6 +100,7 @@ miMassPersonnelTraining.text=Mass Personnel Training...
miMassPersonnelTraining.toolTipText=This launches the Mass Personnel Training Dialog, which allows you to train large numbers of personnel at the same time.
miScenarioEditor.text=Scenario Template Editor...
miCompanyGenerator.text=Company Generator...
+miAutoResolveBehaviorSettings.text=Auto Resolve Behavior Settings
# Help Menu
menuHelp.text=Help
@@ -177,6 +178,8 @@ btnClearAssignedUnits.toolTipText=Clear all assigned units for this scenario
btnClearAssignedUnits.text=Clear Units
btnResolveScenario.toolTipText=Bring up a wizard that will guide you through the process of resolving this scenario either by MUL files from a MegaMek game or by manually editing for tabletop games.
btnResolveScenario.text=Resolve Manually
+btnAutoResolveScenario.toolTipText=Start a game of MegaMek with all the assigned units played by bots.
At the game's conclusion, you will be presented with a series of dialogs for resolving the scenario.
+btnAutoResolveScenario.text=Auto Resolve
lblMission.text=Current Mission
lblPartsChoice.text=Part Type:
lblPartsChoiceView.text=Part Status:
diff --git a/MekHQ/resources/mekhq/resources/GUI.properties b/MekHQ/resources/mekhq/resources/GUI.properties
index 42f9f15646..5c0960aa76 100644
--- a/MekHQ/resources/mekhq/resources/GUI.properties
+++ b/MekHQ/resources/mekhq/resources/GUI.properties
@@ -452,6 +452,10 @@ CompleteMissionDialog.title=Complete Mission
lblOutcomeStatus.text=Outcome
lblOutcomeStatus.toolTipText=This is the mission's outcome, with Active meaning the mission has not been completed.
+### AutoResolveBehaviorSettingsDialog Class
+AutoResolveBehaviorSettingsDialog.title=Auto Resolve Behavior Settings
+
+
### ContractMarketDialog Class
ContractMarketDialog.title=Contract Market
diff --git a/MekHQ/src/mekhq/AtBGameThread.java b/MekHQ/src/mekhq/AtBGameThread.java
index c677ff356d..8ac4d68dec 100644
--- a/MekHQ/src/mekhq/AtBGameThread.java
+++ b/MekHQ/src/mekhq/AtBGameThread.java
@@ -18,47 +18,32 @@
*/
package mekhq;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.UUID;
-
-import javax.swing.JOptionPane;
-
import io.sentry.Sentry;
import megamek.client.AbstractClient;
import megamek.client.Client;
import megamek.client.bot.BotClient;
+import megamek.client.bot.princess.BehaviorSettings;
import megamek.client.bot.princess.Princess;
+import megamek.client.bot.princess.PrincessException;
import megamek.client.generator.RandomCallsignGenerator;
import megamek.client.ui.swing.ClientGUI;
-import megamek.common.Entity;
-import megamek.common.IAero;
-import megamek.common.Infantry;
-import megamek.common.MapSettings;
-import megamek.common.Minefield;
-import megamek.common.UnitType;
+import megamek.common.*;
import megamek.common.planetaryconditions.PlanetaryConditions;
import megamek.logging.MMLogger;
import mekhq.campaign.force.Force;
import mekhq.campaign.force.Lance;
-import mekhq.campaign.mission.AtBContract;
-import mekhq.campaign.mission.AtBDynamicScenario;
-import mekhq.campaign.mission.AtBScenario;
-import mekhq.campaign.mission.BotForce;
-import mekhq.campaign.mission.Scenario;
+import mekhq.campaign.mission.*;
import mekhq.campaign.personnel.Person;
import mekhq.campaign.unit.Unit;
+import javax.swing.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.stream.Collectors;
+
/**
* Enhanced version of GameThread which imports settings and non-player units
* into the MM game
@@ -69,16 +54,23 @@ public class AtBGameThread extends GameThread {
private static final MMLogger logger = MMLogger.create(AtBGameThread.class);
private final AtBScenario scenario;
+ private final BehaviorSettings autoResolveBehaviorSettings;
public AtBGameThread(String name, String password, Client c, MekHQ app, List units,
AtBScenario scenario) {
- this(name, password, c, app, units, scenario, true);
+ this(name, password, c, app, units, scenario, null, true);
+ }
+
+ public AtBGameThread(String name, String password, Client c, MekHQ app, List units,
+ AtBScenario scenario, BehaviorSettings autoResolveBehaviorSettings) {
+ this(name, password, c, app, units, scenario, autoResolveBehaviorSettings, true);
}
public AtBGameThread(String name, String password, Client c, MekHQ app, List units,
- AtBScenario scenario, boolean started) {
+ AtBScenario scenario, BehaviorSettings autoResolveBehaviorSettings, boolean started) {
super(name, password, c, app, units, scenario, started);
this.scenario = Objects.requireNonNull(scenario);
+ this.autoResolveBehaviorSettings = autoResolveBehaviorSettings;
}
// String tokens for dialog boxes used for transport loading
@@ -118,7 +110,7 @@ public void run() {
while (client.getLocalPlayer() == null) {
Thread.sleep(MekHQ.getMHQOptions().getStartGameClientDelay());
}
-
+ var player = client.getLocalPlayer();
// if game is running, shouldn't do the following, so detect the phase
for (int i = 0; (i < MekHQ.getMHQOptions().getStartGameClientRetryCount())
&& client.getGame().getPhase().isUnknown(); i++) {
@@ -185,23 +177,7 @@ public void run() {
client.sendMapSettings(mapSettings);
Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());
- PlanetaryConditions planetaryConditions = new PlanetaryConditions();
- if (campaign.getCampaignOptions().isUseLightConditions()) {
- planetaryConditions.setLight(scenario.getLight());
- }
- if (campaign.getCampaignOptions().isUseWeatherConditions()) {
- planetaryConditions.setWeather(scenario.getWeather());
- planetaryConditions.setWind(scenario.getWind());
- planetaryConditions.setFog(scenario.getFog());
- planetaryConditions.setEMI(scenario.getEMI());
- planetaryConditions.setBlowingSand(scenario.getBlowingSand());
- planetaryConditions.setTemperature(scenario.getModifiedTemperature());
- }
- if (campaign.getCampaignOptions().isUsePlanetaryConditions()) {
- planetaryConditions.setAtmosphere(scenario.getAtmosphere());
- planetaryConditions.setGravity(scenario.getGravity());
- }
- client.sendPlanetaryConditions(planetaryConditions);
+ client.sendPlanetaryConditions(getPlanetaryConditions());
Thread.sleep(MekHQ.getMHQOptions().getStartGameDelay());
// set player deployment
@@ -392,7 +368,7 @@ public void run() {
}
// All player and bot units have been added to the lobby
- // Prompt the player to auto load units into transports
+ // Prompt the player to autoload units into transports
if (!scenario.getPlayerTransportLinkages().isEmpty()) {
for (UUID id : scenario.getPlayerTransportLinkages().keySet()) {
boolean loadDropShips = false;
@@ -454,6 +430,14 @@ public void run() {
}
}
}
+
+
+ // if AtB was loaded with the auto resolve bot behavior settings then it loads a new bot,
+ // set to the players team
+ // and then moves all the player forces under this new bot
+ if (Objects.nonNull(autoResolveBehaviorSettings)) {
+ setupPlayerBotForAutoResolve(player);
+ }
}
while (!stop) {
@@ -471,6 +455,77 @@ public void run() {
}
}
+ private void setupPlayerBotForAutoResolve(Player player) throws InterruptedException, PrincessException {
+ var botName = player.getName() + ":AI";
+ var autoResolveBot = new BotForce();
+ autoResolveBot.setName(botName);
+
+ Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());
+ var botClient = new Princess(botName, client.getHost(), client.getPort());
+ botClient.setBehaviorSettings(autoResolveBehaviorSettings.getCopy());
+ try {
+ botClient.connect();
+ Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());
+ } catch (Exception e) {
+ Sentry.captureException(e);
+ logger.error(String.format("Could not connect with Bot name %s", botName),
+ e);
+ }
+ swingGui.getLocalBots().put(botName, botClient);
+
+ var retryCount = MekHQ.getMHQOptions().getStartGameBotClientRetryCount();
+ while (botClient.getLocalPlayer() == null) {
+ Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());
+ retryCount--;
+ if (retryCount <= 0) {
+ break;
+ }
+ }
+ if (retryCount <= 0) {
+ logger.error(String.format("Could not connect with Bot name %s", botName));
+ }
+ botClient.getLocalPlayer().setName(botName);
+ botClient.getLocalPlayer().setStartingPos(player.getStartingPos());
+ botClient.getLocalPlayer().setStartOffset(player.getStartOffset());
+ botClient.getLocalPlayer().setStartWidth(player.getStartWidth());
+ botClient.getLocalPlayer().setStartingAnyNWx(player.getStartingAnyNWx());
+ botClient.getLocalPlayer().setStartingAnyNWy(player.getStartingAnyNWy());
+ botClient.getLocalPlayer().setStartingAnySEx(player.getStartingAnySEx());
+ botClient.getLocalPlayer().setStartingAnySEy(player.getStartingAnySEy());
+ botClient.getLocalPlayer().setCamouflage(player.getCamouflage().clone());
+ botClient.getLocalPlayer().setColour(player.getColour());
+ botClient.getLocalPlayer().setTeam(player.getTeam());
+ botClient.sendPlayerInfo();
+ Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());
+
+ var ent = client.getEntitiesVector().stream()
+ .filter(entity -> entity.getOwnerId() == player.getId())
+ .collect(Collectors.toList());
+ botClient.sendChangeOwner(ent, botClient.getLocalPlayer().getId());
+ Thread.sleep(MekHQ.getMHQOptions().getStartGameBotClientDelay());
+
+ }
+
+ private PlanetaryConditions getPlanetaryConditions() {
+ PlanetaryConditions planetaryConditions = new PlanetaryConditions();
+ if (campaign.getCampaignOptions().isUseLightConditions()) {
+ planetaryConditions.setLight(scenario.getLight());
+ }
+ if (campaign.getCampaignOptions().isUseWeatherConditions()) {
+ planetaryConditions.setWeather(scenario.getWeather());
+ planetaryConditions.setWind(scenario.getWind());
+ planetaryConditions.setFog(scenario.getFog());
+ planetaryConditions.setEMI(scenario.getEMI());
+ planetaryConditions.setBlowingSand(scenario.getBlowingSand());
+ planetaryConditions.setTemperature(scenario.getModifiedTemperature());
+ }
+ if (campaign.getCampaignOptions().isUsePlanetaryConditions()) {
+ planetaryConditions.setAtmosphere(scenario.getAtmosphere());
+ planetaryConditions.setGravity(scenario.getGravity());
+ }
+ return planetaryConditions;
+ }
+
/**
* wait for the server to add the bot client, then send starting position,
* camo, and entities
diff --git a/MekHQ/src/mekhq/MekHQ.java b/MekHQ/src/mekhq/MekHQ.java
index 45c0ca79c7..1c82c20309 100644
--- a/MekHQ/src/mekhq/MekHQ.java
+++ b/MekHQ/src/mekhq/MekHQ.java
@@ -26,6 +26,7 @@
import megamek.MegaMek;
import megamek.SuiteConstants;
import megamek.client.Client;
+import megamek.client.bot.princess.BehaviorSettings;
import megamek.client.generator.RandomNameGenerator;
import megamek.client.generator.RandomUnitGenerator;
import megamek.client.ui.preferences.PreferencesNode;
@@ -374,6 +375,11 @@ public void joinGame(Scenario scenario, List meks) {
}
public void startHost(Scenario scenario, boolean loadSavegame, List meks) {
+ startHost(scenario, loadSavegame, meks, null);
+ }
+
+ public void startHost(Scenario scenario, boolean loadSavegame, List meks, BehaviorSettings autoResolveBehaviorSettings)
+ {
HostDialog hostDialog = new HostDialog(campaignGUI.getFrame(), getCampaign().getName());
hostDialog.setVisible(true);
@@ -426,7 +432,7 @@ public void startHost(Scenario scenario, boolean loadSavegame, List meks)
// Start the game thread
if (getCampaign().getCampaignOptions().isUseAtB() && (scenario instanceof AtBScenario)) {
- gameThread = new AtBGameThread(playerName, password, client, this, meks, (AtBScenario) scenario);
+ gameThread = new AtBGameThread(playerName, password, client, this, meks, (AtBScenario) scenario, autoResolveBehaviorSettings);
} else {
gameThread = new GameThread(playerName, password, client, this, meks, scenario);
}
diff --git a/MekHQ/src/mekhq/campaign/Campaign.java b/MekHQ/src/mekhq/campaign/Campaign.java
index 605de15616..6a6c8c2144 100644
--- a/MekHQ/src/mekhq/campaign/Campaign.java
+++ b/MekHQ/src/mekhq/campaign/Campaign.java
@@ -21,6 +21,8 @@
*/
package mekhq.campaign;
+import megamek.client.bot.princess.BehaviorSettings;
+import megamek.client.bot.princess.BehaviorSettingsFactory;
import megamek.client.generator.RandomGenderGenerator;
import megamek.client.generator.RandomNameGenerator;
import megamek.client.generator.RandomUnitGenerator;
@@ -268,6 +270,7 @@ public class Campaign implements ITechManager {
private final Quartermaster quartermaster;
private StoryArc storyArc;
private FameAndInfamyController fameAndInfamy;
+ private BehaviorSettings autoResolveBehaviorSettings;
private final transient ResourceBundle resources = ResourceBundle.getBundle("mekhq.resources.Campaign",
MekHQ.getMHQOptions().getLocale());
@@ -335,6 +338,7 @@ public Campaign() {
quartermaster = new Quartermaster(this);
fieldKitchenWithinCapacity = false;
fameAndInfamy = new FameAndInfamyController();
+ autoResolveBehaviorSettings = BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR;
}
/**
@@ -5488,6 +5492,7 @@ public void writeToXML(final PrintWriter pw) {
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "shipSearchType", shipSearchType);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "shipSearchResult", shipSearchResult);
MHQXMLUtility.writeSimpleXMLTag(pw, indent, "shipSearchExpiration", getShipSearchExpiration());
+ MHQXMLUtility.writeSimpleXMLTag(pw, indent, "autoResolveBehaviorSettings", autoResolveBehaviorSettings.getDescription());
}
retirementDefectionTracker.writeToXML(pw, indent);
@@ -8374,4 +8379,13 @@ public boolean useVariableTechLevel() {
public boolean showExtinct() {
return !campaignOptions.isDisallowExtinctStuff();
}
+
+ public BehaviorSettings getAutoResolveBehaviorSettings() {
+ return autoResolveBehaviorSettings;
+ }
+
+ public void setAutoResolveBehaviorSettings(BehaviorSettings settings) {
+ autoResolveBehaviorSettings = settings;
+ }
+
}
diff --git a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
index e9ddb64378..1291e5d5f7 100644
--- a/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
+++ b/MekHQ/src/mekhq/campaign/io/CampaignXmlParser.java
@@ -19,6 +19,7 @@
package mekhq.campaign.io;
import megamek.Version;
+import megamek.client.bot.princess.BehaviorSettingsFactory;
import megamek.client.generator.RandomGenderGenerator;
import megamek.client.generator.RandomNameGenerator;
import megamek.client.ui.swing.util.PlayerColour;
@@ -77,6 +78,8 @@
import java.util.*;
import java.util.Map.Entry;
+import static mekhq.utilities.MoreObjects.firstNonNull;
+
public class CampaignXmlParser {
private final InputStream is;
private final MekHQ app;
@@ -292,6 +295,11 @@ public Campaign parse() throws CampaignXmlParseException, NullEntityException {
retVal.setShipSearchResult(wn.getTextContent());
} else if (xn.equalsIgnoreCase("shipSearchExpiration")) {
retVal.setShipSearchExpiration(MHQXMLUtility.parseDate(wn.getTextContent().trim()));
+ } else if (xn.equalsIgnoreCase("autoResolveBehaviorSettings")) {
+ retVal.setAutoResolveBehaviorSettings(
+ firstNonNull(BehaviorSettingsFactory.getInstance().getBehavior(wn.getTextContent()),
+ BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR)
+ );
} else if (xn.equalsIgnoreCase("customPlanetaryEvents")) {
updatePlanetaryEventsFromXML(wn);
}
diff --git a/MekHQ/src/mekhq/gui/BriefingTab.java b/MekHQ/src/mekhq/gui/BriefingTab.java
index 741bb29fc2..ca96aadcc8 100644
--- a/MekHQ/src/mekhq/gui/BriefingTab.java
+++ b/MekHQ/src/mekhq/gui/BriefingTab.java
@@ -18,6 +18,7 @@
*/
package mekhq.gui;
+import megamek.client.bot.princess.BehaviorSettings;
import megamek.client.generator.ReconfigurationParameters;
import megamek.client.generator.TeamLoadOutGenerator;
import megamek.client.ui.baseComponents.MMComboBox;
@@ -94,6 +95,7 @@ public final class BriefingTab extends CampaignGuiTab {
private JButton btnGetMul;
private JButton btnClearAssignedUnits;
private JButton btnResolveScenario;
+ private JButton btnAutoResolveScenario;
private ScenarioTableModel scenarioModel;
@@ -267,6 +269,12 @@ public void initTab() {
btnResolveScenario.setEnabled(false);
panScenarioButtons.add(btnResolveScenario);
+ btnAutoResolveScenario = new JButton(resourceMap.getString("btnAutoResolveScenario.text"));
+ btnAutoResolveScenario.setToolTipText(resourceMap.getString("btnAutoResolveScenario.toolTipText"));
+ btnAutoResolveScenario.addActionListener(ev -> autoResolveScenario());
+ btnAutoResolveScenario.setEnabled(false);
+ panScenarioButtons.add(btnAutoResolveScenario);
+
btnClearAssignedUnits = new JButton(resourceMap.getString("btnClearAssignedUnits.text"));
btnClearAssignedUnits.setToolTipText(resourceMap.getString("btnClearAssignedUnits.toolTipText"));
btnClearAssignedUnits.addActionListener(ev -> clearAssignedUnits());
@@ -842,6 +850,14 @@ private void loadScenario() {
}
private void startScenario() {
+ startScenario(null);
+ }
+
+ private void autoResolveScenario() {
+ startScenario(getCampaign().getAutoResolveBehaviorSettings());
+ }
+
+ private void startScenario(BehaviorSettings autoResolveBehaviorSettings) {
int row = scenarioTable.getSelectedRow();
if (row < 0) {
return;
@@ -927,7 +943,9 @@ private void startScenario() {
// Ensure that the MegaMek year GameOption matches the campaign year
getCampaign().getGameOptions().getOption(OptionsConstants.ALLOWED_YEAR)
.setValue(getCampaign().getGameYear());
- getCampaignGui().getApplication().startHost(scenario, false, chosen);
+ getCampaignGui().getApplication()
+ .startHost(scenario, false, chosen, autoResolveBehaviorSettings);
+
}
}
@@ -1242,6 +1260,7 @@ public void refreshScenarioView() {
btnGetMul.setEnabled(false);
btnClearAssignedUnits.setEnabled(false);
btnResolveScenario.setEnabled(false);
+ btnAutoResolveScenario.setEnabled(false);
btnPrintRS.setEnabled(false);
selectedScenario = -1;
return;
@@ -1269,6 +1288,7 @@ public void refreshScenarioView() {
btnGetMul.setEnabled(canStartGame);
btnClearAssignedUnits.setEnabled(canStartGame);
btnResolveScenario.setEnabled(canStartGame);
+ btnAutoResolveScenario.setEnabled(canStartGame);
btnPrintRS.setEnabled(canStartGame);
}
diff --git a/MekHQ/src/mekhq/gui/CampaignGUI.java b/MekHQ/src/mekhq/gui/CampaignGUI.java
index bd8c2a55bd..147fd8b65a 100644
--- a/MekHQ/src/mekhq/gui/CampaignGUI.java
+++ b/MekHQ/src/mekhq/gui/CampaignGUI.java
@@ -1012,6 +1012,19 @@ private void initMenu() {
.addActionListener(evt -> new CompanyGenerationDialog(getFrame(), getCampaign()).setVisible(true));
menuManage.add(miCompanyGenerator);
+ JMenuItem miAutoResolveBehaviorEditor = new JMenuItem(resourceMap.getString("miAutoResolveBehaviorSettings.text"));
+ miAutoResolveBehaviorEditor.setMnemonic(KeyEvent.VK_T);
+ miAutoResolveBehaviorEditor.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, InputEvent.ALT_DOWN_MASK));
+ miAutoResolveBehaviorEditor
+ .addActionListener(evt -> {
+ var autoResolveBehaviorSettingsDialog = new AutoResolveBehaviorSettingsDialog(getFrame(), getCampaign());
+ autoResolveBehaviorSettingsDialog.setVisible(true);
+ autoResolveBehaviorSettingsDialog.setModal(true);
+ autoResolveBehaviorSettingsDialog.pack();
+ });
+
+ menuManage.add(miAutoResolveBehaviorEditor);
+
menuBar.add(menuManage);
// endregion Manage Campaign Menu
@@ -1614,7 +1627,7 @@ public void refitUnit(Refit r, boolean selectModelName) {
StringBuilder nameBuilder = new StringBuilder(128);
nameBuilder.append("")
.append(tech.getFullName())
- .append(", ")
+ .append(", ")
.append(SkillType.getColoredExperienceLevelName(tech.getSkillLevel(getCampaign(), false)))
.append(" ")
.append(tech.getPrimaryRoleDesc())
diff --git a/MekHQ/src/mekhq/gui/dialog/AutoResolveBehaviorSettingsDialog.java b/MekHQ/src/mekhq/gui/dialog/AutoResolveBehaviorSettingsDialog.java
new file mode 100644
index 0000000000..724b04b351
--- /dev/null
+++ b/MekHQ/src/mekhq/gui/dialog/AutoResolveBehaviorSettingsDialog.java
@@ -0,0 +1,560 @@
+package mekhq.gui.dialog;
+
+import megamek.client.bot.princess.BehaviorSettings;
+import megamek.client.bot.princess.BehaviorSettingsFactory;
+import megamek.client.bot.princess.CardinalEdge;
+import megamek.client.bot.princess.PrincessException;
+import megamek.client.ui.Messages;
+import megamek.client.ui.baseComponents.MMComboBox;
+import megamek.client.ui.dialogs.helpDialogs.PrincessHelpDialog;
+import megamek.client.ui.swing.MMToggleButton;
+import megamek.client.ui.swing.util.ScalingPopup;
+import megamek.client.ui.swing.util.UIUtil;
+import megamek.logging.MMLogger;
+import mekhq.campaign.Campaign;
+import mekhq.gui.baseComponents.AbstractMHQDialog;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import java.awt.*;
+import java.awt.event.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class AutoResolveBehaviorSettingsDialog
+ extends AbstractMHQDialog
+ implements ActionListener, ListSelectionListener, ChangeListener
+{
+ private final static MMLogger logger = MMLogger.create(AutoResolveBehaviorSettingsDialog.class);
+
+ private static final String OK_ACTION = "Ok_Action";
+
+ private final transient BehaviorSettingsFactory behaviorSettingsFactory = BehaviorSettingsFactory.getInstance();
+ private BehaviorSettings autoResolveBehavior;
+
+ private final JLabel nameLabel = new JLabel(Messages.getString("BotConfigDialog.nameLabel"));
+ private final UIUtil.TipTextField nameField = new UIUtil.TipTextField("", 16);
+
+ private final MMToggleButton forcedWithdrawalCheck = new UIUtil.TipMMToggleButton(
+ Messages.getString("BotConfigDialog.forcedWithdrawalCheck"));
+ private final JLabel withdrawEdgeLabel = new JLabel(Messages.getString("BotConfigDialog.retreatEdgeLabel"));
+ private final MMComboBox withdrawEdgeCombo = new UIUtil.TipCombo<>("EdgeToWithdraw", CardinalEdge.values());
+ private final MMToggleButton autoFleeCheck = new UIUtil.TipMMToggleButton(Messages.getString("BotConfigDialog.autoFleeCheck"));
+ private final JLabel fleeEdgeLabel = new JLabel(Messages.getString("BotConfigDialog.homeEdgeLabel"));
+ private final MMComboBox fleeEdgeCombo = new UIUtil.TipCombo<>("EdgeToFlee", CardinalEdge.values());
+
+ private final UIUtil.TipSlider aggressionSlidebar = new UIUtil.TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5);
+ private final UIUtil.TipSlider fallShameSlidebar = new UIUtil.TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5);
+ private final UIUtil.TipSlider herdingSlidebar = new UIUtil.TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5);
+ private final UIUtil.TipSlider selfPreservationSlidebar = new UIUtil.TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5);
+ private final UIUtil.TipSlider braverySlidebar = new UIUtil.TipSlider(SwingConstants.HORIZONTAL, 0, 10, 5);
+// private final UIUtil.TipButton savePreset = new UIUtil.TipButton(Messages.getString("BotConfigDialog.save"));
+ private final UIUtil.TipButton saveNewPreset = new UIUtil.TipButton(Messages.getString("BotConfigDialog.saveNew"));
+
+ private final JButton princessHelpButton = new JButton(Messages.getString("BotConfigDialog.help"));
+
+ private JPanel presetsPanel;
+ private final JLabel chooseLabel = new JLabel(Messages.getString("BotConfigDialog.behaviorNameLabel"));
+ /**
+ * A copy of the current presets. Modifications will only be saved when
+ * accepted.
+ */
+ private List presets;
+ private final AutoResolveBehaviorSettingsDialog.PresetsModel presetsModel = new AutoResolveBehaviorSettingsDialog.PresetsModel();
+ private final JList presetsList = new JList<>(presetsModel);
+
+ private final JButton butOK = new JButton(Messages.getString("Okay"));
+ private final JButton butCancel = new JButton(Messages.getString("Cancel"));
+
+ /**
+ * Stores the currently chosen preset. Used to detect if the player has changed
+ * the sliders.
+ */
+ private BehaviorSettings chosenPreset;
+ private Campaign campaign;
+
+ //region Constructors
+ public AutoResolveBehaviorSettingsDialog(final JFrame frame, final Campaign campaign) {
+ super(frame, "AutoResolveBehaviorSettingsDialog", "AutoResolveBehaviorSettingsDialog.title");
+ setAlwaysOnTop(true);
+ setCampaign(campaign);
+ autoResolveBehavior = (
+ campaign.getAutoResolveBehaviorSettings() != null ?
+ campaign.getAutoResolveBehaviorSettings() : new BehaviorSettings());
+ updatePresets();
+ initialize();
+ updateDialogFields();
+ }
+
+ private String getAutoResolveBehaviorSettingName() {
+ return campaign.getName() + ":AI";
+ }
+
+ public void setCampaign(final Campaign campaign) {
+ this.campaign = campaign;
+ }
+
+ @Override
+ protected void initialize() {
+ // Make Enter confirm and close the dialog
+ final KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
+ getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(enter, OK_ACTION);
+ getRootPane().getInputMap(JComponent.WHEN_FOCUSED).put(enter, OK_ACTION);
+ getRootPane().getActionMap().put(OK_ACTION, new AbstractAction() {
+ @Override
+ public void actionPerformed(ActionEvent evt) {
+ okAction();
+ }
+ });
+ super.initialize();
+ }
+
+ @Override
+ protected Container createCenterPane() {
+ JPanel result = new JPanel();
+ result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
+ result.add(nameSection());
+ result.add(settingSection());
+ return result;
+ }
+
+ /**
+ * The setting section contains the presets list on the left side and the
+ * princess settings on the right.
+ */
+ private JPanel settingSection() {
+// var princessScroll = new JScrollPane(princessPanel());
+// princessScroll.getVerticalScrollBar().setUnitIncrement(16);
+// princessScroll.setBorder(null);
+// presetsPanel = presetsPanel();
+
+ var result = new JPanel(new BorderLayout(0, 0));
+ result.setAlignmentX(LEFT_ALIGNMENT);
+ result.add(princessPanel(), BorderLayout.CENTER);
+// result.add(presetsPanel, BorderLayout.LINE_START);
+ return result;
+ }
+
+ /** The princess panel contains the individual princess settings. */
+ private JPanel princessPanel() {
+ JPanel result = new JPanel();
+ result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
+ result.add(behaviorSection());
+// result.add(retreatSection());
+ result.add(createButtonPanel());
+ return result;
+ }
+
+ private JPanel nameSection() {
+ JPanel result = new JPanel();
+ result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
+ result.setBorder(BorderFactory.createEmptyBorder(5, 0, 5, 0));
+ UIUtil.Content panContent = new UIUtil.Content();
+ panContent.setLayout(new BoxLayout(panContent, BoxLayout.PAGE_AXIS));
+ result.add(panContent);
+
+ var namePanel = new JPanel();
+ nameField.setToolTipText(Messages.getString("BotConfigDialog.namefield.tooltip"));
+ // When the dialog configures an existing player, the name must not be changed
+ nameField.setText(getAutoResolveBehaviorSettingName());
+ nameField.setEnabled(false);
+ nameLabel.setLabelFor(nameField);
+ nameLabel.setDisplayedMnemonic(KeyEvent.VK_N);
+ namePanel.add(nameLabel);
+ namePanel.add(nameField);
+
+ panContent.add(namePanel);
+ return result;
+ }
+
+ /** The presets panel has a list of behavior presets for Princess. */
+ private JPanel presetsPanel() {
+ var result = new JPanel();
+ result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
+ result.setBorder(new EmptyBorder(0, 10, 0, 20));
+
+ chooseLabel.setAlignmentX(CENTER_ALIGNMENT);
+ chooseLabel.setDisplayedMnemonic(KeyEvent.VK_P);
+ chooseLabel.setLabelFor(presetsList);
+ var headerPanel = new UIUtil.FixedYPanel();
+ headerPanel.add(chooseLabel);
+
+ presetsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ presetsList.addListSelectionListener(this);
+ presetsList.setCellRenderer(new PresetsRenderer());
+ presetsList.addMouseListener(presetsMouseListener);
+
+ result.add(headerPanel);
+ result.add(Box.createVerticalStrut(10));
+ result.add(presetsList);
+
+ return result;
+ }
+
+ private JPanel behaviorSection() {
+ JPanel result = new UIUtil.OptionPanel("BotConfigDialog.behaviorSection");
+ UIUtil.Content panContent = new UIUtil.Content();
+ panContent.setLayout(new BoxLayout(panContent, BoxLayout.PAGE_AXIS));
+ result.add(panContent);
+
+ panContent.add(buildSlider(braverySlidebar, Messages.getString("BotConfigDialog.braverySliderMin"),
+ Messages.getString("BotConfigDialog.braverySliderMax"),
+ Messages.getString("BotConfigDialog.braveryTooltip"),
+ Messages.getString("BotConfigDialog.braverySliderTitle")));
+ panContent.add(Box.createVerticalStrut(7));
+
+ panContent.add(
+ buildSlider(selfPreservationSlidebar, Messages.getString("BotConfigDialog.selfPreservationSliderMin"),
+ Messages.getString("BotConfigDialog.selfPreservationSliderMax"),
+ Messages.getString("BotConfigDialog.selfPreservationTooltip"),
+ Messages.getString("BotConfigDialog.selfPreservationSliderTitle")));
+ panContent.add(Box.createVerticalStrut(7));
+
+ panContent.add(buildSlider(aggressionSlidebar, Messages.getString("BotConfigDialog.aggressionSliderMin"),
+ Messages.getString("BotConfigDialog.aggressionSliderMax"),
+ Messages.getString("BotConfigDialog.aggressionTooltip"),
+ Messages.getString("BotConfigDialog.aggressionSliderTitle")));
+ panContent.add(Box.createVerticalStrut(7));
+
+ panContent.add(buildSlider(herdingSlidebar, Messages.getString("BotConfigDialog.herdingSliderMin"),
+ Messages.getString("BotConfigDialog.herdingSliderMax"),
+ Messages.getString("BotConfigDialog.herdingToolTip"),
+ Messages.getString("BotConfigDialog.herdingSliderTitle")));
+ panContent.add(Box.createVerticalStrut(7));
+
+ panContent.add(buildSlider(fallShameSlidebar, Messages.getString("BotConfigDialog.fallShameSliderMin"),
+ Messages.getString("BotConfigDialog.fallShameSliderMax"),
+ Messages.getString("BotConfigDialog.fallShameToolTip"),
+ Messages.getString("BotConfigDialog.fallShameSliderTitle")));
+
+ var buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 10));
+ buttonPanel.setAlignmentX(SwingConstants.CENTER);
+ result.add(buttonPanel);
+
+// savePreset.addActionListener(this);
+// savePreset.setMnemonic(KeyEvent.VK_S);
+// savePreset.setToolTipText(Messages.getString("BotConfigDialog.saveTip"));
+// buttonPanel.add(savePreset);
+// saveNewPreset.addActionListener(this);
+// saveNewPreset.setMnemonic(KeyEvent.VK_A);
+// saveNewPreset.setToolTipText(Messages.getString("BotConfigDialog.saveNewTip"));
+// buttonPanel.add(saveNewPreset);
+
+ return result;
+ }
+
+
+ private JPanel retreatSection() {
+ JPanel result = new UIUtil.OptionPanel("BotConfigDialog.retreatSection");
+ UIUtil.Content panContent = new UIUtil.Content();
+ panContent.setLayout(new BoxLayout(panContent, BoxLayout.PAGE_AXIS));
+ result.add(panContent);
+
+ autoFleeCheck.setToolTipText(Messages.getString("BotConfigDialog.autoFleeTooltip"));
+ autoFleeCheck.addActionListener(this);
+ autoFleeCheck.setMnemonic(KeyEvent.VK_F);
+
+ fleeEdgeCombo.removeItem(CardinalEdge.NONE);
+ fleeEdgeCombo.setToolTipText(Messages.getString("BotConfigDialog.homeEdgeTooltip"));
+ fleeEdgeCombo.setSelectedIndex(0);
+ fleeEdgeCombo.addActionListener(this);
+
+ forcedWithdrawalCheck.setToolTipText(Messages.getString("BotConfigDialog.forcedWithdrawalTooltip"));
+ forcedWithdrawalCheck.addActionListener(this);
+ forcedWithdrawalCheck.setMnemonic(KeyEvent.VK_W);
+
+ withdrawEdgeCombo.removeItem(CardinalEdge.NONE);
+ withdrawEdgeCombo.setToolTipText(Messages.getString("BotConfigDialog.retreatEdgeTooltip"));
+ withdrawEdgeCombo.setSelectedIndex(0);
+
+ var firstLine = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ var secondLine = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ firstLine.add(forcedWithdrawalCheck);
+ firstLine.add(Box.createHorizontalStrut(20));
+ firstLine.add(withdrawEdgeLabel);
+ firstLine.add(withdrawEdgeCombo);
+ secondLine.add(autoFleeCheck);
+ secondLine.add(Box.createHorizontalStrut(20));
+ secondLine.add(fleeEdgeLabel);
+ secondLine.add(fleeEdgeCombo);
+ panContent.add(firstLine);
+ panContent.add(Box.createVerticalStrut(5));
+ panContent.add(secondLine);
+
+ return result;
+ }
+
+ protected void updatePresetFields() {
+ selfPreservationSlidebar.setValue(autoResolveBehavior.getSelfPreservationIndex());
+ aggressionSlidebar.setValue(autoResolveBehavior.getHyperAggressionIndex());
+ fallShameSlidebar.setValue(autoResolveBehavior.getFallShameIndex());
+ herdingSlidebar.setValue(autoResolveBehavior.getHerdMentalityIndex());
+ braverySlidebar.setValue(autoResolveBehavior.getBraveryIndex());
+ }
+
+ private void updateDialogFields() {
+ updatePresetFields();
+
+ forcedWithdrawalCheck.setSelected(autoResolveBehavior.isForcedWithdrawal());
+ withdrawEdgeCombo.setSelectedItem(autoResolveBehavior.getRetreatEdge());
+
+ autoFleeCheck.setSelected(autoResolveBehavior.shouldAutoFlee());
+ fleeEdgeCombo.setSelectedItem(autoResolveBehavior.getDestinationEdge());
+
+ updateEnabledStates();
+ }
+
+ /** Updates all necessary enabled states of buttons/dropdowns. */
+ private void updateEnabledStates() {
+ fleeEdgeLabel.setEnabled(autoFleeCheck.isSelected());
+ fleeEdgeCombo.setEnabled(autoFleeCheck.isSelected());
+ withdrawEdgeLabel.setEnabled(forcedWithdrawalCheck.isSelected());
+ withdrawEdgeCombo.setEnabled(forcedWithdrawalCheck.isSelected());
+// savePreset.setEnabled(isChangedPreset());
+ }
+
+ /**
+ * Returns true if a preset is selected and is different from the current slider
+ * settings.
+ */
+ private boolean isChangedPreset() {
+ return (chosenPreset != null)
+ && (chosenPreset.getSelfPreservationIndex() != selfPreservationSlidebar.getValue()
+ || chosenPreset.getHyperAggressionIndex() != aggressionSlidebar.getValue()
+ || chosenPreset.getFallShameIndex() != fallShameSlidebar.getValue()
+ || chosenPreset.getHerdMentalityIndex() != herdingSlidebar.getValue()
+ || chosenPreset.getBraveryIndex() != braverySlidebar.getValue());
+ }
+
+ private JPanel buildSlider(JSlider thisSlider, String minMsgProperty,
+ String maxMsgProperty, String toolTip, String title) {
+ TitledBorder border = BorderFactory.createTitledBorder(title);
+ border.setTitlePosition(TitledBorder.TOP);
+ border.setTitleJustification(TitledBorder.CENTER);
+ var result = new UIUtil.TipPanel();
+ result.setBorder(border);
+ result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
+ result.setToolTipText(toolTip);
+ thisSlider.setToolTipText(toolTip);
+ thisSlider.setPaintLabels(false);
+ thisSlider.setSnapToTicks(true);
+ thisSlider.addChangeListener(this);
+
+ var panLabels = new JPanel();
+ panLabels.setLayout(new BoxLayout(panLabels, BoxLayout.LINE_AXIS));
+ panLabels.add(new JLabel(minMsgProperty, SwingConstants.LEFT));
+ panLabels.add(Box.createHorizontalGlue());
+ panLabels.add(new JLabel(maxMsgProperty, SwingConstants.RIGHT));
+
+ result.add(panLabels);
+ result.add(thisSlider);
+ result.revalidate();
+ return result;
+ }
+
+ protected JPanel createButtonPanel() {
+ JPanel result = new JPanel(new FlowLayout(FlowLayout.CENTER, 20, 10));
+
+ butOK.addActionListener((l) -> {
+ okAction();
+ setVisible(false);
+ });
+ butOK.setMnemonic(KeyEvent.VK_K);
+ result.add(butOK);
+
+ butCancel.addActionListener(this::cancelActionPerformed);
+ butCancel.setMnemonic(KeyEvent.VK_C);
+ result.add(butCancel);
+
+ princessHelpButton.addActionListener(this);
+ princessHelpButton.setMnemonic(KeyEvent.VK_H);
+ result.add(princessHelpButton);
+
+ return result;
+ }
+
+ private void showPrincessHelp() {
+ new PrincessHelpDialog(getFrame()).setVisible(true);
+ }
+
+ private void okAction() {
+ try {
+ savePrincessProperties();
+ } catch (PrincessException e) {
+ logger.error("Error saving AutoResolveBehaviorSettings properties", e);
+ }
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (e.getSource() == princessHelpButton) {
+ showPrincessHelp();
+ }
+ }
+
+ /** Saves the current Behavior to the currently selected Behavior Preset. */
+ private void savePreset() {
+ writePreset(getAutoResolveBehaviorSettingName());
+ }
+
+ /** Removes the given Behavior Preset. */
+ private void removePreset(String name) {
+ behaviorSettingsFactory.removeBehavior(name);
+ behaviorSettingsFactory.saveBehaviorSettings(false);
+ updatePresets();
+ }
+
+ private void savePrincessProperties() throws PrincessException {
+ BehaviorSettings tempBehavior = new BehaviorSettings();
+ tempBehavior.setFallShameIndex(fallShameSlidebar.getValue());
+ tempBehavior.setForcedWithdrawal(forcedWithdrawalCheck.isSelected());
+ tempBehavior.setAutoFlee(autoFleeCheck.isSelected());
+ tempBehavior.setDestinationEdge(fleeEdgeCombo.getSelectedItem());
+ tempBehavior.setRetreatEdge(withdrawEdgeCombo.getSelectedItem());
+ tempBehavior.setHyperAggressionIndex(aggressionSlidebar.getValue());
+ tempBehavior.setSelfPreservationIndex(selfPreservationSlidebar.getValue());
+ tempBehavior.setHerdMentalityIndex(herdingSlidebar.getValue());
+ tempBehavior.setBraveryIndex(braverySlidebar.getValue());
+ tempBehavior.setDescription(getAutoResolveBehaviorSettingName());
+ autoResolveBehavior = tempBehavior;
+ campaign.setAutoResolveBehaviorSettings(tempBehavior);
+ savePreset();
+ }
+
+ private void writePreset(String name) {
+ BehaviorSettings newBehavior = new BehaviorSettings();
+ try {
+ newBehavior.setDescription(name);
+ } catch (PrincessException e1) {
+ return;
+ }
+ newBehavior.setFallShameIndex(fallShameSlidebar.getValue());
+ newBehavior.setHyperAggressionIndex(aggressionSlidebar.getValue());
+ newBehavior.setSelfPreservationIndex(selfPreservationSlidebar.getValue());
+ newBehavior.setHerdMentalityIndex(herdingSlidebar.getValue());
+ newBehavior.setBraveryIndex(braverySlidebar.getValue());
+ behaviorSettingsFactory.addBehavior(newBehavior);
+ behaviorSettingsFactory.saveBehaviorSettings(false);
+ }
+
+ @Override
+ public void valueChanged(ListSelectionEvent event) {
+ if (event.getValueIsAdjusting()) {
+ return;
+ }
+
+ if (event.getSource() == presetsList) {
+ presetSelected();
+ }
+ }
+
+ /** Shows a popup menu for a behavior preset, allowing to delete it. */
+ private transient MouseListener presetsMouseListener = new MouseAdapter() {
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ int row = presetsList.locationToIndex(e.getPoint());
+ if (e.isPopupTrigger() && (row != -1)) {
+ ScalingPopup popup = new ScalingPopup();
+ String behavior = presetsList.getModel().getElementAt(row);
+ var deleteItem = new JMenuItem("Delete " + behavior);
+ deleteItem.addActionListener(event -> removePreset(behavior));
+ popup.add(deleteItem);
+ popup.show(e.getComponent(), e.getX(), e.getY());
+ }
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent evt) {
+ if (SwingUtilities.isLeftMouseButton(evt) && evt.getClickCount() == 1) {
+ presetSelected();
+ }
+ }
+ };
+
+ /**
+ * Called when a Preset is selected. This will often be called twice when
+ * clicking with the mouse (by the listselectionlistener and the mouselistener).
+ * In this way the list will react when copying a Preset from another bot and
+ * then clicking the already selected Preset again. And it will also react to
+ * keyboard navigation.
+ */
+ private void presetSelected() {
+ if (presetsList.isSelectionEmpty()) {
+ chosenPreset = null;
+ } else {
+ autoResolveBehavior = behaviorSettingsFactory.getBehavior(presetsList.getSelectedValue());
+ chosenPreset = behaviorSettingsFactory.getBehavior(presetsList.getSelectedValue());
+
+ if (autoResolveBehavior == null) {
+ autoResolveBehavior = new BehaviorSettings();
+ }
+ updatePresetFields();
+ }
+ updateEnabledStates();
+ }
+
+ /**
+ * Sets up/Updates the displayed preset list (e.g. after adding or deleting a
+ * preset)
+ */
+ private void updatePresets() {
+ presets = new ArrayList<>(Arrays.asList(behaviorSettingsFactory.getBehaviorNames()));
+ ((AutoResolveBehaviorSettingsDialog.PresetsModel) presetsList.getModel()).fireUpdate();
+ }
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateEnabledStates();
+ }
+
+ private class PresetsModel extends DefaultListModel {
+
+ @Override
+ public int getSize() {
+ return presets.size();
+ }
+
+ @Override
+ public String getElementAt(int index) {
+ return presets.get(index);
+ }
+
+ /** Call when elements of the list change. */
+ private void fireUpdate() {
+ fireContentsChanged(this, 0, getSize() - 1);
+ }
+ }
+
+ /**
+ * A renderer for the Behavior Presets list. Adapts the font size to the gui
+ * scaling and
+ * colors the special list elements (other bot Configurations and original
+ * Config).
+ */
+ private class PresetsRenderer extends DefaultListCellRenderer {
+
+ @Override
+ public Component getListCellRendererComponent(JList> list, Object value, int index, boolean isSelected,
+ boolean cellHasFocus) {
+ Component comp = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+ comp.setFont(UIUtil.getScaledFont());
+ String preset = (String) value;
+ if (preset.startsWith(UIUtil.BOT_MARKER)) {
+ comp.setForeground(UIUtil.uiLightBlue());
+ }
+
+ if (preset.equals(Messages.getString("BotConfigDialog.previousConfig"))) {
+ comp.setForeground(UIUtil.uiGreen());
+ }
+
+ return comp;
+ }
+ }
+}
diff --git a/MekHQ/src/mekhq/utilities/MoreObjects.java b/MekHQ/src/mekhq/utilities/MoreObjects.java
new file mode 100644
index 0000000000..088b361cad
--- /dev/null
+++ b/MekHQ/src/mekhq/utilities/MoreObjects.java
@@ -0,0 +1,12 @@
+package mekhq.utilities;
+
+public class MoreObjects {
+
+ private MoreObjects() {
+ }
+
+ public static O firstNonNull(O first, O second) {
+ return first != null ? first : second;
+ }
+}
+