Skip to content

Commit

Permalink
Merge pull request #5211 from SJuliez/scenario-loader
Browse files Browse the repository at this point in the history
Dialog for choosing scenarios
  • Loading branch information
SJuliez authored Mar 9, 2024
2 parents 49db1b5 + 922a9a9 commit ebc08e3
Show file tree
Hide file tree
Showing 19 changed files with 845 additions and 109 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

MMSVersion=1

Name=Instant Fame:Northwind Highlanders
Name=Instant Fame: Northwind Highlanders
Description=This scenario visits a fight of McCormack's Fusiliers in the campaign on Bellatrix against Ajax's Avengers. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.

Description=The campaign on Bellatrix against Ajax's Avengers brought McCormack's Fusiliers instant fame. Arriving to find themselves outnumbered three-to-one, the Fusiliers quickly used their superior grasp of tactics and their ferocity to smash the defending unit. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.
#FullDescription=The campaign on Bellatrix against Ajax's Avengers brought McCormack's Fusiliers instant fame. Arriving to find themselves outnumbered three-to-one, the Fusiliers quickly used their superior grasp of tactics and their ferocity to smash the defending unit. The retreating Avengers tried deperately to shake the Highlanders as they fled east across the southern continent of Bellatrix, but to no avail. Exhausted and bogged down by bad weather, the Avengers had no choice but to stand and fight.

BoardWidth=2
BoardHeight=1
Expand Down
3 changes: 3 additions & 0 deletions megamek/i18n/megamek/client/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4539,3 +4539,6 @@ CMVPanel.font=Font:
Gamemaster.Gamemaster=Gamemaster
Gamemaster.EditDamage=Edit Damage
Gamemaster.Configure=Configure

