First checkpoint commit of fringing in AS. This still needs testing and some callback-wrangling related to getting tile images.

git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@947 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Mike Thomas
2010-07-13 06:08:25 +00:00
parent 13b74c92a1
commit 263cc28f99
5 changed files with 683 additions and 0 deletions
@@ -0,0 +1,416 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.miso.tile {
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.display.Bitmap;
import flash.display.BitmapData;
import com.threerings.display.ImageUtil;
import com.threerings.media.tile.NoSuchTileSetError;
import com.threerings.media.tile.BaseTile;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TileManager;
import com.threerings.media.tile.TileUtil;
import com.threerings.util.ArrayUtil;
import com.threerings.util.Integer;
import com.threerings.util.Log;
import com.threerings.util.Maps;
import com.threerings.util.Map;
import com.threerings.util.WeakReference;
import com.threerings.miso.data.MisoSceneModel;
public class AutoFringer
{
private var log :Log = Log.getLog(AutoFringer);
public function AutoFringer (fringeConf :FringeConfiguration, tMgr :TileManager)
{
_fringeConf = fringeConf;
_tMgr = tMgr;
// Construct the BITS_TO_INDEX array.
var initIdx :int;
// first clear everything to -1 (meaning there is no tile defined)
for (initIdx = 0; initIdx < INIT_CT; initIdx++) {
BITS_TO_INDEX[initIdx] = -1;
}
// then fill in with the defined tiles.
for (initIdx = 0; initIdx < FRINGETILES.length; initIdx++) {
BITS_TO_INDEX[FRINGETILES[initIdx]] = initIdx;
}
}
/**
* Returns the fringe configuration used by this fringer.
*/
public function getFringeConf () :FringeConfiguration
{
return _fringeConf;
}
/**
* Compute and return the fringe tile to be inserted at the specified location.
*/
public function getFringeTile (scene :MisoSceneModel, col :int, row :int, fringes :Map,
masks :Map) :BaseTile
{
// get the tileset id of the base tile we are considering
var underset :int = adjustTileSetId(scene.getBaseTileId(col, row) >> 16);
// start with a clean temporary fringer map
_fringers.clear();
var passable :Boolean = true;
// walk through our influence tiles
for (var y :int = row - 1, maxy :int = row + 2; y < maxy; y++) {
for (var x :int = col - 1, maxx :int = col + 2; x < maxx; x++) {
// we sensibly do not consider ourselves
if ((x == col) && (y == row)) {
continue;
}
// determine the tileset for this tile
var btid :int= scene.getBaseTileId(x, y);
var baseset :int= adjustTileSetId((btid <= 0) ?
scene.getDefaultBaseTileSet() : (btid >> 16));
// determine if it fringes on our tile
var pri :int = _fringeConf.fringesOn(baseset, underset);
if (pri == -1) {
continue;
}
var fringer :FringerRec = FringerRec(_fringers.get(baseset));
if (fringer == null) {
fringer = new FringerRec(baseset, pri);
_fringers.put(baseset, fringer);
}
// now turn on the appropriate fringebits
fringer.bits |= FLAGMATRIX[y - row + 1][x - col + 1];
// See if a tile that fringes on us kills our passability,
// but don't count the default base tile against us, as
// we allow users to splash in the water.
if (passable && (btid > 0)) {
try {
var bt :BaseTile = BaseTile(_tMgr.getTile(btid));
passable = bt.isPassable();
} catch (nstse :NoSuchTileSetError) {
log.warning("Autofringer couldn't find a base set while attempting to " +
"figure passability", nstse);
}
}
}
}
// if nothing fringed, we're done
var numfringers :int = _fringers.size();
if (numfringers == 0) {
return null;
}
// otherwise compose a FringeTile from the specified fringes
var frecs :Array = new Array(numfringers);
for (var ii :int = 0, pp :int = 0; ii < 16; ii++) {
var rec :FringerRec = FringerRec(_fringers.get(ii));
if (rec != null) {
frecs[pp++] = rec;
}
}
return composeFringeTile(frecs, fringes, TileUtil.getTileHash(col, row), passable, masks);
}
/**
* Compose a FringeTile out of the various fringe images needed.
*/
protected function composeFringeTile (fringers :Array,
fringes :Map, hashValue :int, passable :Boolean, masks :Map) :FringeTile
{
// sort the array so that higher priority fringers get drawn first
ArrayUtil.sort(fringers);
// Generate an identifier for the fringe tile being created as an array of the keys of its
// component tiles in the order they'll be drawn in the fringe tile.
var fringeIds :Array = [];
for each (var fringer :FringerRec in fringers) {
var indexes :Array = getFringeIndexes(fringer.bits);
var tsr :FringeTileSetRecord = _fringeConf.getFringe(fringer.baseset, hashValue);
var fringeset :int = tsr.fringe_tsid;
for each (var index :int in indexes) {
// Add a key for this tile as an int containing its base tile, the fringe set it's
// working with and the index used in that set.
fringeIds.push((fringer.baseset << 20) + (fringeset << 8) + index);
}
}
var frTile :FringeTile = new FringeTile(fringeIds, passable);
// If the fringes map contains something with the same fringe identifier, this will pull
// it out and we can use it instead.
var result :WeakReference = fringes.get(frTile);
if (result != null) {
var fringe :FringeTile = result.get();
if (fringe != null) {
return fringe;
}
}
// There's no fringe with the same identifier, so we need to create the tile.
var img :Bitmap = null;
for each (fringer in fringers) {
indexes = getFringeIndexes(fringer.bits);
tsr = _fringeConf.getFringe(fringer.baseset, hashValue);
for each (index in indexes) {
try {
img = getTileImage(img, tsr, fringer.baseset, index, hashValue, masks);
} catch (nstse :NoSuchTileSetError) {
log.warning("Autofringer couldn't find a needed tileset", nstse);
}
}
}
frTile.setImage(img);
fringes.put(frTile, new WeakReference(frTile));
return frTile;
}
/**
* Retrieve or compose an image for the specified fringe.
*/
protected function getTileImage (img :Bitmap, tsr :FringeTileSetRecord ,
baseset :int, index :int, hashValue :int, masks :Map) :Bitmap
{
var fringeset :int = tsr.fringe_tsid;
var fset :TileSet = _tMgr.getTileSet(fringeset);
if (!tsr.mask) {
// oh good, this is easy
var stamp :Tile = fset.getTile(index);
return stampTileImage(stamp, img, stamp.getWidth(), stamp.getHeight());
}
// otherwise, it's a mask..
var maskkey :int = (baseset << 20) + (fringeset << 8) + index;
var mask :Bitmap = masks.get(maskkey);
if (mask == null) {
// TODO - callbacks
var fsrc :Bitmap = Bitmap(_tMgr.getTileSet(fringeset).getTileImage(index, null, null));
var bsrc :Bitmap = Bitmap(_tMgr.getTileSet(baseset).getTileImage(0, null, null));
mask = composeMaskedImage(fsrc, bsrc);
masks.put(maskkey, mask);
}
return stampTileImage(mask, img, mask.width, mask.height);
}
/**
* Create an image using the alpha channel from the first and the RGB values from the second.
*/
public static function composeMaskedImage (mask :Bitmap, base :Bitmap) :Bitmap
{
var data :BitmapData = new BitmapData(base.width, base.height);
data.copyPixels(base.bitmapData, new Rectangle(0, 0, base.width, base.height),
new Point(0, 0), mask.bitmapData, new Point(0, 0), false);
return new Bitmap(data);
}
/** Helper function for {@link #getTileImage}. */
protected function stampTileImage (stamp :Object, ftimg :Bitmap, width :int,
height :int) :Bitmap
{
// create the target image if necessary
if (ftimg == null) {
ftimg = new Bitmap(new BitmapData(width, height, true, 0x00000000));
}
var img :Bitmap;
if (stamp is Tile) {
img = Bitmap((Tile(stamp)).getImage());
} else {
img = Bitmap(stamp);
}
ftimg.bitmapData.draw(img);
return ftimg;
}
/**
* Get the fringe index specified by the fringebits. If no index is available, try breaking
* down the bits into contiguous regions of bits and look for indexes for those.
*/
protected function getFringeIndexes (bits :int) :Array
{
var index :int = BITS_TO_INDEX[bits];
if (index != -1) {
return [index];
}
// otherwise, split the bits into contiguous components
// look for a zero and start our first split
var start :int = 0;
while ((((1 << start) & bits) != 0) && (start < NUM_FRINGEBITS)) {
start++;
}
if (start == NUM_FRINGEBITS) {
// we never found an empty fringebit, and since index (above)
// was already -1, we have no fringe tile for these bits.. sad.
return new Array(0);
}
var indexes :Array = [];
var weebits :int = 0;
for (var ii :int = (start + 1) % NUM_FRINGEBITS; ii != start;
ii = (ii + 1) % NUM_FRINGEBITS) {
if (((1 << ii) & bits) != 0) {
weebits |= (1 << ii);
} else if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.add(index);
}
weebits = 0;
}
}
if (weebits != 0) {
index = BITS_TO_INDEX[weebits];
if (index != -1) {
indexes.add(index);
}
}
return indexes;
}
/**
* Allow subclasses to apply arbitrary modifications to tileset ids for whatever nefarious
* purposes they may have.
*/
protected function adjustTileSetId (tileSetId :int) :int
{
// by default, nothing.
return tileSetId;
}
// fringe bits
// see docs/miso/fringebits.png
//
protected static const NORTH :int = 1 << 0;
protected static const NORTHEAST :int = 1 << 1;
protected static const EAST :int = 1 << 2;
protected static const SOUTHEAST :int = 1 << 3;
protected static const SOUTH :int = 1 << 4;
protected static const SOUTHWEST :int = 1 << 5;
protected static const WEST :int = 1 << 6;
protected static const NORTHWEST :int = 1 << 7;
protected static const NUM_FRINGEBITS :int = 8;
// A matrix mapping adjacent tiles to which fringe bits they affect.
// (x and y are offset by +1, since we can't have -1 as an array index)
// again, see docs/miso/fringebits.png
//
protected static const FLAGMATRIX :Array= [
[ NORTHEAST, (NORTHEAST | EAST | SOUTHEAST), SOUTHEAST ],
[ (NORTHWEST | NORTH | NORTHEAST), 0, (SOUTHEAST | SOUTH | SOUTHWEST) ],
[ NORTHWEST, (NORTHWEST | WEST | SOUTHWEST), SOUTHWEST ]
];
/**
* The fringe tiles we use. These are the 17 possible tiles made up of continuous fringebits
* sections. Huh? see docs/miso/fringebits.png
*/
protected static const FRINGETILES :Array = [
SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST,
NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST,
NORTHEAST,
NORTHWEST | NORTH | NORTHEAST,
NORTHWEST,
SOUTHWEST | WEST | NORTHWEST | NORTH | NORTHEAST,
NORTHWEST | NORTH | NORTHEAST | EAST | SOUTHEAST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST,
SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST | WEST | SOUTHWEST,
SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST | NORTH | NORTHWEST,
NORTHWEST | WEST | SOUTHWEST | SOUTH | SOUTHEAST | EAST | NORTHEAST,
// all the directions!
NORTH | NORTHEAST | EAST | SOUTHEAST |
SOUTH | SOUTHWEST | WEST | NORTHWEST
];
// A reverse map of the above array, for quickly looking up which tile
// we want.
protected const INIT_CT :int = (1 << NUM_FRINGEBITS);
protected const BITS_TO_INDEX :Array = new Array(INIT_CT);
protected var _tMgr :TileManager;
protected var _fringeConf :FringeConfiguration;
protected var _fringers :Map = Maps.newMapOf(int);
}
}
import com.threerings.util.Comparable;
/**
* A record for holding information about a particular fringe as we're computing what it will
* look like.
*/
class FringerRec
implements Comparable
{
public var baseset :int;
public var priority :int;
public var bits :int;
public function FringerRec (base :int, pri :int)
{
baseset = base;
priority = pri;
}
public function compareTo (o :Object) :int
{
return priority - FringerRec(o).priority;
}
public function toString () :String
{
return "[base=" + baseset + ", pri=" + priority + ", bits="
+ bits.toString(16) + "]";
}
}
@@ -0,0 +1,90 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.miso.tile {
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.media.tile.TileSetIdMap;
/**
* Used to manage data about which base tilesets fringe on which others
* and how they fringe.
*/
public class FringeConfiguration
{
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeConfiguration
{
var config :FringeConfiguration = new FringeConfiguration();
for each (var baseXml :XML in xml.base) {
config.addFringeRecord(FringeRecord.fromXml(baseXml, idMap));
}
return config;
}
/**
* Adds a parsed FringeRecord to this instance. This is used when parsing
* the fringerecords from xml.
*/
public function addFringeRecord (frec :FringeRecord) :void
{
_frecs.put(frec.base_tsid, frec);
}
/**
* If the first base tileset fringes upon the second, return the
* fringe priority of the first base tileset, otherwise return -1.
*/
public function fringesOn (first :int, second :int) :int
{
var f1 :FringeRecord = _frecs.get(first);
// we better have a fringe record for the first
if (null != f1) {
// it had better have some tilesets defined
if (f1.tilesets.size() > 0) {
var f2 :FringeRecord = _frecs.get(second);
// and we only fringe if second doesn't exist or has a lower
// priority
if ((null == f2) || (f1.priority > f2.priority)) {
return f1.priority;
}
}
}
return -1;
}
/**
* Get a random FringeTileSetRecord from amongst the ones
* listed for the specified base tileset.
*/
public function getFringe (baseset :int, hashValue :int) :FringeTileSetRecord
{
var f :FringeRecord = _frecs.get(baseset);
return f.tilesets.get(hashValue % f.tilesets.size());
}
/** The mapping from base tileset id to fringerecord. */
protected var _frecs :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,65 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.miso.tile {
import com.threerings.media.tile.TileSetIdMap;
import com.threerings.util.StringUtil;
public class FringeRecord
{
/** The tileset id of the base tileset to which this applies. */
public var base_tsid :int;
/** The fringe priority of this base tileset. */
public var priority :int;
/** A list of the possible tilesets that can be used for fringing. */
public var tilesets :Array;
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeRecord
{
var rec :FringeRecord = new FringeRecord();
rec.base_tsid = idMap.getTileSetId(xml.@name);
rec.priority = xml.@priority;
for each (var tsXml :XML in xml.tileset) {
rec.addTileset(FringeTileSetRecord.fromXml(tsXml, idMap));
}
return rec;
}
/** Used when parsing the tilesets definitions. */
public function addTileset (record :FringeTileSetRecord) :void
{
tilesets.push(record);
}
/** Did everything parse well? */
public function isValid () :Boolean
{
return ((base_tsid != 0) && (priority > 0));
}
public function toString () :String
{
return "[base_tsid=" + base_tsid + ", priority=" + priority +
", tilesets=" + StringUtil.toString(tilesets) + "]";
}
}
}
@@ -0,0 +1,60 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.miso.tile {
import com.threerings.util.ArrayUtil;
import com.threerings.util.Hashable;
import com.threerings.media.tile.BaseTile;
public class FringeTile extends BaseTile
implements Hashable
{
public function FringeTile (fringeIds :Array, passable :Boolean)
{
setPassable(passable);
_fringeIds = fringeIds;
}
public function equals (obj :Object) :Boolean
{
if (!(obj is FringeTile)) {
return false;
}
var fObj :FringeTile = FringeTile(obj);
return _passable == fObj._passable && ArrayUtil.equals(_fringeIds, fObj._fringeIds);
}
public function hashCode () :int
{
var result :int = 33; // can't use Arrays.hashCode(long) as it's 1.5 only
for each (var key :int in _fringeIds) {
result = result * 37 + key;
}
if (_passable) {
result++;
}
return result;
}
/** The fringe keys of the tiles that went into this tile in the order they were drawn. */
protected var _fringeIds :Array;
}
}
@@ -0,0 +1,52 @@
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/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.miso.tile {
import com.threerings.media.tile.TileSetIdMap;
public class FringeTileSetRecord
{
/** The tileset id of the fringe tileset. */
public var fringe_tsid :int;
/** Is this a mask? */
public var mask :Boolean;
public static function fromXml (xml :XML, idMap :TileSetIdMap) :FringeTileSetRecord
{
var rec :FringeTileSetRecord = new FringeTileSetRecord();
rec.fringe_tsid = idMap.getTileSetId(xml.@name);
rec.mask = xml.@mask;
return rec;
}
/** Did everything parse well? */
public function isValid () :Boolean
{
return (fringe_tsid != 0);
}
public function toString () :String
{
return "[fringe_tsid=" + fringe_tsid + ", mask=" + mask + "]";
}
}
}