diff --git a/src/as/com/threerings/crowd/chat/client/ChatDirector.as b/src/as/com/threerings/crowd/chat/client/ChatDirector.as index 676fbcec4..56bb3dea1 100644 --- a/src/as/com/threerings/crowd/chat/client/ChatDirector.as +++ b/src/as/com/threerings/crowd/chat/client/ChatDirector.as @@ -24,6 +24,7 @@ package com.threerings.crowd.chat.client { import mx.collections.ArrayCollection; import com.threerings.util.ArrayUtil; +import com.threerings.util.ResultListener; import com.threerings.util.SimpleMap; import com.threerings.presents.client.BasicDirector; diff --git a/src/as/com/threerings/crowd/chat/data/SpeakMarshaller.as b/src/as/com/threerings/crowd/chat/data/SpeakMarshaller.as new file mode 100644 index 000000000..d1fa3a4bd --- /dev/null +++ b/src/as/com/threerings/crowd/chat/data/SpeakMarshaller.as @@ -0,0 +1,50 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.crowd.chat.data { + +import com.threerings.util.Byte; + +import com.threerings.crowd.chat.client.SpeakService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; + +/** + * Provides the implementation of the {@link SpeakService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class SpeakMarshaller extends InvocationMarshaller + implements SpeakService +{ + /** The method id used to dispatch {@link #speak} requests. */ + public static const SPEAK :int = 1; + + // documentation inherited from interface + public function speak (arg1 :Client, arg2 :String, arg3 :int) :void + { + sendRequest(arg1, SPEAK, [ arg2, new Byte(arg3) ]); + } +} +} diff --git a/src/as/com/threerings/crowd/client/LocationDirector.as b/src/as/com/threerings/crowd/client/LocationDirector.as new file mode 100644 index 000000000..5d00dac4a --- /dev/null +++ b/src/as/com/threerings/crowd/client/LocationDirector.as @@ -0,0 +1,642 @@ +// +// $Id: LocationDirector.java 3638 2005-06-30 00:02:13Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.crowd.client { + +// TODO: class is in progress of conversion + +import com.threerings.util.ObserverList; +import com.threerings.util.ResultListener; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.ObjectAccessError; +import com.threerings.presents.dobj.Subscriber; + +import com.threerings.crowd.Log; +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.LocationCodes; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +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 extends BasicDirector + implements LocationCodes, Subscriber, LocationReceiver, + LocationService.MoveListener +{ + /** + * 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 function LocationDirector (ctx :CrowdContext) + { + super(ctx); + + // keep this around for later + _cctx = ctx; + + // register for location notifications + _cctx.getClient().getInvocationDirector().registerReceiver( + new LocationDecoder(this)); + } + + /** + * Adds a location observer to the list. This observer will + * subsequently be notified of potential, effected and failed location + * changes. + */ + public function addLocationObserver (observer :LocationObserver) :void + { + _observers.add(observer); + } + + /** + * Removes a location observer from the list. + */ + public function removeLocationObserver (observer :LocationObserver) :void + { + _observers.remove(observer); + } + + /** + * Returns the place object for the location we currently occupy or + * null if we're not currently occupying any location. + */ + public function getPlaceObject () :PlaceObject + { + return _plobj; + } + + /** + * Returns true if there is a pending move request. + */ + public function movePending () :Boolean + { + return (_pendingPlaceId > 0); + } + + /** + * 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. + * + * @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 (placeId :int) :Boolean + { + // make sure the placeId is valid + if (placeId < 0) { + Log.warning("Refusing moveTo(): invalid placeId " + placeId + "."); + return false; + } + + // first check to see if our observers are happy with this move + // request + if (!mayMoveTo(placeId, null)) { + 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 = checkRepeatMove(); + + // complain if we're over-writing a pending request + if (_pendingPlaceId != -1) { + // if the pending request has been outstanding more than a + // minute, go ahead and let this new one through in an attempt + // to recover from dropped moveTo requests + if (refuse) { + Log.warning("Refusing moveTo; We have a request outstanding " + + "[ppid=" + _pendingPlaceId + + ", npid=" + placeId + "]."); + return false; + + } else { + Log.warning("Overriding stale moveTo request " + + "[ppid=" + _pendingPlaceId + + ", npid=" + placeId + "]."); + } + } + + // make a note of our pending place id + _pendingPlaceId = placeId; + + // issue a moveTo request + Log.info("Issuing moveTo(" + placeId + ")."); + _lservice.moveTo(_cctx.getClient(), placeId, this); + return true; + } + + /** + * 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 function moveBack () :Boolean + { + if (_previousPlaceId == -1) { + return false; + + } else { + moveTo(_previousPlaceId); + return true; + } + } + + /** + * Issues a request to leave our current location. + * + * @return true if we were able to leave, false if we are in the + * middle of moving somewhere and can't yet leave. + */ + public function leavePlace () :Boolean + { + if (_pendingPlaceId != -1) { + return false; + } + + _lservice.leavePlace(_cctx.getClient()); + didLeavePlace(); + 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 function mayMoveTo (placeId :int, rl :ResultListener) :Boolean + { + var vetoed :Boolean = false; + _observers.apply(function (obs :Object) :Boolean { + var lobs :LocationObserver = (obs as LocationObserver); + vetoed = vetoed || !lobs.locationMayChange(placeId); + return true; + }); + + // if we're actually going somewhere, let the controller know that + // we might be leaving + mayLeavePlace(); + + // if we have a result listener, let it know if we failed + // or keep it for later if we're still going + if (rl != null) { + if (vetoed) { + rl.requestFailed(new MoveVetoedException()); + } else { + _moveListener = rl; + } + } + // and return the result + return !vetoed; + } + + /** + * Called to inform our controller that we may be leaving the current + * place. + */ + protected void mayLeavePlace () + { + if (_controller != null) { + try { + _controller.mayLeavePlace(_plobj); + } catch (Exception e) { + Log.warning("Place controller choked in " + + "mayLeavePlace [plobj=" + _plobj + "]."); + Log.logStackTrace(e); + } + } + } + + /** + * 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) + { + if (_moveListener != null) { + _moveListener.requestCompleted(config); + _moveListener = null; + } + + // keep track of our previous place id + _previousPlaceId = _placeId; + + // clear out our last request time + _lastRequestTime = 0; + + // do some cleaning up in case we were previously in a place + didLeavePlace(); + + // make a note that we're now mostly in the new location + _placeId = placeId; + + // check whether we should use a custom class loader + ClassLoader loader = getClassLoader(config); + if (loader != null) { + config.setClassLoader(loader); + } + + Class cclass = config.getControllerClass(); + try { + // start up a new place controller to manage the new place + _controller = (PlaceController)cclass.newInstance(); + _controller.init(_cctx, 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 + _cctx.getDObjectManager().subscribeToObject(_placeId, this); + } + + /** + * Called when we're leaving our current location. Informs the + * location's controller that we're departing, unsubscribes from the + * location's place object, and clears out our internal place + * information. + */ + public void didLeavePlace () + { + 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 + _cctx.getDObjectManager().unsubscribeFromObject( + _plobj.getOid(), this); + _plobj = null; + + // and clear out the associated place id + _placeId = -1; + } + } + + /** + * 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) + { + if (_moveListener != null) { + _moveListener.requestFailed(new MoveFailedException(reason)); + _moveListener = null; + } + + // clear out our last request time + _lastRequestTime = 0; + + // let our observers know what's up + notifyFailure(placeId, reason); + } + + /** + * Called to test and set a time stamp that we use to determine if a + * pending moveTo request is stale. + */ + public boolean checkRepeatMove () + { + long now = System.currentTimeMillis(); + if (now - _lastRequestTime < STALE_REQUEST_DURATION) { + return true; + + } else { + _lastRequestTime = now; + return false; + } + } + + // documentation inherited from interface + public void clientDidLogon (Client client) + { + super.clientDidLogon(client); + + // subscribe to 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); + } + + // documentation inherited from interface + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + + // clear ourselves out and inform observers of our departure + mayLeavePlace(); + didLeavePlace(); + + // let our observers know that we're no longer in a location + _observers.apply(_didChangeOp); + + // clear out everything else (it's possible that we were logged + // off in the middle of a change location request) + _pendingPlaceId = -1; + _previousPlaceId = -1; + _lastRequestTime = 0L; + _lservice = null; + } + + // documentation inherited + protected void fetchServices (Client client) + { + // obtain our service handle + _lservice = (LocationService) + client.requireService(LocationService.class); + } + + 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 + } + + // documentation inherited from interface + public void moveSucceeded (PlaceConfig config) + { + // handle the successful move + didMoveTo(_pendingPlaceId, config); + + // and clear out the tracked pending oid + _pendingPlaceId = -1; + } + + // documentation inherited from interface + public void requestFailed (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); + } + + // documentation inherited from interface + public void forcedMove (final int placeId) + { + Log.info("Moving at request of server [placeId=" + placeId + "]."); + + if (movePending()) { + // clear out our old place information + mayLeavePlace(); + didLeavePlace(); + + // move to the new place + moveTo(placeId); + } + } + + /** + * 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 + _observers.apply(_didChangeOp); + } + + /** + * 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 (final int placeId, final String reason) + { + _observers.apply(new ObserverOp() { + public boolean apply (Object obs) { + ((LocationObserver)obs).locationChangeFailed(placeId, reason); + return true; + } + }); + } + + /** + * By overriding this method, it is possible to customize the location + * director to cause it to load the classes associated with a + * particular place via a custom class loader. That loader may enforce + * restricted privileges or obtain the classes from some special + * source. + */ + protected ClassLoader getClassLoader (PlaceConfig config) + { + return null; + } + + /** The context through which we access needed services. */ + protected CrowdContext _cctx; + + /** Provides access to location services. */ + protected LocationService _lservice; + + /** Our location observer list. */ + protected ObserverList _observers = + new ObserverList(ObserverList.SAFE_IN_ORDER_NOTIFY); + + /** 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 last time we requested a move to. */ + protected long _lastRequestTime; + + /** The entity that deals when we fail to subscribe to a place + * object. */ +// protected FailureHandler _failureHandler; + + /** A listener that wants to know if we succeeded or + * how we failed to move. */ + protected ResultListener _moveListener; + + /** The operation used to inform observers that the location changed. */ + protected ObserverOp _didChangeOp = new ObserverOp() { + public boolean apply (Object obs) { + ((LocationObserver)obs).locationDidChange(_plobj); + return true; + } + }; + + /** We require that a moveTo request be outstanding for one minute + * before it is declared to be stale. */ + protected static final long STALE_REQUEST_DURATION = 60L * 1000L; +} +} diff --git a/src/as/com/threerings/crowd/client/OccupantDirector.as b/src/as/com/threerings/crowd/client/OccupantDirector.as new file mode 100644 index 000000000..f51eaa555 --- /dev/null +++ b/src/as/com/threerings/crowd/client/OccupantDirector.as @@ -0,0 +1,209 @@ +// +// $Id: OccupantDirector.java 3406 2005-03-15 02:12:03Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.crowd.client { + +import com.threerings.util.Iterator; +import com.threerings.util.ObserverList; +import com.threerings.util.Name; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.ClientEvent; +import com.threerings.presents.dobj.EntryAddedEvent; +import com.threerings.presents.dobj.EntryRemovedEvent; +import com.threerings.presents.dobj.EntryUpdatedEvent; +import com.threerings.presents.dobj.SetListener; + +import com.threerings.crowd.Log; +import com.threerings.crowd.data.OccupantInfo; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +/** + * The occupant director listens for occupants of places to enter and + * exit, and dispatches notices to interested parties about these events. + * + *
It will eventually provide a framework for keeping track of + * occupant information in a network efficient manner. The idea being that + * we want to store as little information about occupants as possible in + * the place object (probably just body oid and username), but upon + * entering a place, this will be all we know about the occupants. We then + * dispatch a request to get information about all of the occupants in the + * room (things like avatar information for a graphical display or perhaps + * their ratings in the game that is associated with a place for a gaming + * site) which we then pass on to the occupant observers when it becomes + * available. + * + *
This information would be cached and we could return cached + * information for occupants for which we have cached info. We will + * probably want to still make a request for the occupant info so that we + * can update non-static occupant data rather than permanently using + * what's in the cache. + */ +public class OccupantDirector extends BasicDirector + implements LocationObserver, SetListener +{ + /** + * Constructs a new occupant director with the supplied context. + */ + public function OccupantDirector (ctx :CrowdContext) + { + super(ctx); + + // register ourselves as a location observer + ctx.getLocationDirector().addLocationObserver(this); + } + + /** + * Adds the specified occupant observer to the list. + */ + public function addOccupantObserver (obs :OccupantObserver) :void + { + _observers.add(obs); + } + + /** + * Removes the specified occupant observer from the list. + */ + public function removeOccupantObserver (obs :OccupantObserver) :void + { + _observers.remove(obs); + } + + /** + * Returns the occupant info for the user in question if it exists in + * the currently occupied place. Returns null if no occupant info + * exists for the specified body. + */ + public function getOccupantInfo (bodyOid :int) :OccupantInfo + { + // make sure we're somewhere + return (_place == null) ? null : + (_place.occupantInfo.get(bodyOid) as OccupantInfo); + } + + /** + * Returns the occupant info for the user in question if it exists in + * the currently occupied place. Returns null if no occupant info + * exists with the specified username. + */ + public function getOccupantInfoByName (username :Name) :OccupantInfo + { + return (_place == null) ? null : _place.getOccupantInfo(username); + } + + // documentation inherited + public override function clientDidLogoff (event :ClientEvent) :void + { + super.clientDidLogoff(event); + // clear things out + if (_place != null) { + _place.removeListener(this); + _place = null; + } + } + + // documentation inherited from interface LocationObserver + public function locationMayChange (placeId :int) :Boolean + { + // we've got no opinion + return true; + } + + // documentation inherited from interface LocationObserver + public function locationDidChange (place :PlaceObject) :void + { + // unlisten to the old place object if there was one + if (_place != null) { + _place.removeListener(this); + } + + // listen to the new one + _place = place; + if (_place != null) { + _place.addListener(this); + } + } + + // documentation inherited from interface LocationObserver + public function locationChangeFailed (placeId :int, reason :String) :void + { + // nothing to do here either + } + + // documentation inherited from interface SetListener + public function entryAdded (event :EntryAddedEvent) :void + { + // bail if this isn't for the OCCUPANT_INFO field + if (event.getName() != PlaceObject.OCCUPANT_INFO) { + return; + } + + // now let the occupant observers know what's up + var info :OccupantInfo = (event.getEntry() as OccupantInfo); + _observers.apply(function (obj :Object) :Boolean { + (obj as OccupantObserver).occupantEntered(info); + return true; + }); + } + + // documentation inherited from interface SetListener + public function entryUpdated (event :EntryUpdatedEvent) :void + { + // bail if this isn't for the OCCUPANT_INFO field + if (event.getName() != PlaceObject.OCCUPANT_INFO) { + return; + } + + // now let the occupant observers know what's up + var info :OccupantInfo = (event.getEntry() as OccupantInfo); + var oinfo :OccupantInfo = (event.getOldEntry() as OccupantInfo); + _observers.apply(function (obj :Object) :Boolean { + (obj as OccupantObserver).occupantUpdated(oinfo, info); + return true; + }); + } + + // documentation inherited from interface SetListener + public function entryRemoved (event :EntryRemovedEvent) :void + { + // bail if this isn't for the OCCUPANT_INFO field + if (event.getName() != PlaceObject.OCCUPANT_INFO) { + return; + } + + // let the occupant observers know what's up + var oinfo :OccupantInfo = (event.getOldEntry() as OccupantInfo); + _observers.apply(function (obj :Object) :Boolean { + (obj as OccupantObserver).occupantLeft(oinfo); + return true; + }); + } + + /** The occupant observers to keep abreast of occupant antics. */ + protected var _observers :ObserverList = + new ObserverList(ObserverList.SAFE_IN_ORDER_NOTIFY); + + /** The user's current location. */ + protected var _place :PlaceObject; +} +} diff --git a/src/as/com/threerings/crowd/client/OccupantObserver.as b/src/as/com/threerings/crowd/client/OccupantObserver.as new file mode 100644 index 000000000..5e7ce17c9 --- /dev/null +++ b/src/as/com/threerings/crowd/client/OccupantObserver.as @@ -0,0 +1,52 @@ +// +// $Id: OccupantObserver.java 3098 2004-08-27 02:12:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.crowd.client { + +import com.threerings.crowd.data.OccupantInfo; + +/** + * An entity that is interested in hearing about bodies that enter and + * leave a location (as well as disconnect and reconnect) can implement + * this interface and register itself with the {@link OccupantDirector}. + */ +public interface OccupantObserver +{ + /** + * Called when a body enters the place. + */ + function occupantEntered (info :OccupantInfo) :void; + + /** + * Called when a body leaves the place. + */ + function occupantLeft (info :OccupantInfo) :void; + + /** + * Called when an occupant is updated. + * + * @param oldinfo the occupant info prior to the update. + * @param newinfo the newly update info record. + */ + function occupantUpdated ( + oldinfo :OccupantInfo, newinfo :OccupantInfo) :void; +} +} diff --git a/src/as/com/threerings/crowd/client/PlaceView.as b/src/as/com/threerings/crowd/client/PlaceView.as new file mode 100644 index 000000000..1b486b815 --- /dev/null +++ b/src/as/com/threerings/crowd/client/PlaceView.as @@ -0,0 +1,77 @@ +// +// $Id: PlaceView.java 3098 2004-08-27 02:12:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.crowd.client { + +import com.threerings.crowd.data.PlaceObject; + +/** + * This interface provides a convenient means for decoupling user + * interface components that interact with a place object and that need to + * keep themselves up to date when the client moves from place to place. + * + *
In general, such components need to know when the client is about + * to enter a place so that they can subscribe if necessary or at least + * extract information about the place. They also need to know when a + * client has left a place so that they can unsubscribe and clean up after + * themselves. This is the information that the place view interface makes + * available to them in a decoupled way. + * + *
The part of the client implementation that is responsible for the
+ * main user interface can act as a location observer, and it can make use
+ * of {@link PlaceViewUtil} to dispatch notification of place changes to
+ * every PlaceView implementing user interface element in the
+ * user interface hierarchy with calls to {@link
+ * PlaceViewUtil#dispatchWillEnterPlace} and {@link
+ * PlaceViewUtil#dispatchDidLeavePlace}. These functions traverse the UI
+ * hierarchy (starting with the element provided which would generally be
+ * the top-level UI element, and dispatch calls to {@link #willEnterPlace}
+ * and {@link #didLeavePlace} respectively on any UI element they find
+ * that implements PlaceView.
+ *
+ *
By doing this, the client code can simply create place-sensitive + * user interface elements and stick them in the user interface and + * essentially forget about them, knowing that they will all be notified + * of place entering and exiting by virtue of the single dispatching + * calls. It is useful to note that place-sensitive user interface + * elements will also generally need a reference to the {@link + * com.threerings.crowd.util.CrowdContext} derivative in use by + * the client, but those are best supplied at construct time. + */ +public interface PlaceView +{ + /** + * Called when the client has entered a place and is about to display + * the user interface for that place. + * + * @param plobj the place object that was just entered. + */ + function willEnterPlace (plobj :PlaceObject) :void; + + /** + * Called after the client has left a place and needs to clean up + * after the user interface that was displaying that place. + * + * @param plobj the place object that was just left. + */ + function didLeavePlace (plobj :PlaceObject) :void; +} +} diff --git a/src/as/com/threerings/presents/client/TestClient.as b/src/as/com/threerings/presents/client/TestClient.as index c0cda5d08..6eaacca27 100644 --- a/src/as/com/threerings/presents/client/TestClient.as +++ b/src/as/com/threerings/presents/client/TestClient.as @@ -146,6 +146,11 @@ public class TestClient extends Client ": " + arguments); } + public function crazyArgs (num :int = 0, str :String = "lol") :void + { + Log.debug("crazyArgs " + num + ", " + str); + } + prototype var _foo :String; protected var _savedFunc :Function; diff --git a/src/as/com/threerings/util/ArrayUtil.as b/src/as/com/threerings/util/ArrayUtil.as index f7e84e2aa..6d0a01944 100644 --- a/src/as/com/threerings/util/ArrayUtil.as +++ b/src/as/com/threerings/util/ArrayUtil.as @@ -5,6 +5,19 @@ package com.threerings.util { */ public class ArrayUtil { + /** + * Return the first index of the specified element, or -1 if not present. + */ + public static function indexOf (arr :Array, element :Object) :int + { + for (var ii :int = 0; ii < arr.length; ii++) { + if (arr[ii] === element) { + return ii; + } + } + return -1; + } + /** * Remove the first instance of the specified element from the array. */ diff --git a/src/as/com/threerings/util/MessageBundle.as b/src/as/com/threerings/util/MessageBundle.as index 54acc4ec6..867afc055 100644 --- a/src/as/com/threerings/util/MessageBundle.as +++ b/src/as/com/threerings/util/MessageBundle.as @@ -384,3 +384,8 @@ public class MessageBundle protected static const QUAL_SEP :String = ":"; } } + +// TODO: figure out how this is all actually going to even work +class ResourceBundle +{ +} diff --git a/src/as/com/threerings/util/ObserverList.as b/src/as/com/threerings/util/ObserverList.as new file mode 100644 index 000000000..f685e507a --- /dev/null +++ b/src/as/com/threerings/util/ObserverList.as @@ -0,0 +1,78 @@ +package com.threerings.util { + +public class ObserverList +{ + /** A notification ordering policy indicating that the observers + * should be notified in the order they were added and that the + * notification should be done on a snapshot of the array. */ + public static const SAFE_IN_ORDER_NOTIFY :int = 1; + + /** A notification ordering policy wherein the observers are notified + * last to first so that they can be removed during the notification + * process and new observers added will not inadvertently be notified + * as well, but no copy of the observer list need be made. This will + * not work if observers are added or removed from arbitrary positions + * in the list during a notification call. */ + public static const FAST_UNSAFE_NOTIFY :int = 2; + + /** + * Constructor + */ + public function ObserverList (notifyPolicy :int = 2) + { + _notifyPolicy = notifyPolicy; + } + + /** + * Add an observer to this list. + */ + public function add (observer :Object) :void + { + if (ArrayUtil.indexOf(_list, observer) == -1) { + _list.push(observer); + } + } + + /** + * Remove an observer from this list. + */ + public function remove (observer :Object) :void + { + ArrayUtil.removeFirst(_list, observer); + } + + /** + * Apply some operation to all observers. + * The function to be passed in should expect one argument and return a + * Boolean. + * function (observer :Object) :Boolean. + * The function should return false if the observer should be removed + * from the list. + */ + public function apply (func :Function) :void + { + var list :Array = _list; + if (_notifyPolicy == SAFE_IN_ORDER_NOTIFY) { + list = list.concat(); // make a duplicate + list.reverse(); // reverse it so that we start with earlier obs + } + for (var ii :int = list.length-1; ii >= 0; ii--) { + try { + var result :Boolean = func(list[ii]); + if (!result) { + // remove it if directed to do so + remove(list[ii]); + } + } catch (err :Error) { + Log.warning("ObserverOp choked during notification."); + } + } + } + + /** Our notification policy. */ + protected var _notifyPolicy :int; + + /** The actual list of observers. */ + protected var _list :Array = new Array(); +} +} diff --git a/src/as/com/threerings/util/ResultListener.as b/src/as/com/threerings/util/ResultListener.as new file mode 100644 index 000000000..e561feb3f --- /dev/null +++ b/src/as/com/threerings/util/ResultListener.as @@ -0,0 +1,9 @@ +package com.threerings.util { + +public interface ResultListener +{ + function requestCompleted (obj :Object) :void; + + function requestFailed (cause :Error) :void; +} +}