feat: add support for saved states for games exported to file loader

-Add option in preferences to show saved states for file loader
-Add tool to copy saved states for the 1.5.2 Carousel to File Loader
fix: trim and remove any " character from the title, author and composer fileds when saving a game.
This commit is contained in:
lantzelot-swe 2022-06-22 22:51:31 +02:00
parent 24fd322d53
commit 97d1c0aa6f
10 changed files with 279 additions and 27 deletions

View File

@ -42,6 +42,7 @@ import se.lantz.gui.translation.TranslationProgressDialog;
import se.lantz.gui.translation.TranslationWorker;
import se.lantz.manager.ScraperManager;
import se.lantz.model.MainViewModel;
import se.lantz.model.PreferencesModel;
import se.lantz.model.data.GameListData;
import se.lantz.model.data.ScraperFields;
import se.lantz.util.FileManager;
@ -506,7 +507,12 @@ public class GameDetailsBackgroundPanel extends JPanel
public void updateSavedStatesTabTitle()
{
String carouselVersion = FileManager.getConfiguredSavedStatesCarouselVersion();
getSystemSavesTabbedPane().setTitleAt(1, "Saved States (Carousel " + carouselVersion + ")");
String title = "Saved States (Carousel " + carouselVersion + ")";
if (carouselVersion.equals(PreferencesModel.FILE_LOADER))
{
title = "Saved States (File Loader)";
}
getSystemSavesTabbedPane().setTitleAt(1, title);
getSavesBackgroundPanel().resetCurrentGameReference();
}
}

View File

