/******************************************************************************* * Copyright (c) 2015 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.logic.map.loading.original; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import jsettlers.common.buildings.EBuildingType; import jsettlers.common.map.object.BuildingObject; import jsettlers.common.map.object.MapObject; import jsettlers.common.position.RelativePoint; import jsettlers.common.position.ShortPoint2D; import jsettlers.logic.map.loading.EMapStartResources; /** * @author Thomas Zeugner */ public class OriginalMapFileContentReader { // --------------------------------------------------// public static class MapResourceInfo { OriginalMapFileDataStructs.EMapFilePartType partType; public int offset = 0; public int size = 0; public int cryptKey = 0; public boolean hasBeenDecrypted = false; } // --------------------------------------------------// private final List<MapResourceInfo> resources; public int fileChecksum = 0; public int widthHeight; public boolean isSinglePlayerMap = false; private boolean hasBuildings = false; private byte[] mapContent; @SuppressWarnings("unused") private EMapStartResources startResources = EMapStartResources.HIGH_GOODS; private String mapQuestTip = null; private String mapQuestText = null; private short previewImage[] = null; private short previewWidth = 0; private short previewHeight = 0; public OriginalMapFileContent mapData = new OriginalMapFileContent(0); /** * Charset of read strings */ private static final Charset TEXT_CHARSET = Charset.forName("ISO-8859-1"); public OriginalMapFileContentReader(InputStream originalMapFile) throws IOException { // - init Resource Info resources = new LinkedList<MapResourceInfo>(); // - init players mapData.setPlayerCount(1); // - read File into buffer mapContent = getBytesFromInputStream(originalMapFile); } // - reads the whole stream and returns it as BYTE-Array public static byte[] getBytesFromInputStream(InputStream is) throws IOException { // - read file to buffer try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { byte[] buffer = new byte[0xFFFF]; for (int len; (len = is.read(buffer)) != -1;) { os.write(buffer, 0, len); } os.flush(); return os.toByteArray(); } catch (Exception e) { return new byte[0]; } } // - Read UNSIGNED Byte from Buffer public int readByteFrom(int offset) { if (mapContent == null) return 0; return mapContent[offset] & 0xFF; } // - Read Big-Ending INT from Buffer public int readBEIntFrom(int offset) { if (mapContent == null) return 0; return ((mapContent[offset] & 0xFF)) | ((mapContent[offset + 1] & 0xFF) << 8) | ((mapContent[offset + 2] & 0xFF) << 16) | ((mapContent[offset + 3] & 0xFF) << 24); } // - Read Big-Ending 2 Byte Number from Buffer public int readBEWordFrom(int offset) { if (mapContent == null) return 0; return ((mapContent[offset] & 0xFF)) | ((mapContent[offset + 1] & 0xFF) << 8); } // - read the Higher 4-Bit of the buffer public int readHighNibbleFrom(int offset) { if (mapContent == null) return 0; return (mapContent[offset] >> 4) & 0x0F; } // - read the Lower 4-Bit of the buffer public int readLowNibbleFrom(int offset) { if (mapContent == null) return 0; return (mapContent[offset]) & 0x0F; } // - read a C-Style String from Buffer (ends with the first \0) public String readCStrFrom(int offset, int length) { if (mapContent == null) return ""; if (mapContent.length <= offset + length) return ""; // - find \0 char in buffer int i = 0; for (; i < length; i++) { if (mapContent[offset + i] == 0) { break; } } if (i == 0) { return ""; } // - substring + encoding return new String(mapContent, offset, i - 1, TEXT_CHARSET); } // - returns a File Resources private MapResourceInfo findResource(OriginalMapFileDataStructs.EMapFilePartType type) { for (MapResourceInfo element : resources) { if (element.partType == type) return element; } return null; } // - calculates the checksum of the file and compares it boolean isChecksumValid() { // - read Checksum from File int fileChecksum = readBEIntFrom(0); mapData.fileChecksum = fileChecksum; // - make "count" a Multiple of four int count = mapContent.length & 0xFFFFFFFC; int currentChecksum = 0; // - Map Content starts at Byte 8 for (int i = 8; i < count; i += 4) { // - read DWord int currentInt = ((mapContent[i] & 0xFF)) | ((mapContent[i + 1] & 0xFF) << 8) | ((mapContent[i + 2] & 0xFF) << 16) | ((mapContent[i + 3] & 0xFF) << 24); // - using: Logic Right-Shift-Operator: >>> currentChecksum = ((currentChecksum >>> 31) | ((currentChecksum << 1) ^ currentInt)); } // - return TRUE if the checksum is OK! return (currentChecksum == fileChecksum); } // - Reads in the Map-File-Structure boolean loadMapResources() { // - Version of File: 0x0A : Original Settlers Map ; 0x0B : Amazon Map int fileVersion = readBEIntFrom(4); // - check if the Version is compatible? if ((fileVersion != OriginalMapFileDataStructs.EMapFileVersion.DEFAULT.value) && (fileVersion != OriginalMapFileDataStructs.EMapFileVersion.AMAZONS.value)) return false; // - Data length int dataLength = mapContent.length; // - start of map-content int filePos = 8; int partTypeTemp; do { partTypeTemp = readBEIntFrom(filePos); int partLen = readBEIntFrom(filePos + 4); // - don't know what the [FileTypeSub] is for -> it should by zero int partType = (partTypeTemp & 0x0000FFFF); // - position/start of data int mapPartPos = filePos + 8; // - debug output // System.out.println("@ "+ filePos +" size: "+ PartLen +" Type:"+ partType +" Sub:"+ PartTypeSub +" -> "+ // OriginalMapFileDataStructs.EMapFilePartType .getTypeByInt(partType).toString() ); // - next position in File filePos = filePos + partLen; // - save the values if ((partType > 0) && (partType < OriginalMapFileDataStructs.EMapFilePartType.length) && (partLen >= 0)) { MapResourceInfo newRes = new MapResourceInfo(); newRes.partType = OriginalMapFileDataStructs.EMapFilePartType.getTypeByInt(partType); newRes.cryptKey = partType; newRes.hasBeenDecrypted = false; newRes.offset = mapPartPos; newRes.size = partLen - 8; resources.add(newRes); } } while ((partTypeTemp != 0) && ((filePos + 8) <= dataLength)); return true; } // - freeing the internal File-Buffer public void freeBuffer() { mapContent = null; mapData.freeBuffer(); } // - to process a map File this class loads the whole file to memory. To save memory this File-Buffer is // - closed after using/when done processing. If more data are requested from the File, the File-Biffer // - is loaded again with this reOpen() function. public void reOpen(InputStream originalMapFile) { // - read File into buffer try { mapContent = getBytesFromInputStream(originalMapFile); } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } // - reset Crypt Info for (MapResourceInfo element : resources) { element.hasBeenDecrypted = false; } } public void readBasicMapInformation() { this.readBasicMapInformation(0, 0); } public void readBasicMapInformation(int previewWidth, int previewHeight) { // - Reset fileChecksum = 0; widthHeight = 0; hasBuildings = false; // - safety checks if (mapContent == null) return; if (mapContent.length < 100) return; // - checksum is the first DWord in File fileChecksum = readBEIntFrom(0); // - read Map Information readMapInfo(); readPlayerInfo(); readMapQuestText(); readMapQuestTip(); // - create preview Image for cache if ((previewWidth > 0) && (previewHeight > 0)) { this.previewImage = getPreviewImage(previewWidth, previewHeight); this.previewWidth = (short) previewWidth; this.previewHeight = (short) previewHeight; } // - get resource information for the area to get map height and width MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.AREA); if (filePart == null) return; if (filePart.size < 4) return; // TODO: original map: the whole AREA-Block is decrypted but we only need the first 4 byte. Problem... maybe later we need the rest but only // if this map is selected for playing AND there was no freeBuffer() and reOpen() call in between. // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return; // - file position of this part int pos = filePart.offset; // - read height and width (they are the same) widthHeight = readBEIntFrom(pos); } public short[] getPreviewImage() { // - return cached Image return previewImage; } public short[] getPreviewImage(int width, int height) { // - return cached Image if available if ((previewWidth == width) && (previewHeight == height) && (previewImage != null)) { return previewImage; } // - create new Image short[] outImg = new short[width * height]; // - get resource information for the area MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.PREVIEW); if (filePart == null) return outImg; if (filePart.size < 4) return outImg; // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return outImg; // - file position int pos = filePart.offset; // - height and width are the same int wh = readBEWordFrom(pos); pos += 2; @SuppressWarnings("unused") int unknown = readBEWordFrom(pos); pos += 2; float scaleX = wh / width; float scaleY = wh / height; int outIndex = 0; int offset = pos; for (int y = 0; y < height; y++) { int srcRow = offset + ((int) (Math.floor(scaleY * y)) * wh) * 2; for (int x = 0; x < width; x++) { int inIndex = srcRow + ((int) Math.floor(x * scaleX)) * 2; int colorValue = ((mapContent[inIndex] & 0xFF)) | ((mapContent[inIndex + 1] & 0xFF) << 8); // - the Settlers Remake uses Short-Colors like argb_1555 (alpha, r, g, b) outImg[outIndex] = (short) (1 | colorValue << 1); outIndex++; } } return outImg; } public String readMapQuestText() { if (mapQuestText != null) return mapQuestText; MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.QUEST_TEXT); if ((filePart == null) || (filePart.size == 0)) return ""; // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return ""; // - read Text mapQuestText = readCStrFrom(filePart.offset, filePart.size); // System.out.println("Quest: "+ mapQuestText); return mapQuestText; } public String readMapQuestTip() { if (mapQuestTip != null) return mapQuestTip; MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.QUEST_TIP); if ((filePart == null) || (filePart.size == 0)) return ""; // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return ""; // - read Text mapQuestTip = readCStrFrom(filePart.offset, filePart.size); // System.out.println("Tip: "+ mapQuestTip); return mapQuestTip; } // - Read some common information from the map-file public void readMapInfo() { MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.MAP_INFO); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No Player information available in mapfile!"); return; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return; // - file position int pos = filePart.offset; // ---------------------------------- // - read mapType (single / multiplayer map?) int mapType = readBEIntFrom(pos); pos += 4; if (mapType == 1) { isSinglePlayerMap = true; } else if (mapType == 0) { isSinglePlayerMap = false; } else { System.err.println("wrong value for 'isSinglePlayerMap' " + Integer.toString(mapType) + " in mapfile!"); } // ---------------------------------- // - read Player count int playerCount = readBEIntFrom(pos); pos += 4; mapData.setPlayerCount(playerCount); // ---------------------------------- // - read start resources int startResourcesValue = readBEIntFrom(pos); this.startResources = EMapStartResources.fromMapValue(startResourcesValue); } // - read buildings from the map-file public boolean readBuildings() { hasBuildings = false; MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.BUILDINGS); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No Buildings available in mapfile!"); return false; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return false; // - file position int pos = filePart.offset; // - Number of buildings int buildingsCount = readBEIntFrom(pos); pos += 4; // - safety check if ((buildingsCount * 12 > filePart.size) || (buildingsCount < 0)) { System.err.println("wrong number of buildings in map File: " + buildingsCount); return false; } hasBuildings = true; // - read all Buildings for (int i = 0; i < buildingsCount; i++) { int party = readByteFrom(pos++); // - Party starts with 0 int buildingType = readByteFrom(pos++); int posX = readBEWordFrom(pos); pos += 2; int posY = readBEWordFrom(pos); pos += 2; pos++; // not used - maybe a filling byte to make the record 12 Byte (= 3 INTs) long or unknown?! // ----------- // - number of soldier in building is saved as 4-Bit (=Nibble): int countSword1 = readHighNibbleFrom(pos); int countSword2 = readLowNibbleFrom(pos); pos++; int countArcher2 = readHighNibbleFrom(pos); int countArcher3 = readLowNibbleFrom(pos); pos++; int countSword3 = readHighNibbleFrom(pos); int countArcher1 = readLowNibbleFrom(pos); pos++; int countSpear3 = readHighNibbleFrom(pos); // low nibble is a not used count pos++; int countSpear1 = readHighNibbleFrom(pos); int countSpear2 = readLowNibbleFrom(pos); pos++; // ------------- // - update data mapData.setBuilding(posX, posY, buildingType, party, countSword1, countSword2, countSword3, countArcher1, countArcher2, countArcher3, countSpear1, countSpear2, countSpear3); } return true; } // - Read stacks from the map-file public boolean readStacks() { MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.STACKS); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No Stacks available in mapfile!"); return false; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return false; // - file position int pos = filePart.offset; // - Number of buildings int stackCount = readBEIntFrom(pos); pos += 4; // - safety check if ((stackCount * 8 > filePart.size) || (stackCount < 0)) { System.err.println("wrong number of stacks in map File: " + stackCount); return false; } // - read all Stacks for (int i = 0; i < stackCount; i++) { int posX = readBEWordFrom(pos); pos += 2; int posY = readBEWordFrom(pos); pos += 2; int stackType = readByteFrom(pos++); int count = readByteFrom(pos++); pos += 2; // not used - maybe: padding to size of 8 (2 INTs) // ------------- // - update data mapData.setStack(posX, posY, stackType, count); } return true; } // - Read settlers from the map-file public boolean readSettlers() { MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.SETTLERS); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No Settlers available in mapfile!"); return false; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return false; // - file position int pos = filePart.offset; // - Number of buildings int settlerCount = readBEIntFrom(pos); pos += 4; // - safety check if ((settlerCount * 6 > filePart.size) || (settlerCount < 0)) { System.err.println("wrong number of settlers in map File: " + settlerCount); return false; } // - read all Stacks for (int i = 0; i < settlerCount; i++) { int party = readByteFrom(pos++); int settlerType = readByteFrom(pos++); int posX = readBEWordFrom(pos); pos += 2; int posY = readBEWordFrom(pos); pos += 2; // ------------- // - update data mapData.setSettler(posX, posY, settlerType, party); } return true; } // - Read the Player Info public void readPlayerInfo() { MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.PLAYER_INFO); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No Player information available in mapfile!"); return; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return; // - file position int pos = filePart.offset; for (int i = 0; i < mapData.getPlayerCount(); i++) { int nation = readBEIntFrom(pos); pos += 4; int startX = readBEIntFrom(pos); pos += 4; int startY = readBEIntFrom(pos); pos += 4; String playerName = readCStrFrom(pos, 33); pos += 33; mapData.setPlayer(i, startX, startY, nation, playerName); } } // - Reads in the Map Data / Landscape and MapObjects like trees public boolean readMapData() { // - get resource information for the area MapResourceInfo filePart = findResource(OriginalMapFileDataStructs.EMapFilePartType.AREA); if ((filePart == null) || (filePart.size == 0)) { System.err.println("Warning: No area information available in mapfile!"); return false; } // - Decrypt this resource if necessary if (!doDecrypt(filePart)) return false; // - file position int pos = filePart.offset; // - height and width are the same int widthHeight = readBEIntFrom(pos); pos += 4; // - init size of MapData mapData.setWidthHeight(widthHeight); // - points to read int dataCount = widthHeight * widthHeight; for (int i = 0; i < dataCount; i++) { mapData.setLandscapeHeight(i, readByteFrom(pos++)); mapData.setLandscape(i, readByteFrom(pos++)); mapData.setMapObject(i, readByteFrom(pos++)); readByteFrom(pos++); // - which Player is the owner of this position mapData.setAccessible(i, mapContent[pos++]); mapData.setResources(i, readHighNibbleFrom(pos), readLowNibbleFrom(pos)); pos++; } return true; } public void addStartTowerMaterialsAndSettlers(EMapStartResources startResources) { // - only if there are no buildings if (hasBuildings) return; int playerCount = mapData.getPlayerCount(); for (byte playerId = 0; playerId < playerCount; playerId++) { ShortPoint2D startPoint = mapData.getStartPoint(playerId); // - add the start Tower for this player mapData.setMapObject(startPoint.x, startPoint.y, new BuildingObject(EBuildingType.TOWER, playerId)); // - list of all objects that have to be added for this player List<MapObject> mapObjects = EMapStartResources.generateStackObjects(startResources); mapObjects.addAll(EMapStartResources.generateMovableObjects(startResources, playerId)); // - blocking area of the tower List<RelativePoint> towerTiles = Arrays.asList(EBuildingType.TOWER.getProtectedTiles()); RelativePoint relativeMapObjectPoint = new RelativePoint(-3, 3); for (MapObject currentMapObject : mapObjects) { do { // - get next point relativeMapObjectPoint = nextPointOnSpiral(relativeMapObjectPoint); // - don't put things under the tower if (towerTiles.contains(relativeMapObjectPoint)) continue; // - get absolute position int x = relativeMapObjectPoint.calculateX(startPoint.x); int y = relativeMapObjectPoint.calculateY(startPoint.y); // - is this place free? if (mapData.getMapObject(x, y) == null) { // - add Object mapData.setMapObject(x, y, currentMapObject); // - break DO: next object... break; } } while (true); } } } private RelativePoint nextPointOnSpiral(RelativePoint previousPoint) { short previousX = previousPoint.getDx(); short previousY = previousPoint.getDy(); short basis = (short) Math.max(Math.abs(previousX), Math.abs(previousY)); if (previousX == basis && previousY > -basis) return new RelativePoint(previousX, previousY - 1); if (previousX == -basis && previousY <= basis) return new RelativePoint(previousX, previousY + 1); if (previousX < basis && previousY == basis) return new RelativePoint(previousX + 1, previousY); if (previousX > -basis && previousY == -basis) return new RelativePoint(previousX - 1, previousY); return null; } // - Decrypt a file resource private boolean doDecrypt(MapResourceInfo filePart) { if (filePart == null) return false; if (mapContent == null) { System.err.println("OriginalMapFile-Warning: Unable to decrypt map file: no data loaded!"); return false; } // - already encrypted if (filePart.hasBeenDecrypted) return true; // - length of data int length = filePart.size; if (length <= 0) return true; // - start of data int pos = filePart.offset; // - check if the file has enough data if ((pos + length) >= mapContent.length) { System.err.println("Error: Unable to decrypt map file: out of data!"); return false; } // - init the key int key = (filePart.cryptKey & 0xFF); for (int i = length; i > 0; i--) { // - read one byte and uncrypt it int byt = (mapContent[pos] ^ key); // - calculate next Key key = (key << 1) ^ byt; // - write Byte mapContent[pos] = (byte) byt; pos++; } filePart.hasBeenDecrypted = true; return true; } }