Moved the serverless game stuff out of msoy and back into the vilya library.

We'll be using this in game gardens, at least.
Note that the actionscript side currently doesn't compile because of
limitations in building a .swc file, but that'll be fixed soon.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@47 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
Ray Greenwell
2006-08-23 02:28:36 +00:00
parent a90d7e655b
commit 70b0d759c0
27 changed files with 2835 additions and 0 deletions
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}