diff --git a/src/as/com/threerings/whirled/client/SceneDirector.as b/src/as/com/threerings/whirled/client/SceneDirector.as index ac1d02c9..86a3a57e 100644 --- a/src/as/com/threerings/whirled/client/SceneDirector.as +++ b/src/as/com/threerings/whirled/client/SceneDirector.as @@ -173,7 +173,7 @@ public class SceneDirector extends BasicDirector var refuse :Boolean = _locdir.checkRepeatMove(); // complain if we're over-writing a pending request - if (_pendingData != null) { + if (movePending()) { if (refuse) { log.warning("Refusing moveTo; We have a request outstanding", "psid", _pendingData.sceneId, "nsid", sceneId); @@ -383,9 +383,26 @@ public class SceneDirector extends BasicDirector clearScene(); } + /** + * Returns true if there is a pending move request. + */ + public function movePending () :Boolean + { + return (_pendingData != null); + } + // documentation inherited from interface public function forcedMove (sceneId :int) :void { + // if we're in the middle of a move, we can't abort it or we will screw everything up, so + // just finish up what we're doing and assume that the repeated move request was the + // spurious one as it would be in the case of lag causing rapid-fire repeat requests + if (movePending()) { + log.info("Dropping forced move because we have a move pending", + "pendId", _pendingData.sceneId, "reqId", sceneId); + return; + } + log.info("Moving at request of server", "sceneId", sceneId); // clear out our old scene and place data diff --git a/src/as/com/threerings/whirled/zone/client/ZoneDecoder.as b/src/as/com/threerings/whirled/zone/client/ZoneDecoder.as new file mode 100644 index 00000000..de0a1e81 --- /dev/null +++ b/src/as/com/threerings/whirled/zone/client/ZoneDecoder.as @@ -0,0 +1,68 @@ +// +// $Id: SceneDecoder.as 887 2010-01-05 22:12:02Z dhoover $ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/vilya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client { + +import com.threerings.presents.client.InvocationDecoder; +import com.threerings.whirled.zone.client.ZoneReceiver; + +/** + * Dispatches calls to a {@link ZoneReceiver} instance. + */ +public class ZoneDecoder extends InvocationDecoder +{ + /** The generated hash code used to identify this receiver class. */ + public static const RECEIVER_CODE :String = + "2d900cf54355111b4bb4befcdff42b82"; + + /** The method id used to dispatch {@link ZoneReceiver#forcedMove} + * notifications. */ + public static const FORCED_MOVE :int = 1; + + /** + * Creates a decoder that may be registered to dispatch invocation + * service notifications to the specified receiver. + */ + public function ZoneDecoder (receiver :ZoneReceiver) + { + this.receiver = receiver; + } + + // documentation inherited + override public function getReceiverCode () :String + { + return RECEIVER_CODE; + } + + // documentation inherited + override public function dispatchNotification (methodId :int, args :Array) :void + { + switch (methodId) { + case FORCED_MOVE: + (receiver as ZoneReceiver).forcedMove(args[0] as int, args[1] as int); + return; + + default: + super.dispatchNotification(methodId, args); + } + } +} +} diff --git a/src/as/com/threerings/whirled/zone/client/ZoneDirector.as b/src/as/com/threerings/whirled/zone/client/ZoneDirector.as new file mode 100644 index 00000000..4ff3a951 --- /dev/null +++ b/src/as/com/threerings/whirled/zone/client/ZoneDirector.as @@ -0,0 +1,267 @@ +package com.threerings.whirled.zone.client { + +import com.threerings.io.TypedArray; +import com.threerings.util.Log; +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.crowd.data.PlaceConfig; + +import com.threerings.whirled.client.SceneDirector; +import com.threerings.whirled.client.SceneDirector_MoveHandler; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.util.WhirledContext; + +import com.threerings.whirled.zone.client.ZoneService_ZoneMoveListener; +import com.threerings.whirled.zone.data.ZoneSummary; +import com.threerings.whirled.zone.util.ZoneUtil; + + +public class ZoneDirector extends BasicDirector + implements ZoneReceiver, ZoneService_ZoneMoveListener, SceneDirector_MoveHandler +{ + private static const log :Log = Log.getLog(ZoneDirector); + + // We could be streamed one of these... + SceneUpdate; + + /** + * Constructs a zone director with the supplied context, and delegate scene director (which the + * zone director will coordinate with when moving from scene to scene). A zone director is + * required on the client side for systems that wish to use the zone services. + */ + public function ZoneDirector (ctx :WhirledContext, scdir :SceneDirector) + { + super(ctx); + _wCtx = ctx; + _scdir = scdir; + _scdir.setMoveHandler(this); + + // register for zone notifications + _wCtx.getClient().getInvocationDirector().registerReceiver(new ZoneDecoder(this)); + } + + /** + * 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 function getZoneSummary () :ZoneSummary + { + return _summary; + } + + /** + * Adds a zone observer to the list. This observer will subsequently be notified of effected + * and failed zone changes. + */ + public function addZoneObserver (observer :ZoneObserver) :void + { + _observers.add(observer); + } + + /** + * Removes a zone observer from the list. + */ + public function removeZoneObserver (observer :ZoneObserver) :void + { + _observers.remove(observer); + } + + /** + * 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 function moveTo (zoneId :int, sceneId :int, rl :ResultListener = null) :Boolean + { + // make sure the zoneId and sceneId are valid + if (zoneId < 0 || sceneId < 0) { + log.warning("Refusing moveTo(): invalid sceneId or zoneId", + "zoneId", zoneId, "sceneId", sceneId); + return false; + } + + // 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) { + return _scdir.moveTo(sceneId); + } + + // otherwise, we make a zoned moveTo request; prepare to move to this scene (sets up + // pending data) + if (!_scdir.prepareMoveTo(sceneId, rl)) { + return false; + } + + // let our zone observers know that we're attempting to switch zones + notifyObservers(zoneId); + + // 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 pendingModel :SceneModel = _scdir.getPendingModel(); + if (pendingModel != null) { + sceneVers = pendingModel.version; + } + + // issue a moveTo request + log.info("Issuing zoned moveTo(" + ZoneUtil.toString(zoneId) + + ", " + sceneId + ", " + sceneVers + ")."); + _zservice.moveTo(zoneId, sceneId, sceneVers, this); + return true; + } + + override protected function fetchServices (client :Client) :void + { + _zservice = (ZoneService)(client.requireService(ZoneService)); + } + + override public function clientDidLogoff (event :ClientEvent) :void + { + super.clientDidLogoff(event); + + // clear out our business + _zservice = null; + _summary = null; + _previousZoneId = -1; + } + + // from interface ZoneService.ZoneMoveListener + public function moveSucceeded (placeId :int, config :PlaceConfig, summary :ZoneSummary) :void + { + if (_summary != null) { + // keep track of our previous zone info + _previousZoneId = _summary.zoneId; + } + + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceeded(placeId, config); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + // from interface ZoneService.ZoneMoveListener + public function moveSucceededWithUpdates ( + placeId :int, config :PlaceConfig, summary :ZoneSummary, + updates :TypedArray /* of SceneUpdate */) :void + { + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceededWithUpdates(placeId, config, updates); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + // from interface ZoneService.ZoneMoveListener + public function moveSucceededWithScene ( + placeId :int, config :PlaceConfig, summary :ZoneSummary, model :SceneModel) :void + { + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceededWithScene(placeId, config, model); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + // from interface ZoneService.ZoneMoveListener + public function requestFailed (reason :String) :void + { + // let the scene director cope + _scdir.requestFailed(reason); + + // and let the observers know what's up + notifyObservers(reason); + } + + + // documentation inherited from interface + public function forcedMove (zoneId :int, sceneId :int) :void + { + // if we're in the middle of a move, we can't abort it or we will screw everything up, so + // just finish up what we're doing and assume that the repeated move request was the + // spurious one as it would be in the case of lag causing rapid-fire repeat requests + if (_scdir.movePending()) { + log.info("Dropping forced move because we have a move pending", + "pend", _scdir.getPendingModel(), "rzId", zoneId, "rsId", sceneId); + return; + } + + log.info("Moving at request of server", + "zoneId", zoneId, "sceneId", sceneId); + // clear out our old scene and place data + _scdir.didLeaveScene(); + // move to the new zone and scene + moveTo(zoneId, sceneId, null); + } + + // from SceneDirector.MoveHandler + public function recoverMoveTo (previousSceneId :int) :void + { + if (_summary != null) { + return; // if we're currently somewhere, just stay there + } + + // otherwise if we were previously in a zone/scene, try going back there + if (_previousZoneId != -1) { + moveTo(_previousZoneId, previousSceneId); + } else { + _scdir.moveTo(previousSceneId); + } + } + + /** + * Notifies observers of success or failure, depending on the type of object provided as data. + */ + protected function notifyObservers (data :Object) :void + { + // let our observers know that all is well on the western front + for each (var obs :ZoneObserver in _observers) { + try { + if (data is Number) { + obs.zoneWillChange(data as Number); + } else if (data is ZoneSummary) { + obs.zoneDidChange(data as ZoneSummary); + } else { + obs.zoneChangeFailed(data as String); + } + + } catch (e :Error) { + log.warning("Zone observer choked during notification", + "data", data, "obs", obs, e); + } + } + } + + /** A reference to the active client context. */ + protected var _wCtx :WhirledContext; + + /** A reference to the scene director with which we coordinate. */ + protected var _scdir :SceneDirector; + + /** Provides access to zone services. */ + protected var _zservice :ZoneService; + + /** A reference to the zone summary for the currently occupied zone. */ + protected var _summary :ZoneSummary; + + /** Our zone observer list. */ + protected var _observers :Array = []; + + /** Our previous zone id. */ + protected var _previousZoneId :int = -1; +} +} diff --git a/src/as/com/threerings/whirled/zone/client/ZoneObserver.as b/src/as/com/threerings/whirled/zone/client/ZoneObserver.as new file mode 100644 index 00000000..1cb0ac64 --- /dev/null +++ b/src/as/com/threerings/whirled/zone/client/ZoneObserver.as @@ -0,0 +1,38 @@ +package com.threerings.whirled.zone.client { + +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * The zone observer interface makes it possible for entities to be + * notified when the client moves to a new zone. + */ +public interface ZoneObserver +{ + /** + * Called when we begin the process of switching to a new zone. This + * will be followed by a call to {@link #zoneDidChange} to indicate + * that the change was successful or {@link #zoneChangeFailed} if the + * change fails. + * + * @param zoneId the zone id of the zone to which we are changing. + */ + function zoneWillChange (zoneId :int) :void; + + /** + * Called when we have switched to a new zone. + * + * @param summary the summary information for the new zone or null if + * we have switched to no zone. + */ + function zoneDidChange (summary :ZoneSummary) :void; + + /** + * This is called on all zone observers when a zone change request is + * rejected by the server or fails for some other reason. + * + * @param reason the reason code that explains why the zone change + * request was rejected or otherwise failed. + */ + function zoneChangeFailed (reason :String) :void; +} +} diff --git a/src/as/com/threerings/whirled/zone/client/ZoneReceiver.as b/src/as/com/threerings/whirled/zone/client/ZoneReceiver.as new file mode 100644 index 00000000..3a8fd75e --- /dev/null +++ b/src/as/com/threerings/whirled/zone/client/ZoneReceiver.as @@ -0,0 +1,19 @@ +package com.threerings.whirled.zone.client { + +import com.threerings.presents.client.InvocationReceiver; + +/** + * Defines, for the zone services, a set of notifications delivered asynchronously by the server + * to the client. + */ +public interface ZoneReceiver extends InvocationReceiver +{ + /** + * Used to communicate a required move notification to the client. The server will have + * removed the client from their existing scene and the client is then responsible for + * generating a {@link ZoneService#moveTo} request to move to the new scene in the specified + * zone. + */ + function forcedMove (zoneId :int, sceneId :int) :void; +} +} diff --git a/src/as/com/threerings/whirled/zone/data/SceneSummary.as b/src/as/com/threerings/whirled/zone/data/SceneSummary.as index dd27ca06..f0af92e1 100644 --- a/src/as/com/threerings/whirled/zone/data/SceneSummary.as +++ b/src/as/com/threerings/whirled/zone/data/SceneSummary.as @@ -66,8 +66,8 @@ public class SceneSummary { sceneId = ins.readInt(); name = (ins.readField(String) as String); - neighbors = TypedArray(ins.readObject()); - neighborDirs = TypedArray(ins.readObject()); + neighbors = TypedArray(ins.readField(TypedArray.getJavaType(int))); + neighborDirs = TypedArray(ins.readField(TypedArray.getJavaType(int))); } // from interface Streamable @@ -75,8 +75,8 @@ public class SceneSummary { out.writeInt(sceneId); out.writeField(name); - out.writeObject(neighbors); - out.writeObject(neighborDirs); + out.writeField(neighbors); + out.writeField(neighborDirs); } } } diff --git a/src/as/com/threerings/whirled/zone/util/ZoneUtil.as b/src/as/com/threerings/whirled/zone/util/ZoneUtil.as new file mode 100644 index 00000000..198fa088 --- /dev/null +++ b/src/as/com/threerings/whirled/zone/util/ZoneUtil.as @@ -0,0 +1,46 @@ +package com.threerings.whirled.zone.util { + +/** + * Server-specific, zone-related utility functions. + */ +public class ZoneUtil +{ + /** + * Composes the zone type and zone id into a qualified zone id. A + * qualified zone id is what should be passed around so that the + * server can determine the zone type from the zone id when necessary. + */ + public static function qualifyZoneId (zoneType :int, zoneId :int) :int + { + var qualifiedZoneId :int = zoneType; + qualifiedZoneId <<= 24; + qualifiedZoneId |= zoneId; + return qualifiedZoneId; + } + + /** + * Extracts the zone type from a qualified zone id. + */ + public static function zoneType (qualifiedZoneId :int) :int + { + return (0xFF000000 & qualifiedZoneId) >> 24; + } + + /** + * Extracts the zone id from a qualified zone id. + */ + public static function zoneId (qualifiedZoneId :int) :int + { + return (0x00FFFFFF & qualifiedZoneId); + } + + /** + * Returns an easier to read representation of the supplied qualified + * zone id: type:id. + */ + public static function toString (qualifiedZoneId :int) :String + { + return zoneType(qualifiedZoneId) + ":" + zoneId(qualifiedZoneId); + } +} +}