diff --git a/src/java/com/threerings/whirled/zone/client/ZoneCodes.java b/src/java/com/threerings/whirled/zone/client/ZoneCodes.java
new file mode 100644
index 000000000..718551118
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/client/ZoneCodes.java
@@ -0,0 +1,20 @@
+//
+// $Id: ZoneCodes.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.client;
+
+import com.threerings.whirled.client.SceneCodes;
+
+/**
+ * Contains codes used by the zone services.
+ */
+public interface ZoneCodes extends SceneCodes
+{
+ /** The module name for the zone services. */
+ public static final String MODULE_NAME = "whirled!zone";
+
+ /** An error code indicating that a zone identified by a particular
+ * zone id does not exist. Usually generated by a failed moveTo
+ * request. */
+ public static final String NO_SUCH_ZONE = "m.no_such_zone";
+}
diff --git a/src/java/com/threerings/whirled/zone/client/ZoneDirector.java b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java
new file mode 100644
index 000000000..bd2080112
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java
@@ -0,0 +1,114 @@
+//
+// $Id: ZoneDirector.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.client;
+
+import com.threerings.crowd.data.PlaceConfig;
+
+import com.threerings.whirled.client.DisplaySceneFactory;
+import com.threerings.whirled.client.SceneDirector;
+import com.threerings.whirled.client.persist.SceneRepository;
+
+import com.threerings.whirled.data.SceneModel;
+import com.threerings.whirled.util.WhirledContext;
+
+import com.threerings.whirled.zone.data.ZoneSummary;
+
+/**
+ * The zone director extends the scene director with the notion of zones.
+ * Zones are self-contained, connected groups of scenes. The normal scene
+ * director services can be used to move from scene to scene, but moving
+ * to a new zone requires a special move request which can be accomplished
+ * via the zone director. The zone director also makes available the zone
+ * summary which provides information on the zone which can be used to
+ * generate an overview map or similar.
+ */
+public class ZoneDirector extends SceneDirector
+{
+ /**
+ * Constructs a zone director with the supplied context, repository
+ * and scene factory. A zone director is required on the client side
+ * for systems that wish to use the zone services.
+ */
+ public ZoneDirector (WhirledContext ctx, SceneRepository screp,
+ DisplaySceneFactory dsfact)
+ {
+ super(ctx, screp, dsfact);
+ }
+
+ /**
+ * Returns the summary for the zone currently occupied by the client
+ * or null if the client does not currently occupy a zone (not a
+ * normal situation).
+ */
+ public ZoneSummary getZoneSummary ()
+ {
+ return _summary;
+ }
+
+ /**
+ * Requests that this client move the specified scene in the specified
+ * zone. A request will be made and when the response is received, the
+ * location observers will be notified of success or failure.
+ */
+ public void moveTo (int zoneId, int sceneId)
+ {
+ // if the requested zone is the same as our current zone, we just
+ // want a regular old moveTo request
+ if (_summary != null && zoneId == _summary.zoneId) {
+ moveTo(sceneId);
+
+ } else { // otherwise, we make a zoned moveTo request
+ // prepare to move to this scene (sets up pending data)
+ if (!prepareMoveTo(sceneId)) {
+ return;
+ }
+
+ // 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;
+ if (_pendingModel != null) {
+ sceneVers = _pendingModel.version;
+ }
+
+ // issue a moveTo request
+ ZoneService.moveTo(_ctx.getClient(), zoneId,
+ sceneId, sceneVers, this);
+ }
+ }
+
+ /**
+ * Called in response to a successful zoned moveTo
+ * request.
+ */
+ public void handleMoveSucceeded (
+ int invid, int placeId, PlaceConfig config, ZoneSummary summary)
+ {
+ // keep track of the summary
+ _summary = summary;
+
+ // and pass the rest off to the standard scene transition code
+ handleMoveSucceeded(invid, placeId, config);
+ }
+
+ /**
+ * Called in response to a successful zoned moveTo
+ * request when our cached scene was out of date and the server
+ * determined that we needed an updated copy.
+ */
+ public void handleMoveSucceededPlusUpdate (
+ int invid, int placeId, PlaceConfig config, ZoneSummary summary,
+ SceneModel model)
+ {
+ // keep track of the summary
+ _summary = summary;
+
+ // and pass the rest off to the standard scene transition code
+ handleMoveSucceededPlusUpdate(invid, placeId, config, model);
+ }
+
+ /** A reference to the zone summary for the currently occupied
+ * zone. */
+ protected ZoneSummary _summary;
+}
diff --git a/src/java/com/threerings/whirled/zone/client/ZoneService.java b/src/java/com/threerings/whirled/zone/client/ZoneService.java
new file mode 100644
index 000000000..13d376f82
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/client/ZoneService.java
@@ -0,0 +1,36 @@
+//
+// $Id: ZoneService.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.client;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationDirector;
+
+import com.threerings.whirled.Log;
+
+/**
+ * The zone service class provides the client interface to the zone
+ * related invocation services (e.g. moving between zones).
+ */
+public class ZoneService implements ZoneCodes
+{
+ /**
+ * Requests that that this client's body be moved to the specified
+ * scene in the specified zone.
+ *
+ * @param zoneId the zone id to which we want to move.
+ * @param sceneId the scene id to which we want to move.
+ * @param sceneVers the version number of the scene object that we
+ * have in our local repository.
+ */
+ public static void moveTo (Client client, int zoneId, int sceneId,
+ int sceneVers, ZoneDirector rsptarget)
+ {
+ InvocationDirector invdir = client.getInvocationDirector();
+ Object[] args = new Object[] {
+ new Integer(zoneId), new Integer(sceneId), new Integer(sceneVers) };
+ invdir.invoke(MODULE_NAME, MOVE_TO_REQUEST, args, rsptarget);
+ Log.info("Sent moveTo request [zone=" + zoneId +
+ ", scene=" + sceneId + ", version=" + sceneVers + "].");
+ }
+}
diff --git a/src/java/com/threerings/whirled/zone/data/SceneSummary.java b/src/java/com/threerings/whirled/zone/data/SceneSummary.java
new file mode 100644
index 000000000..2b0cbcebe
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/data/SceneSummary.java
@@ -0,0 +1,71 @@
+//
+// $Id: SceneSummary.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.data;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import com.threerings.presents.io.Streamable;
+import com.threerings.presents.io.StreamableUtil;
+
+/**
+ * The scene summary class is used to provide info about the connected
+ * group of scenes that make up an island. The group of scenes that make
+ * up an island is a self-contained set of scenes, connected with one
+ * another (by portals) but not to any scenes outside the group.
+ */
+public class SceneSummary implements Streamable
+{
+ /** The id of this scene. */
+ public int sceneId;
+
+ /** The name of this scene. */
+ public String name;
+
+ /** The ids of the scenes to which this scene is connected via
+ * portals. */
+ public int[] neighbors;
+
+ /**
+ * Returns the population of this scene summary instance. This is
+ * synchronized because the population can be updated by a background
+ * thread.
+ */
+ public synchronized int getPopulation ()
+ {
+ return _population;
+ }
+
+ /**
+ * Used to set the population of this scene summary instance.
+ */
+ public synchronized void setPopulation (int population)
+ {
+ _population = population;
+ }
+
+ // documentation inherited
+ public void writeTo (DataOutputStream out)
+ throws IOException
+ {
+ out.writeInt(sceneId);
+ out.writeUTF(name);
+ StreamableUtil.writeInts(out, neighbors);
+ out.writeInt(_population);
+ }
+
+ // documentation inherited
+ public void readFrom (DataInputStream in)
+ throws IOException
+ {
+ sceneId = in.readInt();
+ name = in.readUTF();
+ neighbors = StreamableUtil.readInts(in);
+ _population = in.readInt();
+ }
+
+ /** The number of people currently occupying this scene. */
+ protected int _population;
+}
diff --git a/src/java/com/threerings/whirled/zone/data/ZoneSummary.java b/src/java/com/threerings/whirled/zone/data/ZoneSummary.java
new file mode 100644
index 000000000..a9a1f2eb4
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/data/ZoneSummary.java
@@ -0,0 +1,54 @@
+//
+// $Id: ZoneSummary.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.data;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import com.threerings.presents.io.Streamable;
+
+/**
+ * The zone summary contains information on a zone, including its name and
+ * summary info on all of the scenes in this zone (which can be used to
+ * generate a map of the zone on the client).
+ */
+public class ZoneSummary implements Streamable
+{
+ /** The zone's unique identifier. */
+ public int zoneId;
+
+ /** The name of the zone. */
+ public String name;
+
+ /** The summary information for all of the scenes in the zone. */
+ public SceneSummary[] scenes;
+
+ // documentation inherited
+ public void writeTo (DataOutputStream out)
+ throws IOException
+ {
+ out.writeInt(zoneId);
+ out.writeUTF(name);
+ int scount = scenes.length;
+ out.writeInt(scount);
+ for (int i = 0; i < scount; i++) {
+ scenes[i].writeTo(out);
+ }
+ }
+
+ // documentation inherited
+ public void readFrom (DataInputStream in)
+ throws IOException
+ {
+ zoneId = in.readInt();
+ name = in.readUTF();
+ int scount = in.readInt();
+ scenes = new SceneSummary[scount];
+ for (int i = 0; i < scount; i++) {
+ scenes[i] = new SceneSummary();
+ scenes[i].readFrom(in);
+ }
+ }
+}
diff --git a/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java b/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java
new file mode 100644
index 000000000..9c0dbb605
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java
@@ -0,0 +1,21 @@
+//
+// $Id: ZonedBodyObject.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.data;
+
+/**
+ * A system that uses the zone services must provide a body object
+ * extension that implements this interface.
+ */
+public interface ZonedBodyObject
+{
+ /**
+ * Returns the zone id currently occupied by this body.
+ */
+ public int getZoneId ();
+
+ /**
+ * Sets the zone id currently occupied by this body.
+ */
+ public void setZoneId (int zoneId);
+}
diff --git a/src/java/com/threerings/whirled/zone/server/ZoneManager.java b/src/java/com/threerings/whirled/zone/server/ZoneManager.java
new file mode 100644
index 000000000..738eb9217
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/server/ZoneManager.java
@@ -0,0 +1,42 @@
+//
+// $Id: ZoneManager.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.server;
+
+import com.threerings.whirled.zone.data.ZoneSummary;
+
+/**
+ * A zone is a collection of scenes organized into a connected group. A
+ * user can wander around within a zone, moving from scene to scene via
+ * the standard mechanisms. To move between zones, they must use a special
+ * mechanism (like at the dock, they can move from an island zone into
+ * their ship zone; or they can move from an island zone into their house
+ * zone). A zone provides scene summary information that can be used to
+ * display a map of the zone to the client.
+ */
+public interface ZoneManager
+{
+ /**
+ * Used to notify requesters when an asynchronous zone load has
+ * completed (successfully or not).
+ */
+ public static interface ResolutionListener
+ {
+ /**
+ * Called when a zone was successfully resolved.
+ */
+ public void zoneWasResolved (ZoneSummary summary);
+
+ /**
+ * Called when a zone failed to resolve.
+ */
+ public void zoneFailedToResolve (int zoneId, Exception reason);
+ }
+
+ /**
+ * Resolves and delivers the scene summary information for the
+ * requested zone. Zone resolution is an asynchronous process, which
+ * necessitates this callback-style interface.
+ */
+ public void resolveZone (int zoneId, ResolutionListener listener);
+}
diff --git a/src/java/com/threerings/whirled/zone/server/ZoneProvider.java b/src/java/com/threerings/whirled/zone/server/ZoneProvider.java
new file mode 100644
index 000000000..e7eaa3a1e
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/server/ZoneProvider.java
@@ -0,0 +1,177 @@
+//
+// $Id: ZoneProvider.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.server;
+
+import com.threerings.presents.server.InvocationProvider;
+import com.threerings.presents.server.ServiceFailedException;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.crowd.data.PlaceObject;
+import com.threerings.crowd.server.LocationProvider;
+
+import com.threerings.whirled.Log;
+
+import com.threerings.whirled.data.SceneModel;
+import com.threerings.whirled.server.SceneRegistry;
+import com.threerings.whirled.server.SceneManager;
+
+import com.threerings.whirled.zone.client.ZoneCodes;
+import com.threerings.whirled.zone.data.ZoneSummary;
+import com.threerings.whirled.zone.data.ZonedBodyObject;
+
+/**
+ * Provides zone related services which are presently the ability to move
+ * from zone to zone.
+ */
+public class ZoneProvider
+ extends InvocationProvider implements ZoneCodes
+{
+ /**
+ * Constructs a zone provider that will interoperate with the supplied
+ * zone and scene registries. A system that wishes to use the zone
+ * services must instantiate a zone provider and register it with the
+ * invocation services.
+ */
+ public ZoneProvider (ZoneRegistry zonereg, SceneRegistry screg)
+ {
+ _zonereg = zonereg;
+ _screg = screg;
+ }
+
+ /**
+ * Processes a request from a client to move to a scene in a new zone.
+ *
+ * @param source the user requesting the move.
+ * @param invid the invocation id of the request.
+ * @param zoneId the identifier of the new zone.
+ * @param sceneId the identifier of the new scene.
+ * @param sceneVew the version of the scene model currently held by
+ * the client.
+ */
+ public void handleMoveToRequest (BodyObject source, int invid,
+ int zoneId, int sceneId, int sceneVer)
+ {
+ // look up the zone manager for the zone
+ ZoneManager zmgr = _zonereg.getZoneManager(zoneId);
+ if (zmgr == null) {
+ Log.warning("Requested to enter a zone for which we have no " +
+ "manager [user=" + source +
+ ", zoneId=" + zoneId + "].");
+ sendResponse(source, invid, MOVE_FAILED_RESPONSE, NO_SUCH_ZONE);
+ return;
+ }
+
+ // make sure they're not already in that zone
+ if (((ZonedBodyObject)source).getZoneId() == zoneId) {
+ sendResponse(source, invid, MOVE_FAILED_RESPONSE, ALREADY_THERE);
+ return;
+ }
+
+ // avoid cluttering up the method declaration with final keywords
+ final BodyObject fsource = source;
+ final int finvid = invid;
+ final int fsceneId = sceneId;
+ final int fsceneVer = sceneVer;
+
+ // resolve the zone!
+ ZoneManager.ResolutionListener zl = new ZoneManager.ResolutionListener()
+ {
+ public void zoneWasResolved (ZoneSummary summary) {
+ continueMoveToRequest(fsource, finvid, summary,
+ fsceneId, fsceneVer);
+ }
+
+ public void zoneFailedToResolve (int zoneId, Exception reason) {
+ Log.warning("Unable to resolve zone [zoneId=" + zoneId +
+ ", reason=" + reason + "].");
+ sendResponse(fsource, finvid,
+ MOVE_FAILED_RESPONSE, NO_SUCH_ZONE);
+ }
+ };
+ zmgr.resolveZone(zoneId, zl);
+ }
+
+ /**
+ * This is called after we have resolved our zone.
+ */
+ protected void continueMoveToRequest (
+ BodyObject source, int invid, ZoneSummary summary,
+ int sceneId, int sceneVer)
+ {
+ // avoid cluttering up the method declaration with final keywords
+ final BodyObject fsource = source;
+ final int finvid = invid;
+ final ZoneSummary fsum = summary;
+ final int fsceneVer = sceneVer;
+
+ // create a callback object that will handle the resolution or
+ // failed resolution of the scene
+ SceneRegistry.ResolutionListener rl =
+ new SceneRegistry.ResolutionListener()
+ {
+ public void sceneWasResolved (SceneManager scmgr) {
+ finishMoveToRequest(fsource, finvid, fsum, scmgr, fsceneVer);
+ }
+
+ public void sceneFailedToResolve (int sceneId, Exception reason) {
+ Log.warning("Unable to resolve scene [sceneid=" + sceneId +
+ ", reason=" + reason + "].");
+ // pretend like the scene doesn't exist to the client
+ sendResponse(fsource, finvid,
+ MOVE_FAILED_RESPONSE, NO_SUCH_PLACE);
+ }
+ };
+
+ // make sure the scene they are headed to is actually loaded into
+ // the server
+ _screg.resolveScene(sceneId, rl);
+ }
+
+ /**
+ * This is called after the scene to which we are moving is guaranteed
+ * to have been loaded into the server.
+ */
+ protected void finishMoveToRequest (
+ BodyObject source, int invid, ZoneSummary summary,
+ SceneManager scmgr, int sceneVersion)
+ {
+ // move to the place object associated with this scene
+ PlaceObject plobj = scmgr.getPlaceObject();
+ int ploid = plobj.getOid();
+
+ try {
+ // try doing the actual move
+ PlaceConfig config = LocationProvider.moveTo(source, ploid);
+
+ // now that we've finally moved, we can update the user object
+ // with the new zone id
+ ((ZonedBodyObject)source).setZoneId(summary.zoneId);
+
+ // check to see if they need a newer version of the scene data
+ SceneModel model = scmgr.getSceneModel();
+ if (sceneVersion < model.version) {
+ // then send the moveTo response
+ sendResponse(source, invid, MOVE_SUCCEEDED_PLUS_UPDATE_RESPONSE,
+ new Object[] { new Integer(ploid), config,
+ summary, model });
+
+ } else {
+ // then send the moveTo response
+ sendResponse(source, invid, MOVE_SUCCEEDED_RESPONSE,
+ new Integer(ploid), config, summary);
+ }
+
+ } catch (ServiceFailedException sfe) {
+ sendResponse(source, invid,
+ MOVE_FAILED_RESPONSE, sfe.getMessage());
+ }
+ }
+
+ /** The zone registry with which we communicate. */
+ protected ZoneRegistry _zonereg;
+
+ /** The scene registry with which we communicate. */
+ protected SceneRegistry _screg;
+}
diff --git a/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java b/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java
new file mode 100644
index 000000000..6a2a8a15b
--- /dev/null
+++ b/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java
@@ -0,0 +1,54 @@
+//
+// $Id: ZoneRegistry.java,v 1.1 2001/12/04 00:31:58 mdb Exp $
+
+package com.threerings.whirled.zone.server;
+
+import com.samskivert.util.Config;
+import com.samskivert.util.HashIntMap;
+
+import com.threerings.whirled.Log;
+
+/**
+ * The zone registry takes care of mapping zone requests to the
+ * appropriate registered zone manager.
+ */
+public class ZoneRegistry
+{
+ /**
+ * Creates a zone manager with the supplied configuration.
+ */
+ public ZoneRegistry (Config config)
+ {
+ }
+
+ /**
+ * Registers the supplied zone manager as the manager for the
+ * specified zone type. Zone types are 7 bits and managers are
+ * responsible for making sure they don't use a zone type that
+ * collides with another manager (given that we have only three zone
+ * types at present, this doesn't seem unreasonable).
+ */
+ public void registerZoneManager (byte zoneType, ZoneManager manager)
+ {
+ ZoneManager old = (ZoneManager)_managers.get(zoneType);
+ if (old != null) {
+ Log.warning("Zone manager already registered with requested " +
+ "type [type=" + zoneType + ", old=" + old +
+ ", new=" + manager + "].");
+ } else {
+ _managers.put(zoneType, manager);
+ }
+ }
+
+ /**
+ * Returns the zone manager that handles the specified zone id.
+ */
+ public ZoneManager getZoneManager (int zoneId)
+ {
+ int zoneType = (0xFF000000 & zoneId) >> 24;
+ return (ZoneManager)_managers.get(zoneType);
+ }
+
+ /** A table of zone managers. */
+ protected HashIntMap _managers = new HashIntMap();
+}