8ad7f5eaef
git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@375 c613c5cb-e716-0410-b11b-feb51c14d237
580 lines
21 KiB
Java
580 lines
21 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Vilya library - tools for developing networked games
|
|
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/vilya/
|
|
//
|
|
// 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.whirled.spot.server;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
|
|
import com.samskivert.util.HashIntMap;
|
|
import com.threerings.util.Name;
|
|
|
|
import com.threerings.presents.dobj.DObject;
|
|
import com.threerings.presents.dobj.Subscriber;
|
|
import com.threerings.presents.dobj.ObjectAccessException;
|
|
import com.threerings.presents.server.InvocationException;
|
|
|
|
import com.threerings.crowd.chat.server.SpeakProvider;
|
|
import com.threerings.crowd.data.BodyObject;
|
|
import com.threerings.crowd.data.OccupantInfo;
|
|
import com.threerings.crowd.server.CrowdServer;
|
|
import com.threerings.whirled.server.SceneManager;
|
|
|
|
import com.threerings.whirled.spot.Log;
|
|
import com.threerings.whirled.spot.data.Cluster;
|
|
import com.threerings.whirled.spot.data.ClusterObject;
|
|
import com.threerings.whirled.spot.data.ClusteredBodyObject;
|
|
import com.threerings.whirled.spot.data.Location;
|
|
import com.threerings.whirled.spot.data.Portal;
|
|
import com.threerings.whirled.spot.data.SceneLocation;
|
|
import com.threerings.whirled.spot.data.SpotCodes;
|
|
import com.threerings.whirled.spot.data.SpotScene;
|
|
import com.threerings.whirled.spot.data.SpotSceneObject;
|
|
|
|
/**
|
|
* Handles the movement of bodies between locations in the scene and creates the necessary
|
|
* distributed objects to allow bodies in clusters to chat with one another.
|
|
*/
|
|
public class SpotSceneManager extends SceneManager
|
|
implements SpotCodes
|
|
{
|
|
/**
|
|
* Move the specified body to the default portal, if possible.
|
|
*/
|
|
public static void moveBodyToDefaultPortal (BodyObject body)
|
|
{
|
|
SpotSceneManager mgr = (SpotSceneManager)
|
|
CrowdServer.plreg.getPlaceManager(body.getPlaceOid());
|
|
if (mgr == null) {
|
|
return;
|
|
}
|
|
|
|
SpotScene scene = (SpotScene)mgr.getScene();
|
|
if (scene == null) {
|
|
Log.warning("No scene in moveBodyToDefaultPortal()? [who=" + body.who() +
|
|
", where=" + mgr.where() + "].");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Location eloc = scene.getDefaultEntrance().getLocation();
|
|
mgr.handleChangeLoc(body, eloc);
|
|
} catch (InvocationException ie) {
|
|
Log.warning("Could not move user to default portal [where=" + mgr.where() +
|
|
", who=" + body.who() + ", error=" + ie + "].");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assigns a starting location for an entering body. This will happen before the body is made
|
|
* to "occupy" the scene (defined by their having an occupant info record). So when they do
|
|
* finally occupy the scene, the client will know where to render them.
|
|
*/
|
|
public void mapEnteringBody (BodyObject body, Portal from)
|
|
{
|
|
// don't save a null from portal, because it simply means "use the default entrance"
|
|
if (from != null) {
|
|
_enterers.put(body.getOid(), from);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called if a body failed to enter our scene after we assigned them an entering position.
|
|
*/
|
|
public void clearEnteringBody (BodyObject body)
|
|
{
|
|
_enterers.remove(body.getOid());
|
|
}
|
|
|
|
/**
|
|
* This is called when a user requests to traverse a portal from this scene to another scene.
|
|
* The manager may return an error code string that will be reported back to the caller
|
|
* explaining the failure or <code>null</code> indicating that it is OK for the caller to
|
|
* traverse the portal.
|
|
*/
|
|
public String mayTraversePortal (BodyObject body, Portal portal)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* This is called to let this scene manager know that the user is about to traverse the
|
|
* specified portal. The default implementation relocates the user to the location associated
|
|
* with the portal. It is still possible that the traversal will fail, so don't do anything too
|
|
* crazy.
|
|
*/
|
|
public void willTraversePortal (BodyObject body, Portal portal)
|
|
{
|
|
updateLocation(body, portal.getLocation());
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void didStartup ()
|
|
{
|
|
// get a casted reference to our place object (we need to do this before calling
|
|
// super.didStartup() because that will call sceneManagerDidResolve() which may start
|
|
// letting people into the scene)
|
|
_ssobj = (SpotSceneObject)_plobj;
|
|
|
|
super.didStartup();
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void gotSceneData ()
|
|
{
|
|
super.gotSceneData();
|
|
|
|
// keep a casted reference around to our scene
|
|
_sscene = (SpotScene)_scene;
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void bodyLeft (int bodyOid)
|
|
{
|
|
super.bodyLeft(bodyOid);
|
|
|
|
// clear out their location information
|
|
_ssobj.removeFromOccupantLocs(Integer.valueOf(bodyOid));
|
|
|
|
// clear any cluster they may occupy
|
|
removeFromCluster(bodyOid);
|
|
|
|
// let's make damned sure they're not in any cluster
|
|
Iterator<ClusterRecord> cliter = _clusters.values().iterator();
|
|
while (cliter.hasNext()) {
|
|
ClusterRecord clrec = cliter.next();
|
|
if (clrec.containsKey(bodyOid)) {
|
|
Log.info("Pruning departed body from cluster [boid=" + bodyOid +
|
|
", cluster=" + clrec + "].");
|
|
clrec.removeBody(bodyOid);
|
|
if (clrec.size() == 0) {
|
|
// if we just removed the last body, destroy the cluster, need to use the
|
|
// iterator's removal so we don't hose ourselves
|
|
clrec.destroy(false);
|
|
cliter.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void insertOccupantInfo (OccupantInfo info, BodyObject body)
|
|
{
|
|
super.insertOccupantInfo(info, body);
|
|
|
|
// we don't actually populate their occupant info, but instead assign them their starting
|
|
// location in the scene
|
|
assignStartingLocation(body);
|
|
}
|
|
|
|
/**
|
|
* Give our new body a starting location.
|
|
*/
|
|
protected void assignStartingLocation (BodyObject body)
|
|
{
|
|
Portal from = _enterers.remove(body.getOid());
|
|
Portal entry;
|
|
if (from != null && from.targetPortalId != -1) {
|
|
entry = _sscene.getPortal(from.targetPortalId);
|
|
if (entry == null) {
|
|
Log.warning("Body mapped at invalid portal [where=" + where() +
|
|
", who=" + body.who() + ", from=" + from + "].");
|
|
entry = _sscene.getDefaultEntrance();
|
|
}
|
|
} else {
|
|
entry = _sscene.getDefaultEntrance();
|
|
}
|
|
|
|
// Log.debug("Positioning entering body [who=" + body.who() +
|
|
// ", where=" + entry.getOppLocation() + "].");
|
|
|
|
// create a scene location for them located on the entrance portal but facing the opposite
|
|
// direction
|
|
_ssobj.addToOccupantLocs(computeEnteringLocation(body, from, entry));
|
|
}
|
|
|
|
/**
|
|
* Called when the supplied body is entering our scene via the specified portal. The default
|
|
* location is the one associated with the portal, but derived classes may wish to adjust this.
|
|
*
|
|
* @param from the portal the body followed to get to this scene (or null).
|
|
* @param entry the portal referenced by the from portal's targetPortalId or the scene's
|
|
* default entrance if the from portal did not exist or had no target portal.
|
|
*/
|
|
protected SceneLocation computeEnteringLocation (BodyObject body, Portal from, Portal entry)
|
|
{
|
|
return new SceneLocation(entry.getOppLocation(), body.getOid());
|
|
}
|
|
|
|
/**
|
|
* Called by the {@link SpotProvider} when we receive a request by a user to occupy a
|
|
* particular location.
|
|
*
|
|
* @param source the body to be moved.
|
|
* @param loc the location to which to move the body.
|
|
*
|
|
* @exception InvocationException thrown with a reason code explaining the failure if there is
|
|
* a problem processing the request.
|
|
*/
|
|
protected void handleChangeLoc (BodyObject source, Location loc)
|
|
throws InvocationException
|
|
{
|
|
// make sure they are in our scene
|
|
if (!_ssobj.occupants.contains(source.getOid())) {
|
|
Log.warning("Refusing change loc from non-scene occupant [where=" + where() +
|
|
", who=" + source.who() + ", loc=" + loc + "].");
|
|
throw new InvocationException(INTERNAL_ERROR);
|
|
}
|
|
|
|
// let our derived classes decide if this is an OK place to stand
|
|
if (!validateLocation(source, loc)) {
|
|
throw new InvocationException(INVALID_LOCATION);
|
|
}
|
|
|
|
// update the user's location information in the scene which will indicate to the client
|
|
// that their avatar should be moved from its current position to their new position
|
|
updateLocation(source, loc);
|
|
|
|
// remove them from any cluster as they've departed
|
|
removeFromCluster(source.getOid());
|
|
}
|
|
|
|
/**
|
|
* Derived classes can override this method and validate that the specified body can stand in
|
|
* the requested location. The default implementation returns <code>true</code> in all
|
|
* circumstances; stand where ye may!
|
|
*/
|
|
protected boolean validateLocation (BodyObject source, Location loc)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Updates the location of the specified body.
|
|
*/
|
|
protected void updateLocation (BodyObject source, Location loc)
|
|
{
|
|
SceneLocation sloc = new SceneLocation(loc, source.getOid());
|
|
if (!_ssobj.occupantLocs.contains(sloc)) {
|
|
// complain if they don't already have a location configured
|
|
Log.warning("Changing loc for occupant without previous loc [where=" + where() +
|
|
", who=" + source.who() + ", nloc=" + loc + "].");
|
|
Thread.dumpStack();
|
|
_ssobj.addToOccupantLocs(sloc);
|
|
} else {
|
|
_ssobj.updateOccupantLocs(sloc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the {@link SpotProvider} when we receive a request by a user to join a particular
|
|
* cluster.
|
|
*
|
|
* @param joiner the body to be moved.
|
|
* @param targetOid the bodyOid of another user or the oid of an existing cluster; the moving
|
|
* user will be made to join the other user's cluster.
|
|
*
|
|
* @exception InvocationException thrown with a reason code explaining the failure if there is
|
|
* a problem processing the request.
|
|
*/
|
|
protected void handleJoinCluster (BodyObject joiner, int targetOid)
|
|
throws InvocationException
|
|
{
|
|
// if the cluster already exists, add this user and be done
|
|
ClusterRecord clrec = _clusters.get(targetOid);
|
|
if (clrec != null) {
|
|
clrec.addBody(joiner);
|
|
return;
|
|
}
|
|
|
|
// otherwise see if they sent us the user's oid
|
|
DObject tobj = CrowdServer.omgr.getObject(targetOid);
|
|
if (!(tobj instanceof BodyObject)) {
|
|
Log.info("Can't join cluster, missing target [creator=" + joiner.who() +
|
|
", targetOid=" + targetOid + "].");
|
|
throw new InvocationException(NO_SUCH_CLUSTER);
|
|
}
|
|
|
|
// make sure we're in the same scene as said user
|
|
BodyObject friend = (BodyObject)tobj;
|
|
if (friend.getPlaceOid() != joiner.getPlaceOid()) {
|
|
Log.info("Refusing cluster join from non-proximate user [joiner=" + joiner.who() +
|
|
", jloc=" + joiner.location + ", target=" + friend.who() +
|
|
", tloc=" + friend.location + "].");
|
|
throw new InvocationException(NO_SUCH_CLUSTER);
|
|
}
|
|
|
|
// see if the friend is already in a cluster
|
|
clrec = getCluster(friend.getOid());
|
|
if (clrec != null) {
|
|
clrec.addBody(joiner);
|
|
return;
|
|
}
|
|
|
|
// confirm that they can start a cluster with this unsuspecting other person
|
|
checkCanCluster(joiner, friend);
|
|
|
|
// otherwise we create a new cluster and add our charter members!
|
|
// Log.debug("Creating cluster [starter=" + joiner.who() + ", tgt=" + friend.who() + "].");
|
|
clrec = createClusterRecord();
|
|
clrec.addBody(friend);
|
|
clrec.addBody(joiner);
|
|
}
|
|
|
|
/**
|
|
* Creates the cluster record instance that we'll use to manage our cluster.
|
|
*/
|
|
protected ClusterRecord createClusterRecord ()
|
|
{
|
|
return new ClusterRecord();
|
|
}
|
|
|
|
/**
|
|
* Gives derived classes an opportunity to veto a user's attempt to start a cluster with
|
|
* another user. If the attempt should be vetoed, this method should throw an {@link
|
|
* InvocationException} indicating the reason for veto.
|
|
*/
|
|
protected void checkCanCluster (BodyObject initiator, BodyObject target)
|
|
throws InvocationException
|
|
{
|
|
// nothing to do by default
|
|
}
|
|
|
|
/**
|
|
* Removes the specified user from any cluster they occupy.
|
|
*/
|
|
protected void removeFromCluster (int bodyOid)
|
|
{
|
|
ClusterRecord clrec = getCluster(bodyOid);
|
|
if (clrec != null) {
|
|
clrec.removeBody(bodyOid);
|
|
// If that was the last person, destroy the cluster
|
|
if (clrec.size() == 0) {
|
|
clrec.destroy(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches the cluster record for the specified body.
|
|
*/
|
|
protected ClusterRecord getCluster (int bodyOid)
|
|
{
|
|
ClusteredBodyObject bobj = (ClusteredBodyObject)CrowdServer.omgr.getObject(bodyOid);
|
|
return (bobj == null) ? null : _clusters.get(bobj.getClusterOid());
|
|
}
|
|
|
|
/**
|
|
* Called by the {@link SpotProvider} when we receive a cluster speak request.
|
|
*/
|
|
protected void handleClusterSpeakRequest (int sourceOid, Name source, String bundle,
|
|
String message, byte mode)
|
|
{
|
|
ClusterRecord clrec = getCluster(sourceOid);
|
|
if (clrec == null) {
|
|
Log.warning("Non-clustered user requested cluster speak [where=" + where() +
|
|
", chatter=" + source + " (" + sourceOid + "), msg=" + message + "].");
|
|
} else {
|
|
SpeakProvider.sendSpeak(clrec.getClusterObject(), source, bundle, message, mode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the location of the specified body or null if they have no location in this scene.
|
|
*/
|
|
protected SceneLocation locationForBody (int bodyOid)
|
|
{
|
|
return (SceneLocation)_ssobj.occupantLocs.get(Integer.valueOf(bodyOid));
|
|
}
|
|
|
|
/**
|
|
* Verifies that the specified cluster can be expanded to include another body.
|
|
*/
|
|
protected boolean canAddBody (ClusterRecord clrec, BodyObject body)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when a user is added to a cluster. The scene manager implementation should take this
|
|
* opportunity to rearrange everyone in the cluster appropriately for the new size.
|
|
*/
|
|
protected void bodyAdded (ClusterRecord clrec, BodyObject body)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Called when a user is removed from a cluster. The scene manager implementation should take
|
|
* this opportunity to rearrange everyone in the cluster appropriately for the new size.
|
|
*/
|
|
protected void bodyRemoved (ClusterRecord clrec, BodyObject body)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Used to manage clusters which are groups of users that can chat to one another.
|
|
*/
|
|
protected class ClusterRecord extends HashIntMap
|
|
{
|
|
public ClusterRecord ()
|
|
{
|
|
_clobj = CrowdServer.omgr.registerObject(new ClusterObject());
|
|
_clusters.put(_clobj.getOid(), this);
|
|
|
|
// let any mapped users know about our cluster
|
|
Iterator iter = values().iterator();
|
|
while (iter.hasNext()) {
|
|
ClusteredBodyObject body = (ClusteredBodyObject)iter.next();
|
|
body.setClusterOid(_clobj.getOid());
|
|
_clobj.addToOccupants(((BodyObject)body).getOid());
|
|
}
|
|
|
|
// configure our cluster record and publish it
|
|
_cluster.clusterOid = _clobj.getOid();
|
|
_ssobj.addToClusters(_cluster);
|
|
}
|
|
|
|
public boolean addBody (BodyObject body)
|
|
throws InvocationException
|
|
{
|
|
if (!(body instanceof ClusteredBodyObject)) {
|
|
Log.warning("Refusing to add non-clustered body to cluster [cloid=" +
|
|
_clobj.getOid() + ", size=" + size() + ", who=" + body.who() + "].");
|
|
throw new InvocationException(INTERNAL_ERROR);
|
|
}
|
|
|
|
// if they're already in the cluster, do nothing
|
|
if (containsKey(body.getOid())) {
|
|
return false;
|
|
}
|
|
|
|
// make sure we can add this body
|
|
if (!canAddBody(this, body)) {
|
|
// Log.debug("Cluster full, refusing growth " + this + ".");
|
|
throw new InvocationException(CLUSTER_FULL);
|
|
}
|
|
|
|
// make sure our intrepid joiner is not in any another cluster
|
|
removeFromCluster(body.getOid());
|
|
|
|
put(body.getOid(), body);
|
|
_ssobj.startTransaction();
|
|
try {
|
|
body.startTransaction();
|
|
try {
|
|
bodyAdded(this, body); // do the hokey pokey
|
|
if (_clobj != null) {
|
|
((ClusteredBodyObject)body).setClusterOid(_clobj.getOid());
|
|
_clobj.addToOccupants(body.getOid());
|
|
_ssobj.updateClusters(_cluster);
|
|
}
|
|
|
|
} finally {
|
|
body.commitTransaction();
|
|
}
|
|
} finally {
|
|
_ssobj.commitTransaction();
|
|
}
|
|
|
|
// Log.debug("Added " + body.who() + " to "+ this + ".");
|
|
return true;
|
|
}
|
|
|
|
public void removeBody (int bodyOid)
|
|
{
|
|
BodyObject body = (BodyObject)remove(bodyOid);
|
|
if (body == null) {
|
|
Log.warning("Requested to remove unknown body from cluster [cloid=" +
|
|
_clobj.getOid() + ", size=" + size() + ", who=" + bodyOid + "].");
|
|
return;
|
|
}
|
|
|
|
if (body.isActive()) {
|
|
body.startTransaction();
|
|
}
|
|
try {
|
|
_ssobj.startTransaction();
|
|
try {
|
|
((ClusteredBodyObject)body).setClusterOid(-1);
|
|
bodyRemoved(this, body); // do the hokey pokey
|
|
if (_clobj != null) {
|
|
_clobj.removeFromOccupants(bodyOid);
|
|
_ssobj.updateClusters(_cluster);
|
|
}
|
|
|
|
} finally {
|
|
_ssobj.commitTransaction();
|
|
}
|
|
|
|
} finally {
|
|
if (body.isActive()) {
|
|
body.commitTransaction();
|
|
}
|
|
}
|
|
|
|
// Log.debug("Removed " + bodyOid + " from "+ this + ".");
|
|
|
|
}
|
|
|
|
public ClusterObject getClusterObject ()
|
|
{
|
|
return _clobj;
|
|
}
|
|
|
|
public Cluster getCluster ()
|
|
{
|
|
return _cluster;
|
|
}
|
|
|
|
public String toString ()
|
|
{
|
|
return "[cluster=" + _cluster + ", size=" + size() + "]";
|
|
}
|
|
|
|
protected void destroy (boolean doRemoval)
|
|
{
|
|
// Log.debug("Cluster empty, going away [cloid=" + _clobj.getOid() + "].");
|
|
_ssobj.removeFromClusters(_cluster.getKey());
|
|
|
|
// if we've also been requested to remove ourself from the clusters list, do that
|
|
if (doRemoval) {
|
|
_clusters.remove(_clobj.getOid());
|
|
}
|
|
CrowdServer.omgr.destroyObject(_clobj.getOid());
|
|
}
|
|
|
|
protected ClusterObject _clobj;
|
|
protected Cluster _cluster = new Cluster();
|
|
}
|
|
|
|
/** A casted reference to our place object. */
|
|
protected SpotSceneObject _ssobj;
|
|
|
|
/** A casted reference to our scene instance. */
|
|
protected SpotScene _sscene;
|
|
|
|
/** Records with information on all clusters in this scene. */
|
|
protected HashIntMap<ClusterRecord> _clusters = new HashIntMap<ClusterRecord>();
|
|
|
|
/** A mapping of entering bodies to portal ids. */
|
|
protected HashMap<Integer,Portal> _enterers = new HashMap<Integer,Portal>();
|
|
}
|