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; +} +}