Files
vilya/src/as/com/threerings/ezgame/EZGameControl.as
T
Ray Greenwell 03876838b3 Gah, my bad: we've got to return the value from the friend function.
Of course, the flash documentation says that function.apply() returns
void, but luckily this is not the case. DO NOT TRUST THEIR DOCS, they are
riddled with errors.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@210 c613c5cb-e716-0410-b11b-feb51c14d237
2007-02-24 00:42:24 +00:00

663 lines
21 KiB
ActionScript

//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/vilya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.ezgame {
import flash.errors.IllegalOperationError;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.display.DisplayObject;
/**
* Dispatched when a key is pressed when the game has focus.
*
* @eventType flash.events.KeyboardEvent.KEY_DOWN
*/
[Event(name="keyDown", type="flash.events.KeyboardEvent")]
/**
* Dispatched when a key is released when the game has focus.
*
* @eventType flash.events.KeyboardEvent.KEY_UP
*/
[Event(name="keyUp", type="flash.events.KeyboardEvent")]
/**
* Dispatched when the game starts, usually after all players are present.
*
* @eventType com.threerings.ezgame.StateChangedEvent.GAME_STARTED
*/
[Event(name="GameStarted", type="com.threerings.ezgame.StateChangedEvent")]
/**
* Dispatched when the turn changes in a turn-based game.
*
* @eventType com.threerings.ezgame.StateChangedEvent.TURN_CHANGED
*/
[Event(name="GameStarted", type="com.threerings.ezgame.StateChangedEvent")]
/**
* Dispatched when the game ends.
*
* @eventType com.threerings.ezgame.StateChangedEvent.GAME_ENDED
*/
[Event(name="GameEnded", type="com.threerings.ezgame.StateChangedEvent")]
/**
* Dispatched when a property has changed in the shared game state.
*
* @eventType com.threerings.ezgame.PropertyChangedEvent.TYPE
*/
[Event(name="PropChanged", type="com.threerings.ezgame.PropertyChangedEvent")]
/**
* Dispatched when a message arrives with information that is not part
* of the shared game state.
*
* @eventType com.threerings.ezgame.MessageReceivedEvent.TYPE
*/
[Event(name="msgReceived", type="com.threerings.ezgame.MessageReceivedEvent")]
/**
* The single point of control for each client in your multiplayer EZGame.
*
* TODO: lots of documentation.
*/
public class EZGameControl extends BaseControl
{
/**
* Create an EZGameControl object using some display object currently
* on the hierarchy.
*/
public function EZGameControl (disp :DisplayObject)
{
var event :DynEvent = new DynEvent();
event.userProps = new Object();
populateProperties(event.userProps);
disp.root.loaderInfo.sharedEvents.dispatchEvent(event);
if ("ezProps" in event) {
setEZProps(event.ezProps);
}
// set up our focusing click handler
disp.root.addEventListener(MouseEvent.CLICK, handleRootClick);
// TODO: this should only be available if the game uses it
_seating = new SeatingControl(this);
}
// documentation inherited
override public function addEventListener (
type :String, listener :Function, useCapture :Boolean = false,
priority :int = 0, useWeakReference :Boolean = false) :void
{
super.addEventListener(type, listener, useCapture, priority,
useWeakReference);
switch (type) {
case KeyboardEvent.KEY_UP:
case KeyboardEvent.KEY_DOWN:
if (hasEventListener(type)) { // ensure it was added
callEZCode("alterKeyEvents_v1", type, true);
}
break;
}
}
// documentation inherited
override public function removeEventListener (
type :String, listener :Function, useCapture :Boolean = false) :void
{
super.removeEventListener(type, listener, useCapture);
switch (type) {
case KeyboardEvent.KEY_UP:
case KeyboardEvent.KEY_DOWN:
if (!hasEventListener(type)) { // once it's no longer needed
callEZCode("alterKeyEvents_v1", type, false);
}
break;
}
}
/**
* Are we connected and running inside the EZGame environment, or
* has someone just loaded up this swf by itself?
*/
public function isConnected () :Boolean
{
return (_gameData != null);
}
/**
* Get the CollectionsControl, which contains methods for utilizing
* the server to dispatch private information.
*/
public function get collections () :CollectionsControl
{
if (_collections == null) {
_collections = new CollectionsControl(this);
}
return _collections;
}
/**
* Get the SeatingControl, which contains methods for checking
* and assigning player seating positions.
*/
public function get seating () :SeatingControl
{
return _seating;
}
// /**
// * Data accessor.
// */
// public function get data () :Object
// {
// return _gameData;
// }
/**
* Get a property from data.
*/
public function get (propName :String, index :int = -1) :Object
{
checkIsConnected();
var value :Object = _gameData[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;
}
/**
* Set a property that will be distributed.
*/
public function set (propName :String, value :Object, index :int = -1) :void
{
callEZCode("setProperty_v1", propName, value, index, false);
}
/**
* Set a property, but have this client immediately set the value so that
* it can be re-read. The property change event will still arrive and
* will be your clue as to when the other clients will see the newly
* set value. Be careful with this method, as it can allow data
* inconsistency: two clients may see different values for a property
* if one of them recently set it immediately, and the resultant
* PropertyChangedEvent's oldValue also may not be consistent.
*/
public function setImmediate (propName :String, value :Object, index :int = -1) :void
{
callEZCode("setProperty_v1", propName, value, index, true);
}
/**
* Set a property that will be distributed, but only if it's equal
* to the specified test value.
*
* Please note that, unlike in the standard set() function, the property
* will not be updated right away, but will require a request to the server
* and a response back. For this reason, there may be a considerable delay
* between calling testAndSet, and seeing the result of the update.
*
* The operation is 'atomic', in the sense that testing and setting take place
* during the same server event. In comparison, a separate 'get' followed by
* a 'set' operation would involve two events with two network round-trips,
* and no guarantee that the value won't change between the events.
*/
public function testAndSet (
propName :String, newValue :Object, testValue :Object, index :int = -1) :void
{
callEZCode("testAndSetProperty_v1", propName, newValue, testValue, index);
}
/**
* Register an object to receive whatever events it should receive,
* based on which event listeners it implements.
*/
public function registerListener (obj :Object) :void
{
if (obj is MessageReceivedListener) {
var mrl :MessageReceivedListener = (obj as MessageReceivedListener);
addEventListener(
MessageReceivedEvent.TYPE, mrl.messageReceived,
false, 0, true);
}
if (obj is PropertyChangedListener) {
var pcl :PropertyChangedListener = (obj as PropertyChangedListener);
addEventListener(
PropertyChangedEvent.TYPE, pcl.propertyChanged,
false, 0, true);
}
if (obj is StateChangedListener) {
var scl :StateChangedListener = (obj as StateChangedListener);
addEventListener(
StateChangedEvent.GAME_STARTED, scl.stateChanged,
false, 0, true);
addEventListener(
StateChangedEvent.TURN_CHANGED, scl.stateChanged,
false, 0, true);
addEventListener(
StateChangedEvent.GAME_ENDED, scl.stateChanged,
false, 0, true);
}
if (obj is OccupantChangedListener) {
var ocl :OccupantChangedListener = (obj as OccupantChangedListener);
addEventListener(OccupantChangedEvent.OCCUPANT_ENTERED, ocl.occupantEntered,
false, 0, true);
addEventListener(OccupantChangedEvent.OCCUPANT_LEFT, ocl.occupantLeft,
false, 0, true);
}
}
/**
* Unregister the specified object from receiving events.
*/
public function unregisterListener (obj :Object) :void
{
if (obj is MessageReceivedListener) {
var mrl :MessageReceivedListener = (obj as MessageReceivedListener);
removeEventListener(
MessageReceivedEvent.TYPE, mrl.messageReceived);
}
if (obj is PropertyChangedListener) {
var pcl :PropertyChangedListener = (obj as PropertyChangedListener);
removeEventListener(
PropertyChangedEvent.TYPE, pcl.propertyChanged);
}
if (obj is StateChangedListener) {
var scl :StateChangedListener = (obj as StateChangedListener);
removeEventListener(
StateChangedEvent.GAME_STARTED, scl.stateChanged);
removeEventListener(
StateChangedEvent.TURN_CHANGED, scl.stateChanged);
removeEventListener(
StateChangedEvent.GAME_ENDED, scl.stateChanged);
}
if (obj is OccupantChangedListener) {
var ocl :OccupantChangedListener = (obj as OccupantChangedListener);
removeEventListener(OccupantChangedEvent.OCCUPANT_ENTERED, ocl.occupantEntered);
removeEventListener(OccupantChangedEvent.OCCUPANT_LEFT, ocl.occupantLeft);
}
}
/**
* Requests a set of random letters from the dictionary service.
* The letters will arrive in a separate message with the specified key,
* as an array of strings.
*
* @param locale RFC 3066 string that represents language settings
* @param count the number of letters to be produced
* @param callback the function that will process the results, of the form:
* function (letters :Array) :void
* where letters is an array of strings containing letters
* for the given language settings (potentially empty).
*/
public function getDictionaryLetterSet (
locale :String, count :int, callback :Function) :void
{
callEZCode("getDictionaryLetterSet_v1", locale, count, callback);
}
/**
* Requests a set of random letters from the dictionary service.
* The letters will arrive in a separate message with the specified key,
* as an array of strings.
*
* @param RFC 3066 string that represents language settings
* @param word the string contains the word to be checked
* @param callback the function that will process the results, of the form:
* function (word :String, result :Boolean) :void
* where word is a copy of the word that was requested, and result
* specifies whether the word is valid given language settings
*/
public function checkDictionaryWord (
locale :String, word :String, callback :Function) :void
{
callEZCode("checkDictionaryWord_v1", locale, word, callback);
}
/**
* 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 playerId if 0 (or unset), sends to all players, otherwise
* the message will be private to just one player
*/
public function sendMessage (
messageName :String, value :Object, playerId :int = 0) :void
{
callEZCode("sendMessage_v2", messageName, value, playerId);
}
/**
* Start the ticker with the specified name. It will deliver
* messages to the game object at the specified delay,
* the value of each message being a single integer, starting with 0
* and increasing by one with each messsage.
*/
public function startTicker (tickerName :String, msOfDelay :int) :void
{
callEZCode("setTicker_v1", tickerName, msOfDelay);
}
/**
* Stop the specified ticker.
*/
public function stopTicker (tickerName :String) :void
{
startTicker(tickerName, 0);
}
/**
* Send a message that will be heard by everyone in the game room,
* even observers.
*/
public function sendChat (msg :String) :void
{
callEZCode("sendChat_v1", msg);
}
/**
* Display the specified message immediately locally: not sent
* to any other players or observers in the game room.
*/
public function localChat (msg :String) :void
{
callEZCode("localChat_v1", msg);
}
// TODO: NEW
public function getOccupants () :Array /* of playerId */
{
return (callEZCode("getOccupants_v1") as Array);
}
// TODO: NEW
/**
* Get the display name of the specified occupant.
* Two players may have the same name: always use playerId to
* purposes of identification and comparison. The name is for display
* only.
*/
public function getOccupantName (playerId :int) :String
{
return String(callEZCode("getOccupantName_v1", playerId));
}
// TODO: NEW
public function getMyId () :int
{
return int(callEZCode("getMyId_v1"));
}
public function getTurnHolder () :int
{
return int(callEZCode("getTurnHolder_v1"));
}
/**
* Get the user-specific game data for the specified user. The
* first time this is requested per game instance it will be retrieved
* from the database. After that, it will be returned from memory.
*/
public function getUserCookie (playerId :int, callback :Function) :void
{
callEZCode("getUserCookie_v1", playerId, callback);
}
/**
* Store persistent data that can later be retrieved by an instance
* of this game. The maximum size of this data is 4096 bytes AFTER
* AMF3 encoding.
*
* Note: there is no playerIndex parameter because a cookie may only
* be stored for the current player.
*
* @return false if the cookie could not be encoded to 4096 bytes
* or less; true if the cookie is going to try to be saved. There is
* no guarantee it will be saved and no way to find out if it failed,
* but if it fails it will be because the shit hit the fan so hard that
* there's nothing you can do anyway.
*/
public function setUserCookie (cookie :Object) :Boolean
{
return Boolean(callEZCode("setUserCookie_v1", cookie));
}
/**
* A convenience method to just check if it's our turn.
*/
public function isMyTurn () :Boolean
{
return Boolean(callEZCode("isMyTurn_v1"));
}
/**
* Is the game currently in play?
*/
public function isInPlay () :Boolean
{
return Boolean(callEZCode("isInPlay_v1"));
}
/**
* End the current turn. If no next player id is specified,
* then the next player after the current one is used.
*/
public function endTurn (nextPlayerId :int = 0) :void
{
callEZCode("endTurn_v2", nextPlayerId);
}
/**
* End the game. The specified player ids are winners!
*/
public function endGame (... winnerIds) :void
{
var args :Array = winnerIds;
args.unshift("endGame_v2");
// goddamn var-args complications in actionscript
callEZCode.apply(null, args);
}
/**
* Populate any properties or functions we want to expose to
* the other side of the ezgame security boundary.
*/
protected function populateProperties (o :Object) :void
{
o["propertyWasSet_v1"] = propertyWasSet_v1;
o["turnDidChange_v1"] = turnDidChange_v1;
o["messageReceived_v1"] = messageReceived_v1;
o["gameDidStart_v1"] = gameDidStart_v1;
o["gameDidEnd_v1"] = gameDidEnd_v1;
o["dispatchEvent_v1"] = dispatch;
o["occupantChanged_v1"] = occupantChanged_v1;
}
/**
* Private method to post a PropertyChangedEvent.
*/
private function propertyWasSet_v1 (
name :String, newValue :Object, oldValue :Object, index :int) :void
{
dispatch(
new PropertyChangedEvent(this, name, newValue, oldValue, index));
}
/**
* Private method to post a StateChangedEvent.
*/
private function turnDidChange_v1 () :void
{
dispatch(new StateChangedEvent(StateChangedEvent.TURN_CHANGED, this));
}
/**
* Private method to post a MessageReceivedEvent.
*/
private function messageReceived_v1 (name :String, value :Object) :void
{
dispatch(new MessageReceivedEvent(this, name, value));
}
/**
* Private method to post a StateChangedEvent.
*/
private function gameDidStart_v1 () :void
{
dispatch(new StateChangedEvent(StateChangedEvent.GAME_STARTED, this));
}
/**
* Private method to post a StateChangedEvent.
*/
private function gameDidEnd_v1 () :void
{
dispatch(new StateChangedEvent(StateChangedEvent.GAME_ENDED, this));
}
/**
* Private method to post a OccupantEvent.
*/
private function occupantChanged_v1 (occupantId :int, player :Boolean, enter :Boolean) :void
{
dispatch(new OccupantChangedEvent(
enter ? OccupantChangedEvent.OCCUPANT_ENTERED
: OccupantChangedEvent.OCCUPANT_LEFT,
this, occupantId, player));
}
/**
* Sets the properties we received from the EZ game framework
* on the other side of the security boundary.
*/
protected function setEZProps (o :Object) :void
{
// get our gamedata
_gameData = o.gameData;
// and functions
_funcs = o;
}
/**
* Call a method across the security boundary.
*/
protected function callEZCode (name :String, ... args) :*
{
if (_funcs != null) {
try {
var func :Function = (_funcs[name] as Function);
if (func != null) {
return func.apply(null, args);
}
} catch (err :Error) {
trace("Unable to call ez code: " + err);
}
} else {
// if _funcs is null, this will almost certainly throw an error..
checkIsConnected();
}
}
/**
* A "friend" version of callEZCode for subControls.
*/
internal function callEZCodeFriend (name :String, ... args) :*
{
// var args
args.unshift(name);
return callEZCode.apply(this, args);
}
/**
* Throw an error if we're not connected.
*/
protected function checkIsConnected () :void
{
if (!isConnected()) {
throw new IllegalOperationError("The game is not connected " +
"to The Whirled, please check isConnected(). If false, your " +
"game is being viewed standalone and should adjust.");
}
}
/**
* Internal method that is called whenever the mouse clicks our root.
*/
protected function handleRootClick (evt :MouseEvent) :void
{
try {
if (evt.target.stage.focus != null) {
return;
}
} catch (err :SecurityError) {
}
callEZCode("focusContainer_v1");
}
/** Contains the data properties shared by all players in the game. */
protected var _gameData :Object;
/** Contains functions exposed to us from the EZGame host. */
protected var _funcs :Object;
/** Sub-controls. */
protected var _collections :CollectionsControl;
protected var _seating :SeatingControl;
}
}
import flash.events.Event;
dynamic class DynEvent extends Event
{
public function DynEvent ()
{
super("ezgameQuery", true, false);
}
override public function clone () :Event
{
return new DynEvent();
}
}