com.threerings.whirled.spot.server.SpotSceneManager Maven / Gradle / Ivy
The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.google.common.collect.Maps;
import com.samskivert.util.HashIntMap;
import com.threerings.util.Name;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.chat.server.SpeakUtil;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.OccupantInfo;
import com.threerings.crowd.server.PlaceRegistry;
import com.threerings.whirled.server.SceneManager;
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;
import static com.threerings.whirled.spot.Log.log;
/**
* 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 (PlaceRegistry plreg, BodyObject body)
{
SpotSceneManager mgr = (SpotSceneManager)plreg.getPlaceManager(body.getPlaceOid());
if (mgr != null) {
mgr.moveToDefaultPortal(body);
}
}
/**
* 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 null
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());
}
@Override
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();
}
@Override
protected void gotSceneData (Object extras)
{
super.gotSceneData(extras);
// keep a casted reference around to our scene
_sscene = (SpotScene)_scene;
}
@Override
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 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();
}
}
}
}
@Override
protected void addOccupantInfo (BodyObject body, OccupantInfo info)
{
// assign this body a starting location in the scene
assignStartingLocation(body);
// then call super, so we know that our info will be ready before bodyAdded is triggered
super.addOccupantInfo(body, info);
}
/**
* 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());
}
/**
* Move the specified body to the default portal, if possible.
*/
protected void moveToDefaultPortal (BodyObject body)
{
SpotScene scene = (SpotScene)getScene();
if (scene == null) {
log.warning("No scene in moveBodyToDefaultPortal()?",
"who", body.who(), "where", where());
return;
}
try {
Location eloc = scene.getDefaultEntrance().getLocation();
handleChangeLoc(body, eloc);
} catch (InvocationException ie) {
log.warning("Could not move user to default portal",
"where", where(), "who", body.who(), "error", ie);
}
}
/**
* 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 true
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, new Exception());
_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 = _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)
{
BodyObject bobj = (BodyObject)_omgr.getObject(bodyOid);
if (bobj instanceof ClusteredBodyObject) {
return _clusters.get(((ClusteredBodyObject)bobj).getClusterOid());
} else {
return null;
}
}
/**
* Called by the {@link SpotProvider} when we receive a cluster speak request.
*/
@Deprecated
protected void handleClusterSpeakRequest (
int sourceOid, Name source, String bundle, String message, byte mode)
{
handleClusterMessageRequest(sourceOid, new UserMessage(source, bundle, message, mode));
}
/**
* Called by the {@link SpotProvider} when we receive a cluster message request.
*/
protected void handleClusterMessageRequest (int sourceOid, UserMessage message)
{
ClusterRecord clrec = getCluster(sourceOid);
if (clrec == null) {
log.warning("Non-clustered user requested cluster speak",
"where", where(), "chatter", (message.speaker + " (" + sourceOid + ")"),
"msg", message);
} else {
SpeakUtil.sendMessage(clrec.getClusterObject(), message);
}
}
/**
* Returns the location of the specified body or null if they have no location in this scene.
*/
protected SceneLocation locationForBody (int bodyOid)
{
return _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 = _omgr.registerObject(new ClusterObject());
_clusters.put(_clobj.getOid(), this);
// let any mapped users know about our cluster
for (ClusteredBodyObject body : values()) {
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(), (ClusteredBodyObject)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;
}
@Override
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());
}
_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 _clusters = new HashIntMap();
/** A mapping of entering bodies to portal ids. */
protected HashMap _enterers = Maps.newHashMap();
}