Worldmap: PNG file names now contain the hash of the tiles that the map contains.

* This way, when a map has some replacements applied, the hash will change, thus making it possible to have several different PNG files for each map, each with its own combination of replacements.
* Only display maps on worldmap that are visited by the current player.
* Update worldmap after replacements have been applied.
This commit is contained in:
Oskar Wiksten
2013-06-22 16:19:10 +02:00
parent c34614f09b
commit e65492c837
13 changed files with 158 additions and 76 deletions

View File

@@ -18,7 +18,7 @@ public final class AndorsTrailApplication extends Application {
public static final boolean DEVELOPMENT_VALIDATEDATA = true;
public static final boolean DEVELOPMENT_DEBUGMESSAGES = true;
public static final boolean DEVELOPMENT_INCOMPATIBLE_SAVEGAMES = DEVELOPMENT_DEBUGRESOURCES;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? 999 : 35;
public static final int CURRENT_VERSION = DEVELOPMENT_INCOMPATIBLE_SAVEGAMES ? 999 : 36;
public static final String CURRENT_VERSION_DISPLAY = "0.7.0dev";
private final AndorsTrailPreferences preferences = new AndorsTrailPreferences();

View File

@@ -121,7 +121,7 @@ public final class MainActivity extends Activity implements PlayerMovementListen
break;
case INTENTREQUEST_CONVERSATION:
MovementController.refreshMonsterAggressiveness(world.model.currentMap, world.model.player);
controllers.mapController.applyCurrentMapReplacements(getResources());
controllers.mapController.applyCurrentMapReplacements(getResources(), true);
break;
case INTENTREQUEST_SAVEGAME:
if (resultCode != Activity.RESULT_OK) break;

View File

@@ -20,7 +20,6 @@ import com.gpl.rpg.AndorsTrail.model.item.Loot;
import com.gpl.rpg.AndorsTrail.model.quest.QuestLogEntry;
import com.gpl.rpg.AndorsTrail.model.quest.QuestProgress;
import com.gpl.rpg.AndorsTrail.util.ConstRange;
import com.gpl.rpg.AndorsTrail.util.Coord;
import java.util.ArrayList;

View File

@@ -8,9 +8,11 @@ import com.gpl.rpg.AndorsTrail.controller.listeners.WorldEventListeners;
import com.gpl.rpg.AndorsTrail.model.ability.SkillCollection;
import com.gpl.rpg.AndorsTrail.model.actor.Monster;
import com.gpl.rpg.AndorsTrail.model.actor.Player;
import com.gpl.rpg.AndorsTrail.model.map.*;
import com.gpl.rpg.AndorsTrail.model.map.LayeredTileMap;
import com.gpl.rpg.AndorsTrail.model.map.MapObject;
import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap;
import com.gpl.rpg.AndorsTrail.model.map.ReplaceableMapSection;
import com.gpl.rpg.AndorsTrail.util.Coord;
import com.gpl.rpg.AndorsTrail.util.CoordRect;
public final class MapController {
@@ -110,25 +112,26 @@ public final class MapController {
}
}
public void applyCurrentMapReplacements(final Resources res) {
if (!applyReplacements(world.model.currentTileMap)) return;
world.model.currentMap.visited = false; // Force worldmap png to be updated.
WorldMapController.updateWorldMapForCurrentMap(world, res);
world.model.currentMap.visited = true;
public void applyCurrentMapReplacements(final Resources res, boolean updateWorldmap) {
if (!applyReplacements(world.model.currentMap, world.model.currentTileMap)) return;
world.maps.worldMapRequiresUpdate = true;
if (!updateWorldmap) return;
WorldMapController.updateWorldMap(world, res);
mapLayoutListeners.onMapTilesChanged(world.model.currentMap, world.model.currentTileMap);
}
private boolean applyReplacements(LayeredTileMap map) {
if (map.replacements == null) return false;
private boolean applyReplacements(PredefinedMap map, LayeredTileMap tileMap) {
boolean hasUpdated = false;
for(ReplaceableMapSection replacement : map.replacements) {
if (replacement.isApplied) continue;
if (!satisfiesCondition(replacement)) continue;
map.applyReplacement(replacement);
hasUpdated = true;
if (tileMap.replacements != null) {
for(ReplaceableMapSection replacement : tileMap.replacements) {
if (replacement.isApplied) continue;
if (!satisfiesCondition(replacement)) continue;
tileMap.applyReplacement(replacement);
hasUpdated = true;
}
}
map.lastSeenLayoutHash = tileMap.getCurrentLayoutHash();
return hasUpdated;
}

View File

@@ -88,6 +88,7 @@ public final class MovementController implements TimedMessageTask.Callback {
private void playerVisitsMapFirstTime(PredefinedMap m) {
m.reset();
m.createAllContainerLoot();
world.maps.worldMapRequiresUpdate = true;
}
public void prepareMapAsCurrentMap(PredefinedMap newMap, Resources res, boolean spawnMonsters) {
@@ -99,12 +100,12 @@ public final class MovementController implements TimedMessageTask.Callback {
controllers.monsterSpawnController.spawnAll(newMap, model.currentTileMap);
}
}
controllers.mapController.applyCurrentMapReplacements(res);
WorldMapController.updateWorldMapForCurrentMap(world, res);
controllers.mapController.applyCurrentMapReplacements(res, false);
newMap.visited = true;
moveBlockedActors(newMap, model.currentTileMap);
refreshMonsterAggressiveness(newMap, model.player);
controllers.effectController.updateSplatters(newMap);
WorldMapController.updateWorldMap(world, res);
}
private boolean mayMovePlayer() {

View File

@@ -4,8 +4,7 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashSet;
import java.util.*;
import android.content.Context;
import android.content.Intent;
@@ -22,10 +21,7 @@ import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
import com.gpl.rpg.AndorsTrail.R;
import com.gpl.rpg.AndorsTrail.activity.DisplayWorldMapActivity;
import com.gpl.rpg.AndorsTrail.context.WorldContext;
import com.gpl.rpg.AndorsTrail.model.map.LayeredTileMap;
import com.gpl.rpg.AndorsTrail.model.map.MapLayer;
import com.gpl.rpg.AndorsTrail.model.map.PredefinedMap;
import com.gpl.rpg.AndorsTrail.model.map.WorldMapSegment;
import com.gpl.rpg.AndorsTrail.model.map.*;
import com.gpl.rpg.AndorsTrail.model.map.WorldMapSegment.NamedWorldMapArea;
import com.gpl.rpg.AndorsTrail.model.map.WorldMapSegment.WorldMapSegmentMap;
import com.gpl.rpg.AndorsTrail.resource.tiles.TileCollection;
@@ -39,11 +35,11 @@ public final class WorldMapController {
private static final int WORLDMAP_SCREENSHOT_TILESIZE = 8;
public static final int WORLDMAP_DISPLAY_TILESIZE = WORLDMAP_SCREENSHOT_TILESIZE;
public static void updateWorldMapForCurrentMap(final WorldContext world, final Resources res) {
public static void updateWorldMap(final WorldContext world, final Resources res) {
updateWorldMap(world, world.model.currentMap, world.model.currentTileMap, world.tileManager.currentMapTiles, res);
}
public static void updateWorldMap(
private static void updateWorldMap(
final WorldContext world,
final PredefinedMap map,
final LayeredTileMap mapTiles,
@@ -52,7 +48,7 @@ public final class WorldMapController {
final String worldMapSegmentName = world.maps.getWorldMapSegmentNameForMap(map.name);
if (worldMapSegmentName == null) return;
if (!shouldUpdateWorldMap(map, worldMapSegmentName)) return;
if (!shouldUpdateWorldMap(map, worldMapSegmentName, world.maps.worldMapRequiresUpdate)) return;
(new AsyncTask<Void, Void, Void>() {
@Override
@@ -61,6 +57,7 @@ public final class WorldMapController {
try {
updateCachedBitmap(map, renderer);
updateWorldMapSegment(res, world, worldMapSegmentName);
world.maps.worldMapRequiresUpdate = false;
if (AndorsTrailApplication.DEVELOPMENT_DEBUGMESSAGES) {
L.log("WorldMapController: Updated worldmap segment " + worldMapSegmentName + " for map " + map.name);
}
@@ -72,16 +69,15 @@ public final class WorldMapController {
}).execute();
}
private static boolean shouldUpdateWorldMap(PredefinedMap map, String worldMapSegmentName) {
private static boolean shouldUpdateWorldMap(PredefinedMap map, String worldMapSegmentName, boolean forceUpdate) {
if (forceUpdate) return true;
if (!map.visited) return true;
File file = getFileForMap(map);
if (!file.exists()) return true;
if (map.lastVisitVersion < AndorsTrailApplication.CURRENT_VERSION) return true;
file = getCombinedWorldMapFile(worldMapSegmentName);
if (!file.exists()) return true;
return false;
}
@@ -89,9 +85,7 @@ public final class WorldMapController {
ensureWorldmapDirectoryExists();
File file = getFileForMap(map);
if (file.exists()) {
if (map.lastVisitVersion == AndorsTrailApplication.CURRENT_VERSION) return;
}
if (file.exists()) return;
Bitmap image = renderer.drawMap();
FileOutputStream fos = new FileOutputStream(file);
@@ -99,6 +93,17 @@ public final class WorldMapController {
fos.flush();
fos.close();
image.recycle();
L.log("WorldMapController: Wrote " + file.getAbsolutePath());
// Before we had the hash as part of the filename, the png files were just named [mapname].png
// let's just remove those old files since the new hash-based filenames contain the same data.
if (map.lastSeenLayoutHash.length() > 0) {
File oldFile = getFileForMap(map.name);
if (!oldFile.getName().equalsIgnoreCase(file.getName())) {
oldFile.delete();
L.log("WorldMapController: Deleted " + oldFile.getAbsolutePath());
}
}
}
private static final class MapRenderer {
@@ -155,11 +160,14 @@ public final class WorldMapController {
if (!dir.exists()) dir.mkdir();
dir = new File(dir, Constants.FILENAME_WORLDMAP_DIRECTORY);
if (!dir.exists()) dir.mkdir();
File noMediaFile = new File(dir, ".nomedia");
if (!noMediaFile.exists()) noMediaFile.createNewFile();
}
private static File getFileForMap(PredefinedMap map) { return getFileForMap(map.name); }
private static File getFileForMap(PredefinedMap map) {
if (map.lastSeenLayoutHash.length() <= 0) return getFileForMap(map.name);
else return getFileForMap(map.name + "." + map.lastSeenLayoutHash);
}
private static File getFileForMap(String mapName) {
return new File(getWorldmapDirectory(), mapName + ".png");
}
@@ -172,19 +180,19 @@ public final class WorldMapController {
return new File(getWorldmapDirectory(), Constants.FILENAME_WORLDMAP_HTMLFILE_PREFIX + segmentName + Constants.FILENAME_WORLDMAP_HTMLFILE_SUFFIX);
}
private static boolean shouldDisplayMapOnWorldmap(String mapName) {
File f = WorldMapController.getFileForMap(mapName);
return f.exists();
}
private static String getWorldMapSegmentAsHtml(Resources res, WorldContext world, String segmentName) {
WorldMapSegment segment = world.maps.worldMapSegments.get(segmentName);
Collection<String> displayedMapNames = new HashSet<String>();
Map<String, File> displayedMapFilenamesPerMapName = new HashMap<String, File>(segment.maps.size());
Coord offsetWorldmapTo = new Coord(999999, 999999);
for (WorldMapSegmentMap map : segment.maps.values()) {
if (!shouldDisplayMapOnWorldmap(map.mapName)) continue;
PredefinedMap predefinedMap = world.maps.findPredefinedMap(map.mapName);
if (predefinedMap == null) continue;
if (!predefinedMap.visited) continue;
File f = WorldMapController.getFileForMap(predefinedMap);
if (!f.exists()) continue;
displayedMapFilenamesPerMapName.put(map.mapName, f);
displayedMapNames.add(map.mapName);
offsetWorldmapTo.x = Math.min(offsetWorldmapTo.x, map.worldPosition.x);
offsetWorldmapTo.y = Math.min(offsetWorldmapTo.y, map.worldPosition.y);
}
@@ -193,8 +201,8 @@ public final class WorldMapController {
StringBuilder mapsAsHtml = new StringBuilder(1000);
for (WorldMapSegmentMap segmentMap : segment.maps.values()) {
File f = WorldMapController.getFileForMap(segmentMap.mapName);
if (!f.exists()) continue;
File f = displayedMapFilenamesPerMapName.get(segmentMap.mapName);
if (f == null) continue;
Size size = getMapSize(segmentMap, world);
mapsAsHtml
@@ -223,7 +231,7 @@ public final class WorldMapController {
StringBuilder namedAreasAsHtml = new StringBuilder(500);
for (NamedWorldMapArea area : segment.namedAreas.values()) {
CoordRect r = determineNamedAreaBoundary(area, segment, world, displayedMapNames);
CoordRect r = determineNamedAreaBoundary(area, segment, world, displayedMapFilenamesPerMapName.keySet());
if (r == null) continue;
namedAreasAsHtml
.append("<div class=\"namedarea ")
@@ -255,7 +263,7 @@ public final class WorldMapController {
return world.maps.findPredefinedMap(map.mapName).size;
}
private static CoordRect determineNamedAreaBoundary(NamedWorldMapArea area, WorldMapSegment segment, WorldContext world, Collection<String> displayedMapNames) {
private static CoordRect determineNamedAreaBoundary(NamedWorldMapArea area, WorldMapSegment segment, WorldContext world, Set<String> displayedMapNames) {
Coord topLeft = null;
Coord bottomRight = null;

View File

@@ -18,6 +18,7 @@ public final class LayeredTileMap {
private final Size size;
public final MapSection currentLayout;
private String currentLayoutHash;
public final ReplaceableMapSection[] replacements;
public final String colorFilter;
public final Collection<Integer> usedTileIDs;
@@ -32,6 +33,7 @@ public final class LayeredTileMap {
this.replacements = replacements;
this.colorFilter = colorFilter;
this.usedTileIDs = usedTileIDs;
this.currentLayoutHash = currentLayout.calculateHash();
}
public final boolean isWalkable(final Coord p) {
@@ -89,7 +91,12 @@ public final class LayeredTileMap {
});
}
public String getCurrentLayoutHash() {
return currentLayoutHash;
}
public void applyReplacement(ReplaceableMapSection replacement) {
replacement.apply(currentLayout);
currentLayoutHash = currentLayout.calculateHash();
}
}

View File

@@ -17,6 +17,7 @@ import java.util.List;
public final class MapCollection {
private final HashMap<String, PredefinedMap> predefinedMaps = new HashMap<String, PredefinedMap>();
public final HashMap<String, WorldMapSegment> worldMapSegments = new HashMap<String, WorldMapSegment>();
public boolean worldMapRequiresUpdate = true;
public MapCollection() {}
@@ -43,6 +44,7 @@ public final class MapCollection {
for (PredefinedMap m : getAllMaps()) {
m.reset();
}
worldMapRequiresUpdate = true;
}
public String getWorldMapSegmentNameForMap(String mapName) {

View File

@@ -1,5 +1,6 @@
package com.gpl.rpg.AndorsTrail.model.map;
import com.gpl.rpg.AndorsTrail.util.ByteUtils;
import com.gpl.rpg.AndorsTrail.util.CoordRect;
public final class MapSection {
@@ -7,16 +8,19 @@ public final class MapSection {
public final MapLayer layerObjects;
public final MapLayer layerAbove;
public final boolean[][] isWalkable;
private final byte[] layoutHash;
public MapSection(
MapLayer layerGround,
MapLayer layerObjects,
MapLayer layerAbove,
boolean[][] isWalkable) {
boolean[][] isWalkable,
byte[] layoutHash) {
this.layerGround = layerGround;
this.layerObjects = layerObjects;
this.layerAbove = layerAbove;
this.isWalkable = isWalkable;
this.layoutHash = layoutHash;
}
public void replaceLayerContentsWith(final MapSection replaceLayersWith, final CoordRect replacementArea) {
@@ -30,6 +34,9 @@ public final class MapSection {
System.arraycopy(replaceLayersWith.isWalkable[sx], 0, isWalkable[dx], dy, height);
}
}
for(int i = 0; i < layoutHash.length; ++i) {
layoutHash[i] ^= replaceLayersWith.layoutHash[i];
}
}
private static void replaceTileLayerSection(MapLayer dest, MapLayer src, CoordRect area) {
@@ -40,4 +47,8 @@ public final class MapSection {
System.arraycopy(src.tiles[sx], 0, dest.tiles[dx], dy, height);
}
}
public String calculateHash() {
return ByteUtils.toHexString(layoutHash);
}
}

View File

@@ -29,7 +29,7 @@ public final class PredefinedMap {
public final ArrayList<Loot> groundBags = new ArrayList<Loot>();
public boolean visited = false;
public long lastVisitTime = VISIT_RESET;
public int lastVisitVersion = 0;
public String lastSeenLayoutHash = "";
private final boolean isOutdoors;
public final ArrayList<BloodSplatter> splatters = new ArrayList<BloodSplatter>();
@@ -136,7 +136,7 @@ public final class PredefinedMap {
}
groundBags.clear();
visited = false;
lastVisitVersion = 0;
lastSeenLayoutHash = "";
}
public boolean isRecentlyVisited() {
@@ -145,7 +145,6 @@ public final class PredefinedMap {
}
public void updateLastVisitTime() {
lastVisitTime = System.currentTimeMillis();
lastVisitVersion = AndorsTrailApplication.CURRENT_VERSION;
}
public void resetTemporaryData() {
for(MonsterSpawnArea a : spawnAreas) {
@@ -213,10 +212,13 @@ public final class PredefinedMap {
lastVisitTime = src.readLong();
if (visited) {
if (fileversion <= 30) lastVisitVersion = 30;
else lastVisitVersion = src.readInt();
if (fileversion > 30 && fileversion < 36) {
/*int lastVisitVersion = */src.readInt();
}
if (fileversion < 36) lastSeenLayoutHash = "";
else lastSeenLayoutHash = src.readUTF();
}
for(int i = loadedSpawnAreas; i < spawnAreas.length; ++i) {
MonsterSpawnArea area = this.spawnAreas[i];
if (area.isUnique && visited) controllers.monsterSpawnController.spawnAllInArea(this, null, area, true);
@@ -235,6 +237,8 @@ public final class PredefinedMap {
}
dest.writeBoolean(visited);
dest.writeLong(lastVisitTime);
if (visited) dest.writeInt(lastVisitVersion);
if (visited) {
dest.writeUTF(lastSeenLayoutHash);
}
}
}

View File

@@ -1,19 +1,19 @@
package com.gpl.rpg.AndorsTrail.model.map;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
import org.xmlpull.v1.XmlPullParserException;
import com.gpl.rpg.AndorsTrail.util.Base64;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.XmlResourceParserUtils;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import com.gpl.rpg.AndorsTrail.util.Base64;
import com.gpl.rpg.AndorsTrail.util.L;
import com.gpl.rpg.AndorsTrail.util.XmlResourceParserUtils;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
public final class TMXMapFileParser {
public static TMXObjectMap readObjectMap(Resources r, int xmlResourceId, String name) {
@@ -200,11 +200,17 @@ public final class TMXMapFileParser {
for(int y = 0; y < layer.height; ++y) {
for(int x = 0; x < layer.width; ++x, i += 4) {
int gid = readIntLittleEndian(buffer, i);
//if (gid != 0) L.log(getHexString(buffer, i) + " -> " + gid);
layer.gids[x][y] = gid;
//L.log("(" + x + "," + y + ") : " + layer.gids[x][y]);
}
}
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(buffer);
layer.layoutHash = digest.digest();
} catch (NoSuchAlgorithmException e) {
throw new IOException("Failed to create layout hash for map layer " + layer.name);
}
}
private static TMXProperty readTMXProperty(XmlResourceParser xrp) {
@@ -275,6 +281,7 @@ public final class TMXMapFileParser {
public int width;
public int height;
public int[][] gids;
public byte[] layoutHash;
}
public static final class TMXObjectGroup {
public String name;

View File

@@ -1,5 +1,7 @@
package com.gpl.rpg.AndorsTrail.model.map;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import com.gpl.rpg.AndorsTrail.AndorsTrailApplication;
@@ -244,11 +246,12 @@ public final class TMXMapTranslator {
HashSet<Integer> usedTileIDs,
SetOfLayerNames layerNames
) {
final MapLayer layerGround = transformMapLayer(findLayer(layersPerLayerName, layerNames.groundLayerName, srcMap.name), srcMap, tileCache, area, usedTileIDs);
final MapLayer layerObjects = transformMapLayer(findLayer(layersPerLayerName, layerNames.objectsLayerName, srcMap.name), srcMap, tileCache, area, usedTileIDs);
final MapLayer layerAbove = transformMapLayer(findLayer(layersPerLayerName, layerNames.aboveLayersName, srcMap.name), srcMap, tileCache, area, usedTileIDs);
final MapLayer layerGround = transformMapLayer(layersPerLayerName, layerNames.groundLayerName, srcMap, tileCache, area, usedTileIDs);
final MapLayer layerObjects = transformMapLayer(layersPerLayerName, layerNames.objectsLayerName, srcMap, tileCache, area, usedTileIDs);
final MapLayer layerAbove = transformMapLayer(layersPerLayerName, layerNames.aboveLayersName, srcMap, tileCache, area, usedTileIDs);
boolean[][] isWalkable = transformWalkableMapLayer(findLayer(layersPerLayerName, layerNames.walkableLayersName, srcMap.name), area);
return new MapSection(layerGround, layerObjects, layerAbove, isWalkable);
byte[] layoutHash = calculateLayoutHash(srcMap, layersPerLayerName, layerNames);
return new MapSection(layerGround, layerObjects, layerAbove, isWalkable, layoutHash);
}
private static TMXLayer findLayer(HashMap<String, TMXLayer> layersPerLayerName, String layerName, String mapName) {
@@ -264,12 +267,14 @@ public final class TMXMapTranslator {
}
private static MapLayer transformMapLayer(
TMXLayer srcLayer,
HashMap<String, TMXLayer> layersPerLayerName,
String layerName,
TMXLayerMap srcMap,
TileCache tileCache,
CoordRect area,
HashSet<Integer> usedTileIDs
) {
TMXLayer srcLayer = findLayer(layersPerLayerName, layerName, srcMap.name);
if (srcLayer == null) return null;
final MapLayer result = new MapLayer(area.size);
Tile tile = new Tile();
@@ -305,6 +310,26 @@ public final class TMXMapTranslator {
return isWalkable;
}
private static byte[] calculateLayoutHash(TMXLayerMap map, HashMap<String, TMXLayer> layersPerLayerName, SetOfLayerNames layerNames) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digestLayer(layersPerLayerName, layerNames.groundLayerName, map, digest);
digestLayer(layersPerLayerName, layerNames.objectsLayerName, map, digest);
digestLayer(layersPerLayerName, layerNames.aboveLayersName, map, digest);
return digest.digest();
} catch (NoSuchAlgorithmException e) {
L.log("ERROR: Failed to create layout hash for map " + map.name + " : " + e.toString());
}
return new byte[0];
}
private static void digestLayer(HashMap<String, TMXLayer> layersPerLayerName, String layerName, TMXLayerMap map, MessageDigest digest) {
TMXLayer srcLayer = findLayer(layersPerLayerName, layerName, map.name);
if (srcLayer == null) return;
if (srcLayer.layoutHash == null) return;
digest.update(srcLayer.layoutHash);
}
private static boolean getTile(final TMXLayerMap map, final int gid, final Tile dest) {
for(int i = map.tileSets.length - 1; i >= 0; --i) {
TMXTileSet ts = map.tileSets[i];

View File

@@ -0,0 +1,15 @@
package com.gpl.rpg.AndorsTrail.util;
public final class ByteUtils {
public static String toHexString(byte[] bytes) {
if (bytes == null) return "";
if (bytes.length == 0) return "";
StringBuffer result = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
String h = Integer.toHexString(0xFF & bytes[i]);
if (h.length() < 2) result.append('0');
result.append(h);
}
return result.toString();
}
}