diff --git a/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java b/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java
index 13253b05..e4d2a920 100644
--- a/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java
+++ b/core/src/main/java/com/threerings/media/tile/bundle/BundleUtil.java
@@ -35,10 +35,14 @@ import com.threerings.resource.ResourceBundle;
*/
public class BundleUtil
{
- /** The path to the metadata resource that we will attempt to load
- * from our resource set. */
+ /** The path to the binary metadata resource that jvm clients will attempt to load
+ * from the bundle's resource set. */
public static final String METADATA_PATH = "tsbundles.dat";
+ /** The path to the text metadata resource that playn clients will attempt to load
+ * from the bundle's resource set. */
+ public static final String METADATA_JSON_PATH = "tsbundles.json";
+
/**
* Extracts, but does not initialize, a serialized tileset bundle
* instance from the supplied resource bundle.
diff --git a/tools/pom.xml b/tools/pom.xml
index 8ccb51e8..4cde20fc 100644
--- a/tools/pom.xml
+++ b/tools/pom.xml
@@ -38,6 +38,13 @@
playn-core1.5.1
+
+ net.sf.json-lib
+ json-lib
+ 2.4
+ jdk15
+ true
+
diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java
new file mode 100644
index 00000000..1019a514
--- /dev/null
+++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/BundleWriter.java
@@ -0,0 +1,174 @@
+// Nenya library - tools for developing networked games
+// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
+// http://code.google.com/p/nenya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.media.tile.bundle.tools;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.Deflater;
+
+/**
+ * Writes files to disk either in a directory in a jar. The majority of the calling code doesn't
+ * need to distinguish. Note that jars are not opened for appending, the class is intended to be
+ * used in situations where the contents can be generated entirely.
+ */
+public class BundleWriter
+{
+ /**
+ * Creates a new bundle writer that will write to the given target jar file.
+ * @param target file to open as a jar
+ * @param uncompressed whether compression is off
+ */
+ public BundleWriter (File target, final boolean uncompressed)
+ throws IOException
+ {
+ _target = target;
+ _dir = null;
+ _jar = new JarImpl();
+ _jar.uncompressed = uncompressed;
+ }
+
+ /**
+ * Creates a new bundle writer that will write files into the given directory.
+ * @param targetDir directory to write files to
+ * @throws IOException if the directory cannot be created
+ */
+ public BundleWriter (File targetDir)
+ throws IOException
+ {
+ _target = targetDir;
+ _jar = null;
+ _dir = new DirImpl();
+ }
+
+ /**
+ * Opens a new file for writing in the jar or directory.
+ */
+ public OutputStream startNewFile (String path)
+ throws IOException
+ {
+ if (_jar != null) {
+ _jar.get().putNextEntry(new JarEntry(path));
+ return _jar.get();
+ } else {
+ _dir.closeFile();
+ return _dir.newFile(path);
+ }
+ }
+
+ /**
+ * Tests if the target is newer than the given time. Note that if we are in directory-mode,
+ * false is returned.
+ */
+ public boolean isNewerThan (long newest)
+ {
+ if (_jar != null) {
+ return _target.lastModified() > newest;
+ }
+ return false;
+ }
+
+ /**
+ * Closes the writer.
+ */
+ public void close ()
+ throws IOException
+ {
+ if (_jar != null) {
+ _jar.get().close();
+ } else {
+ _dir.closeFile();
+ }
+ }
+
+ public String toString ()
+ {
+ return _target.toString() + (_jar == null ? "/" : "");
+ }
+
+ /**
+ * Deletes the target.
+ */
+ public boolean delete ()
+ {
+ if (_jar != null) {
+ return _target.delete();
+ }
+ // TODO: delete directory
+ return false;
+ }
+
+ protected class JarImpl
+ {
+ JarOutputStream get ()
+ throws IOException
+ {
+ if (jar == null) {
+ FileOutputStream fout = new FileOutputStream(_target);
+ Manifest manifest = new Manifest();
+ jar = new JarOutputStream(fout, manifest);
+ jar.setLevel(uncompressed ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION);
+ }
+ return jar;
+ }
+
+ JarOutputStream jar;
+ boolean uncompressed;
+ }
+
+ protected class DirImpl
+ {
+ public DirImpl ()
+ throws IOException
+ {
+ _target.mkdirs();
+ if (!_target.isDirectory()) {
+ throw new IOException("Not a directory: " + _target);
+ }
+ }
+
+ public void closeFile ()
+ {
+ if (current != null) {
+ try {
+ current.close();
+ } catch (IOException ex) {
+ System.err.println("Unable to close file: " + current);
+ }
+ current = null;
+ }
+ }
+
+ public OutputStream newFile (String path)
+ throws IOException
+ {
+ return current = new FileOutputStream(new File(_target, path));
+ }
+
+ OutputStream current;
+ }
+
+ protected final File _target;
+ protected final JarImpl _jar;
+ protected final DirImpl _dir;
+}
diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java
deleted file mode 100644
index 29f8c92c..00000000
--- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundler.java
+++ /dev/null
@@ -1,171 +0,0 @@
-//
-// Nenya library - tools for developing networked games
-// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
-// https://github.com/threerings/nenya
-//
-// This library is free software; you can redistribute it and/or modify it
-// under the terms of the GNU Lesser General Public License as published
-// by the Free Software Foundation; either version 2.1 of the License, or
-// (at your option) any later version.
-//
-// This library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-// Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public
-// License along with this library; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-
-package com.threerings.media.tile.bundle.tools;
-
-import java.util.Iterator;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.ObjectOutputStream;
-
-import javax.imageio.ImageIO;
-
-import com.samskivert.io.StreamUtil;
-
-import com.threerings.media.tile.ImageProvider;
-import com.threerings.media.tile.ObjectTileSet;
-import com.threerings.media.tile.TileSet;
-import com.threerings.media.tile.TrimmedObjectTileSet;
-import com.threerings.media.tile.bundle.BundleUtil;
-import com.threerings.media.tile.bundle.TileSetBundle;
-
-import static com.threerings.media.Log.log;
-
-public class DirectoryTileSetBundler extends TileSetBundler
-{
- public DirectoryTileSetBundler (File configFile)
- throws IOException
- {
- super(configFile);
- }
-
- public DirectoryTileSetBundler (String configPath)
- throws IOException
- {
- super(configPath);
- }
-
- @Override
- public boolean createBundle (
- File target, TileSetBundle bundle, ImageProvider improv, String imageBase, long newestMod)
- throws IOException
- {
- try {
- // write all of the image files to the bundle's target path, converting the
- // tilesets to trimmed tilesets in the process
- Iterator iditer = bundle.enumerateTileSetIds();
- while (iditer.hasNext()) {
- int tileSetId = iditer.next().intValue();
- TileSet set = bundle.getTileSet(tileSetId);
- String imagePath = set.getImagePath();
-
- // sanity checks
- if (imagePath == null) {
- log.warning("Tileset contains no image path " +
- "[set=" + set + "]. It ain't gonna work.");
- continue;
- }
-
- // if this is an object tileset, trim it
- if (set instanceof ObjectTileSet) {
- // set the tileset up with an image provider; we
- // need to do this so that we can trim it!
- set.setImageProvider(improv);
-
- try {
- // create a trimmed object tileset, which will
- // write the trimmed tileset image to the destination output stream
- File outFile = new File(target, imagePath);
- FileOutputStream fout = null;
-
- if (outFile.lastModified() > newestMod) {
- // Our file's newer than the newest bundle mod - up to date.
- // So don't actually do anything
-
- // TODO: Ideally, we'd like to skip re-trimming altogether, since that's
- // expensive, but for the moment, we're at least doing half as much by
- // not writing out the trimmed image.
-
- } else {
- // It's changed, so let's open the file & do all that jazz
- outFile.getParentFile().mkdirs();
- fout = new FileOutputStream(outFile);
- }
-
- TrimmedObjectTileSet tset =
- TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, fout, "png");
- tset.setImagePath(imagePath);
- // replace the original set with the trimmed tileset in the tileset bundle
- bundle.addTileSet(tileSetId, tset);
-
- } catch (Exception e) {
- e.printStackTrace(System.err);
-
- String msg = "Error adding tileset to bundle " + imagePath +
- ", " + set.getName() + ": " + e;
- throw (IOException) new IOException(msg).initCause(e);
- }
-
- } else {
- // read the image file and write it to the proper place
- File ifile = new File(imageBase, imagePath);
- if (ifile.lastModified() > newestMod) {
- // Our file's newer than the newest bundle mod - up to date.
- continue;
- }
-
- try {
- // We read the image to ensure it is a valid image.
- ImageIO.read(ifile);
-
- File outFile = new File(target, imagePath);
- if (outFile.lastModified() > newestMod) {
- // Our file's newer than the newest bundle mod - up to date.
- continue;
- }
- outFile.getParentFile().mkdirs();
- FileOutputStream fout = new FileOutputStream(outFile);
- FileInputStream imgin = new FileInputStream(ifile);
- StreamUtil.copy(imgin, fout);
- } catch (Exception e) {
- String msg = "Failure bundling image " + ifile + ": " + e;
- throw (IOException) new IOException(msg).initCause(e);
- }
- }
- }
-
- // now write a serialized representation of the tileset bundle
- // object to the bundle jar file
- File outFile = new File(target, BundleUtil.METADATA_PATH);
-
- outFile.getParentFile().mkdirs();
- FileOutputStream fout = new FileOutputStream(outFile);
- ObjectOutputStream oout = new ObjectOutputStream(fout);
- oout.writeObject(bundle);
- oout.flush();
- fout.close();
-
- return true;
-
- } catch (Exception e) {
- String errmsg = "Failed to create bundle " + target + ": " + e;
- throw (IOException) new IOException(errmsg).initCause(e);
- }
- }
-
- @Override
- protected boolean skipIfTargetNewer ()
- {
- // We have to check modification later on a file-by-file basis, so cannot skip.
- return false;
- }
-}
diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java
index 7e2ec172..b42317bb 100644
--- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java
+++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/DirectoryTileSetBundlerTask.java
@@ -46,17 +46,11 @@ public class DirectoryTileSetBundlerTask extends TileSetBundlerTask
}
@Override
- protected TileSetBundler createBundler ()
+ protected BundleWriter createWriter (File fromDir, String path)
throws IOException
- {
- return new DirectoryTileSetBundler(_config);
- }
-
- @Override
- protected String getTargetPath (File fromDir, String path)
{
File xmlFile = new File(path.replace(fromDir.getPath(), _deployDir.getPath()));
- return xmlFile.getParent();
+ return new BundleWriter(xmlFile.getParentFile());
}
/** The directory in which we want to place our tile set files for deployment. */
diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java
index 1486dda3..4f0c4c3c 100644
--- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java
+++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundler.java
@@ -21,21 +21,21 @@ package com.threerings.media.tile.bundle.tools;
import java.util.ArrayList;
import java.util.Iterator;
-import java.util.jar.JarEntry;
-import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
-import java.util.zip.Deflater;
import java.io.File;
import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
+import java.io.OutputStream;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import com.google.common.collect.Lists;
+
+import net.sf.json.JSONArray;
+import net.sf.json.JSONObject;
+
import org.apache.commons.digester.Digester;
import org.xml.sax.SAXException;
@@ -45,6 +45,8 @@ import com.samskivert.util.HashIntMap;
import com.threerings.resource.FastImageIO;
+import com.threerings.tools.JSONConversion;
+
import com.threerings.media.tile.ImageProvider;
import com.threerings.media.tile.ObjectTileSet;
import com.threerings.media.tile.SimpleCachingImageProvider;
@@ -112,6 +114,90 @@ import static com.threerings.media.Log.log;
*/
public class TileSetBundler
{
+ /**
+ * Wraps up the configuration options for writing a tile set bundle to disk and provides a
+ * method to perform the write.
+ */
+ public static class Writer
+ {
+ /** The target jar file or directory. */
+ public final BundleWriter bwriter;
+
+ /** The bundle to write. */
+ public final TileSetBundle bundle;
+
+ /** Loads images for the bundle. */
+ public final ImageProvider improv;
+
+ /**
+ * Creates a new writer.
+ *
+ * @param bwriter the tileset bundle file or directory that will be created.
+ * @param bundle contains the tilesets we'd like to save out to the bundle.
+ * @param improv the image provider.
+ */
+ public Writer (BundleWriter bwriter, TileSetBundle bundle, ImageProvider improv)
+ {
+ this.bundle = bundle;
+ this.bwriter = bwriter;
+ this.improv = improv;
+ }
+
+ /**
+ * Sets whether or not we trim the tileset's image in the process of writing out the
+ * new bundle. Trimming will mutate tilesets directly, so beware.
+ */
+ public Writer trimImages (boolean trim)
+ {
+ this.trim = trim;
+ return this;
+ }
+
+ /**
+ * Sets whether or not we write out raw images.
+ */
+ public Writer useRawImages (boolean raw)
+ {
+ this.raw = raw;
+ return this;
+ }
+
+ /**
+ * Sets the image base.
+ * TODO: learn what this does and improve comment. should it just be a special improv?
+ */
+ public Writer imageBase (String imageBase)
+ {
+ this.imageBase = imageBase;
+ return this;
+ }
+
+ /**
+ * Sets whether we write out a json file containing the meta data.
+ * @param json if non-null, used to write out the bundle contents to tsbundles.json text
+ * file. Otherwise, the default binary obbject output is used.
+ */
+ public Writer useJson (JSONConversion.Config json)
+ {
+ this.json = json;
+ return this;
+ }
+
+ /**
+ * Using the configured options, creates the new target bundle.
+ */
+ public void create ()
+ throws IOException
+ {
+ TileSetBundler.createBundle(this);
+ }
+
+ boolean trim = true;
+ boolean raw = true;
+ String imageBase;
+ JSONConversion.Config json;
+ }
+
/**
* Constructs a tileset bundler with the specified path to a bundler
* configuration file. The configuration file will be loaded and used
@@ -130,19 +216,6 @@ public class TileSetBundler
public TileSetBundler (File configFile)
throws IOException
{
- this(configFile, false, false);
- }
-
- /**
- * Constructs a tileset bundler with the specified bundler config
- * file and whether to keep pngs as-is or if not, re-encode them.
- */
- public TileSetBundler (File configFile, boolean keepRawPngs, boolean uncompressed)
- throws IOException
- {
- _keepRawPngs = keepRawPngs;
- _uncompressed = uncompressed;
-
// we parse our configuration with a digester
Digester digester = new Digester();
@@ -198,46 +271,24 @@ public class TileSetBundler
}
/**
- * Creates a tileset bundle at the location specified by the
- * targetPath parameter, based on the description
+ * Prepares to create a tileset bundle at the location specified by the
+ * target parameter, based on the description
* provided via the bundleDesc parameter.
*
* @param idBroker the tileset id broker that will be used to map
* tileset names to tileset ids.
* @param bundleDesc a file object pointing to the bundle description
* file.
- * @param targetPath the path of the tileset bundle file that will be
- * created.
+ * @param target the tileset bundle file or directory that will be created.
+ *
+ * @return a writer object that can be used to configure and create the target
+ * bundle, or null if the target is up to date with respect to all source files
*
* @exception IOException thrown if an error occurs reading, writing
* or processing anything.
*/
- public void createBundle (
- TileSetIDBroker idBroker, File bundleDesc, String targetPath)
- throws IOException
- {
- createBundle(idBroker, bundleDesc, new File(targetPath));
- }
-
- /**
- * Creates a tileset bundle at the location specified by the
- * targetPath parameter, based on the description
- * provided via the bundleDesc parameter.
- *
- * @param idBroker the tileset id broker that will be used to map
- * tileset names to tileset ids.
- * @param bundleDesc a file object pointing to the bundle description
- * file.
- * @param target the tileset bundle file that will be created.
- *
- * @return true if the bundle was rebuilt, false if it was not because
- * the bundle file was newer than all involved source files.
- *
- * @exception IOException thrown if an error occurs reading, writing
- * or processing anything.
- */
- public boolean createBundle (
- TileSetIDBroker idBroker, final File bundleDesc, File target)
+ public Writer process (
+ TileSetIDBroker idBroker, final File bundleDesc, BundleWriter target)
throws IOException
{
// stick an array list on the top of the stack into which we will
@@ -319,8 +370,8 @@ public class TileSetBundler
}
// see if our newest file is newer than the tileset bundle
- if (skipIfTargetNewer() && newest < target.lastModified()) {
- return false;
+ if (target.isNewerThan(newest)) {
+ return null;
}
// create an image provider for loading our tileset images
@@ -332,59 +383,26 @@ public class TileSetBundler
}
};
- return createBundle(target, bundle, improv, bundleDesc.getParent(), newest);
+ return new Writer(target, bundle, improv).imageBase(bundleDesc.getParent());
}
/**
- * Finish the creation of a tileset bundle jar file.
- *
- * @param target the tileset bundle file that will be created.
- * @param bundle contains the tilesets we'd like to save out to the bundle.
- * @param improv the image provider.
- * @param imageBase the base directory for getting images for non
- * @param newestMod the most recent modification to any part of the bundle. By default we
- * ignore this since we normally duck out if we're up to date.
- * ObjectTileSet tilesets.
+ * Create the tileset bundle on disk using previously configured options.
*/
- public boolean createBundle (
- File target, TileSetBundle bundle, ImageProvider improv, String imageBase, long newestMod)
+ protected static void createBundle (Writer target)
throws IOException
{
- return createBundleJar(target, bundle, improv, imageBase, _keepRawPngs, _uncompressed);
- }
-
- /**
- * Create a tileset bundle jar file.
- *
- * @param target the tileset bundle file that will be created.
- * @param bundle contains the tilesets we'd like to save out to the bundle.
- * @param improv the image provider.
- * @param imageBase the base directory for getting images for non-ObjectTileSet tilesets.
- * @param keepOriginalPngs bundle up the original PNGs as PNGs instead of converting to the
- * FastImageIO raw format
- */
- public static boolean createBundleJar (
- File target, TileSetBundle bundle, ImageProvider improv, String imageBase,
- boolean keepOriginalPngs, boolean uncompressed)
- throws IOException
- {
- // now we have to create the actual bundle file
- FileOutputStream fout = new FileOutputStream(target);
- Manifest manifest = new Manifest();
- JarOutputStream jar = new JarOutputStream(fout, manifest);
- jar.setLevel(uncompressed ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION);
-
try {
// write all of the image files to the bundle, converting the
// tilesets to trimmed tilesets in the process
- Iterator iditer = bundle.enumerateTileSetIds();
+ Iterator iditer = target.bundle.enumerateTileSetIds();
// Store off the updated TileSets in a separate Map so we can wait to change the
// bundle till we're done iterating.
HashIntMap toUpdate = new HashIntMap();
while (iditer.hasNext()) {
int tileSetId = iditer.next().intValue();
- TileSet set = bundle.getTileSet(tileSetId);
+ TileSet set = target.bundle.getTileSet(tileSetId);
String imagePath = set.getImagePath();
// sanity checks
@@ -395,22 +413,23 @@ public class TileSetBundler
}
// if this is an object tileset, trim it
- if (!keepOriginalPngs && (set instanceof ObjectTileSet)) {
+ if (target.trim && (set instanceof ObjectTileSet)) {
// set the tileset up with an image provider; we
// need to do this so that we can trim it!
- set.setImageProvider(improv);
+ set.setImageProvider(target.improv);
- // we're going to trim it, so adjust the path
- imagePath = adjustImagePath(imagePath);
- jar.putNextEntry(new JarEntry(imagePath));
+ // add .raw if requested
+ if (target.raw) {
+ imagePath = adjustImagePath(imagePath);
+ }
+ OutputStream dest = target.bwriter.startNewFile(imagePath);
try {
// create a trimmed object tileset, which will
- // write the trimmed tileset image to the jar
- // output stream
+ // write the trimmed tileset image to the target file
TrimmedObjectTileSet tset =
- TrimmedObjectTileSet.trimObjectTileSet(
- (ObjectTileSet)set, jar);
+ TrimmedObjectTileSet.trimObjectTileSet((ObjectTileSet)set, dest,
+ target.raw ? FastImageIO.FILE_SUFFIX : "png");
tset.setImagePath(imagePath);
// replace the original set with the trimmed
// tileset in the tileset bundle
@@ -427,18 +446,21 @@ public class TileSetBundler
} else {
// read the image file and convert it to our custom
// format in the bundle
- File ifile = new File(imageBase, imagePath);
+ File ifile = new File(target.imageBase, imagePath);
try {
BufferedImage image = ImageIO.read(ifile);
- if (!keepOriginalPngs && FastImageIO.canWrite(image)) {
+ if (target.raw && FastImageIO.canWrite(image)) {
imagePath = adjustImagePath(imagePath);
- jar.putNextEntry(new JarEntry(imagePath));
+ // NOTE: DirectoryTileSetBundler used to check the modification time
+ // of the target image here. There may be something to it, but it
+ // looked unnecessary
+ OutputStream dest = target.bwriter.startNewFile(imagePath);
set.setImagePath(imagePath);
- FastImageIO.write(image, jar);
+ FastImageIO.write(image, dest);
} else {
- jar.putNextEntry(new JarEntry(imagePath));
+ OutputStream dest = target.bwriter.startNewFile(imagePath);
FileInputStream imgin = new FileInputStream(ifile);
- StreamUtil.copy(imgin, jar);
+ StreamUtil.copy(imgin, dest);
}
} catch (Exception e) {
String msg = "Failure bundling image " + ifile +
@@ -447,25 +469,39 @@ public class TileSetBundler
}
}
}
- bundle.putAll(toUpdate);
+ target.bundle.putAll(toUpdate);
// now write a serialized representation of the tileset bundle
- // object to the bundle jar file
- JarEntry entry = new JarEntry(BundleUtil.METADATA_PATH);
- jar.putNextEntry(entry);
- ObjectOutputStream oout = new ObjectOutputStream(jar);
- oout.writeObject(bundle);
- oout.flush();
+ if (target.json != null) {
+ JSONArray array = new JSONArray();
+ for (Iterator tileSetId = target.bundle.enumerateTileSetIds();
+ tileSetId.hasNext(); ) {
+ JSONObject tset = new JSONObject();
+ int id = tileSetId.next();
+ tset.put("id", id);
+ tset.put("set", target.json.convert(target.bundle.get(id)));
+ array.add(tset);
+ }
+
+ // now write a serialized representation of the tileset bundle
+ OutputStream fout = target.bwriter.startNewFile(BundleUtil.METADATA_JSON_PATH);
+ fout.write(array.toString().getBytes());
+ fout.close();
+ } else {
+ // object to the bundle jar file
+ ObjectOutputStream oout = new ObjectOutputStream(
+ target.bwriter.startNewFile(BundleUtil.METADATA_PATH));
+ oout.writeObject(target.bundle);
+ oout.flush();
+ }
// finally close up the jar file and call ourself done
- jar.close();
-
- return true;
+ target.bwriter.close();
} catch (Exception e) {
// remove the incomplete jar file and rethrow the exception
- jar.close();
- if (!target.delete()) {
+ target.bwriter.close();
+ if (!target.bwriter.delete()) {
log.warning("Failed to close botched bundle '" + target + "'.");
}
String errmsg = "Failed to create bundle " + target + ": " + e;
@@ -473,14 +509,6 @@ public class TileSetBundler
}
}
- /**
- * Returns whether we should skip updating the bundle if the target is newer than any component.
- */
- protected boolean skipIfTargetNewer ()
- {
- return true;
- }
-
/** Replaces the image suffix with .raw. */
protected static String adjustImagePath (String imagePath)
{
@@ -511,10 +539,4 @@ public class TileSetBundler
/** The digester we use to parse bundle descriptions. */
protected Digester _digester;
-
- /** Whether we should keep pngs as-is rather than re-encoding. */
- protected boolean _keepRawPngs;
-
- /** Normally we compress the jar, but if we want to leave them uncompressed, we set this. */
- protected boolean _uncompressed;
}
diff --git a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java
index a90f129c..25e9dbb5 100644
--- a/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java
+++ b/tools/src/main/java/com/threerings/media/tile/bundle/tools/TileSetBundlerTask.java
@@ -97,7 +97,7 @@ public class TileSetBundlerTask extends Task
File cfile = null;
try {
// create a tileset bundler
- TileSetBundler bundler = createBundler();
+ TileSetBundler bundler = new TileSetBundler(_config);
// create our tileset id broker
MapFileTileSetIDBroker broker =
@@ -123,16 +123,18 @@ public class TileSetBundlerTask extends Task
"Config file should end with .xml.");
continue;
}
- String bpath = getTargetPath(fromDir, cpath);
- File bfile = new File(bpath);
+ BundleWriter bwriter = createWriter(fromDir, cpath);
// create the bundle
- if (bundler.createBundle(broker, cfile, bfile)) {
+ TileSetBundler.Writer writer = bundler.process(broker, cfile, bwriter);
+ if (writer != null) {
+ writer.useRawImages(!_keepRawPngs); // something is lost in translation here
+ writer.create();
System.out.println(
"Created bundle from '" + cpath + "'...");
} else {
System.out.println(
- "Tileset bundle up to date '" + bpath + "'.");
+ "Tileset bundle up to date '" + bwriter + "'.");
}
}
}
@@ -150,18 +152,11 @@ public class TileSetBundlerTask extends Task
/**
* Create the bundler to use during creation.
*/
- protected TileSetBundler createBundler ()
+ protected BundleWriter createWriter (File fromDir, String path)
throws IOException
{
- return new TileSetBundler(_config, _keepRawPngs, _uncompressed);
- }
-
- /**
- * Returns the target path in which our bundler will write the tile set.
- */
- protected String getTargetPath (File fromDir, String path)
- {
- return path.substring(0, path.length()-4) + ".jar";
+ return new BundleWriter(
+ new File(path.substring(0, path.length()-4) + ".jar"), _uncompressed);
}
protected void ensureSet (Object value, String errmsg)
diff --git a/tools/src/main/java/com/threerings/tools/AWTConversions.java b/tools/src/main/java/com/threerings/tools/AWTConversions.java
new file mode 100644
index 00000000..2c8867e8
--- /dev/null
+++ b/tools/src/main/java/com/threerings/tools/AWTConversions.java
@@ -0,0 +1,94 @@
+// Nenya library - tools for developing networked games
+// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
+// http://code.google.com/p/nenya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.tools;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Rectangle;
+
+import net.sf.json.JSONArray;
+
+import com.threerings.tools.JSONConversion.Config;
+import com.threerings.tools.JSONConversion.Converter;
+
+/**
+ * Defines some common formats for converting nenya's uses of java.awt geometric types to json.
+ */
+public class AWTConversions
+{
+ /**
+ * Adds all our conversion to the given config and returns it for chaining.
+ */
+ public static Config addAll (Config config)
+ {
+ return config.
+ addClassConverter(Rectangle.class, RECTANGLE).
+ addClassConverter(Point.class, POINT).
+ addClassConverter(Dimension.class, DIMENSION).
+ addClassConverter(Color.class, COLOR);
+ }
+
+ /**
+ * Converts instances of color to a standard RGB byte triplet.
+ */
+ public static final Converter COLOR = new Converter() {
+ public Object convert (Color c, Config cfg) {
+ return new Integer((c.getRed() << 16) | (c.getGreen()<<8) | c.getBlue());
+ }
+ };
+
+ /**
+ * Converts a rectangle into an json array with entries x, y, width, height.
+ */
+ public static final Converter RECTANGLE = new Converter() {
+ public Object convert (Rectangle r, Config cfg) {
+ JSONArray out = new JSONArray();
+ out.add(r.x);
+ out.add(r.y);
+ out.add(r.width);
+ out.add(r.height);
+ return out;
+ }
+ };
+
+ /**
+ * Converts a point into a json array with entries x, y.
+ */
+ public static final Converter POINT = new Converter() {
+ public Object convert (Point p, Config cfg) {
+ JSONArray out = new JSONArray();
+ out.add(p.x);
+ out.add(p.y);
+ return out;
+ }
+ };
+
+ /**
+ * Converts a dimension into an array with entries width, height.
+ */
+ public static final Converter DIMENSION = new Converter() {
+ public Object convert (Dimension d, Config cfg) {
+ JSONArray out = new JSONArray();
+ out.add(d.width);
+ out.add(d.height);
+ return out;
+ }
+ };
+}
diff --git a/tools/src/main/java/com/threerings/tools/JSONConversion.java b/tools/src/main/java/com/threerings/tools/JSONConversion.java
new file mode 100644
index 00000000..222e8c04
--- /dev/null
+++ b/tools/src/main/java/com/threerings/tools/JSONConversion.java
@@ -0,0 +1,399 @@
+// Nenya library - tools for developing networked games
+// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
+// http://code.google.com/p/nenya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.tools;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import com.samskivert.util.ClassUtil;
+import com.samskivert.util.StringUtil;
+
+import net.sf.json.JSON;
+import net.sf.json.JSONArray;
+import net.sf.json.JSONObject;
+
+
+/**
+ * Utilities for converting instances of java objects to json. This is basically automatic for
+ * simple classes, but field enumeration and overall conversion can be customized.
+ FieldAccessor<SomeImpl, Collection<?>> entryConv =
+ new FieldAccessor<SomeImpl, Collection<?>>() {
+ public Collection<SomeValue> get (SomeImpl impl) {
+ return impl.getValuesAsCollection();
+ }
+ };
+ cfg.addFieldConfig(SomeImpl.class, new FieldConfig<SomeImpl>().
+ addExclusions("privateStuff").
+ addFieldConverter("valueMap", new ConvertCollectionFieldToArray<SomeImpl>(entryConv)));
+ *
+ */
+public class JSONConversion
+{
+ /**
+ * Controls verbose logging for the class.
+ */
+ public static boolean vlog = false;
+
+ /**
+ * Logs the given arguments separated by spaces, if {@link #vlog} is set.
+ */
+ public static void vlog (Object...args)
+ {
+ if (vlog) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("JSONConversion: ");
+ StringUtil.toString(sb, args, "", "", " ");
+ System.out.println(sb.toString());
+ }
+ }
+
+ /**
+ * Converts T to a suitable parameter for {@link JSONArray#add(Object)},
+ * {@link JSONObject#element(String, Object)}, etc.
+ */
+ public static interface Converter
+ {
+ /**
+ * Returns an instance of {@code T} in json form. The cfg may be used to convert aggregated
+ * objects.
+ */
+ Object convert (T obj, Config cfg);
+ }
+
+ /**
+ * Plugs into some common cases for automating the conversion of special fields. For example
+ * a map that is reconstructed from the values on deserialization.
+ */
+ public static interface FieldAccessor
+ {
+ F get (T obj);
+ }
+
+ /**
+ * Defines the field layout for serializing instances of {@code T}.
+ */
+ public static class FieldConfig
+ {
+ /**
+ * Fields that should not be serialized.
+ */
+ public List excludeFields = Lists.newArrayList();
+
+ /**
+ * Fields with custom conversions.
+ */
+ public Map> customFields = Maps.newHashMap();
+
+ /**
+ * Adds some excluded fields and returns this for chaining.
+ */
+ public FieldConfig addExclusions (String ...names)
+ {
+ excludeFields.addAll(Arrays.asList(names));
+ return this;
+ }
+
+ /**
+ * Adds a custom converter for a field and returns this for chaining.
+ */
+ public FieldConfig addFieldConverter (String name, Converter field)
+ {
+ customFields.put(name, (Converter>)field);
+ return this;
+ }
+
+ /**
+ * Tests if a field ought to be excluded, based on current configuration.
+ */
+ public boolean excludeField (Field field)
+ {
+ int mask = Modifier.FINAL | Modifier.TRANSIENT | Modifier.STATIC;
+ return (field.getModifiers() & mask) != 0 ||
+ excludeFields.contains(field.getName());
+ }
+ }
+
+ /**
+ * Defines the conversion of java objects to json. Works mostly by reflection, with
+ * customizability.
+ */
+ public static class Config
+ {
+ /**
+ * Overrides the field treatment for encountered instances.
+ */
+ public Map, FieldConfig>> classes = Maps.newHashMap();
+
+ /**
+ * Overrides the conversion process for encountered instances.
+ */
+ public Map, Converter>> converters = Maps.newHashMap();
+
+ /**
+ * Overrides the way fields are serialized for instances of {@code T} and returns this
+ * for chaining.
+ */
+ public Config addFieldConfig (Class clazz, FieldConfig config)
+ {
+ classes.put(clazz, config);
+ return this;
+ }
+
+ /**
+ * Overrides the way instances of a given type are serialized and returns this for chaining.
+ */
+ public Config addClassConverter (Class clazz, Converter converter)
+ {
+ converters.put(clazz, converter);
+ return this;
+ }
+
+ /**
+ * Looks up the field access for a class. This also makes a fairly lame attempt to
+ * ensure the most derived entry is used.
+ * TODO: spruce up class sort
+ */
+ public FieldConfig> findConfig (Class> clazz)
+ {
+ Class> found = null;
+ for (Class> entry : classes.keySet()) {
+ if (entry.isAssignableFrom(clazz)) {
+ if (found == null || found.isAssignableFrom(entry)) {
+ found = entry;
+ }
+ }
+ }
+ return classes.get(found);
+ }
+
+ /**
+ * Tests if a class is primitive for our purposes.
+ */
+ public boolean isPrimitive (Class> cl)
+ {
+ return cl.isPrimitive() ||
+ Number.class.isAssignableFrom(cl) ||
+ String.class.isAssignableFrom(cl) ||
+ Boolean.class.isAssignableFrom(cl) ||
+ (cl.isArray() && isPrimitive(cl.getComponentType()));
+ }
+
+ /**
+ * Converts the given object to json.
+ * @return either a {@link JSON} instance of an object suitable for insertion to
+ * {@link JSONObject#element(String, Object)}, etc.
+ */
+ public Object convert (final Object obj)
+ {
+ if (obj == null) {
+ return null;
+ }
+
+ vlog("Converting object", obj, "of", obj.getClass());
+
+ @SuppressWarnings("unchecked")
+ Converter