// // $Id: SceneBlock.java,v 1.20 2003/07/12 04:16:32 mdb Exp $ package com.threerings.miso.client; import java.awt.Polygon; import java.awt.Rectangle; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import com.samskivert.util.ArrayUtil; import com.samskivert.util.HashIntMap; import com.samskivert.util.StringUtil; import com.threerings.geom.GeomUtil; import com.threerings.media.tile.NoSuchTileException; import com.threerings.media.tile.NoSuchTileSetException; import com.threerings.media.tile.ObjectTile; import com.threerings.media.tile.Tile; import com.threerings.media.tile.TileManager; import com.threerings.media.tile.TileSet; import com.threerings.media.util.MathUtil; import com.threerings.miso.Log; import com.threerings.miso.data.MisoSceneModel; import com.threerings.miso.data.ObjectInfo; import com.threerings.miso.tile.BaseTile; import com.threerings.miso.util.MisoUtil; import com.threerings.miso.util.ObjectSet; /** * Contains the base and object tile information on a particular * rectangular region of a scene. */ public class SceneBlock { /** * Creates a scene block and resolves the base and object tiles that * reside therein. */ public SceneBlock ( MisoScenePanel panel, int tx, int ty, int width, int height) { _panel = panel; _bounds = new Rectangle(tx, ty, width, height); _base = new BaseTile[width*height]; _fringe = new Tile[width*height]; _covered = new boolean[width*height]; // compute our screen-coordinate footprint polygon _footprint = MisoUtil.getFootprintPolygon( panel.getSceneMetrics(), tx, ty, width, height); // the rest of our resolution will happen in resolve() } /** * Makes a note that this block was considered to be visible at the * time it was created. This is purely for debugging purposes. */ public void setVisiBlock (boolean visi) { _visi = visi; } /** * This method is called by the {@link SceneBlockResolver} on the * block resolution thread to allow us to load up our image data * without blocking the AWT thread. */ protected boolean resolve () { // if we got canned before we were resolved, go ahead and bail now if (_panel.getBlock(_bounds.x, _bounds.y) != this) { // Log.info("Not resolving abandoned block " + this + "."); _panel.blockAbandoned(this); return false; } _panel.blockResolving(this); // start with the bounds of the footprint polygon Rectangle sbounds = new Rectangle(_footprint.getBounds()); Rectangle obounds = null; // resolve our base tiles long now = System.currentTimeMillis(); int baseCount = 0, fringeCount = 0; MisoSceneModel model = _panel.getSceneModel(); for (int yy = 0; yy < _bounds.height; yy++) { for (int xx = 0; xx < _bounds.width; xx++) { int x = _bounds.x + xx, y = _bounds.y + yy; int fqTileId = model.getBaseTileId(x, y); if (fqTileId <= 0) { continue; } // load up this base tile updateBaseTile(fqTileId, x, y); baseCount++; // if there's no tile here, we don't need no fringe int tidx = index(x, y); if (_base[tidx] == null) { continue; } // compute the fringe for this tile _fringe[tidx] = _panel.computeFringeTile(x, y); fringeCount++; } } // DEBUG: check for long resolution times long stamp = System.currentTimeMillis(); long elapsed = stamp - now; if (elapsed > 500L) { Log.warning("Base and fringe resolution took long time " + "[block=" + this + ", baseCount=" + baseCount + ", fringeCount=" + fringeCount + ", elapsed=" + elapsed + "]."); } // resolve our objects ObjectSet set = new ObjectSet(); model.getObjects(_bounds, set); _objects = new SceneObject[set.size()]; now = System.currentTimeMillis(); for (int ii = 0; ii < _objects.length; ii++) { _objects[ii] = new SceneObject(_panel, set.get(ii)); sbounds.add(_objects[ii].bounds); obounds = GeomUtil.grow(obounds, _objects[ii].bounds); // DEBUG: check for long resolution times stamp = System.currentTimeMillis(); elapsed = stamp - now; now = stamp; if (elapsed > 250L) { Log.warning("Scene object took look time to resolve " + "[block=" + this + ", scobj=" + _objects[ii] + ", elapsed=" + elapsed + "]."); } } // resolve our default tileset int bsetid = model.getDefaultBaseTileSet(); try { if (bsetid > 0) { _defset = _panel.getTileManager().getTileSet(bsetid); } } catch (Exception e) { Log.warning("Unable to fetch default base tileset [tsid=" + bsetid + ", error=" + e + "]."); } // this both marks us as resolved and makes all our other updated // fields visible synchronized (this) { _obounds = obounds; _sbounds = sbounds; } return true; } /** * This is called by the {@link SceneBlockResolver} on the AWT thread * when our resolution has completed. We inform our containing panel. */ protected void wasResolved () { _panel.blockResolved(this); } /** * Returns true if this block has been resolved, false if not. */ protected synchronized boolean isResolved () { return _sbounds != null; } /** * Returns the bounds of this block, in tile coordinates. */ public Rectangle getBounds () { return _bounds; } /** * Returns the bounds of the screen coordinate rectangle that contains * all pixels that are drawn on by all tiles and objects in this * block. */ public Rectangle getScreenBounds () { return _sbounds; } /** * Returns the bounds of the screen coordinate rectangle that contains * all pixels that are drawn on by all objects (but not base tiles) in * this block. Note: this will return null if * the block has no objects. */ public Rectangle getObjectBounds () { return _obounds; } /** * Returns the screen-coordinate polygon bounding the footprint of * this block. */ public Polygon getFootprint () { return _footprint; } /** * Returns an array of all resolved scene objects in this block. */ public SceneObject[] getObjects () { return _objects; } /** * Returns the base tile at the specified coordinates or null if * there's no tile at said coordinates. */ public BaseTile getBaseTile (int tx, int ty) { BaseTile tile = _base[index(tx, ty)]; if (tile == null && _defset != null) { tile = (BaseTile)_defset.getTile( MisoUtil.getTileHash(tx, ty) % _defset.getTileCount()); } return tile; } /** * Returns the fringe tile at the specified coordinates or null if * there's no tile at said coordinates. */ public Tile getFringeTile (int tx, int ty) { return _fringe[index(tx, ty)]; } /** * Informs this scene block that the specified base tile has been * changed. */ public void updateBaseTile (int fqTileId, int tx, int ty) { String errmsg = null; int tidx = index(tx, ty); // this is a bit magical: we pass the fully qualified tile id to // the tile manager which loads up from the configured tileset // repository the appropriate tileset (which should be a // BaseTileSet) and then extracts the appropriate base tile (the // index of which is also in the fqTileId) try { if (fqTileId <= 0) { _base[tidx] = null; } else { _base[tidx] = (BaseTile) _panel.getTileManager().getTile(fqTileId); } // clear out the fringe (it must be recomputed by the caller) _fringe[tidx] = null; } catch (ClassCastException cce) { errmsg = "Scene contains non-base tile in base layer"; } catch (NoSuchTileSetException nste) { errmsg = "Scene contains non-existent tileset"; } catch (NoSuchTileException nste) { errmsg = "Scene contains non-existent tile"; } if (errmsg != null) { Log.warning(errmsg + " [fqtid=" + fqTileId + ", x=" + tx + ", y=" + ty + "]."); } } /** * Instructs this block to recompute its fringe at the specified * location. */ public void updateFringe (int tx, int ty) { int tidx = index(tx, ty); if (_base[tidx] != null) { _fringe[tidx] = _panel.computeFringeTile(tx, ty); } } /** * Adds the supplied object to this block. Coverage is not computed * for the added object, a subsequent call to {@link #update} will be * needed. * * @return true if the object was added, false if it was not because * another object of the same type already occupies that location. */ public boolean addObject (ObjectInfo info) { // make sure we don't already have this same object at these // coordinates for (int ii = 0; ii < _objects.length; ii++) { if (_objects[ii].info.equals(info)) { return false; } } _objects = (SceneObject[]) ArrayUtil.append(_objects, new SceneObject(_panel, info)); // clear out our neighbors array so that the subsequent update // causes us to recompute our coverage Arrays.fill(_neighbors, null); return true; } /** * Removes the specified object from this block. Coverage is not * recomputed, so a subsequent call to {@link #update} will be needed. * * @return true if the object was deleted, false if it was not found * in our object list. */ public boolean deleteObject (ObjectInfo info) { int oidx = -1; for (int ii = 0; ii < _objects.length; ii++) { if (_objects[ii].info.equals(info)) { oidx = ii; break; } } if (oidx == -1) { return false; } _objects = (SceneObject[])ArrayUtil.splice(_objects, oidx, 1); // clear out our neighbors array so that the subsequent update // causes us to recompute our coverage Arrays.fill(_neighbors, null); return true; } /** * Returns true if the specified traverser can traverse the specified * tile (which is assumed to be in the bounds of this scene block). */ public boolean canTraverse (Object traverser, int tx, int ty) { BaseTile base = getBaseTile(tx, ty); return !_covered[index(tx, ty)] && (base != null && base.isPassable()); } /** * Computes the memory usage of the base and object tiles in this * scene block; registering counted tiles in the hash map so that * other blocks can be sure not to double count them. Base tile usage * is placed into the zeroth array element, fringe tile usage into the * first and object tile usage into the second. */ public void computeMemoryUsage ( HashMap bases, HashSet fringes, HashMap objects, long[] usage) { // account for our base tiles MisoSceneModel model = _panel.getSceneModel(); for (int yy = 0; yy < _bounds.height; yy++) { for (int xx = 0; xx < _bounds.width; xx++) { int x = _bounds.x + xx, y = _bounds.y + yy; int tidx = index(x, y); BaseTile base = _base[tidx]; if (base == null) { continue; } BaseTile sbase = (BaseTile)bases.get(base.key); if (sbase == null) { bases.put(base.key, base); usage[0] += base.getEstimatedMemoryUsage(); } else if (base != _base[tidx]) { Log.warning("Multiple instances of same base tile " + "[base=" + base + ", x=" + xx + ", y=" + yy + "]."); usage[0] += base.getEstimatedMemoryUsage(); } // now account for the fringe if (_fringe[tidx] == null) { continue; } else if (!fringes.contains(_fringe[tidx])) { fringes.add(_fringe[tidx]); usage[1] += _fringe[tidx].getEstimatedMemoryUsage(); } } } // now get the object tiles int ocount = (_objects == null) ? 0 : _objects.length; for (int ii = 0; ii < ocount; ii++) { SceneObject scobj = _objects[ii]; ObjectTile tile = (ObjectTile)objects.get(scobj.tile.key); if (tile == null) { objects.put(scobj.tile.key, scobj.tile); usage[2] += scobj.tile.getEstimatedMemoryUsage(); } else if (tile != scobj.tile) { Log.warning("Multiple instances of same object tile: " + scobj.info + "."); usage[2] += scobj.tile.getEstimatedMemoryUsage(); } } } /** * Returns a string representation of this instance. */ public String toString () { int bx = MathUtil.floorDiv(_bounds.x, _bounds.width); int by = MathUtil.floorDiv(_bounds.y, _bounds.height); return StringUtil.coordsToString(bx, by) + ":" + StringUtil.toString(_bounds) + ":" + ((_objects == null) ? 0 : _objects.length) + (_visi ? ":v" : ":i"); } /** * Returns the index into our arrays of the specified tile. */ protected final int index (int tx, int ty) { // if (!_bounds.contains(tx, ty)) { // String errmsg = "Coordinates out of bounds: +" + tx + "+" + ty + // " not in " + StringUtil.toString(_bounds); // throw new IllegalArgumentException(errmsg); // } return (ty-_bounds.y)*_bounds.width + (tx-_bounds.x); } /** * Links this block to its neighbors; informs neighboring blocks of * object coverage. */ protected void update (HashIntMap blocks) { boolean recover = false; // link up to our neighbors for (int ii = 0; ii < DX.length; ii++) { SceneBlock neigh = (SceneBlock) blocks.get(neighborKey(DX[ii], DY[ii])); if (neigh != _neighbors[ii]) { _neighbors[ii] = neigh; // if we're linking up to a neighbor for the first time; // we need to recalculate our coverage recover = recover || (neigh != null); // Log.info(this + " was introduced to " + neigh + "."); } } // if we need to regenerate the set of tiles covered by our // objects, do so if (recover) { for (int ii = 0; ii < _objects.length; ii++) { setCovered(blocks, _objects[ii]); } } } /** Computes the key of our neighbor. */ protected final int neighborKey (int dx, int dy) { int nx = MathUtil.floorDiv(_bounds.x, _bounds.width)+dx; int ny = MathUtil.floorDiv(_bounds.y, _bounds.height)+dy; return MisoScenePanel.compose(nx, ny); } /** Computes the key for the block that holds the specified tile. */ protected final int blockKey (int tx, int ty) { int bx = MathUtil.floorDiv(tx, _bounds.width); int by = MathUtil.floorDiv(ty, _bounds.height); return MisoScenePanel.compose(bx, by); } /** * Sets the footprint of this object tile */ protected void setCovered (HashIntMap blocks, SceneObject scobj) { int endx = scobj.info.x - scobj.tile.getBaseWidth() + 1; int endy = scobj.info.y - scobj.tile.getBaseHeight() + 1; for (int xx = scobj.info.x; xx >= endx; xx--) { for (int yy = scobj.info.y; yy >= endy; yy--) { SceneBlock block = (SceneBlock)blocks.get(blockKey(xx, yy)); if (block != null) { block.setCovered(xx, yy); } } } // Log.info("Updated coverage " + scobj.info + "."); } /** * Indicates that this tile is covered by an object footprint. */ protected void setCovered (int tx, int ty) { _covered[index(tx, ty)] = true; } /** The panel for which we contain a block. */ protected MisoScenePanel _panel; /** The bounds of (in tile coordinates) of this block. */ protected Rectangle _bounds; /** The bounds (in screen coords) of all images rendered by this block. */ protected Rectangle _sbounds; /** The bounds (in screen coords) of all objects rendered by this block. */ protected Rectangle _obounds; /** A polygon bounding the footprint of this block. */ protected Polygon _footprint; /** Used to return a tile where we have none. */ protected TileSet _defset; /** Our base tiles. */ protected BaseTile[] _base; /** Our fringe tiles. */ protected Tile[] _fringe; /** Indicates whether our tiles are covered by an object. */ protected boolean[] _covered; /** Info on our objects. */ protected SceneObject[] _objects; /** Our neighbors in the eight cardinal directions. */ protected SceneBlock[] _neighbors = new SceneBlock[DX.length]; /** A debug flag indicating whether we were visible at creation. */ protected boolean _visi; // used to link up to our neighbors protected static final int[] DX = { -1, -1, 0, 1, 1, 1, 0, -1 }; protected static final int[] DY = { 0, -1, -1, -1, 0, 1, 1, 1 }; }