Bundles! Have component bundles largely working. Wrote test code and ANT

tasks and XML parsing rule sets all the good stuff. Rewired up all the
cast code to be amenable to bundling and did some other revamping.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@640 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Michael Bayne
2001-11-27 08:09:35 +00:00
parent be313f8922
commit c25741d8e6
27 changed files with 1554 additions and 918 deletions
@@ -0,0 +1,55 @@
//
// $Id: BundleUtil.java,v 1.1 2001/11/27 08:09:35 mdb Exp $
package com.threerings.cast.bundle;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import com.threerings.resource.ResourceBundle;
/**
* Utility functions related to creating and manipulating component
* bundles.
*/
public class BundleUtil
{
/** The path in the metadata bundle to the serialized action table. */
public static final String ACTIONS_PATH = "actions.dat";
/** The path in the metadata bundle to the serialized action tile sets
* table. */
public static final String ACTION_SETS_PATH = "action_sets.dat";
/** The path in the metadata bundle to the serialized component class
* table. */
public static final String CLASSES_PATH = "classes.dat";
/** The path in the component bundle to the serialized component id to
* class/type mapping. */
public static final String COMPONENTS_PATH = "components.dat";
/** The file extension of our action tile images. */
public static final String IMAGE_EXTENSION = ".png";
/**
* Attempts to load an object from the supplied resource bundle with
* the specified path.
*
* @return the unserialized object in question or null if no
* serialized object data was available at the specified path.
*
* @exception IOException thrown if an I/O error occurs while reading
* the object from the bundle.
*/
public static Object loadObject (ResourceBundle bundle, String path)
throws IOException, ClassNotFoundException
{
InputStream bin = bundle.getResource(path);
if (bin == null) {
return null;
}
return new ObjectInputStream(bin).readObject();
}
}
@@ -0,0 +1,303 @@
//
// $Id: BundledComponentRepository.java,v 1.1 2001/11/27 08:09:35 mdb Exp $
package com.threerings.cast.bundle;
import java.awt.Image;
import java.awt.image.BufferedImage;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import com.samskivert.io.NestableIOException;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.Tuple;
import org.apache.commons.collections.FilterIterator;
import org.apache.commons.collections.Predicate;
import com.threerings.resource.ResourceBundle;
import com.threerings.resource.ResourceManager;
import com.threerings.media.sprite.MultiFrameImage;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.tile.ImageProvider;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.NoSuchTileException;
import com.threerings.cast.CharacterComponent;
import com.threerings.cast.ComponentClass;
import com.threerings.cast.ComponentRepository;
import com.threerings.cast.FrameProvider;
import com.threerings.cast.Log;
import com.threerings.cast.NoSuchComponentException;
/**
* A component repository implementation that obtains information from
* resource bundles.
*
* @see ResourceManager
*/
public class BundledComponentRepository
implements ComponentRepository
{
/**
* Constructs a repository which will obtain its resource set from the
* supplied resource manager.
*
* @param rmgr the resource manager from which to obtain our resource
* set.
* @param name the name of the resource set from which we will be
* loading our component data.
*
* @exception IOException thrown if an I/O error occurs while reading
* our metadata from the resource bundles.
*/
public BundledComponentRepository (ResourceManager rmgr, String name)
throws IOException
{
// first we obtain the resource set from whence will come our
// bundles
ResourceBundle[] rbundles = rmgr.getResourceSet(name);
// look for our metadata info in each of the bundles
try {
for (int i = 0; i < rbundles.length; i++) {
if (_actions == null) {
_actions = (HashMap)BundleUtil.loadObject(
rbundles[i], BundleUtil.ACTIONS_PATH);
}
if (_actionSets == null) {
_actionSets = (HashMap)BundleUtil.loadObject(
rbundles[i], BundleUtil.ACTION_SETS_PATH);
}
if (_classes == null) {
_classes = (HashMap)BundleUtil.loadObject(
rbundles[i], BundleUtil.CLASSES_PATH);
}
}
// now go back and load up all of the component information
for (int i = 0; i < rbundles.length; i++) {
HashIntMap comps = (HashIntMap)BundleUtil.loadObject(
rbundles[i], BundleUtil.COMPONENTS_PATH);
if (comps == null) {
continue;
}
// create a frame provider for this bundle
FrameProvider fprov = new ResourceBundleProvider(rbundles[i]);
// now create character component instances for each component
// in the serialized table
Iterator iter = comps.keySet().iterator();
while (iter.hasNext()) {
int componentId = ((Integer)iter.next()).intValue();
Tuple info = (Tuple)comps.get(componentId);
createComponent(componentId, (String)info.left,
(String)info.right, fprov);
}
}
} catch (ClassNotFoundException cnfe) {
throw new NestableIOException(
"Internal error unserializing metadata", cnfe);
}
}
// documentation inherited
public CharacterComponent getComponent (int componentId)
throws NoSuchComponentException
{
CharacterComponent component = (CharacterComponent)
_components.get(componentId);
if (component == null) {
throw new NoSuchComponentException(componentId);
}
return component;
}
// documentation inherited
public Iterator enumerateComponentClasses ()
{
return _classes.values().iterator();
}
// documentation inherited
public Iterator enumerateActionSequences ()
{
return _actions.values().iterator();
}
// documentation inherited
public Iterator enumerateComponentIds (final ComponentClass compClass)
{
Predicate classP = new Predicate() {
public boolean evaluate (Object input) {
CharacterComponent comp = (CharacterComponent)
_components.get(input);
return comp.componentClass.equals(compClass);
}
};
return new FilterIterator(_components.keySet().iterator(), classP);
}
/**
* Creates a component and inserts it into the component table.
*/
protected void createComponent (
int componentId, String cclass, String cname, FrameProvider fprov)
{
// look up the component class information
ComponentClass clazz = (ComponentClass)_classes.get(cclass);
if (clazz == null) {
Log.warning("Non-existent component class " +
"[class=" + cclass + ", name=" + cname +
", id=" + componentId + "].");
return;
}
// create the component
CharacterComponent component = new CharacterComponent(
componentId, cname, clazz, fprov);
// cache it
_components.put(componentId, component);
}
/**
* Instances of these provide images to our component action tilesets
* and frames to our components.
*/
protected class ResourceBundleProvider
implements ImageProvider, FrameProvider
{
/**
* Constructs an instance that will obtain image data from the
* specified resource bundle.
*/
public ResourceBundleProvider (ResourceBundle bundle)
{
_bundle = bundle;
}
// documentation inherited
public BufferedImage loadImage (String path)
throws IOException
{
// obtain the image data from our resource bundle
InputStream imgin = _bundle.getResource(path);
if (imgin == null) {
String errmsg = "No such image in resource bundle " +
"[bundle=" + _bundle + ", path=" + path + "].";
throw new FileNotFoundException(errmsg);
}
return ImageIO.read(imgin);
}
// documentation inherited
public MultiFrameImage[] getFrames (
CharacterComponent component, String action)
{
// get the tileset for this action
TileSet aset = (TileSet)_actionSets.get(action);
if (aset == null) {
Log.warning("Can't provide animation frames for undefined " +
"action [action=" + action +
", component=" + component + "].");
return null;
}
// construct the appropriate image path for the component
String path = component.componentClass.name + File.separator +
component.name + File.separator + action +
BundleUtil.IMAGE_EXTENSION;
// clone the tileset with this new image path
try {
TileSet fset = aset.clone(path);
fset.setImageProvider(this);
// and create the necessary multiframe image instances
MultiFrameImage[] frames =
new MultiFrameImage[Sprite.NUM_DIRECTIONS];
for (int i = 0; i < frames.length; i++) {
frames[i] = new TileSetFrameImage(fset, i);
}
return frames;
} catch (CloneNotSupportedException cnse) {
Log.warning("Unable to clone tileset [action=" + action +
", component=" + component +
", set=" + aset + "].");
return null;
}
}
/** The resource bundle from which we obtain image data. */
protected ResourceBundle _bundle;
}
/**
* Used to provide multiframe images using data obtained from a
* tileset.
*/
protected static class TileSetFrameImage implements MultiFrameImage
{
/**
* Constructs a tileset frame image with the specified tileset and
* for the specified orientation.
*/
public TileSetFrameImage (TileSet set, int orientation)
{
_set = set;
_orient = orientation;
}
// documentation inherited
public int getFrameCount ()
{
return _set.getTileCount() / Sprite.NUM_DIRECTIONS;
}
// documentation inherited
public Image getFrame (int index)
{
int tileIndex = _orient * Sprite.NUM_DIRECTIONS + index;
try {
return _set.getTile(tileIndex).getImage();
} catch (NoSuchTileException nste) {
Log.warning("Can't extract action frame [set=" + _set +
", orient=" + _orient + ", index=" + index + "].");
return null;
}
}
/** The tileset from which we obtain our frame images. */
protected TileSet _set;
/** The particular orientation for which we are providing
* frames. */
protected int _orient;
}
/** A table of action sequences. */
protected HashMap _actions;
/** A table of action sequence tilesets. */
protected HashMap _actionSets;
/** A table of component classes. */
protected HashMap _classes;
/** The component table. */
protected HashIntMap _components = new HashIntMap();
}
@@ -0,0 +1,263 @@
//
// $Id: ComponentBundlerTask.java,v 1.1 2001/11/27 08:09:35 mdb Exp $
package com.threerings.cast.tools.bundle;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.jar.JarOutputStream;
import java.util.jar.JarEntry;
import com.samskivert.io.PersistenceException;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import org.apache.commons.util.StreamUtils;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import com.threerings.cast.ComponentIDBroker;
import com.threerings.cast.bundle.BundleUtil;
/**
* Ant task for creating component bundles. This task must be configured
* with a number of parameters:
*
* <pre>
* target=[path to bundle file, which will be created]
* mapfile=[path to the component map file which maintains a mapping from
* component id to component class/name, it will be created the
* first time and updated as new components are mapped]
* </pre>
*
* It should also contain one or more nested &lt;fileset&gt; elements that
* enumerate the action tileset images that should be included in the
* component bundle.
*/
public class ComponentBundlerTask extends Task
{
/**
* Sets the path to the bundle file that we'll be creating.
*/
public void setTarget (File target)
{
_target = target;
}
/**
* Sets the path to the component map file that we'll use to obtain
* component ids for the bundled components.
*/
public void setMapfile (File mapfile)
{
_mapfile = mapfile;
}
/**
* Adds a nested &lt;fileset&gt; element.
*/
public void addFileset (FileSet set)
{
_filesets.add(set);
}
/**
* Performs the actual work of the task.
*/
public void execute () throws BuildException
{
// make sure everythign was set up properly
ensureSet(_target, "Must specify the path to the target bundle " +
"file via the 'file' attribute.");
ensureSet(_mapfile, "Must specify the path to the component map " +
"file via the 'mapfile' attribute.");
// load up our component ID broker
ComponentIDBroker broker = loadBroker(_mapfile);
try {
// make sure we can create our bundle file
FileOutputStream fout = new FileOutputStream(_target);
JarOutputStream jout = new JarOutputStream(fout);
// we'll fill this with component id to tuple mappings as we go
HashIntMap mapping = new HashIntMap();
// deal with the filesets
for (int i = 0; i < _filesets.size(); i++) {
FileSet fs = (FileSet)_filesets.get(i);
DirectoryScanner ds = fs.getDirectoryScanner(project);
File fromDir = fs.getDir(project);
String[] srcFiles = ds.getIncludedFiles();
for (int f = 0; f < srcFiles.length; f++) {
File cfile = new File(fromDir, srcFiles[f]);
// determine the [class, name, action] triplet
String[] info = decomposePath(cfile.getPath());
// 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]));
// construct the path that'll go in the jar file and
// stuff the component into the jarfile
jout.putNextEntry(new JarEntry(composePath(info)));
StreamUtils.pipe(new FileInputStream(cfile), jout);
}
}
// write our mapping table to the jar file as well
jout.putNextEntry(new JarEntry(BundleUtil.COMPONENTS_PATH));
ObjectOutputStream oout = new ObjectOutputStream(jout);
oout.writeObject(mapping);
oout.flush();
// seal up our jar file
jout.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);
}
/**
* Decomposes the full path to a component image into a [class, name,
* action] triplet.
*/
protected String[] decomposePath (String path)
throws BuildException
{
// first 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
String[] components = StringUtil.split(path, File.separator);
int clen = components.length;
if (clen < 3) {
throw new BuildException("Can't bundle malformed image file " +
"[path=" + path + "].");
}
String[] info = new String[3];
System.arraycopy(components, clen-3, info, 0, 3);
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)
{
return (info[0] + File.separator + info[1] + File.separator +
info[2] + BundleUtil.IMAGE_EXTENSION);
}
protected void ensureSet (Object value, String errmsg)
throws BuildException
{
if (value == null) {
throw new BuildException(errmsg);
}
}
/**
* Loads the hashmap ID broker from its persistent representation in
* the specified file.
*/
protected HashMapIDBroker loadBroker (File mapfile)
throws BuildException
{
HashMapIDBroker broker;
try {
FileInputStream fin = new FileInputStream(mapfile);
ObjectInputStream oin = new ObjectInputStream(fin);
broker = (HashMapIDBroker)oin.readObject();
} catch (FileNotFoundException fnfe) {
// if the file doesn't yet exist, start with a blank broker
broker = new HashMapIDBroker();
} catch (Exception e) {
throw new BuildException("Error loading component ID map.", 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
{
try {
FileOutputStream fout = new FileOutputStream(mapfile);
ObjectOutputStream oout = new ObjectOutputStream(fout);
oout.writeObject(broker);
oout.close();
} catch (IOException ioe) {
throw new BuildException("Unable to store component ID map.", ioe);
}
}
protected static class HashMapIDBroker
extends HashMap implements ComponentIDBroker
{
public int getComponentID (String cclass, String cname)
throws PersistenceException
{
Tuple key = new Tuple(cclass, cname);
Integer cid = (Integer)get(key);
if (cid == null) {
cid = new Integer(++_nextCID);
put(key, cid);
}
return cid.intValue();
}
public void commit ()
throws PersistenceException
{
// nothing doing
}
protected int _nextCID = 0;
}
/** The path to our component bundle file. */
protected File _target;
/** The path to our component map file. */
protected File _mapfile;
/** A list of filesets that contain tile images. */
protected ArrayList _filesets = new ArrayList();
}
@@ -0,0 +1,241 @@
//
// $Id: MetadataBundlerTask.java,v 1.1 2001/11/27 08:09:35 mdb Exp $
package com.threerings.cast.tools.bundle;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.jar.JarOutputStream;
import java.util.jar.JarEntry;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.xml.sax.SAXException;
import org.apache.commons.digester.Digester;
import com.samskivert.util.Tuple;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tools.tile.xml.SwissArmyTileSetRuleSet;
import com.threerings.cast.ActionSequence;
import com.threerings.cast.ComponentClass;
import com.threerings.cast.bundle.BundleUtil;
import com.threerings.cast.tools.xml.ActionRuleSet;
import com.threerings.cast.tools.xml.ClassRuleSet;
/**
* Ant task for creating metadata bundles, which contain action sequence
* and component class definition information. This task must be
* configured with a number of parameters:
*
* <pre>
* actiondef=[path to actions.xml]
* classdef=[path to classes.xml]
* file=[path to metadata bundle, which will be created]
* </pre>
*/
public class MetadataBundlerTask extends Task
{
public void setActiondef (String actiondef)
{
_actionDef = actiondef;
}
public void setClassdef (String classdef)
{
_classDef = classdef;
}
public void setTarget (File target)
{
_target = target;
}
/**
* Performs the actual work of the task.
*/
public void execute ()
throws BuildException
{
// make sure everythign was set up properly
ensureSet(_actionDef, "Must specify the action sequence " +
"definitions via the 'actiondef' attribute.");
ensureSet(_classDef, "Must specify the component class definitions " +
"via the 'classdef' attribute.");
ensureSet(_target, "Must specify the path to the target bundle " +
"file via the 'target' attribute.");
// make sure we can write to the target bundle file
FileOutputStream fout = null;
try {
fout = new FileOutputStream(_target);
// parse our metadata
Tuple tuple = parseActions();
HashMap actions = (HashMap)tuple.left;
HashMap actionSets = (HashMap)tuple.right;
HashMap classes = parseClasses();
// and create the bundle file
JarOutputStream jout = new JarOutputStream(fout);
// throw the serialized actions table in there
JarEntry aentry = new JarEntry(BundleUtil.ACTIONS_PATH);
jout.putNextEntry(aentry);
ObjectOutputStream oout = new ObjectOutputStream(jout);
oout.writeObject(actions);
oout.flush();
// throw the serialized action tilesets table in there
JarEntry sentry = new JarEntry(BundleUtil.ACTION_SETS_PATH);
jout.putNextEntry(sentry);
oout = new ObjectOutputStream(jout);
oout.writeObject(actionSets);
oout.flush();
// throw the serialized classes table in there
JarEntry centry = new JarEntry(BundleUtil.CLASSES_PATH);
jout.putNextEntry(centry);
oout = new ObjectOutputStream(jout);
oout.writeObject(classes);
oout.flush();
// close it up and we're done
jout.close();
} catch (IOException ioe) {
String errmsg = "Unable to output to target bundle " +
"[path=" + _target.getPath() + "].";
throw new BuildException(errmsg, ioe);
} finally {
if (fout != null) {
try {
fout.close();
} catch (IOException ioe) {
// nothing to complain about here
}
}
}
}
protected Tuple parseActions ()
throws BuildException
{
// scan through the XML once to read the actions
Digester digester = new Digester();
ActionRuleSet arules = new ActionRuleSet();
arules.setPrefix("actions");
digester.addRuleSet(arules);
digester.addSetNext("actions" + ActionRuleSet.ACTION_PATH,
"add", Object.class.getName());
ArrayList actlist = parseList(digester, _actionDef);
// now go through a second time reading the tileset info
digester = new Digester();
SwissArmyTileSetRuleSet srules = new SwissArmyTileSetRuleSet();
srules.setPrefix("actions" + ActionRuleSet.ACTION_PATH);
digester.addRuleSet(srules);
digester.addSetNext("actions" + ActionRuleSet.ACTION_PATH +
SwissArmyTileSetRuleSet.TILESET_PATH, "add",
Object.class.getName());
ArrayList setlist = parseList(digester, _actionDef);
// sanity check
if (actlist.size() != setlist.size()) {
String errmsg = "An action is missing its tileset " +
"definition, or something even wackier is going on.";
throw new BuildException(errmsg);
}
// now create our mappings
HashMap actmap = new HashMap();
HashMap setmap = new HashMap();
// create the action map
for (int i = 0; i < setlist.size(); i++) {
TileSet set = (TileSet)setlist.get(i);
ActionSequence act = (ActionSequence)actlist.get(i);
actmap.put(act.name, act);
setmap.put(act.name, set);
}
return new Tuple(actmap, setmap);
}
protected HashMap parseClasses ()
throws BuildException
{
// load up our action and class info
Digester digester = new Digester();
// add our action rule set and a a rule to grab parsed actions
ClassRuleSet crules = new ClassRuleSet();
crules.setPrefix("classes");
digester.addRuleSet(crules);
digester.addSetNext("classes" + ClassRuleSet.CLASS_PATH,
"add", Object.class.getName());
ArrayList setlist = parseList(digester, _classDef);
HashMap clmap = new HashMap();
// create the action map
for (int i = 0; i < setlist.size(); i++) {
ComponentClass cl = (ComponentClass)setlist.get(i);
clmap.put(cl.name, cl);
}
return clmap;
}
protected ArrayList parseList (Digester digester, String path)
throws BuildException
{
try {
FileInputStream fin = new FileInputStream(path);
BufferedInputStream bin = new BufferedInputStream(fin);
ArrayList setlist = new ArrayList();
digester.push(setlist);
// now fire up the digester to parse the stream
try {
digester.parse(bin);
} catch (Exception e) {
throw new BuildException("Parsing error.", e);
}
return setlist;
} catch (FileNotFoundException fnfe) {
String errmsg = "Unable to load metadata definition file " +
"[path=" + path + "].";
throw new BuildException(errmsg, fnfe);
}
}
protected void ensureSet (Object value, String errmsg)
throws BuildException
{
if (value == null) {
throw new BuildException(errmsg);
}
}
protected String _actionDef;
protected String _classDef;
protected File _target;
}