Files
vilya/src/java/com/threerings/ezgame/server/EZGameManager.java
T
Robert Zubeck 83e02730cf Fixes for property setting via atomic test-and-set:
- Moved property testing, so that it happens before any propertySet events are
  dispatched. In the previous version, testing happened while processing the
  set event on the server - but since by that time client events have already been 
  sent, it introduced the possibility of short-lived inconsistencies between client 
  and server data models.   

- Introduced a separate EZ API call for test and set - not only does it perform
  the test, but unlike regular set, it does not cache the new value ahead of time. 
  Instead the new value will have to arrive from the server, at some future point.

- Trimmed PropertySetEvent and other handlers back down - they don't need to
  carry any of the test info around, after it's already been performed. Also 
  cut redundant testing on the clients.




git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@195 c613c5cb-e716-0410-b11b-feb51c14d237
2007-02-19 19:40:09 +00:00

649 lines
19 KiB
Java

//
// $Id$
package com.threerings.ezgame.server;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.CollectionUtil;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.Interval;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.ResultListener;
import com.threerings.util.Name;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationCodes;
import com.threerings.presents.dobj.AccessController;
import com.threerings.presents.dobj.DObjectManager;
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.MessageEvent;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.server.InvocationException;
import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.server.CrowdServer;
import com.threerings.parlor.game.data.GameConfig;
import com.threerings.parlor.game.server.GameManager;
import com.threerings.parlor.turn.server.TurnGameManager;
import com.threerings.ezgame.data.EZGameConfig;
import com.threerings.ezgame.data.EZGameObject;
import com.threerings.ezgame.data.EZGameMarshaller;
import com.threerings.ezgame.data.PropertySetEvent;
import com.threerings.ezgame.data.UserCookie;
import static com.threerings.ezgame.server.Log.log;
/**
* A manager for "ez" games.
*/
public class EZGameManager extends GameManager
implements EZGameProvider, TurnGameManager
{
public EZGameManager ()
{
addDelegate(_turnDelegate = new EZGameTurnDelegate(this));
}
// from TurnGameManager
public void turnWillStart ()
{
}
// from TurnGameManager
public void turnDidStart ()
{
}
// from TurnGameManager
public void turnDidEnd ()
{
}
// from EZGameProvider
public void endTurn (
ClientObject caller, int nextPlayerId,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateStateModification(caller);
Name nextTurnHolder = null;
if (nextPlayerId != 0) {
BodyObject target = getPlayerByOid(nextPlayerId);
if (target != null) {
nextTurnHolder = target.getVisibleName();
}
}
_turnDelegate.endTurn(nextTurnHolder);
}
// from EZGameProvider
public void endGame (
ClientObject caller, int[] winnerOids,
InvocationService.InvocationListener listener)
throws InvocationException
{
if (!_gameObj.isInPlay()) {
throw new InvocationException("e.already_ended");
}
validateStateModification(caller);
_winnerIds = winnerOids;
endGame();
}
// from EZGameProvider
public void sendMessage (
ClientObject caller, String msg, Object data, int playerId,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
if (playerId == 0) {
_gameObj.postMessage(EZGameObject.USER_MESSAGE,
new Object[] { msg, data });
} else {
sendPrivateMessage(playerId, msg, data);
}
}
// from EZGameProvider
public void setProperty (
ClientObject caller, String propName, Object data, int index,
boolean testAndSet, Object testValue,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
setProperty(propName, data, index, testAndSet, testValue);
}
// from EZGameProvider
public void getDictionaryLetterSet (
ClientObject caller, String locale, int count,
InvocationService.ResultListener listener)
throws InvocationException
{
DictionaryService dictionary = getDictionaryService ();
dictionary.getLetterSet (locale, count, listener);
}
// from EZGameProvider
public void checkDictionaryWord (
ClientObject caller, String locale, String word,
InvocationService.ResultListener listener)
throws InvocationException
{
DictionaryService dictionary = getDictionaryService ();
dictionary.checkWord (locale, word, listener);
}
/**
* Find the dictionary manager
*/
protected DictionaryService getDictionaryService ()
throws InvocationException
{
DictionaryService dictionary = DictionaryService.getInstance ();
if (dictionary == null) {
log.warning("DictionaryService not initialized.");
throw new InvocationException(INTERNAL_ERROR);
}
return dictionary;
}
// from EZGameProvider
public void addToCollection (
ClientObject caller, String collName, byte[][] data,
boolean clearExisting, InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
if (_collections == null) {
_collections = new HashMap<String, ArrayList<byte[]>>();
}
// figure out if we're adding to an existing collection
// or creating a new one
ArrayList<byte[]> list = null;
if (!clearExisting) {
list = _collections.get(collName);
}
if (list == null) {
list = new ArrayList<byte[]>();
_collections.put(collName, list);
}
CollectionUtil.addAll(list, data);
}
// from EZGameProvider
public void getFromCollection (
ClientObject caller, String collName, boolean consume, int count,
String msgOrPropName, int playerId,
InvocationService.ConfirmListener listener)
throws InvocationException
{
validateUser(caller);
int srcSize = 0;
if (_collections != null) {
ArrayList<byte[]> src = _collections.get(collName);
srcSize = (src == null) ? 0 : src.size();
if (srcSize >= count) {
byte[][] result = new byte[count][];
for (int ii=0; ii < count; ii++) {
int pick = RandomUtil.getInt(srcSize);
if (consume) {
result[ii] = src.remove(pick);
srcSize--;
} else {
result[ii] = src.get(pick);
}
}
if (playerId == 0) {
setProperty(msgOrPropName, result, -1, false, null);
} else {
sendPrivateMessage(playerId, msgOrPropName, result);
}
// SUCCESS!
listener.requestProcessed();
return;
}
}
// TODO: decide what we want to return here
throw new InvocationException(String.valueOf(srcSize));
}
// from EZGameProvider
public void mergeCollection (
ClientObject caller, String srcColl, String intoColl,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
// non-existent collections are treated as empty, so if the
// source doesn't exist, we silently accept it
if (_collections != null) {
ArrayList<byte[]> src = _collections.remove(srcColl);
if (src != null) {
ArrayList<byte[]> dest = _collections.get(intoColl);
if (dest == null) {
_collections.put(intoColl, src);
} else {
dest.addAll(src);
}
}
}
}
// from EZGameProvider
public void setTicker (
ClientObject caller, String tickerName, int msOfDelay,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
Ticker t;
if (msOfDelay >= MIN_TICKER_DELAY) {
if (_tickers != null) {
t = _tickers.get(tickerName);
} else {
_tickers = new HashMap<String, Ticker>();
t = null;
}
if (t == null) {
if (_tickers.size() >= MAX_TICKERS) {
throw new InvocationException(ACCESS_DENIED);
}
t = new Ticker(tickerName, _gameObj);
_tickers.put(tickerName, t);
}
t.start(msOfDelay);
} else if (msOfDelay <= 0) {
if (_tickers != null) {
t = _tickers.remove(tickerName);
if (t != null) {
t.stop();
}
}
} else {
throw new InvocationException(ACCESS_DENIED);
}
}
// from EZGameProvider
public void getCookie (
ClientObject caller, final int playerId,
InvocationService.InvocationListener listener)
throws InvocationException
{
GameCookieManager gcm = getCookieManager();
if (_gameObj.userCookies.containsKey(playerId)) {
// already loaded: we do nothing
return;
}
if (_cookieLookups == null) {
_cookieLookups = new ArrayIntSet();
}
// we only start looking up the cookie if nobody else already is
if (!_cookieLookups.contains(playerId)) {
gcm.getCookie(getPersistentGameId(), getPlayerByOid(playerId),
new ResultListener<byte[]>() {
public void requestCompleted (byte[] result) {
// Result may be null: that's ok, it means
// we've looked up the user's nonexistant cookie.
// Only set the cookie if the playerIndex is
// still in the lookup set, otherwise they left!
if (_cookieLookups.remove(playerId) &&
_gameObj.isActive()) {
_gameObj.addToUserCookies(
new UserCookie(playerId, result));
}
}
public void requestFailed (Exception cause) {
log.warning("Unable to retrieve cookie " +
"[cause=" + cause + "].");
requestCompleted(null);
}
});
// indicate that we're looking up a cookie
_cookieLookups.add(playerId);
}
}
// from EZGameProvider
public void setCookie (
ClientObject caller, byte[] value,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
GameCookieManager gcm = getCookieManager();
UserCookie cookie = new UserCookie(caller.getOid(), value);
if (_gameObj.userCookies.containsKey(cookie.getKey())) {
_gameObj.updateUserCookies(cookie);
} else {
_gameObj.addToUserCookies(cookie);
}
gcm.setCookie(getPersistentGameId(), caller, value);
}
/**
* Get the cookie manager, and do a bit of other setup.
*/
protected GameCookieManager getCookieManager ()
throws InvocationException
{
GameCookieManager gcm = GameCookieManager.getInstance();
if (gcm == null) {
log.warning("GameCookieManager not initialized.");
throw new InvocationException(INTERNAL_ERROR);
}
if (_gameObj.userCookies == null) {
// lazy-init this
_gameObj.setUserCookies(new DSet<UserCookie>());
}
return gcm;
}
/**
* Helper method to send a private message to the specified player oid
* (must already be verified).
*/
protected void sendPrivateMessage (
int playerId, String msg, Object data)
throws InvocationException
{
BodyObject target = getPlayerByOid(playerId);
if (target == null) {
// TODO: this code has no corresponding translation
throw new InvocationException("m.player_not_around");
}
target.postMessage(
EZGameObject.USER_MESSAGE + ":" + _gameObj.getOid(),
new Object[] { msg, data });
}
/**
* Helper method to post a property set event.
*/
protected void setProperty (
String propName, Object value, int index,
boolean testAndSet, Object testValue)
{
if (_gameObj.testProperty (propName, index, testAndSet, testValue))
{
_gameObj.postEvent(
new PropertySetEvent(
_gameObj.getOid(), propName, value, index));
}
}
/**
* Get the game id of this ezgame, as set in the config.
*/
protected int getPersistentGameId ()
throws InvocationException
{
int id = ((EZGameConfig) _config).persistentGameId;
if (id == 0) {
throw new InvocationException("Persistent game id not set.");
}
return id;
}
/**
* Validate that the specified user has access to do things in the game.
*/
protected void validateUser (ClientObject caller)
throws InvocationException
{
switch (getGameType()) {
case GameConfig.PARTY:
return; // always validate.
default:
if (getPresentPlayerIndex(caller.getOid()) == -1) {
throw new InvocationException(InvocationCodes.ACCESS_DENIED);
}
return;
}
}
/**
* Validate that the specified listener has access to make a
* change.
*/
protected void validateStateModification (ClientObject caller)
throws InvocationException
{
validateUser(caller);
Name holder = _gameObj.turnHolder;
if (holder != null &&
!holder.equals(((BodyObject) caller).getVisibleName())) {
throw new InvocationException(InvocationCodes.ACCESS_DENIED);
}
}
/**
* Get the specified player body by Oid.
*/
protected BodyObject getPlayerByOid (int oid)
{
// verify that they're in the room
if (!_gameObj.occupants.contains(oid)) {
return null;
}
// verify that they're a player
switch (getGameType()) {
case GameConfig.PARTY:
// all occupants are players in a party game
break;
default:
if (!IntListUtil.contains(_playerOids, oid)) {
return null; // not a player!
}
break;
}
// return the body
return (BodyObject) CrowdServer.omgr.getObject(oid);
}
@Override
protected PlaceObject createPlaceObject ()
{
return new EZGameObject();
}
@Override
protected void didStartup ()
{
super.didStartup();
_gameObj = (EZGameObject) _plobj;
_gameObj.setEzGameService(
(EZGameMarshaller) CrowdServer.invmgr.registerDispatcher(new EZGameDispatcher(this)));
// if we don't need the no-show timer, start.
if (!needsNoShowTimer()) {
startGame();
}
}
@Override
protected void didShutdown ()
{
CrowdServer.invmgr.clearDispatcher(_gameObj.ezGameService);
stopTickers();
super.didShutdown();
}
@Override
protected void gameDidEnd ()
{
stopTickers();
super.gameDidEnd();
}
@Override
protected void playerGameDidEnd (int pidx)
{
super.playerGameDidEnd(pidx);
// kill any of their cookies
if (_gameObj.userCookies != null &&
_gameObj.userCookies.containsKey(pidx)) {
_gameObj.removeFromUserCookies(pidx);
}
// halt the loading of their cookie, if in progress
if (_cookieLookups != null) {
_cookieLookups.remove(pidx);
}
}
@Override
protected void assignWinners (boolean[] winners)
{
if (_winnerIds != null) {
for (int oid : _winnerIds) {
int index = IntListUtil.indexOf(_playerOids, oid);
if (index >= 0 && index < winners.length) {
winners[index] = true;
}
}
_winnerIds = null;
}
}
/**
* Stop and clear all tickers.
*/
protected void stopTickers ()
{
if (_tickers != null) {
for (Ticker ticker : _tickers.values()) {
ticker.stop();
}
_tickers = null;
}
}
/**
* A timer that fires message events to a game.
*/
protected static class Ticker
{
/**
* Create a Ticker.
*/
public Ticker (String name, EZGameObject gameObj)
{
_name = name;
// once we are constructed, we want to avoid calling
// methods on dobjs.
_oid = gameObj.getOid();
_omgr = gameObj.getManager();
}
public void start (int msOfDelay)
{
_value = 0;
_interval.schedule(0, msOfDelay);
}
public void stop ()
{
_interval.cancel();
}
/** The interval that does our work. Note well that this is
* not a 'safe' interval that 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.
* If we were using a RunQueue it would be the same event queue
* and we would be posted there, wait our turn, and then do the same
* thing: post this event. We just expedite the process.
*/
protected Interval _interval = new Interval() {
public void expired ()
{
_omgr.postEvent(
new MessageEvent(_oid, EZGameObject.TICKER,
new Object[] { _name, _value++ }));
}
};
protected int _oid;
protected DObjectManager _omgr;
protected String _name;
protected int _value;
} // End: static class Ticker
/** A nice casted reference to the game object. */
protected EZGameObject _gameObj;
/** Our turn delegate. */
protected EZGameTurnDelegate _turnDelegate;
/** The map of collections, lazy-initialized. */
protected HashMap<String, ArrayList<byte[]>> _collections;
/** The map of tickers, lazy-initialized. */
protected HashMap<String, Ticker> _tickers;
/** Tracks which cookies are currently being retrieved from the db. */
protected ArrayIntSet _cookieLookups;
// /** User tokens, lazy-initialized. */
// protected HashIntMap<HashSet<String>> _tokens;
/** The array of winner oids, after the user has filled it in. */
protected int[] _winnerIds;
/** The minimum delay a ticker can have. */
protected static final int MIN_TICKER_DELAY = 50;
/** The maximum number of tickers allowed at one time. */
protected static final int MAX_TICKERS = 3;
}