PATCH Submit: Map Importer

Developer discussion regarding MapTool 1.4

Moderators: dorpond, trevor, Azhrei

Forum rules
Posting now open to all registered forum users.
Post Reply
Wyrframe
Cave Troll
Posts: 59
Joined: Sun Apr 14, 2013 7:52 pm

PATCH Submit: Map Importer

Post by Wyrframe »

After much study of the Map export/import format, I decided it was going to be far easier to make an importer for MapTool, than a MapTool-compatible exporter for my map editor software.

Submitted for your consideration: below patch file, and below-linked example map (SLBM being a renamed ZIP file; SLBM standing for Simple Logical Background Map).

https://www.dropbox.com/s/mokjpo5nhqig9 ... .slbm?dl=0

Patch file (below) includes:
* Change to PersistenceUtils.loadMap(File):PersistedMap to test for an SLBM extension, and to load using the SLBMLoader instead of the built-in PersistedMap loader if present.
* Old PersistenceUtils.loadMap(File):PersistedMap functionality moved to new method PersistenceUtils.loadMap(PackedFile):PersistedMap
* New class org.moonshot.maptool.util.SLBMLoader added. Code is offered freely for use, with the usual assumption of credit. If you need me to provide an explicit license, expect me to forward a Creative Commons CC-BY license on request.
Patch

Code: Select all

Index: src/net/rptools/maptool/util/PersistenceUtil.java
===================================================================
--- src/net/rptools/maptool/util/PersistenceUtil.java	(revision 5987)
+++ src/net/rptools/maptool/util/PersistenceUtil.java	(working copy)
@@ -65,6 +65,7 @@
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.log4j.Logger;
+import org.moonshot.maptool.util.SLBMLoader;
 
 import com.caucho.hessian.io.HessianInput;
 import com.thoughtworks.xstream.XStream;
@@ -171,47 +172,65 @@
 	}
 
 	public static PersistedMap loadMap(File mapFile) throws IOException {
-		PackedFile pakFile = null;
+	
 		try {
-			pakFile = new PackedFile(mapFile);
+			if (mapFile.getName().endsWith(".slbm"))
+			{
+				return new SLBMLoader().loadMap(mapFile);
+			}
+			else
+			{
+				PackedFile pakFile = null;
+				try {
+					return loadMap(new PackedFile(mapFile));
 
-			// Sanity check
-			String progVersion = (String) pakFile.getProperty(PROP_VERSION);
-			if (!versionCheck(progVersion))
-				return null;
+				} finally {
+					if (pakFile != null)
+						pakFile.close();
+				}
+			}
 
-			PersistedMap persistedMap = (PersistedMap) pakFile.getContent();
-
-			// Now load up any images that we need
-			loadAssets(persistedMap.assetMap.keySet(), pakFile);
-
-			// FJE We only want the token's graphical data, so loop through all tokens and
-			// destroy all properties and macros.  Keep some fields, though.  Since that type
-			// of object editing doesn't belong here, we just call Token.imported() and let
-			// that method Do The Right Thing.
-			for (Iterator<Token> iter = persistedMap.zone.getAllTokens().iterator(); iter.hasNext();) {
-				Token token = iter.next();
-				token.imported();
-			}
-			// XXX FJE This doesn't work the way I want it to.  But doing this the Right Way
-			// is too much work right now. :-}
-			Zone z = persistedMap.zone;
-			String n = fixupZoneName(z.getName());
-			z.setName(n);
-			z.imported(); // Resets creation timestamp and init panel, among other things
-			z.optimize(); // Collapses overlaid or redundant drawables
-			return persistedMap;
 		} catch (ConversionException ce) {
 			MapTool.showError("PersistenceUtil.error.mapVersion", ce);
 		} catch (IOException ioe) {
 			MapTool.showError("PersistenceUtil.error.mapRead", ioe);
-		} finally {
-			if (pakFile != null)
-				pakFile.close();
+		} catch (Throwable t) {
+			MapTool.showError("PersistenceUtil.error.unknown", t);
 		}
 		return null;
 	}
 
