diff --git a/src/as/com/threerings/README.txt b/src/as/com/threerings/README.txt
index 8ead7c4a1..33a135ea2 100644
--- a/src/as/com/threerings/README.txt
+++ b/src/as/com/threerings/README.txt
@@ -11,6 +11,8 @@ Design decisions
- I am embracing flash's event distribution model because it saved me a bunch
of work.
+- We could use the setter methods on DObject properties to generate dobj
+ events, but so far I haven't gone there.
Notes
diff --git a/src/as/com/threerings/io/ObjectInputStream.as b/src/as/com/threerings/io/ObjectInputStream.as
index 95de8f07d..14afc9d8d 100644
--- a/src/as/com/threerings/io/ObjectInputStream.as
+++ b/src/as/com/threerings/io/ObjectInputStream.as
@@ -63,7 +63,7 @@ public class ObjectInputStream
var target :*;
if (cmap.streamer === null) {
- var clazz :Class = flash.util.getClassByName(cmap.cname);
+ var clazz :Class = flash.util.getClassByName(cmap.classname);
target = new clazz();
} else {
@@ -87,8 +87,7 @@ public class ObjectInputStream
{
// streamable objects
if (streamer == null) {
- var sable :Streamable = (obj as Streamable);
- sable.readObject(this);
+ obj.readObject(this); // obj is a Streamable.
return;
}
@@ -117,7 +116,7 @@ public class ObjectInputStream
public function defaultReadObject () :void
//throws IOError
{
- _streamer.readObject(_current, this, false);
+ _streamer.readObject(_current, this);
}
public function readBoolean () :Boolean
diff --git a/src/as/com/threerings/io/ObjectOutputStream.as b/src/as/com/threerings/io/ObjectOutputStream.as
index 2a030d78c..08150c476 100644
--- a/src/as/com/threerings/io/ObjectOutputStream.as
+++ b/src/as/com/threerings/io/ObjectOutputStream.as
@@ -29,6 +29,7 @@ public class ObjectOutputStream
// create a class mapping if we've not got one
if (cmap === undefined) {
var streamer :Streamer = Streamer.getStreamer(obj);
+ // streamer may be null to indicate a Streamable object
if (streamer === undefined) {
// TODO
trace("OMG, cannot stream ", cname);
@@ -60,8 +61,7 @@ public class ObjectOutputStream
{
// if it's Streamable, it goes straight through
if (streamer == null) {
- var sable :Streamable = (obj as Streamable);
- sable.writeObject(this);
+ obj.writeObject(this); // obj is a Streamable
return;
}
@@ -101,7 +101,7 @@ public class ObjectOutputStream
}
// write the instance data
- _streamer.writeObject(_current, this, false)
+ _streamer.writeObject(_current, this);
}
public function writeBoolean (value :Boolean) :void
diff --git a/src/as/com/threerings/io/Streamer.as b/src/as/com/threerings/io/Streamer.as
index 5237f530c..9ca0a0b4b 100644
--- a/src/as/com/threerings/io/Streamer.as
+++ b/src/as/com/threerings/io/Streamer.as
@@ -40,11 +40,9 @@ public class Streamer
createStreamers();
}
- var s :Streamer;
for (var ii :int = 0; ii < _streamers.lenght; ii++) {
- s = (_streamers[ii] as Streamer);
- if (s.isStreamerFor(obj)) {
- return s;
+ if (_streamers[ii].isStreamerFor(obj)) {
+ return _streamers[ii];
}
}
@@ -57,11 +55,9 @@ public class Streamer
createStreamers();
}
- var s :Streamer;
for (var ii :int = 0; ii < _streamers.length; ii++) {
- s = (_streamers[ii] as Streamer);
- if (s.getJavaClassName() === jname) {
- return s;
+ if (_streamers[ii].getJavaClassName() === jname) {
+ return _streamers[ii];
}
}
diff --git a/src/as/com/threerings/presents/client/Communicator.as b/src/as/com/threerings/presents/client/Communicator.as
index 6df5253e3..667b216af 100644
--- a/src/as/com/threerings/presents/client/Communicator.as
+++ b/src/as/com/threerings/presents/client/Communicator.as
@@ -109,8 +109,12 @@ public class Communicator
protected function inputFrameReceived (event :FrameAvailableEvent) :void
{
// convert the frame data into a message from the server
- _inStream.setSource(event.getFrameData());
+ var frameData :ByteArray = event.getFrameData();
+ _inStream.setSource(frameData);
var msg :DownstreamMessage = _inStream.readObject();
+ if (frameData.bytesAvailable > 0) {
+ trace("OMG, we didn't fully read the frame. Surely there's a bug");
+ }
if (_omgr != null) {
// if we're logged on, then just do the normal thing
@@ -119,7 +123,7 @@ public class Communicator
}
// Otherwise, this would be the AuthResponse to our logon attempt.
- var rsp :AuthResponse = (msg as AuthResponse); // TODO: as correct?
+ var rsp :AuthResponse = (msg as AuthResponse);
var data :AuthResponseData = rsp.getData();
if (data.code !== AuthResponseData.SUCCESS) {
shutdown(new Error(data.code));
diff --git a/src/as/com/threerings/presents/client/InvocationDirector.as b/src/as/com/threerings/presents/client/InvocationDirector.as
new file mode 100644
index 000000000..ec60dcf95
--- /dev/null
+++ b/src/as/com/threerings/presents/client/InvocationDirector.as
@@ -0,0 +1,33 @@
+package com.threerings.presents.client {
+
+import com.threerings.presents.dobj.DObjectManager;
+
+public class InvocationDirector
+{
+ public function init (omgr :DObjectManager, cloid :int, client :Client) :void
+ {
+ if (_clobj != null) {
+ trace("Zoiks, client object around during invmgr init!");
+ cleanup();
+ }
+
+ _omgr = omgr;
+ _client = client;
+
+ // TODO : lots more
+ }
+
+ public function cleanup () :void
+ {
+ _clobj = null;
+
+ // TODO: lots more
+ }
+
+ protected var _omgr :DObjectManager;
+
+ protected var _client :Client;
+
+ protected var _clobj :ClientObject;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/AttributeChangedEvent.as b/src/as/com/threerings/presents/dobj/AttributeChangedEvent.as
new file mode 100644
index 000000000..f8b230abc
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/AttributeChangedEvent.as
@@ -0,0 +1,89 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+/**
+ * An attribute changed event is dispatched when a single attribute of a
+ * distributed object has changed. It can also be constructed to request
+ * an attribute change on an object and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class AttributeChangedEvent extends NamedEvent
+{
+ /**
+ * Returns the new value of the attribute.
+ */
+ public function getValue () :*
+ {
+ return _value;
+ }
+
+ /**
+ * Returns the value of the attribute prior to the application of this
+ * event.
+ */
+ public function getOldValue () :*
+ {
+ return _oldValue;
+ }
+
+ /**
+ * Applies this attribute change to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ // if we have no old value, that means we're not running on the
+ // master server and we have not already applied this attribute
+ // change to the object, so we must grab the previous value and
+ // actually apply the attribute change
+ if (_oldValue === UNSET_OLD_ENTRY) {
+ _oldValue = target[_name]; // fucking wack-ass language
+ }
+ target[_name] = _value;
+ return true;
+ }
+
+ /**
+ * Constructs a new attribute changed event on the specified target
+ * object with the supplied attribute name and value. Do not
+ * construct these objects by hand. Use {@link
+ * DObject#changeAttribute} instead.
+ *
+ * @param targetOid the object id of the object whose attribute has
+ * changed.
+ * @param name the name of the attribute (data member) that has
+ * changed.
+ * @param value the new value of the attribute (in the case of
+ * primitive types, the reflection-defined object-alternative is
+ * used).
+ */
+ protected function AttributeChangedEvent (
+ targetOid :int, name :String, value :*, oldValue :*)
+ {
+ super(targetOid, name);
+ _value = value;
+ _oldValue = oldValue;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is AttributeChangeListener) {
+ listener.attributeChanged(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder) :void
+ {
+ buf.append("CHANGE:");
+ super.toString(buf);
+ buf.append(", value=", _value);
+ }
+
+ protected var _value :*;
+ protected var _oldValue :* = UNSET_OLD_ENTRY;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/ChangeListener.as b/src/as/com/threerings/presents/dobj/ChangeListener.as
new file mode 100644
index 000000000..5511aca3c
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/ChangeListener.as
@@ -0,0 +1,14 @@
+package com.threerings.presents.dobj {
+
+/**
+ * The various listener interfaces (e.g. {@link EventListener}, {@link
+ * AttributeChangeListener}, etc.) all extend this base interface so that
+ * the distributed object can check to make sure when an object is adding
+ * itself as a listener of some sort that it actually implements at least
+ * one of the listener interfaces. Thus, all listener interfaces must
+ * extend this one.
+ */
+public interface ChangeListener {
+ // nada
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/DEvent.as b/src/as/com/threerings/presents/dobj/DEvent.as
index e5b10e304..c4e81b6aa 100644
--- a/src/as/com/threerings/presents/dobj/DEvent.as
+++ b/src/as/com/threerings/presents/dobj/DEvent.as
@@ -1,6 +1,13 @@
package com.threerings.presents.dobj {
-public class DEvent extends Object
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamable;
+
+public class DEvent
+ implements Streamable
{
public function DEvent ()
{
@@ -11,12 +18,91 @@ public class DEvent extends Object
_toid = targetOid;
}
+ /**
+ * Returns the oid of the object that is the target of this event.
+ */
public function getTargetOid () :int
{
return _toid;
}
+
+ /**
+ * Some events are used only internally on the server and need not be
+ * broadcast to subscribers, proxy or otherwise. Such events can
+ * return true here and short-circuit the normal proxy event dispatch
+ * mechanism.
+ */
+ public function applyToObject (target :DObject) :void
+ {
+ trace("TODO: abstract methods?");
+ }
+
+ /**
+ * Events with associated listener interfaces should implement this
+ * function and notify the supplied listener if it implements their
+ * event listening interface. For example, the {@link
+ * AttributeChangedEvent} will notify listeners that implement {@link
+ * AttributeChangeListener}.
+ */
+ protected function notifyListener (listener :*) :void
+ {
+ // the default is to do nothing
+ }
+
+ /**
+ * Constructs and returns a string representation of this event.
+ */
+ public override function toString () :String
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append("[");
+ toString(buf);
+ buf.append("]");
+ return buf.toString();
+ }
+
+ // documentation inherited from interface Streamable
+ public function writeObject (out :ObjectOutputStream) :void
+ {
+ out.writeInt(_toid);
+ }
+
+ // documentation inherited from interface Streamable
+ public function readObject (ins :ObjectInputStream) :void
+ {
+ _toid = ins.readInt();
+ }
+
+ /**
+ * This should be overridden by derived classes (which should be sure
+ * to call super.toString()) to append the derived class
+ * specific event information to the string buffer.
+ */
+ protected function toString (buf :StringBuilder) :void
+ {
+ buf.append("targetOid=", _toid);
+ }
+
/** The oid of the object that is the target of this event. */
protected var _toid :int;
+
+ protected static const UNSET_OLD_ENTRY :DSetEntry = new DummyEntry();
}
}
+
+class DummyEntry implements DSetEntry
+{
+ public function getKey () :Comparable
+ {
+ return null;
+ }
+
+ public function writeObject (out :ObjectOutputStream) :void
+ {
+ }
+
+ public function readObject (ins :ObjectInputStream) :void
+ {
+ }
+}
diff --git a/src/as/com/threerings/presents/dobj/DObject.as b/src/as/com/threerings/presents/dobj/DObject.as
index dad3289d2..ac9583b53 100644
--- a/src/as/com/threerings/presents/dobj/DObject.as
+++ b/src/as/com/threerings/presents/dobj/DObject.as
@@ -2,11 +2,13 @@ package com.threerings.presents.dobj {
import flash.events.EventDispatcher;
+import mx.collections.ArrayCollection;
+
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamable;
-public class DObject extends EventDispatcher
+public class DObject // extends EventDispatcher
implements Streamable
{
public function getOid ():int
@@ -14,9 +16,152 @@ public class DObject extends EventDispatcher
return _oid;
}
+ public function getManager () :DObjectManager
+ {
+ return _omgr;
+ }
+
+ public function addListener (listener :ChangeListener) :void
+ {
+ if (_listeners == null) {
+ _listeners = new ArrayCollection();
+
+ } else if (_listeners.contains(listener)) {
+ trace("Refusing repeat listener registration");
+ return;
+ }
+ _listeners.addItem(listener);
+ }
+
+ public function removeListener (listener :ChangeListener) :void
+ {
+ if (_listeners != null) {
+ var dex :int = _listeners.getItemIndex(listener);
+ if (dex != -1) {
+ _listeners.removeItemAt(dex);
+ }
+ }
+ }
+
+ public function notifyListeners (event :DEvent) :void
+ {
+ if (_listeners == null) {
+ return;
+ }
+
+ for (var ii :int = 0; ii < _listeners.length; ii++) {
+ var listener :* = _listeners.getItemAt(ii);
+ try {
+ event.notifyListener(listener);
+
+ if (listener is EventListener) {
+ listener.eventReceived(event);
+ }
+ } catch (e :Error) {
+ trace("Listener choked during notification");
+ trace(e.getStackTrace());
+ }
+ }
+ }
+
+ public function postMessage (name :String, args :Array) :void
+ {
+ postEvent(new MessageEvent(_oid, name, args));
+ }
+
public function postEvent (DEvent event) :void
{
+ // TODO: transactons?
+ if (_omgr != null) {
+ _omgr.postEvent(event);
+ } else {
+ trace("Unable to post event, object has no omgr");
+ }
+ }
+
+ public function isActive () :Boolean
+ {
+ return (_omgr != null);
+ }
+
+ /**
+ * Don't call this function! It initializes this distributed object
+ * with the supplied distributed object manager. This is called by the
+ * distributed object manager when an object is created and registered
+ * with the system.
+ *
+ * @see DObjectManager#createObject
+ */
+ public function setManager (omgr :DObjectManager) :void
+ {
+ _omgr = omgr;
+ }
+
+ /**
+ * Called by derived instances when an attribute setter method was
+ * called.
+ */
+ protected function requestAttributeChange (
+ name :String, value :*, oldValue :*) :void
+ {
+ postEvent(new AttributeChangedEvent(_oid, name, value, oldValue));
+ }
+
+ /**
+ * Called by derived instances when an element updater method was
+ * called.
+ */
+ protected function requestElementUpdate (
+ name :String, index :int, value :*, oldValue :*) :void
+ {
+ // dispatch an attribute changed event
+ postEvent(new ElementUpdatedEvent(_oid, name, value, oldValue, index));
+ }
+
+ /**
+ * Calls by derived instances when an oid adder method was called.
+ */
+ protected function requestOidAdd (name :String, oid :int) :void
+ {
+ // dispatch an object added event
+ postEvent(new ObjectAddedEvent(_oid, name, oid));
+ }
+
+ /**
+ * Calls by derived instances when an oid remover method was called.
+ */
+ protected function requestOidRemove (name :String, oid :int) :void
+ {
+ // dispatch an object removed event
+ postEvent(new ObjectRemovedEvent(_oid, name, oid));
+ }
+
+ /**
+ * Calls by derived instances when a set adder method was called.
+ */
+ protected function requestEntryAdd (name :String, entry :DSet.Entry) :void
+ {
+ // dispatch an entry added event
+ postEvent(new EntryAddedEvent(_oid, name, entry, false));
+ }
+
+ /**
+ * Calls by derived instances when a set remover method was called.
+ */
+ protected function requestEntryRemove (name :String, key :Comparable) :void
+ {
+ // dispatch an entry removed event
+ postEvent(new EntryRemovedEvent(_oid, name, key, null));
+ }
+
+ /**
+ * Calls by derived instances when a set updater method was called.
+ */
+ protected function requestEntryUpdate (name :String, entry :DSet.Entry)
+ {
+ // dispatch an entry updated event
+ postEvent(new EntryUpdatedEvent(_oid, name, entry, null));
}
// documentation inherited from interface Streamable
@@ -32,5 +177,11 @@ public class DObject extends EventDispatcher
}
protected var _oid :int;
+
+ /** A reference to our object manager. */
+ protected var _omgr :DObjectManager;
+
+ /** Our event listeners. */
+ protected var _listeners :ArrayCollection;
}
}
diff --git a/src/as/com/threerings/presents/dobj/DObjectManager.as b/src/as/com/threerings/presents/dobj/DObjectManager.as
new file mode 100644
index 000000000..e3c873389
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/DObjectManager.as
@@ -0,0 +1,97 @@
+package com.threerings.presents.dobj {
+
+/**
+ * The distributed object manager is responsible for managing the creation
+ * and destruction of distributed objects and propagating dobj events to
+ * the appropriate subscribers. On the client, objects are managed as
+ * proxies to the real objects managed by the server, so attribute change
+ * requests are forwarded to the server and events coming down from the
+ * server are delivered to the local subscribers. On the server, the
+ * objects are managed directly.
+ */
+public interface DObjectManager
+{
+ /**
+ * Returns true if this distributed object manager is the
+ * authoritative manager for the specified distributed object, or fals
+ * if we are only providing a proxy to the object.
+ */
+ function isManager (obj :DObject) :Boolean;
+
+ /**
+ * Creates a distributed object instance of the supplied class and
+ * notifies the specified subscriber when it becomes available. This
+ * is the proper mechanism for constructing a new distributed object
+ * as it is the only way in which it can be properly registered with
+ * the dobj system.
+ *
+ * @param dclass The class object of the derived class of
+ * DObject (or DObject.class itself) that
+ * should be instantiated.
+ * @param target The subscriber to be notified when the object is
+ * created and available, or if there was a problem creating the
+ * object.
+ */
+ function createObject (dclass :Class, target :Subscriber) :void;
+
+ /**
+ * Requests that the specified subscriber be subscribed to the object
+ * identified by the supplied object id. That subscriber will be
+ * notified when the object is available or if the subscription
+ * request failed.
+ *
+ * @param oid The object id of the distributed object to which
+ * subscription is desired.
+ * @param target The subscriber to be subscribed.
+ *
+ * @see Subscriber#objectAvailable
+ * @see Subscriber#requestFailed
+ */
+ function subscribeToObject (oid :int, target :Subscriber) :void;
+
+ /**
+ * Requests that the specified subscriber be unsubscribed from the
+ * object identified by the supplied object id.
+ *
+ * @param oid The object id of the distributed object from which
+ * unsubscription is desired.
+ * @param target The subscriber to be unsubscribed.
+ */
+ function unsubscribeFromObject (oid :int, target :Subscriber) :void;
+
+ /**
+ * Requests that the specified object be destroyed. Once destroyed an
+ * object is removed from the runtime system and may no longer have
+ * events dispatched on it.
+ *
+ * @param oid The object id of the distributed object to be destroyed.
+ */
+ function destroyObject (oid :int) :void;
+
+ /**
+ * Posts a distributed object event into the system. Instead of
+ * requesting the modification of a distributed object attribute by
+ * calling the setter for that attribute on the object itself, an
+ * AttributeChangedEvent can be constructed and posted
+ * directly. This is true for all event types and is useful for
+ * situations where one doesn't have access to the object in question,
+ * but needs to affect some event.
+ *
+ *
This event will be forwarded to the ultimate manager of the + * object (on the client, this means it will be forwarded to the + * server) where it will be checked for validity and then applied to + * the object and dispatched to all its subscribers. + * + * @param event The event to be dispatched. + */ + function postEvent (event :DEvent) :void; + + /** + * When a distributed object removes its last subscriber, it will call + * this function to let the object manager know. The manager might + * then choose to flush this object from the system or unregister from + * some upstream manager whose object it was proxying, for example. + */ + function removedLastSubscriber (obj :DObject, deathWish :Boolean) :void; +} +} diff --git a/src/as/com/threerings/presents/dobj/DSet.as b/src/as/com/threerings/presents/dobj/DSet.as new file mode 100644 index 000000000..eebef0e3f --- /dev/null +++ b/src/as/com/threerings/presents/dobj/DSet.as @@ -0,0 +1,275 @@ +package com.threerings.presents.dobj { + +import flash.util.StringBuilder; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Streamable; + + +/** + * The distributed set class provides a means by which an unordered set of + * objects can be maintained as a distributed object field. Entries can be + * added to and removed from the set, requests for which will generate + * events much like other distributed object fields. + * + *
Classes that wish to act as set entries must implement the {@link
+ * Entry} interface which extends {@link Streamable} and adds the
+ * requirement that the object provide a key which will be used to
+ * identify entry equality. Thus an entry is declared to be in a set of
+ * the object returned by that entry's {@link Entry#getKey} method is
+ * equal (using {@link Object#equals}) to the entry returned by the {@link
+ * Entry#getKey} method of some other entry in the set. Additionally, in
+ * the case of entry removal, only the key for the entry to be removed
+ * will be transmitted with the removal event to save network
+ * bandwidth. Lastly, the object returned by {@link Entry#getKey} must be
+ * a {@link Streamable} type.
+ */
+public class DSet
+ implements Streamable
+{
+ /**
+ * Returns the number of entries in this set.
+ */
+ public function size () :int
+ {
+ return _entries.length;
+ }
+
+ /**
+ * Returns true if the set contains an entry whose
+ * getKey() method returns a key that
+ * equals() the key returned by getKey() of
+ * the supplied entry. Returns false otherwise.
+ */
+ public function contains (elem :DSetEntry) :Boolean
+ {
+ return containsKey(elem.getKey());
+ }
+
+ /**
+ * Returns true if an entry in the set has a key that
+ * equals() the supplied key. Returns false otherwise.
+ */
+ public function containsKey (key :Comparable) :Boolean
+ {
+ return getByKey(key) != null;
+ }
+
+ /**
+ * Returns the entry that matches (getKey().equals(key))
+ * the specified key or null if no entry could be found that matches
+ * the key.
+ */
+ public function getByKey (key :Comparable) :DSetEntry
+ {
+ // o(n) for now
+ // TODO
+ for (var ii :int = 0; ii < _entries.length; ii++) {
+ var entry :DSetEntry = _entries[ii];
+ if (key.compareTo(entry.getKey()) == 0) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns an iterator over the entries of this set. It does not
+ * support modification (nor iteration while modifications are being
+ * made to the set). It should not be kept around as it can quickly
+ * become out of date.
+ *
+ * @deprecated
+ */
+// public Iterator entries ()
+// {
+// return iterator();
+// }
+
+ /**
+ * Returns an iterator over the entries of this set. It does not
+ * support modification (nor iteration while modifications are being
+ * made to the set). It should not be kept around as it can quickly
+ * become out of date.
+ */
+// public Iterator iterator ()
+// {
+// // the crazy sanity checks
+// if (_size < 0 ||_size > _entries.length ||
+// (_size > 0 && _entries[_size-1] == null)) {
+// Log.warning("DSet in a bad way [size=" + _size +
+// ", entries=" + StringUtil.toString(_entries) + "].");
+// Thread.dumpStack();
+// }
+//
+// return new Iterator() {
+// public boolean hasNext () {
+// checkComodification();
+// return (_index < _size);
+// }
+// public Object next () {
+// checkComodification();
+// return _entries[_index++];
+// }
+// public void remove () {
+// throw new UnsupportedOperationException();
+// }
+// protected void checkComodification () {
+// if (_modCount != _expectedModCount) {
+// throw new ConcurrentModificationException();
+// }
+// if (_ssize != _size) {
+// Log.warning("Size changed during iteration " +
+// "[ssize=" + _ssize + ", nsize=" + _size +
+// ", entsries=" + StringUtil.toString(_entries) +
+// "].");
+// Thread.dumpStack();
+// }
+// }
+// protected int _index = 0;
+// protected int _ssize = _size;
+// protected int _expectedModCount = _modCount;
+// };
+// }
+
+ /**
+ * Copies the elements of this distributed set into the supplied
+ * array. If the array is not large enough to hold all of the
+ * elements, as many as fit into the array will be copied. If the
+ * array argument is null, an object array of sufficient
+ * size to contain all of the elements of this set will be created and
+ * returned.
+ */
+ public function toArray () :Array
+ {
+ return _entries.concat(null); // makes a copy
+ }
+
+ /**
+ * Adds the specified entry to the set. This should not be called
+ * directly, instead the associated addTo{Set}() method
+ * should be called on the distributed object that contains the set in
+ * question.
+ *
+ * @return true if the entry was added, false if it was already in
+ * the set.
+ */
+ protected function add (elem :DSetEntry) :Boolean
+ {
+ if (contains(elem)) {
+ trace("Refusing to add duplicate entry [set=" + this +
+ ", entry=" + elem + "].");
+ return false;
+ }
+
+ _entries.push(elem);
+ return true;
+ }
+
+ /**
+ * Removes the specified entry from the set. This should not be called
+ * directly, instead the associated removeFrom{Set}()
+ * method should be called on the distributed object that contains the
+ * set in question.
+ *
+ * @return true if the entry was removed, false if it was not in the
+ * set.
+ */
+ protected function remove (elem :DSetEntry) :Boolean
+ {
+ return (null != removeKey(elem.getKey()));
+ }
+
+ /**
+ * Removes from the set the entry whose key matches the supplied
+ * key. This should not be called directly, instead the associated
+ * removeFrom{Set}() method should be called on the
+ * distributed object that contains the set in question.
+ *
+ * @return the old matching entry if found and removed, null if not
+ * found.
+ */
+ protected function removeKey (key :Comparable) :DSetEntry
+ {
+ // o(n) for now
+ // TODO
+ for (var ii :int = 0; ii < _entries.length; ii++) {
+ var entry :DSetEntry = _entries[ii];
+ if (key.compareTo(entry.getKey()) == 0) {
+ _entries.splice(ii, 1);
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates the specified entry by locating an entry whose key matches
+ * the key of the supplied entry and overwriting it. This should not
+ * be called directly, instead the associated
+ * update{Set}() method should be called on the
+ * distributed object that contains the set in question.
+ *
+ * @return the old entry that was replaced, or null if it was not
+ * found (in which case nothing is updated).
+ */
+ protected function update (elem :DSetEntry) :DSetEntry
+ {
+ var key :Comparable = elem.getKey();
+ for (var ii :int = 0; ii < _entries.length; ii++) {
+ var entry :DSetEntry = _entries[ii];
+ if (key.compareTo(entry.getKey()) == 0) {
+ _entries[ii] = elem;
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Generates a string representation of this set instance.
+ */
+ public override function toString () :String
+ {
+ StringBuilder buf = new StringBuilder("(");
+ buf.append(_entries.toString());
+ buf.append(")");
+ return buf.toString();
+ }
+
+ // documentation inherited from interface Streamable
+ public function writeObject (out :ObjectOutputStream) :void
+ {
+ trace("TODO");
+ }
+
+ // documentation inherited from interface Streamable
+ public function readObject (ins :ObjectInputStream) :void
+ {
+ _entries = ins.readField(Array);
+ _entries.length = ins.readInt(); // then read the length and limit it
+ }
+
+ /** The entries of the set (in a sparse array). */
+ protected var _entries :Array;
+
+// /** Used to check for concurrent modification. */
+// protected transient int _modCount;
+//
+// /** The default capacity of a set instance. */
+// protected static final int INITIAL_CAPACITY = 2;
+//
+// /** Used for lookups and to keep the set contents sorted on
+// * insertions. */
+// protected static Comparator ENTRY_COMP = new Comparator() {
+// public int compare (Object o1, Object o2) {
+// Comparable c1 = (o1 instanceof Entry) ?
+// ((Entry)o1).getKey() : (Comparable)o1;
+// Comparable c2 = (o2 instanceof Entry) ?
+// ((Entry)o2).getKey() : (Comparable)o2;
+// return c1.compareTo(c2);
+// }
+// };
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/DSetEntry.as b/src/as/com/threerings/presents/dobj/DSetEntry.as
new file mode 100644
index 000000000..a5d00ac93
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/DSetEntry.as
@@ -0,0 +1,9 @@
+package com.threerings.presents.dobj {
+
+import com.threerings.io.Streamable;
+
+public interface DSetEntry extends Streamable
+{
+ function getKey () :Comparable;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/ElementUpdatedEvent.as b/src/as/com/threerings/presents/dobj/ElementUpdatedEvent.as
new file mode 100644
index 000000000..85d3e4983
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/ElementUpdatedEvent.as
@@ -0,0 +1,117 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+
+/**
+ * An element updated event is dispatched when an element of an array
+ * field in a distributed object is updated. It can also be constructed to
+ * request the update of an entry and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class ElementUpdatedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new element updated event on the specified target
+ * object with the supplied attribute name, element and index.
+ *
+ * @param targetOid the object id of the object whose attribute has
+ * changed.
+ * @param name the name of the attribute (data member) for which an
+ * element has changed.
+ * @param value the new value of the element (in the case of primitive
+ * types, the reflection-defined object-alternative is used).
+ * @param oldValue the previous value of the element (in the case of
+ * primitive types, the reflection-defined object-alternative is
+ * used).
+ * @param index the index in the array of the updated element.
+ */
+ public ElementUpdatedEvent (
+ targetOid :int, name :String, value :*, oldValue :*, index :int)
+ {
+ super(targetOid, name);
+ _value = value;
+ _oldValue = oldValue;
+ _index = index;
+ }
+
+ /**
+ * Returns the new value of the element.
+ */
+ public function getValue () :*
+ {
+ return _value;
+ }
+
+ /**
+ * Returns the value of the element prior to the application of this
+ * event.
+ */
+ public function getOldValue () :*
+ {
+ return _oldValue;
+ }
+
+ /**
+ * Returns the index of the element.
+ */
+ public function getIndex () :int
+ {
+ return _index;
+ }
+
+ /**
+ * Applies this element update to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ if (_oldValue === UNSET_OLD_ENTRY) {
+ _oldValue = target[_name][_index];
+ }
+ target[_name][_index] = _value;
+ return true;
+ }
+
+ // documentation inherited
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeField(_value);
+ out.writeInt(_index);
+ }
+
+ // documentation inherited
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _value = ins.readField(Object);
+ _index = ins.readInt();
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is ElementUpdateListener) {
+ listener.elementUpdated(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder) :void
+ {
+ buf.append("UPDATE:");
+ super.toString(buf);
+ buf.append(", value=", _value);
+ buf.append(", index=", _index);
+ }
+
+ protected var _value :*;
+ protected var _index :int;
+ protected var _oldValue :* = UNSET_OLD_ENTRY;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/EntryAddedEvent.as b/src/as/com/threerings/presents/dobj/EntryAddedEvent.as
new file mode 100644
index 000000000..0c7b5dbe4
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/EntryAddedEvent.as
@@ -0,0 +1,85 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * An entry added event is dispatched when an entry is added to a {@link
+ * DSet} attribute of a distributed entry. It can also be constructed to
+ * request the addition of an entry to a set and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class EntryAddedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new entry added event on the specified target object
+ * with the supplied set attribute name and entry to add.
+ *
+ * @param targetOid the object id of the object to whose set we will
+ * add an entry.
+ * @param name the name of the attribute to which to add the specified
+ * entry.
+ * @param entry the entry to add to the set attribute.
+ */
+ public function EntryAddedEvent (
+ targetOid :int, name :String, entry :DSetEntry)
+ {
+ super(targetOid, name);
+ _entry = entry;
+ }
+
+ /**
+ * Returns the entry that has been added.
+ */
+ public function getEntry () :DSetEntry
+ {
+ return _entry;
+ }
+
+ /**
+ * Applies this event to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ var added :Boolean = target[_name].add(_entry);
+ if (!added) {
+ trace("Duplicate entry found [event=" + this + "].");
+ }
+ return true;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is SetListener) {
+ listener.entryAdded(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (StringBuilder buf)
+ {
+ buf.append("ELADD:");
+ super.toString(buf);
+ buf.append(", entry=", _entry);
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeField(_entry);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _entry = ins.readField(DSetEntry);
+ }
+
+ protected var _entry :DSetEntry;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as b/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as
new file mode 100644
index 000000000..18a2b91b8
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as
@@ -0,0 +1,105 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+
+/**
+ * An entry removed event is dispatched when an entry is removed from a
+ * {@link DSet} attribute of a distributed object. It can also be
+ * constructed to request the removal of an entry from a set and posted to
+ * the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class EntryRemovedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new entry removed event on the specified target object
+ * with the supplied set attribute name and entry key to remove.
+ *
+ * @param targetOid the object id of the object from whose set we will
+ * remove an entry.
+ * @param name the name of the attribute from which to remove the
+ * specified entry.
+ * @param key the entry key that identifies the entry to remove.
+ * @param oldEntry the previous value of the entry.
+ */
+ public function EntryRemovedEvent (
+ targetOid :int, name :String, key :Comparable, oldEntry :DSetEntry)
+ {
+ super(targetOid, name);
+ _key = key;
+ _oldEntry = oldEntry;
+ }
+
+ /**
+ * Returns the key that identifies the entry that has been removed.
+ */
+ public function getKey () :Comparable
+ {
+ return _key;
+ }
+
+ /**
+ * Returns the entry that was in the set prior to being updated.
+ */
+ public function getOldEntry () :DSetEntry
+ {
+ return _oldEntry;
+ }
+
+ /**
+ * Applies this event to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ if (_oldEntry == UNSET_OLD_ENTRY) {
+ var dset :DSet = target[_name];
+ // remove, fetch the previous value for interested callers
+ _oldEntry = dset.removeKey(_key);
+ if (_oldEntry == null) {
+ // complain if there was actually nothing there
+ trace("No matching entry to remove [key=" + _key +
+ ", set=" + set + "].");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is SetListener) {
+ listener.entryRemoved(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder)
+ {
+ buf.append("ELREM:");
+ super.toString(buf);
+ buf.append(", key=", _key);
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeField(_key);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _key = ins.readField(Comparable);
+ }
+
+ protected var _key :Comparable;
+ protected var _oldEntry :DSetEntry = UNSET_OLD_ENTRY;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/EntryUpdatedEvent.as b/src/as/com/threerings/presents/dobj/EntryUpdatedEvent.as
new file mode 100644
index 000000000..d991ebd43
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/EntryUpdatedEvent.as
@@ -0,0 +1,104 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * An entry updated event is dispatched when an entry of a {@link DSet} is
+ * updated. It can also be constructed to request the update of an entry
+ * and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class EntryUpdatedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new entry updated event on the specified target object
+ * for the specified set name and with the supplied updated entry.
+ *
+ * @param targetOid the object id of the object to whose set we will
+ * add an entry.
+ * @param name the name of the attribute in which to update the
+ * specified entry.
+ * @param entry the entry to update.
+ * @param oldEntry the previous value of the entry.
+ */
+ public function EntryUpdatedEvent (
+ targetOid :int, name :String, entry :DSetEntry, oldEntry :DSetEntry)
+ {
+ super(targetOid, name);
+ _entry = entry;
+ _oldEntry = oldEntry;
+ }
+
+ /**
+ * Returns the entry that has been updated.
+ */
+ public function getEntry () :DSetEntry
+ {
+ return _entry;
+ }
+
+ /**
+ * Returns the entry that was in the set prior to being updated.
+ */
+ public function getOldEntry ():DSetEntry
+ {
+ return _oldEntry;
+ }
+
+ /**
+ * Applies this event to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ // only apply the change if we haven't already
+ if (_oldEntry == UNSET_OLD_ENTRY) {
+ var dset :DSet = target[_name];
+ // fetch the previous value for interested callers
+ _oldEntry = dset.update(_entry);
+ if (_oldEntry == null) {
+ // complain if we didn't update anything
+ trace("No matching entry to update [entry=" + this +
+ ", set=" + set + "].");
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is SetListener) {
+ listener.entryUpdated(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder) :void
+ {
+ buf.append("ELUPD:");
+ super.toString(buf);
+ buf.append(", entry=", _entry);
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeField(_entry);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _entry = ins.readField(DSetEntry);
+ }
+
+ protected var _entry :DSetEntry;
+ protected var _oldEntry :DSetEntry = UNSET_OLD_ENTRY;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/MessageEvent.as b/src/as/com/threerings/presents/dobj/MessageEvent.as
new file mode 100644
index 000000000..83e0f46ea
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/MessageEvent.as
@@ -0,0 +1,81 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * A message event is used to dispatch a message to all subscribers of a
+ * distributed object without actually changing any of the fields of the
+ * object. A message has a name, by which different subscribers of the
+ * same object can distinguish their different messages, and an array of
+ * arguments by which any contents of the message can be delivered.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class MessageEvent extends NamedEvent
+{
+ /**
+ * Constructs a new message event on the specified target object with
+ * the supplied name and arguments.
+ *
+ * @param targetOid the object id of the object whose attribute has
+ * changed.
+ * @param name the name of the message event.
+ * @param args the arguments for this message. This array should
+ * contain only values of valid distributed object types.
+ */
+ public function MessageEvent (targetOid :int, name :String, args :Array)
+ {
+ super(targetOid, name);
+ _args = args;
+ }
+
+ /**
+ * Returns the arguments to this message.
+ */
+ public function getArgs () :Array
+ {
+ return _args;
+ }
+
+ /**
+ * Replaces the arguments associated with this message event.
+ * Note: this should only be called on events that have not
+ * yet been dispatched into the distributed object system.
+ */
+ public function setArgs (args :Array) :void
+ {
+ _args = args;
+ }
+
+ /**
+ * Applies this attribute change to the object.
+ */
+ public override function applyToObject (target :DObject) :Boolean
+ //throws ObjectAccessException
+ {
+ // nothing to do here
+ return true;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is MessageListener) {
+ listener.messageReceived(this);
+ }
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder) :void
+ {
+ buf.append("MSG:");
+ super.toString(buf);
+ buf.append(", args=", _args);
+ }
+
+ protected var _args :Array;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/NamedEvent.as b/src/as/com/threerings/presents/dobj/NamedEvent.as
new file mode 100644
index 000000000..290e77e9d
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/NamedEvent.as
@@ -0,0 +1,56 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * A common parent class for all events that are associated with a name
+ * (in some cases a field name, in other cases just an identifying name).
+ */
+public class NamedEvent extends DEvent
+{
+ /**
+ * Constructs a new named event for the specified target object with
+ * the supplied attribute name.
+ *
+ * @param targetOid the object id of the object in question.
+ * @param name the name associated with this event.
+ */
+ public NamedEvent (targetOid :int, name :String)
+ {
+ super(targetOid);
+ _name = name;
+ }
+
+ /**
+ * Returns the name of the attribute to which this event pertains.
+ */
+ public function getName () :String
+ {
+ return _name;
+ }
+
+ protected override function toString (buf :StringBuilder) :void
+ {
+ super.toString(buf);
+ buf.append(", name=", _name);
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeField(_name);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _name = ins.readField(String);
+ }
+
+ /** The name of the event. */
+ protected var _name :String;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/ObjectAddedEvent.as b/src/as/com/threerings/presents/dobj/ObjectAddedEvent.as
new file mode 100644
index 000000000..3552f20c5
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/ObjectAddedEvent.as
@@ -0,0 +1,83 @@
+package com.threerings.presents.dobj {
+
+import flash.util.StringBuilder;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * An object added event is dispatched when an object is added to an
+ * OidList attribute of a distributed object. It can also be
+ * constructed to request the addition of an oid to an
+ * OidList attribute of an object and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class ObjectAddedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new object added event on the specified target object
+ * with the supplied oid list attribute name and object id to add.
+ *
+ * @param targetOid the object id of the object to whose oid list we
+ * will add an oid.
+ * @param name the name of the attribute (data member) to which to add
+ * the specified oid.
+ * @param oid the oid to add to the oid list attribute.
+ */
+ public function ObjectAddedEvent (targetOid :int, name :String, oid :int)
+ {
+ super(targetOid, name);
+ _oid = oid;
+ }
+
+ /**
+ * Returns the oid that has been added.
+ */
+ public function getOid () :int
+ {
+ return _oid;
+ }
+
+ /**
+ * Applies this event to the object.
+ */
+ public override function applyToObject (target :DObject) :void
+ //throws ObjectAccessException
+ {
+ var list :OidList = target[_name];
+ list.add(_oid);
+ return true;
+ }
+
+ // documentation inherited
+ protected override function notifyListener (listener :*) :void
+ {
+ if (listener is OidListListener) {
+ listener.objectAdded(this);
+ }
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeInt(_oid);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ _oid = ins.readInt();
+ }
+
+ // documentation inherited
+ protected override function toString (buf :StringBuilder) :void
+ {
+ buf.append("OBJADD:");
+ super.toString(buf);
+ buf.append(", oid=", _oid);
+ }
+
+ protected var _oid :int;
+}
+}
diff --git a/src/as/com/threerings/presents/dobj/ObjectRemovedEvent.as b/src/as/com/threerings/presents/dobj/ObjectRemovedEvent.as
new file mode 100644
index 000000000..447862ee3
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/ObjectRemovedEvent.as
@@ -0,0 +1,95 @@
+//
+// $Id: ObjectRemovedEvent.java 3099 2004-08-27 02:21:06Z 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.presents.dobj;
+
+/**
+ * An object removed event is dispatched when an object is removed from an
+ * OidList attribute of a distributed object. It can also be
+ * constructed to request the removal of an oid from an
+ * OidList attribute of an object and posted to the dobjmgr.
+ *
+ * @see DObjectManager#postEvent
+ */
+public class ObjectRemovedEvent extends NamedEvent
+{
+ /**
+ * Constructs a new object removed event on the specified target
+ * object with the supplied oid list attribute name and object id to
+ * remove.
+ *
+ * @param targetOid the object id of the object from whose oid list we
+ * will remove an oid.
+ * @param name the name of the attribute (data member) from which to
+ * remove the specified oid.
+ * @param oid the oid to remove from the oid list attribute.
+ */
+ public ObjectRemovedEvent (int targetOid, String name, int oid)
+ {
+ super(targetOid, name);
+ _oid = oid;
+ }
+
+ /**
+ * Constructs a blank instance of this event in preparation for
+ * unserialization from the network.
+ */
+ public ObjectRemovedEvent ()
+ {
+ }
+
+ /**
+ * Returns the oid that has been removed.
+ */
+ public int getOid ()
+ {
+ return _oid;
+ }
+
+ /**
+ * Applies this event to the object.
+ */
+ public boolean applyToObject (DObject target)
+ throws ObjectAccessException
+ {
+ OidList list = (OidList)target.getAttribute(_name);
+ list.remove(_oid);
+ return true;
+ }
+
+ // documentation inherited
+ protected void notifyListener (Object listener)
+ {
+ if (listener is OidListListener) {
+ ((OidListListener)listener).objectRemoved(this);
+ }
+ }
+
+ // documentation inherited
+ protected void toString (StringBuffer buf)
+ {
+ buf.append("OBJREM:");
+ super.toString(buf);
+ buf.append(", oid=").append(_oid);
+ }
+
+ protected int _oid;
+}
diff --git a/src/as/com/threerings/presents/dobj/OidList.as b/src/as/com/threerings/presents/dobj/OidList.as
new file mode 100644
index 000000000..965e0c30a
--- /dev/null
+++ b/src/as/com/threerings/presents/dobj/OidList.as
@@ -0,0 +1,132 @@
+package com.threerings.presents.dobj {
+
+import com.threerings.io.Streamable;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * An oid list is used to store lists of object ids. The list will not
+ * allow duplicate ids. This class is not synchronized, with the
+ * expectation that all modifications of instances will take place on the
+ * dobjmgr thread.
+ */
+public class OidList
+ implements Streamable
+{
+ /**
+ * Creates an empty oidlist.
+ */
+ public function OidList ()
+ {
+ _oids = new Array();
+ }
+
+ /**
+ * Returns the number of object ids in the list.
+ */
+ public function size () :int
+ {
+ return _size;
+ }
+
+ /**
+ * Adds the specified object id to the list if it is not already
+ * there.
+ *
+ * @return true if the object was added, false if it was already in
+ * the list.
+ */
+ public function add (oid :int) :Boolean
+ {
+ // check for existence
+ for (int i = 0; i < _size; i++) {
+ if (_oids[i] == oid) {
+ return false;
+ }
+ }
+
+ // make room if necessary
+ if (_size+1 >= _oids.length) {
+ expand();
+ }
+
+ // add the oid
+ _oids[_size++] = oid;
+ return true;
+ }
+
+ /**
+ * Removes the specified oid from the list.
+ *
+ * @return true if the oid was in the list and was removed, false
+ * otherwise.
+ */
+ public boolean remove (int oid)
+ {
+ // scan for the oid in question
+ for (int i = 0; i < _size; i++) {
+ if (_oids[i] == oid) {
+ // shift the rest of the list back one
+ System.arraycopy(_oids, i+1, _oids, i, --_size-i);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the specified oid is in the list, false if not.
+ */
+ public boolean contains (int oid)
+ {
+ for (int i = 0; i < _size; i++) {
+ if (_oids[i] == oid) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the object id at the specified index. This does no boundary
+ * checking.
+ */
+ public function getAt (index :int) :int
+ {
+ return _oids[index];
+ }
+
+ // documentation inherited from interface Streamable
+ public function writeObject (out :ObjectOutputStream) :void
+ {
+ trace("Not implemented");
+ }
+
+ // documentation inherited from interface Streamable
+ public function readObject (ins :ObjectInputStream) :void
+ {
+ // TODO: this is some custom shit. We need to work out array streaming
+ _oids = new Array();
+ var ecount :int = ins.readInt();
+ for (var ii :int = 0; ii < ecount; ii++) {
+ _oids.push(ins.readInt());
+ }
+ _oids.length = ins.readInt(); // then, limit the length to the size
+ }
+
+
+ public override function toString () :String
+ {
+ StringBuilder buf = new StringBuilder();
+ buf.append("{");
+ buf.append(_oids.toString());
+ buf.append("}");
+ return buf.toString();
+ }
+
+ private var _oids :Array;
+}
+}