//
// $Id: LocationDirector.java,v 1.16 2001/12/16 05:18:20 mdb Exp $
package com.threerings.crowd.client;
import java.util.ArrayList;
import java.util.List;
import com.threerings.presents.client.*;
import com.threerings.presents.dobj.*;
import com.threerings.crowd.Log;
import com.threerings.crowd.data.*;
import com.threerings.crowd.util.CrowdContext;
/**
* The location director provides a means by which entities on the client
* can request to move from place to place and can be notified if other
* entities have caused the client to move to a new place. It also
* provides a mechanism for ratifying a request to move to a new place
* before actually issuing the request.
*/
public class LocationDirector
implements ClientObserver, Subscriber
{
/**
* Used to recover from a moveTo request that was accepted but
* resulted in a failed attempt to fetch the place object to which we
* were moving.
*/
public static interface FailureHandler
{
/**
* Should instruct the client to move to the last known working
* location (as well as clean up after the failed moveTo request).
*/
public void recoverFailedMove (int placeId);
}
/**
* Constructs a location director which will configure itself for
* operation using the supplied context.
*/
public LocationDirector (CrowdContext ctx)
{
// keep this around for later
_ctx = ctx;
// register ourselves as a client observer
ctx.getClient().addObserver(this);
}
/**
* Adds a location observer to the list. This observer will
* subsequently be notified of potential, effected and failed location
* changes.
*/
public void addLocationObserver (LocationObserver observer)
{
_observers.add(observer);
}
/**
* Removes a location observer from the list.
*/
public void removeLocationObserver (LocationObserver observer)
{
_observers.remove(observer);
}
/**
* Returns the place object for the location we currently occupy or
* null if we're not currently occupying any location.
*/
public PlaceObject getPlaceObject ()
{
return _plobj;
}
/**
* Requests that this client be moved to the specified place. 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 placeId)
{
// first check to see if our observers are happy with this move
// request
if (!mayMoveTo(placeId)) {
return;
}
// complain if we're over-writing a pending request
if (_pendingPlaceId != -1) {
Log.warning("We appear to have a moveTo request outstanding " +
"[ppid=" + _pendingPlaceId +
", npid=" + placeId + "].");
// but we're going to fall through and do it anyway because
// refusing to switch rooms at this point will inevitably
// result in some strange bug causing a move request to be
// dropped by the server and the client that did it to be
// totally hosed because they can no longer move to new
// locations because they still have an outstanding request
}
// make a note of our pending place id
_pendingPlaceId = placeId;
// issue a moveTo request
LocationService.moveTo(_ctx.getClient(), placeId, this);
}
/**
* Requests to move to the room that we last occupied, if such a room
* exists.
*
* @return true if we had a previous room and we requested to move to
* it, false if we had no previous room.
*/
public boolean moveBack ()
{
if (_previousPlaceId == -1) {
return false;
} else {
moveTo(_previousPlaceId);
return true;
}
}
/**
* This can be called by cooperating directors that need to coopt the
* moving process to extend it in some way or other. In such
* situations, they should call this method before moving to a new
* location to check to be sure that all of the registered location
* observers are amenable to a location change.
*
* @param placeId the place oid of our tentative new location.
*
* @return true if everyone is happy with the move, false if it was
* vetoed by one of the location observers.
*/
public boolean mayMoveTo (int placeId)
{
for (int i = 0; i < _observers.size(); i++) {
LocationObserver obs = (LocationObserver)_observers.get(i);
if (!obs.locationMayChange(placeId)) {
Log.info("Location change vetoed by observer " +
"[pid=" + placeId + ", obs=" + obs + "].");
return false;
}
}
return true;
}
/**
* This can be called by cooperating directors that need to coopt the
* moving process to extend it in some way or other. In such
* situations, they will be responsible for receiving the successful
* move response and they should let the location director know that
* the move has been effected.
*
* @param placeId the place oid of our new location.
* @param config the configuration information for the new place.
*/
public void didMoveTo (int placeId, PlaceConfig config)
{
DObjectManager omgr = _ctx.getDObjectManager();
// do some cleaning up if we were previously in a place
if (_plobj != null) {
// let the old controller know that things are going away
if (_controller != null) {
try {
_controller.didLeavePlace(_plobj);
} catch (Exception e) {
Log.warning("Place controller choked in " +
"didLeavePlace [plobj=" + _plobj + "].");
Log.logStackTrace(e);
}
_controller = null;
}
// unsubscribe from our old place object
omgr.unsubscribeFromObject(_plobj.getOid(), this);
_plobj = null;
}
// make a note that we're now mostly in the new location
_previousPlaceId = _placeId;
_placeId = placeId;
Class cclass = config.getControllerClass();
try {
// start up a new place controller to manage the new place
_controller = (PlaceController)cclass.newInstance();
_controller.init(_ctx, config);
} catch (Exception e) {
Log.warning("Error creating or initializing place controller " +
"[cclass=" + cclass.getName() +
", config=" + config + "].");
Log.logStackTrace(e);
}
// subscribe to our new place object to complete the move
omgr.subscribeToObject(_placeId, this);
}
/**
* This can be called by cooperating directors that need to coopt the
* moving process to extend it in some way or other. If the coopted
* move request fails, this failure can be propagated to the location
* observers if appropriate.
*
* @param placeId the place oid to which we failed to move.
* @param reason the reason code given for failure.
*/
public void failedToMoveTo (int placeId, String reason)
{
// let our observers know what's up
notifyFailure(placeId, reason);
}
public void clientDidLogon (Client client)
{
// get a copy of our body object
Subscriber sub = new Subscriber() {
public void objectAvailable (DObject object)
{
gotBodyObject((BodyObject)object);
}
public void requestFailed (int oid, ObjectAccessException cause)
{
Log.warning("Location director unable to fetch body " +
"object; all has gone horribly wrong" +
"[cause=" + cause + "].");
}
};
int cloid = client.getClientOid();
client.getDObjectManager().subscribeToObject(cloid, sub);
}
protected void gotBodyObject (BodyObject clobj)
{
// check to see if we are already in a location, in which case
// we'll want to be going there straight away
}
public void clientFailedToLogon (Client client, Exception cause)
{
// we're fair weather observers. we do nothing until logon
// succeeds
}
public void clientConnectionFailed (Client client, Exception cause)
{
// nothing doing
}
public boolean clientWillLogoff (Client client)
{
// we have no objections
return true;
}
public void clientDidLogoff (Client client)
{
// nothing doing
}
/**
* Called in response to a successful moveTo request.
*/
public void handleMoveSucceeded (int invid, PlaceConfig config)
{
// handle the successful move
didMoveTo(_pendingPlaceId, config);
// and clear out the tracked pending oid
_pendingPlaceId = -1;
}
/**
* Called in response to a failed moveTo request.
*/
public void handleMoveFailed (int invid, String reason)
{
// clear out our pending request oid
int placeId = _pendingPlaceId;
_pendingPlaceId = -1;
Log.info("moveTo failed [pid=" + placeId +
", reason=" + reason + "].");
// let our observers know that something has gone horribly awry
notifyFailure(placeId, reason);
}
/**
* Called when we receive the place object to which we subscribed
* after a successful moveTo request.
*/
public void objectAvailable (DObject object)
{
// yay, we have our new place object
_plobj = (PlaceObject)object;
// let the place controller know that we're ready to roll
if (_controller != null) {
try {
_controller.willEnterPlace(_plobj);
} catch (Exception e) {
Log.warning("Controller choked in willEnterPlace " +
"[place=" + _plobj + "].");
Log.logStackTrace(e);
}
}
// let our observers know that all is well on the western front
for (int i = 0; i < _observers.size(); i++) {
LocationObserver obs = (LocationObserver)_observers.get(i);
obs.locationDidChange(_plobj);
}
}
/**
* Called if we are unable to subscribe to the place object that was
* provided to us with our successful moveTo request. This is
* generally a bad scene and we do our best to recover by going back
* to the previously known location.
*/
public void requestFailed (int oid, ObjectAccessException cause)
{
// aiya! we were unable to fetch our new place object; something
// is badly wrong
Log.warning("Aiya! Unable to fetch place object for new location " +
"[plid=" + oid + ", reason=" + cause + "].");
// clear out our half initialized place info
int placeId = _placeId;
_placeId = -1;
// let the kids know shit be fucked
notifyFailure(placeId, "m.unable_to_fetch_place_object");
// we need to sort out what to do about the half-initialized place
// controller. presently we punt and hope that calling
// didLeavePlace() without ever having called willEnterPlace()
// does whatever's necessary
// try to return to our previous location
if (_failureHandler != null) {
_failureHandler.recoverFailedMove(placeId);
} else {
// if we were previously somewhere (and that somewhere isn't
// where we just tried to go), try going back to that happy
// place
if (_previousPlaceId != -1 && _previousPlaceId != placeId) {
moveTo(_previousPlaceId);
}
}
}
/**
* Sets the failure handler which will recover from place object
* fetching failures. In the event that we are unable to fetch our
* place object after making a successful moveTo request, we attempt
* to rectify the failure by moving back to the last known working
* location. Because entites that cooperate with the location director
* may need to become involved in this failure recovery, we provide
* this interface whereby they can interject themseves into the
* failure recovery process and do their own failure recovery.
*/
public void setFailureHandler (FailureHandler handler)
{
if (_failureHandler != null) {
Log.warning("Requested to set failure handler, but we've " +
"already got one. The conflicting entities will " +
"likely need to perform more sophisticated " +
"coordination to deal with failures. " +
"[old=" + _failureHandler + ", new=" + handler + "].");
} else {
_failureHandler = handler;
}
}
protected void notifyFailure (int placeId, String reason)
{
for (int i = 0; i < _observers.size(); i++) {
LocationObserver obs = (LocationObserver)_observers.get(i);
obs.locationChangeFailed(placeId, reason);
}
}
/** The context through which we access needed services. */
protected CrowdContext _ctx;
/** Our location observer list. */
protected List _observers = new ArrayList();
/** The oid of the place we currently occupy. */
protected int _placeId = -1;
/** The place object that we currently occupy. */
protected PlaceObject _plobj;
/** The place controller in effect for our current place. */
protected PlaceController _controller;
/**
* The oid of the place for which we have an outstanding moveTo
* request, or -1 if we have no outstanding request.
*/
protected int _pendingPlaceId = -1;
/** The oid of the place we previously occupied. */
protected int _previousPlaceId = -1;
/** The entity that deals when we fail to subscribe to a place
* object. */
protected FailureHandler _failureHandler;
}