+	protected static PersistedMap loadMap(PackedFile pakFile)
+			throws Exception
+	{
+		// Sanity check
+		String progVersion = (String) pakFile.getProperty(PROP_VERSION);
+		if (!versionCheck(progVersion))
+			return null;
+
+		PersistedMap persistedMap = (PersistedMap) pakFile.getContent();
+
+		// Now load up any images that we need
+		loadAssets(persistedMap.assetMap.keySet(), pakFile);
+
+		// FJE We only want the token's graphical data, so loop through all tokens and
+		// destroy all properties and macros.  Keep some fields, though.  Since that type
+		// of object editing doesn't belong here, we just call Token.imported() and let
+		// that method Do The Right Thing.
+		for (Iterator<Token> iter = persistedMap.zone.getAllTokens().iterator(); iter.hasNext();) {
+			Token token = iter.next();
+			token.imported();
+		}
+		// XXX FJE This doesn't work the way I want it to.  But doing this the Right Way
+		// is too much work right now. :-}
+		Zone z = persistedMap.zone;
+		String n = fixupZoneName(z.getName());
+		z.setName(n);
+		z.imported(); // Resets creation timestamp and init panel, among other things
+		z.optimize(); // Collapses overlaid or redundant drawables
+		return persistedMap;
+	}
+
 	/**
 	 * Determines whether the incoming map name is unique. If it is, it's
 	 * returned as-is. If it's not unique, a newly generated name is returned.
@@ -220,7 +239,7 @@
 	 *            name from imported map
 	 * @return new name to use for the map
 	 */
