diff --git a/src/as/com/threerings/whirled/zone/client/ZoneDirector.as b/src/as/com/threerings/whirled/zone/client/ZoneDirector.as
index 4ff3a951..0aeb72c8 100644
--- a/src/as/com/threerings/whirled/zone/client/ZoneDirector.as
+++ b/src/as/com/threerings/whirled/zone/client/ZoneDirector.as
@@ -7,6 +7,7 @@ import com.threerings.util.ResultListener;
import com.threerings.presents.client.BasicDirector;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.ClientEvent;
+import com.threerings.presents.client.ConfirmAdapter;
import com.threerings.crowd.data.PlaceConfig;
@@ -97,22 +98,31 @@ public class ZoneDirector extends BasicDirector
return false;
}
+ _pendingZoneId = zoneId;
+
+ sendMoveRequest();
+
+ return true;
+ }
+
+ protected function sendMoveRequest () :void
+ {
// let our zone observers know that we're attempting to switch zones
- notifyObservers(zoneId);
+ notifyObservers(_pendingZoneId);
// check the version of our cached copy of the scene to which we're requesting to move; if
// we were unable to load it, assume a cached version of zero
var sceneVers :int = 0;
+ var sceneId :int = -1;
var pendingModel :SceneModel = _scdir.getPendingModel();
if (pendingModel != null) {
sceneVers = pendingModel.version;
}
// issue a moveTo request
- log.info("Issuing zoned moveTo(" + ZoneUtil.toString(zoneId) +
+ log.info("Issuing zoned moveTo(" + ZoneUtil.toString(_pendingZoneId) +
", " + sceneId + ", " + sceneVers + ").");
- _zservice.moveTo(zoneId, sceneId, sceneVers, this);
- return true;
+ _zservice.moveTo(_pendingZoneId, sceneId, sceneVers, this);
}
override protected function fetchServices (client :Client) :void
@@ -141,6 +151,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceeded(placeId, config);
@@ -156,6 +169,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceededWithUpdates(placeId, config, updates);
@@ -170,6 +186,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceededWithScene(placeId, config, model);
@@ -177,6 +196,17 @@ public class ZoneDirector extends BasicDirector
notifyObservers(summary);
}
+ // from interface ZoneService_ZoneMoveListener
+ public function moveRequiresServerSwitch (hostname :String, ports :TypedArray) :void
+ {
+ log.info("Zone switch requires server switch", "host", hostname, "ports", ports);
+ // ship on over to the other server
+ _wCtx.getClient().moveToServer(hostname, ports, new ConfirmAdapter(
+ function () :void { // succeeded
+ sendMoveRequest();
+ }, requestFailed));
+ }
+
// from interface ZoneService.ZoneMoveListener
public function requestFailed (reason :String) :void
{
@@ -215,6 +245,9 @@ public class ZoneDirector extends BasicDirector
return; // if we're currently somewhere, just stay there
}
+ // Not gonna get there, so clear it.
+ _pendingZoneId = -1;
+
// otherwise if we were previously in a zone/scene, try going back there
if (_previousZoneId != -1) {
moveTo(_previousZoneId, previousSceneId);
@@ -263,5 +296,8 @@ public class ZoneDirector extends BasicDirector
/** Our previous zone id. */
protected var _previousZoneId :int = -1;
+
+ /** Where we're headed. */
+ protected var _pendingZoneId :int = -1;
}
}
diff --git a/src/as/com/threerings/whirled/zone/client/ZoneService_ZoneMoveListener.as b/src/as/com/threerings/whirled/zone/client/ZoneService_ZoneMoveListener.as
index c69f692f..695ac6b9 100644
--- a/src/as/com/threerings/whirled/zone/client/ZoneService_ZoneMoveListener.as
+++ b/src/as/com/threerings/whirled/zone/client/ZoneService_ZoneMoveListener.as
@@ -33,6 +33,9 @@ import com.threerings.whirled.zone.data.ZoneSummary;
public interface ZoneService_ZoneMoveListener
extends InvocationService_InvocationListener
{
+ // from Java ZoneService_ZoneMoveListener
+ function moveRequiresServerSwitch (arg1 :String, arg2 :TypedArray /* of int */) :void
+
// from Java ZoneService_ZoneMoveListener
function moveSucceeded (arg1 :int, arg2 :PlaceConfig, arg3 :ZoneSummary) :void
diff --git a/src/as/com/threerings/whirled/zone/data/ZoneMarshaller_ZoneMoveMarshaller.as b/src/as/com/threerings/whirled/zone/data/ZoneMarshaller_ZoneMoveMarshaller.as
index bdff60dc..e3bb8111 100644
--- a/src/as/com/threerings/whirled/zone/data/ZoneMarshaller_ZoneMoveMarshaller.as
+++ b/src/as/com/threerings/whirled/zone/data/ZoneMarshaller_ZoneMoveMarshaller.as
@@ -33,19 +33,27 @@ import com.threerings.whirled.zone.client.ZoneService_ZoneMoveListener;
public class ZoneMarshaller_ZoneMoveMarshaller
extends InvocationMarshaller_ListenerMarshaller
{
+ /** The method id used to dispatch moveRequiresServerSwitch responses. */
+ public static const MOVE_REQUIRES_SERVER_SWITCH :int = 1;
+
/** The method id used to dispatch moveSucceeded responses. */
- public static const MOVE_SUCCEEDED :int = 1;
+ public static const MOVE_SUCCEEDED :int = 2;
/** The method id used to dispatch moveSucceededWithScene responses. */
- public static const MOVE_SUCCEEDED_WITH_SCENE :int = 2;
+ public static const MOVE_SUCCEEDED_WITH_SCENE :int = 3;
/** The method id used to dispatch moveSucceededWithUpdates responses. */
- public static const MOVE_SUCCEEDED_WITH_UPDATES :int = 3;
+ public static const MOVE_SUCCEEDED_WITH_UPDATES :int = 4;
// from InvocationMarshaller_ListenerMarshaller
override public function dispatchResponse (methodId :int, args :Array) :void
{
switch (methodId) {
+ case MOVE_REQUIRES_SERVER_SWITCH:
+ (listener as ZoneService_ZoneMoveListener).moveRequiresServerSwitch(
+ (args[0] as String), (args[1] as TypedArray /* of int */));
+ return;
+
case MOVE_SUCCEEDED:
(listener as ZoneService_ZoneMoveListener).moveSucceeded(
(args[0] as int), (args[1] as PlaceConfig), (args[2] as ZoneSummary));
diff --git a/src/java/com/threerings/whirled/zone/client/ZoneDirector.java b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java
index cf36e2b1..15d8ef97 100644
--- a/src/java/com/threerings/whirled/zone/client/ZoneDirector.java
+++ b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java
@@ -129,22 +129,31 @@ public class ZoneDirector extends BasicDirector
return false;
}
+ _pendingZoneId = zoneId;
+
+ sendMoveRequest();
+ return true;
+ }
+
+ protected void sendMoveRequest ()
+ {
// let our zone observers know that we're attempting to switch zones
- notifyObservers(Integer.valueOf(zoneId));
+ notifyObservers(Integer.valueOf(_pendingZoneId));
// check the version of our cached copy of the scene to which we're requesting to move; if
// we were unable to load it, assume a cached version of zero
int sceneVers = 0;
+ int sceneId = -1;
SceneModel pendingModel = _scdir.getPendingModel();
if (pendingModel != null) {
sceneVers = pendingModel.version;
+ sceneId = pendingModel.sceneId;
}
// issue a moveTo request
- log.info("Issuing zoned moveTo(" + ZoneUtil.toString(zoneId) +
+ log.info("Issuing zoned moveTo(" + ZoneUtil.toString(_pendingZoneId) +
", " + sceneId + ", " + sceneVers + ").");
- _zservice.moveTo(_ctx.getClient(), zoneId, sceneId, sceneVers, this);
- return true;
+ _zservice.moveTo(_ctx.getClient(), _pendingZoneId, + sceneId, sceneVers, this);
}
@Override
@@ -164,6 +173,21 @@ public class ZoneDirector extends BasicDirector
_previousZoneId = -1;
}
+ // from interface SceneService.SceneMoveListener
+ public void moveRequiresServerSwitch (String hostname, int[] ports)
+ {
+ // ship on over to the other server
+ _ctx.getClient().moveToServer(hostname, ports, new ZoneService.ConfirmListener() {
+ public void requestProcessed () {
+ // resend our move request now that we're connected to the new server
+ sendMoveRequest();
+ }
+ public void requestFailed (String reason) {
+ ZoneDirector.this.requestFailed(reason);
+ }
+ });
+ }
+
// from interface ZoneService.ZoneMoveListener
public void moveSucceeded (int placeId, PlaceConfig config, ZoneSummary summary)
{
@@ -175,6 +199,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceeded(placeId, config);
@@ -189,6 +216,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceededWithUpdates(placeId, config, updates);
@@ -203,6 +233,9 @@ public class ZoneDirector extends BasicDirector
// keep track of the summary
_summary = summary;
+ // We're not heading there any more.
+ _pendingZoneId = -1;
+
// pass the rest off to the standard scene transition code
_scdir.moveSucceededWithScene(placeId, config, model);
@@ -247,6 +280,9 @@ public class ZoneDirector extends BasicDirector
return; // if we're currently somewhere, just stay there
}
+ // Not gonna get there, so clear it.
+ _pendingZoneId = -1;
+
// otherwise if we were previously in a zone/scene, try going back there
if (_previousZoneId != -1) {
moveTo(_previousZoneId, previousSceneId);
@@ -295,4 +331,7 @@ public class ZoneDirector extends BasicDirector
/** Our previous zone id. */
protected int _previousZoneId = -1;
+
+ /** Where we're headed. */
+ protected int _pendingZoneId = -1;
}
diff --git a/src/java/com/threerings/whirled/zone/client/ZoneService.java b/src/java/com/threerings/whirled/zone/client/ZoneService.java
index 9a7e85d5..186435b7 100644
--- a/src/java/com/threerings/whirled/zone/client/ZoneService.java
+++ b/src/java/com/threerings/whirled/zone/client/ZoneService.java
@@ -56,6 +56,12 @@ public interface ZoneService extends InvocationService
*/
public void moveSucceededWithScene (
int placeId, PlaceConfig config, ZoneSummary summary, SceneModel model);
+
+ /**
+ * Indicates that the client must switch to the specified server and reissue its move
+ * request in order to relocate to its desired zone.
+ */
+ public void moveRequiresServerSwitch (String hostname, int[] ports);
}
/**
diff --git a/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java b/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java
index 830c9995..738743ac 100644
--- a/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java
+++ b/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java
@@ -49,9 +49,22 @@ public class ZoneMarshaller extends InvocationMarshaller
public static class ZoneMoveMarshaller extends ListenerMarshaller
implements ZoneMoveListener
{
+ /** The method id used to dispatch {@link #moveRequiresServerSwitch}
+ * responses. */
+ public static final int MOVE_REQUIRES_SERVER_SWITCH = 1;
+
+ // from interface ZoneMoveMarshaller
+ public void moveRequiresServerSwitch (String arg1, int[] arg2)
+ {
+ _invId = null;
+ omgr.postEvent(new InvocationResponseEvent(
+ callerOid, requestId, MOVE_REQUIRES_SERVER_SWITCH,
+ new Object[] { arg1, arg2 }, transport));
+ }
+
/** The method id used to dispatch {@link #moveSucceeded}
* responses. */
- public static final int MOVE_SUCCEEDED = 1;
+ public static final int MOVE_SUCCEEDED = 2;
// from interface ZoneMoveMarshaller
public void moveSucceeded (int arg1, PlaceConfig arg2, ZoneSummary arg3)
@@ -64,7 +77,7 @@ public class ZoneMarshaller extends InvocationMarshaller
/** The method id used to dispatch {@link #moveSucceededWithScene}
* responses. */
- public static final int MOVE_SUCCEEDED_WITH_SCENE = 2;
+ public static final int MOVE_SUCCEEDED_WITH_SCENE = 3;
// from interface ZoneMoveMarshaller
public void moveSucceededWithScene (int arg1, PlaceConfig arg2, ZoneSummary arg3, SceneModel arg4)
@@ -77,7 +90,7 @@ public class ZoneMarshaller extends InvocationMarshaller
/** The method id used to dispatch {@link #moveSucceededWithUpdates}
* responses. */
- public static final int MOVE_SUCCEEDED_WITH_UPDATES = 3;
+ public static final int MOVE_SUCCEEDED_WITH_UPDATES = 4;
// from interface ZoneMoveMarshaller
public void moveSucceededWithUpdates (int arg1, PlaceConfig arg2, ZoneSummary arg3, SceneUpdate[] arg4)
@@ -92,6 +105,11 @@ public class ZoneMarshaller extends InvocationMarshaller
public void dispatchResponse (int methodId, Object[] args)
{
switch (methodId) {
+ case MOVE_REQUIRES_SERVER_SWITCH:
+ ((ZoneMoveListener)listener).moveRequiresServerSwitch(
+ (String)args[0], (int[])args[1]);
+ return;
+
case MOVE_SUCCEEDED:
((ZoneMoveListener)listener).moveSucceeded(
((Integer)args[0]).intValue(), (PlaceConfig)args[1], (ZoneSummary)args[2]);
diff --git a/src/java/com/threerings/whirled/zone/peer/data/HostedZone.java b/src/java/com/threerings/whirled/zone/peer/data/HostedZone.java
new file mode 100644
index 00000000..80dd9848
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/peer/data/HostedZone.java
@@ -0,0 +1,38 @@
+package com.threerings.whirled.zone.peer.data;
+
+import com.threerings.io.SimpleStreamableObject;
+
+import com.threerings.util.Name;
+
+import com.threerings.presents.dobj.DSet;
+
+/**
+ * Represents a zone hosted on some node.
+ */
+public class HostedZone extends SimpleStreamableObject
+ implements DSet.Entry
+{
+ /** The unique ID for the zone in question. */
+ public int zoneId;
+
+ /** The name of the zone being hosted. */
+ public Name name;
+
+ public HostedZone ()
+ {
+ }
+
+ public HostedZone (int zoneId, Name name)
+ {
+ this.zoneId = zoneId;
+ this.name = name;
+ }
+
+ // from DSet.Entry
+ public Comparable> getKey ()
+ {
+ return zoneId;
+ }
+
+
+}
diff --git a/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneManager.java b/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneManager.java
new file mode 100644
index 00000000..1d016080
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneManager.java
@@ -0,0 +1,11 @@
+package com.threerings.whirled.zone.peer.server;
+
+import com.threerings.whirled.zone.peer.server.PeeredZoneRegistry.PeerZoneShutdownListener;
+import com.threerings.whirled.zone.server.ZoneManager;
+
+public interface PeeredZoneManager
+ extends ZoneManager
+{
+ /** Registers a shutdown listener with the zone. */
+ public void setShutdownListener (int zoneId, PeerZoneShutdownListener shutdowner);
+}
diff --git a/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneRegistry.java b/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneRegistry.java
new file mode 100644
index 00000000..ec6456dc
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/peer/server/PeeredZoneRegistry.java
@@ -0,0 +1,193 @@
+package com.threerings.whirled.zone.peer.server;
+
+import com.google.inject.Inject;
+import com.samskivert.util.ResultListener;
+import com.samskivert.util.Tuple;
+
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationCodes;
+import com.threerings.presents.server.InvocationException;
+import com.threerings.presents.server.InvocationManager;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.server.LocationManager;
+
+import com.threerings.whirled.server.SceneRegistry;
+import com.threerings.whirled.zone.client.ZoneService;
+import com.threerings.whirled.zone.data.ZoneCodes;
+import com.threerings.whirled.zone.data.ZoneSummary;
+import com.threerings.whirled.zone.peer.data.HostedZone;
+import com.threerings.whirled.zone.server.ZoneManager;
+import com.threerings.whirled.zone.server.ZoneMoveHandler;
+import com.threerings.whirled.zone.server.ZoneRegistry;
+import com.threerings.whirled.zone.server.ZoneManager.ResolutionListener;
+
+import static com.threerings.whirled.zone.Log.log;
+
+public abstract class PeeredZoneRegistry extends ZoneRegistry
+{
+ /**
+ * Peer-awareness for finding out about zones.
+ */
+ public static interface PeerZoneResolutionListener extends ResolutionListener
+ {
+ /**
+ * Called when the zone is already hosted on another node.
+ */
+ public void zoneOnNode (Tuple nodeInfo);
+ }
+
+ /**
+ * Peered zones should be sure to call back to one of these when they're going away.
+ */
+ public static interface PeerZoneShutdownListener
+ {
+ public void zoneDidShutdown (int zoneId);
+ }
+
+ /**
+ * A ZoneMoveHandler that can receive the sceneOnNode() callback.
+ */
+ public abstract static class PeerZoneMoveHandler extends ZoneMoveHandler
+ implements PeerZoneResolutionListener
+ {
+ public PeerZoneMoveHandler (
+ LocationManager locman, ZoneManager zmgr, SceneRegistry scReg, BodyObject body,
+ int sceneId, int sceneVer, ZoneService.ZoneMoveListener listener)
+ {
+ super(locman, zmgr, scReg, body, sceneId, sceneVer, listener);
+ }
+ }
+
+ protected abstract PeerZoneMoveHandler createMoveHandler (LocationManager locman,
+ ZoneManager zmgr, SceneRegistry scReg, BodyObject body, int sceneId, int sceneVer,
+ ZoneService.ZoneMoveListener listener);
+
+ @Inject public PeeredZoneRegistry (InvocationManager invmgr, ZonePeerManager peerMgr)
+ {
+ super(invmgr);
+ _peerMgr = peerMgr;
+ }
+
+ public void registerZoneManager (byte zoneType, ZoneManager manager)
+ {
+ if (!(manager instanceof PeeredZoneManager)) {
+ throw new IllegalArgumentException("All ZoneManagers received by PeeredZoneRegistry " +
+ "must be PeeredZoneManagers");
+ }
+ super.registerZoneManager(zoneType, manager);
+ }
+
+ @Override
+ public PeeredZoneManager getZoneManager (int qualifiedZoneId)
+ {
+ return (PeeredZoneManager)super.getZoneManager(qualifiedZoneId);
+ }
+
+ @Override
+ public void moveTo (ClientObject caller, int zoneId, int sceneId,
+ int sceneVer, ZoneService.ZoneMoveListener listener)
+ throws InvocationException
+ {
+ if (!(caller instanceof BodyObject)) {
+ log.warning("Request to switch zones by non-BodyObject " +
+ "[clobj=" + caller.getClass() + "].");
+ throw new InvocationException(ZoneCodes.INTERNAL_ERROR);
+ }
+ BodyObject body = (BodyObject)caller;
+
+ PeerZoneMoveHandler handler = createMoveHandler(_locman, getZoneManager(zoneId),
+ _screg, body, sceneId, sceneVer, listener);
+
+ resolvePeerZone(zoneId, handler);
+ }
+
+ public void resolveZone (int zoneId, ResolutionListener listener)
+ {
+ PeeredZoneManager zmgr = getZoneManager(zoneId);
+ if (zmgr == null) {
+ log.warning("Trying to resolve a zone for which we have no manager", "zoneId", zoneId);
+ listener.zoneFailedToResolve(zoneId, new Exception(InvocationCodes.INTERNAL_ERROR));
+ } else {
+ zmgr.resolveZone(zoneId, listener);
+ }
+ }
+
+ /**
+ * Resolve a zone, or return the information on the peer on which it's hosted.
+ */
+ public void resolvePeerZone (final int zoneId, final PeerZoneResolutionListener listener)
+ {
+ // check to see if the destination zone is already hosted on a server
+ Tuple nodeInfo = _peerMgr.getZoneHost(zoneId);
+
+ // if it's already hosted...
+ if (nodeInfo != null) {
+ // it's hosted on this server! It should already be resolved...
+ if (_peerMgr.getNodeObject().nodeName.equals(nodeInfo.left)) {
+ resolveZone(zoneId, listener);
+ } else {
+ listener.zoneOnNode(nodeInfo); // somewhere else, pass the buck
+ }
+ return;
+ }
+
+ // otherwise the zone is not resolved here nor there; so we claim the zone by acquiring a
+ // distributed lock and then resolve it locally
+ _peerMgr.acquireLock(ZonePeerManager.getZoneLock(zoneId), new ResultListener() {
+ public void requestCompleted (String nodeName) {
+ if (_peerMgr.getNodeObject().nodeName.equals(nodeName)) {
+ log.debug("Got lock, resolving zone", "zoneId", zoneId);
+ resolveZone(zoneId, new ResolutionListener() {
+ public void zoneWasResolved (ZoneSummary zonesum) {
+ releaseLock();
+ _peerMgr.zoneDidStartup(zonesum.zoneId, zonesum.name);
+ PeerZoneShutdownListener shutdowner = new PeerZoneShutdownListener() {
+ public void zoneDidShutdown (int zoneId) {
+ _peerMgr.zoneDidShutdown(zoneId);
+ }
+ };
+ getZoneManager(zonesum.zoneId).setShutdownListener(
+ zonesum.zoneId, shutdowner);
+ listener.zoneWasResolved(zonesum);
+ }
+ public void zoneFailedToResolve (int zoneId, Exception reason) {
+ releaseLock();
+ listener.zoneFailedToResolve(zoneId, reason);
+ }
+ protected void releaseLock () {
+ _peerMgr.releaseLock(ZonePeerManager.getZoneLock(zoneId),
+ new ResultListener.NOOP());
+ }
+ });
+
+ } else {
+ // we didn't get the lock, so let's see what happened by re-checking
+ Tuple nodeInfo = _peerMgr.getZoneHost(zoneId);
+ if (nodeName == null || nodeInfo == null || !nodeName.equals(nodeInfo.left)) {
+ log.warning("Zone resolved on wacked-out node?",
+ "zoneId", zoneId, "nodeName", nodeName, "nodeInfo", nodeInfo);
+ listener.zoneFailedToResolve(zoneId,
+ new Exception("Zone on bogus host node"));
+ } else {
+ listener.zoneOnNode(nodeInfo); // somewhere else
+ }
+ }
+ }
+ public void requestFailed (Exception cause) {
+ log.warning("Failed to acquire zone resolution lock", "id", zoneId, cause);
+ listener.zoneFailedToResolve(zoneId, cause);
+ }
+ });
+ }
+
+ /**
+ * Pass on that the zone has shutdown.
+ */
+ public void zoneDidShutdown (int zoneId)
+ {
+ _peerMgr.zoneDidShutdown(zoneId);
+ }
+
+ protected ZonePeerManager _peerMgr;
+}
diff --git a/src/java/com/threerings/whirled/zone/peer/server/ZoneNodeObject.java b/src/java/com/threerings/whirled/zone/peer/server/ZoneNodeObject.java
new file mode 100644
index 00000000..2eefa153
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/peer/server/ZoneNodeObject.java
@@ -0,0 +1,73 @@
+package com.threerings.whirled.zone.peer.server;
+
+import javax.annotation.Generated;
+import com.threerings.presents.dobj.DSet;
+import com.threerings.crowd.peer.data.CrowdNodeObject;
+
+import com.threerings.whirled.zone.peer.data.HostedZone;
+
+public class ZoneNodeObject extends CrowdNodeObject
+{
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the hostedZones field. */
+ @Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
+ public static final String HOSTED_ZONES = "hostedZones";
+ // AUTO-GENERATED: FIELDS END
+
+ /** Contains info on all zones hosted by this server. */
+ public DSet hostedZones = DSet.newDSet();
+
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the specified entry be added to the
+ * hostedZones set. The set will not change until the event is
+ * actually propagated through the system.
+ */
+ @Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
+ public void addToHostedZones (HostedZone elem)
+ {
+ requestEntryAdd(HOSTED_ZONES, hostedZones, elem);
+ }
+
+ /**
+ * Requests that the entry matching the supplied key be removed from
+ * the hostedZones set. The set will not change until the
+ * event is actually propagated through the system.
+ */
+ @Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
+ public void removeFromHostedZones (Comparable> key)
+ {
+ requestEntryRemove(HOSTED_ZONES, hostedZones, key);
+ }
+
+ /**
+ * Requests that the specified entry be updated in the
+ * hostedZones set. The set will not change until the event is
+ * actually propagated through the system.
+ */
+ @Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
+ public void updateHostedZones (HostedZone elem)
+ {
+ requestEntryUpdate(HOSTED_ZONES, hostedZones, elem);
+ }
+
+ /**
+ * Requests that the hostedZones field be set to the
+ * specified value. Generally one only adds, updates and removes
+ * entries of a distributed set, but certain situations call for a
+ * complete replacement of the set value. The local value will be
+ * updated immediately and an event will be propagated through the
+ * system to notify all listeners that the attribute did
+ * change. Proxied copies of this object (on clients) will apply the
+ * value change when they received the attribute changed notification.
+ */
+ @Generated(value={"com.threerings.presents.tools.GenDObjectTask"})
+ public void setHostedZones (DSet value)
+ {
+ requestAttributeChange(HOSTED_ZONES, value, this.hostedZones);
+ DSet clone = (value == null) ? null : value.clone();
+ this.hostedZones = clone;
+ }
+ // AUTO-GENERATED: METHODS END
+}
diff --git a/src/java/com/threerings/whirled/zone/peer/server/ZonePeerManager.java b/src/java/com/threerings/whirled/zone/peer/server/ZonePeerManager.java
new file mode 100644
index 00000000..05df2051
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/peer/server/ZonePeerManager.java
@@ -0,0 +1,80 @@
+package com.threerings.whirled.zone.peer.server;
+
+import com.google.common.base.Function;
+import com.google.inject.Inject;
+import com.samskivert.util.Lifecycle;
+import com.samskivert.util.Tuple;
+
+import com.threerings.util.Name;
+
+import com.threerings.presents.peer.data.NodeObject;
+import com.threerings.crowd.peer.server.CrowdPeerManager;
+
+import com.threerings.whirled.zone.peer.data.HostedZone;
+
+import static com.threerings.whirled.zone.Log.log;
+
+public abstract class ZonePeerManager extends CrowdPeerManager
+{
+ /**
+ * Creates an uninitialized peer manager.
+ */
+ @Inject public ZonePeerManager (Lifecycle cycle)
+ {
+ super(cycle);
+ }
+
+ @Override // from CrowdPeerManager
+ protected NodeObject createNodeObject ()
+ {
+ return new ZoneNodeObject();
+ }
+
+ /** Returns a lock used to claim resolution of the specified scene. */
+ public static NodeObject.Lock getZoneLock (int sceneId)
+ {
+ return new NodeObject.Lock("ZoneHost", sceneId);
+ }
+
+ /**
+ * Returns the node name of the peer that is hosting the specified scene, or null if no peer
+ * has published that they are hosting the scene.
+ */
+ public Tuple getZoneHost (final int zoneId)
+ {
+ return lookupNodeDatum(new NodeFunc>() {
+ public Tuple apply (ZoneNodeObject nodeobj) {
+ HostedZone info = nodeobj.hostedZones.get(zoneId);
+ return (info == null) ? null : Tuple.newTuple(nodeobj.nodeName, info);
+ }
+ });
+ }
+
+ /**
+ * Called by the ZoneManager when it is hosting a zone.
+ */
+ public void zoneDidStartup (int zoneId, Name name)
+ {
+ log.debug("Hosting zone", "id", zoneId, "name", name);
+ ((ZoneNodeObject)_nodeobj).addToHostedZones(new HostedZone(zoneId, name));
+ }
+
+ /**
+ * Called by the ZoneManager when it is no longer hosting a scene.
+ */
+ public void zoneDidShutdown (int zoneId)
+ {
+ log.debug("No longer hosting zone", "id", zoneId);
+ ((ZoneNodeObject)_nodeobj).removeFromHostedZones(zoneId);
+ }
+
+ /** Useful with {@link #lookupNodeDatum}. */
+ public static abstract class NodeFunc implements Function
+ {
+ public abstract T apply (ZoneNodeObject mnobj);
+
+ public T apply (NodeObject nodeobj) {
+ return apply((ZoneNodeObject)nodeobj);
+ }
+ }
+}