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