package org.mafagafogigante.dungeon.io; import org.mafagafogigante.dungeon.game.DungeonString; import org.mafagafogigante.dungeon.game.Game; import org.mafagafogigante.dungeon.game.GameState; import org.mafagafogigante.dungeon.logging.DungeonLogger; import org.mafagafogigante.dungeon.util.Messenger; import org.mafagafogigante.dungeon.util.StopWatch; import org.jetbrains.annotations.NotNull; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.swing.JOptionPane; /** * Loader class that handles saving and loading the game. */ public final class Loader { private static final File SAVES_FOLDER = new File("saves/"); private static final String SAVE_EXTENSION = ".dungeon"; private static final String DEFAULT_SAVE_NAME = "default" + SAVE_EXTENSION; private static final String SAVE_CONFIRM = "Do you want to save the game?"; private static final String LOAD_CONFIRM = "Do you want to load the game?"; private Loader() { // Ensure that this class cannot be instantiated. throw new AssertionError(); } /** * Evaluates if a File is a save file by checking that it is a file and ends with the save extension. * * @param file a File object * @return true if the specified File is not {@code null} and has the properties of a save file */ private static boolean isSaveFile(File file) { return file != null && file.getName().endsWith(SAVE_EXTENSION) && file.isFile(); } /** * Checks if any file in the saves folder ends with the save extension. */ public static boolean checkForSave() { return !getSavedFiles().isEmpty(); } /** * Prompts the user to confirm an operation using a dialog window. */ private static boolean confirmOperation(String confirmation) { int result = JOptionPane.showConfirmDialog(Game.getGameWindow(), confirmation, null, JOptionPane.YES_NO_OPTION); Game.getGameWindow().requestFocusOnTextField(); return result == JOptionPane.YES_OPTION; } /** * Appends the save file extension to a file name it if it does not ends with it already. * * @param name a file name * @return a String ending with the file extension */ private static String ensureSaveEndsWithExtension(String name) { return name.endsWith(SAVE_EXTENSION) ? name : name + SAVE_EXTENSION; } /** * Generates a new GameState and returns it. */ public static GameState newGame() { GameState gameState = new GameState(); DungeonString string = new DungeonString(); string.append("Created a new game.\n\n"); string.append(gameState.getPreface()); string.append("\n"); Writer.write(string); Game.getGameWindow().requestFocusOnTextField(); return gameState; } /** * Loads the newest save file if there is a save file. Otherwise, returns {@code null}. * * <p>Note that if the user does not confirm the operation in the dialog that pops up, this method return {@code * null}. * * @param requireConfirmation whether or not this method should require confirmation from the user * @return a GameState or null */ public static GameState loadGame(boolean requireConfirmation) { if (checkForSave()) { if (!requireConfirmation || confirmOperation(LOAD_CONFIRM)) { return loadFile(getMostRecentlySavedFile()); } } return null; } /** * Attempts to load the save file indicated by the first argument of the "load" command. * * <p>If the filename was provided but didn't match any files, a message about this is written. * * <p>If the load command was issued without arguments, this method delegates save loading to {@code * Loader.loadGame(false)}. * * <p>If no save file could be found, a message is written. * * <p>This method guarantees that the if null is returned, something is written to the screen. */ public static GameState parseLoadCommand(String[] arguments) { if (arguments.length != 0) { // A save name was provided. String argument = arguments[0]; argument = ensureSaveEndsWithExtension(argument); File save = createFileFromName(argument); if (isSaveFile(save)) { return loadFile(save); } else { Writer.write(save.getName() + " does not exist or is not a file."); return null; } } else { GameState loadResult = loadGame(false); // Don't ask for confirmation. Typing load is not an easy mistake. if (loadResult == null) { Writer.write("No saved game could be found."); } return loadResult; } } /** * Saves the specified GameState to the default save file. */ public static void saveGame(GameState gameState) { saveGame(gameState, null); } /** * Saves the specified GameState, using the default save file or the one defined in the IssuedCommand. * * <p>Only asks for confirmation if there already is a save file with the name. */ public static void saveGame(GameState gameState, String[] arguments) { String saveName = DEFAULT_SAVE_NAME; if (arguments != null && arguments.length != 0) { saveName = arguments[0]; } if (saveFileDoesNotExist(saveName) || confirmOperation(SAVE_CONFIRM)) { saveFile(gameState, saveName); } } /** * Tests whether the save file corresponding to the provided name does not exist. * * @param name the provided filename * @return true if the corresponding file does not exist */ private static boolean saveFileDoesNotExist(String name) { return !createFileFromName(name).exists(); } /** * Returns a File object for the corresponding save file for a specified name. * * @param name the provided filename * @return a File object */ private static File createFileFromName(String name) { return new File(SAVES_FOLDER, ensureSaveEndsWithExtension(name)); } /** * Attempts to load a GameState from a file. * * @param file a File object * @return a GameState or {@code null} if something goes wrong. */ private static GameState loadFile(File file) { StopWatch stopWatch = new StopWatch(); try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file)); ObjectInputStream objectInputStream = new ObjectInputStream(in)) { GameState loadedGameState = (GameState) objectInputStream.readObject(); loadedGameState.setSaved(true); // It is saved, we just loaded it (needed as it now defaults to false). String sizeString = Converter.bytesToHuman(file.length()); DungeonLogger.info(String.format("Loaded %s in %s.", sizeString, stopWatch.toString())); Writer.write(String.format("Successfully loaded the game (read %s from %s).", sizeString, file.getName())); return loadedGameState; } catch (FileNotFoundException bad) { // The filed was moved or deleted. Writer.write("Could not find the specified saved game."); return null; } catch (ClassNotFoundException | IOException exception) { Writer.write("Could not load the saved game."); DungeonLogger.logSevere(exception); return null; } } /** * Serializes the specified {@code GameState} state to a file. * * @param state a GameState * @param name the name of the file */ private static void saveFile(GameState state, String name) { StopWatch stopWatch = new StopWatch(); File file = createFileFromName(name); ensureSavesFolderExists(); try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file)); ObjectOutputStream objectOutputStream = new ObjectOutputStream(out)) { objectOutputStream.writeObject(state); state.setSaved(true); String sizeString = Converter.bytesToHuman(file.length()); DungeonLogger.info(String.format("Saved %s in %s.", sizeString, stopWatch.toString())); Writer.write(String.format("Successfully saved the game (wrote %s to %s).", sizeString, file.getName())); } catch (IOException exception) { Writer.write("Could not save the game."); DungeonLogger.logSevere(exception); } } private static void ensureSavesFolderExists() { if (!SAVES_FOLDER.exists()) { if (!SAVES_FOLDER.mkdir()) { Messenger.printFailedToCreateDirectoryMessage(SAVES_FOLDER.getName()); } } } /** * Returns a list of abstract pathnames denoting the files in the saves folder that end with a valid extension sorted * in natural order. */ @NotNull public static List<File> getSavedFiles() { File[] fileArray = SAVES_FOLDER.listFiles(DungeonFilenameFilters.getExtensionFilter()); if (fileArray == null) { fileArray = new File[0]; } List<File> fileList = new ArrayList<>(Arrays.asList(fileArray)); for (Iterator<File> fileIterator = fileList.iterator(); fileIterator.hasNext(); ) { File file = fileIterator.next(); if (!file.isFile()) { fileIterator.remove(); } } Collections.sort(fileList, new FileLastModifiedComparator()); return fileList; } /** * Returns the most recently saved file. As a precondition, there must be at least one save file. */ private static File getMostRecentlySavedFile() { List<File> fileList = getSavedFiles(); if (fileList.isEmpty()) { throw new IllegalStateException("called getMostRecentlySavedFile() but there are no save files."); } return fileList.get(0); } }