
com.threerings.miso.client.SceneBlock Maven / Gradle / Ivy
//
// 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 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. 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(
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 bases, Set fringes,
Map 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 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 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 };
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy