package org.erikaredmark.monkeyshines.encoder; import static com.google.common.base.Preconditions.checkArgument; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import org.erikaredmark.monkeyshines.editor.WorldEditor; import org.erikaredmark.monkeyshines.encoder.exception.WorldRestoreException; import org.erikaredmark.monkeyshines.encoder.exception.WorldSaveException; /** * Provides utility methods for saving and restoring worlds. Used as a saving mechanism for the level editor; changes to a world * during gameplay should never be encoded and saved! * * @author Erika Redmark * */ public final class WorldIO { private WorldIO() { } private static final String WORLD_EXTENSION = ".world"; /** * * Takes a world editor and translates the underlying world, and ALL data making it up into a persistable format. * This only saves the logical level data (placement of tiles and their underlying types and data) and does not hold * any resource data. * <p/> * This method is blocking until the file is saved. The file is always saved as the name of the world with a .world * extension. * * @param worldEditor * a world editor that needs to be encoded * * @param path * a location to save the world to. The encoder will generate the save format at that location. This does not include * the file name, and should point to a folder * @throws IOException * * @throws IlegalArgumentException * if the given path points to anything other than a valid folder * * @throws WorldSaveException * if an error occurs during saving the world due to high level issues (such as world corruption) * * @throws IOException * if an error occurs saving the world due to low level I/O issues * */ public static void saveWorld( WorldEditor worldEditor, Path path ) throws WorldSaveException, IOException { if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS) == false) throw new IllegalArgumentException("Path " + path + " must point to a valid folder"); EncodedWorld encoded = EncodedWorld.fromMemory(worldEditor.getWorld() ); final Path outputPath = path.resolve(worldEditor.getWorldName() + WorldIO.WORLD_EXTENSION); try (OutputStream out = Files.newOutputStream(outputPath) ) { encoded.save(out); } } /** * * Takes the world editor's underlying world ONLY and saves that to the file pointed to by path. This method differs * from the basic {@link #saveWorld(WorldEditor, Path)} in that it only updates the .world file and leaves the * resource pack as is. This is the default operation for when the user is saving changes to the world in the basic * editor. * * @param worldEditor * a world editor that needs to be encoded * * @param path * a location to save the world to. Unlike the other save method, this must point to an existing file in which * to overwrite (the original world) * * @throws IlegalArgumentException * if the given path points to anything other than a valid file * * @throws WorldSaveException * if an error occurs during saving the world due to high level issues (such as world corruption) * * @throws IOException * if an error occurs saving the world due to low level I/O issues * */ public static void saveOnlyWorld( WorldEditor worldEditor, Path path ) throws WorldSaveException, IOException { if (Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS) == false) throw new IllegalArgumentException("Path " + path + " must point to the .world file to overwrite"); EncodedWorld encoded = EncodedWorld.fromMemory(worldEditor.getWorld() ); try (OutputStream out = Files.newOutputStream(path) ) { encoded.save(out); } } /** * * Creates a new world folder at the given location with the given name. The default resources pack is used by copying * it to that location and renaming it to the given world name. Additionally, a clean-slate 'worldName'.world file is * created with empty content ready for the editor. * <p/> * This method is guaranteed to have created the folder if it does not throw an exception. * * @param newWorldFolder * the location to create the new world folder. This location's parent must exist, but the actually folder should * not as this method will create a new one * * @param worldName * the name of the world, which will be the name of the .world file as well as the name of the resource pack * @throws IOException * * @throws IllegalArgumentException * if the path to the new world folder already exists, or if the parent of the path (all folders leading up) * do not exist * * @throws IOException * if something occurs during folder and/or file creation that prevents the full creation of this world * */ public static void newWorldWithDefault(Path newWorldFolder, String worldName) throws WorldSaveException, IOException { // Don't create the world data until we first verify the resource pack is valid InputStream resourceSource = WorldIO.class.getResourceAsStream("/resources/standard/default.zip"); if (resourceSource == null) throw new RuntimeException("Bad .jar file, default resource pack unavailable"); newWorldData(newWorldFolder, worldName); // Copy resources and create <worldName>.zip file Path resourceDestination = newWorldFolder.resolve(worldName + ".zip"); Files.copy(resourceSource, resourceDestination); } /** * * Creates a new world folder at the given location with the given name, copying the contents of the resource pack * pointed to to the new folder with the world. * <p/> * This method is guaranteed to have created the folder if it does not throw an exception. * * @param newWorldFolder * the location to create the new world folder. This location's parent must exist, but the actually folder should * not as this method will create a new one * * @param worldName * the name of the world, which will be the name of the .world file as well as the name of the resource pack * * @throws IOException * * @throws IllegalArgumentException * if the path to the new world folder already exists, or if the parent of the path (all folders leading up) * do not exist, or if the given resource pack does not exist * * @throws IOException * if something occurs during folder and/or file creation that prevents the full creation of this world * */ public static void newWorldWithResources(Path newWorldFolder, String worldName, Path rsrcPack) throws WorldSaveException, IOException { checkArgument(Files.exists(rsrcPack, LinkOption.NOFOLLOW_LINKS) ); newWorldData(newWorldFolder, worldName); // Copy resources from target into <worldName>.zip file InputStream resourceSource = new FileInputStream(rsrcPack.toFile() ); Path resourceDestination = newWorldFolder.resolve(worldName + ".zip"); Files.copy(resourceSource, resourceDestination); } /** Common code to both newWorldYYY functions. Makes the .world file, does not handle resources. */ private static void newWorldData(Path newWorldFolder, String worldName) throws WorldSaveException, IOException { checkArgument(Files.exists(newWorldFolder, LinkOption.NOFOLLOW_LINKS) == false ); // child must not exist checkArgument(Files.exists(newWorldFolder.getParent(), LinkOption.NOFOLLOW_LINKS) ); // ... but parent must Files.createDirectory(newWorldFolder); // Create .world file EncodedWorld newWorld = EncodedWorld.fresh(worldName); Path worldLocation = newWorldFolder.resolve(worldName + WorldIO.WORLD_EXTENSION); try (OutputStream out = Files.newOutputStream(worldLocation) ) { newWorld.save(out); } } /** * * Restores a world saved at a given location. This only loads world data, and not resources (graphics) for the world * which are loaded separately. * <p/> * The returned object represents an immutable version of the starting state of the world for either the world editor * or game to blow into a full world world with operational real-time. * * @param world * path to the .world file * * @throws IllegalArgumentException * if the given path does not point to an actual file * * @throws WorldRestoreException * if the given file cannot be read due to world corruption resulting in an inability to * generate a world from it * * @throws IOException * if a low level I/O error prevents read * */ public static EncodedWorld restoreWorld(Path world) throws WorldRestoreException, IOException { if (Files.isRegularFile(world, LinkOption.NOFOLLOW_LINKS) == false) throw new IllegalArgumentException("Path " + world + " must point to a valid file."); try (InputStream in = Files.newInputStream(world) ) { return EncodedWorld.fromStream(in); } } }