Added a server managed notion of which client is controlling the game.

Doing this on the client, while theoretically possible, is more complex. We
have a server, we use it to provide commonly needed services, the assignment of
a single client to control the game is a commonly needed service. This also
matches the way other services like turn change and game start and end are
implemented.

A side note: the client-side code was not properly handling disconnected
players, which the server code properly handles.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@224 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
Michael Bayne
2007-03-04 20:05:58 +00:00
parent 3c595d85a5
commit d77b8363f7
8 changed files with 318 additions and 268 deletions
+1 -1
View File
@@ -149,7 +149,7 @@
</javac> </javac>
</target> </target>
<!-- build the javadoc documentation --> <!-- builds the javadoc documentation -->
<target name="javadoc" depends="prepare"> <target name="javadoc" depends="prepare">
<javadoc sourcepath="src/java" packagenames="com.threerings.*" <javadoc sourcepath="src/java" packagenames="com.threerings.*"
destdir="${javadoc.home}" stylesheetfile="docs/stylesheet.css" destdir="${javadoc.home}" stylesheetfile="docs/stylesheet.css"
+123 -115
View File
@@ -44,6 +44,13 @@ import flash.display.DisplayObject;
*/ */
[Event(name="keyUp", type="flash.events.KeyboardEvent")] [Event(name="keyUp", type="flash.events.KeyboardEvent")]
/**
* Dispatched when the controller changes for the game.
*
* @eventType com.threerings.ezgame.StateChangedEvent.CONTROL_CHANGED
*/
[Event(name="ControlChanged", type="com.threerings.ezgame.StateChangedEvent")]
/** /**
* Dispatched when the game starts, usually after all players are present. * Dispatched when the game starts, usually after all players are present.
* *
@@ -56,7 +63,7 @@ import flash.display.DisplayObject;
* *
* @eventType com.threerings.ezgame.StateChangedEvent.TURN_CHANGED * @eventType com.threerings.ezgame.StateChangedEvent.TURN_CHANGED
*/ */
[Event(name="GameStarted", type="com.threerings.ezgame.StateChangedEvent")] [Event(name="TurnChanged", type="com.threerings.ezgame.StateChangedEvent")]
/** /**
* Dispatched when the game ends. * Dispatched when the game ends.
@@ -73,8 +80,7 @@ import flash.display.DisplayObject;
[Event(name="PropChanged", type="com.threerings.ezgame.PropertyChangedEvent")] [Event(name="PropChanged", type="com.threerings.ezgame.PropertyChangedEvent")]
/** /**
* Dispatched when a message arrives with information that is not part * Dispatched when a message arrives with information that is not part of the shared game state.
* of the shared game state.
* *
* @eventType com.threerings.ezgame.MessageReceivedEvent.TYPE * @eventType com.threerings.ezgame.MessageReceivedEvent.TYPE
*/ */
@@ -89,8 +95,7 @@ import flash.display.DisplayObject;
public class EZGameControl extends BaseControl public class EZGameControl extends BaseControl
{ {
/** /**
* Create an EZGameControl object using some display object currently * Create an EZGameControl object using some display object currently on the hierarchy.
* on the hierarchy.
*/ */
public function EZGameControl (disp :DisplayObject) public function EZGameControl (disp :DisplayObject)
{ {
@@ -114,8 +119,7 @@ public class EZGameControl extends BaseControl
type :String, listener :Function, useCapture :Boolean = false, type :String, listener :Function, useCapture :Boolean = false,
priority :int = 0, useWeakReference :Boolean = false) :void priority :int = 0, useWeakReference :Boolean = false) :void
{ {
super.addEventListener(type, listener, useCapture, priority, super.addEventListener(type, listener, useCapture, priority, useWeakReference);
useWeakReference);
switch (type) { switch (type) {
case KeyboardEvent.KEY_UP: case KeyboardEvent.KEY_UP:
@@ -144,8 +148,8 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Are we connected and running inside the EZGame environment, or * Are we connected and running inside the EZGame environment, or has someone just loaded up
* has someone just loaded up this swf by itself? * this swf by itself?
*/ */
public function isConnected () :Boolean public function isConnected () :Boolean
{ {
@@ -153,8 +157,8 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Get the CollectionsControl, which contains methods for utilizing * Get the CollectionsControl, which contains methods for utilizing the server to dispatch
* the server to dispatch private information. * private information.
*/ */
public function get collections () :CollectionsControl public function get collections () :CollectionsControl
{ {
@@ -165,8 +169,8 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Get the SeatingControl, which contains methods for checking * Get the SeatingControl, which contains methods for checking and assigning player seating
* and assigning player seating positions. * positions.
*/ */
public function get seating () :SeatingControl public function get seating () :SeatingControl
{ {
@@ -186,8 +190,7 @@ public class EZGameControl extends BaseControl
return (value as Array)[index]; return (value as Array)[index];
} else { } else {
throw new ArgumentError("Property " + propName + throw new ArgumentError("Property " + propName + " is not an array.");
" is not an array.");
} }
} }
return value; return value;
@@ -202,13 +205,12 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Set a property, but have this client immediately set the value so that * Set a property, but have this client immediately set the value so that it can be
* it can be re-read. The property change event will still arrive and * re-read. The property change event will still arrive and will be your clue as to when the
* will be your clue as to when the other clients will see the newly * other clients will see the newly set value. Be careful with this method, as it can allow
* set value. Be careful with this method, as it can allow data * data inconsistency: two clients may see different values for a property if one of them
* inconsistency: two clients may see different values for a property * recently set it immediately, and the resultant PropertyChangedEvent's oldValue also may not
* if one of them recently set it immediately, and the resultant * be consistent.
* PropertyChangedEvent's oldValue also may not be consistent.
*/ */
public function setImmediate (propName :String, value :Object, index :int = -1) :void public function setImmediate (propName :String, value :Object, index :int = -1) :void
{ {
@@ -216,18 +218,17 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Set a property that will be distributed, but only if it's equal * Set a property that will be distributed, but only if it's equal to the specified test value.
* to the specified test value.
* *
* Please note that, unlike in the standard set() function, the property * <p> Please note that, unlike in the standard set() function, the property will not be
* will not be updated right away, but will require a request to the server * updated right away, but will require a request to the server and a response back. For this
* and a response back. For this reason, there may be a considerable delay * reason, there may be a considerable delay between calling testAndSet, and seeing the result
* between calling testAndSet, and seeing the result of the update. * of the update.
* *
* The operation is 'atomic', in the sense that testing and setting take place * <p> The operation is 'atomic', in the sense that testing and setting take place during the
* during the same server event. In comparison, a separate 'get' followed by * same server event. In comparison, a separate 'get' followed by a 'set' operation would
* a 'set' operation would involve two events with two network round-trips, * involve two events with two network round-trips, and no guarantee that the value won't
* and no guarantee that the value won't change between the events. * change between the events.
*/ */
public function testAndSet ( public function testAndSet (
propName :String, newValue :Object, testValue :Object, index :int = -1) :void propName :String, newValue :Object, testValue :Object, index :int = -1) :void
@@ -236,41 +237,31 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Register an object to receive whatever events it should receive, * Register an object to receive whatever events it should receive, based on which event
* based on which event listeners it implements. * listeners it implements.
*/ */
public function registerListener (obj :Object) :void public function registerListener (obj :Object) :void
{ {
if (obj is MessageReceivedListener) { if (obj is MessageReceivedListener) {
var mrl :MessageReceivedListener = (obj as MessageReceivedListener); var mrl :MessageReceivedListener = (obj as MessageReceivedListener);
addEventListener( addEventListener(MessageReceivedEvent.TYPE, mrl.messageReceived, false, 0, true);
MessageReceivedEvent.TYPE, mrl.messageReceived,
false, 0, true);
} }
if (obj is PropertyChangedListener) { if (obj is PropertyChangedListener) {
var pcl :PropertyChangedListener = (obj as PropertyChangedListener); var pcl :PropertyChangedListener = (obj as PropertyChangedListener);
addEventListener( addEventListener(PropertyChangedEvent.TYPE, pcl.propertyChanged, false, 0, true);
PropertyChangedEvent.TYPE, pcl.propertyChanged,
false, 0, true);
} }
if (obj is StateChangedListener) { if (obj is StateChangedListener) {
var scl :StateChangedListener = (obj as StateChangedListener); var scl :StateChangedListener = (obj as StateChangedListener);
addEventListener( addEventListener(StateChangedEvent.CONTROL_CHANGED, scl.stateChanged, false, 0, true);
StateChangedEvent.GAME_STARTED, scl.stateChanged, addEventListener(StateChangedEvent.GAME_STARTED, scl.stateChanged, false, 0, true);
false, 0, true); addEventListener(StateChangedEvent.TURN_CHANGED, scl.stateChanged, false, 0, true);
addEventListener( addEventListener(StateChangedEvent.GAME_ENDED, scl.stateChanged, false, 0, true);
StateChangedEvent.TURN_CHANGED, scl.stateChanged,
false, 0, true);
addEventListener(
StateChangedEvent.GAME_ENDED, scl.stateChanged,
false, 0, true);
} }
if (obj is OccupantChangedListener) { if (obj is OccupantChangedListener) {
var ocl :OccupantChangedListener = (obj as OccupantChangedListener); var ocl :OccupantChangedListener = (obj as OccupantChangedListener);
addEventListener(OccupantChangedEvent.OCCUPANT_ENTERED, ocl.occupantEntered, addEventListener(OccupantChangedEvent.OCCUPANT_ENTERED, ocl.occupantEntered,
false, 0, true); false, 0, true);
addEventListener(OccupantChangedEvent.OCCUPANT_LEFT, ocl.occupantLeft, addEventListener(OccupantChangedEvent.OCCUPANT_LEFT, ocl.occupantLeft, false, 0, true);
false, 0, true);
} }
} }
@@ -306,61 +297,54 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Requests a set of random letters from the dictionary service. * Requests a set of random letters from the dictionary service. The letters will arrive in a
* The letters will arrive in a separate message with the specified key, * separate message with the specified key, as an array of strings.
* as an array of strings.
* *
* @param locale RFC 3066 string that represents language settings * @param locale RFC 3066 string that represents language settings
* @param count the number of letters to be produced * @param count the number of letters to be produced
* @param callback the function that will process the results, of the form: * @param callback the function that will process the results, of the form:
* function (letters :Array) :void * <pre>function (letters :Array) :void</pre>
* where letters is an array of strings containing letters * where letters is an array of strings containing letters for the given language settings
* for the given language settings (potentially empty). * (potentially empty).
*/ */
public function getDictionaryLetterSet ( public function getDictionaryLetterSet (locale :String, count :int, callback :Function) :void
locale :String, count :int, callback :Function) :void
{ {
callEZCode("getDictionaryLetterSet_v1", locale, count, callback); callEZCode("getDictionaryLetterSet_v1", locale, count, callback);
} }
/** /**
* Requests a set of random letters from the dictionary service. * Requests a set of random letters from the dictionary service. The letters will arrive in a
* The letters will arrive in a separate message with the specified key, * separate message with the specified key, as an array of strings.
* as an array of strings.
* *
* @param RFC 3066 string that represents language settings * @param RFC 3066 string that represents language settings
* @param word the string contains the word to be checked * @param word the string contains the word to be checked
* @param callback the function that will process the results, of the form: * @param callback the function that will process the results, of the form:
* function (word :String, result :Boolean) :void * <pre>function (word :String, result :Boolean) :void</pre>
* where word is a copy of the word that was requested, and result * where word is a copy of the word that was requested, and result specifies whether the word
* specifies whether the word is valid given language settings * is valid given language settings
*/ */
public function checkDictionaryWord ( public function checkDictionaryWord (locale :String, word :String, callback :Function) :void
locale :String, word :String, callback :Function) :void
{ {
callEZCode("checkDictionaryWord_v1", locale, word, callback); callEZCode("checkDictionaryWord_v1", locale, word, callback);
} }
/** /**
* Send a "message" to other clients subscribed to the game. * Send a "message" to other clients subscribed to the game. These is similar to setting a
* These is similar to setting a property, except that the * property, except that the value will not be saved- it will merely end up coming out as a
* value will not be saved- it will merely end up coming out * MessageReceivedEvent.
* as a MessageReceivedEvent.
* *
* @param playerId if 0 (or unset), sends to all players, otherwise * @param playerId if 0 (or unset), sends to all players, otherwise the message will be private
* the message will be private to just one player * to just one player
*/ */
public function sendMessage ( public function sendMessage (messageName :String, value :Object, playerId :int = 0) :void
messageName :String, value :Object, playerId :int = 0) :void
{ {
callEZCode("sendMessage_v2", messageName, value, playerId); callEZCode("sendMessage_v2", messageName, value, playerId);
} }
/** /**
* Start the ticker with the specified name. It will deliver * Start the ticker with the specified name. It will deliver messages to the game object at the
* messages to the game object at the specified delay, * specified delay, the value of each message being a single integer, starting with 0 and
* the value of each message being a single integer, starting with 0 * increasing by one with each messsage.
* and increasing by one with each messsage.
*/ */
public function startTicker (tickerName :String, msOfDelay :int) :void public function startTicker (tickerName :String, msOfDelay :int) :void
{ {
@@ -376,8 +360,7 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Send a message that will be heard by everyone in the game room, * Send a message that will be heard by everyone in the game room, even observers.
* even observers.
*/ */
public function sendChat (msg :String) :void public function sendChat (msg :String) :void
{ {
@@ -385,25 +368,25 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Display the specified message immediately locally: not sent * Display the specified message immediately locally: not sent to any other players or
* to any other players or observers in the game room. * observers in the game room.
*/ */
public function localChat (msg :String) :void public function localChat (msg :String) :void
{ {
callEZCode("localChat_v1", msg); callEZCode("localChat_v1", msg);
} }
// TODO: NEW /**
* Returns the player ids of all occupants in the game room.
*/
public function getOccupants () :Array /* of playerId */ public function getOccupants () :Array /* of playerId */
{ {
return (callEZCode("getOccupants_v1") as Array); return (callEZCode("getOccupants_v1") as Array);
} }
// TODO: NEW
/** /**
* Get the display name of the specified occupant. * Get the display name of the specified occupant. Two players may have the same name: always
* Two players may have the same name: always use playerId to * use playerId to purposes of identification and comparison. The name is for display
* purposes of identification and comparison. The name is for display
* only. Will be null is the specified playerId is not in the game. * only. Will be null is the specified playerId is not in the game.
*/ */
public function getOccupantName (playerId :int) :String public function getOccupantName (playerId :int) :String
@@ -411,21 +394,42 @@ public class EZGameControl extends BaseControl
return String(callEZCode("getOccupantName_v1", playerId)); return String(callEZCode("getOccupantName_v1", playerId));
} }
// TODO: NEW /**
* Returns this client's player id.
*/
public function getMyId () :int public function getMyId () :int
{ {
return int(callEZCode("getMyId_v1")); return int(callEZCode("getMyId_v1"));
} }
/**
* Returns true if we are in control of this game. False if another client is in control.
*/
public function amInControl () :Boolean
{
return getControllerId() == getMyId();
}
/**
* Returns the player id of the client that is in control of this game.
*/
public function getControllerId () :int
{
return int(callEZCode("getControllerId_v1"));
}
/**
* Returns the player id of the current turn holder.
*/
public function getTurnHolder () :int public function getTurnHolder () :int
{ {
return int(callEZCode("getTurnHolder_v1")); return int(callEZCode("getTurnHolder_v1"));
} }
/** /**
* Get the user-specific game data for the specified user. The * Get the user-specific game data for the specified user. The first time this is requested per
* first time this is requested per game instance it will be retrieved * game instance it will be retrieved from the database. After that, it will be returned from
* from the database. After that, it will be returned from memory. * memory.
*/ */
public function getUserCookie (occupantId :int, callback :Function) :void public function getUserCookie (occupantId :int, callback :Function) :void
{ {
@@ -433,18 +437,14 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Store persistent data that can later be retrieved by an instance * Store persistent data that can later be retrieved by an instance of this game. The maximum
* of this game. The maximum size of this data is 4096 bytes AFTER * size of this data is 4096 bytes AFTER AMF3 encoding. Note: there is no playerId parameter
* AMF3 encoding. * because a cookie may only be stored for the current player.
* *
* Note: there is no playerIndex parameter because a cookie may only * @return false if the cookie could not be encoded to 4096 bytes or less; true if the cookie
* be stored for the current player. * 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
* @return false if the cookie could not be encoded to 4096 bytes * nothing you can do anyway.
* 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 public function setUserCookie (cookie :Object) :Boolean
{ {
@@ -468,8 +468,8 @@ public class EZGameControl extends BaseControl
} }
/** /**
* End the current turn. If no next player id is specified, * End the current turn. If no next player id is specified, then the next player after the
* then the next player after the current one is used. * current one is used.
*/ */
public function endTurn (nextPlayerId :int = 0) :void public function endTurn (nextPlayerId :int = 0) :void
{ {
@@ -489,12 +489,13 @@ public class EZGameControl extends BaseControl
} }
/** /**
* Populate any properties or functions we want to expose to * Populate any properties or functions we want to expose to the other side of the ezgame
* the other side of the ezgame security boundary. * security boundary.
*/ */
protected function populateProperties (o :Object) :void protected function populateProperties (o :Object) :void
{ {
o["propertyWasSet_v1"] = propertyWasSet_v1; o["propertyWasSet_v1"] = propertyWasSet_v1;
o["controlDidChange_v1"] = controlDidChange_v1;
o["turnDidChange_v1"] = turnDidChange_v1; o["turnDidChange_v1"] = turnDidChange_v1;
o["messageReceived_v1"] = messageReceived_v1; o["messageReceived_v1"] = messageReceived_v1;
o["gameDidStart_v1"] = gameDidStart_v1; o["gameDidStart_v1"] = gameDidStart_v1;
@@ -513,6 +514,14 @@ public class EZGameControl extends BaseControl
new PropertyChangedEvent(this, name, newValue, oldValue, index)); new PropertyChangedEvent(this, name, newValue, oldValue, index));
} }
/**
* Private method to post a StateChangedEvent.
*/
private function controlDidChange_v1 () :void
{
dispatch(new StateChangedEvent(StateChangedEvent.CONTROL_CHANGED, this));
}
/** /**
* Private method to post a StateChangedEvent. * Private method to post a StateChangedEvent.
*/ */
@@ -551,14 +560,13 @@ public class EZGameControl extends BaseControl
private function occupantChanged_v1 (occupantId :int, player :Boolean, enter :Boolean) :void private function occupantChanged_v1 (occupantId :int, player :Boolean, enter :Boolean) :void
{ {
dispatch(new OccupantChangedEvent( dispatch(new OccupantChangedEvent(
enter ? OccupantChangedEvent.OCCUPANT_ENTERED enter ? OccupantChangedEvent.OCCUPANT_ENTERED :
: OccupantChangedEvent.OCCUPANT_LEFT, OccupantChangedEvent.OCCUPANT_LEFT, this, occupantId, player));
this, occupantId, player));
} }
/** /**
* Sets the properties we received from the EZ game framework * Sets the properties we received from the EZ game framework on the other side of the security
* on the other side of the security boundary. * boundary.
*/ */
protected function setEZProps (o :Object) :void protected function setEZProps (o :Object) :void
{ {
@@ -606,9 +614,9 @@ public class EZGameControl extends BaseControl
protected function checkIsConnected () :void protected function checkIsConnected () :void
{ {
if (!isConnected()) { if (!isConnected()) {
throw new IllegalOperationError("The game is not connected " + throw new IllegalOperationError(
"to The Whirled, please check isConnected(). If false, your " + "The game is not connected to The Whirled, please check isConnected(). " +
"game is being viewed standalone and should adjust."); "If false, your game is being viewed standalone and should adjust.");
} }
} }
@@ -34,6 +34,9 @@ public class StateChangedEvent extends EZEvent
/** Indicates that the game has transitioned to a ended state. */ /** Indicates that the game has transitioned to a ended state. */
public static const GAME_ENDED :String = "GameEnded"; public static const GAME_ENDED :String = "GameEnded";
/** Indicates that a new controller has been assigned. */
public static const CONTROL_CHANGED :String = "ControlChanged";
/** Indicates that the turn has changed. */ /** Indicates that the turn has changed. */
// TODO: move to own event? // TODO: move to own event?
public static const TURN_CHANGED :String = "TurnChanged"; public static const TURN_CHANGED :String = "TurnChanged";
@@ -25,6 +25,8 @@ import flash.events.Event;
import com.threerings.util.Name; import com.threerings.util.Name;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.crowd.client.PlaceView; import com.threerings.crowd.client.PlaceView;
import com.threerings.crowd.data.BodyObject; import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.PlaceConfig; import com.threerings.crowd.data.PlaceConfig;
@@ -44,22 +46,21 @@ import com.threerings.ezgame.data.EZGameObject;
public class EZGameController extends GameController public class EZGameController extends GameController
implements TurnGameController implements TurnGameController
{ {
/**
*/
public function EZGameController () public function EZGameController ()
{ {
addDelegate(_turnDelegate = new TurnGameControllerDelegate(this)); addDelegate(_turnDelegate = new TurnGameControllerDelegate(this));
} }
/** /**
* This is called by the GameControlBackend once it has initialized * This is called by the GameControlBackend once it has initialized and made contact with
* and made contact with usercode. * usercode.
*/ */
public function userCodeIsConnected () :void public function userCodeIsConnected () :void
{ {
playerReady(); playerReady();
} }
// from PlaceController
override public function willEnterPlace (plobj :PlaceObject) :void override public function willEnterPlace (plobj :PlaceObject) :void
{ {
_ezObj = (plobj as EZGameObject); _ezObj = (plobj as EZGameObject);
@@ -67,6 +68,7 @@ public class EZGameController extends GameController
super.willEnterPlace(plobj); super.willEnterPlace(plobj);
} }
// from PlaceController
override public function didLeavePlace (plobj :PlaceObject) :void override public function didLeavePlace (plobj :PlaceObject) :void
{ {
super.didLeavePlace(plobj); super.didLeavePlace(plobj);
@@ -80,13 +82,24 @@ public class EZGameController extends GameController
_panel.backend.turnDidChange(); _panel.backend.turnDidChange();
} }
// from GameController
override public function attributeChanged (event :AttributeChangedEvent) :void
{
var name :String = event.getName();
if (EZGameObject.CONTROLLER_OID == name) {
_panel.backend.controlDidChange();
} else {
super.attributeChanged(event);
}
}
// from GameController
override protected function playerReady () :void override protected function playerReady () :void
{ {
// we require the user to be connected, and we redundantly only // we require the user to be connected, and we redundantly only do this if the user is in
// do this if the user is in the players array // the players array
if (_panel.backend.isConnected()) { if (_panel.backend.isConnected()) {
var bobj :BodyObject = var bobj :BodyObject = (_ctx.getClient().getClientObject() as BodyObject);
(_ctx.getClient().getClientObject() as BodyObject);
if (_gobj.getPlayerIndex(bobj.getVisibleName()) != -1) { if (_gobj.getPlayerIndex(bobj.getVisibleName()) != -1) {
super.playerReady(); super.playerReady();
} }
@@ -96,36 +109,28 @@ public class EZGameController extends GameController
} }
} }
// from GameController
override protected function gameDidStart () :void override protected function gameDidStart () :void
{ {
super.gameDidStart(); super.gameDidStart();
_panel.backend.gameDidStart(); _panel.backend.gameDidStart();
} }
// from GameController
override protected function gameDidEnd () :void override protected function gameDidEnd () :void
{ {
super.gameDidEnd(); super.gameDidEnd();
_panel.backend.gameDidEnd(); _panel.backend.gameDidEnd();
} }
// from PlaceController
override protected function createPlaceView (ctx :CrowdContext) :PlaceView override protected function createPlaceView (ctx :CrowdContext) :PlaceView
{ {
return new EZGamePanel(ctx, this); return _panel = new EZGamePanel(ctx, this);
}
override protected function didInit () :void
{
super.didInit();
// retain a casted reference to our panel
_panel = (_view as EZGamePanel);
} }
protected var _ezObj :EZGameObject; protected var _ezObj :EZGameObject;
protected var _turnDelegate :TurnGameControllerDelegate; protected var _turnDelegate :TurnGameControllerDelegate;
protected var _panel :EZGamePanel; protected var _panel :EZGamePanel;
} }
} }
@@ -189,6 +189,7 @@ public class GameControlBackend
o["sendMessage_v2"] = sendMessage_v2; o["sendMessage_v2"] = sendMessage_v2;
o["getOccupants_v1"] = getOccupants_v1; o["getOccupants_v1"] = getOccupants_v1;
o["getMyId_v1"] = getMyId_v1; o["getMyId_v1"] = getMyId_v1;
o["getControllerId_v1"] = getControllerId_v1;
o["getUserCookie_v2"] = getUserCookie_v2; o["getUserCookie_v2"] = getUserCookie_v2;
o["endTurn_v2"] = endTurn_v2; o["endTurn_v2"] = endTurn_v2;
o["endGame_v2"] = endGame_v2; o["endGame_v2"] = endGame_v2;
@@ -316,6 +317,11 @@ public class GameControlBackend
return _ctx.getClient().getClientObject().getOid(); return _ctx.getClient().getClientObject().getOid();
} }
public function getControllerId_v1 () :int
{
return _ezObj.controllerOid;
}
// TODO: table games only // TODO: table games only
public function getPlayerPosition_v1 (playerId :int) :int public function getPlayerPosition_v1 (playerId :int) :int
{ {
@@ -659,6 +665,14 @@ public class GameControlBackend
} }
} }
/**
* Called by the EZGameController when the controller changes.
*/
public function controlDidChange () :void
{
callUserCode("controlDidChange_v1");
}
/** /**
* Called by the EZGameController when the turn changes. * Called by the EZGameController when the turn changes.
*/ */
@@ -51,6 +51,9 @@ public class EZGameObject extends GameObject
public static const TICKER :String = "Utick"; public static const TICKER :String = "Utick";
// AUTO-GENERATED: FIELDS START // AUTO-GENERATED: FIELDS START
/** The field name of the <code>controllerOid</code> field. */
public static const CONTROLLER_OID :String = "controllerOid";
/** The field name of the <code>turnHolder</code> field. */ /** The field name of the <code>turnHolder</code> field. */
public static const TURN_HOLDER :String = "turnHolder"; public static const TURN_HOLDER :String = "turnHolder";
@@ -61,6 +64,10 @@ public class EZGameObject extends GameObject
public static const EZ_GAME_SERVICE :String = "ezGameService"; public static const EZ_GAME_SERVICE :String = "ezGameService";
// AUTO-GENERATED: FIELDS END // AUTO-GENERATED: FIELDS END
/** The client that is in control of this game. The first client to enter will be assigned
* control and control will subsequently be reassigned if that client disconnects or leaves. */
public var controllerOid :int;
/** The current turn holder. */ /** The current turn holder. */
public var turnHolder :Name; public var turnHolder :Name;
@@ -96,30 +103,11 @@ public class EZGameObject extends GameObject
return players; return players;
} }
// // AUTO-GENERATED: METHODS START
// /**
// * Requests that the <code>turnHolder</code> 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. * Called by a PropertySetEvent to enact a property change.
* @return the old value * @return the old value
*/ */
public function applyPropertySet ( public function applyPropertySet (propName :String, value :Object, index :int) :Object
propName :String, value :Object, index :int) :Object
{ {
var oldValue :Object = _props[propName]; var oldValue :Object = _props[propName];
if (index >= 0) { if (index >= 0) {
@@ -143,20 +131,6 @@ public class EZGameObject extends GameObject
return oldValue; return oldValue;
} }
// override public function writeObject (out :ObjectOutputStream) :void
// {
// super.writeObject(out);
//
// out.writeObject(turnHolder);
// out.writeObject(ezGameService);
//
// out.writeInt(_props.length);
// for (var key :String in _props) {
// out.writeUTF(key);
// out.writeObject(EZObjectMarshaller.encode(_props[key]));
// }
// }
override public function readObject (ins :ObjectInputStream) :void override public function readObject (ins :ObjectInputStream) :void
{ {
super.readObject(ins); super.readObject(ins);
@@ -178,6 +152,7 @@ public class EZGameObject extends GameObject
*/ */
protected function readDefaultFields (ins :ObjectInputStream) :void protected function readDefaultFields (ins :ObjectInputStream) :void
{ {
controllerOid = ins.readInt();
turnHolder = (ins.readObject() as Name); turnHolder = (ins.readObject() as Name);
userCookies = (ins.readObject() as DSet); userCookies = (ins.readObject() as DSet);
ezGameService = (ins.readObject() as EZGameMarshaller); ezGameService = (ins.readObject() as EZGameMarshaller);
@@ -58,6 +58,9 @@ public class EZGameObject extends GameObject
public static final String TICKER = "Utick"; public static final String TICKER = "Utick";
// AUTO-GENERATED: FIELDS START // AUTO-GENERATED: FIELDS START
/** The field name of the <code>controllerOid</code> field. */
public static final String CONTROLLER_OID = "controllerOid";
/** The field name of the <code>turnHolder</code> field. */ /** The field name of the <code>turnHolder</code> field. */
public static final String TURN_HOLDER = "turnHolder"; public static final String TURN_HOLDER = "turnHolder";
@@ -68,6 +71,10 @@ public class EZGameObject extends GameObject
public static final String EZ_GAME_SERVICE = "ezGameService"; public static final String EZ_GAME_SERVICE = "ezGameService";
// AUTO-GENERATED: FIELDS END // AUTO-GENERATED: FIELDS END
/** The client that is in control of this game. The first client to enter will be assigned
* control and control will subsequently be reassigned if that client disconnects or leaves. */
public int controllerOid;
/** The current turn holder. */ /** The current turn holder. */
public Name turnHolder; public Name turnHolder;
@@ -223,6 +230,22 @@ public class EZGameObject extends GameObject
// AUTO-GENERATED: METHODS START // AUTO-GENERATED: METHODS START
/**
* Requests that the <code>controllerOid</code> 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 setControllerOid (int value)
{
int ovalue = this.controllerOid;
requestAttributeChange(
CONTROLLER_OID, Integer.valueOf(value), Integer.valueOf(ovalue));
this.controllerOid = value;
}
/** /**
* Requests that the <code>turnHolder</code> field be set to the * Requests that the <code>turnHolder</code> field be set to the
* specified value. The local value will be updated immediately and an * specified value. The local value will be updated immediately and an
@@ -47,6 +47,7 @@ import com.threerings.presents.client.InvocationService;
import com.threerings.presents.server.InvocationException; import com.threerings.presents.server.InvocationException;
import com.threerings.crowd.data.BodyObject; import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.OccupantInfo;
import com.threerings.crowd.data.PlaceObject; import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.server.CrowdServer; import com.threerings.crowd.server.CrowdServer;
@@ -92,8 +93,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void endTurn ( public void endTurn (ClientObject caller, int nextPlayerId,
ClientObject caller, int nextPlayerId,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
@@ -111,8 +111,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void endGame ( public void endGame (ClientObject caller, int[] winnerOids,
ClientObject caller, int[] winnerOids,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
@@ -126,25 +125,21 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void sendMessage ( public void sendMessage (ClientObject caller, String msg, Object data, int playerId,
ClientObject caller, String msg, Object data, int playerId,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
validateUser(caller); validateUser(caller);
if (playerId == 0) { if (playerId == 0) {
_gameObj.postMessage(EZGameObject.USER_MESSAGE, _gameObj.postMessage(EZGameObject.USER_MESSAGE, new Object[] { msg, data });
new Object[] { msg, data });
} else { } else {
sendPrivateMessage(playerId, msg, data); sendPrivateMessage(playerId, msg, data);
} }
} }
// from EZGameProvider // from EZGameProvider
public void setProperty ( public void setProperty (ClientObject caller, String propName, Object data, int index,
ClientObject caller, String propName, Object data, int index,
boolean testAndSet, Object testValue, boolean testAndSet, Object testValue,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
@@ -157,8 +152,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void getDictionaryLetterSet ( public void getDictionaryLetterSet (ClientObject caller, String locale, int count,
ClientObject caller, String locale, int count,
InvocationService.ResultListener listener) InvocationService.ResultListener listener)
throws InvocationException throws InvocationException
{ {
@@ -167,8 +161,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void checkDictionaryWord ( public void checkDictionaryWord (ClientObject caller, String locale, String word,
ClientObject caller, String locale, String word,
InvocationService.ResultListener listener) InvocationService.ResultListener listener)
throws InvocationException throws InvocationException
{ {
@@ -191,9 +184,9 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void addToCollection ( public void addToCollection (ClientObject caller, String collName, byte[][] data,
ClientObject caller, String collName, byte[][] data, boolean clearExisting,
boolean clearExisting, InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
validateUser(caller); validateUser(caller);
@@ -201,8 +194,7 @@ public class EZGameManager extends GameManager
_collections = new HashMap<String, ArrayList<byte[]>>(); _collections = new HashMap<String, ArrayList<byte[]>>();
} }
// figure out if we're adding to an existing collection // figure out if we're adding to an existing collection or creating a new one
// or creating a new one
ArrayList<byte[]> list = null; ArrayList<byte[]> list = null;
if (!clearExisting) { if (!clearExisting) {
list = _collections.get(collName); list = _collections.get(collName);
@@ -216,8 +208,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void getFromCollection ( public void getFromCollection (ClientObject caller, String collName, boolean consume, int count,
ClientObject caller, String collName, boolean consume, int count,
String msgOrPropName, int playerId, String msgOrPropName, int playerId,
InvocationService.ConfirmListener listener) InvocationService.ConfirmListener listener)
throws InvocationException throws InvocationException
@@ -243,12 +234,10 @@ public class EZGameManager extends GameManager
if (playerId == 0) { if (playerId == 0) {
setProperty(msgOrPropName, result, -1); setProperty(msgOrPropName, result, -1);
} else { } else {
sendPrivateMessage(playerId, msgOrPropName, result); sendPrivateMessage(playerId, msgOrPropName, result);
} }
// SUCCESS! listener.requestProcessed(); // SUCCESS!
listener.requestProcessed();
return; return;
} }
} }
@@ -258,15 +247,14 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void mergeCollection ( public void mergeCollection (ClientObject caller, String srcColl, String intoColl,
ClientObject caller, String srcColl, String intoColl,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
validateUser(caller); validateUser(caller);
// non-existent collections are treated as empty, so if the // non-existent collections are treated as empty, so if the source doesn't exist, we
// source doesn't exist, we silently accept it // silently accept it
if (_collections != null) { if (_collections != null) {
ArrayList<byte[]> src = _collections.remove(srcColl); ArrayList<byte[]> src = _collections.remove(srcColl);
if (src != null) { if (src != null) {
@@ -281,8 +269,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void setTicker ( public void setTicker (ClientObject caller, String tickerName, int msOfDelay,
ClientObject caller, String tickerName, int msOfDelay,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
@@ -292,11 +279,11 @@ public class EZGameManager extends GameManager
if (msOfDelay >= MIN_TICKER_DELAY) { if (msOfDelay >= MIN_TICKER_DELAY) {
if (_tickers != null) { if (_tickers != null) {
t = _tickers.get(tickerName); t = _tickers.get(tickerName);
} else { } else {
_tickers = new HashMap<String, Ticker>(); _tickers = new HashMap<String, Ticker>();
t = null; t = null;
} }
if (t == null) { if (t == null) {
if (_tickers.size() >= MAX_TICKERS) { if (_tickers.size() >= MAX_TICKERS) {
throw new InvocationException(ACCESS_DENIED); throw new InvocationException(ACCESS_DENIED);
@@ -320,8 +307,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void getCookie ( public void getCookie (ClientObject caller, final int playerId,
ClientObject caller, final int playerId,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
@@ -338,28 +324,22 @@ public class EZGameManager extends GameManager
if (!_cookieLookups.contains(playerId)) { if (!_cookieLookups.contains(playerId)) {
BodyObject body = getOccupantByOid(playerId); BodyObject body = getOccupantByOid(playerId);
if (body == null) { if (body == null) {
log.fine("getCookie() called with invalid occupantId " + log.fine("getCookie() called with invalid occupant [occupantId=" + playerId + "].");
"[occupantId=" + playerId + "].");
throw new InvocationException(INTERNAL_ERROR); throw new InvocationException(INTERNAL_ERROR);
} }
gcm.getCookie(getPersistentGameId(), body, gcm.getCookie(getPersistentGameId(), body, new ResultListener<byte[]>() {
new ResultListener<byte[]>() {
public void requestCompleted (byte[] result) { public void requestCompleted (byte[] result) {
// Result may be null: that's ok, it means // Result may be null: that's ok, it means we've looked up the user's
// we've looked up the user's nonexistant cookie. // nonexistant cookie. Only set the cookie if the playerIndex is still in the
// Only set the cookie if the playerIndex is // lookup set, otherwise they left!
// still in the lookup set, otherwise they left! if (_cookieLookups.remove(playerId) && _gameObj.isActive()) {
if (_cookieLookups.remove(playerId) && _gameObj.addToUserCookies(new UserCookie(playerId, result));
_gameObj.isActive()) {
_gameObj.addToUserCookies(
new UserCookie(playerId, result));
} }
} }
public void requestFailed (Exception cause) { public void requestFailed (Exception cause) {
log.warning("Unable to retrieve cookie " + log.warning("Unable to retrieve cookie [cause=" + cause + "].");
"[cause=" + cause + "].");
requestCompleted(null); requestCompleted(null);
} }
}); });
@@ -370,8 +350,7 @@ public class EZGameManager extends GameManager
} }
// from EZGameProvider // from EZGameProvider
public void setCookie ( public void setCookie (ClientObject caller, byte[] value,
ClientObject caller, byte[] value,
InvocationService.InvocationListener listener) InvocationService.InvocationListener listener)
throws InvocationException throws InvocationException
{ {
@@ -408,8 +387,8 @@ public class EZGameManager extends GameManager
} }
/** /**
* Helper method to send a private message to the specified player oid * Helper method to send a private message to the specified player oid (must already be
* (must already be verified). * verified).
*/ */
protected void sendPrivateMessage ( protected void sendPrivateMessage (
int playerId, String msg, Object data) int playerId, String msg, Object data)
@@ -421,21 +400,19 @@ public class EZGameManager extends GameManager
throw new InvocationException("m.player_not_around"); throw new InvocationException("m.player_not_around");
} }
target.postMessage( target.postMessage(EZGameObject.USER_MESSAGE + ":" + _gameObj.getOid(),
EZGameObject.USER_MESSAGE + ":" + _gameObj.getOid(),
new Object[] { msg, data }); new Object[] { msg, data });
} }
/** /**
* Helper method to post a property set event. * Helper method to post a property set event.
*/ */
protected void setProperty ( protected void setProperty (String propName, Object value, int index)
String propName, Object value, int index)
{ {
// apply the property set immediately // apply the property set immediately
Object oldValue = _gameObj.applyPropertySet(propName, value, index); Object oldValue = _gameObj.applyPropertySet(propName, value, index);
_gameObj.postEvent(new PropertySetEvent( _gameObj.postEvent(
_gameObj.getOid(), propName, value, index, oldValue)); new PropertySetEvent(_gameObj.getOid(), propName, value, index, oldValue));
} }
/** /**
@@ -470,8 +447,7 @@ public class EZGameManager extends GameManager
} }
/** /**
* Validate that the specified listener has access to make a * Validate that the specified listener has access to make a change.
* change.
*/ */
protected void validateStateModification (ClientObject caller) protected void validateStateModification (ClientObject caller)
throws InvocationException throws InvocationException
@@ -479,8 +455,7 @@ public class EZGameManager extends GameManager
validateUser(caller); validateUser(caller);
Name holder = _gameObj.turnHolder; Name holder = _gameObj.turnHolder;
if (holder != null && if (holder != null && !holder.equals(((BodyObject) caller).getVisibleName())) {
!holder.equals(((BodyObject) caller).getVisibleName())) {
throw new InvocationException(InvocationCodes.ACCESS_DENIED); throw new InvocationException(InvocationCodes.ACCESS_DENIED);
} }
} }
@@ -539,6 +514,44 @@ public class EZGameManager extends GameManager
} }
} }
@Override // from PlaceManager
protected void bodyEntered (int bodyOid)
{
super.bodyEntered(bodyOid);
// if we have no controller, then our new friend gets control
if (_gameObj.controllerOid == 0) {
_gameObj.setControllerOid(bodyOid);
}
}
@Override // from PlaceManager
protected void bodyUpdated (OccupantInfo info)
{
super.bodyUpdated(info);
// if the controller just disconnected, reassign control
if (info.status == OccupantInfo.DISCONNECTED && info.bodyOid == _gameObj.controllerOid) {
_gameObj.setControllerOid(getControllerOid());
// if everyone in the room was disconnected and this client just reconnected, it becomes
// the new controller
} else if (_gameObj.controllerOid == 0) {
_gameObj.setControllerOid(info.bodyOid);
}
}
@Override // from PlaceManager
protected void bodyLeft (int bodyOid)
{
super.bodyLeft(bodyOid);
// if this player was the controller, reassign control
if (bodyOid == _gameObj.controllerOid) {
_gameObj.setControllerOid(getControllerOid());
}
}
@Override @Override
protected void didShutdown () protected void didShutdown ()
{ {
@@ -562,8 +575,7 @@ public class EZGameManager extends GameManager
super.playerGameDidEnd(pidx); super.playerGameDidEnd(pidx);
// kill any of their cookies // kill any of their cookies
if (_gameObj.userCookies != null && if (_gameObj.userCookies != null && _gameObj.userCookies.containsKey(pidx)) {
_gameObj.userCookies.containsKey(pidx)) {
_gameObj.removeFromUserCookies(pidx); _gameObj.removeFromUserCookies(pidx);
} }
// halt the loading of their cookie, if in progress // halt the loading of their cookie, if in progress
@@ -599,6 +611,20 @@ public class EZGameManager extends GameManager
} }
} }
/**
* Returns the oid of a player to whom to assign control of the game or zero if no players
* qualify for control.
*/
protected int getControllerOid ()
{
for (OccupantInfo info : _gameObj.occupantInfo) {
if (info.status != OccupantInfo.DISCONNECTED) {
return info.bodyOid;
}
}
return 0;
}
/** /**
* A timer that fires message events to a game. * A timer that fires message events to a game.
*/ */
@@ -610,8 +636,7 @@ public class EZGameManager extends GameManager
public Ticker (String name, EZGameObject gameObj) public Ticker (String name, EZGameObject gameObj)
{ {
_name = name; _name = name;
// once we are constructed, we want to avoid calling // once we are constructed, we want to avoid calling methods on dobjs.
// methods on dobjs.
_oid = gameObj.getOid(); _oid = gameObj.getOid();
_omgr = gameObj.getManager(); _omgr = gameObj.getManager();
} }
@@ -627,20 +652,17 @@ public class EZGameManager extends GameManager
_interval.cancel(); _interval.cancel();
} }
/** The interval that does our work. Note well that this is /**
* not a 'safe' interval that operates using a RunQueue. * The interval that does our work. Note well that this is not a 'safe' interval that
* This interval instead does something that we happen to know * operates using a RunQueue. This interval instead does something that we happen to know
* is safe for any thread: posting an event to the dobj manager. * is safe for any thread: posting an event to the dobj manager. If we were using a
* If we were using a RunQueue it would be the same event queue * RunQueue it would be the same event queue and we would be posted there, wait our turn,
* and we would be posted there, wait our turn, and then do the same * and then do the same thing: post this event. We just expedite the process.
* thing: post this event. We just expedite the process.
*/ */
protected Interval _interval = new Interval() { protected Interval _interval = new Interval() {
public void expired () public void expired () {
{
_omgr.postEvent( _omgr.postEvent(
new MessageEvent(_oid, EZGameObject.TICKER, new MessageEvent(_oid, EZGameObject.TICKER, new Object[] { _name, _value++ }));
new Object[] { _name, _value++ }));
} }
}; };