All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.miso.client.SceneBlock Maven / Gradle / Ivy

The newest version!
//
// 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