From 1752708fbb7aeb4b05c94b68811458ab373f7daf Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 22 Jan 2014 10:50:42 -0800 Subject: [PATCH] Factored ComponentBundler logic into separate class. --- .../cast/bundle/tools/ComponentBundler.java | 520 +++++++++++++++++ .../bundle/tools/ComponentBundlerTask.java | 525 +----------------- .../tools/DirectoryComponentBundlerTask.java | 69 ++- .../bundle/tools/DirectoryTileSetBundler.java | 1 + 4 files changed, 569 insertions(+), 546 deletions(-) create mode 100644 tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundler.java diff --git a/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundler.java b/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundler.java new file mode 100644 index 00000000..7641ebdc --- /dev/null +++ b/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundler.java @@ -0,0 +1,520 @@ +// +// 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.cast.bundle.tools; + +import java.awt.image.BufferedImage; +import javax.imageio.ImageIO; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.Deflater; + +import com.google.common.collect.Lists; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.ComparableArrayList; +import com.samskivert.util.FileUtil; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.Tuple; + +import com.threerings.cast.ComponentIDBroker; +import com.threerings.cast.StandardActions; +import com.threerings.cast.bundle.BundleUtil; +import com.threerings.media.tile.ImageProvider; +import com.threerings.media.tile.SimpleCachingImageProvider; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TrimmedTileSet; + +/** + * Handles the logic of generating component bundles. Used by the Ant task and Maven plugin. + */ +public abstract class ComponentBundler { + + public ComponentBundler (File mapfile, File actionDef) { + _mapfile = mapfile; + _actionDef = actionDef; + } + + protected boolean keepRawPngs () { return false; } + protected boolean uncompressed () { return false; } + + public void execute (String root, File target, List>> sourceDirs) { + // load the id broker + HashMapIDBroker broker = new HashMapIDBroker(); + try { + BufferedReader bin = new BufferedReader(new FileReader(_mapfile)); + broker.readFrom(bin); + bin.close(); + } catch (FileNotFoundException fnfe) { + // if the file doesn't yet exist, start with a blank broker + } catch (Exception e) { + throw new RuntimeException( + "Error loading component ID map [mapfile=" + _mapfile + "]", e); + } + + // load the action tilesets + Map actsets; + try { + actsets = ComponentBundlerUtil.parseActionTileSets(_actionDef); + } catch (FileNotFoundException fnfe) { + throw new RuntimeException( + "Unable to load action definition file [path=" + _actionDef.getPath() + "].", fnfe); + } catch (Exception e) { + throw new RuntimeException("Parsing error.", e); + } + + // check to see if any of the source files are newer than the target file + List sources = Lists.newArrayList(); + for (Tuple> source : sourceDirs) { + File fromDir = source.left; + for (String srcFile : source.right) { + sources.add(new File(fromDir, srcFile)); + } + } + sources.add(_mapfile); + sources.add(_actionDef); + + long newest = getNewestDate(sources); + if (skipIfTargetNewer() && newest < target.lastModified()) { + System.out.println(target.getPath() + " is up to date."); + return; + } + + System.out.println("Generating " + target + "..."); + + try { + // make sure we can create our bundle file + OutputStream fout = createOutputStream(target); + + // we'll fill this with component id to tuple mappings + HashIntMap> mapping = new HashIntMap>(); + + // process our files; control is inverted here so that the Ant task can enumerate using + // its internal data structures and the Maven plugin can use its + for (Tuple> source : sourceDirs) { + File fromDir = source.left; + for (String srcFile : source.right) { + File cfile = new File(fromDir, srcFile); + // determine the [class, name, action] triplet + String[] info = decomposePath(root, cfile.getPath()); + + // make sure we have an action tileset definition + TileSet aset = actsets.get(info[2]); + if (aset == null) { + System.err.println( + "No tileset definition for component action '" + info[2] + + "' [class=" + info[0] + ", name=" + info[1] + "]."); + continue; + } + aset.setImageProvider(_improv); + + // obtain the component id from our id broker + int cid = broker.getComponentID(info[0], info[1]); + // add a mapping for this component + mapping.put(cid, new Tuple(info[0], info[1])); + + // process and store the main component image + processComponent(info, aset, cfile, fout, newest); + + // pick up any auxiliary images as well like the shadow or + // crop files + String action = info[2]; + String ext = BundleUtil.IMAGE_EXTENSION; + for (String element : AUX_EXTS) { + File afile = new File(FileUtil.resuffix(cfile, ext, element + ext)); + if (afile.exists()) { + info[2] = action + element; + processComponent(info, aset, afile, fout, newest); + } + } + } + } + + // write our mapping table to the jar file as well + if (!skipEntry(BundleUtil.COMPONENTS_PATH, newest)) { + fout = nextEntry(fout, BundleUtil.COMPONENTS_PATH); + ObjectOutputStream oout = new ObjectOutputStream(fout); + oout.writeObject(mapping); + oout.flush(); + } + + if (fout != null) { + // seal up our jar file if we created one + fout.close(); + } + + } catch (IOException ioe) { + String errmsg = "Unable to create component bundle."; + throw new RuntimeException(errmsg, ioe); + + } catch (PersistenceException pe) { + String errmsg = "Unable to obtain component ID mapping."; + throw new RuntimeException(errmsg, pe); + } + + // save our updated component ID broker + saveBroker(_mapfile, broker); + } + + protected void processComponent ( + String[] info, TileSet aset, File cfile, OutputStream fout, long newest) throws IOException + { + // construct the path that'll go in the jar file + String ipath = composePath( + info, BundleUtil.IMAGE_EXTENSION); + + // If we decide that the entry is up to date and we don't need to process it, bail out. + if (skipEntry(ipath, newest)) { + return; + } + + fout = nextEntry(fout, ipath); + + aset.setImagePath(cfile.getPath()); + + TileSet tset; + if (keepRawPngs()) { + // We've elected to keep the pngs as they are and just stuff them into the jar. + try { + tset = aset; + BufferedImage image = aset.getRawTileSetImage(); + ImageIO.write(image, "png", fout); + } catch (Throwable t) { + System.err.println( + "Failure storing tileset in jar" + + "[class=" + info[0] + ", name=" + info[1] + + ", action=" + info[2] + + ", srcimg=" + aset.getImagePath() + "]."); + t.printStackTrace(System.err); + + String errmsg = "Failure trimming tileset."; + throw new RuntimeException(errmsg, t); + } + + } else { + // create a trimmed tileset based on the source action tileset and + // stuff the new trimmed image into the jar file at the same time + try { + tset = trim(aset, fout); + tset.setImagePath(ipath); + } catch (Throwable t) { + System.err.println( + "Failure trimming tileset " + + "[class=" + info[0] + ", name=" + info[1] + + ", action=" + info[2] + + ", srcimg=" + aset.getImagePath() + "]."); + t.printStackTrace(System.err); + + String errmsg = "Failure trimming tileset."; + throw new RuntimeException(errmsg, t); + } + } + + // then write our trimmed tileset bundle data + String tpath = composePath(info, BundleUtil.TILESET_EXTENSION); + if (!skipEntry(tpath, newest) && !keepRawPngs()) { + fout = nextEntry(fout, tpath); + + ObjectOutputStream oout = new ObjectOutputStream(fout); + oout.writeObject(tset); + oout.flush(); + } + } + + protected long getNewestDate (List sources) + { + long newest = 0L; + for (int ii = 0; ii < sources.size(); ii++) { + newest = Math.max(newest, sources.get(ii).lastModified()); + } + return newest; + } + + /** + * Returns whether we should skip updating the bundle if the target is newer than any component. + */ + protected boolean skipIfTargetNewer () + { + return true; + } + + /** + * Decomposes the full path to a component image into a [class, name, + * action] triplet. + */ + protected String[] decomposePath (String root, String path) + { + // first strip off the root + if (!path.startsWith(root)) { + throw new RuntimeException("Can't bundle images outside the root directory " + + "[root=" + root + ", path=" + path + "]."); + } + path = path.substring(root.length()); + + // strip off any preceding file separator + if (path.startsWith(File.separator)) { + path = path.substring(1); + } + + // now strip off the file extension + if (!path.endsWith(BundleUtil.IMAGE_EXTENSION)) { + throw new RuntimeException("Can't bundle malformed image file [path=" + path + "]."); + } + path = path.substring(0, path.length() - BundleUtil.IMAGE_EXTENSION.length()); + + // now decompose the path; the component type and action must always be a single string but + // the class can span multiple directories for easier component organization; thus + // "male/head/goatee/standing" will be parsed as + // [class=male/head, type=goatee, action=standing] + String malmsg = "Can't decode malformed image path: '" + path + "'"; + String[] info = new String[3]; + int lsidx = path.lastIndexOf(File.separator); + if (lsidx == -1) { + throw new RuntimeException(malmsg); + } + info[2] = path.substring(lsidx+1); + int slsidx = path.lastIndexOf(File.separator, lsidx-1); + if (slsidx == -1) { + throw new RuntimeException(malmsg); + } + info[1] = path.substring(slsidx+1, lsidx); + info[0] = path.substring(0, slsidx); + // we need to turn file separator characters (platform dependent) into jar path separator + // characters (always forward slash) + info[0].replace(File.separatorChar, '/'); + return info; + } + + /** + * Composes a triplet of [class, name, action] into the path that should be supplied to the + * JarEntry that contains the associated image data. + */ + protected String composePath (String[] info, String extension) + { + return (info[0] + "/" + info[1] + "/" + info[2] + extension); + } + + /** + * Creates the base output stream to which to write our bundle's files. + */ + protected OutputStream createOutputStream (File target) + throws IOException + { + // make sure the parent directory exists + target.getParentFile().mkdirs(); + // now create our file + JarOutputStream jout = new JarOutputStream(new FileOutputStream(target)); + jout.setLevel(uncompressed() ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION); + return jout; + } + + /** + * Advances to the next named entry in the bundle and returns the stream to which to write + * that entry. + */ + protected OutputStream nextEntry (OutputStream lastEntry, String path) + throws IOException + { + ((JarOutputStream)lastEntry).putNextEntry(new JarEntry(path)); + return lastEntry; + } + + /** + * Returns whether we should skip the specified entry in the bundle, presumably if it was + * already created and up to date. + */ + protected boolean skipEntry (String path, long newest) + { + // If we're making the bundle, by default, we don't skip anything. + return false; + } + + /** + * Converts the tileset to a trimmed tile set and saves it at the specified location. + */ + protected TrimmedTileSet trim (TileSet aset, OutputStream fout) throws IOException + { + return TrimmedTileSet.trimTileSet(aset, fout); + } + + /** + * Stores a persistent representation of the supplied hashmap ID + * broker in the specified file. + */ + protected void saveBroker (File mapfile, HashMapIDBroker broker) + throws RuntimeException + { + // bail if the broker wasn't modified + if (!broker.isModified()) { + return; + } + + try { + BufferedWriter bout = new BufferedWriter(new FileWriter(mapfile)); + broker.writeTo(bout); + bout.close(); + } catch (IOException ioe) { + throw new RuntimeException( + "Unable to store component ID map [mapfile=" + mapfile + "]", ioe); + } + } + + protected static class HashMapIDBroker + extends HashMap, Integer> implements ComponentIDBroker + { + public int getComponentID (String cclass, String cname) + throws PersistenceException + { + Tuple key = new Tuple(cclass, cname); + Integer cid = get(key); + if (cid == null) { + cid = Integer.valueOf(++_nextCID); + put(key, cid); + } + return cid.intValue(); + } + + public void commit () + throws PersistenceException + { + // nothing doing + } + + public boolean isModified () + { + return _nextCID != _startCID; + } + + public void writeTo (BufferedWriter bout) + throws IOException + { + // write out our most recently assigned component id + String cidline = "" + _nextCID; + bout.write(cidline, 0, cidline.length()); + bout.newLine(); + + // write out the keys and values + ComparableArrayList lines = new ComparableArrayList(); + Iterator> keys = keySet().iterator(); + while (keys.hasNext()) { + Tuple key = keys.next(); + Integer value = get(key); + String line = key.left + SEP_STR + key.right + SEP_STR + value; + lines.add(line); + } + + // sort the output + lines.sort(); + + // now write it to the file + int lcount = lines.size(); + for (int ii = 0; ii < lcount; ii++) { + String line = lines.get(ii); + bout.write(line, 0, line.length()); + bout.newLine(); + } + } + + public void readFrom (BufferedReader bin) + throws IOException + { + // read in our most recently assigned component id + _nextCID = readInt(bin); + // keep track of this so that we can tell if we were modified + _startCID = _nextCID; + + // now read in our keys and values + String line; + while ((line = bin.readLine()) != null) { + String orig = line; + int sidx = line.indexOf(SEP_STR); + if (sidx == -1) { + throw new IOException("Malformed line '" + orig + "'"); + } + String cclass = line.substring(0, sidx); + line = line.substring(sidx + SEP_STR.length()); + sidx = line.indexOf(SEP_STR); + if (sidx == -1) { + throw new IOException("Malformed line '" + orig + "'"); + } + String cname = line.substring(0, sidx); + line = line.substring(sidx + SEP_STR.length()); + try { + put(new Tuple(cclass, cname), Integer.valueOf(line)); + } catch (NumberFormatException nfe) { + String err = "Malformed line, invalid code '" + orig + "'"; + throw new IOException(err); + } + } + } + + protected int readInt (BufferedReader bin) + throws IOException + { + String line = bin.readLine(); + try { + return Integer.parseInt(line); + } catch (NumberFormatException nfe) { + throw new IOException("Expected number, got '" + line + "'"); + } + } + + protected int _nextCID = 0; + protected int _startCID = 0; + } + + // an image provider for loading our component images + protected final ImageProvider _improv = new SimpleCachingImageProvider() { + @Override + protected BufferedImage loadImage (String path) + throws IOException { + return ImageIO.read(new File(path)); + } + }; + + /** The path to our component map file. */ + protected final File _mapfile; + + /** The path to our action tilesets definition file. */ + protected final File _actionDef; + + /** Used to separate keys and values in the map file. */ + protected static final String SEP_STR = " := "; + + /** Used to process auxilliary tilesets. */ + protected static final String[] AUX_EXTS = { "_" + StandardActions.SHADOW_TYPE, + "_" + StandardActions.CROP_TYPE }; + +} diff --git a/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java b/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java index 23997c87..09d6f0f8 100644 --- a/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java +++ b/tools/src/main/java/com/threerings/cast/bundle/tools/ComponentBundlerTask.java @@ -19,28 +19,10 @@ package com.threerings.cast.bundle.tools; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.zip.Deflater; +import java.util.Arrays; +import java.util.List; -import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.OutputStream; - -import java.awt.image.BufferedImage; - -import javax.imageio.ImageIO; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; @@ -49,21 +31,8 @@ import org.apache.tools.ant.types.FileSet; import com.google.common.collect.Lists; -import com.samskivert.io.PersistenceException; -import com.samskivert.util.ComparableArrayList; -import com.samskivert.util.FileUtil; -import com.samskivert.util.HashIntMap; import com.samskivert.util.Tuple; -import com.threerings.media.tile.ImageProvider; -import com.threerings.media.tile.SimpleCachingImageProvider; -import com.threerings.media.tile.TileSet; -import com.threerings.media.tile.TrimmedTileSet; - -import com.threerings.cast.ComponentIDBroker; -import com.threerings.cast.StandardActions; -import com.threerings.cast.bundle.BundleUtil; - /** * Ant task for creating component bundles. This task must be configured * with a number of parameters: @@ -156,481 +125,29 @@ public class ComponentBundlerTask extends Task ensureSet(_actionDef, "Must specify the action sequence " + "definitions via the 'actiondef' attribute."); - // parse in the action tilesets - Map actsets; - try { - actsets = ComponentBundlerUtil.parseActionTileSets(_actionDef); - } catch (FileNotFoundException fnfe) { - throw new BuildException("Unable to load action definition file " + - "[path=" + _actionDef.getPath() + "].", fnfe); - } catch (Exception e) { - throw new BuildException("Parsing error.", e); - } - - // load up our component ID broker - ComponentIDBroker broker = loadBroker(_mapfile); - - // check to see if any of the source files are newer than the - // target file - ArrayList sources = Lists.newArrayList(); - sources.addAll(_filesets); - sources.add(_mapfile); - sources.add(_actionDef); - long newest = getNewestDate(sources); - if (skipIfTargetNewer() && newest < _target.lastModified()) { - System.out.println(_target.getPath() + " is up to date."); - return; - } - - // create an image provider for loading our component images - ImageProvider improv = new SimpleCachingImageProvider() { - @Override - protected BufferedImage loadImage (String path) - throws IOException { - return ImageIO.read(new File(path)); - } - }; - - System.out.println("Generating " + _target + "..."); - - try { - // make sure we can create our bundle file - OutputStream fout = createOutputStream(_target); - - // we'll fill this with component id to tuple mappings - HashIntMap> mapping = new HashIntMap>(); - - // deal with the filesets - for (FileSet fs : _filesets) { - DirectoryScanner ds = fs.getDirectoryScanner(getProject()); - File fromDir = fs.getDir(getProject()); - String[] srcFiles = ds.getIncludedFiles(); - - for (String srcFile : srcFiles) { - File cfile = new File(fromDir, srcFile); - // determine the [class, name, action] triplet - String[] info = decomposePath(cfile.getPath()); - - // make sure we have an action tileset definition - TileSet aset = actsets.get(info[2]); - if (aset == null) { - System.err.println( - "No tileset definition for component action '" + info[2] + - "' [class=" + info[0] + ", name=" + info[1] + "]."); - continue; - } - aset.setImageProvider(improv); - - // obtain the component id from our id broker - int cid = broker.getComponentID(info[0], info[1]); - // add a mapping for this component - mapping.put(cid, new Tuple(info[0], info[1])); - - // process and store the main component image - processComponent(info, aset, cfile, fout, newest); - - // pick up any auxiliary images as well like the shadow or - // crop files - String action = info[2]; - String ext = BundleUtil.IMAGE_EXTENSION; - for (String element : AUX_EXTS) { - File afile = new File( - FileUtil.resuffix(cfile, ext, element + ext)); - if (afile.exists()) { - info[2] = action + element; - processComponent(info, aset, afile, fout, newest); - } - } - } - } - - // write our mapping table to the jar file as well - if (!skipEntry(BundleUtil.COMPONENTS_PATH, newest)) { - fout = nextEntry(fout, BundleUtil.COMPONENTS_PATH); - ObjectOutputStream oout = new ObjectOutputStream(fout); - oout.writeObject(mapping); - oout.flush(); - } - - if (fout != null) { - // seal up our jar file if we created one - fout.close(); - } - - } catch (IOException ioe) { - String errmsg = "Unable to create component bundle."; - throw new BuildException(errmsg, ioe); - - } catch (PersistenceException pe) { - String errmsg = "Unable to obtain component ID mapping."; - throw new BuildException(errmsg, pe); - } - - // save our updated component ID broker - saveBroker(_mapfile, broker); - } - - protected void processComponent ( - String[] info, TileSet aset, File cfile, OutputStream fout, long newest) - throws IOException, BuildException - { - // construct the path that'll go in the jar file - String ipath = composePath( - info, BundleUtil.IMAGE_EXTENSION); - - // If we decide that the entry is up to date and we don't need to process it, bail out. - if (skipEntry(ipath, newest)) { - return; - } - - fout = nextEntry(fout, ipath); - - aset.setImagePath(cfile.getPath()); - - TileSet tset; - if (_keepRawPngs) { - // We've elected to keep the pngs as they are and just stuff them into the jar. - try { - tset = aset; - BufferedImage image = aset.getRawTileSetImage(); - ImageIO.write(image, "png", fout); - } catch (Throwable t) { - System.err.println( - "Failure storing tileset in jar" + - "[class=" + info[0] + ", name=" + info[1] + - ", action=" + info[2] + - ", srcimg=" + aset.getImagePath() + "]."); - t.printStackTrace(System.err); - - String errmsg = "Failure trimming tileset."; - throw new BuildException(errmsg, t); - } - } else { - // create a trimmed tileset based on the source action tileset and - // stuff the new trimmed image into the jar file at the same time - try { - tset = trim(aset, fout); - tset.setImagePath(ipath); - } catch (Throwable t) { - System.err.println( - "Failure trimming tileset " + - "[class=" + info[0] + ", name=" + info[1] + - ", action=" + info[2] + - ", srcimg=" + aset.getImagePath() + "]."); - t.printStackTrace(System.err); - - String errmsg = "Failure trimming tileset."; - throw new BuildException(errmsg, t); - } - } - // then write our trimmed tileset bundle data - String tpath = composePath(info, BundleUtil.TILESET_EXTENSION); - if (!skipEntry(tpath, newest) && !_keepRawPngs) { - fout = nextEntry(fout, tpath); - - ObjectOutputStream oout = new ObjectOutputStream(fout); - oout.writeObject(tset); - oout.flush(); - } - } - - protected long getNewestDate (ArrayList sources) - { - long newest = 0L; - for (int ii = 0; ii < sources.size(); ii++) { - newest = Math.max(newest, getNewestDate(sources.get(ii))); - } - return newest; - } - - protected long getNewestDate (Object source) - { - if (source instanceof FileSet) { - FileSet fs = (FileSet)source; + // enumerate all the dirs/files to be processed + List>> sourceDirs = Lists.newArrayList(); + for (FileSet fs : _filesets) { DirectoryScanner ds = fs.getDirectoryScanner(getProject()); File fromDir = fs.getDir(getProject()); String[] srcFiles = ds.getIncludedFiles(); - long newest = 0L; - for (String srcFile : srcFiles) { - File cfile = new File(fromDir, srcFile); - newest = Math.max(newest, cfile.lastModified()); - } - return newest; - - } else if (source instanceof File) { - return ((File)source).lastModified(); - - } else { - System.err.println("Can't get newest date for source: " + source + "."); - return 0L; + sourceDirs.add(Tuple.newTuple(fromDir, Arrays.asList(srcFiles))); } + + // do the deed! + createBundler().execute(_root, _target, sourceDirs); } - /** - * Returns whether we should skip updating the bundle if the target is newer than any component. - */ - protected boolean skipIfTargetNewer () - { - return true; + protected ComponentBundler createBundler () { + return new ComponentBundler(_mapfile, _actionDef) { + @Override protected boolean keepRawPngs () { return _keepRawPngs; } + @Override protected boolean uncompressed () { return _uncompressed; } + }; } - /** - * Decomposes the full path to a component image into a [class, name, - * action] triplet. - */ - protected String[] decomposePath (String path) - throws BuildException + protected void ensureSet (Object value, String errmsg) throws BuildException { - // first strip off the root - if (!path.startsWith(_root)) { - throw new BuildException("Can't bundle images outside the root directory " + - "[root=" + _root + ", path=" + path + "]."); - } - path = path.substring(_root.length()); - - // strip off any preceding file separator - if (path.startsWith(File.separator)) { - path = path.substring(1); - } - - // now strip off the file extension - if (!path.endsWith(BundleUtil.IMAGE_EXTENSION)) { - throw new BuildException("Can't bundle malformed image file [path=" + path + "]."); - } - path = path.substring(0, path.length() - BundleUtil.IMAGE_EXTENSION.length()); - - // now decompose the path; the component type and action must always be a single string but - // the class can span multiple directories for easier component organization; thus - // "male/head/goatee/standing" will be parsed as - // [class=male/head, type=goatee, action=standing] - String malmsg = "Can't decode malformed image path: '" + path + "'"; - String[] info = new String[3]; - int lsidx = path.lastIndexOf(File.separator); - if (lsidx == -1) { - throw new BuildException(malmsg); - } - info[2] = path.substring(lsidx+1); - int slsidx = path.lastIndexOf(File.separator, lsidx-1); - if (slsidx == -1) { - throw new BuildException(malmsg); - } - info[1] = path.substring(slsidx+1, lsidx); - info[0] = path.substring(0, slsidx); - // we need to turn file separator characters (platform dependent) into jar path separator - // characters (always forward slash) - info[0].replace(File.separatorChar, '/'); - return info; - } - - /** - * Composes a triplet of [class, name, action] into the path that should be supplied to the - * JarEntry that contains the associated image data. - */ - protected String composePath (String[] info, String extension) - { - return (info[0] + "/" + info[1] + "/" + info[2] + extension); - } - - protected void ensureSet (Object value, String errmsg) - throws BuildException - { - if (value == null) { - throw new BuildException(errmsg); - } - } - - /** - * Creates the base output stream to which to write our bundle's files. - */ - protected OutputStream createOutputStream (File target) - throws IOException - { - JarOutputStream jout = new JarOutputStream(new FileOutputStream(target)); - jout.setLevel(_uncompressed ? Deflater.NO_COMPRESSION : Deflater.BEST_COMPRESSION); - return jout; - } - - /** - * Advances to the next named entry in the bundle and returns the stream to which to write - * that entry. - */ - protected OutputStream nextEntry (OutputStream lastEntry, String path) - throws IOException - { - ((JarOutputStream)lastEntry).putNextEntry(new JarEntry(path)); - return lastEntry; - } - - /** - * Returns whether we should skip the specified entry in the bundle, presumably if it was - * already created and up to date. - */ - protected boolean skipEntry (String path, long newest) - { - // If we're making the bundle, by default, we don't skip anything. - return false; - } - - /** - * Converts the tileset to a trimmed tile set and saves it at the specified location. - */ - protected TrimmedTileSet trim (TileSet aset, OutputStream fout) - throws IOException - { - return TrimmedTileSet.trimTileSet(aset, fout); - } - - /** - * Loads the hashmap ID broker from its persistent representation in - * the specified file. - */ - protected HashMapIDBroker loadBroker (File mapfile) - throws BuildException - { - HashMapIDBroker broker = new HashMapIDBroker(); - - try { - BufferedReader bin = new BufferedReader(new FileReader(mapfile)); - broker.readFrom(bin); - bin.close(); - - } catch (FileNotFoundException fnfe) { - // if the file doesn't yet exist, start with a blank broker - - } catch (Exception e) { - throw new BuildException("Error loading component ID map " + - "[mapfile=" + mapfile + "]", e); - } - - return broker; - } - - /** - * Stores a persistent representation of the supplied hashmap ID - * broker in the specified file. - */ - protected void saveBroker (File mapfile, ComponentIDBroker broker) - throws BuildException - { - HashMapIDBroker hbroker = (HashMapIDBroker)broker; - - // bail if the broker wasn't modified - if (!hbroker.isModified()) { - return; - } - - try { - BufferedWriter bout = new BufferedWriter(new FileWriter(mapfile)); - hbroker.writeTo(bout); - bout.close(); - } catch (IOException ioe) { - throw new BuildException("Unable to store component ID map " + - "[mapfile=" + mapfile + "]", ioe); - } - } - - protected static class HashMapIDBroker - extends HashMap, Integer> implements ComponentIDBroker - { - public int getComponentID (String cclass, String cname) - throws PersistenceException - { - Tuple key = new Tuple(cclass, cname); - Integer cid = get(key); - if (cid == null) { - cid = Integer.valueOf(++_nextCID); - put(key, cid); - } - return cid.intValue(); - } - - public void commit () - throws PersistenceException - { - // nothing doing - } - - public boolean isModified () - { - return _nextCID != _startCID; - } - - public void writeTo (BufferedWriter bout) - throws IOException - { - // write out our most recently assigned component id - String cidline = "" + _nextCID; - bout.write(cidline, 0, cidline.length()); - bout.newLine(); - - // write out the keys and values - ComparableArrayList lines = new ComparableArrayList(); - Iterator> keys = keySet().iterator(); - while (keys.hasNext()) { - Tuple key = keys.next(); - Integer value = get(key); - String line = key.left + SEP_STR + key.right + SEP_STR + value; - lines.add(line); - } - - // sort the output - lines.sort(); - - // now write it to the file - int lcount = lines.size(); - for (int ii = 0; ii < lcount; ii++) { - String line = lines.get(ii); - bout.write(line, 0, line.length()); - bout.newLine(); - } - } - - public void readFrom (BufferedReader bin) - throws IOException - { - // read in our most recently assigned component id - _nextCID = readInt(bin); - // keep track of this so that we can tell if we were modified - _startCID = _nextCID; - - // now read in our keys and values - String line; - while ((line = bin.readLine()) != null) { - String orig = line; - int sidx = line.indexOf(SEP_STR); - if (sidx == -1) { - throw new IOException("Malformed line '" + orig + "'"); - } - String cclass = line.substring(0, sidx); - line = line.substring(sidx + SEP_STR.length()); - sidx = line.indexOf(SEP_STR); - if (sidx == -1) { - throw new IOException("Malformed line '" + orig + "'"); - } - String cname = line.substring(0, sidx); - line = line.substring(sidx + SEP_STR.length()); - try { - put(new Tuple(cclass, cname), Integer.valueOf(line)); - } catch (NumberFormatException nfe) { - String err = "Malformed line, invalid code '" + orig + "'"; - throw new IOException(err); - } - } - } - - protected int readInt (BufferedReader bin) - throws IOException - { - String line = bin.readLine(); - try { - return Integer.parseInt(line); - } catch (NumberFormatException nfe) { - throw new IOException("Expected number, got '" + line + "'"); - } - } - - protected int _nextCID = 0; - protected int _startCID = 0; + if (value == null) throw new BuildException(errmsg); } /** The path to our component bundle file. */ @@ -646,19 +163,11 @@ public class ComponentBundlerTask extends Task protected String _root; /** A list of filesets that contain tile images. */ - protected ArrayList _filesets = Lists.newArrayList(); + protected List _filesets = Lists.newArrayList(); /** Whether we should keep raw pngs rather than reencoding them in the bundle. */ protected boolean _keepRawPngs; /** Whether we should keep the bundle jars uncompressed rather than zipped. */ protected boolean _uncompressed; - - /** Used to separate keys and values in the map file. */ - protected static final String SEP_STR = " := "; - - /** Used to process auxilliary tilesets. */ - protected static final String[] AUX_EXTS = { - "_" + StandardActions.SHADOW_TYPE, - "_" + StandardActions.CROP_TYPE }; } diff --git a/tools/src/main/java/com/threerings/cast/bundle/tools/DirectoryComponentBundlerTask.java b/tools/src/main/java/com/threerings/cast/bundle/tools/DirectoryComponentBundlerTask.java index 129008d3..851d9806 100644 --- a/tools/src/main/java/com/threerings/cast/bundle/tools/DirectoryComponentBundlerTask.java +++ b/tools/src/main/java/com/threerings/cast/bundle/tools/DirectoryComponentBundlerTask.java @@ -29,51 +29,44 @@ import com.threerings.media.tile.TrimmedTileSet; /** * Creates all the information for a component bundle but places it into a specified directory - * rather than a bundle jar file. + * rather than a bundle jar file. */ public class DirectoryComponentBundlerTask extends ComponentBundlerTask { - @Override - protected OutputStream createOutputStream (File target) - throws IOException - { - // Since we recreate our output stream on every entry, we don't need one to start with. - return null; - } + @Override protected ComponentBundler createBundler () { + return new ComponentBundler(_mapfile, _actionDef) { + @Override protected boolean keepRawPngs () { return _keepRawPngs; } + @Override protected boolean uncompressed () { return _uncompressed; } + @Override protected OutputStream createOutputStream (File target) throws IOException { + // we recreate our output stream on every entry; we don't need one to start with + return null; + } + @Override protected OutputStream nextEntry (OutputStream lastEntry, + String path) throws IOException { + File file = new File(_target, path); + file.getParentFile().mkdirs(); + if (!file.getParentFile().isDirectory()) { + throw new IOException( + "Unable to make component directory. [dir=" + file.getParentFile() + "]"); + } + return new FileOutputStream(file); + } - @Override - protected OutputStream nextEntry (OutputStream lastEntry, String path) - throws IOException - { - File file = new File(_target, path); - file.getParentFile().mkdirs(); - if (!file.getParentFile().isDirectory()) { - throw new IOException("Unable to make component directory.[dir=" + - file.getParentFile() + "]"); - } - return new FileOutputStream(file); - } + @Override protected boolean skipEntry (String path, long newest) { + File file = new File(_target, path); + return (file.lastModified() > newest); + } - @Override - protected boolean skipEntry (String path, long newest) - { - File file = new File(_target, path); - return (file.lastModified() > newest); - } + @Override protected TrimmedTileSet trim (TileSet aset, + OutputStream fout) throws IOException { + return TrimmedTileSet.trimTileSet(aset, fout, "png"); + } - @Override - protected TrimmedTileSet trim (TileSet aset, OutputStream fout) - throws IOException - { - return TrimmedTileSet.trimTileSet(aset, fout, "png"); - } - - @Override - protected boolean skipIfTargetNewer () - { - // We have to check modification later on a file-by-file basis, so cannot skip. - return false; + @Override protected boolean skipIfTargetNewer () { + return false; // we have to check mod later on a file-by-file basis, so can't skip + } + }; } } 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 index 31b404a5..29f8c92c 100644 --- 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 @@ -152,6 +152,7 @@ public class DirectoryTileSetBundler extends TileSetBundler ObjectOutputStream oout = new ObjectOutputStream(fout); oout.writeObject(bundle); oout.flush(); + fout.close(); return true;