// // 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.miso.client; import java.util.ArrayList; import java.util.Arrays; import java.util.Map; import java.util.Set; import java.awt.Polygon; import java.awt.Rectangle; import com.google.common.collect.Lists; import com.samskivert.util.ArrayUtil; import com.samskivert.util.StringUtil; 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.tile.TileUtil; import com.threerings.media.util.MathUtil; import com.threerings.geom.GeomUtil; import com.threerings.miso.data.MisoSceneModel; import com.threerings.miso.data.ObjectInfo; import com.threerings.miso.tile.BaseTile; import com.threerings.miso.util.MisoSceneMetrics; import com.threerings.miso.util.MisoUtil; import com.threerings.miso.util.ObjectSet; import static com.threerings.miso.Log.log; /** * Contains the base and object tile information on a particular rectangular region of a scene. */ public class SceneBlock { /** * Creates a scene block belonging to panel in preparation for its later resolution. */ public SceneBlock (MisoScenePanel panel, int tx, int ty, int width, int height) { this(panel.getSceneModel(), panel.getSceneMetrics(), panel.getTileManager(), tx, ty, width, height); _panel = panel; } public SceneBlock (MisoSceneModel model, MisoSceneMetrics metrics, TileManager tileMgr, int tx, int ty, int width, int height) { _model = model; _metrics = metrics; _tileMgr = tileMgr; _bounds = new Rectangle(tx, ty, width, height); _base = new BaseTile[width*height]; _fringe = new BaseTile[width*height]; _covered = new boolean[width*height]; // compute our screen-coordinate footprint polygon _footprint = MisoUtil.getFootprintPolygon(_metrics, 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. */ public boolean resolve () { // if we got canned before we were resolved, go ahead and bail now if (_panel != null) { if (_panel.getBlock(_bounds.x, _bounds.y) != this) { // Log.info("Not resolving abandoned block " + this + "."); _wasAbandoned = true; return false; } _panel.blockResolving(this); } // We're doing our work, we're needed! _wasAbandoned = false; // 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; 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] = 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); ArrayList<SceneObject> scobjs = Lists.newArrayList(); now = System.currentTimeMillis(); for (int ii = 0, ll = set.size(); ii < ll; ii++) { SceneObject scobj = makeSceneObject(set.get(ii)); // ignore this object if it failed to resolve if (scobj.bounds == null) { continue; } sbounds.add(scobj.bounds); obounds = GeomUtil.grow(obounds, scobj.bounds); scobjs.add(scobj); // 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=" + scobj + ", elapsed=" + elapsed + "]."); } } _objects = scobjs.toArray(new SceneObject[scobjs.size()]); // resolve our default tileset int bsetid = _model.getDefaultBaseTileSet(); try { if (bsetid > 0) { _defset = _tileMgr.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; } protected SceneObject makeSceneObject (ObjectInfo info) { return new SceneObject(_metrics, _tileMgr, _panel == null ? null : _panel.getColorizer(info), info); } /** Computes the fringe tile for the specified coordinate. */ protected BaseTile computeFringeTile (int tx, int ty) { return _panel == null ? null : _panel.computeFringeTile(tx, ty); } /** * This is called by the {@link SceneBlockResolver} on the AWT thread when our resolution has * completed. We inform our containing panel. */ protected void wasResolved () { if (_panel != null) { if (_wasAbandoned) { _panel.blockAbandoned(this); } else { _panel.blockResolved(this); } } } /** * Returns true if this block has been resolved, false if not. */ public 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. <em>Note:</em> this will return <code>null</code> 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( TileUtil.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 BaseTile 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)_tileMgr.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"; } 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] = 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 (SceneObject _object : _objects) { if (_object.info.equals(info)) { return false; } } _objects = ArrayUtil.append(_objects, makeSceneObject(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 = 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) { if (_covered[index(tx, ty)]) { return false; } // null base or impassable base kills traversal BaseTile base = getBaseTile(tx, ty); if ((base == null) || !base.isPassable()) { return false; } // fringe can only kill traversal if it is present BaseTile fringe = getFringeTile(tx, ty); return (fringe == null) || fringe.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 (Map<Tile.Key,BaseTile> bases, Set<BaseTile> fringes, Map<Tile.Key,ObjectTile> objects, long[] usage) { // account for our base tiles 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 = 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 = 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(); } } } @Override 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 (Map<Integer, SceneBlock> blocks) { boolean recover = false; // link up to our neighbors for (int ii = 0; ii < DX.length; ii++) { SceneBlock neigh = 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 (SceneObject _object : _objects) { setCovered(blocks, _object); } } } /** 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 (Map<Integer, SceneBlock> 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 = 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 or null if we aren't backed by a panel. */ protected MisoScenePanel _panel; protected MisoSceneMetrics _metrics; protected MisoSceneModel _model; protected TileManager _tileMgr; /** 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 BaseTile[] _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; /** If we discovered we were no longer needed in our last call to resolve. */ protected boolean _wasAbandoned; // 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 }; }