Moved the serverless game stuff out of msoy and back into the vilya library.

We'll be using this in game gardens, at least.
Note that the actionscript side currently doesn't compile because of
limitations in building a .swc file, but that'll be fixed soon.


git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@47 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
Ray Greenwell
2006-08-23 02:28:36 +00:00
parent a90d7e655b
commit 70b0d759c0
27 changed files with 2835 additions and 0 deletions
@@ -0,0 +1,67 @@
//
// $Id$
package com.threerings.ezgame.client;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
/**
* Provides services for ez games.
*/
public interface EZGameService extends InvocationService
{
/**
* Request to set the specified property.
*/
public void setProperty (
Client client, String propName, Object value, int index,
InvocationListener listener);
/**
* Request to end the turn, possibly futzing the next turn holder unless
* -1 is specified for the nextPlayerIndex.
*/
public void endTurn (
Client client, int nextPlayerIndex, InvocationListener listener);
/**
* Request to end the game, with the specified player indices assigned
* as winners.
*/
public void endGame (
Client client, int[] winners, InvocationListener listener);
/**
* Request to send a private message to one other player in
* the game.
*/
public void sendMessage (
Client client, String msgName, Object value, int playerIdx,
InvocationListener listener);
/**
* Add to the specified named collection.
*
* @param clearExisting if true, wipe the old contents.
*/
public void addToCollection (
Client client, String collName, byte[][] data, boolean clearExisting,
InvocationListener listener);
/**
* Merge the specified collection into the other.
*/
public void mergeCollection (
Client client, String srcColl, String intoColl,
InvocationListener listener);
/**
* Pick or deal some number of elements from the specified collection,
* and either set a property in the flash object, or delivery the
* picks to the specified player index via a game message.
*/
public void getFromCollection (
Client client, String collName, boolean consume, int count,
String msgOrPropName, int playerIndex, ConfirmListener listener);
}
@@ -0,0 +1,68 @@
//
// $Id$
package com.threerings.ezgame.data;
import com.threerings.util.MessageBundle;
import com.threerings.parlor.game.client.GameConfigurator;
import com.threerings.parlor.game.data.GameConfig;
import com.threerings.parlor.game.data.PartyGameConfig;
/**
* A game config for a simple multiplayer game.
*/
public class EZGameConfig extends GameConfig
implements PartyGameConfig
{
/** A creator-submitted name of the game. */
public String gameName;
// from abstract GameConfig
public String getBundleName ()
{
return "general";
}
// from abstract GameConfig
public GameConfigurator createConfigurator ()
{
return null; // nothing here on the java side
}
@Override
public String getGameName ()
{
return MessageBundle.taint(gameName);
}
// from abstract PlaceConfig
public String getManagerClassName ()
{
return "com.threerings.ezgame.server.EZGameManager";
}
// from PartyGameConfig
public byte getPartyGameType ()
{
// TODO
return NOT_PARTY_GAME;
}
@Override
public boolean equals (Object other)
{
if (!super.equals(other)) {
return false;
}
EZGameConfig that = (EZGameConfig) other;
return this.gameName.equals(that.gameName);
}
@Override
public int hashCode ()
{
return super.hashCode(); // TODO: incorp game name?
}
}
@@ -0,0 +1,113 @@
//
// $Id$
package com.threerings.ezgame.data;
import com.threerings.ezgame.client.EZGameService;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.dobj.InvocationResponseEvent;
/**
* Provides the implementation of the {@link EZGameService} interface
* that marshalls the arguments and delivers the request to the provider
* on the server. Also provides an implementation of the response listener
* interfaces that marshall the response arguments and deliver them back
* to the requesting client.
*/
public class EZGameMarshaller extends InvocationMarshaller
implements EZGameService
{
/** The method id used to dispatch {@link #addToCollection} requests. */
public static final int ADD_TO_COLLECTION = 1;
// documentation inherited from interface
public void addToCollection (Client arg1, String arg2, byte[][] arg3, boolean arg4, InvocationService.InvocationListener arg5)
{
ListenerMarshaller listener5 = new ListenerMarshaller();
listener5.listener = arg5;
sendRequest(arg1, ADD_TO_COLLECTION, new Object[] {
arg2, arg3, Boolean.valueOf(arg4), listener5
});
}
/** The method id used to dispatch {@link #endGame} requests. */
public static final int END_GAME = 2;
// documentation inherited from interface
public void endGame (Client arg1, int[] arg2, InvocationService.InvocationListener arg3)
{
ListenerMarshaller listener3 = new ListenerMarshaller();
listener3.listener = arg3;
sendRequest(arg1, END_GAME, new Object[] {
arg2, listener3
});
}
/** The method id used to dispatch {@link #endTurn} requests. */
public static final int END_TURN = 3;
// documentation inherited from interface
public void endTurn (Client arg1, int arg2, InvocationService.InvocationListener arg3)
{
ListenerMarshaller listener3 = new ListenerMarshaller();
listener3.listener = arg3;
sendRequest(arg1, END_TURN, new Object[] {
Integer.valueOf(arg2), listener3
});
}
/** The method id used to dispatch {@link #getFromCollection} requests. */
public static final int GET_FROM_COLLECTION = 4;
// documentation inherited from interface
public void getFromCollection (Client arg1, String arg2, boolean arg3, int arg4, String arg5, int arg6, InvocationService.ConfirmListener arg7)
{
InvocationMarshaller.ConfirmMarshaller listener7 = new InvocationMarshaller.ConfirmMarshaller();
listener7.listener = arg7;
sendRequest(arg1, GET_FROM_COLLECTION, new Object[] {
arg2, Boolean.valueOf(arg3), Integer.valueOf(arg4), arg5, Integer.valueOf(arg6), listener7
});
}
/** The method id used to dispatch {@link #mergeCollection} requests. */
public static final int MERGE_COLLECTION = 5;
// documentation inherited from interface
public void mergeCollection (Client arg1, String arg2, String arg3, InvocationService.InvocationListener arg4)
{
ListenerMarshaller listener4 = new ListenerMarshaller();
listener4.listener = arg4;
sendRequest(arg1, MERGE_COLLECTION, new Object[] {
arg2, arg3, listener4
});
}
/** The method id used to dispatch {@link #sendMessage} requests. */
public static final int SEND_MESSAGE = 6;
// documentation inherited from interface
public void sendMessage (Client arg1, String arg2, Object arg3, int arg4, InvocationService.InvocationListener arg5)
{
ListenerMarshaller listener5 = new ListenerMarshaller();
listener5.listener = arg5;
sendRequest(arg1, SEND_MESSAGE, new Object[] {
arg2, arg3, Integer.valueOf(arg4), listener5
});
}
/** The method id used to dispatch {@link #setProperty} requests. */
public static final int SET_PROPERTY = 7;
// documentation inherited from interface
public void setProperty (Client arg1, String arg2, Object arg3, int arg4, InvocationService.InvocationListener arg5)
{
ListenerMarshaller listener5 = new ListenerMarshaller();
listener5.listener = arg5;
sendRequest(arg1, SET_PROPERTY, new Object[] {
arg2, arg3, Integer.valueOf(arg4), listener5
});
}
}
@@ -0,0 +1,162 @@
//
// $Id$
package com.threerings.ezgame.data;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import com.threerings.util.Name;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamable;
import com.threerings.parlor.game.data.GameObject;
import com.threerings.parlor.turn.data.TurnGameObject;
/**
* Contains the data for an ez game.
*/
public class EZGameObject extends GameObject
implements TurnGameObject
{
/** The identifier for a MessageEvent containing a user message. */
public static final String USER_MESSAGE = "Umsg";
/** The identifier for a MessageEvent containing game-system chat. */
public static final String GAME_CHAT = "Uchat";
// AUTO-GENERATED: FIELDS START
/** The field name of the <code>turnHolder</code> field. */
public static final String TURN_HOLDER = "turnHolder";
/** The field name of the <code>ezGameService</code> field. */
public static final String EZ_GAME_SERVICE = "ezGameService";
// AUTO-GENERATED: FIELDS END
/** The current turn holder. */
public Name turnHolder;
/** The service interface for requesting special things from the server. */
public EZGameMarshaller ezGameService;
// from TurnGameObject
public String getTurnHolderFieldName ()
{
return TURN_HOLDER;
}
// from TurnGameObject
public Name getTurnHolder ()
{
return turnHolder;
}
// from TurnGameObject
public Name[] getPlayers ()
{
return players;
}
/**
* Called by PropertySetEvent to effect the property update.
*/
protected void applyPropertySet (String propName, Object data, int index)
{
if (index >= 0) {
Object something = _props.get(propName);
byte[][] arr = (something instanceof byte[][])
? (byte[][]) something : null;
if (arr == null || arr.length <= index) {
// TODO: in case a user sets element 0 and element 90000,
// we might want to store elements in a hash
byte[][] newArr = new byte[index + 1][];
if (arr != null) {
System.arraycopy(arr, 0, newArr, 0, arr.length);
}
_props.put(propName, newArr);
arr = newArr;
}
arr[index] = (byte[]) data;
} else if (data != null) {
_props.put(propName, data);
} else {
_props.remove(propName);
}
}
// 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 void setTurnHolder (Name value)
{
Name ovalue = this.turnHolder;
requestAttributeChange(
TURN_HOLDER, value, ovalue);
this.turnHolder = value;
}
/**
* Requests that the <code>ezGameService</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 setEZGameService (EZGameMarshaller value)
{
EZGameMarshaller ovalue = this.ezGameService;
requestAttributeChange(
EZ_GAME_SERVICE, value, ovalue);
this.ezGameService = value;
}
// AUTO-GENERATED: METHODS END
/**
* A custom serialization method.
*/
public void writeObject (ObjectOutputStream out)
throws IOException
{
out.defaultWriteObject();
// write the number of properties, followed by each one
out.writeInt(_props.size());
for (Map.Entry<String, Object> entry : _props.entrySet()) {
out.writeUTF(entry.getKey());
out.writeObject(entry.getValue());
}
}
/**
* A custom serialization method.
*/
public void readObject (ObjectInputStream ins)
throws IOException, ClassNotFoundException
{
ins.defaultReadObject();
_props.clear();
int count = ins.readInt();
while (count-- > 0) {
String key = ins.readUTF();
_props.put(key, ins.readObject());
}
}
/** The current state of game data, opaque to us here on the server. */
protected transient HashMap<String, Object> _props =
new HashMap<String, Object>();
}
@@ -0,0 +1,50 @@
//
// $Id$
package com.threerings.ezgame.data;
import java.io.IOException;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamable;
import com.threerings.io.Streamer;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.NamedEvent;
/**
* Represents a property change on the actionscript object we
* use in EZGameObject.
*/
public class PropertySetEvent extends NamedEvent
{
/** Suitable for unserialization. */
public PropertySetEvent ()
{
}
/**
* Create a PropertySetEvent.
*/
public PropertySetEvent (
int targetOid, String propName, Object value, int index)
{
super(targetOid, propName);
_data = value;
_index = index;
}
// from abstract DEvent
public boolean applyToObject (DObject target)
{
((EZGameObject) target).applyPropertySet(_name, _data, _index);
return true;
}
/** The index. */
protected int _index;
/** The client-side data that is assigned to this property. */
protected Object _data;
}
@@ -0,0 +1,95 @@
//
// $Id$
package com.threerings.ezgame.server;
import com.threerings.ezgame.client.EZGameService;
import com.threerings.ezgame.data.EZGameMarshaller;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.data.InvocationMarshaller;
import com.threerings.presents.server.InvocationDispatcher;
import com.threerings.presents.server.InvocationException;
/**
* Dispatches requests to the {@link EZGameProvider}.
*/
public class EZGameDispatcher extends InvocationDispatcher
{
/**
* Creates a dispatcher that may be registered to dispatch invocation
* service requests for the specified provider.
*/
public EZGameDispatcher (EZGameProvider provider)
{
this.provider = provider;
}
// documentation inherited
public InvocationMarshaller createMarshaller ()
{
return new EZGameMarshaller();
}
// documentation inherited
public void dispatchRequest (
ClientObject source, int methodId, Object[] args)
throws InvocationException
{
switch (methodId) {
case EZGameMarshaller.ADD_TO_COLLECTION:
((EZGameProvider)provider).addToCollection(
source,
(String)args[0], (byte[][])args[1], ((Boolean)args[2]).booleanValue(), (InvocationService.InvocationListener)args[3]
);
return;
case EZGameMarshaller.END_GAME:
((EZGameProvider)provider).endGame(
source,
(int[])args[0], (InvocationService.InvocationListener)args[1]
);
return;
case EZGameMarshaller.END_TURN:
((EZGameProvider)provider).endTurn(
source,
((Integer)args[0]).intValue(), (InvocationService.InvocationListener)args[1]
);
return;
case EZGameMarshaller.GET_FROM_COLLECTION:
((EZGameProvider)provider).getFromCollection(
source,
(String)args[0], ((Boolean)args[1]).booleanValue(), ((Integer)args[2]).intValue(), (String)args[3], ((Integer)args[4]).intValue(), (InvocationService.ConfirmListener)args[5]
);
return;
case EZGameMarshaller.MERGE_COLLECTION:
((EZGameProvider)provider).mergeCollection(
source,
(String)args[0], (String)args[1], (InvocationService.InvocationListener)args[2]
);
return;
case EZGameMarshaller.SEND_MESSAGE:
((EZGameProvider)provider).sendMessage(
source,
(String)args[0], (Object)args[1], ((Integer)args[2]).intValue(), (InvocationService.InvocationListener)args[3]
);
return;
case EZGameMarshaller.SET_PROPERTY:
((EZGameProvider)provider).setProperty(
source,
(String)args[0], (Object)args[1], ((Integer)args[2]).intValue(), (InvocationService.InvocationListener)args[3]
);
return;
default:
super.dispatchRequest(source, methodId, args);
return;
}
}
}
@@ -0,0 +1,304 @@
//
// $Id$
package com.threerings.ezgame.server;
import java.util.ArrayList;
import java.util.HashMap;
import com.samskivert.util.ArrayUtil;
import com.samskivert.util.CollectionUtil;
import com.samskivert.util.RandomUtil;
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.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.server.GameManager;
import com.threerings.parlor.turn.server.TurnGameManager;
import com.threerings.ezgame.data.EZGameObject;
import com.threerings.ezgame.data.EZGameMarshaller;
import com.threerings.ezgame.data.PropertySetEvent;
/**
* 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 nextPlayerIndex,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateStateModification(caller);
_turnDelegate.endTurn(nextPlayerIndex);
}
// from EZGameProvider
public void endGame (
ClientObject caller, int[] winners,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateStateModification(caller);
_winnerIndexes = winners;
endGame();
}
// from EZGameProvider
public void sendMessage (
ClientObject caller, String msg, Object data, int playerIdx,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
if (playerIdx < 0 || playerIdx >= _gameObj.players.length) {
_gameObj.postMessage(EZGameObject.USER_MESSAGE,
new Object[] { msg, data });
} else {
sendPrivateMessage(playerIdx, msg, data);
}
}
// from EZGameProvider
public void setProperty (
ClientObject caller, String propName, Object data, int index,
InvocationService.InvocationListener listener)
throws InvocationException
{
validateUser(caller);
setProperty(propName, data, index);
}
// 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 playerIndex,
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 (playerIndex >= 0 && playerIndex < _gameObj.players.length) {
sendPrivateMessage(playerIndex, msgOrPropName, result);
} else {
setProperty(msgOrPropName, result, -1);
}
// 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);
}
}
}
}
/**
* Helper method to send a private message to the specified player
* index (must already be verified).
*/
protected void sendPrivateMessage (
int playerIdx, String msg, Object data)
throws InvocationException
{
BodyObject target = getPlayer(playerIdx);
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)
{
_gameObj.postEvent(
new PropertySetEvent(_gameObj.getOid(), propName, value, index));
}
/**
* Validate that the specified user has access to do things in the game.
*/
protected void validateUser (ClientObject caller)
throws InvocationException
{
if (getPresentPlayerIndex(caller.getOid()) == -1) {
throw new InvocationException(InvocationCodes.ACCESS_DENIED);
}
}
/**
* 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);
}
}
@Override
protected Class<? extends PlaceObject> getPlaceObjectClass ()
{
return EZGameObject.class;
}
@Override
protected void didStartup ()
{
super.didStartup();
_gameObj = (EZGameObject) _plobj;
_gameObj.setEZGameService(
(EZGameMarshaller) CrowdServer.invmgr.registerDispatcher(
new EZGameDispatcher(this), false));
}
@Override
protected void didShutdown ()
{
CrowdServer.invmgr.clearDispatcher(_gameObj.ezGameService);
super.didShutdown();
}
@Override
protected void assignWinners (boolean[] winners)
{
if (_winnerIndexes != null) {
for (int index : _winnerIndexes) {
if (index >= 0 && index < winners.length) {
winners[index] = true;
}
}
_winnerIndexes = null;
}
}
/** A nice casted reference to the game object. */
protected EZGameObject _gameObj;
/** Our turn delegate. */
protected EZGameTurnDelegate _turnDelegate;
/** The array of winners, after the user has filled it in. */
protected int[] _winnerIndexes;
/** The map of collections, lazy-initialized. */
protected HashMap<String, ArrayList<byte[]>> _collections;
}
@@ -0,0 +1,59 @@
//
// $Id$
package com.threerings.ezgame.server;
import com.threerings.ezgame.client.EZGameService;
import com.threerings.presents.client.Client;
import com.threerings.presents.client.InvocationService;
import com.threerings.presents.data.ClientObject;
import com.threerings.presents.server.InvocationException;
import com.threerings.presents.server.InvocationProvider;
/**
* Defines the server-side of the {@link EZGameService}.
*/
public interface EZGameProvider extends InvocationProvider
{
/**
* Handles a {@link EZGameService#addToCollection} request.
*/
public void addToCollection (ClientObject caller, String arg1, byte[][] arg2, boolean arg3, InvocationService.InvocationListener arg4)
throws InvocationException;
/**
* Handles a {@link EZGameService#endGame} request.
*/
public void endGame (ClientObject caller, int[] arg1, InvocationService.InvocationListener arg2)
throws InvocationException;
/**
* Handles a {@link EZGameService#endTurn} request.
*/
public void endTurn (ClientObject caller, int arg1, InvocationService.InvocationListener arg2)
throws InvocationException;
/**
* Handles a {@link EZGameService#getFromCollection} request.
*/
public void getFromCollection (ClientObject caller, String arg1, boolean arg2, int arg3, String arg4, int arg5, InvocationService.ConfirmListener arg6)
throws InvocationException;
/**
* Handles a {@link EZGameService#mergeCollection} request.
*/
public void mergeCollection (ClientObject caller, String arg1, String arg2, InvocationService.InvocationListener arg3)
throws InvocationException;
/**
* Handles a {@link EZGameService#sendMessage} request.
*/
public void sendMessage (ClientObject caller, String arg1, Object arg2, int arg3, InvocationService.InvocationListener arg4)
throws InvocationException;
/**
* Handles a {@link EZGameService#setProperty} request.
*/
public void setProperty (ClientObject caller, String arg1, Object arg2, int arg3, InvocationService.InvocationListener arg4)
throws InvocationException;
}
@@ -0,0 +1,46 @@
//
// $Id$
package com.threerings.ezgame.server;
import com.threerings.parlor.turn.server.TurnGameManagerDelegate;
/**
* A special turn delegate for ez games.
*/
public class EZGameTurnDelegate extends TurnGameManagerDelegate
{
public EZGameTurnDelegate (EZGameManager mgr)
{
super(mgr);
}
/**
* A form of endTurn where you can specify the next turn holder index.
*/
public void endTurn (int playerIndex)
{
_nextPlayerIndex = playerIndex;
endTurn();
}
@Override
protected void setNextTurnHolder ()
{
// if the user-supplied value seems to make sense, use it!
if ((_nextPlayerIndex >= 0) &&
(_nextPlayerIndex < _turnGame.getPlayers().length)) {
_turnIdx = _nextPlayerIndex;
} else {
// otherwise, do the default behavior
super.setNextTurnHolder();
}
// always clear out the override
_nextPlayerIndex = -1;
}
/** An override next turn holder, or -1. */
protected int _nextPlayerIndex = -1;
}