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; + } +} +