diff --git a/src/as/com/threerings/README.txt b/src/as/com/threerings/README.txt index 7def758d9..ee4de3e0c 100644 --- a/src/as/com/threerings/README.txt +++ b/src/as/com/threerings/README.txt @@ -16,9 +16,10 @@ Design decisions - We need a realistic HashMap implementation. Using Object properties (a-la my SimpleMap) is not going to cut it because keys must always - be Strings and there's no way to *really* remove a value from an Object - (you can set the property to null, but now the property is forever defined: - the key is not cleared) + be Strings.and there's no way to *really* remove a value from + an Object (you can set the property to null, but now the property is + forever defined: the key is not cleared) + ***Update: the 'delete' operator removes properties. mx.utils.UIDUtil.getUID() can be used to generate a (huge) unique String for any object for use as a key or something. @@ -81,10 +82,9 @@ Notes and accessing only public properties of the main class from the helper. -- Similarly, I'm unclear about sandboxes. If a user-created .swf is playing - inside ours, I don't know if it can interact with our classes, and if so, - what happens if it proceeds to define a class like - com.threerings.presents.client.Client? +- Sandboxing classes is done with ApplicationDomains. When we load a sub-swf + we'll want to put it into a different domain so that nothing malicious + can be done to our classes. - constructors do not defaultly call super()- be sure to do it explicitely. Maybe we should get in the habit of doing it in Java for consistency and @@ -169,6 +169,14 @@ ActionScript Perhaps we'll want a util method that always generates an error if the object's type is not identical or a subclass of the casted-to type. + ***Update: + var o1 :String = null; + var o2 :String = String(o1); // ends up being "" or something + + The 2nd kind of cast destroys null, at least for String. So fuck that, + I was trying to use it when pulling a value out of a hash, but if it + wasn't there it got booched. + - Pitfall! This is perfectly legal: var b :int = 3; @@ -198,5 +206,10 @@ ActionScript me from correctly streaming arrays, as we need to know if the class is final. I can't just pass an instance in because it may be a pain to construct, it may even be unconstructable if the type of the array - is an interface. The adobe forums are broken right now, but I'm going - to post a request for enhancement so that we can get this information. + is an interface. Posted as a request for enhancement on the AS3.0 forums. + + +- Actionscript's property accessors are a cool feature, but beware hidden + performance issues: accessing a simple property of a variable + (like myArray.length) may actually be executing arbitrary code, possibly + creating many objects, each time. diff --git a/src/as/com/threerings/io/ObjectInputStream.as b/src/as/com/threerings/io/ObjectInputStream.as index fd9d93bc6..dd4da198e 100644 --- a/src/as/com/threerings/io/ObjectInputStream.as +++ b/src/as/com/threerings/io/ObjectInputStream.as @@ -28,14 +28,6 @@ public class ObjectInputStream _source = source; } - /** - * Add a translation that should be used when writing objects. - */ - public function addTranslation (oldName :String, newName :String) :void - { - _translations[oldName] = newName; - } - public function readObject () :Object //throws IOError { @@ -57,13 +49,8 @@ public class ObjectInputStream code *= -1; // read in the class metadata - var cname :String = readUTF(); + var cname :String = Translations.getFromServer(readUTF()); Log.debug("read cname: " + cname); - // if we have a translation, use it - var tname :String = _translations[cname]; - if (tname != null) { - cname = tname; - } var streamer :Streamer = Streamer.getStreamerByJavaName(cname); cmap = new ClassMapping(code, cname, streamer); @@ -215,9 +202,5 @@ public class ObjectInputStream /** A map of short class code to ClassMapping info. */ protected var _classMap :Array = new Array(); - - /** An collection of class name translations to use when unserializing - * objects. */ - protected var _translations :SimpleMap = new SimpleMap(); } } diff --git a/src/as/com/threerings/io/ObjectOutputStream.as b/src/as/com/threerings/io/ObjectOutputStream.as index 1d8e3e080..bc5f7bbb6 100644 --- a/src/as/com/threerings/io/ObjectOutputStream.as +++ b/src/as/com/threerings/io/ObjectOutputStream.as @@ -15,15 +15,6 @@ public class ObjectOutputStream _targ = targ; } - /** - * Add a classname translation for rewriting the names of objects - * that we send to the server. - */ - public function addTranslation (oldName :String, newName :String) :void - { - _translations[oldName] = newName; - } - public function writeObject (obj :Object) :void //throws IOError { @@ -35,7 +26,7 @@ public class ObjectOutputStream var cname :String = ClassUtil.getClassName(obj); // look up the class mapping record - var cmap :ClassMapping = _classMap[cname]; + var cmap :ClassMapping = (_classMap.get(cname) as ClassMapping); // create a class mapping if we've not got one if (cmap == null) { @@ -48,18 +39,13 @@ public class ObjectOutputStream } cmap = new ClassMapping(_nextCode++, cname, streamer); - _classMap[cname] = cmap; + _classMap.put(cname, cmap); // TODO: if _nextCode blows short, log an error - // see if there's a translation we should use - var tname :String = _translations[cname]; - if (tname != null) { - cname = tname; - } - writeShort(-cmap.code); - writeUTF((streamer == null) ? cname : streamer.getJavaClassName()); + writeUTF((streamer == null) ? Translations.getToServer(cname) + : streamer.getJavaClassName()); } else { writeShort(cmap.code); @@ -200,8 +186,5 @@ public class ObjectOutputStream /** A map of classname to ClassMapping info. */ protected var _classMap :SimpleMap = new SimpleMap(); - - /** A map of classname translations. */ - protected var _translations :SimpleMap = new SimpleMap(); } } diff --git a/src/as/com/threerings/io/Streamer.as b/src/as/com/threerings/io/Streamer.as index 02579c8a2..3e688afc5 100644 --- a/src/as/com/threerings/io/Streamer.as +++ b/src/as/com/threerings/io/Streamer.as @@ -48,6 +48,10 @@ public class Streamer { initStreamers(); + if (jname.charAt(0) === "[") { + // Oh ghod + } + for each (var streamer :Streamer in _streamers) { if (streamer.getJavaClassName() === jname) { return streamer; @@ -86,7 +90,7 @@ public class Streamer if (obj is Array) { trace("Arrays not yet done. Crap!"); /** - var arr :Array = (obj as Array); // not strictly necessary + var arr :Array = Array(obj); // not strictly necessary var length :int = arr.length; out.writeInt(length); */ diff --git a/src/as/com/threerings/presents/client/ClientDObjectMgr.as b/src/as/com/threerings/presents/client/ClientDObjectMgr.as index c4c4f8c92..8c3427546 100644 --- a/src/as/com/threerings/presents/client/ClientDObjectMgr.as +++ b/src/as/com/threerings/presents/client/ClientDObjectMgr.as @@ -208,14 +208,13 @@ public class ClientDObjectMgr } else if (obj is UnsubscribeResponse) { var oid :int = obj.getOid(); - if (_dead[oid] == null) { + if (_dead.remove(oid) == null) { Log.warning("Received unsub ACK from unknown object " + "[oid=" + oid + "]."); } - _dead[oid] = undefined; } else if (obj is FailureResponse) { - var oid :int = obj.getOid(); + var oid :int = (obj as FailureResponse).getOid(); notifyFailure(oid); } else if (obj is PongResponse) { @@ -250,9 +249,9 @@ public class ClientDObjectMgr // look up the object on which we're dispatching this event var toid :int = event.getTargetOid(); - var target :DObject = _ocache[toid]; + var target :DObject = (_ocache.get(toid) as DEvent); if (target == null) { - if (_dead[toid] === undefined) { + if (_dead.get(toid) == null) { Log.warning("Unable to dispatch event on non-proxied " + "object [event=" + event + "]."); } @@ -269,7 +268,7 @@ public class ClientDObjectMgr // Log.info("Pitching destroyed object " + // "[oid=" + toid + ", class=" + // StringUtil.shortClassName(target) + "]."); - _ocache[toid] = undefined; + _ocache.remove(toid); } // have the object pass this event on to its listeners @@ -295,7 +294,7 @@ public class ClientDObjectMgr var oid :int = obj.getOid(); // stick the object into the proxy object table - _ocache[oid] = obj; + _ocache.put(oid, obj); // let the penders know that the object is available var req :PendingRequest = (_penders.remove(oid) as PendingRequest); @@ -321,8 +320,7 @@ public class ClientDObjectMgr protected function notifyFailure (oid :int) :void { // let the penders know that the object is not available - var req :PendingRequest = _penders[oid]; - _penders[oid] = undefined; + var req :PendingRequest = (_penders.remove(oid) as PendingRequest); if (req == null) { Log.warning("Failed to get object, but no one cares?! " + "[oid=" + oid + "]."); @@ -345,10 +343,10 @@ public class ClientDObjectMgr // Log.info("doSubscribe: " + oid + ": " + target); // first see if we've already got the object in our table - var obj :DObject = _ocache[oid]; + var obj :DObject = (_ocache.get(oid) as DObject); if (obj != null) { // clear the object out of the flush table if it's in there - _flushes[oid] = undefined; + _flushes.remove(oid); // add the subscriber and call them back straight away obj.addSubscriber(target); target.objectAvailable(obj); @@ -356,7 +354,7 @@ public class ClientDObjectMgr } // see if we've already got an outstanding request for this object - var req :PendingRequest = _penders[oid]; + var req :PendingRequest = (_penders.get(oid) as PendingRequest); if (req != null) { // add this subscriber to the list of subscribers to be // notified when the request is satisfied @@ -367,7 +365,7 @@ public class ClientDObjectMgr // otherwise we need to create a new request req = new PendingRequest(oid); req.addTarget(target); - _penders[oid] = req; + _penders.put(oid, req); // Log.info("Registering pending request [oid=" + oid + "]."); // and issue a request to get things rolling @@ -380,7 +378,7 @@ public class ClientDObjectMgr */ protected function doUnsubscribe (oid :int, target :Subscriber) :void { - var dobj :DObject = _ocache[oid]; + var dobj :DObject = (_ocache.get(oid) as DObject); if (dobj != null) { dobj.removeSubscriber(target); @@ -401,8 +399,8 @@ public class ClientDObjectMgr // have it around anymore; once our unsubscribe message is // processed, it'll be 86ed var ooid :int = obj.getOid(); - _ocache[ooid] = undefined; - _dead[ooid] = obj; + _ocache.remove(ooid); + _dead.put(ooid, obj); // ship off an unsubscribe message to the server; we'll remove the // object from our table when we get the unsub ack @@ -416,10 +414,10 @@ public class ClientDObjectMgr protected function flushObjects () :void { var now :Number = new Date().getTime(); - for (var oid :String in _flushes) { - var rec :FlushRecord = _flushes[oid]; + for (var oid :String in _flushes.keys()) { + var rec :FlushRecord = (_flushes.get(oid) as FlushRecord); if (rec.expire <= now) { - _flushes[oid] = undefined; + _flushes.remove(oid); flushObject(rec.obj); } } diff --git a/src/as/com/threerings/presents/client/Communicator.as b/src/as/com/threerings/presents/client/Communicator.as index 8eebf1dd8..c36194716 100644 --- a/src/as/com/threerings/presents/client/Communicator.as +++ b/src/as/com/threerings/presents/client/Communicator.as @@ -14,6 +14,7 @@ import com.threerings.io.FrameAvailableEvent; import com.threerings.io.FrameReader; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Translations; import com.threerings.presents.Log; @@ -57,9 +58,8 @@ public class Communicator private function addClassTranslations () :void { - _inStream.addTranslation("com.threerings.presents.dobj.DSet$Entry", - "com.threerings.presents.dobj.DSetEntry"); - _outStream.addTranslation("com.threerings.presents.dobj.DSetEntry", + Translations.addTranslation( + "com.threerings.presents.dobj.DSetEntry", "com.threerings.presents.dobj.DSet$Entry"); } @@ -102,7 +102,7 @@ public class Communicator // write the message (ends up in _outBuffer) _outStream.writeObject(msg); - //Log.debug("outBuffer: " + Util.bytesToString(_outBuffer)); + Log.debug("outBuffer: " + Util.bytesToString(_outBuffer)); // Frame it by writing the length, then the bytes. // We add 4 to the length, because the length is of the entire frame diff --git a/src/as/com/threerings/presents/client/InvocationDirector.as b/src/as/com/threerings/presents/client/InvocationDirector.as index 9623c3fd6..6b8f95b4a 100644 --- a/src/as/com/threerings/presents/client/InvocationDirector.as +++ b/src/as/com/threerings/presents/client/InvocationDirector.as @@ -103,7 +103,7 @@ public class InvocationDirector var reg :InvocationRegistration = new InvocationRegistration( decoder.getReceiverCode(), nextReceiverId()); _clobj.addToReceivers(reg); - _receivers[reg.receiverId] = decoder; + _receivers.put(reg.receiverId, decoder); } /** @@ -139,7 +139,7 @@ public class InvocationDirector // create a mapping for this marshaller so that we can // properly dispatch responses sent to it - _listeners[lm.requestId] = lm; + _listeners.put(lm.requestId, lm); } } @@ -187,7 +187,7 @@ public class InvocationDirector (reqId :int, methodId :int, args :Array) :void { var listener :ListenerMarshaller = - (_listeners.remove(reqId) as ListenerMarshaller); + (_listeners.remove(reqId) as ListenerMarshaller); if (listener == null) { Log.warning("Received invocation response for which we have " + "no registered listener [reqId=" + reqId + @@ -229,7 +229,7 @@ public class InvocationDirector { // look up the decoder registered for this receiver var decoder :InvocationDecoder = - (_receivers[receiverId] as InvocationDecoder); + (_receivers.get(receiverId) as InvocationDecoder); if (decoder == null) { Log.warning("Received notification for which we have no " + "registered receiver [recvId=" + receiverId + @@ -268,11 +268,11 @@ public class InvocationDirector protected function flushListeners (now :Number) :void { var then :Number = now - LISTENER_MAX_AGE; - for (var skey :String in _listeners) { + for (var skey :String in _listeners.keys()) { var lm :ListenerMarshaller = - (_listeners[skey] as ListenerMarshaller); + (_listeners.get(skey) as ListenerMarshaller); if (lm.mapStamp < then) { - _listeners.clear(skey); + _listeners.remove(skey); } } } diff --git a/src/as/com/threerings/presents/client/TestClient.as b/src/as/com/threerings/presents/client/TestClient.as index c7d79c603..f17e094c6 100644 --- a/src/as/com/threerings/presents/client/TestClient.as +++ b/src/as/com/threerings/presents/client/TestClient.as @@ -15,13 +15,41 @@ public class TestClient extends Client setServer("tasman.sea.earth.threerings.net", DEFAULT_SERVER_PORT); logon(); - var a :Object = new HelperClass(this); + var g1 :String = null; + var g2 :String = String(g1); + Log.debug("foo: " + (g1 === g2)); - Log.debug("instance: " + describeType(a).toXMLString()); - Log.debug("class : " + describeType(HelperClass).toXMLString()); + var duckie :Duck = new Goose(); + duckie.screw(); + + var arr :Array = new Array(); + arr[0] = "Florp"; + arr[1] = "Blanger"; + arr[2] = "Swissbrat"; + + for (var key:* in arr) { + Log.debug("a key=" + key + " -> " + arr[key]); + } + delete arr[1]; + arr["1"] = "oinkenheimer"; + for (var key:* in arr) { + Log.debug("a key=" + key + " -> " + arr[key]); + } + Log.debug("length: " + arr.length); + Log.debug("arr[0]: " + arr[0]); + Log.debug("arr[1]: " + arr[1]); + Log.debug("arr[2]: " + arr[2]); /* - var b :Object = new PooperClass(); + var a :Object = new OldClass(); + var c :Class = OldClass; + + Log.debug("instance: " + describeType(a).toXMLString()); + Log.debug("class : " + describeType(Pork).toXMLString()); + */ + + /* + var b :Object = new OldClass(); var c :Object = 2.4; var d :Object = new HooperClass(this); @@ -74,6 +102,13 @@ public class TestClient extends Client import com.threerings.presents.Log; import com.threerings.presents.client.TestClient; +dynamic class OldClass +{ + public function OldClass () + { + } +} + class HelperClass { public function HelperClass (cli :TestClient) @@ -113,10 +148,3 @@ final class HooperClass extends HelperClass // nada } } - -class PooperClass -{ - public function PooperClass () - { - } -} diff --git a/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as b/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as index 765f711c7..27cf95831 100644 --- a/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as +++ b/src/as/com/threerings/presents/dobj/EntryRemovedEvent.as @@ -5,7 +5,6 @@ import flash.util.StringBuilder; import com.threerings.io.ObjectInputStream; import com.threerings.io.ObjectOutputStream; -import com.threerings.util.Comparable; import com.threerings.presents.Log; @@ -31,7 +30,7 @@ public class EntryRemovedEvent extends NamedEvent * @param oldEntry the previous value of the entry. */ public function EntryRemovedEvent ( - targetOid :int, name :String, key :Comparable, oldEntry :DSetEntry) + targetOid :int, name :String, key :Object, oldEntry :DSetEntry) { super(targetOid, name); _key = key; @@ -41,7 +40,7 @@ public class EntryRemovedEvent extends NamedEvent /** * Returns the key that identifies the entry that has been removed. */ - public function getKey () :Comparable + public function getKey () :Object { return _key; } @@ -99,10 +98,10 @@ public class EntryRemovedEvent extends NamedEvent public override function readObject (ins :ObjectInputStream) :void { super.readObject(ins); - _key = (ins.readObject() as Comparable); + _key = ins.readObject(); } - protected var _key :Comparable; + protected var _key :Object; protected var _oldEntry :DSetEntry = UNSET_OLD_ENTRY; } } diff --git a/src/as/com/threerings/util/ClassUtil.as b/src/as/com/threerings/util/ClassUtil.as index 64d059f0e..f727ba695 100644 --- a/src/as/com/threerings/util/ClassUtil.as +++ b/src/as/com/threerings/util/ClassUtil.as @@ -26,6 +26,7 @@ public class ClassUtil public static function getClassByName (cname :String) :Class { + // see also ApplicationDomain.currentDomain.getClass(cname) return flash.util.getClassByName(cname); } diff --git a/src/as/com/threerings/util/SimpleMap.as b/src/as/com/threerings/util/SimpleMap.as index 60fa7a55e..407328d50 100644 --- a/src/as/com/threerings/util/SimpleMap.as +++ b/src/as/com/threerings/util/SimpleMap.as @@ -4,15 +4,44 @@ package com.threerings.util { * Marker class that allows us to use ActionScript's built-in hashing * function without hassle. */ -public dynamic class SimpleMap extends Object +public class SimpleMap extends Object { + public function clear () :void + { + _data = new Object(); + } + + public function get (key :Object) :Object + { + var skey :String = key.toString(); + return _data[skey]; + } + + public function keys () :Array + { + var arr :Array = new Array(); + for (var skey :String in _data) { + arr.push(skey); + } + return arr; + } + + public function put (key :Object, value :Object) :Object + { + var skey :String = key.toString(); + var oldValue :Object = _data[skey]; + _data[skey] = value; + return oldValue; + } + public function remove (key :Object) :Object { - // I'm not sure this really works var skey :String = key.toString(); - var value :Object = this[skey]; - this[skey] = undefined; + var value :Object = _data[skey]; + delete _data[skey]; return value; } + + private var _data :Object = new Object(); } } diff --git a/src/as/com/threerings/util/TypedArray.as b/src/as/com/threerings/util/TypedArray.as index 91613ec9b..ab39ea13c 100644 --- a/src/as/com/threerings/util/TypedArray.as +++ b/src/as/com/threerings/util/TypedArray.as @@ -1,7 +1,5 @@ package com.threerings.util { -import flash.util.describeType; - import mx.utils.ObjectUtil; import com.threerings.io.ArrayMask; @@ -83,6 +81,7 @@ public dynamic class TypedArray extends Array * except gives it a clue as to how to stream to our server. */ protected var _type :Class; + /** Whether or not the type of the array represents a final class. */ protected var _final :Boolean; protected var _delegate :Streamer;