-	private static String fixupZoneName(String n) {
+	public static String fixupZoneName(String n) {
 		List<Zone> zones = MapTool.getCampaign().getZones();
 		for (Zone zone : zones) {
 			if (zone.getName().equals(n)) {
Index: src/org/moonshot/maptool/util/SLBMLoader.java
===================================================================
--- src/org/moonshot/maptool/util/SLBMLoader.java	(revision 0)
+++ src/org/moonshot/maptool/util/SLBMLoader.java	(working copy)
@@ -0,0 +1,219 @@
+package org.moonshot.maptool.util;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+import net.rptools.lib.MD5Key;
+import net.rptools.maptool.client.MapTool;
+import net.rptools.maptool.model.Asset;
+import net.rptools.maptool.model.AssetManager;
+import net.rptools.maptool.model.Grid;
+import net.rptools.maptool.model.SquareGrid;
+import net.rptools.maptool.model.Token;
+import net.rptools.maptool.model.Token.TokenShape;
+import net.rptools.maptool.model.Zone;
+import net.rptools.maptool.model.Zone.Layer;
+import net.rptools.maptool.model.ZoneFactory;
+import net.rptools.maptool.util.PersistenceUtil;
+import net.rptools.maptool.util.PersistenceUtil.PersistedMap;
+
+public class SLBMLoader
+{
+	private final HashMap<String, MD5Key> assetLookup = new HashMap<String, MD5Key>();
+
+	private final PersistedMap pm;
+	private final Zone z;
+
+	private int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
+
+	private ArrayList<TileData> data = new ArrayList<TileData>();
+	private int tileWidth = 0, tileHeight = 0;
+	private int grid = 0;
+		
+	
+	protected static final class TileData
+	{
+		public final String id, name;
+		public final int x, y, rot;
+		public final boolean fh, fv;
+	
+		public TileData(String[] aParts) {
+			x = Integer.valueOf(aParts[1]);
+			y = Integer.valueOf(aParts[2]);
+			rot = Integer.valueOf(aParts[3]);
+			fh = aParts[4].indexOf('h') > -1 || aParts[4].equals("y");
+			fv = aParts[4].indexOf('v') > -1;
+			id = aParts[5];
+			
+			if( aParts.length >= 7 ) {
+				name = aParts[6];
+			} else {
+				name = x+","+y;
+			}
+		}
+	}
+
+	
+	public SLBMLoader()
+	{
+		pm = new PersistedMap();
+		pm.zone = z = ZoneFactory.createZone();
+		pm.assetMap = new HashMap<MD5Key, Asset>();
+		pm.mapToolVersion = MapTool.getVersion();
+	}
+
+	public PersistedMap loadMap(File mapFile)
+		throws Exception
+	{
+		ZipFile zf = new ZipFile(mapFile);
+		ZipEntry maptext = zf.getEntry("map.txt");
+
+		try {
+			readMapData(new InputStreamReader(zf.getInputStream(maptext)));
+		} catch (Throwable t) {
+			//TODO Report
+			return null;
+		}
+
+		if (data.size() == 0) {
+			//TODO Report
+			return null;
+		}
+
+		if (tileWidth <= 0 || tileHeight <= 0) {
+			//TODO Report
+			return null;
+		}
+
+		// Set grid size, if one was given.
+		if (grid > 1) {
+			// XXX Dear Fellow MapTools Devs: I've no idea why this doesn't work.
+			Grid g = new SquareGrid();
+			g.setFacings(true, true);
+			g.setOffset(0, 0);
+			g.setSize(grid);
+			z.setGrid(g);
+		}
+
+		for (TileData td : data)
+		{
+			MD5Key assetKey = assetLookup.get(td.id);
+			if (assetKey == null) {
+				assetKey = importAsset(zf, td.id);
+
+				if (assetKey == null) {
+					//TODO Report
+					return null;
+				}
+
+				assetLookup.put(td.id, assetKey);
+			}
+
+			// Make new Token
+			Token tile = new Token(td.name, assetKey);
+
+			// Standard config
+			tile.setLayer(Layer.BACKGROUND);
+			tile.setShape(TokenShape.TOP_DOWN);
+			tile.setSnapToGrid(true);
+			tile.setSnapToScale(false);
+			tile.setVisible(true);
+
+			// Plotted config
+			tile.setX((td.x - minX + 1) * tileWidth);
+			tile.setY((td.y - minY + 1) * tileHeight);
+			tile.setFlippedX(td.fh);
+			tile.setFlippedY(td.fv);
+			tile.setWidth(tileWidth);
+			tile.setHeight(tileHeight);
+			tile.setFacing(td.rot * -90 - 90);
+
+			// Add to Zone
+			z.putToken(tile);
+		}
+
+		z.setName(PersistenceUtil.fixupZoneName(mapFile.getName()));
+		z.imported(); // Resets creation timestamp and init panel, among other things
+		z.optimize(); // Collapses overlaid or redundant drawables
+		return pm;
+	}
+
+	private void readMapData(Reader aReader)
+			throws Throwable
+	{
+		BufferedReader br = new BufferedReader(aReader);
+		try {
+			String line;
+			while (null != (line = br.readLine()))
+			{
+				line = line.trim();
+				if (line.length() > 0 && line.charAt(0) != '#')
+				{
+					String[] parts = line.split("\t+");
+
+					if ("grid".equals(parts[0]))
+					{
+						grid = Integer.parseInt(parts[1]);
+					}
+					else if ("size".equals(parts[0]))
+					{
+						tileWidth = Integer.parseInt(parts[1]);
+						tileHeight = Integer.parseInt(parts[2]);
+					}
+					else if ("plot".equals(parts[0]))
+					{
+						TileData td = new TileData(parts);
+						data.add(td);
+
+						if (td.x < minX)
+							minX = td.x;
+						if (td.y < minY)
+							minY = td.y;
+					}
+				}
+			}
+
+		} finally {
+			br.close();
+		}
+	}
+
+	private MD5Key importAsset(ZipFile zf, String id)
+			throws IOException
+	{
+		ZipEntry ze = zf.getEntry("assets/" + id);
+		InputStream is = zf.getInputStream(ze);
+		try {
+			long size = ze.getSize();
+			byte[] buffer = new byte[(int) size];
+			int pos = 0;
+			while (pos < size) {
+				int num = is.read(buffer, pos, (int) (size - pos));
+				if (num < 0)
+					return null;
+				pos += num;
+			}
+
+			MD5Key key = new MD5Key(buffer);
+			Asset asset = new Asset(key.toString(), buffer);
+
+			// XXX Dear Fellow MapTools Devs: is it more efficient to do this in batches?
+			AssetManager.putAsset(asset);
+			MapTool.serverCommand().putAsset(asset);
+
+			return key;
+
+		} finally {
+			is.close();
+		}
+	}
+
+}

An SLBM file is a renamed ZIP, containing:
* map.txt - map description
* assets/ - folder containing images

Map content format follows (plain text file "map.txt" inside the SLBM)...

Code: Select all

# Parser ignores blank lines and lines STARTING with hash (#)
# All declarations (lines of tab-delimited fields) are case-sensitive.

# Size; declares pixel dimensions of tiles. Non-square tiles are supported.
size	900	900

# Grid; if present, sets map grid to given pixel size. If not present, leaves default.
grid 150

# Each PLOT places a tile.
# Parameters are X, Y, R, F, Asset.
#	X,Y: coordinates, in whole tiles. The map will be centered on 0,0 during load.
#	R: rotation in 90° clockwise steps. 0 is "original" rotation, 1,2,3 are other accepted values.
#	F: 'n', 'h', 'v', 'hv', or 'vh'. Flips the tile not at all, horizontally, vertically, or both. Flips are applied before rotation, as normal for MapTool.
#	Asset: the name of an image in the assets/ directory to use. Each one will be given a MapTool MD5 ID, and only loaded once no matter how many times it is mentioned in the map.
plot	0	0	0	n	RT05.jpg
plot	1	0	2	h	RT08.jpg
plot	0	1	3	h	RT10.jpg
plot	1	1	0	n	RT16.jpg


# Specifications to consider, but which are not yet supported:
#	option degrees	- enables 1-degree rotation steps, instead of 90°
#	option pixels	- coordinates are in pixels, not tiles
#	option layer X	- enables multi-layer support, and selects layer #X as current.
#	                 Each layer has its own SIZE and PLOTS.
#	                 Layers are sorted from lowest integer index to highest.
Last edited by wolph42 on Tue May 19, 2015 7:11 am, edited 3 times in total.
Reason: moved to 1.4 dev section. left shadow cause im not sure wyrframe has access to that forum.

Wyrframe
Cave Troll
Posts: 59
Joined: Sun Apr 14, 2013 7:52 pm

Re: Patch Submit: Map Importer

Post by Wyrframe »

Still finding some issues, just FYI. I've just discovered there's something really hinky going on with MapTool's wonky rounding logic (i.e. how it remembers the exact pixel position a token is moved to even after "snapping" it to the grid, and that causes unexpected behaviour when subsequently manually correcting the tile positions, and how it chooses to round tokens towards zero, instead of towards negative infinity).

Wyrframe
Cave Troll
Posts: 59
Joined: Sun Apr 14, 2013 7:52 pm

Re: Patch Submit: Map Importer

Post by Wyrframe »

Fixed the alignment issues. Just made the map load from 1,1 and down-left, instead of centering the map on 0,0. Original post updated to reflect this.

I think the SLBM format is simple enough that nearly any map editor can be made to export to it. I'd share my tool, but it's a geomorph-based tile layer... which means it's useless without a large, large ream of geomorphs, like ProBono's maptile set or the E-Adventure Tile series from Skeleton Key Games.


User avatar
Jagged
Great Wyrm
Posts: 1306
Joined: Mon Sep 15, 2008 9:27 am
Location: Bristol, UK

Re: PATCH Submit: Map Importer

Post by Jagged »

I am glad this has been included here, as I had emailed Wyrframe, asking where I can find his editor. Can he be given access to this forum?

However, thinking of the future, what I would really like to see is some OSGI work so that we can produce a framework for people to build plugin map editors. Its something I would love to take a look at, but to be honest, I have no idea where you would start ;)

User avatar
RPTroll
TheBard
Posts: 3159
Joined: Tue Mar 21, 2006 7:26 pm
Location: Austin, Tx
Contact:

Re: PATCH Submit: Map Importer

Post by RPTroll »

He should have access to this forum now.
ImageImage ImageImageImageImage
Support RPTools by shopping
Image
Image

Wyrframe
Cave Troll
Posts: 59
Joined: Sun Apr 14, 2013 7:52 pm

Re: PATCH Submit: Map Importer

Post by Wyrframe »

I do indeed; much obliged.

Post Reply

Return to “MapTool 1.4”