/** * THIS IS CREATED BY tom_mai78101. PLEASE GIVE CREDIT FOR WORKING ON A CLONE. * * ALL WORKS COPYRIGHTED TO The Pokémon Company and Nintendo. I REPEAT, THIS IS A CLONE. * * YOU MAY NOT SELL COMMERCIALLY, OR YOU WILL BE PROSECUTED BY The Pokémon Company AND Nintendo. * * THE CREATOR IS NOT LIABLE FOR ANY DAMAGES DONE. FOLLOW LOCAL LAWS, BE RESPECTFUL, AND HAVE A GOOD DAY! * */ package saving; import item.ItemText; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import level.Area; import level.PixelData; import level.WorldConstants; import main.Game; import submenu.DummyMenu; import submenu.Inventory; import submenu.Save; import abstracts.Item; import abstracts.SubMenu; import dialogue.StartMenu; import entity.Player; public class GameSave { /* * Refer to the documentation on the file format used for saving game data. * * All saving/loading schemes use the documentation as guidelines. It will be changed in the future, as the development continues. */ public static final byte[] SIGNATURE = new byte[] { (byte) 137, 0x53, 0x41, 0x56, 0x20, 0x20, 0x20, 0x20 }; public HeaderInfo headerInfo; public PlayerInfo playerInfo; public AreaInfo areaInfo; private GameSave() { //TODO (6/25/2015): Needs a way to load save data for modded map. headerInfo = new HeaderInfo(); playerInfo = new PlayerInfo(); areaInfo = new AreaInfo(); } private void write(RandomAccessFile raf) throws IOException { raf.seek(0); headerInfo.write(raf); playerInfo.write(raf); areaInfo.write(raf); } private void read(RandomAccessFile raf) throws IOException { raf.seek(0); headerInfo.read(raf); playerInfo.read(raf); areaInfo.read(raf); } private void generateSaveData(Game game) { Player gamePlayer = game.getPlayer(); if (this.playerInfo != null) this.playerInfo.reset(); // Name of the player. System.arraycopy(gamePlayer.getByteName(), 0, this.playerInfo.player_name, 0, 16); // 16 is the name length limit. byte[] byteArray = concatenate(PlayerInfo.NAME, gamePlayer.getByteName()); this.playerInfo.increment(concatenate(new byte[] { 0x0 }, byteArray)); // Gender of the player. System.arraycopy(gamePlayer.getByteGender(), 0, this.playerInfo.player_gender, 0, 1); // 1 is the gender boolean size. byteArray = concatenate(PlayerInfo.GNDR, gamePlayer.getByteGender()); this.playerInfo.increment(concatenate(new byte[] { 0x0 }, byteArray)); // All active submenus the player has unlocked. byteArray = new byte[] {}; List<Map.Entry<Integer, SubMenu>> startMenuList = game.getStartMenu().getSubMenusList(); for (Map.Entry<Integer, SubMenu> entry : startMenuList) { SubMenu sub = entry.getValue(); byteArray = concatenate(byteArray, concatenate(new byte[] { 0x0 }, sub.getSubMenuData())); this.playerInfo.startMenu.add(sub.getSubMenuData()); } this.playerInfo.increment(concatenate(new byte[] { 0x0 }, concatenate(PlayerInfo.MENU, byteArray))); // Inventory byte listType = 0x1; this.playerInfo.increment(concatenate(new byte[] { 0x0 }, PlayerInfo.ITEM)); for (List<Map.Entry<Item, Integer>> itemList : game.getStartMenu().getInventory().getAllItemsList()) { if (itemList.size() - 1 > 0) { byteArray = new byte[] { listType }; // Type of list (Potions/KeyItems/Pokeball/TMHM) byteArray = concatenate(byteArray, ByteBuffer.allocate(2).putChar((char) ((itemList.size() - 1) & 0xFFFF)).array()); // How many items in a list? byteArray = concatenate(byteArray, ByteBuffer.allocate(1).put((byte) itemList.size()).array()); // ItemInfo size. byte[] itemInfo = null; for (int i = 0; i < itemList.size() - 1; i++) { Map.Entry<Item, Integer> entry = itemList.get(i); ByteBuffer buffer = ByteBuffer.allocate(1); buffer.put((byte) (entry.getKey().getName().getBytes().length & 0xFF)); itemInfo = buffer.array(); buffer = ByteBuffer.allocate(4 * 2); buffer.putInt(entry.getKey().getID()); // Item ID buffer.putInt(entry.getValue()); // Item quantity itemInfo = concatenate(itemInfo, entry.getKey().getName().getBytes()); itemInfo = concatenate(itemInfo, buffer.array()); byteArray = concatenate(byteArray, itemInfo); // Item Name Length <- Item Name <- ID <- Quantity } this.playerInfo.getAllItemsList().get(listType - 0x1).add(itemInfo); this.playerInfo.increment(byteArray); listType++; } } // Current Area & Player State Area currentArea = game.getWorld().getCurrentArea(); byte[] bufArea = ByteBuffer.allocate(4).putInt(currentArea.getAreaID()).array(); byte[] bufSector = ByteBuffer.allocate(4).putInt(currentArea.getSectorID()).array(); byte[] bufX = ByteBuffer.allocate(4).putInt(currentArea.getPlayerXInArea()).array(); byte[] bufY = ByteBuffer.allocate(4).putInt(currentArea.getPlayerYInArea()).array(); byte[] bufFacing = ByteBuffer.allocate(4).putInt(gamePlayer.getFacing()).array(); for (int i = 0; i < 4; i++) { this.areaInfo.current_area_id[i] = bufArea[i]; this.areaInfo.current_area_sector_id[i] = bufSector[i]; this.playerInfo.player_x[i] = bufX[i]; this.playerInfo.player_y[i] = bufY[i]; this.playerInfo.player_facing[i] = bufFacing[i]; } byteArray = new byte[] {}; byteArray = concatenate(byteArray, bufArea); byteArray = concatenate(byteArray, bufSector); // Size of total AreaInfo chunk + size of AreaInfo header chunk for the two 0x0s. this.areaInfo.increment(concatenate(concatenate(new byte[] { 0x0, 0x0 }, AreaInfo.AREA), byteArray)); bufArea = bufSector = null; byteArray = new byte[] {}; byteArray = concatenate(byteArray, bufX); byteArray = concatenate(byteArray, bufY); this.playerInfo.increment(concatenate(concatenate(new byte[] { 0x0 }, PlayerInfo.AXIS), byteArray)); bufX = bufY = null; // Player State this.playerInfo.increment(concatenate(concatenate(new byte[] { 0x0 }, PlayerInfo.TURN), bufFacing)); bufFacing = null; // Area Data byteArray = new byte[] {}; List<Area> areaList = game.getWorld().getAllAreas(); for (Area area : areaList) { ArrayList<PixelData> pixelList = area.getModifiedPixelDataList(); if (!pixelList.isEmpty()) { for (PixelData px : pixelList) { byte[] pixelArray = ByteBuffer.allocate(4 * 5).putInt(area.getAreaID()).putInt(area.getSectorID()).putInt(px.xPosition).putInt(px.yPosition).putInt(px.getColor()).array(); this.areaInfo.changedPixelData.add(pixelArray); byteArray = concatenate(byteArray, pixelArray); } this.areaInfo.increment(concatenate(concatenate(new byte[] { 0x0 }, AreaInfo.PIXELDATA), byteArray)); } } } private void generateLoadData(Game game) throws Exception { // Get Player Player gamePlayer = game.getPlayer(); // Get name gamePlayer.setName(new String(this.playerInfo.player_name)); // Get gender gamePlayer.setGender(this.playerInfo.player_gender[0] == 0x1 ? Boolean.TRUE.booleanValue() : Boolean.FALSE.booleanValue()); // Get menu options. if (!game.getStartMenu().getSubMenusList().isEmpty()) game.getStartMenu().getSubMenusList().clear(); for (int i = 0; i < this.playerInfo.startMenu.size(); i++) { byte[] data = this.playerInfo.startMenu.get(i); switch (new String(data)) { case StartMenu.ITEM_NAME_EXIT: game.getStartMenu().addMenuItem(new DummyMenu(StartMenu.ITEM_NAME_EXIT, "Close this menu", "Close this menu", game)); break; case StartMenu.ITEM_NAME_INVENTORY: game.getStartMenu().addMenuItem(new Inventory(StartMenu.ITEM_NAME_INVENTORY, "Open the bag.", "Open the bag.", game).initialize(game.getPlayer().keys)); break; case StartMenu.ITEM_NAME_SAVE: game.getStartMenu().addMenuItem(new Save(StartMenu.ITEM_NAME_SAVE, "Save the game.", "Save the game.", game).initialize(game.getPlayer().keys)); break; } } // Get inventory items Inventory inventory = game.getStartMenu().getInventory(); for (int k = 0; k < this.playerInfo.getAllItemsList().size(); k++) { ArrayList<byte[]> list = this.playerInfo.getAllItemsList().get(k); for (int i = 0; i < list.size(); i++) { byte[] data = list.get(i); byte[] name = new byte[data[0]]; byte offset = 0x0; for (; offset < data[0]; offset++) name[offset] = data[offset + 0x1]; offset++; int id = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; offset += 4; int quantity = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; ItemText itemText = null; ArrayList<Map.Entry<ItemText, Item>> itemList = WorldConstants.items; for (Map.Entry<ItemText, Item> e : itemList) { if (e.getKey().id == id) { itemText = e.getKey(); break; } } for (; quantity > 0; quantity--) { inventory.addItem(itemText); } } } // Get current area // TODO: Probably need to set world ID first before setting the current area ID and SECTOR. int currentAreaID = (this.areaInfo.current_area_id[0] & 0xFF) << 24 | (this.areaInfo.current_area_id[1] & 0xFF) << 16 | (this.areaInfo.current_area_id[2] & 0xFF) << 8 | (this.areaInfo.current_area_id[3] & 0xFF); game.getWorld().setCurrentArea(WorldConstants.convertToArea(game.getWorld().getAllAreas(), currentAreaID)); if (game.getWorld().getCurrentArea() == null) throw new Exception("There is no area set."); int currentSectorID = (this.areaInfo.current_area_sector_id[0] & 0xFF) << 24 | (this.areaInfo.current_area_sector_id[1] & 0xFF) << 16 | (this.areaInfo.current_area_sector_id[2] & 0xFF) << 8 | this.areaInfo.current_area_sector_id[3] & 0xFF; game.getWorld().getCurrentArea().setSectorID(currentSectorID); // Get Player Position int x = (this.playerInfo.player_x[0] << 24) | (this.playerInfo.player_x[1] << 16) | (this.playerInfo.player_x[2] << 8) | this.playerInfo.player_x[3]; int y = (this.playerInfo.player_y[0] << 24) | (this.playerInfo.player_y[1] << 16) | (this.playerInfo.player_y[2] << 8) | this.playerInfo.player_y[3]; game.getPlayer().setAreaPosition(x, y); game.getWorld().getCurrentArea().setPlayerX(x); game.getWorld().getCurrentArea().setPlayerY(y); game.getWorld().getCurrentArea().setPlayer(game.getPlayer()); // Get Player direction facing. int facing = (this.playerInfo.player_facing[0] & 0xFF) << 24 | (this.playerInfo.player_facing[1] & 0xFF) << 16 | (this.playerInfo.player_facing[2] & 0xFF) << 8 | this.playerInfo.player_facing[3] & 0xFF; game.getPlayer().setFacing(facing); // Get modified pixel data for all areas. if (this.areaInfo.changedPixelData.size() > 0) { List<Area> loadedAreas = game.getWorld().getAllAreas(); for (Iterator<byte[]> it = this.areaInfo.changedPixelData.iterator(); it.hasNext();) { byte[] data = it.next(); int offset = 0; int areaID = (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | data[offset + 3] & 0xFF; offset += 4; // Currently, unknown use at the moment. @SuppressWarnings("unused") int sectorID = (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | data[offset + 3] & 0xFF; offset += 4; LOADED_AREA: for (Area area : loadedAreas) { if (areaID == area.getAreaID()) { int xPixelData = (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | data[offset + 3] & 0xFF; offset += 4; int yPixelData = (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | data[offset + 3] & 0xFF; offset += 4; int color = (data[offset] & 0xFF) << 24 | (data[offset + 1] & 0xFF) << 16 | (data[offset + 2] & 0xFF) << 8 | data[offset + 3] & 0xFF; PixelData pxData = new PixelData(color, xPixelData, yPixelData); area.getModifiedPixelDataList().add(pxData); area.loadModifiedPixelDataList(); break LOADED_AREA; } } } game.getWorld().refresh(); } } public static void save(Game game, String filename) { GameSave data = new GameSave(); data.generateSaveData(game); File save = new File(filename); if (save.isFile()) save.delete(); RandomAccessFile raf = null; try { raf = new RandomAccessFile(save, "rw"); data.write(raf); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } public static void load(Game game, String filename) { File load = null; if (WorldConstants.isModsEnabled.booleanValue()){ File modDirectory = new File("mod"); if (modDirectory.exists() && modDirectory.isDirectory()){ File[] files = modDirectory.listFiles(); NESTED_LOOP: for (File f : files){ if (f.getName().equals("area")){ File[] saves = f.listFiles(); for (File saveFile : saves){ if (saveFile.getName().equals(filename)){ load = saveFile; break NESTED_LOOP; } } } } } } else load = new File(filename); if (load != null && load.isFile()) { GameSave data = new GameSave(); RandomAccessFile raf = null; try { raf = new RandomAccessFile(load, "r"); data.read(raf); data.generateLoadData(game); } catch (Exception e) { e.printStackTrace(); } finally { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * Checks to see if the game saved data exists in relative to where the game is located. The save data is a SAV binary format file. * * <p> * Note that {@link java.io.File#isFile() File.isFile()} is used to check for saved data that is generated by the game. This is because the saved data file generated by the game is a <i>normal</i> file. * * <p> * For more information on the file format, please read the documentation provided. * * @param filename * The file name of the saved data. * @return True, if the saved data file exists. False, if otherwise. * */ public static boolean check(String filename) { File save = new File(filename); boolean fileIsValid = true; DataInputStream dataInputStream = null; try { dataInputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(save))); byte sizeOfChunk = dataInputStream.readByte(); for (int i = 0; i < sizeOfChunk / 4; i++) { switch (i) { case 0: { // Identification Code byte identity = '1' & 0xFF; for (int j = 0; j < 4; j++) { byte byteData = dataInputStream.readByte(); if (j < 3) continue; if (byteData != identity) fileIsValid = false; } break; } case 1: { // Header Code byte[] header = { 0x48, 0x45, 0x41, 0x44 }; for (int j = 0; j < 4; j++) { byte byteData = dataInputStream.readByte(); if (byteData != header[j]) fileIsValid = false; } break; } case 2: { // File format extension byte[] format = { 0x2E, 0x53, 0x41, 0x56 }; for (int j = 0; j < 4; j++) { byte byteData = dataInputStream.readByte(); if (byteData != format[j]) fileIsValid = false; } break; } default: { fileIsValid = false; break; } } } if (!fileIsValid) throw new IOException("Save file is invalid."); } catch (FileNotFoundException e) { e.printStackTrace(); return false; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (dataInputStream != null) { try { dataInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } return save.isFile(); } public static byte[] concatenate(byte[] first, byte[] second) { byte[] result = new byte[first.length + second.length]; System.arraycopy(first, 0, result, 0, first.length); System.arraycopy(second, 0, result, first.length, second.length); return result; } }