Behold Vilya, Ring of Air and repository for our game and virtual worldly

extensions to the distributed environment provided by Narya.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@1 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
Michael Bayne
2006-06-23 17:58:11 +00:00
commit a4df87e52f
317 changed files with 45818 additions and 0 deletions
@@ -0,0 +1,34 @@
//
// $Id: SpotClient.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// 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 com.threerings.whirled.server.WhirledClient;
/**
* Extends the Whirled client and handles the necessary notifications that
* take place when a user logs off to let the clients displaying the scene
* they were occupying know that they didn't exit via a portal, but
* instead just disappeared.
*/
public class SpotClient extends WhirledClient
{
}
@@ -0,0 +1,95 @@
//
// $Id: SpotDispatcher.java 4145 2006-05-24 01:24:24Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// 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 com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
import com.threerings.whirled.client.SceneService;
import com.threerings.whirled.data.SceneMarshaller;
import com.threerings.whirled.spot.client.SpotService;
import com.threerings.whirled.spot.data.Location;
import com.threerings.whirled.spot.data.SpotMarshaller;
/**
* Dispatches requests to the {@link SpotProvider}.
*/
public class SpotDispatcher extends InvocationDispatcher
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public SpotDispatcher (SpotProvider provider)
{
this.provider = provider;
}
// documentation inherited
public InvocationMarshaller createMarshaller ()
{
return new SpotMarshaller();
}
// documentation inherited
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case SpotMarshaller.CHANGE_LOCATION:
((SpotProvider)provider).changeLocation(
source,
((Integer)args[0]).intValue(), (Location)args[1], (InvocationService.ConfirmListener)args[2]
);
return;
case SpotMarshaller.CLUSTER_SPEAK:
((SpotProvider)provider).clusterSpeak(
source,
(String)args[0], ((Byte)args[1]).byteValue()
);
return;
case SpotMarshaller.JOIN_CLUSTER:
((SpotProvider)provider).joinCluster(
source,
((Integer)args[0]).intValue(), (InvocationService.ConfirmListener)args[1]
);
return;
case SpotMarshaller.TRAVERSE_PORTAL:
((SpotProvider)provider).traversePortal(
source,
((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), ((Integer)args[2]).intValue(), (SceneService.SceneMoveListener)args[3]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,324 @@
//
// $Id: SpotProvider.java 3832 2006-02-04 03:49:53Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// 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 com.samskivert.util.StringUtil;
import com.threerings.util.MessageManager;
import com.threerings.util.Name;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.RootDObjectManager;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationProvider;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.server.SpeakProvider;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.server.PlaceRegistry;
import com.threerings.whirled.client.SceneService.SceneMoveListener;
import com.threerings.whirled.data.ScenedBodyObject;
import com.threerings.whirled.server.SceneManager;
import com.threerings.whirled.server.SceneRegistry;
import com.threerings.whirled.spot.Log;
import com.threerings.whirled.spot.client.SpotService;
import com.threerings.whirled.spot.data.Location;
import com.threerings.whirled.spot.data.Portal;
import com.threerings.whirled.spot.data.SpotCodes;
import com.threerings.whirled.spot.data.SpotScene;
/**
* Provides the server-side implementation of the spot services.
*/
public class SpotProvider
implements SpotCodes, InvocationProvider
{
/**
* Creates a spot provider that can be registered with the invocation
* manager to handle spot services.
*/
public SpotProvider (RootDObjectManager omgr, PlaceRegistry plreg,
SceneRegistry screg)
{
// we'll need these later
_plreg = plreg;
_screg = screg;
_omgr = omgr;
}
/**
* Processes a {@link SpotService#traversePortal} request.
*/
public void traversePortal (ClientObject caller, int sceneId, int portalId,
int destSceneVer, SceneMoveListener listener)
throws InvocationException
{
// le sanity check
int cSceneId = getCallerSceneId(caller);
if (cSceneId != sceneId) {
Log.info("Ignoring stale traverse portal request " +
"[caller=" + caller.who() + ", oSceneId=" + sceneId +
", portalId=" + portalId + ", cSceneId=" + cSceneId + "].");
InvocationMarshaller.setNoResponse(listener);
return;
}
// avoid cluttering up the method declaration with final keywords
final BodyObject fsource = (BodyObject)caller;
final int fportalId = portalId;
final int fsceneVer = destSceneVer;
final SceneMoveListener flistener = listener;
// obtain the source scene
final SpotSceneManager srcmgr = (SpotSceneManager)
_screg.getSceneManager(sceneId);
if (srcmgr == null) {
Log.warning("Traverse portal missing source scene " +
"[user=" + fsource.who() + ", sceneId=" + sceneId +
", portalId=" + portalId + "].");
throw new InvocationException(INTERNAL_ERROR);
}
// obtain the destination scene and location id
SpotScene rss = (SpotScene)srcmgr.getScene();
final Portal fdest = rss.getPortal(portalId);
// give the source scene manager a chance to do access control
String errmsg = srcmgr.mayTraversePortal(fsource, fdest);
if (errmsg != null) {
throw new InvocationException(errmsg);
}
// make sure this portal has valid info
if (fdest == null || !fdest.isValid()) {
Log.warning("Traverse portal with invalid portal " +
"[user=" + fsource.who() + ", scene=" + srcmgr.where() +
", pid=" + portalId + ", portal=" + fdest +
", portals=" + StringUtil.toString(rss.getPortals()) +
"].");
throw new InvocationException(NO_SUCH_PORTAL);
}
// resolve their destination scene
SceneRegistry.ResolutionListener rl =
new SceneRegistry.ResolutionListener() {
public void sceneWasResolved (SceneManager scmgr) {
// make sure our caller is still around; under heavy
// load, clients might end their session while the
// scene is resolving
if (!fsource.isActive()) {
Log.info("Abandoning portal traversal, client gone " +
"[who=" + fsource.who() +
", dest=" + scmgr.where() + "].");
InvocationMarshaller.setNoResponse(flistener);
return;
}
// let the source manager know that this guy is
// departing via the specified portal
srcmgr.willTraversePortal(fsource, fdest);
SpotSceneManager sscmgr = (SpotSceneManager)scmgr;
finishTraversePortalRequest(
fsource, sscmgr, fsceneVer, fdest, flistener);
}
public void sceneFailedToResolve (
int rsceneId, Exception reason) {
Log.warning("Unable to resolve target scene " +
"[sceneId=" + rsceneId +
", reason=" + reason + "].");
// pretend like the scene doesn't exist to the client
flistener.requestFailed(NO_SUCH_PLACE);
}
};
_screg.resolveScene(fdest.targetSceneId, rl);
}
/**
* This is called after the scene to which we are moving is guaranteed
* to have been loaded into the server.
*/
protected void finishTraversePortalRequest (
BodyObject source, SpotSceneManager destmgr, int sceneVer,
Portal dest, SceneMoveListener listener)
{
// let the destination scene manager know that we're coming in
destmgr.mapEnteringBody(source, dest.targetPortalId);
try {
// move to the place object associated with this scene
_screg.sceneprov.effectSceneMove(
source, destmgr, sceneVer, listener);
} catch (InvocationException sfe) {
listener.requestFailed(sfe.getMessage());
// and let the destination scene manager know that we're no
// longer coming in
destmgr.clearEnteringBody(source);
}
}
/**
* Processes a {@link SpotService#changeLocation} request.
*/
public void changeLocation (ClientObject caller, int sceneId, Location loc,
SpotService.ConfirmListener listener)
throws InvocationException
{
BodyObject source = (BodyObject)caller;
int cSceneId = getCallerSceneId(caller);
if (cSceneId != sceneId) {
Log.info("Rejecting changeLocation for invalid scene " +
"[user=" + source.who() + ", insid=" + cSceneId +
", wantsid=" + sceneId + ", loc=" + loc + "].");
throw new InvocationException(INVALID_LOCATION);
}
// look up the scene manager for the specified scene
SpotSceneManager smgr = (SpotSceneManager)
_screg.getSceneManager(sceneId);
if (smgr == null) {
Log.warning("User requested to change location from " +
"non-existent scene [user=" + source.who() +
", sceneId=" + sceneId + ", loc=" + loc +"].");
throw new InvocationException(INTERNAL_ERROR);
}
// pass the buck to yon scene manager
smgr.handleChangeLoc(source, loc);
// if that method finished, we're good to go
listener.requestProcessed();
}
/**
* Processes a {@link SpotService#joinCluster} request.
*/
public void joinCluster (ClientObject caller, int friendOid,
SpotService.ConfirmListener listener)
throws InvocationException
{
int sceneId = getCallerSceneId(caller);
BodyObject source = (BodyObject)caller;
// look up the scene manager for the specified scene
SpotSceneManager smgr = (SpotSceneManager)
_screg.getSceneManager(sceneId);
if (smgr == null) {
Log.warning("User requested to join cluster from " +
"non-existent scene [user=" + source.who() +
", sceneId=" + sceneId + ", foid=" + friendOid +"].");
throw new InvocationException(INTERNAL_ERROR);
}
// pass the buck to yon scene manager
smgr.handleJoinCluster(source, friendOid);
// if that method finished, we're good to go
listener.requestProcessed();
}
/**
* Handles request to generate a speak message in the specified cluster.
*/
public void clusterSpeak (ClientObject caller, String message, byte mode)
throws InvocationException
{
// ensure the caller has normal chat access
BodyObject source = (BodyObject)caller;
String errmsg = source.checkAccess(ChatCodes.CHAT_ACCESS, null);
if (errmsg != null) {
SpeakProvider.sendFeedback(source,
MessageManager.GLOBAL_BUNDLE, errmsg);
return;
}
sendClusterChatMessage(getCallerSceneId(caller), source.getOid(),
source.getVisibleName(), null, message, mode);
}
/**
* Sends a cluster chat notification to the specified location in the
* specified place object originating with the specified speaker (the
* speaker can be a server entity that wishes to fake a "speak"
* message, in which case the bundle argument should be non-null and
* should contain the id of the bundle to be used to translate the
* message text) and with the supplied message content.
*
* @param sceneId the scene id in which to deliver the chat message.
* @param speakerOid the body object id of the speaker (used to verify
* that they are in the cluster in question).
* @param speaker the username of the user that generated the message
* (or some special speaker name for server messages).
* @param bundle the bundle identifier that will be used by the client
* to translate the message text (or null if the message originated
* from a real live human who wrote it in their native tongue).
* @param message the text of the chat message.
*/
public void sendClusterChatMessage (
int sceneId, int speakerOid, Name speaker,
String bundle, String message, byte mode)
{
// look up the scene manager for the specified scene
SpotSceneManager smgr = (SpotSceneManager)
_screg.getSceneManager(sceneId);
if (smgr == null) {
Log.warning("User requested cluster chat in non-existent scene " +
"[user=" + speaker + ", sceneId=" + sceneId +
", message=" + message + "].");
return;
}
// pass this request on to the spot scene manager
smgr.handleClusterSpeakRequest(
speakerOid, speaker, bundle, message, mode);
}
/**
* Obtains the scene id occupied by the supplied caller.
*
* @exception InvocationException thrown if the caller does not
* implement {@link ScenedBodyObject}.
*/
protected static int getCallerSceneId (ClientObject caller)
throws InvocationException
{
if (caller instanceof ScenedBodyObject) {
return ((ScenedBodyObject)caller).getSceneId();
} else {
Log.warning("Can't get scene from non-scened caller " +
caller.who() + ".");
throw new InvocationException(INTERNAL_ERROR);
}
}
/** The place registry with which we interoperate. */
protected PlaceRegistry _plreg;
/** The scene registry with which we interoperate. */
protected SceneRegistry _screg;
/** The object manager we use to do dobject stuff. */
protected RootDObjectManager _omgr;
}
@@ -0,0 +1,626 @@
//
// $Id: SpotSceneManager.java 4145 2006-05-24 01:24:24Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// 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.Iterator;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.IntIntMap;
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.location);
if (mgr != null) {
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, int portalId)
{
_enterers.put(body.getOid(), portalId);
}
/**
* 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 cliter = _clusters.values().iterator();
while (cliter.hasNext()) {
ClusterRecord clrec = (ClusterRecord)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
int portalId = _enterers.remove(body.getOid());
Portal entry;
if (portalId != -1) {
entry = _sscene.getPortal(portalId);
if (entry == null) {
Log.warning("Body mapped at invalid portal [where=" + where() +
", who=" + body.who() +
", portalId=" + portalId + "].");
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, 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.
*/
protected SceneLocation computeEnteringLocation (
BodyObject body, 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 = (ClusterRecord)_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.location != joiner.location) {
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() +
// ", target=" + 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 :
(ClusterRecord)_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
implements Subscriber
{
public ClusterRecord ()
{
CrowdServer.omgr.createObject(ClusterObject.class, this);
}
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 void objectAvailable (DObject object)
{
// keep this feller around
_clobj = (ClusterObject)object;
_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);
// if we didn't manage to add our creating user when we first
// started up, there's no point in our sticking around
if (size() == 0) {
destroy(true);
}
}
public void requestFailed (int oid, ObjectAccessException cause)
{
Log.warning("Aiya! Failed to create cluster object " +
"[cause=" + cause + ", penders=" + size() + "].");
// let any mapped users know that we're hosed
Iterator iter = values().iterator();
while (iter.hasNext()) {
ClusteredBodyObject body = (ClusteredBodyObject)iter.next();
body.setClusterOid(-1);
}
}
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 _clusters = new HashIntMap();
/** A mapping of entering bodies to portal ids. */
protected IntIntMap _enterers = new IntIntMap();
}