Revamped EZGame to work over the security boundary.
Now, instead of implementing "Game" and having the EZGame object assigned to you, you create it yourself using your top-level component. You must register listeners manually, and keyboard focus is currently an issue that I'm working on. git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@137 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
package com.threerings.ezgame.client {
|
||||
|
||||
import flash.errors.IllegalOperationError;
|
||||
|
||||
import flash.events.Event;
|
||||
import flash.events.EventDispatcher;
|
||||
import flash.events.IEventDispatcher;
|
||||
|
||||
import flash.utils.IExternalizable;
|
||||
import flash.utils.ByteArray;
|
||||
import flash.utils.Dictionary;
|
||||
|
||||
import com.threerings.io.TypedArray;
|
||||
|
||||
import com.threerings.util.ClassUtil;
|
||||
import com.threerings.util.Integer;
|
||||
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.presents.dobj.EntryAddedEvent;
|
||||
import com.threerings.presents.dobj.EntryRemovedEvent;
|
||||
import com.threerings.presents.dobj.EntryUpdatedEvent;
|
||||
import com.threerings.presents.dobj.SetListener;
|
||||
import com.threerings.presents.dobj.MessageAdapter;
|
||||
import com.threerings.presents.dobj.MessageEvent;
|
||||
import com.threerings.presents.dobj.MessageListener;
|
||||
|
||||
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.data.PropertySetListener;
|
||||
import com.threerings.ezgame.data.UserCookie;
|
||||
import com.threerings.ezgame.util.EZObjectMarshaller;
|
||||
|
||||
public class GameControlBackend
|
||||
implements MessageListener, SetListener, PropertySetListener
|
||||
{
|
||||
public var log :Log = Log.getLog(this);
|
||||
|
||||
public function GameControlBackend (
|
||||
ctx :CrowdContext, ezObj :EZGameObject)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_ezObj = ezObj;
|
||||
_gameData = new GameData(setProperty_v1, _ezObj.getUserProps());
|
||||
|
||||
_ezObj.addListener(this);
|
||||
_ctx.getClient().getClientObject().addListener(_userListener);
|
||||
}
|
||||
|
||||
public function setSharedEvents (disp :IEventDispatcher) :void
|
||||
{
|
||||
disp.addEventListener("ezgameQuery", handleEZQuery);
|
||||
}
|
||||
|
||||
public function shutdown () :void
|
||||
{
|
||||
_ezObj.removeListener(this);
|
||||
_ctx.getClient().getClientObject().removeListener(_userListener);
|
||||
}
|
||||
|
||||
protected function handleEZQuery (evt :Object) :void
|
||||
{
|
||||
setUserCodeProperties(evt.userProps);
|
||||
evt.ezProps = new Object();
|
||||
populateProperties(evt.ezProps);
|
||||
}
|
||||
|
||||
protected function setUserCodeProperties (o :Object) :void
|
||||
{
|
||||
// here we would handle adapting old functions to a new version
|
||||
|
||||
_userFuncs = o;
|
||||
}
|
||||
|
||||
protected function callUserCode (name :String, ... args) :*
|
||||
{
|
||||
if (_userFuncs != null) {
|
||||
try {
|
||||
var func :Function = (_userFuncs[name] as Function);
|
||||
if (func != null) {
|
||||
return func.apply(null, args);
|
||||
}
|
||||
|
||||
} catch (err :Error) {
|
||||
log.warning("Error in user-code: " + err);
|
||||
log.logStackTrace(err);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected function populateProperties (o :Object) :void
|
||||
{
|
||||
// straight data
|
||||
o["gameData"] = _gameData;
|
||||
|
||||
// functions
|
||||
o["setProperty_v1"] = setProperty_v1;
|
||||
o["mergeCollection_v1"] = mergeCollection_v1;
|
||||
o["sendMessage_v1"] = sendMessage_v1;
|
||||
o["setTicker_v1"] = setTicker_v1;
|
||||
o["sendChat_v1"] = sendChat_v1;
|
||||
o["localChat_v1"] = localChat_v1;
|
||||
o["getPlayerCount_v1"] = getPlayerCount_v1;
|
||||
o["getPlayerNames_v1"] = getPlayerNames_v1;
|
||||
o["getMyIndex_v1"] = getMyIndex_v1;
|
||||
o["getTurnHolderIndex_v1"] = getTurnHolderIndex_v1;
|
||||
o["getWinnerIndexes_v1"] = getWinnerIndexes_v1;
|
||||
o["getUserCookie_v1"] = getUserCookie_v1;
|
||||
o["setUserCookie_v1"] = setUserCookie_v1;
|
||||
o["isMyTurn_v1"] = isMyTurn_v1;
|
||||
o["isInPlay_v1"] = isInPlay_v1;
|
||||
o["endTurn_v1"] = endTurn_v1;
|
||||
o["endGame_v1"] = endGame_v1;
|
||||
o["populateCollection_v1"] = populateCollection_v1;
|
||||
o["getFromCollection_v1"] = getFromCollection_v1;
|
||||
}
|
||||
|
||||
public function setProperty_v1 (
|
||||
propName :String, value :Object, index :int) :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);
|
||||
}
|
||||
|
||||
public function mergeCollection_v1 (
|
||||
srcColl :String, intoColl :String) :void
|
||||
{
|
||||
validateName(srcColl);
|
||||
validateName(intoColl);
|
||||
_ezObj.ezGameService.mergeCollection(_ctx.getClient(),
|
||||
srcColl, intoColl, createLoggingListener("mergeCollection"));
|
||||
}
|
||||
|
||||
public function sendMessage_v1 (
|
||||
messageName :String, value :Object, playerIndex :int) :void
|
||||
{
|
||||
validateName(messageName);
|
||||
validateValue(value);
|
||||
|
||||
var encoded :Object = EZObjectMarshaller.encode(value);
|
||||
_ezObj.ezGameService.sendMessage(_ctx.getClient(),
|
||||
messageName, encoded, playerIndex,
|
||||
createLoggingListener("sendMessage"));
|
||||
}
|
||||
|
||||
public function setTicker_v1 (tickerName :String, msOfDelay :int) :void
|
||||
{
|
||||
validateName(tickerName);
|
||||
_ezObj.ezGameService.setTicker(_ctx.getClient(),
|
||||
tickerName, msOfDelay, createLoggingListener("setTicker"));
|
||||
}
|
||||
|
||||
public function sendChat_v1 (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 ]);
|
||||
}
|
||||
|
||||
public function localChat_v1 (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));
|
||||
}
|
||||
|
||||
public function getPlayerCount_v1 () :int
|
||||
{
|
||||
return _ezObj.getPlayerCount();
|
||||
}
|
||||
|
||||
public function getPlayerNames_v1 () :Array
|
||||
{
|
||||
var names :Array = new Array();
|
||||
for each (var name :Name in _ezObj.players) {
|
||||
names.push((name == null) ? null : name.toString());
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
public function getMyIndex_v1 () :int
|
||||
{
|
||||
return _ezObj.getPlayerIndex(getUsername());
|
||||
}
|
||||
|
||||
public function getTurnHolderIndex_v1 () :int
|
||||
{
|
||||
return _ezObj.getPlayerIndex(_ezObj.turnHolder);
|
||||
}
|
||||
|
||||
public function getWinnerIndexes_v1 () :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;
|
||||
}
|
||||
|
||||
public function getUserCookie_v1 (
|
||||
playerIndex :int, callback :Function) :void
|
||||
{
|
||||
// see if that cookie is already published
|
||||
if (_ezObj.userCookies != null) {
|
||||
var uc :UserCookie =
|
||||
(_ezObj.userCookies.get(playerIndex) as UserCookie);
|
||||
if (uc != null) {
|
||||
callback(uc.cookie);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_cookieCallbacks == null) {
|
||||
_cookieCallbacks = new Dictionary();
|
||||
}
|
||||
var arr :Array = (_cookieCallbacks[playerIndex] as Array);
|
||||
if (arr == null) {
|
||||
arr = [];
|
||||
_cookieCallbacks[playerIndex] = arr;
|
||||
}
|
||||
arr.push(callback);
|
||||
|
||||
// request it to be made so by the server
|
||||
_ezObj.ezGameService.getCookie(_ctx.getClient(), playerIndex,
|
||||
createLoggingListener("getUserCookie"));
|
||||
}
|
||||
|
||||
public function setUserCookie_v1 (cookie :Object) :Boolean
|
||||
{
|
||||
var ba :ByteArray =
|
||||
(EZObjectMarshaller.encode(cookie, false) as ByteArray);
|
||||
if (ba.length > MAX_USER_COOKIE) {
|
||||
// not saved!
|
||||
return false;
|
||||
}
|
||||
|
||||
_ezObj.ezGameService.setCookie(_ctx.getClient(), ba,
|
||||
createLoggingListener("setUserCookie"));
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isMyTurn_v1 () :Boolean
|
||||
{
|
||||
return getUsername().equals(_ezObj.turnHolder);
|
||||
}
|
||||
|
||||
public function isInPlay_v1 () :Boolean
|
||||
{
|
||||
return _ezObj.isInPlay();
|
||||
}
|
||||
|
||||
public function endTurn_v1 (nextPlayerIndex :int = -1) :void
|
||||
{
|
||||
_ezObj.ezGameService.endTurn(_ctx.getClient(), nextPlayerIndex,
|
||||
createLoggingListener("endTurn"));
|
||||
}
|
||||
|
||||
public function endGame_v1 (... winnerDexes) :void
|
||||
{
|
||||
var winners :TypedArray = TypedArray.create(int);
|
||||
while (winnerDexes.length > 0) {
|
||||
winners.push(int(winnerDexes.shift()));
|
||||
}
|
||||
_ezObj.ezGameService.endGame(_ctx.getClient(), winners,
|
||||
createLoggingListener("endGame"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for setCollection and addToCollection.
|
||||
*/
|
||||
public function populateCollection_v1 (
|
||||
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.
|
||||
*/
|
||||
public function getFromCollection_v1 (
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get our name.
|
||||
*/
|
||||
protected function getUsername () :Name
|
||||
{
|
||||
var body :BodyObject =
|
||||
(_ctx.getClient().getClientObject() as BodyObject);
|
||||
return body.getVisibleName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a listener for service requests.
|
||||
*/
|
||||
protected function createLoggingListener (
|
||||
service :String) :InvocationService_ConfirmListener
|
||||
{
|
||||
return new ConfirmAdapter(function (cause :String) :void {
|
||||
Log.getLog(this).warning("Service failure " +
|
||||
"[service=" + service + ", cause=" + cause + "].");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the property name / value are valid.
|
||||
*/
|
||||
protected 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.
|
||||
*/
|
||||
protected function validateName (name :String) :void
|
||||
{
|
||||
if (name == null) {
|
||||
throw new ArgumentError(
|
||||
"Property, message, and collection names must not be null.");
|
||||
}
|
||||
}
|
||||
|
||||
protected 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.
|
||||
*/
|
||||
protected 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.getClassName(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;
|
||||
}
|
||||
var clazz :Class = ClassUtil.getClass(value);
|
||||
if (clazz == ByteArray) {
|
||||
return; // kosher
|
||||
}
|
||||
if (clazz != 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the EZGameController when the turn changes.
|
||||
*/
|
||||
public function turnDidChange () :void
|
||||
{
|
||||
callUserCode("turnDidChange_v1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the EZGameController when the game starts.
|
||||
*/
|
||||
public function gameDidStart () :void
|
||||
{
|
||||
callUserCode("gameDidStart_v1");
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the EZGameController when the game ends.
|
||||
*/
|
||||
public function gameDidEnd () :void
|
||||
{
|
||||
callUserCode("gameDidEnd_v1");
|
||||
}
|
||||
|
||||
// from SetListener
|
||||
public function entryAdded (event :EntryAddedEvent) :void
|
||||
{
|
||||
if (EZGameObject.USER_COOKIES == event.getName()) {
|
||||
receivedUserCookie(event.getEntry() as UserCookie);
|
||||
}
|
||||
}
|
||||
|
||||
// from SetListener
|
||||
public function entryUpdated (event :EntryUpdatedEvent) :void
|
||||
{
|
||||
if (EZGameObject.USER_COOKIES == event.getName()) {
|
||||
receivedUserCookie(event.getEntry() as UserCookie);
|
||||
}
|
||||
}
|
||||
|
||||
// from SetListener
|
||||
public function entryRemoved (event :EntryRemovedEvent) :void
|
||||
{
|
||||
// nada
|
||||
}
|
||||
|
||||
// from PropertySetListener
|
||||
public function propertyWasSet (event :PropertySetEvent) :void
|
||||
{
|
||||
callUserCode("propertyWasSet_v1", event.getName(), event.getValue(),
|
||||
event.getOldValue(), event.getIndex());
|
||||
}
|
||||
|
||||
public function messageReceived (event :MessageEvent) :void
|
||||
{
|
||||
var name :String = event.getName();
|
||||
if (EZGameObject.USER_MESSAGE == name) {
|
||||
var args :Array = event.getArgs();
|
||||
callUserCode("messageReceived_v1", (args[0] as String),
|
||||
EZObjectMarshaller.decode(args[1]));
|
||||
|
||||
} 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
|
||||
localChat_v1(String(event.getArgs()[0]));
|
||||
|
||||
} else if (EZGameObject.TICKER == name) {
|
||||
var targs :Array = event.getArgs();
|
||||
callUserCode("messageReceived_v1", (targs[0] as String),
|
||||
(targs[1] as Integer).value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()) {
|
||||
var args :Array = event.getArgs();
|
||||
callUserCode("messageReceived_v1", (args[0] as String),
|
||||
EZObjectMarshaller.decode(args[1]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle the arrival of a new UserCookie.
|
||||
*/
|
||||
protected function receivedUserCookie (cookie :UserCookie) :void
|
||||
{
|
||||
if (_cookieCallbacks != null) {
|
||||
var arr :Array = (_cookieCallbacks[cookie.playerIndex] as Array);
|
||||
if (arr != null) {
|
||||
delete _cookieCallbacks[cookie.playerIndex];
|
||||
for each (var fn :Function in arr) {
|
||||
try {
|
||||
fn(cookie.cookie);
|
||||
} catch (err :Error) {
|
||||
// cope
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected var _ctx :CrowdContext;
|
||||
|
||||
protected var _userListener :MessageAdapter =
|
||||
new MessageAdapter(messageReceivedOnUserObject);
|
||||
|
||||
protected var _ezObj :EZGameObject;
|
||||
|
||||
protected var _userFuncs :Object;
|
||||
|
||||
protected var _gameData :GameData;
|
||||
|
||||
/** playerIndex -> callback functions waiting for the cookie. */
|
||||
protected var _cookieCallbacks :Dictionary;
|
||||
|
||||
protected static const MAX_USER_COOKIE :int = 4096;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user