@ -131,7 +131,7 @@ public class GameListDataRenderer extends DefaultListCellRenderer
}
else
{
int numberOfSavedStates = savedStatesManager.getNumberOfSavedStatesForGame(listData.getGameFileName());
int numberOfSavedStates = savedStatesManager.getNumberOfSavedStatesForGame(listData.getGameFileName(), listData.getTitle());
if (numberOfSavedStates == 1)
{
this.setIcon(saves1Icon);

View File

@ -114,6 +114,7 @@ public class MenuManager
private JMenuItem validateDbItem;
private JMenuItem palNtscFixItem;
private JMenuItem convertSavedStatesItem;
private JMenuItem copySavedStatesItem;
private JMenuItem resetJoystickConfigItem;
private JMenuItem installPCUAEItem;
@ -209,6 +210,7 @@ public class MenuManager
toolsMenu.add(getDeleteGamesForViewMenuItem());
toolsMenu.addSeparator();
toolsMenu.add(getConvertSavedStatesItem());
toolsMenu.add(getCopySavedStatesToFileLoaderItem());
toolsMenu.add(getResetJoystickConfigItem());
toolsMenu.addSeparator();
toolsMenu.add(getPalNtscFixMenuItem());
@ -815,6 +817,18 @@ public class MenuManager
}
return convertSavedStatesItem;
}
private JMenuItem getCopySavedStatesToFileLoaderItem()
{
if (copySavedStatesItem == null)
{
copySavedStatesItem = new JMenuItem("Copy Saved states to File Loader...");
copySavedStatesItem.setMnemonic('f');
copySavedStatesItem.addActionListener(e -> copySavedStatesFromCarouselToFileLoader());
}
return copySavedStatesItem;
}
private JMenuItem getResetJoystickConfigItem()
{
@ -1289,6 +1303,52 @@ public class MenuManager
}
}
}
private void copySavedStatesFromCarouselToFileLoader()
{
JOptionPane.showMessageDialog(MainWindow.getInstance().getMainPanel(),
"<html>A Carousel saves the saved states for a game in a folder named after the game file name.<br>When exporting games to File Loader the " +
"file name will be based on the title for the game, so the carousel saved states<br>will not be available for exported games.<p><p>With this " +
"function you can copy existing saved states for the games in the carousel to a new folder that the File loader will find. <br>It will check all " +
"imported saved states and match them towards available games in the manager and create a new folder in the saves directory.<br>You can then export the saved states to your USB stick to use them in the File Loader. <p><p>Press OK to check for saved states that can be copied.</html>",
"Copy saved states to File Loader",
JOptionPane.INFORMATION_MESSAGE);
int numberOfSavesNotAvailableForFileLoader = savedStatesManager.checkForSavedStatesToCopyToFileLoader();
if (numberOfSavesNotAvailableForFileLoader == 0)
{
JOptionPane.showMessageDialog(MainWindow.getInstance().getMainPanel(),
"No carousel saved states exists that are not available for File Loader. All is up to date.",
"Copy saved states to File Loader",
JOptionPane.INFORMATION_MESSAGE);
}
else
{
String message = String
.format("<html>There are %s games that have saved states used by the Carousel version 1.5.2 or later.<br>Do you want to copy them to the File Loader format so that they are accessible from the File Loader?</html>",
Integer.valueOf(numberOfSavesNotAvailableForFileLoader));
int option = JOptionPane.showConfirmDialog(MainWindow.getInstance().getMainPanel(),
message,
"Copy saved states to File Loader",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (option == JOptionPane.YES_OPTION)
{
savedStatesManager.copyFromCarouselToFileLoader();
//Refresh after converting
MainWindow.getInstance().refreshMenuAndUI();
//Refresh game views
uiModel.reloadGameViews();
//Set all games as selected
uiModel.setSelectedGameView(null);
}
}
}
private void resetJoystickConfigs()
{

View File

@ -311,7 +311,7 @@ public class SaveStatePanel extends JPanel
BufferedImage image = null;
if (!filename.isEmpty())
{
String fileName = SavedStatesManager.getGameFolderName(model.getInfoModel().getGamesFile());
String fileName = SavedStatesManager.getGameFolderName(model.getInfoModel().getGamesFile(), model.getInfoModel().getTitle());
logger.debug(fileName.toString());
Path saveFolderPath = new File("./saves/" + fileName).toPath();

View File

@ -16,7 +16,7 @@ public class PreferencesDialog extends BaseDialog
addContent(getPreferencesTabPanel());
getOkButton().setText("Save");
getOkButton().setPreferredSize(null);
this.setPreferredSize(new Dimension(366, 580));
this.setPreferredSize(new Dimension(366, 630));
this.setResizable(false);
}

View File

@ -21,6 +21,7 @@ public class SaveStatePrefPanel extends JPanel
private JRadioButton v152RadioButton;
private final ButtonGroup buttonGroup = new ButtonGroup();
private PreferencesModel model;
private JRadioButton fileLoaderRadioButton;
public SaveStatePrefPanel(PreferencesModel model)
{
@ -43,13 +44,21 @@ public class SaveStatePrefPanel extends JPanel
gbc_v132RadioButton.gridy = 1;
add(getV132RadioButton(), gbc_v132RadioButton);
GridBagConstraints gbc_v152RadioButton = new GridBagConstraints();
gbc_v152RadioButton.weighty = 1.0;
gbc_v152RadioButton.insets = new Insets(0, 40, 10, 0);
gbc_v152RadioButton.weighty = 0.0;
gbc_v152RadioButton.insets = new Insets(0, 40, 0, 0);
gbc_v152RadioButton.anchor = GridBagConstraints.NORTHWEST;
gbc_v152RadioButton.weightx = 1.0;
gbc_v152RadioButton.gridx = 0;
gbc_v152RadioButton.gridy = 2;
add(getV152RadioButton(), gbc_v152RadioButton);
GridBagConstraints gbc_fileLoaderRadioButton = new GridBagConstraints();
gbc_fileLoaderRadioButton.insets = new Insets(0, 40, 10, 0);
gbc_fileLoaderRadioButton.anchor = GridBagConstraints.NORTHWEST;
gbc_fileLoaderRadioButton.gridx = 0;
gbc_fileLoaderRadioButton.gridy = 3;
gbc_fileLoaderRadioButton.weightx = 1.0;
gbc_fileLoaderRadioButton.weighty = 1.0;
add(getFileLoaderRadioButton(), gbc_fileLoaderRadioButton);
if (!Beans.isDesignTime())
{
model.addPropertyChangeListener(e -> modelChanged());
@ -63,8 +72,8 @@ public class SaveStatePrefPanel extends JPanel
if (infoLabel == null)
{
String info = "<html>Different versions of the Carousel adds the saved states in different folders. " +
"You have to choose which version of the Carousel you want the manager to read the saved states for. " +
"Only saved states for one carousel version at a time is shown for the games in the gamelist views.</html>";
"You have to choose which version of the Carousel (or the File Loader) you want the manager to read the saved states for. " +
"Only saved states for one option at a time is shown for the games in the gamelist views. Saved states for the File loader is located in a folder mathching the game title.</html>";
infoLabel = new JLabel(info);
}
return infoLabel;
@ -111,8 +120,30 @@ public class SaveStatePrefPanel extends JPanel
return v152RadioButton;
}
private JRadioButton getFileLoaderRadioButton()
{
if (fileLoaderRadioButton == null)
{
fileLoaderRadioButton = new JRadioButton("File Loader");
buttonGroup.add(fileLoaderRadioButton);
fileLoaderRadioButton.addItemListener(new ItemListener()
{
public void itemStateChanged(ItemEvent e)
{
if (fileLoaderRadioButton.isSelected())
{
model.setSavedStatesCarouselVersion(PreferencesModel.FILE_LOADER);
}
}
});
}
return fileLoaderRadioButton;
}
private void modelChanged()
{
getV132RadioButton().setSelected(model.getSavedStatesCarouselVersion().equals(PreferencesModel.CAROUSEL_132));
getV152RadioButton().setSelected(model.getSavedStatesCarouselVersion().equals(PreferencesModel.CAROUSEL_152));
getFileLoaderRadioButton().setSelected(model.getSavedStatesCarouselVersion().equals(PreferencesModel.FILE_LOADER));
}
}

View File

@ -9,6 +9,7 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -33,6 +34,7 @@ import se.lantz.model.MainViewModel;
import se.lantz.model.PreferencesModel;
import se.lantz.model.SavedStatesModel;
import se.lantz.model.SavedStatesModel.SAVESTATE;
import se.lantz.model.data.GameValidationDetails;
import se.lantz.util.ExceptionHandler;
import se.lantz.util.FileManager;
@ -107,15 +109,16 @@ public class SavedStatesManager
//If the game has been renamed, make sure to rename the saves folder also
String oldFileName = model.getInfoModel().getOldGamesFile();
String newFileName = model.getInfoModel().getGamesFile();
File oldSaveFolder = new File(SAVES + getGameFolderName(oldFileName));
File oldSaveFolder = new File(SAVES + getGameFolderName(oldFileName, model.getInfoModel().getTitle()));
if (!oldFileName.equals(newFileName) && oldSaveFolder.exists())
{
//Rename old folder to new name
oldSaveFolder.renameTo(new File(SAVES + getGameFolderName(model.getInfoModel().getGamesFile())));
oldSaveFolder.renameTo(new File(SAVES +
getGameFolderName(model.getInfoModel().getGamesFile(), model.getInfoModel().getTitle())));
}
String fileName = model.getInfoModel().getGamesFile();
Path saveFolder = new File(SAVES + getGameFolderName(fileName)).toPath();
Path saveFolder = new File(SAVES + getGameFolderName(fileName, model.getInfoModel().getTitle())).toPath();
int numberofSaves = 0;
//Check which ones are available
Path mta0Path = saveFolder.resolve(MTA0);
@ -166,9 +169,10 @@ public class SavedStatesManager
{
savedStatesModel.resetProperties();
//Read from state directory, update model
String fileName = getGameFolderName(model.getInfoModel().getGamesFile());
String fileName = getGameFolderName(model.getInfoModel().getGamesFile(), model.getInfoModel().getTitle());
if (!fileName.isEmpty())
{
fileName = fileName.trim();
//Check if folder is available
Path saveFolder = new File(SAVES + fileName).toPath();
if (Files.exists(saveFolder))
@ -335,7 +339,7 @@ public class SavedStatesManager
private void deleteSavedState(SAVESTATE state)
{
String fileName = getGameFolderName(model.getInfoModel().getGamesFile());
String fileName = getGameFolderName(model.getInfoModel().getGamesFile(), model.getInfoModel().getTitle());
Path saveFolder = new File(SAVES + fileName).toPath();
try
{
@ -557,10 +561,18 @@ public class SavedStatesManager
}
}
public int getNumberOfSavedStatesForGame(String gameFileName)
public int getNumberOfSavedStatesForGame(String gameFileName, String title)
{
String fileName = getGameFolderName(gameFileName);
return savedStatesMap.get(fileName) != null ? savedStatesMap.get(fileName) : 0;
String fileName = getGameFolderName(gameFileName, title);
if (savedStatesMap.get(fileName) == null)
{
//Check with only uppercase also
return savedStatesMap.get(fileName.toUpperCase()) != null ? savedStatesMap.get(fileName.toUpperCase()) : 0;
}
else
{
return savedStatesMap.get(fileName);
}
}
public void checkEnablementOfPalNtscMenuItem(boolean check)
@ -574,7 +586,7 @@ public class SavedStatesManager
if (!fileName.isEmpty() && fileName.contains(".vsf"))
{
//Check if folder is available
Path saveFolder = new File(SAVES + getGameFolderName(fileName)).toPath();
Path saveFolder = new File(SAVES + getGameFolderName(fileName, model.getInfoModel().getTitle())).toPath();
if (Files.exists(saveFolder))
{
//Check which ones are available
@ -593,7 +605,8 @@ public class SavedStatesManager
{
String gamesFile = model.getInfoModel().getGamesFile();
Path gameFilePath = new File(FileManager.GAMES + gamesFile).toPath();
Path firstSavedStatePath = new File(SAVES + getGameFolderName(gamesFile)).toPath().resolve(VSZ0);
Path firstSavedStatePath =
new File(SAVES + getGameFolderName(gamesFile, model.getInfoModel().getTitle())).toPath().resolve(VSZ0);
Path tempFilePath = new File(FileManager.GAMES + "temp.gz").toPath();
try
@ -620,7 +633,7 @@ public class SavedStatesManager
return false;
}
public static String getGameFolderName(String fileName)
public static String getGameFolderName(String fileName, String gameTitle)
{
String returnValue = "";
switch (FileManager.getConfiguredSavedStatesCarouselVersion())
@ -634,6 +647,12 @@ public class SavedStatesManager
returnValue = fileName.substring(0, fileName.indexOf("."));
}
break;
case PreferencesModel.FILE_LOADER:
if (fileName.length() > 0)
{
returnValue = FileManager.generateSavedStatesFolderNameForFileLoader(gameTitle, 0);
}
break;
default:
break;
}
@ -727,6 +746,82 @@ public class SavedStatesManager
}
}
public void copyFromCarouselToFileLoader()
{
//1. look through all folders and try to find a game in the db that matches the file name.
//2. for all that matches, get the title and copy the existing folder to a folder named as the title
File saveFolder = new File(SAVES);
try (Stream<Path> stream = Files.walk(saveFolder.toPath().toAbsolutePath(), 1))
{
List<GameValidationDetails> allGamesDetailsList = this.model.getDbConnector().fetchAllGamesForDbValdation();
List<Path> filteredPathList = getMatchingCarousel152FoldersThatCanBeCopied(stream, allGamesDetailsList);
//Copy for all
filteredPathList.stream().forEachOrdered(sourcePath -> {
try
{
File originalDir = sourcePath.toFile();
String fileName = originalDir.getName();
GameValidationDetails gameDetails = allGamesDetailsList.stream()
.filter(game -> fileName.equals(get152VersionFileName(game.getGame()))).findAny().orElse(null);
if (gameDetails != null)
{
File newDir = new File(originalDir.getParent() + "\\" +
FileManager.generateSavedStatesFolderNameForFileLoader(gameDetails.getTitle(), 0));
copyDirectory(sourcePath.toString(), newDir.toPath().toString());
}
}
catch (Exception e)
{
ExceptionHandler.logException(e, "Could not copy available saved states for " + sourcePath.toString());
}
});
}
catch (IOException e1)
{
ExceptionHandler.handleException(e1, "Could not copy saved states");
}
}
private void copyDirectory(String sourceDirectoryLocation, String destinationDirectoryLocation) throws IOException
{
Files.walk(Paths.get(sourceDirectoryLocation)).forEach(source -> {
Path destination =
Paths.get(destinationDirectoryLocation, source.toString().substring(sourceDirectoryLocation.length()));
try
{
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException e)
{
ExceptionHandler.logException(e, "Could not copy available saved states from " + sourceDirectoryLocation);
}
});
}
private boolean is152VersionFolderName(Path folderPath)
{
String folderName = folderPath.toFile().getName();
String newName = folderName;
if (folderName.indexOf(".") > -1)
{
newName = folderName.substring(0, folderName.indexOf("."));
}
return newName.equalsIgnoreCase(folderName);
}
private String get152VersionFileName(String fileName)
{
String newName = fileName;
if (fileName.indexOf(".") > -1)
{
newName = fileName.substring(0, fileName.indexOf("."));
}
return newName;
}
public int checkFor132SavedStates()
{
File saveFolder = new File(SAVES);
@ -754,4 +849,41 @@ public class SavedStatesManager
}
return (int) returnValue;
}
public int checkForSavedStatesToCopyToFileLoader()
{
//1. look through all folders and try to find a game in the db that matches the file name.
//2. for all that matches, get the title and check so that no folder exists already
File saveFolder = new File(SAVES);
try (Stream<Path> stream = Files.walk(saveFolder.toPath().toAbsolutePath(), 1))
{
List<GameValidationDetails> allGamesDetailsList = this.model.getDbConnector().fetchAllGamesForDbValdation();
List<Path> filteredPathList = getMatchingCarousel152FoldersThatCanBeCopied(stream, allGamesDetailsList);
return filteredPathList.size();
}
catch (IOException e1)
{
ExceptionHandler.handleException(e1, "Could not check saved states for File Loader");
}
return 0;
}
private List<Path> getMatchingCarousel152FoldersThatCanBeCopied(Stream<Path> stream,
List<GameValidationDetails> allGamesDetailsList)
{
List<Path> filteredPathList = stream.filter(sourcePath -> is152VersionFolderName(sourcePath)).filter(sourcePath -> {
File originalDir = sourcePath.toFile();
String fileName = originalDir.getName();
GameValidationDetails gameDetails = allGamesDetailsList.stream()
.filter(game -> fileName.equals(get152VersionFileName(game.getGame()))).findAny().orElse(null);
if (gameDetails != null)
{
File newDir = new File(originalDir.getParent() + "\\" +
FileManager.generateSavedStatesFolderNameForFileLoader(gameDetails.getTitle(), 0));
return !newDir.exists();
}
return false;
}).collect(Collectors.toList());
return filteredPathList;
}
}

View File

@ -63,7 +63,8 @@ public class InfoModel extends AbstractModel implements CommonInfoModel
public void setTitle(String title)
{
String old = getTitle();
this.title = title;
//Remove all " since that messes with the SQL
this.title = title.replace("\"", "").trim();
if (!Objects.equals(old, title))
{
notifyChange();
@ -213,7 +214,8 @@ public class InfoModel extends AbstractModel implements CommonInfoModel
public void setComposer(String composer)
{
String old = getComposer();
this.composer = composer;
//Remove all " since that messes with the SQL
this.composer = composer.replace("\"", "").trim();
if (!Objects.equals(old, composer))
{
notifyChange();
@ -228,7 +230,8 @@ public class InfoModel extends AbstractModel implements CommonInfoModel
public void setAuthor(String author)
{
String old = getAuthor();
this.author = author;
//Remove all " since that messes with the SQL
this.author = author.replace("\"", "").trim();
if (!Objects.equals(old, author))
{
notifyChange();

View File

@ -9,6 +9,7 @@ public class PreferencesModel extends AbstractModel implements CommonInfoModel
{
public static final String CAROUSEL_132 = "1.3.2";
public static final String CAROUSEL_152 = "1.5.2";
public static final String FILE_LOADER = "fl";
public static final String PCUAE_VERSION_CHECK = "checkForPCUAEVersion";
public static final String MANGER_VERSION_CHECK = "checkForManagerVersion";

View File

@ -619,7 +619,7 @@ public class FileManager
public static String generateFileNameFromTitleForFileLoader(String title, int duplicateIndex)
{
List<Character> forbiddenCharsList =
":,'<27>!+*<>/[]?|".chars().mapToObj(item -> (char) item).collect(Collectors.toList());
":,'<27>!+*<>/[]?|\"".chars().mapToObj(item -> (char) item).collect(Collectors.toList());
List<Character> newName = title.chars().mapToObj(item -> (char) item)
.filter(character -> !forbiddenCharsList.contains(character)).collect(Collectors.toList());
@ -629,10 +629,29 @@ public class FileManager
//Just add the duplicate index if there are several games with the same name
newNameString = newNameString + "_" + duplicateIndex;
}
newNameString = newNameString.trim();
logger.debug("Game title: \"{}\" ---- New fileName: \"{}\"", title, newNameString);
return newNameString;
}
public static String generateSavedStatesFolderNameForFileLoader(String title, int duplicateIndex)
{
String name = generateFileNameFromTitleForFileLoader(title, duplicateIndex);
//Special handling of titles where dots occur, e.g. G.A.C.C.R.R. or H.E.R.O.
int dotCount = (int)name.chars().filter(ch -> ch == '.').count();
if (dotCount > 4)
{
//Strip string to only include 4 dots, seems to be a limit there
int endIndex = StringUtils.ordinalIndexOf(name, ".", 5);
name = name.substring(0, endIndex);
}
if (name.endsWith("."))
{
name = name.substring(0, name.lastIndexOf("."));
}
return name;
}
public void exportGameInfoFile(GameDetails gameDetails, File targetDir, PublishWorker worker, boolean fileLoader)
{
@ -769,7 +788,7 @@ public class FileManager
}
else
{
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile()) +
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile(), infoModel.getTitle()) +
"/" + savedStatesModel.getState1File();
}
}
@ -783,7 +802,7 @@ public class FileManager
}
else
{
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile()) +
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile(), infoModel.getTitle()) +
"/" + savedStatesModel.getState2File();
}
break;
@ -796,7 +815,7 @@ public class FileManager
}
else
{
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile()) +
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile(), infoModel.getTitle()) +
"/" + savedStatesModel.getState3File();
}
break;
@ -809,7 +828,7 @@ public class FileManager
}
else
{
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile()) +
gamePathString = SavedStatesManager.SAVES + SavedStatesManager.getGameFolderName(infoModel.getGamesFile(), infoModel.getTitle()) +
"/" + savedStatesModel.getState4File();
}
break;