23b46bd7ea
git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@987 c613c5cb-e716-0410-b11b-feb51c14d237
657 lines
23 KiB
ActionScript
657 lines
23 KiB
ActionScript
//
|
|
// $Id$
|
|
//
|
|
// Vilya library - tools for developing networked games
|
|
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
|
|
// http://code.google.com/p/vilya/
|
|
//
|
|
// This library is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License as published
|
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
package com.threerings.whirled.client {
|
|
|
|
import flash.errors.IOError;
|
|
import flash.errors.IllegalOperationError;
|
|
|
|
import com.threerings.util.Log;
|
|
import com.threerings.util.Map;
|
|
import com.threerings.util.Maps;
|
|
import com.threerings.util.ResultListener;
|
|
|
|
import com.threerings.io.TypedArray;
|
|
|
|
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.presents.data.InvocationCodes;
|
|
|
|
import com.threerings.crowd.client.LocationDirector;
|
|
import com.threerings.crowd.client.LocationDirector_FailureHandler;
|
|
import com.threerings.crowd.client.LocationObserver;
|
|
import com.threerings.crowd.data.PlaceConfig;
|
|
import com.threerings.crowd.data.PlaceObject;
|
|
|
|
import com.threerings.whirled.client.persist.SceneRepository;
|
|
import com.threerings.whirled.data.Scene;
|
|
import com.threerings.whirled.data.SceneCodes;
|
|
import com.threerings.whirled.data.SceneMarshaller;
|
|
import com.threerings.whirled.data.SceneModel;
|
|
import com.threerings.whirled.data.SceneObject;
|
|
import com.threerings.whirled.data.SceneUpdate;
|
|
import com.threerings.whirled.util.NoSuchSceneError;
|
|
import com.threerings.whirled.util.SceneFactory;
|
|
import com.threerings.whirled.util.WhirledContext;
|
|
|
|
/**
|
|
* The scene director is the client's interface to all things scene related. It interfaces with the
|
|
* scene repository to ensure that scene objects are available when the client enters a particular
|
|
* scene. It handles moving from scene to scene (it coordinates with the {@link LocationDirector}
|
|
* in order to do this).
|
|
*
|
|
* <p> Note that when the scene director is in use instead of the location director, scene ids
|
|
* instead of place oids will be supplied to {@link LocationObserver#locationMayChange} and {@link
|
|
* LocationObserver#locationChangeFailed}.
|
|
*/
|
|
public class SceneDirector extends BasicDirector
|
|
implements LocationDirector_FailureHandler, SceneReceiver, SceneService_SceneMoveListener,
|
|
LocationObserver
|
|
{
|
|
private static const log :Log = Log.getLog(SceneDirector);
|
|
|
|
// statically reference classes we require
|
|
SceneMarshaller;
|
|
|
|
/**
|
|
* Creates a new scene director with the specified context.
|
|
*
|
|
* @param ctx the active client context.
|
|
* @param locdir the location director in use on the client, with which the scene director will
|
|
* coordinate when changing location.
|
|
* @param screp the entity from which the scene director will load scene data from the local
|
|
* client scene storage. This may be null when the SceneDirector is constructed, but it should
|
|
* be supplied via {@link #setSceneRepository} prior to really using this director.
|
|
* @param fact the factory that knows which derivation of {@link Scene} to create for the
|
|
* current system.
|
|
*/
|
|
public function SceneDirector (ctx :WhirledContext, locdir :LocationDirector,
|
|
screp :SceneRepository, fact :SceneFactory)
|
|
{
|
|
super(ctx);
|
|
|
|
// we'll need these for later
|
|
_wctx = ctx;
|
|
_locdir = locdir;
|
|
setSceneRepository(screp);
|
|
_fact = fact;
|
|
|
|
// we need to observe scene moves so that we can clear out if we change to a non-scene
|
|
_locdir.addLocationObserver(this);
|
|
|
|
// set ourselves up as a failure handler with the location director because we need to do
|
|
// special processing
|
|
_locdir.setFailureHandler(this);
|
|
|
|
// register for scene notifications
|
|
_wctx.getClient().getInvocationDirector().registerReceiver(new SceneDecoder(this));
|
|
}
|
|
|
|
/**
|
|
* Set the scene repository.
|
|
*/
|
|
public function setSceneRepository (screp :SceneRepository) :void
|
|
{
|
|
_screp = screp;
|
|
_scache.clear();
|
|
}
|
|
|
|
/**
|
|
* Returns the display scene object associated with the scene we currently occupy or null if we
|
|
* currently occupy no scene.
|
|
*/
|
|
public function getScene () :Scene
|
|
{
|
|
return _scene;
|
|
}
|
|
|
|
/**
|
|
* Requests that this client move the specified scene. A request will be made and when the
|
|
* response is received, the location observers will be notified of success or failure.
|
|
*
|
|
* @return true if the move to request was issued, false if it was rejected by a location
|
|
* observer or because we have another request outstanding.
|
|
*/
|
|
public function moveTo (sceneId :int) :Boolean
|
|
{
|
|
// make sure the sceneId is valid
|
|
if (sceneId < 0) {
|
|
log.warning("Refusing moveTo(): invalid sceneId", "sceneId", sceneId);
|
|
return false;
|
|
}
|
|
|
|
// sanity-check the destination scene id
|
|
if (sceneId == _sceneId) {
|
|
log.warning("Refusing request to move to the same scene", "sceneId", sceneId);
|
|
return false;
|
|
}
|
|
|
|
// prepare to move to this scene (sets up pending data)
|
|
if (!prepareMoveTo(sceneId, null)) {
|
|
return false;
|
|
}
|
|
|
|
// do the deed
|
|
sendMoveRequest();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Prepares to move to the requested scene. The location observers are asked to ratify the move
|
|
* and our pending scene mode is loaded from the scene repository. This can be called by
|
|
* cooperating directors that need to coopt the moveTo process.
|
|
*/
|
|
public function prepareMoveTo (sceneId :int, rl :ResultListener) :Boolean
|
|
{
|
|
// first check to see if our observers are happy with this move request
|
|
if (!_locdir.mayMoveTo(sceneId, rl)) {
|
|
return false;
|
|
}
|
|
|
|
// we need to call this both to mark that we're issuing a move request and to check to see
|
|
// if the last issued request should be considered stale
|
|
var refuse :Boolean = _locdir.checkRepeatMove();
|
|
|
|
// complain if we're over-writing a pending request
|
|
if (movePending()) {
|
|
if (refuse) {
|
|
log.warning("Refusing moveTo; We have a request outstanding",
|
|
"psid", _pendingData.sceneId, "nsid", sceneId);
|
|
return false;
|
|
|
|
} else {
|
|
log.warning("Overriding stale moveTo request",
|
|
"psid", _pendingData.sceneId, "nsid", sceneId);
|
|
}
|
|
}
|
|
|
|
// create and initialize a new pending data record
|
|
_pendingData = createPendingData();
|
|
|
|
// load up the cached pending scene so that we can communicate its version to the server
|
|
_pendingData.model = loadSceneModel(sceneId);
|
|
|
|
// make a note of our pending scene id
|
|
_pendingData.sceneId = sceneId;
|
|
|
|
// all systems go
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns a function that may be used to restore the scene director's pendingData to its
|
|
* current state. This is primarily useful if you know something (such as a logoff) is about
|
|
* to happen and you want to ensure you can restore the pending data after re-login.
|
|
*/
|
|
public function getPendingDataRestoreFunc () :Function
|
|
{
|
|
var pendingData :PendingData = _pendingData;
|
|
|
|
return function () :void {
|
|
_pendingData = pendingData;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the model loaded in preparation for a scene transition. This is made available only
|
|
* for cooperating directors which may need to coopt the scene transition process. The pending
|
|
* model is only valid immediately following a call to {@link #prepareMoveTo}.
|
|
*/
|
|
public function getPendingModel () :SceneModel
|
|
{
|
|
return _pendingData == null ? null : _pendingData.model;
|
|
}
|
|
|
|
/**
|
|
* Returns the scene id set in preparation for a scene transition. As with
|
|
* {@link #getPendingModel}, this is for cooperating directors.
|
|
*/
|
|
public function getPendingSceneId () :int
|
|
{
|
|
return _pendingData == null ? -1 : _pendingData.sceneId;
|
|
}
|
|
|
|
// from interface SceneService_SceneMoveListener
|
|
public function moveSucceeded (placeId :int, config :PlaceConfig) :void
|
|
{
|
|
// our move request was successful, deal with subscribing to our new place object
|
|
_locdir.didMoveTo(placeId, config);
|
|
|
|
// keep track of our previous scene info
|
|
_previousSceneId = _sceneId;
|
|
|
|
// clear out the old info
|
|
clearScene();
|
|
|
|
// make the pending scene the active scene
|
|
if (_pendingData == null) {
|
|
log.warning("Aiya! Scene move succeeded but we have no pending data!",
|
|
"prevId", _previousSceneId);
|
|
return;
|
|
}
|
|
_sceneId = _pendingData.sceneId;
|
|
_pendingData = null;
|
|
|
|
// since we're committed to moving to the new scene, we'll parallelize and go ahead and
|
|
// load up the new scene now rather than wait until subscription to our place object
|
|
// succeeds
|
|
_model = loadSceneModel(_sceneId);
|
|
|
|
// complain if we didn't find a scene
|
|
if (_model == null) {
|
|
log.warning("Aiya! Unable to load scene", "sid", _sceneId, "plid", placeId);
|
|
return;
|
|
}
|
|
|
|
// and finally create a display scene instance with the model and the place config
|
|
_scene = _fact.createScene(_model, config);
|
|
|
|
handlePendingForcedMove();
|
|
}
|
|
|
|
// from interface SceneService_SceneMoveListener
|
|
public function moveSucceededWithUpdates (
|
|
placeId :int, config :PlaceConfig, updates :TypedArray) :void
|
|
{
|
|
log.info("Got updates", "placeId", placeId, "config", config, "updates", updates);
|
|
|
|
// apply the updates to our cached scene
|
|
var model :SceneModel = loadSceneModel(_pendingData.sceneId);
|
|
var failure :Boolean = false;
|
|
for each (var update :SceneUpdate in updates) {
|
|
try {
|
|
update.validate(model);
|
|
} catch (ise :IllegalOperationError) {
|
|
log.warning("Scene update failed validation",
|
|
"model", model, "update", update, "error", ise.message);
|
|
failure = true;
|
|
break;
|
|
}
|
|
|
|
try {
|
|
update.apply(model);
|
|
} catch (e :Error) {
|
|
log.warning("Failure applying scene update", "model", model, "update", update, e);
|
|
failure = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (failure) {
|
|
// delete the now half-booched scene model from the repository
|
|
try {
|
|
_screp.deleteSceneModel(_pendingData.sceneId);
|
|
} catch (ioe :IOError) {
|
|
log.warning("Failure removing booched scene model",
|
|
"sceneId", _pendingData.sceneId, ioe);
|
|
}
|
|
|
|
// act as if the scene move failed, though we'll be in a funny state because the server
|
|
// thinks we've changed scenes, but the client can try again without its booched scene
|
|
// model
|
|
requestFailed(InvocationCodes.INTERNAL_ERROR);
|
|
return;
|
|
}
|
|
|
|
// store the updated scene in the repository
|
|
persistSceneModel(model);
|
|
|
|
// finally pass through to the normal success handler
|
|
moveSucceeded(placeId, config);
|
|
}
|
|
|
|
// from interface SceneService_SceneMoveListener
|
|
public function moveSucceededWithScene (
|
|
placeId :int, config :PlaceConfig, model :SceneModel) :void
|
|
{
|
|
log.info("Got updated scene model",
|
|
"placeId", placeId, "config", config,
|
|
"scene", model.sceneId + "/" + model.name + "/" + model.version);
|
|
|
|
// update the model in the repository
|
|
persistSceneModel(model);
|
|
|
|
// update our scene cache
|
|
_scache.put(model.sceneId, model);
|
|
|
|
// and pass through to the normal move succeeded handler
|
|
moveSucceeded(placeId, config);
|
|
}
|
|
|
|
// from interface SceneService_SceneMoveListener
|
|
public function moveRequiresServerSwitch (hostname :String, ports :TypedArray) :void
|
|
{
|
|
log.info("Scene switch requires server switch", "host", hostname, "ports", ports);
|
|
// keep track of our current pending data because it will be cleared when we log off of
|
|
// this server and onto the next one
|
|
var pendingData :PendingData = _pendingData;
|
|
// ship on over to the other server
|
|
_wctx.getClient().moveToServer(hostname, ports, new ConfirmAdapter(
|
|
function () :void { // succeeded
|
|
// resend our move request now that we're connected to the new server
|
|
_pendingData = pendingData;
|
|
sendMoveRequest();
|
|
}, requestFailed));
|
|
}
|
|
|
|
// from interface SceneService_SceneMoveListener
|
|
public function requestFailed (reason :String) :void
|
|
{
|
|
// clear out our pending info
|
|
var sceneId :int = _pendingData.sceneId;
|
|
_pendingData = null;
|
|
|
|
// let our observers know that something has gone horribly awry
|
|
_locdir.failedToMoveTo(sceneId, reason);
|
|
|
|
handlePendingForcedMove();
|
|
}
|
|
|
|
// from interface LocationObserver
|
|
public function locationMayChange (placeId :int) :Boolean
|
|
{
|
|
return true; // fine with us
|
|
}
|
|
|
|
// from interface LocationObserver
|
|
public function locationDidChange (place :PlaceObject) :void
|
|
{
|
|
// if we're no longer in a scene, we need to clear out our scene information
|
|
if (!(place is SceneObject)) {
|
|
clearScene();
|
|
}
|
|
}
|
|
|
|
// from interface LocationObserver
|
|
public function locationChangeFailed (placeId :int, reason :String) :void
|
|
{
|
|
// we don't care about this notification as we're registered as a LocationDirector
|
|
// FailureHandler so we will later be requested to recover from our failed move
|
|
}
|
|
|
|
/**
|
|
* Called by SceneController instances to tell us about an update to the current scene.
|
|
*/
|
|
public function updateReceived (update :SceneUpdate) :void
|
|
{
|
|
_scene.updateReceived(update);
|
|
persistSceneModel(_scene.getSceneModel());
|
|
}
|
|
|
|
/**
|
|
* Called to clean up our place and scene state information when we leave a scene.
|
|
*/
|
|
public function didLeaveScene () :void
|
|
{
|
|
// let the location director know what's up
|
|
_locdir.didLeavePlace();
|
|
|
|
// clear out our own scene state
|
|
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()) {
|
|
if (_pendingData.sceneId == sceneId) {
|
|
log.info("Dropping forced move because we have a move pending",
|
|
"pendId", _pendingData.sceneId, "reqId", sceneId);
|
|
} else {
|
|
log.info("Delaying forced move because we have a move pending",
|
|
"pendId", _pendingData.sceneId, "reqId", sceneId);
|
|
addPendingForcedMove(new function() :void {
|
|
forcedMove(sceneId);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
log.info("Moving at request of server", "sceneId", sceneId);
|
|
|
|
// clear out our old scene and place data
|
|
didLeaveScene();
|
|
|
|
// move to the new scene
|
|
moveTo(sceneId);
|
|
}
|
|
|
|
/**
|
|
* Sets the moveHandler for use in recoverFailedMove.
|
|
*/
|
|
public function setMoveHandler (handler :SceneDirector_MoveHandler) :void
|
|
{
|
|
if (_moveHandler != null) {
|
|
log.warning("Requested to set move handler, but we've already got one. The " +
|
|
"conflicting entities will likely need to perform more sophisticated " +
|
|
"coordination to deal with failures.", "old", _moveHandler, "new", handler);
|
|
|
|
} else {
|
|
_moveHandler = handler;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when something breaks down in the process of performing a <code>moveTo</code>
|
|
* request.
|
|
*/
|
|
public function recoverFailedMove (placeId :int) :void
|
|
{
|
|
// we'll need this momentarily
|
|
var sceneId :int = _sceneId;
|
|
|
|
// clear out our now bogus scene tracking info
|
|
clearScene();
|
|
|
|
// if we were previously somewhere (and that somewhere isn't where we just tried to go),
|
|
// try going back to that happy place
|
|
if (_previousSceneId != -1 && _previousSceneId != sceneId) {
|
|
// if we have a move handler use that
|
|
if (_moveHandler != null) {
|
|
_moveHandler.recoverMoveTo(_previousSceneId);
|
|
|
|
} else {
|
|
moveTo(_previousSceneId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// documentation inherited
|
|
override public function clientDidLogoff (event :ClientEvent) :void
|
|
{
|
|
super.clientDidLogoff(event);
|
|
|
|
// clear out our business
|
|
clearScene();
|
|
_scache.clear();
|
|
_pendingData = null;
|
|
_previousSceneId = -1;
|
|
_sservice = null;
|
|
}
|
|
|
|
public function cancelMoveRequest () :void
|
|
{
|
|
_pendingData = null;
|
|
handlePendingForcedMove();
|
|
}
|
|
|
|
/**
|
|
* Issues the scene move request using information from the supplied pending data.
|
|
*/
|
|
protected function sendMoveRequest () :void
|
|
{
|
|
// 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;
|
|
if (_pendingData.model != null) {
|
|
sceneVers = _pendingData.model.version;
|
|
}
|
|
|
|
// issue a moveTo request
|
|
log.info("Issuing moveTo(" + _pendingData.sceneId + ", " + sceneVers + ").");
|
|
_sservice.moveTo(_pendingData.sceneId, sceneVers, this);
|
|
}
|
|
|
|
/**
|
|
* Creates a pending scene move request. Derived classes may wish to extend this data with
|
|
* additional information to be tracked during movement between scenes. This is encapsulated so
|
|
* that if a scene move requires a switch to a new server, all necessary data can be properly
|
|
* tracked during the server switch.
|
|
*/
|
|
protected function createPendingData () :PendingData
|
|
{
|
|
return new PendingData();
|
|
}
|
|
|
|
/**
|
|
* Clears out our current scene information and releases the scene model for the loaded scene
|
|
* back to the cache.
|
|
*/
|
|
protected function clearScene () :void
|
|
{
|
|
// clear out our scene id info
|
|
_sceneId = -1;
|
|
|
|
// clear out our references
|
|
_model = null;
|
|
_scene = null;
|
|
}
|
|
|
|
public function addPendingForcedMove (move :Function) :void
|
|
{
|
|
_pendingForcedMoves.push(move);
|
|
}
|
|
|
|
protected function handlePendingForcedMove () :void
|
|
{
|
|
if (!_pendingForcedMoves.length == 0) {
|
|
_ctx.getClient().callLater(_pendingForcedMoves.pop());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads a scene from the repository. If the scene is cached, it will be returned from the
|
|
* cache instead.
|
|
*/
|
|
protected function loadSceneModel (sceneId :int) :SceneModel
|
|
{
|
|
// first look in the model cache
|
|
var model :SceneModel = (_scache.get(sceneId) as SceneModel);
|
|
|
|
// load from the repository if it's not cached
|
|
if (model == null) {
|
|
try {
|
|
model = _screp.loadSceneModel(sceneId);
|
|
_scache.put(sceneId, model);
|
|
|
|
} catch (nsse :NoSuchSceneError) {
|
|
// nothing special here, just fall through and return null
|
|
|
|
} catch (ioe :IOError) {
|
|
// complain first, then return null
|
|
log.warning("Error loading scene", "scid", sceneId, "error", ioe);
|
|
}
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
/**
|
|
* Persist the scene model to the clientside persistant cache.
|
|
*/
|
|
protected function persistSceneModel (model :SceneModel) :void
|
|
{
|
|
try {
|
|
_screp.storeSceneModel(model);
|
|
} catch (ioe :IOError) {
|
|
log.warning("Failed to update repository with updated scene",
|
|
"sceneId", model.sceneId, "nvers", model.version, ioe);
|
|
}
|
|
}
|
|
|
|
// from BasicDirector
|
|
override protected function registerServices (client :Client) :void
|
|
{
|
|
client.addServiceGroup(SceneCodes.WHIRLED_GROUP);
|
|
}
|
|
|
|
// from BasicDirector
|
|
override protected function fetchServices (client :Client) :void
|
|
{
|
|
// get a handle on our scene service
|
|
_sservice = (client.requireService(SceneService) as SceneService);
|
|
}
|
|
|
|
/** Access to general client services. */
|
|
protected var _wctx :WhirledContext;
|
|
|
|
/** Access to our scene services. */
|
|
protected var _sservice :SceneService;
|
|
|
|
/** The client's active location director. */
|
|
protected var _locdir :LocationDirector;
|
|
|
|
/** The entity via which we load scene data. */
|
|
protected var _screp :SceneRepository;
|
|
|
|
/** The entity we use to create scenes from scene models. */
|
|
protected var _fact :SceneFactory;
|
|
|
|
/** A cache of scene model information. */
|
|
protected var _scache :Map = Maps.newBuilder(int).makeLR(5).build();
|
|
|
|
/** The display scene object for the scene we currently occupy. */
|
|
protected var _scene :Scene;
|
|
|
|
/** The scene model for the scene we currently occupy. */
|
|
protected var _model :SceneModel;
|
|
|
|
/** The id of the scene we currently occupy. */
|
|
protected var _sceneId :int = -1;
|
|
|
|
/** Information for the scene for which we have an outstanding moveTo request, or null if we
|
|
* have no outstanding request. */
|
|
protected var _pendingData :PendingData;
|
|
|
|
/** The id of the scene we previously occupied. */
|
|
protected var _previousSceneId :int = -1;
|
|
|
|
/** Reference to our move handler. */
|
|
protected var _moveHandler :SceneDirector_MoveHandler = null;
|
|
|
|
/** Forced move actions we should take once we complete the move we're in the middle of. */
|
|
protected var _pendingForcedMoves :Array = [];
|
|
}
|
|
}
|