diff --git a/src/as/com/threerings/ezgame/CardDeck.as b/src/as/com/threerings/ezgame/CardDeck.as
new file mode 100644
index 00000000..d44e9f3e
--- /dev/null
+++ b/src/as/com/threerings/ezgame/CardDeck.as
@@ -0,0 +1,43 @@
+package com.threerings.ezgame {
+
+/**
+ * A simple card deck that encodes cards as a string like "Ac" for the
+ * ace of clubs, or "Td" for the 10 of diamonds.
+ */
+public class CardDeck
+{
+ public function CardDeck (gameObj :GameObject, deckName :String = "deck")
+ {
+ _gameObj = gameObj;
+ _deckName = deckName;
+
+ var deck :Array = new Array();
+ for each (var rank :String in ["2", "3", "4", "5", "6", "7", "8",
+ "9", "T", "J", "Q", "K", "A"]) {
+ for each (var suit :String in ["c", "d", "h", "s"]) {
+ deck.push(rank + suit);
+ }
+ }
+
+ _gameObj.setCollection(_deckName, deck);
+ }
+
+ public function dealToPlayer (
+ playerIdx :int, count :int, msgName :String) :void
+ {
+ // TODO: support the callback
+ _gameObj.dealFromCollection(_deckName, count, msgName, null, playerIdx);
+ }
+
+ public function dealToData (count :int, propName :String) :void
+ {
+ _gameObj.dealFromCollection(_deckName, count, propName, null);
+ }
+
+ /** The game object. */
+ protected var _gameObj :GameObject;
+
+ /** The name of our deck. */
+ protected var _deckName :String;
+}
+}
diff --git a/src/as/com/threerings/ezgame/Game.as b/src/as/com/threerings/ezgame/Game.as
new file mode 100644
index 00000000..891f3c4d
--- /dev/null
+++ b/src/as/com/threerings/ezgame/Game.as
@@ -0,0 +1,16 @@
+package com.threerings.ezgame {
+
+/**
+ * This interface should be implemented by some class that lives on the
+ * display list. When a DisplayObject that implements this interface
+ * is added, it will be notified of the GameObject.
+ */
+public interface Game
+{
+ /**
+ * Called by the mysterious powers of the cosmos to initialize
+ * your game with the GameObject.
+ */
+ function setGameObject (gameObj :GameObject) :void;
+}
+}
diff --git a/src/as/com/threerings/ezgame/GameObject.as b/src/as/com/threerings/ezgame/GameObject.as
new file mode 100644
index 00000000..5797b9e1
--- /dev/null
+++ b/src/as/com/threerings/ezgame/GameObject.as
@@ -0,0 +1,133 @@
+package com.threerings.ezgame {
+
+import flash.events.IEventDispatcher;
+
+/**
+ * The game object that you'll be using to manage your game.
+ */
+public interface GameObject
+ extends IEventDispatcher
+{
+ /**
+ * Data accessor.
+ */
+ function get data () :Object;
+
+ /**
+ * Get a property from data.
+ */
+ function get (propName :String, index :int = -1) :Object;
+
+ /**
+ * Set a property that will be distributed.
+ */
+ function set (propName :String, value :Object, index :int = -1) :void;
+
+ /**
+ * Set the specified collection to contain the specified values,
+ * clearing any previous values.
+ */
+ function setCollection (collName :String, values :Array) :void;
+
+ /**
+ * Add to an existing collection. If it doesn't exist, it will
+ * be created. The new values will be inserted randomly into the
+ * collection.
+ */
+ function addToCollection (collName :String, values :Array) :void;
+
+ /**
+ * Pick (do not remove) the specified number of elements from a collection,
+ * and distribute them to a specific player or set them as a property
+ * in the game data.
+ */
+ // TODO: a way to specify exclusive picks vs. duplicate-OK picks?
+ function pickFromCollection (
+ collName :String, count :int, msgOrPropName :String,
+ playerIndex :int = -1) :void;
+
+ /**
+ * Deal (remove) the specified number of elements from a collection,
+ * and distribute them to a specific player or set them as a property
+ * in the game data.
+ */
+ // TODO: figure out the method signature of the callback
+ function dealFromCollection (
+ collName :String, count :int, msgOrPropName :String,
+ callBack :Function = null, playerIndex :int = -1) :void;
+
+ /**
+ * Merge the specified collection into the other collection.
+ * The source collection will be destroyed. The elements from
+ * The source collection will be shuffled and appended to the end
+ * of the destination collection.
+ */
+ function mergeCollection (srcColl :String, intoColl :String) :void;
+
+ /**
+ * Send a "message" to other clients subscribed to the game.
+ * These is similar to setting a property, except that the
+ * value will not be saved- it will merely end up coming out
+ * as a MessageReceivedEvent.
+ *
+ * @param playerIndex if -1, sends to all players, otherwise
+ * the message will be private to just one player
+ */
+ function sendMessage (
+ messageName :String, value :Object, playerIndex :int = -1) :void;
+
+ /**
+ * Send a message that will be heard by everyone in the game room,
+ * even observers.
+ */
+ function sendChat (msg :String) :void;
+
+ /**
+ * Display the specified message immediately locally: not sent
+ * to any other players or observers in the game room.
+ */
+ function localChat (msg :String) :void;
+
+ /**
+ * Get the player names, as an array.
+ */
+ function getPlayerNames () :Array /* of String */;
+
+ /**
+ * Get the index into the player names array of the current player,
+ * or -1 if the user is not a player.
+ */
+ function getMyIndex () :int;
+
+ /**
+ * Get the turn holder's index, or -1 if it's nobody's turn.
+ */
+ function getTurnHolderIndex () :int;
+
+ /**
+ * Get the indexes of the winners
+ */
+ function getWinnerIndexes () :Array /* of int */;
+
+ /**
+ * A convenience method to just check if it's our turn.
+ */
+ function isMyTurn () :Boolean;
+
+ /**
+ * Is the game currently in play?
+ */
+ function isInPlay () :Boolean;
+
+ /**
+ * End the current turn. If no next player index is specified,
+ * then the next player after the current one is used.
+ */
+ function endTurn (optionalNextPlayerIndex :int = -1) :void;
+
+ /**
+ * End the game. The specified player indexes are winners!
+ */
+ function endGame (winnerIndex :int, ... rest) :void;
+}
+}
diff --git a/src/as/com/threerings/ezgame/MessageReceivedEvent.as b/src/as/com/threerings/ezgame/MessageReceivedEvent.as
new file mode 100644
index 00000000..cea56a9c
--- /dev/null
+++ b/src/as/com/threerings/ezgame/MessageReceivedEvent.as
@@ -0,0 +1,47 @@
+package com.threerings.ezgame {
+
+import flash.events.Event;
+
+public class MessageReceivedEvent extends Event
+{
+ /** The type of all MessageReceivedEvents. */
+ public static const TYPE :String = "msgReceived";
+
+ /**
+ * Access the message name.
+ */
+ public function get name () :String
+ {
+ return _name;
+ }
+
+ /**
+ * Access the message value.
+ */
+ public function get value () :Object
+ {
+ return _value;
+ }
+
+ public function MessageReceivedEvent (messageName :String, value :Object)
+ {
+ super(TYPE);
+ _name = messageName;
+ _value = value;
+ }
+
+ override public function toString () :String
+ {
+ return "[MessageReceivedEvent name=" + _name +
+ ", value=" + _value + "]";
+ }
+
+ override public function clone () :Event
+ {
+ return new MessageReceivedEvent(_name, _value);
+ }
+
+ protected var _name :String;
+ protected var _value :Object;
+}
+}
diff --git a/src/as/com/threerings/ezgame/PlayersDisplay.as b/src/as/com/threerings/ezgame/PlayersDisplay.as
new file mode 100644
index 00000000..f9c88a65
--- /dev/null
+++ b/src/as/com/threerings/ezgame/PlayersDisplay.as
@@ -0,0 +1,132 @@
+package com.threerings.ezgame {
+
+import flash.display.Sprite;
+
+import flash.text.StyleSheet;
+import flash.text.TextField;
+import flash.text.TextFieldAutoSize;
+
+/**
+ * A sample component that displays the players of a game.
+ * If the game has a turn holder, the current turn holder will be highlighted.
+ *
+ * This class demonstrates that the 'Game' interface may be implemented
+ * by any DisplayObject that want access to the GameObject, not just the
+ * actual DisplayObject that is displaying the game. Here, all we are
+ * interested in is the names of the players and the current turn holder.
+ *
+ * You may use this, with any modifications you desire, in your game. Feel
+ * free to copy/modify or extend this class.
+ */
+public class PlayersDisplay extends Sprite
+ implements Game
+{
+ // implementation of Game method
+ public function setGameObject (gameObj :GameObject) :void
+ {
+ _gameObj = gameObj;
+ _gameObj.addEventListener(StateChangedEvent.TURN_CHANGED, recheckTurn);
+ _gameObj.addEventListener(StateChangedEvent.GAME_ENDED, recheckTurn);
+
+ setupPlayers();
+ }
+
+ /**
+ * Set up the player labels and configure the look of the entire UI.
+ */
+ protected function setupPlayers () :void
+ {
+ var y :Number = BORDER;
+ // create a label at the top, above the player names
+ var label :TextField = new TextField();
+// damn stylesheet doesn't seem to actually -work-
+// var style :StyleSheet = new StyleSheet();
+// style.fontWeight = "bold";
+// style.color = "#0000FF";
+// style.fontFamily = "serif";
+// style.fontSize = 18;
+// label.styleSheet = style;
+ label.autoSize = TextFieldAutoSize.LEFT;
+ label.selectable = false;
+ label.text = "Players";
+ label.x = BORDER;
+ label.y = y;
+ addChild(label);
+ y += label.textHeight;
+
+ var maxWidth :Number = label.textWidth;
+
+ // create a label for each player
+ for each (var name :String in _gameObj.getPlayerNames()) {
+ y += PAD;
+ label = new TextField();
+ label.autoSize = TextFieldAutoSize.LEFT;
+ label.background = true;
+ label.selectable = false;
+ label.text = name;
+ label.x = BORDER;
+ label.y = y;
+ addChild(label);
+ y += label.textHeight;
+ maxWidth = Math.max(maxWidth, label.textWidth);
+
+ _playerLabels.push(label);
+ }
+
+ // make all the player labels the same width
+ // (looks nice when highlighted)
+ for each (label in _playerLabels) {
+ label.autoSize = TextFieldAutoSize.NONE;
+ label.width = maxWidth;
+ }
+
+ // draw a blue rectangle around everything
+ graphics.clear();
+ graphics.lineStyle(1, 0x0000FF);
+ graphics.drawRect(0, 0, maxWidth + (BORDER * 2), y + BORDER);
+
+ displayCurrentTurn();
+ }
+
+ /**
+ * Re-set the background color for every player label, highlighting
+ * only the player who has the turn.
+ */
+ protected function displayCurrentTurn () :void
+ {
+ var idx :int = _gameObj.isInPlay() ? _gameObj.getTurnHolderIndex() : -1;
+ for (var ii :int = 0; ii < _playerLabels.length; ii++) {
+ var label :TextField = (_playerLabels[ii] as TextField);
+ label.backgroundColor = (ii == idx) ? TURN_BACKGROUND
+ : NORMAL_BACKGROUND;
+ }
+ }
+
+ /**
+ * Registered to receive TURN_CHANGED and GAME_ENDED events
+ * from the GameObject.
+ */
+ protected function recheckTurn (event :StateChangedEvent) :void
+ {
+ displayCurrentTurn();
+ }
+
+ /** Our game object. */
+ protected var _gameObj :GameObject;
+
+ /** An array of labels, one for each player name. */
+ protected var _playerLabels :Array = [];
+
+ /** The background color for players that doesn't have the turn. */
+ protected static const NORMAL_BACKGROUND :uint = 0xFFFFFF;
+
+ /** The background color for players that do have the turn. */
+ protected static const TURN_BACKGROUND :uint = 0xFF9999;
+
+ /** The number of pixels to border around all the names. */
+ protected static const BORDER :int = 6;
+
+ /** The additional padding inserted between names. */
+ protected static const PAD :int = 2;
+}
+}
diff --git a/src/as/com/threerings/ezgame/PropertyChangedEvent.as b/src/as/com/threerings/ezgame/PropertyChangedEvent.as
new file mode 100644
index 00000000..dfb0ddd6
--- /dev/null
+++ b/src/as/com/threerings/ezgame/PropertyChangedEvent.as
@@ -0,0 +1,76 @@
+package com.threerings.ezgame {
+
+import flash.events.Event;
+
+/**
+ * Property change events are dispatched after the property change was
+ * validated on the server.
+ */
+public class PropertyChangedEvent extends Event
+{
+ /** The type of a property change event. */
+ public static const TYPE :String = "PropChanged";
+
+ /**
+ * Get the name of the property that changed.
+ */
+ public function get name () :String
+ {
+ return _name;
+ }
+
+ /**
+ * Get the property's new value.
+ */
+ public function get newValue () :Object
+ {
+ return _newValue;
+ }
+
+ /**
+ * Get the property's previous value (handy!).
+ */
+ public function get oldValue () :Object
+ {
+ return _oldValue;
+ }
+
+ /**
+ * If an array element was updated, get the index, or -1 if not applicable.
+ */
+ public function get index () :int
+ {
+ return _index;
+ }
+
+ /**
+ * Constructor.
+ */
+ public function PropertyChangedEvent (
+ propName :String, newValue :Object, oldValue :Object, index :int = -1)
+ {
+ super(TYPE);
+ _name = propName;
+ _newValue = newValue;
+ _oldValue = oldValue;
+ _index = index;
+ }
+
+ override public function toString () :String
+ {
+ return "[PropertyChangedEvent name=" + _name + ", value=" + _newValue +
+ ((index < 0) ? "" : (", index=" + index)) + "]";
+ }
+
+ override public function clone () :Event
+ {
+ return new PropertyChangedEvent(_name, _newValue, _oldValue, _index);
+ }
+
+ /** Our implementation details. */
+ protected var _name :String;
+ protected var _newValue :Object;
+ protected var _oldValue :Object;
+ protected var _index :int;
+}
+}
diff --git a/src/as/com/threerings/ezgame/StateChangedEvent.as b/src/as/com/threerings/ezgame/StateChangedEvent.as
new file mode 100644
index 00000000..123618f1
--- /dev/null
+++ b/src/as/com/threerings/ezgame/StateChangedEvent.as
@@ -0,0 +1,35 @@
+package com.threerings.ezgame {
+
+import flash.events.Event;
+
+/**
+ * Dispatched when the state of the game has changed.
+ */
+public class StateChangedEvent extends Event
+{
+ /** Indicates that the game has transitioned to a started state. */
+ public static const GAME_STARTED :String = "GameStarted";
+
+ /** Indicates that the game has transitioned to a ended state. */
+ public static const GAME_ENDED :String = "GameEnded";
+
+ /** Indicates that the turn has changed. */
+ // TODO: move to own event?
+ public static const TURN_CHANGED :String = "TurnChanged";
+
+ public function StateChangedEvent (type :String)
+ {
+ super(type);
+ }
+
+ override public function toString () :String
+ {
+ return "[StateChangedEvent type=" + type + "]";
+ }
+
+ override public function clone () :Event
+ {
+ return new StateChangedEvent(type);
+ }
+}
+}
diff --git a/src/as/com/threerings/ezgame/client/EZGameController.as b/src/as/com/threerings/ezgame/client/EZGameController.as
new file mode 100644
index 00000000..8176d3e2
--- /dev/null
+++ b/src/as/com/threerings/ezgame/client/EZGameController.as
@@ -0,0 +1,152 @@
+package com.threerings.ezgame.client {
+
+import flash.events.Event;
+
+import flash.utils.ByteArray;
+
+import com.threerings.util.Name;
+
+import com.threerings.presents.dobj.MessageAdapter;
+import com.threerings.presents.dobj.MessageListener;
+import com.threerings.presents.dobj.MessageEvent;
+
+import com.threerings.crowd.client.PlaceView;
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.crowd.data.PlaceObject;
+import com.threerings.crowd.util.CrowdContext;
+
+import com.threerings.parlor.game.client.GameController;
+
+import com.threerings.parlor.turn.client.TurnGameController;
+import com.threerings.parlor.turn.client.TurnGameControllerDelegate;
+
+import com.threerings.ezgame.data.EZGameObject;
+import com.threerings.ezgame.data.PropertySetEvent;
+import com.threerings.ezgame.data.PropertySetListener;
+import com.threerings.ezgame.util.EZObjectMarshaller;
+
+import com.threerings.ezgame.MessageReceivedEvent;
+import com.threerings.ezgame.PropertyChangedEvent;
+import com.threerings.ezgame.StateChangedEvent;
+
+/**
+ * A controller for flash games.
+ */
+public class EZGameController extends GameController
+ implements TurnGameController, PropertySetListener, MessageListener
+{
+ /** The implementation of the GameObject interface for users. */
+ public var gameObjImpl :GameObjectImpl;
+
+ /**
+ */
+ public function EZGameController ()
+ {
+ addDelegate(_turnDelegate = new TurnGameControllerDelegate(this));
+ }
+
+ override public function willEnterPlace (plobj :PlaceObject) :void
+ {
+ _ezObj = (plobj as EZGameObject);
+ gameObjImpl = new GameObjectImpl(_ctx, _ezObj);
+
+ _ctx.getClient().getClientObject().addListener(_userListener);
+
+ super.willEnterPlace(plobj);
+ }
+
+ override public function didLeavePlace (plobj :PlaceObject) :void
+ {
+ super.didLeavePlace(plobj);
+
+ _ctx.getClient().getClientObject().removeListener(_userListener);
+
+ _ezObj = null;
+ }
+
+ // from TurnGameController
+ public function turnDidChange (turnHolder :Name) :void
+ {
+ dispatchUserEvent(
+ new StateChangedEvent(StateChangedEvent.TURN_CHANGED));
+ }
+
+ // from PropertySetListener
+ public function propertyWasSet (event :PropertySetEvent) :void
+ {
+ // notify the user game
+ dispatchUserEvent(new PropertyChangedEvent(
+ event.getName(), event.getValue(), event.getOldValue(),
+ event.getIndex()));
+ }
+
+ // from MessageListener
+ public function messageReceived (event :MessageEvent) :void
+ {
+ var name :String = event.getName();
+ if (EZGameObject.USER_MESSAGE == name) {
+ dispatchUserMessage(event.getArgs());
+
+ } else if (EZGameObject.GAME_CHAT == name) {
+ // this is chat send by the game, let's route it like
+ // localChat, which is also sent by the game
+ gameObjImpl.localChat(String(event.getArgs()[0]));
+ }
+ }
+
+ /**
+ * Called by our user listener when we receive a message event
+ * on the user object.
+ */
+ protected function messageReceivedOnUserObject (event :MessageEvent) :void
+ {
+ // see if it's a message about user games
+ var msgName :String =
+ EZGameObject.USER_MESSAGE + ":" + _ezObj.getOid();
+ if (msgName == event.getName()) {
+ dispatchUserMessage(event.getArgs());
+ }
+ }
+
+ /**
+ * Dispatch the user message.
+ */
+ protected function dispatchUserMessage (args :Array) :void
+ {
+ dispatchUserEvent(new MessageReceivedEvent(
+ (args[0] as String),
+ EZObjectMarshaller.decode(args[1])));
+ }
+
+ override protected function createPlaceView (ctx :CrowdContext) :PlaceView
+ {
+ return new EZGamePanel(ctx, this);
+ }
+
+ override protected function gameDidStart () :void
+ {
+ super.gameDidStart();
+ dispatchUserEvent(
+ new StateChangedEvent(StateChangedEvent.GAME_STARTED));
+ }
+
+ override protected function gameDidEnd () :void
+ {
+ super.gameDidEnd();
+ dispatchUserEvent(
+ new StateChangedEvent(StateChangedEvent.GAME_ENDED));
+ }
+
+ protected function dispatchUserEvent (event :Event) :void
+ {
+ gameObjImpl.dispatch(event);
+ }
+
+ protected var _ezObj :EZGameObject;
+
+ protected var _turnDelegate :TurnGameControllerDelegate;
+
+ protected var _userListener :MessageAdapter =
+ new MessageAdapter(messageReceivedOnUserObject);
+}
+}
diff --git a/src/as/com/threerings/ezgame/client/EZGamePanel.as b/src/as/com/threerings/ezgame/client/EZGamePanel.as
new file mode 100644
index 00000000..89cef683
--- /dev/null
+++ b/src/as/com/threerings/ezgame/client/EZGamePanel.as
@@ -0,0 +1,105 @@
+package com.threerings.ezgame.client {
+
+import flash.display.DisplayObject;
+import flash.display.DisplayObjectContainer;
+
+import flash.events.Event;
+
+import flash.utils.Dictionary;
+
+import mx.containers.Canvas;
+import mx.containers.VBox;
+
+import mx.core.Container;
+import mx.core.IChildList;
+
+import mx.utils.DisplayUtil;
+
+import com.threerings.crowd.client.PlaceView;
+import com.threerings.crowd.data.PlaceObject;
+import com.threerings.crowd.util.CrowdContext;
+
+import com.threerings.ezgame.data.EZGameConfig;
+import com.threerings.ezgame.data.EZGameObject;
+
+import com.threerings.ezgame.Game;
+
+public class EZGamePanel extends VBox
+ implements PlaceView
+{
+ public function EZGamePanel (ctx :CrowdContext, ctrl :EZGameController)
+ {
+ _ctx = ctx;
+ _ctrl = ctrl;
+
+ // add a listener so that we hear about all new children
+ addEventListener(Event.ADDED, childAdded);
+
+ // TODO: figure out how we're going to add the client game
+ var cfg :EZGameConfig = (ctrl.getPlaceConfig() as EZGameConfig);
+ //_gameView = new MsoySprite(cfg.game);
+ //addChild(_gameView);
+
+ // TODO: move chat component into narya?
+ //addChild(new ChatTextArea(ctx));
+ }
+
+ // from PlaceView
+ public function willEnterPlace (plobj :PlaceObject) :void
+ {
+ // don't start notifying anything of the game until we've
+ // notified the game manager that we're in the game
+ // (done in GameController, and it uses callLater, so we do it twice!)
+ _ctx.getClient().callLater(function () :void {
+ _ctx.getClient().callLater(function () :void {
+ _ezObj = (plobj as EZGameObject);
+
+ notifyOfGame(this);
+ });
+ });
+ }
+
+ // from PlaceView
+ public function didLeavePlace (plobj :PlaceObject) :void
+ {
+ _ezObj = null;
+ }
+
+ /**
+ * Handle ADDED events.
+ */
+ protected function childAdded (event :Event) :void
+ {
+ if (_ezObj != null) {
+ notifyOfGame(event.target as DisplayObject);
+ }
+ }
+
+ /**
+ * Find any children of the specified object that implement
+ * com.metasoy.game.Game and provide them with the GameObject.
+ */
+ protected function notifyOfGame (root :DisplayObject) :void
+ {
+ DisplayUtil.walkDisplayObjects(root,
+ function (disp :DisplayObject) :void
+ {
+ if (disp is Game) {
+ // only notify the Game if we haven't seen it before
+ if (null == _seenGames[disp]) {
+ (disp as Game).setGameObject(_ctrl.gameObjImpl);
+ _seenGames[disp] = true;
+ }
+ }
+ });
+ }
+
+ protected var _ctx :CrowdContext;
+ protected var _ctrl :EZGameController;
+
+ /** A weak-key hash of the Game interfaces we've already seen. */
+ protected var _seenGames :Dictionary = new Dictionary(true);
+
+ protected var _ezObj :EZGameObject;
+}
+}
diff --git a/src/as/com/threerings/ezgame/client/EZGameService.as b/src/as/com/threerings/ezgame/client/EZGameService.as
new file mode 100644
index 00000000..29c47715
--- /dev/null
+++ b/src/as/com/threerings/ezgame/client/EZGameService.as
@@ -0,0 +1,78 @@
+//
+// $Id$
+
+package com.threerings.ezgame.client {
+
+import flash.utils.ByteArray;
+
+import com.threerings.io.TypedArray;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.client.InvocationService_ConfirmListener;
+import com.threerings.presents.client.InvocationService_InvocationListener;
+
+/**
+ * Provides services for ez games.
+ */
+public interface EZGameService extends InvocationService
+{
+ /**
+ * Request to set the specified property.
+ */
+ function setProperty (
+ client :Client, propName :String, value :Object, index :int,
+ listener :InvocationService_InvocationListener) :void;
+
+ /**
+ * Request to end the turn, possibly futzing the next turn holder unless
+ * -1 is specified for the nextPlayerIndex.
+ */
+ function endTurn (
+ client :Client, nextPlayerIndex :int,
+ listener :InvocationService_InvocationListener) :void;
+
+ /**
+ * Request to end the game, with the specified player indices assigned
+ * as winners.
+ */
+ function endGame (
+ client :Client, winners :TypedArray /* of int */,
+ listener :InvocationService_InvocationListener) :void;
+
+ /**
+ * Request to send a private message to one other player in
+ * the game.
+ */
+ function sendMessage (
+ client :Client, msgName :String, value :Object, playerIdx :int,
+ listener :InvocationService_InvocationListener) :void;
+
+ /**
+ * Add to the specified named collection.
+ *
+ * @param clearExisting if true, wipe the old contents.
+ */
+ function addToCollection (
+ client :Client, collName :String, data :TypedArray /* of ByteArray */,
+ clearExisting :Boolean, listener :InvocationService_InvocationListener)
+ :void;
+
+ /**
+ * Merge the specified collection into the other.
+ */
+ function mergeCollection (
+ client :Client, srcColl :String, intoColl :String,
+ listener :InvocationService_InvocationListener) :void;
+
+ /**
+ * Pick or deal some number of elements from the specified collection,
+ * and either set a property in the flash object, or delivery the
+ * picks to the specified player index via a game message.
+ */
+ function getFromCollection (
+ client :Client, collName :String, consume :Boolean, count :int,
+ msgOrPropName :String, playerIndex :int,
+ listener :InvocationService_ConfirmListener) :void;
+}
+}
diff --git a/src/as/com/threerings/ezgame/client/GameData.as b/src/as/com/threerings/ezgame/client/GameData.as
new file mode 100644
index 00000000..a59f8266
--- /dev/null
+++ b/src/as/com/threerings/ezgame/client/GameData.as
@@ -0,0 +1,121 @@
+package com.threerings.ezgame.client {
+
+import flash.errors.IllegalOperationError;
+
+import flash.utils.Proxy;
+
+import flash.utils.flash_proxy;
+
+use namespace flash_proxy;
+
+public class GameData extends Proxy
+{
+ public function GameData (gameObjImpl :GameObjectImpl, props :Object)
+ {
+ _gameObjImpl = gameObjImpl;
+ _props = props;
+ }
+
+ public function hasOwnProperty (propName :String) :Boolean
+ {
+ // pass-through
+ return _props.hasOwnProperty(propName);
+ }
+
+ public function propertyIsEnumerable (propName :String) :Boolean
+ {
+ // pass-through
+ return _props.propertyIsEnumerable(propName);
+ }
+
+ public function setPropertyIsEnumerable (
+ propName :String, isEnum :Boolean = true) :void
+ {
+ // pass-through
+ _props.setPropertyIsEnumerable(propName, isEnum);
+ }
+
+ override flash_proxy function callProperty (propName :*, ... rest) :*
+ {
+ // don't allow function calls
+ throw new IllegalOperationError();
+ }
+
+ override flash_proxy function getDescendants (name :*) :*
+ {
+ // we don't need this XML business
+ throw new IllegalOperationError();
+ }
+
+ override flash_proxy function isAttribute (name :*) :Boolean
+ {
+ // we don't need this XML business
+ throw new IllegalOperationError();
+ }
+
+ override flash_proxy function getProperty (propName :*) :*
+ {
+ // pass-through
+ return _props[propName];
+ }
+
+ override flash_proxy function hasProperty (propName :*) :Boolean
+ {
+ // pass-through
+ return (_props[propName] !== undefined);
+ }
+
+ override flash_proxy function setProperty (propName :*, value :*) :void
+ {
+ _gameObjImpl.set(String(propName), value);
+ }
+
+ override flash_proxy function deleteProperty (propName :*) :Boolean
+ {
+ var hasProp :Boolean = hasProperty(propName);
+ _gameObjImpl.set(String(propName), null);
+ return hasProp;
+ }
+
+ override flash_proxy function nextNameIndex (index :int) :int
+ {
+ // possibly set up the property list on the first call
+ if (index == 0) {
+ _propertyList = [];
+ for (var prop :String in _props) {
+ _propertyList.push(prop);
+ }
+ }
+
+ // return a 1-based index to indicate that there is a property
+ if (index < _propertyList.length) {
+ return index + 1;
+
+ } else {
+ // we're done, clear the prop list
+ _propertyList = null;
+ return 0;
+ }
+ }
+
+ override flash_proxy function nextName (index :int) :String
+ {
+ // the index is 1-based, so subtract one
+ return (_propertyList[index - 1] as String);
+ }
+
+ override flash_proxy function nextValue (index :int) :*
+ {
+ return _props[nextName(index)];
+ }
+
+ /** The GameObject that controls things. */
+ protected var _gameObjImpl :GameObjectImpl;
+
+ /** The object we're proxying. */
+ protected var _props :Object;
+
+ /** Used temporarily while iterating over our names or values. */
+ protected var _propertyList :Array;
+}
+}
diff --git a/src/as/com/threerings/ezgame/client/GameObjectImpl.as b/src/as/com/threerings/ezgame/client/GameObjectImpl.as
new file mode 100644
index 00000000..378ccc85
--- /dev/null
+++ b/src/as/com/threerings/ezgame/client/GameObjectImpl.as
@@ -0,0 +1,407 @@
+package com.threerings.ezgame.client {
+
+import flash.errors.IllegalOperationError;
+
+import flash.events.Event;
+import flash.events.EventDispatcher;
+
+import flash.utils.IExternalizable;
+import flash.utils.ByteArray;
+
+import com.threerings.io.TypedArray;
+
+import com.threerings.util.ClassUtil;
+import com.threerings.util.MessageBundle;
+import com.threerings.util.Name;
+import com.threerings.util.StringUtil;
+
+import com.threerings.presents.client.ConfirmAdapter;
+import com.threerings.presents.client.InvocationService_ConfirmListener;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.util.CrowdContext;
+
+import com.threerings.ezgame.data.EZGameObject;
+import com.threerings.ezgame.data.PropertySetEvent;
+import com.threerings.ezgame.util.EZObjectMarshaller;
+
+import com.threerings.ezgame.GameObject;
+import com.threerings.ezgame.PropertyChangedEvent;
+
+public class GameObjectImpl extends EventDispatcher
+ implements GameObject
+{
+ public function GameObjectImpl (ctx :CrowdContext, ezObj :EZGameObject)
+ {
+ _ctx = ctx;
+ _ezObj = ezObj;
+ _gameData = new GameData(this, _ezObj.getUserProps());
+ }
+
+ // from GameObject
+ public function get data () :Object
+ {
+ return _gameData;
+ }
+
+ // from GameObject
+ public function get (propName :String, index :int = -1) :Object
+ {
+ var value :Object = data[propName];
+ if (index >= 0) {
+ if (value is Array) {
+ return (value as Array)[index];
+
+ } else {
+ throw new ArgumentError("Property " + propName +
+ " is not an array.");
+ }
+ }
+ return value;
+ }
+
+ // from GameObject
+ public function set (propName :String, value :Object, index :int = -1) :void
+ {
+ validatePropertyChange(propName, value, index);
+
+ var encoded :Object = EZObjectMarshaller.encode(value);
+ _ezObj.ezGameService.setProperty(
+ _ctx.getClient(), propName, encoded, index,
+ createLoggingListener("setProperty"));
+
+ // set it immediately in the game object
+ _ezObj.applyPropertySet(propName, value, index);
+ }
+
+ // from GameObject
+ public function setCollection (collName :String, values :Array) :void
+ {
+ populateCollection(collName, values, true);
+ }
+
+ // from GameObject
+ public function addToCollection (collName :String, values :Array) :void
+ {
+ populateCollection(collName, values, false);
+ }
+
+ // from GameObject
+ public function pickFromCollection (
+ collName :String, count :int, msgOrPropName :String,
+ playerIndex :int = -1) :void
+ {
+ getFromCollection(collName, count, msgOrPropName, playerIndex,
+ false, null);
+ }
+
+ // from GameObject
+ public function dealFromCollection (
+ collName :String, count :int, msgOrPropName :String,
+ callback :Function = null, playerIndex :int = -1) :void
+ {
+ getFromCollection(collName, count, msgOrPropName, playerIndex,
+ true, callback);
+ }
+
+ // from GameObject
+ public function mergeCollection (srcColl :String, intoColl :String) :void
+ {
+ validateName(srcColl);
+ validateName(intoColl);
+ _ezObj.ezGameService.mergeCollection(_ctx.getClient(),
+ srcColl, intoColl, createLoggingListener("mergeCollection"));
+ }
+
+ // from GameObject
+ public function sendMessage (
+ messageName :String, value :Object, playerIndex :int = -1) :void
+ {
+ validateName(messageName);
+ validateValue(value);
+
+ var encoded :Object = EZObjectMarshaller.encode(value);
+ _ezObj.ezGameService.sendMessage(_ctx.getClient(),
+ messageName, encoded, playerIndex,
+ createLoggingListener("sendMessage"));
+ }
+
+ // from GameObject
+ public function sendChat (msg :String) :void
+ {
+ validateChat(msg);
+ // Post a message to the game object, the controller
+ // will listen and call localChat().
+ _ezObj.postMessage(EZGameObject.GAME_CHAT, [ msg ]);
+ }
+
+ // from GameObject
+ public function localChat (msg :String) :void
+ {
+ validateChat(msg);
+ // The sendChat() messages will end up being routed
+ // through this method on each client.
+ // TODO: make this look distinct from other system chat
+ _ctx.getChatDirector().displayInfo(null, MessageBundle.taint(msg));
+ }
+
+ // from GameObject
+ public function getPlayerNames () :Array
+ {
+ var names :Array = new Array();
+ for each (var name :Name in _ezObj.players) {
+ names.push((name == null) ? null : name.toString());
+ }
+ return names;
+ }
+
+ // from GameObject
+ public function getMyIndex () :int
+ {
+ return _ezObj.getPlayerIndex(getUsername());
+ }
+
+ // from GameObject
+ public function getTurnHolderIndex () :int
+ {
+ return _ezObj.getPlayerIndex(_ezObj.turnHolder);
+ }
+
+ // from GameObject
+ public function getWinnerIndexes () :Array /* of int */
+ {
+ var arr :Array = new Array();
+ if (_ezObj.winners != null) {
+ for (var ii :int = 0; ii < _ezObj.winners.length; ii++) {
+ if (_ezObj.winners[ii]) {
+ arr.push(ii);
+ }
+ }
+ }
+ return arr;
+ }
+
+ // from GameObject
+ public function isMyTurn () :Boolean
+ {
+ return getUsername().equals(_ezObj.turnHolder);
+ }
+
+ // from GameObject
+ public function isInPlay () :Boolean
+ {
+ return _ezObj.isInPlay();
+ }
+
+ // from GameObject
+ public function endTurn (nextPlayerIndex :int = -1) :void
+ {
+ _ezObj.ezGameService.endTurn(_ctx.getClient(), nextPlayerIndex,
+ createLoggingListener("endTurn"));
+ }
+
+ // from GameObject
+ public function endGame (winnerIndex :int, ... rest) :void
+ {
+ var winners :TypedArray = TypedArray.create(int);
+ winners.push(winnerIndex);
+ while (rest.length > 0) {
+ winners.push(int(rest.shift()));
+ }
+ _ezObj.ezGameService.endGame(_ctx.getClient(), winners,
+ createLoggingListener("endGame"));
+ }
+
+ override public function willTrigger (type :String) :Boolean
+ {
+ throw new IllegalOperationError();
+ }
+
+ override public function dispatchEvent (event :Event) :Boolean
+ {
+ // Ideally we want to not be an IEventDispatcher so that people
+ // won't try to do this on us, but if we do that, then some other
+ // object will be the target during dispatch, and that's confusing.
+ // It's really nice to be able to
+ throw new IllegalOperationError();
+ }
+
+ /**
+ * Secret function to dispatch property changed events.
+ */
+ internal function dispatch (event :Event) :void
+ {
+ try {
+ super.dispatchEvent(event);
+ } catch (err :Error) {
+ var log :Log = Log.getLog(this);
+ log.warning("Error dispatching event to user game.");
+ log.logStackTrace(err);
+ }
+ }
+
+ /**
+ * Convenience function to get our name.
+ */
+ private function getUsername () :Name
+ {
+ var body :BodyObject =
+ (_ctx.getClient().getClientObject() as BodyObject);
+ return body.getVisibleName();
+ }
+
+ /**
+ * Create a listener for service requests.
+ */
+ private function createLoggingListener (
+ service :String) :InvocationService_ConfirmListener
+ {
+ return new ConfirmAdapter(function (cause :String) :void {
+ Log.getLog(GameObject).warning("Service failure " +
+ "[service=" + service + ", cause=" + cause + "].");
+ });
+ }
+
+ /**
+ * Helper method for setCollection and addToCollection.
+ */
+ private function populateCollection (
+ collName :String, values :Array, clearExisting :Boolean) :void
+ {
+ validateName(collName);
+ if (values == null) {
+ throw new ArgumentError("Collection values may not be null.");
+ }
+ validateValue(values);
+
+ var encodedValues :TypedArray =
+ (EZObjectMarshaller.encode(values) as TypedArray);
+
+ _ezObj.ezGameService.addToCollection(
+ _ctx.getClient(), collName, encodedValues, clearExisting,
+ createLoggingListener("populateCollection"));
+ }
+
+ /**
+ * Helper method for pickFromCollection and dealFromCollection.
+ */
+ private function getFromCollection(
+ collName :String, count :int, msgOrPropName :String, playerIndex :int,
+ consume :Boolean, callback :Function) :void
+ {
+ validateName(collName);
+ validateName(msgOrPropName);
+ if (count < 1) {
+ throw new ArgumentError("Must retrieve at least one element!");
+ }
+
+ var listener :InvocationService_ConfirmListener;
+ if (callback != null) {
+ // TODO: Figure out the method sig of the callback, and what it
+ // means
+ var fn :Function = function (cause :String = null) :void {
+ if (cause == null) {
+ callback(count);
+ } else {
+ callback(parseInt(cause));
+ }
+ };
+ listener = new ConfirmAdapter(fn, fn);
+
+ } else {
+ listener = createLoggingListener("getFromCollection");
+ }
+
+ _ezObj.ezGameService.getFromCollection(
+ _ctx.getClient(), collName, consume, count, msgOrPropName,
+ playerIndex, listener);
+ }
+
+ /**
+ * Verify that the property name / value are valid.
+ */
+ private function validatePropertyChange (
+ propName :String, value :Object, index :int) :void
+ {
+ validateName(propName);
+
+ // check that we're setting an array element on an array
+ if (index >= 0) {
+ if (!(_gameData[propName] is Array)) {
+ throw new ArgumentError("Property " + propName +
+ " is not an Array.");
+ }
+ }
+
+ // validate the value too
+ validateValue(value);
+ }
+
+ /**
+ * Verify that the specified name is valid.
+ */
+ private function validateName (name :String) :void
+ {
+ if (name == null) {
+ throw new ArgumentError(
+ "Property, message, and collection names must not be null.");
+ }
+ }
+
+ private function validateChat (msg :String) :void
+ {
+ if (StringUtil.isBlank(msg)) {
+ throw new ArgumentError(
+ "Empty chat may not be displayed.");
+ }
+ }
+
+ /**
+ * Verify that the value is legal to be streamed to other clients.
+ */
+ private function validateValue (value :Object) :void
+ {
+ if (value == null) {
+ return;
+
+ } else if (value is IExternalizable) {
+ throw new ArgumentError(
+ "IExternalizable is not yet supported");
+
+ } else if (value is Array) {
+ if (ClassUtil.getClass(value) != Array) {
+ // We can't allow arrays to be serialized as IExternalizables
+ // because we need to know element values (opaquely) on the
+ // server. Also, we don't allow other types because we wouldn't
+ // create the right class on the other side.
+ throw new ArgumentError(
+ "Custom array subclasses are not supported");
+ }
+ // then, continue on with the sub-properties check (below)
+
+ } else {
+ var type :String = typeof(value);
+ if (type == "number" || type == "string" || type == "boolean" ) {
+ // kosher!
+ return;
+ }
+ if (ClassUtil.getClass(value) != Object) {
+ throw new ArgumentError(
+ "Non-simple properties may not be set.");
+ }
+ // fall through and verify the object's sub-properties
+ }
+
+ // check sub-properties (of arrays and objects)
+ for each (var arrValue :Object in (value as Array)) {
+ validateValue(arrValue);
+ }
+ }
+
+ protected var _ctx :CrowdContext;
+
+ protected var _ezObj :EZGameObject;
+
+ protected var _gameData :GameData;
+}
+}
diff --git a/src/as/com/threerings/ezgame/data/EZGameConfig.as b/src/as/com/threerings/ezgame/data/EZGameConfig.as
new file mode 100644
index 00000000..695425c4
--- /dev/null
+++ b/src/as/com/threerings/ezgame/data/EZGameConfig.as
@@ -0,0 +1,83 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data {
+
+import com.threerings.util.MessageBundle;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+import com.threerings.crowd.client.PlaceController;
+
+import com.threerings.parlor.game.client.GameConfigurator;
+import com.threerings.parlor.game.data.GameConfig;
+import com.threerings.parlor.game.data.PartyGameConfig;
+import com.threerings.parlor.game.data.PartyGameCodes;
+
+import com.threerings.ezgame.client.EZGameController;
+
+/**
+ * A game config for a simple multiplayer ez game.
+ */
+public /* abstract */ class EZGameConfig extends GameConfig
+ implements PartyGameConfig
+{
+ /** A creator-submitted name of the game. */
+ public var gameName :String;
+
+ override public function createController () :PlaceController
+ {
+ return new EZGameController();
+ }
+
+ override public function getBundleName () :String
+ {
+ return "general";
+ }
+
+ // TODO
+ //override public function createConfigurator () :GameConfigurator
+
+ override public function getGameName () :String
+ {
+ return MessageBundle.taint(gameName);
+ }
+
+ // from PartyGameConfig
+ public function getPartyGameType () :int
+ {
+ // TODO
+ return PartyGameCodes.NOT_PARTY_GAME;
+ }
+
+ override public function equals (other :Object) :Boolean
+ {
+ if (!super.equals(other)) {
+ return false;
+ }
+
+ var that :EZGameConfig = (other as EZGameConfig);
+ return (this.gameName === that.gameName);
+ }
+
+ override public function hashCode () :int
+ {
+ return super.hashCode(); // TODO: incorporate gamename?
+ }
+
+ override public function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+
+ out.writeField(gameName);
+ }
+
+ override public function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+
+ gameName = (ins.readField(String) as String);
+ }
+}
+}
diff --git a/src/as/com/threerings/ezgame/data/EZGameMarshaller.as b/src/as/com/threerings/ezgame/data/EZGameMarshaller.as
new file mode 100644
index 00000000..9c3ec2de
--- /dev/null
+++ b/src/as/com/threerings/ezgame/data/EZGameMarshaller.as
@@ -0,0 +1,125 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data {
+
+import flash.utils.ByteArray;
+
+import com.threerings.util.langBoolean;
+import com.threerings.util.Integer;
+
+import com.threerings.io.TypedArray;
+
+import com.threerings.ezgame.client.EZGameService;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.client.InvocationService_ConfirmListener;
+import com.threerings.presents.client.InvocationService_InvocationListener;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.data.InvocationMarshaller_ConfirmMarshaller;
+import com.threerings.presents.data.InvocationMarshaller_ListenerMarshaller;
+import com.threerings.presents.dobj.InvocationResponseEvent;
+
+/**
+ * Provides the implementation of the {@link EZGameService} interface
+ * that marshalls the arguments and delivers the request to the provider
+ * on the server. Also provides an implementation of the response listener
+ * interfaces that marshall the response arguments and deliver them back
+ * to the requesting client.
+ */
+public class EZGameMarshaller extends InvocationMarshaller
+ implements EZGameService
+{
+ /** The method id used to dispatch {@link #addToCollection} requests. */
+ public static const ADD_TO_COLLECTION :int = 1;
+
+ // documentation inherited from interface
+ public function addToCollection (arg1 :Client, arg2 :String, arg3 :TypedArray /* of ByteArray */, arg4 :Boolean, arg5 :InvocationService_InvocationListener) :void
+ {
+ var listener5 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, ADD_TO_COLLECTION, [
+ arg2, arg3, langBoolean.valueOf(arg4), listener5
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #endGame} requests. */
+ public static const END_GAME :int = 2;
+
+ // documentation inherited from interface
+ public function endGame (arg1 :Client, arg2 :TypedArray, arg3 :InvocationService_InvocationListener) :void
+ {
+ var listener3 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, END_GAME, [
+ arg2, listener3
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #endTurn} requests. */
+ public static const END_TURN :int = 3;
+
+ // documentation inherited from interface
+ public function endTurn (arg1 :Client, arg2 :int, arg3 :InvocationService_InvocationListener) :void
+ {
+ var listener3 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, END_TURN, [
+ Integer.valueOf(arg2), listener3
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #getFromCollection} requests. */
+ public static const GET_FROM_COLLECTION :int = 4;
+
+ // documentation inherited from interface
+ public function getFromCollection (arg1 :Client, arg2 :String, arg3 :Boolean, arg4 :int, arg5 :String, arg6 :int, arg7 :InvocationService_ConfirmListener) :void
+ {
+ var listener7 :InvocationMarshaller_ConfirmMarshaller = new InvocationMarshaller_ConfirmMarshaller();
+ listener7.listener = arg7;
+ sendRequest(arg1, GET_FROM_COLLECTION, [
+ arg2, langBoolean.valueOf(arg3), Integer.valueOf(arg4), arg5, Integer.valueOf(arg6), listener7
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #mergeCollection} requests. */
+ public static const MERGE_COLLECTION :int = 5;
+
+ // documentation inherited from interface
+ public function mergeCollection (arg1 :Client, arg2 :String, arg3 :String, arg4 :InvocationService_InvocationListener) :void
+ {
+ var listener4 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener4.listener = arg4;
+ sendRequest(arg1, MERGE_COLLECTION, [
+ arg2, arg3, listener4
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #sendMessage} requests. */
+ public static const SEND_MESSAGE :int = 6;
+
+ // documentation inherited from interface
+ public function sendMessage (arg1 :Client, arg2 :String, arg3 :Object, arg4 :int, arg5 :InvocationService_InvocationListener) :void
+ {
+ var listener5 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, SEND_MESSAGE, [
+ arg2, arg3, Integer.valueOf(arg4), listener5
+ ]);
+ }
+
+ /** The method id used to dispatch {@link #setProperty} requests. */
+ public static const SET_PROPERTY :int = 7;
+
+ // documentation inherited from interface
+ public function setProperty (arg1 :Client, arg2 :String, arg3 :Object, arg4 :int, arg5 :InvocationService_InvocationListener) :void
+ {
+ var listener5 :InvocationMarshaller_ListenerMarshaller = new InvocationMarshaller_ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, SET_PROPERTY, [
+ arg2, arg3, Integer.valueOf(arg4), listener5
+ ]);
+ }
+
+}
+}
diff --git a/src/as/com/threerings/ezgame/data/EZGameObject.as b/src/as/com/threerings/ezgame/data/EZGameObject.as
new file mode 100644
index 00000000..0e96761c
--- /dev/null
+++ b/src/as/com/threerings/ezgame/data/EZGameObject.as
@@ -0,0 +1,141 @@
+package com.threerings.ezgame.data {
+
+import flash.events.Event;
+
+import flash.utils.ByteArray;
+
+import com.threerings.util.Name;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.TypedArray;
+
+import com.threerings.parlor.game.data.GameObject;
+import com.threerings.parlor.turn.data.TurnGameObject;
+
+import com.threerings.ezgame.util.EZObjectMarshaller;
+
+public class EZGameObject extends GameObject
+ implements TurnGameObject
+{
+ /** The identifier for a MessageEvent containing a user message. */
+ public static const USER_MESSAGE :String = "Umsg";
+
+ /** The identifier for a MessageEvent containing game-system chat. */
+ public static const GAME_CHAT :String = "Uchat";
+
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the turnHolder field. */
+ public static const TURN_HOLDER :String = "turnHolder";
+
+ /** The field name of the ezGameService field. */
+ public static const EZ_GAME_SERVICE :String = "ezGameService";
+ // AUTO-GENERATED: FIELDS END
+
+ /** The current turn holder. */
+ public var turnHolder :Name;
+
+ /** The service interface for requesting special things from the server. */
+ public var ezGameService :EZGameMarshaller;
+
+ /**
+ * Access the underlying user properties.
+ */
+ public function getUserProps () :Object
+ {
+ return _props;
+ }
+
+ // from TurnGameObject
+ public function getTurnHolderFieldName () :String
+ {
+ return TURN_HOLDER;
+ }
+
+ // from TurnGameObject
+ public function getTurnHolder () :Name
+ {
+ return turnHolder;
+ }
+
+ // from TurnGameObject
+ public function getPlayers () :TypedArray /* of Name */
+ {
+ return players;
+ }
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the turnHolder field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public function setTurnHolder (value :Name) :void
+ {
+ var ovalue :Name = this.turnHolder;
+ requestAttributeChange(
+ TURN_HOLDER, value, ovalue);
+ this.turnHolder = value;
+ }
+ // AUTO-GENERATED: METHODS END
+
+ /**
+ * Called by a PropertySetEvent to enact a property change.
+ * @return the old value
+ */
+ public function applyPropertySet (
+ propName :String, value :Object, index :int) :Object
+ {
+ var oldValue :Object = _props[propName];
+ if (index >= 0) {
+ // set an array element
+ var arr :Array = (oldValue as Array);
+ if (arr == null) {
+ arr = [];
+ _props[propName] = arr;
+ }
+ oldValue = arr[index];
+ arr[index] = value;
+
+ } else if (value != null) {
+ // normal property set
+ _props[propName] = value;
+
+ } else {
+ // remove a property
+ delete _props[propName];
+ }
+ return oldValue;
+ }
+
+ override public function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+
+ throw new Error("Un-needed");
+ }
+
+ override public function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+
+ // first read any regular bits
+ turnHolder = (ins.readObject() as Name);
+ ezGameService = (ins.readObject() as EZGameMarshaller);
+
+ // then user properties
+ var count :int = ins.readInt();
+ while (count-- > 0) {
+ var key :String = ins.readUTF();
+ var value :Object = EZObjectMarshaller.decode(ins.readObject());
+ _props[key] = value;
+ }
+ }
+
+ /** The raw properties set by the game. */
+ protected var _props :Object = new Object();
+}
+}
diff --git a/src/as/com/threerings/ezgame/data/PropertySetEvent.as b/src/as/com/threerings/ezgame/data/PropertySetEvent.as
new file mode 100644
index 00000000..f72c9fde
--- /dev/null
+++ b/src/as/com/threerings/ezgame/data/PropertySetEvent.as
@@ -0,0 +1,87 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data {
+
+import flash.utils.ByteArray;
+import flash.utils.IExternalizable;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamer;
+
+import com.threerings.presents.dobj.DObject;
+import com.threerings.presents.dobj.NamedEvent;
+
+import com.threerings.ezgame.util.EZObjectMarshaller;
+
+/**
+ * Represents a property change on the actionscript object we
+ * use in FlashGameObject.
+ */
+public class PropertySetEvent extends NamedEvent
+{
+ /**
+ * Create a PropertySetEvent.
+ */
+ public function PropertySetEvent () // unserialize-only
+ {
+ super(0, null);
+ }
+
+ /**
+ * Get the value that was set for the property.
+ */
+ public function getValue () :Object
+ {
+ return _data;
+ }
+
+ /**
+ * Get the old value.
+ */
+ public function getOldValue () :Object
+ {
+ return _oldValue;
+ }
+
+ /**
+ * Get the index, or -1 if not applicable.
+ */
+ public function getIndex () :int
+ {
+ return _index;
+ }
+
+ override public function applyToObject (target :DObject) :Boolean
+ {
+ _oldValue =
+ EZGameObject(target).applyPropertySet(_name, _data, _index);
+ return true;
+ }
+
+ override protected function notifyListener (listener :Object) :void
+ {
+ if (listener is PropertySetListener) {
+ (listener as PropertySetListener).propertyWasSet(this);
+ }
+ }
+
+ override public function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+
+ _index = ins.readInt();
+ _data = EZObjectMarshaller.decode(ins.readObject());
+ }
+
+ /** The index of the property, if applicable. */
+ protected var _index :int;
+
+ /** The client-side data that is assigned to this property. */
+ protected var _data :Object;
+
+ /** The old value. */
+ protected var _oldValue :Object;
+}
+}
diff --git a/src/as/com/threerings/ezgame/data/PropertySetListener.as b/src/as/com/threerings/ezgame/data/PropertySetListener.as
new file mode 100644
index 00000000..4d11258a
--- /dev/null
+++ b/src/as/com/threerings/ezgame/data/PropertySetListener.as
@@ -0,0 +1,17 @@
+package com.threerings.ezgame.data {
+
+import com.threerings.presents.dobj.ChangeListener;
+
+/**
+ * A listener that will be notified of PropertySet events.
+ */
+public interface PropertySetListener extends ChangeListener
+{
+ /**
+ * Called when a property was set on a flash game object.
+ * This will be called after the event has been applied
+ * to the object.
+ */
+ function propertyWasSet (event :PropertySetEvent) :void;
+}
+}
diff --git a/src/as/com/threerings/ezgame/util/EZObjectMarshaller.as b/src/as/com/threerings/ezgame/util/EZObjectMarshaller.as
new file mode 100644
index 00000000..c102e2a7
--- /dev/null
+++ b/src/as/com/threerings/ezgame/util/EZObjectMarshaller.as
@@ -0,0 +1,73 @@
+package com.threerings.ezgame.util {
+
+import flash.net.ObjectEncoding;
+
+import flash.system.ApplicationDomain;
+
+import flash.utils.ByteArray;
+import flash.utils.Endian;
+
+import com.threerings.io.TypedArray;
+
+/**
+ * Utility methods for transferring flash properties via
+ * the presents dobj system.
+ */
+public class EZObjectMarshaller
+{
+ /**
+ * Encode the specified object as either a byte[] or a byte[][] if it
+ * is an array. The specific mechanism of encoding is not important,
+ * as long as decode returns a clone of the original object.
+ *
+ * Currently, cycles in the object graph are preserved on the other end.
+ * TODO: serialize IExternalizable implementations, and take
+ * into account the ApplicationDomain.
+ */
+ public static function encode (
+ obj :Object, encodeArrayElements :Boolean = true) :Object
+ {
+ if (obj == null) {
+ return null;
+ }
+ if (encodeArrayElements && obj is Array) {
+ var src :Array = (obj as Array);
+ var dest :TypedArray = TypedArray.create(ByteArray);
+ for each (var o :Object in src) {
+ dest.push(encode(o, false));
+ }
+ return dest;
+ }
+
+ // TODO: Our own encoding, that takes into account
+ // the ApplicationDomain
+ var bytes :ByteArray = new ByteArray();
+ bytes.endian = Endian.BIG_ENDIAN;
+ bytes.objectEncoding = ObjectEncoding.AMF3;
+ bytes.writeObject(obj);
+ return bytes;
+ }
+
+ public static function decode (encoded :Object) :Object
+ {
+ if (encoded == null) {
+ return null;
+ }
+ if (encoded is TypedArray) {
+ var src :TypedArray = (encoded as TypedArray);
+ var dest :Array = [];
+ for each (var b :ByteArray in src) {
+ dest.push(decode(b));
+ }
+ return dest;
+ }
+ var bytes :ByteArray = (encoded as ByteArray);
+
+ // TODO: Our own decoding, that takes into account
+ // the ApplicationDomain
+ bytes.endian = Endian.BIG_ENDIAN;
+ bytes.objectEncoding = ObjectEncoding.AMF3;
+ return bytes.readObject();
+ }
+}
+}
diff --git a/src/java/com/threerings/ezgame/client/EZGameService.java b/src/java/com/threerings/ezgame/client/EZGameService.java
new file mode 100644
index 00000000..f280b2f4
--- /dev/null
+++ b/src/java/com/threerings/ezgame/client/EZGameService.java
@@ -0,0 +1,67 @@
+//
+// $Id$
+
+package com.threerings.ezgame.client;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+
+/**
+ * Provides services for ez games.
+ */
+public interface EZGameService extends InvocationService
+{
+ /**
+ * Request to set the specified property.
+ */
+ public void setProperty (
+ Client client, String propName, Object value, int index,
+ InvocationListener listener);
+
+ /**
+ * Request to end the turn, possibly futzing the next turn holder unless
+ * -1 is specified for the nextPlayerIndex.
+ */
+ public void endTurn (
+ Client client, int nextPlayerIndex, InvocationListener listener);
+
+ /**
+ * Request to end the game, with the specified player indices assigned
+ * as winners.
+ */
+ public void endGame (
+ Client client, int[] winners, InvocationListener listener);
+
+ /**
+ * Request to send a private message to one other player in
+ * the game.
+ */
+ public void sendMessage (
+ Client client, String msgName, Object value, int playerIdx,
+ InvocationListener listener);
+
+ /**
+ * Add to the specified named collection.
+ *
+ * @param clearExisting if true, wipe the old contents.
+ */
+ public void addToCollection (
+ Client client, String collName, byte[][] data, boolean clearExisting,
+ InvocationListener listener);
+
+ /**
+ * Merge the specified collection into the other.
+ */
+ public void mergeCollection (
+ Client client, String srcColl, String intoColl,
+ InvocationListener listener);
+
+ /**
+ * Pick or deal some number of elements from the specified collection,
+ * and either set a property in the flash object, or delivery the
+ * picks to the specified player index via a game message.
+ */
+ public void getFromCollection (
+ Client client, String collName, boolean consume, int count,
+ String msgOrPropName, int playerIndex, ConfirmListener listener);
+}
diff --git a/src/java/com/threerings/ezgame/data/EZGameConfig.java b/src/java/com/threerings/ezgame/data/EZGameConfig.java
new file mode 100644
index 00000000..49ca5a36
--- /dev/null
+++ b/src/java/com/threerings/ezgame/data/EZGameConfig.java
@@ -0,0 +1,68 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data;
+
+import com.threerings.util.MessageBundle;
+
+import com.threerings.parlor.game.client.GameConfigurator;
+import com.threerings.parlor.game.data.GameConfig;
+import com.threerings.parlor.game.data.PartyGameConfig;
+
+/**
+ * A game config for a simple multiplayer game.
+ */
+public class EZGameConfig extends GameConfig
+ implements PartyGameConfig
+{
+ /** A creator-submitted name of the game. */
+ public String gameName;
+
+ // from abstract GameConfig
+ public String getBundleName ()
+ {
+ return "general";
+ }
+
+ // from abstract GameConfig
+ public GameConfigurator createConfigurator ()
+ {
+ return null; // nothing here on the java side
+ }
+
+ @Override
+ public String getGameName ()
+ {
+ return MessageBundle.taint(gameName);
+ }
+
+ // from abstract PlaceConfig
+ public String getManagerClassName ()
+ {
+ return "com.threerings.ezgame.server.EZGameManager";
+ }
+
+ // from PartyGameConfig
+ public byte getPartyGameType ()
+ {
+ // TODO
+ return NOT_PARTY_GAME;
+ }
+
+ @Override
+ public boolean equals (Object other)
+ {
+ if (!super.equals(other)) {
+ return false;
+ }
+
+ EZGameConfig that = (EZGameConfig) other;
+ return this.gameName.equals(that.gameName);
+ }
+
+ @Override
+ public int hashCode ()
+ {
+ return super.hashCode(); // TODO: incorp game name?
+ }
+}
diff --git a/src/java/com/threerings/ezgame/data/EZGameMarshaller.java b/src/java/com/threerings/ezgame/data/EZGameMarshaller.java
new file mode 100644
index 00000000..4419aae9
--- /dev/null
+++ b/src/java/com/threerings/ezgame/data/EZGameMarshaller.java
@@ -0,0 +1,113 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data;
+
+import com.threerings.ezgame.client.EZGameService;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.dobj.InvocationResponseEvent;
+
+/**
+ * Provides the implementation of the {@link EZGameService} interface
+ * that marshalls the arguments and delivers the request to the provider
+ * on the server. Also provides an implementation of the response listener
+ * interfaces that marshall the response arguments and deliver them back
+ * to the requesting client.
+ */
+public class EZGameMarshaller extends InvocationMarshaller
+ implements EZGameService
+{
+ /** The method id used to dispatch {@link #addToCollection} requests. */
+ public static final int ADD_TO_COLLECTION = 1;
+
+ // documentation inherited from interface
+ public void addToCollection (Client arg1, String arg2, byte[][] arg3, boolean arg4, InvocationService.InvocationListener arg5)
+ {
+ ListenerMarshaller listener5 = new ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, ADD_TO_COLLECTION, new Object[] {
+ arg2, arg3, Boolean.valueOf(arg4), listener5
+ });
+ }
+
+ /** The method id used to dispatch {@link #endGame} requests. */
+ public static final int END_GAME = 2;
+
+ // documentation inherited from interface
+ public void endGame (Client arg1, int[] arg2, InvocationService.InvocationListener arg3)
+ {
+ ListenerMarshaller listener3 = new ListenerMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, END_GAME, new Object[] {
+ arg2, listener3
+ });
+ }
+
+ /** The method id used to dispatch {@link #endTurn} requests. */
+ public static final int END_TURN = 3;
+
+ // documentation inherited from interface
+ public void endTurn (Client arg1, int arg2, InvocationService.InvocationListener arg3)
+ {
+ ListenerMarshaller listener3 = new ListenerMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, END_TURN, new Object[] {
+ Integer.valueOf(arg2), listener3
+ });
+ }
+
+ /** The method id used to dispatch {@link #getFromCollection} requests. */
+ public static final int GET_FROM_COLLECTION = 4;
+
+ // documentation inherited from interface
+ public void getFromCollection (Client arg1, String arg2, boolean arg3, int arg4, String arg5, int arg6, InvocationService.ConfirmListener arg7)
+ {
+ InvocationMarshaller.ConfirmMarshaller listener7 = new InvocationMarshaller.ConfirmMarshaller();
+ listener7.listener = arg7;
+ sendRequest(arg1, GET_FROM_COLLECTION, new Object[] {
+ arg2, Boolean.valueOf(arg3), Integer.valueOf(arg4), arg5, Integer.valueOf(arg6), listener7
+ });
+ }
+
+ /** The method id used to dispatch {@link #mergeCollection} requests. */
+ public static final int MERGE_COLLECTION = 5;
+
+ // documentation inherited from interface
+ public void mergeCollection (Client arg1, String arg2, String arg3, InvocationService.InvocationListener arg4)
+ {
+ ListenerMarshaller listener4 = new ListenerMarshaller();
+ listener4.listener = arg4;
+ sendRequest(arg1, MERGE_COLLECTION, new Object[] {
+ arg2, arg3, listener4
+ });
+ }
+
+ /** The method id used to dispatch {@link #sendMessage} requests. */
+ public static final int SEND_MESSAGE = 6;
+
+ // documentation inherited from interface
+ public void sendMessage (Client arg1, String arg2, Object arg3, int arg4, InvocationService.InvocationListener arg5)
+ {
+ ListenerMarshaller listener5 = new ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, SEND_MESSAGE, new Object[] {
+ arg2, arg3, Integer.valueOf(arg4), listener5
+ });
+ }
+
+ /** The method id used to dispatch {@link #setProperty} requests. */
+ public static final int SET_PROPERTY = 7;
+
+ // documentation inherited from interface
+ public void setProperty (Client arg1, String arg2, Object arg3, int arg4, InvocationService.InvocationListener arg5)
+ {
+ ListenerMarshaller listener5 = new ListenerMarshaller();
+ listener5.listener = arg5;
+ sendRequest(arg1, SET_PROPERTY, new Object[] {
+ arg2, arg3, Integer.valueOf(arg4), listener5
+ });
+ }
+
+}
diff --git a/src/java/com/threerings/ezgame/data/EZGameObject.java b/src/java/com/threerings/ezgame/data/EZGameObject.java
new file mode 100644
index 00000000..829e93ab
--- /dev/null
+++ b/src/java/com/threerings/ezgame/data/EZGameObject.java
@@ -0,0 +1,162 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data;
+
+import java.io.IOException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.threerings.util.Name;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamable;
+
+import com.threerings.parlor.game.data.GameObject;
+import com.threerings.parlor.turn.data.TurnGameObject;
+
+/**
+ * Contains the data for an ez game.
+ */
+public class EZGameObject extends GameObject
+ implements TurnGameObject
+{
+ /** The identifier for a MessageEvent containing a user message. */
+ public static final String USER_MESSAGE = "Umsg";
+
+ /** The identifier for a MessageEvent containing game-system chat. */
+ public static final String GAME_CHAT = "Uchat";
+
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the turnHolder field. */
+ public static final String TURN_HOLDER = "turnHolder";
+
+ /** The field name of the ezGameService field. */
+ public static final String EZ_GAME_SERVICE = "ezGameService";
+ // AUTO-GENERATED: FIELDS END
+
+ /** The current turn holder. */
+ public Name turnHolder;
+
+ /** The service interface for requesting special things from the server. */
+ public EZGameMarshaller ezGameService;
+
+ // from TurnGameObject
+ public String getTurnHolderFieldName ()
+ {
+ return TURN_HOLDER;
+ }
+
+ // from TurnGameObject
+ public Name getTurnHolder ()
+ {
+ return turnHolder;
+ }
+
+ // from TurnGameObject
+ public Name[] getPlayers ()
+ {
+ return players;
+ }
+
+ /**
+ * Called by PropertySetEvent to effect the property update.
+ */
+ protected void applyPropertySet (String propName, Object data, int index)
+ {
+ if (index >= 0) {
+ Object something = _props.get(propName);
+ byte[][] arr = (something instanceof byte[][])
+ ? (byte[][]) something : null;
+ if (arr == null || arr.length <= index) {
+ // TODO: in case a user sets element 0 and element 90000,
+ // we might want to store elements in a hash
+ byte[][] newArr = new byte[index + 1][];
+ if (arr != null) {
+ System.arraycopy(arr, 0, newArr, 0, arr.length);
+ }
+ _props.put(propName, newArr);
+ arr = newArr;
+ }
+ arr[index] = (byte[]) data;
+
+ } else if (data != null) {
+ _props.put(propName, data);
+
+ } else {
+ _props.remove(propName);
+ }
+ }
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the turnHolder field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setTurnHolder (Name value)
+ {
+ Name ovalue = this.turnHolder;
+ requestAttributeChange(
+ TURN_HOLDER, value, ovalue);
+ this.turnHolder = value;
+ }
+
+ /**
+ * Requests that the ezGameService field be set to the
+ * specified value. The local value will be updated immediately and an
+ * event will be propagated through the system to notify all listeners
+ * that the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setEZGameService (EZGameMarshaller value)
+ {
+ EZGameMarshaller ovalue = this.ezGameService;
+ requestAttributeChange(
+ EZ_GAME_SERVICE, value, ovalue);
+ this.ezGameService = value;
+ }
+ // AUTO-GENERATED: METHODS END
+
+ /**
+ * A custom serialization method.
+ */
+ public void writeObject (ObjectOutputStream out)
+ throws IOException
+ {
+ out.defaultWriteObject();
+
+ // write the number of properties, followed by each one
+ out.writeInt(_props.size());
+ for (Map.Entry entry : _props.entrySet()) {
+ out.writeUTF(entry.getKey());
+ out.writeObject(entry.getValue());
+ }
+ }
+
+ /**
+ * A custom serialization method.
+ */
+ public void readObject (ObjectInputStream ins)
+ throws IOException, ClassNotFoundException
+ {
+ ins.defaultReadObject();
+
+ _props.clear();
+ int count = ins.readInt();
+ while (count-- > 0) {
+ String key = ins.readUTF();
+ _props.put(key, ins.readObject());
+ }
+ }
+
+ /** The current state of game data, opaque to us here on the server. */
+ protected transient HashMap _props =
+ new HashMap();
+}
diff --git a/src/java/com/threerings/ezgame/data/PropertySetEvent.java b/src/java/com/threerings/ezgame/data/PropertySetEvent.java
new file mode 100644
index 00000000..c2e70ec2
--- /dev/null
+++ b/src/java/com/threerings/ezgame/data/PropertySetEvent.java
@@ -0,0 +1,50 @@
+//
+// $Id$
+
+package com.threerings.ezgame.data;
+
+import java.io.IOException;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamable;
+import com.threerings.io.Streamer;
+
+import com.threerings.presents.dobj.DObject;
+import com.threerings.presents.dobj.NamedEvent;
+
+/**
+ * Represents a property change on the actionscript object we
+ * use in EZGameObject.
+ */
+public class PropertySetEvent extends NamedEvent
+{
+ /** Suitable for unserialization. */
+ public PropertySetEvent ()
+ {
+ }
+
+ /**
+ * Create a PropertySetEvent.
+ */
+ public PropertySetEvent (
+ int targetOid, String propName, Object value, int index)
+ {
+ super(targetOid, propName);
+ _data = value;
+ _index = index;
+ }
+
+ // from abstract DEvent
+ public boolean applyToObject (DObject target)
+ {
+ ((EZGameObject) target).applyPropertySet(_name, _data, _index);
+ return true;
+ }
+
+ /** The index. */
+ protected int _index;
+
+ /** The client-side data that is assigned to this property. */
+ protected Object _data;
+}
diff --git a/src/java/com/threerings/ezgame/server/EZGameDispatcher.java b/src/java/com/threerings/ezgame/server/EZGameDispatcher.java
new file mode 100644
index 00000000..c5c4e029
--- /dev/null
+++ b/src/java/com/threerings/ezgame/server/EZGameDispatcher.java
@@ -0,0 +1,95 @@
+//
+// $Id$
+
+package com.threerings.ezgame.server;
+
+import com.threerings.ezgame.client.EZGameService;
+import com.threerings.ezgame.data.EZGameMarshaller;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.server.InvocationDispatcher;
+import com.threerings.presents.server.InvocationException;
+
+/**
+ * Dispatches requests to the {@link EZGameProvider}.
+ */
+public class EZGameDispatcher extends InvocationDispatcher
+{
+ /**
+ * Creates a dispatcher that may be registered to dispatch invocation
+ * service requests for the specified provider.
+ */
+ public EZGameDispatcher (EZGameProvider provider)
+ {
+ this.provider = provider;
+ }
+
+ // documentation inherited
+ public InvocationMarshaller createMarshaller ()
+ {
+ return new EZGameMarshaller();
+ }
+
+ // documentation inherited
+ public void dispatchRequest (
+ ClientObject source, int methodId, Object[] args)
+ throws InvocationException
+ {
+ switch (methodId) {
+ case EZGameMarshaller.ADD_TO_COLLECTION:
+ ((EZGameProvider)provider).addToCollection(
+ source,
+ (String)args[0], (byte[][])args[1], ((Boolean)args[2]).booleanValue(), (InvocationService.InvocationListener)args[3]
+ );
+ return;
+
+ case EZGameMarshaller.END_GAME:
+ ((EZGameProvider)provider).endGame(
+ source,
+ (int[])args[0], (InvocationService.InvocationListener)args[1]
+ );
+ return;
+
+ case EZGameMarshaller.END_TURN:
+ ((EZGameProvider)provider).endTurn(
+ source,
+ ((Integer)args[0]).intValue(), (InvocationService.InvocationListener)args[1]
+ );
+ return;
+
+ case EZGameMarshaller.GET_FROM_COLLECTION:
+ ((EZGameProvider)provider).getFromCollection(
+ source,
+ (String)args[0], ((Boolean)args[1]).booleanValue(), ((Integer)args[2]).intValue(), (String)args[3], ((Integer)args[4]).intValue(), (InvocationService.ConfirmListener)args[5]
+ );
+ return;
+
+ case EZGameMarshaller.MERGE_COLLECTION:
+ ((EZGameProvider)provider).mergeCollection(
+ source,
+ (String)args[0], (String)args[1], (InvocationService.InvocationListener)args[2]
+ );
+ return;
+
+ case EZGameMarshaller.SEND_MESSAGE:
+ ((EZGameProvider)provider).sendMessage(
+ source,
+ (String)args[0], (Object)args[1], ((Integer)args[2]).intValue(), (InvocationService.InvocationListener)args[3]
+ );
+ return;
+
+ case EZGameMarshaller.SET_PROPERTY:
+ ((EZGameProvider)provider).setProperty(
+ source,
+ (String)args[0], (Object)args[1], ((Integer)args[2]).intValue(), (InvocationService.InvocationListener)args[3]
+ );
+ return;
+
+ default:
+ super.dispatchRequest(source, methodId, args);
+ return;
+ }
+ }
+}
diff --git a/src/java/com/threerings/ezgame/server/EZGameManager.java b/src/java/com/threerings/ezgame/server/EZGameManager.java
new file mode 100644
index 00000000..d92b91f7
--- /dev/null
+++ b/src/java/com/threerings/ezgame/server/EZGameManager.java
@@ -0,0 +1,304 @@
+//
+// $Id$
+
+package com.threerings.ezgame.server;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import com.samskivert.util.ArrayUtil;
+import com.samskivert.util.CollectionUtil;
+import com.samskivert.util.RandomUtil;
+
+import com.threerings.util.Name;
+
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationCodes;
+import com.threerings.presents.dobj.AccessController;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.server.InvocationException;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.crowd.server.CrowdServer;
+
+import com.threerings.parlor.game.server.GameManager;
+
+import com.threerings.parlor.turn.server.TurnGameManager;
+
+import com.threerings.ezgame.data.EZGameObject;
+import com.threerings.ezgame.data.EZGameMarshaller;
+import com.threerings.ezgame.data.PropertySetEvent;
+
+/**
+ * A manager for "ez" games.
+ */
+public class EZGameManager extends GameManager
+ implements EZGameProvider, TurnGameManager
+{
+ public EZGameManager ()
+ {
+ addDelegate(_turnDelegate = new EZGameTurnDelegate(this));
+ }
+
+ // from TurnGameManager
+ public void turnWillStart ()
+ {
+ }
+
+ // from TurnGameManager
+ public void turnDidStart ()
+ {
+ }
+
+ // from TurnGameManager
+ public void turnDidEnd ()
+ {
+ }
+
+ // from EZGameProvider
+ public void endTurn (
+ ClientObject caller, int nextPlayerIndex,
+ InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateStateModification(caller);
+ _turnDelegate.endTurn(nextPlayerIndex);
+ }
+
+ // from EZGameProvider
+ public void endGame (
+ ClientObject caller, int[] winners,
+ InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateStateModification(caller);
+
+ _winnerIndexes = winners;
+ endGame();
+ }
+
+ // from EZGameProvider
+ public void sendMessage (
+ ClientObject caller, String msg, Object data, int playerIdx,
+ InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateUser(caller);
+
+ if (playerIdx < 0 || playerIdx >= _gameObj.players.length) {
+ _gameObj.postMessage(EZGameObject.USER_MESSAGE,
+ new Object[] { msg, data });
+
+ } else {
+ sendPrivateMessage(playerIdx, msg, data);
+ }
+ }
+
+ // from EZGameProvider
+ public void setProperty (
+ ClientObject caller, String propName, Object data, int index,
+ InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateUser(caller);
+ setProperty(propName, data, index);
+ }
+
+ // from EZGameProvider
+ public void addToCollection (
+ ClientObject caller, String collName, byte[][] data,
+ boolean clearExisting, InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateUser(caller);
+ if (_collections == null) {
+ _collections = new HashMap>();
+ }
+
+ // figure out if we're adding to an existing collection
+ // or creating a new one
+ ArrayList list = null;
+ if (!clearExisting) {
+ list = _collections.get(collName);
+ }
+ if (list == null) {
+ list = new ArrayList();
+ _collections.put(collName, list);
+ }
+
+ CollectionUtil.addAll(list, data);
+ }
+
+ // from EZGameProvider
+ public void getFromCollection (
+ ClientObject caller, String collName, boolean consume, int count,
+ String msgOrPropName, int playerIndex,
+ InvocationService.ConfirmListener listener)
+ throws InvocationException
+ {
+ validateUser(caller);
+
+ int srcSize = 0;
+ if (_collections != null) {
+ ArrayList src = _collections.get(collName);
+ srcSize = (src == null) ? 0 : src.size();
+ if (srcSize >= count) {
+ byte[][] result = new byte[count][];
+ for (int ii=0; ii < count; ii++) {
+ int pick = RandomUtil.getInt(srcSize);
+ if (consume) {
+ result[ii] = src.remove(pick);
+ srcSize--;
+
+ } else {
+ result[ii] = src.get(pick);
+ }
+ }
+
+ if (playerIndex >= 0 && playerIndex < _gameObj.players.length) {
+ sendPrivateMessage(playerIndex, msgOrPropName, result);
+
+ } else {
+ setProperty(msgOrPropName, result, -1);
+ }
+ // SUCCESS!
+ listener.requestProcessed();
+ return;
+ }
+ }
+
+ // TODO: decide what we want to return here
+ throw new InvocationException(String.valueOf(srcSize));
+ }
+
+ // from EZGameProvider
+ public void mergeCollection (
+ ClientObject caller, String srcColl, String intoColl,
+ InvocationService.InvocationListener listener)
+ throws InvocationException
+ {
+ validateUser(caller);
+
+ // non-existent collections are treated as empty, so if the
+ // source doesn't exist, we silently accept it
+ if (_collections != null) {
+ ArrayList src = _collections.remove(srcColl);
+ if (src != null) {
+ ArrayList dest = _collections.get(intoColl);
+ if (dest == null) {
+ _collections.put(intoColl, src);
+ } else {
+ dest.addAll(src);
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to send a private message to the specified player
+ * index (must already be verified).
+ */
+ protected void sendPrivateMessage (
+ int playerIdx, String msg, Object data)
+ throws InvocationException
+ {
+ BodyObject target = getPlayer(playerIdx);
+ if (target == null) {
+ // TODO: this code has no corresponding translation
+ throw new InvocationException("m.player_not_around");
+ }
+
+ target.postMessage(
+ EZGameObject.USER_MESSAGE + ":" + _gameObj.getOid(),
+ new Object[] { msg, data });
+ }
+
+ /**
+ * Helper method to post a property set event.
+ */
+ protected void setProperty (String propName, Object value, int index)
+ {
+ _gameObj.postEvent(
+ new PropertySetEvent(_gameObj.getOid(), propName, value, index));
+ }
+
+ /**
+ * Validate that the specified user has access to do things in the game.
+ */
+ protected void validateUser (ClientObject caller)
+ throws InvocationException
+ {
+ if (getPresentPlayerIndex(caller.getOid()) == -1) {
+ throw new InvocationException(InvocationCodes.ACCESS_DENIED);
+ }
+ }
+
+ /**
+ * Validate that the specified listener has access to make a
+ * change.
+ */
+ protected void validateStateModification (ClientObject caller)
+ throws InvocationException
+ {
+ validateUser(caller);
+
+ Name holder = _gameObj.turnHolder;
+ if (holder != null &&
+ !holder.equals(((BodyObject) caller).getVisibleName())) {
+ throw new InvocationException(InvocationCodes.ACCESS_DENIED);
+ }
+ }
+
+ @Override
+ protected Class extends PlaceObject> getPlaceObjectClass ()
+ {
+ return EZGameObject.class;
+ }
+
+ @Override
+ protected void didStartup ()
+ {
+ super.didStartup();
+
+ _gameObj = (EZGameObject) _plobj;
+
+ _gameObj.setEZGameService(
+ (EZGameMarshaller) CrowdServer.invmgr.registerDispatcher(
+ new EZGameDispatcher(this), false));
+ }
+
+ @Override
+ protected void didShutdown ()
+ {
+ CrowdServer.invmgr.clearDispatcher(_gameObj.ezGameService);
+
+ super.didShutdown();
+ }
+
+ @Override
+ protected void assignWinners (boolean[] winners)
+ {
+ if (_winnerIndexes != null) {
+ for (int index : _winnerIndexes) {
+ if (index >= 0 && index < winners.length) {
+ winners[index] = true;
+ }
+ }
+ _winnerIndexes = null;
+ }
+ }
+
+ /** A nice casted reference to the game object. */
+ protected EZGameObject _gameObj;
+
+ /** Our turn delegate. */
+ protected EZGameTurnDelegate _turnDelegate;
+
+ /** The array of winners, after the user has filled it in. */
+ protected int[] _winnerIndexes;
+
+ /** The map of collections, lazy-initialized. */
+ protected HashMap> _collections;
+}
diff --git a/src/java/com/threerings/ezgame/server/EZGameProvider.java b/src/java/com/threerings/ezgame/server/EZGameProvider.java
new file mode 100644
index 00000000..efab18c3
--- /dev/null
+++ b/src/java/com/threerings/ezgame/server/EZGameProvider.java
@@ -0,0 +1,59 @@
+//
+// $Id$
+
+package com.threerings.ezgame.server;
+
+import com.threerings.ezgame.client.EZGameService;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.server.InvocationException;
+import com.threerings.presents.server.InvocationProvider;
+
+/**
+ * Defines the server-side of the {@link EZGameService}.
+ */
+public interface EZGameProvider extends InvocationProvider
+{
+ /**
+ * Handles a {@link EZGameService#addToCollection} request.
+ */
+ public void addToCollection (ClientObject caller, String arg1, byte[][] arg2, boolean arg3, InvocationService.InvocationListener arg4)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#endGame} request.
+ */
+ public void endGame (ClientObject caller, int[] arg1, InvocationService.InvocationListener arg2)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#endTurn} request.
+ */
+ public void endTurn (ClientObject caller, int arg1, InvocationService.InvocationListener arg2)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#getFromCollection} request.
+ */
+ public void getFromCollection (ClientObject caller, String arg1, boolean arg2, int arg3, String arg4, int arg5, InvocationService.ConfirmListener arg6)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#mergeCollection} request.
+ */
+ public void mergeCollection (ClientObject caller, String arg1, String arg2, InvocationService.InvocationListener arg3)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#sendMessage} request.
+ */
+ public void sendMessage (ClientObject caller, String arg1, Object arg2, int arg3, InvocationService.InvocationListener arg4)
+ throws InvocationException;
+
+ /**
+ * Handles a {@link EZGameService#setProperty} request.
+ */
+ public void setProperty (ClientObject caller, String arg1, Object arg2, int arg3, InvocationService.InvocationListener arg4)
+ throws InvocationException;
+}
diff --git a/src/java/com/threerings/ezgame/server/EZGameTurnDelegate.java b/src/java/com/threerings/ezgame/server/EZGameTurnDelegate.java
new file mode 100644
index 00000000..56d9c543
--- /dev/null
+++ b/src/java/com/threerings/ezgame/server/EZGameTurnDelegate.java
@@ -0,0 +1,46 @@
+//
+// $Id$
+
+package com.threerings.ezgame.server;
+
+import com.threerings.parlor.turn.server.TurnGameManagerDelegate;
+
+/**
+ * A special turn delegate for ez games.
+ */
+public class EZGameTurnDelegate extends TurnGameManagerDelegate
+{
+ public EZGameTurnDelegate (EZGameManager mgr)
+ {
+ super(mgr);
+ }
+
+ /**
+ * A form of endTurn where you can specify the next turn holder index.
+ */
+ public void endTurn (int playerIndex)
+ {
+ _nextPlayerIndex = playerIndex;
+ endTurn();
+ }
+
+ @Override
+ protected void setNextTurnHolder ()
+ {
+ // if the user-supplied value seems to make sense, use it!
+ if ((_nextPlayerIndex >= 0) &&
+ (_nextPlayerIndex < _turnGame.getPlayers().length)) {
+ _turnIdx = _nextPlayerIndex;
+
+ } else {
+ // otherwise, do the default behavior
+ super.setNextTurnHolder();
+ }
+
+ // always clear out the override
+ _nextPlayerIndex = -1;
+ }
+
+ /** An override next turn holder, or -1. */
+ protected int _nextPlayerIndex = -1;
+}