Files
vilya/src/as/com/threerings/ezgame/EZGameControl.as
T
Michael Bayne 33f55d5a8d Header patrol. Please be sure to use the header in lib/SOURCE_HEADER when
creating new source files.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@209 c613c5cb-e716-0410-b11b-feb51c14d237
2007-02-24 00:37:33 +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);
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();
}
}