# Scenario Chooser
ScenarioChooser.title=Choose Scenario
1 change: 1 addition & 0 deletions megamek/src/megamek/SuiteConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public abstract class SuiteConstants {

//region File Formats
public static final String TRUETYPE_FONT = ".ttf";
public static final String SCENARIO_EXT = ".mms";
//endregion File Formats

//region File Paths
Expand Down
4 changes: 4 additions & 0 deletions megamek/src/megamek/client/ui/swing/CommonSettingsDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -3111,6 +3111,10 @@ public static List<String> filteredFiles(File path, String fileEnding) {
}

public static List<String> filteredFilesWithSubDirs(File path, String fileEnding) {
if (!path.exists()) {
LogManager.getLogger().warn("Path " + path + " does not exist.");
return new ArrayList<>();
}
try (Stream<Path> entries = Files.walk(path.toPath())) {
return entries.map(Objects::toString).filter(name -> name.endsWith(fileEnding)).collect(toList());
} catch (IOException e) {
Expand Down
46 changes: 6 additions & 40 deletions megamek/src/megamek/client/ui/swing/MegaMekGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import megamek.client.ui.swing.dialog.MainMenuUnitBrowserDialog;
import megamek.client.ui.swing.gameConnectionDialogs.ConnectDialog;
import megamek.client.ui.swing.gameConnectionDialogs.HostDialog;
import megamek.client.ui.swing.scenario.ScenarioChooser;
import megamek.client.ui.swing.skinEditor.SkinEditorMainGUI;
import megamek.client.ui.swing.tooltip.PilotToolTip;
import megamek.client.ui.swing.util.MegaMekController;
Expand All @@ -52,7 +53,7 @@
import megamek.common.util.ImageUtil;
import megamek.common.util.fileUtils.MegaMekFile;
import megamek.server.GameManager;
import megamek.server.ScenarioLoader;
import megamek.common.scenario.ScenarioLoader;
import megamek.server.Server;
import megamek.utilities.xml.MMXMLUtility;
import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -687,48 +688,13 @@ public void quickLoadGame() {
* Host a game constructed from a scenario file
*/
void scenario() {
JFileChooser fc = new JFileChooser("data" + File.separatorChar + "scenarios");
fc.setLocation(frame.getLocation().x + 150, frame.getLocation().y + 100);
fc.setDialogTitle(Messages.getString("MegaMek.SelectScenarioDialog.title"));

FileFilter filter = new FileFilter() {

@Override
public boolean accept(File f) {
if (f.isDirectory()) {
return true;
}

String ext = null;
String s = f.getName();
int i = s.lastIndexOf('.');

if ((i > 0) && (i < (s.length() - 1))) {
ext = s.substring(i + 1).toLowerCase();
}

if (ext != null) {
return ext.equalsIgnoreCase("mms");
}

return false;
}

@Override
public String getDescription() {
return "MegaMek Scenario Files";
}

};
fc.setFileFilter(filter);

int returnVal = fc.showOpenDialog(frame);
if ((returnVal != JFileChooser.APPROVE_OPTION) || (fc.getSelectedFile() == null)) {
// I want a file, y'know!
ScenarioChooser scenarioChooser = new ScenarioChooser(frame);
scenarioChooser.setVisible(true);
if (scenarioChooser.getSelectedScenarioFilename() == null) {
return;
}

ScenarioLoader sl = new ScenarioLoader(fc.getSelectedFile());
ScenarioLoader sl = new ScenarioLoader(new File(scenarioChooser.getSelectedScenarioFilename()));
Game g;
try {
g = sl.createGame();
Expand Down
217 changes: 217 additions & 0 deletions megamek/src/megamek/client/ui/swing/scenario/ScenarioChooser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/*
* Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package megamek.client.ui.swing.scenario;

import megamek.MMConstants;
import megamek.client.ui.Messages;
import megamek.client.ui.baseComponents.AbstractButtonDialog;
import megamek.client.ui.swing.ButtonEsc;
import megamek.client.ui.swing.CloseAction;
import megamek.client.ui.swing.CommonSettingsDialog;
import megamek.client.ui.swing.dialog.DialogButton;
import megamek.client.ui.swing.util.ClickableLabel;
import megamek.client.ui.swing.util.UIUtil;
import megamek.common.Configuration;
import megamek.common.annotations.Nullable;
import megamek.common.preference.PreferenceManager;
import megamek.common.scenario.ScenarioInfo;
import megamek.common.scenario.ScenarioLoader;
import org.apache.logging.log4j.LogManager;

import javax.swing.*;
import javax.swing.border.MatteBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* This dialog lists all scenarios found in MM's scenario directory as well as the corresponding user directory.
* The scenarios are grouped by subdirectory (only the one directly below scenarios, further subdirectories
* are scanned for scenarios but not used for grouping). After closing, a chosen scenario can be retrieved
* by calling {@link #getSelectedScenarioFilename()}. As a fallback, the dialog also allows choosing a
* scenario by file.
*/
public class ScenarioChooser extends AbstractButtonDialog {

private final JTabbedPane tabbedPane = new JTabbedPane();
private final Map<String, List<ScenarioInfo>> sortedScenarios = sortScenarios(getScenarioInfos());
private String scenarioFileName;

public ScenarioChooser(final JFrame parentFrame) {
super(parentFrame, "ScenarioChooser", "ScenarioChooser.title");
initialize();
setMinimumSize(UIUtil.scaleForGUI(ScenarioInfoPanel.BASE_MINIMUM_WIDTH, ScenarioInfoPanel.BASE_MINIMUM_HEIGHT * 3));
}

/**
* @return the selected preset, or null if the dialog was cancelled or no preset was selected
*/
public @Nullable String getSelectedScenarioFilename() {
if (scenarioFileName != null) {
return scenarioFileName;
}
Component selectedTab = tabbedPane.getSelectedComponent();
if (!(selectedTab instanceof ScenarioInfoPane) || !getResult().isConfirmed()) {
return null;
} else {
return ((ScenarioInfoPane) selectedTab).getSelectedPreset().getFileName();
}
}

@Override
protected Container createCenterPane() {
for (String directory : sortedScenarios.keySet()) {
if (!sortedScenarios.get(directory).isEmpty()) {
ScenarioInfoPane pane = new ScenarioInfoPane(sortedScenarios.get(directory));
tabbedPane.addTab(directory.isBlank() ? "Basic" : directory, pane);
}
}
return tabbedPane;
}

@Override
protected JPanel createButtonPanel() {
JButton cancelButton = new ButtonEsc(new CloseAction(this));
JButton okButton = new DialogButton(Messages.getString("Ok.text"));
okButton.addActionListener(this::okButtonActionPerformed);

ClickableLabel chooseFileLabel = new ClickableLabel(this::selectFromFile);
chooseFileLabel.setText("Select from File...");

JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.LINE_AXIS));
buttonPanel.setBorder(BorderFactory.createCompoundBorder(
new MatteBorder(1, 0, 0, 0, UIManager.getColor("Separator.foreground")),
new UIUtil.ScaledEmptyBorder(10, 0, 10, 0)));

Box verticalBox = Box.createVerticalBox();
verticalBox.add(Box.createVerticalGlue());
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
panel.add(chooseFileLabel);
panel.add(Box.createHorizontalGlue());
verticalBox.add(panel);
verticalBox.add(Box.createVerticalGlue());

JPanel rightButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 20, 0));
rightButtonPanel.add(okButton);
rightButtonPanel.add(cancelButton);
getRootPane().setDefaultButton(okButton);

buttonPanel.add(Box.createHorizontalStrut(20));
buttonPanel.add(verticalBox);
buttonPanel.add(rightButtonPanel);
return buttonPanel;
}

private static List<ScenarioInfo> getScenarioInfos() {
List<ScenarioInfo> scenarios = new ArrayList<>(parseScenariosInDirectory(Configuration.scenariosDir()));

String userDir = PreferenceManager.getClientPreferences().getUserDir();
if (!userDir.isBlank()) {
File subDir = new File(userDir, Configuration.scenariosDir().toString());
parseScenariosInDirectory(subDir);
}
return scenarios;
}

/**
* Searches the provided directory and all subdirectories for scenario files and returns a list of
* them.
*
* @param directory the directory to parse
* @return a List of scenarios
*/
private static List<ScenarioInfo> parseScenariosInDirectory(final File directory) {
LogManager.getLogger().info("Parsing scenarios from " + directory);
List<ScenarioInfo> scenarios = new ArrayList<>();
for (String scenarioFile : CommonSettingsDialog.filteredFilesWithSubDirs(directory, MMConstants.SCENARIO_EXT)) {
try {
ScenarioInfo scenario = new ScenarioLoader(new File(scenarioFile)).load();
scenarios.add(scenario);
} catch (Exception ex) {
LogManager.getLogger().error("Failed to parse scenario " + scenarioFile, ex);
}
}
return scenarios;
}

/** Groups the given scenarios by the first subdirectory under scenarios they're in (disregards any deeper dirs) */
private Map<String, List<ScenarioInfo>> sortScenarios(List<ScenarioInfo> scenarioInfos) {
return scenarioInfos.stream().collect(Collectors.groupingBy(this::getSubDirectory, Collectors.toList()));
}

@Override
public void setVisible(boolean b) {
if (b) {
UIUtil.adjustDialog(this, UIUtil.FONT_SCALE1);
}
super.setVisible(b);
}

private void selectFromFile(MouseEvent event) {
JFileChooser fc = new JFileChooser(Configuration.scenariosDir());
fc.setFileFilter(new FileNameExtensionFilter("Scenario files", "mms"));
fc.setDialogTitle(Messages.getString("MegaMek.SelectScenarioDialog.title"));
int returnVal = fc.showOpenDialog(this);
if ((returnVal == JFileChooser.APPROVE_OPTION) && (fc.getSelectedFile() != null)) {
scenarioFileName = fc.getSelectedFile().toString();
okButtonActionPerformed(null);
}
}

/**
* @return The first subdirectory under the scenarios directory that the scenario is in; a scenario
* in scenarios/tukkayid/secondencounter/ would return "tukkayid". This is used for grouping
*/
private String getSubDirectory(ScenarioInfo scenarioInfo) {
String scenariosDir = Configuration.scenariosDir().toString();
if (!scenarioInfo.getFileName().contains(scenariosDir)) {
return "";
} else {
return subDirUnderScenarios(directoriesAsList(scenarioInfo.getFileName()));
}
}

/** @return The directories (and filename) present in the given full fileName as a list. */
private static List<String> directoriesAsList(String fileName) {
Path path = Paths.get(fileName);
List<String> result = new ArrayList<>();
for (int i = 0; i < path.getNameCount(); i++) {
result.add(path.getName(i).toString());
}
return result;
}

private String subDirUnderScenarios(List<String> directoryList) {
if (!directoryList.contains("scenarios")) {
return "";
} else {
int index = directoryList.indexOf("scenarios");
// The next entry must not be the last, as the last entry is the scenario filename
return (index + 2 < directoryList.size()) ? directoryList.get(index + 1) : "";
}
}
}
60 changes: 60 additions & 0 deletions megamek/src/megamek/client/ui/swing/scenario/ScenarioInfoPane.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2024 - The MegaMek Team. All Rights Reserved.
*
* This file is part of MegaMek.
*
* 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 <http://www.gnu.org/licenses/>.
*/
package megamek.client.ui.swing.scenario;

import megamek.common.annotations.Nullable;
import megamek.common.scenario.ScenarioInfo;

import javax.swing.*;
import java.util.ArrayList;
import java.util.List;

/**
* This panel displays a scrollable vertical list of ScenarioInfo panels.
*/
class ScenarioInfoPane extends JScrollPane {

private JList<ScenarioInfo> presets;
private final List<ScenarioInfo> scenarioInfoList = new ArrayList<>();

public ScenarioInfoPane(List<ScenarioInfo> scenarioInfoList) {
this.scenarioInfoList.addAll(scenarioInfoList);
setBorder(null);
getVerticalScrollBar().setUnitIncrement(16);
initialize();
}

public @Nullable ScenarioInfo getSelectedPreset() {
return presets.getSelectedValue();
}

protected void initialize() {
final DefaultListModel<ScenarioInfo> listModel = new DefaultListModel<>();
listModel.addAll(scenarioInfoList);
presets = new JList<>(listModel);
presets.setName("ScenarioInfoList");
presets.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
presets.setLayoutOrientation(JList.VERTICAL);
presets.setCellRenderer(new ScenarioInfoRenderer());

var panel = new JPanel();
panel.add(presets);
setViewportView(panel);
}
}
Loading

0 comments on commit ebc08e3

Please sign in to comment.