diff --git a/src/java/com/threerings/ezgame/CardDeck.java b/src/java/com/threerings/ezgame/CardDeck.java new file mode 100644 index 00000000..c6ca0689 --- /dev/null +++ b/src/java/com/threerings/ezgame/CardDeck.java @@ -0,0 +1,48 @@ +package com.threerings.ezgame; + +import java.util.ArrayList; + +/** + * A simple card deck that encodes cards as a string like "Ac" for the + * ace of clubs, or "Td" for the 10 of diamonds. + */ +public class CardDeck +{ + public CardDeck (EZGame gameObj) + { + this(gameObj, "deck"); + } + + public CardDeck (EZGame gameObj, String deckName) + { + _gameObj = gameObj; + _deckName = deckName; + + ArrayList deck = new ArrayList(); + for (String rank : new String[] { "2", "3", "4", "5", "6", "7", "8", + "9", "T", "J", "Q", "K", "A" }) { + for (String suit : new String[] { "c", "d", "h", "s" }) { + deck.add(rank + suit); + } + } + + _gameObj.setCollection(_deckName, deck); + } + + public void dealToPlayer (int playerIdx, int count, String msgName) + { + // TODO: support the callback + _gameObj.dealFromCollection(_deckName, count, msgName, null, playerIdx); + } + + public void dealToData (int count, String propName) + { + _gameObj.dealFromCollection(_deckName, count, propName, null); + } + + /** The game object. */ + protected EZGame _gameObj; + + /** The name of our deck. */ + protected String _deckName; +} diff --git a/src/java/com/threerings/ezgame/DealListener.java b/src/java/com/threerings/ezgame/DealListener.java new file mode 100644 index 00000000..34ce0ef6 --- /dev/null +++ b/src/java/com/threerings/ezgame/DealListener.java @@ -0,0 +1,7 @@ +package com.threerings.ezgame; + +public interface DealListener +{ + // TODO: figure out what I want to return here + public void dealt (int someValue); +} diff --git a/src/java/com/threerings/ezgame/EZEvent.java b/src/java/com/threerings/ezgame/EZEvent.java new file mode 100644 index 00000000..91df302c --- /dev/null +++ b/src/java/com/threerings/ezgame/EZEvent.java @@ -0,0 +1,20 @@ +package com.threerings.ezgame; + +public class EZEvent +{ + public EZEvent (EZGame ezgame) + { + _ezgame = ezgame; + } + + /** + * Access the game object to which this event applies. + */ + public EZGame getGameObject () + { + return _ezgame; + } + + /** The game object for this event. */ + protected EZGame _ezgame; +} diff --git a/src/java/com/threerings/ezgame/EZGame.java b/src/java/com/threerings/ezgame/EZGame.java new file mode 100644 index 00000000..a55fa784 --- /dev/null +++ b/src/java/com/threerings/ezgame/EZGame.java @@ -0,0 +1,168 @@ +package com.threerings.ezgame; + +import java.util.Collection; + +/** + * The game object that you'll be using to manage your game. + */ +public interface EZGame +{ + /** + * Get a property from data. + */ + public Object get (String propName); + + /** + * Get a property from data. + */ + public Object get (String propName, int index); + + /** + * Set a property that will be distributed. + */ + public void set (String propName, Object value); + + /** + * Set a property that will be distributed. + */ + public void set (String propName, Object value, int index); + + /** + * Register an object to receive whatever events it should receive, + * based on which event listeners it implements. Note that it is not + * necessary to register any objects which appear on the display list, + * as they'll be registered automatically. + */ + public void registerListener (Object obj); + + /** + * Unregister the specified object from receiving events. + */ + public void unregisterListener (Object obj); + + /** + * Set the specified collection to contain the specified values, + * clearing any previous values. + * + * @param values may be a Collection or array. Note that arrays + * of primitive types will be transmogrified into an Object[] containing + * wrapper objects. + */ + public void setCollection (String collName, Object values); + + /** + * Add to an existing collection. If it doesn't exist, it will + * be created. The new values will be inserted randomly into the + * collection. + * + * @param values may be a Collection or array. Note that arrays + * of primitive types will be transmogrified into an Object[] containing + * wrapper objects. + */ + public void addToCollection (String collName, Object values); + + /** + * Pick (do not remove) the specified number of elements from a collection, + * and distribute them to a specific player or set them as a property + * in the game data. + */ + // TODO: a way to specify exclusive picks vs. duplicate-OK picks? + public void pickFromCollection ( + String collName, int count, String propName); + + public void pickFromCollection ( + String collName, int count, String msgName, int playerIndex); + + /** + * Deal (remove) the specified number of elements from a collection, + * and distribute them to a specific player or set them as a property + * in the game data. + */ + // TODO: figure out the method signature of the callback + public void dealFromCollection ( + String collName, int count, String propName, + DealListener listener); + + public void dealFromCollection ( + String collName, int count, String msgName, + DealListener listener, int playerIndex); + + /** + * Merge the specified collection into the other collection. + * The source collection will be destroyed. The elements from + * The source collection will be shuffled and appended to the end + * of the destination collection. + */ + public void mergeCollection (String srcColl, String intoColl); + + /** + * 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 playerIndex if -1, sends to all players, otherwise + * the message will be private to just one player + */ + public void sendMessage ( + String messageName, Object value); + + public void sendMessage ( + String messageName, Object value, int playerIndex); + + /** + * Send a message that will be heard by everyone in the game room, + * even observers. + */ + public void sendChat (String msg); + + /** + * Display the specified message immediately locally: not sent + * to any other players or observers in the game room. + */ + public void localChat (String msg); + + /** + * Get the player names, as an array. + */ + public String[] getPlayerNames (); + + /** + * Get the index into the player names array of the current player, + * or -1 if the user is not a player. + */ + public int getMyIndex (); + + /** + * Get the turn holder's index, or -1 if it's nobody's turn. + */ + public int getTurnHolderIndex (); + + /** + * Get the indexes of the winners + */ + public int[] getWinnerIndexes (); + + /** + * A convenience method to just check if it's our turn. + */ + public boolean isMyTurn (); + + /** + * Is the game currently in play? + */ + public boolean isInPlay (); + + /** + * End the current turn. If no next player index is specified, + * then the next player after the current one is used. + */ + public void endTurn (); + + public void endTurn (int nextPlayerIndex); + + /** + * End the game. The specified player indexes are winners! + */ + public void endGame (int... winners); +} diff --git a/src/java/com/threerings/ezgame/Game.java b/src/java/com/threerings/ezgame/Game.java new file mode 100644 index 00000000..a9039cb2 --- /dev/null +++ b/src/java/com/threerings/ezgame/Game.java @@ -0,0 +1,15 @@ +package com.threerings.ezgame; + +/** + * This interface should be implemented by some class that lives on the + * display list. When a DisplayObject that implements this interface + * is added, it will be notified of the GameObject. + */ +public interface Game +{ + /** + * Called by the mysterious powers of the cosmos to initialize + * your game with the GameObject. + */ + public void setGameObject (EZGame gameObj); +} diff --git a/src/java/com/threerings/ezgame/MessageReceivedEvent.java b/src/java/com/threerings/ezgame/MessageReceivedEvent.java new file mode 100644 index 00000000..089576b0 --- /dev/null +++ b/src/java/com/threerings/ezgame/MessageReceivedEvent.java @@ -0,0 +1,37 @@ +package com.threerings.ezgame; + +public class MessageReceivedEvent extends EZEvent +{ + public MessageReceivedEvent ( + EZGame ezgame, String messageName, Object value) + { + super(ezgame); + _name = messageName; + _value = value; + } + + /** + * Access the message name. + */ + public String getName () + { + return _name; + } + + /** + * Access the message value. + */ + public Object getValue () + { + return _value; + } + + public String toString () + { + return "[MessageReceivedEvent name=" + _name + + ", value=" + _value + "]"; + } + + protected String _name; + protected Object _value; +} diff --git a/src/java/com/threerings/ezgame/MessageReceivedListener.java b/src/java/com/threerings/ezgame/MessageReceivedListener.java new file mode 100644 index 00000000..930a36e1 --- /dev/null +++ b/src/java/com/threerings/ezgame/MessageReceivedListener.java @@ -0,0 +1,13 @@ +package com.threerings.ezgame; + +/** + * Implement this interface to automagically be registered to received + * MessageReceivedEvents. + */ +public interface MessageReceivedListener +{ + /** + * Handle a MessageReceivedEvent. + */ + public void messageReceived (MessageReceivedEvent event); +} diff --git a/src/java/com/threerings/ezgame/PropertyChangedEvent.java b/src/java/com/threerings/ezgame/PropertyChangedEvent.java new file mode 100644 index 00000000..15d9e149 --- /dev/null +++ b/src/java/com/threerings/ezgame/PropertyChangedEvent.java @@ -0,0 +1,67 @@ +package com.threerings.ezgame; + +/** + * Property change events are dispatched after the property change was + * validated on the server. + */ +public class PropertyChangedEvent extends EZEvent +{ + /** + * Constructor. + */ + public PropertyChangedEvent ( + EZGame ezgame, String propName, Object newValue, Object oldValue, + int index) + { + super(ezgame); + _name = propName; + _newValue = newValue; + _oldValue = oldValue; + _index = index; + } + + /** + * Get the name of the property that changed. + */ + public String getName () + { + return _name; + } + + /** + * Get the property's new value. + */ + public Object getNewValue () + { + return _newValue; + } + + /** + * Get the property's previous value (handy!). + */ + public Object getOldValue () + { + return _oldValue; + } + + /** + * If an array element was updated, get the index, or -1 if not applicable. + */ + public int getIndex () + { + return _index; + } + + @Override + public String toString () + { + return "[PropertyChangedEvent name=" + _name + ", value=" + _newValue + + ((_index < 0) ? "" : (", index=" + _index)) + "]"; + } + + /** Our implementation details. */ + protected String _name; + protected Object _newValue; + protected Object _oldValue; + protected int _index; +} diff --git a/src/java/com/threerings/ezgame/PropertyChangedListener.java b/src/java/com/threerings/ezgame/PropertyChangedListener.java new file mode 100644 index 00000000..d6f63417 --- /dev/null +++ b/src/java/com/threerings/ezgame/PropertyChangedListener.java @@ -0,0 +1,13 @@ +package com.threerings.ezgame; + +/** + * Implement this interface to automagically be registered to received + * PropertyChangedEvents. + */ +public interface PropertyChangedListener +{ + /** + * Handle a PropertyChangedEvent. + */ + public void propertyChanged (PropertyChangedEvent event); +} diff --git a/src/java/com/threerings/ezgame/StateChangedEvent.java b/src/java/com/threerings/ezgame/StateChangedEvent.java new file mode 100644 index 00000000..63da22e9 --- /dev/null +++ b/src/java/com/threerings/ezgame/StateChangedEvent.java @@ -0,0 +1,35 @@ +package com.threerings.ezgame; + +/** + * Dispatched when the state of the game has changed. + */ +public class StateChangedEvent extends EZEvent +{ + /** Indicates that the game has transitioned to a started state. */ + public static final String GAME_STARTED = "GameStarted"; + + /** Indicates that the game has transitioned to a ended state. */ + public static final String GAME_ENDED = "GameEnded"; + + /** Indicates that the turn has changed. */ + // TODO: move to own event? + public static final String TURN_CHANGED = "TurnChanged"; + + public StateChangedEvent (EZGame ezgame, String type) + { + super(ezgame); + _type = type; + } + + public String getType () + { + return _type; + } + + public String toString () + { + return "[StateChangedEvent type=" + _type + "]"; + } + + protected String _type; +} diff --git a/src/java/com/threerings/ezgame/StateChangedListener.java b/src/java/com/threerings/ezgame/StateChangedListener.java new file mode 100644 index 00000000..597f659c --- /dev/null +++ b/src/java/com/threerings/ezgame/StateChangedListener.java @@ -0,0 +1,13 @@ +package com.threerings.ezgame; + +/** + * Implement this interface to automagically be registered to receive + * StateChangedEvents. + */ +public interface StateChangedListener +{ + /** + * Handle a StateChangedEvent. + */ + public void stateChanged (StateChangedEvent event); +} diff --git a/src/java/com/threerings/ezgame/client/EZGameController.java b/src/java/com/threerings/ezgame/client/EZGameController.java new file mode 100644 index 00000000..3c058949 --- /dev/null +++ b/src/java/com/threerings/ezgame/client/EZGameController.java @@ -0,0 +1,146 @@ +package com.threerings.ezgame.client; + +import com.threerings.util.Name; + +import com.threerings.presents.dobj.MessageListener; +import com.threerings.presents.dobj.MessageEvent; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.game.client.GameController; + +import com.threerings.parlor.turn.client.TurnGameController; +import com.threerings.parlor.turn.client.TurnGameControllerDelegate; + +import com.threerings.ezgame.data.EZGameObject; +import com.threerings.ezgame.data.PropertySetEvent; +import com.threerings.ezgame.data.PropertySetListener; +import com.threerings.ezgame.util.EZObjectMarshaller; + +import com.threerings.ezgame.EZEvent; +import com.threerings.ezgame.MessageReceivedEvent; +import com.threerings.ezgame.PropertyChangedEvent; +import com.threerings.ezgame.StateChangedEvent; + +/** + * A controller for flash games. + */ +public class EZGameController extends GameController + implements TurnGameController, PropertySetListener, MessageListener +{ + /** The implementation of the GameObject interface for users. */ + public GameObjectImpl gameObjImpl; + + /** + */ + public EZGameController () + { + addDelegate(_turnDelegate = new TurnGameControllerDelegate(this)); + } + + @Override + public void willEnterPlace (PlaceObject plobj) + { + _ezObj = (EZGameObject) plobj; + gameObjImpl = new GameObjectImpl(_ctx, _ezObj); + + _ctx.getClient().getClientObject().addListener(_userListener); + + super.willEnterPlace(plobj); + } + + @Override + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + + _ctx.getClient().getClientObject().removeListener(_userListener); + + _ezObj = null; + } + + // from TurnGameController + public void turnDidChange (Name turnHolder) + { + dispatchUserEvent( + new StateChangedEvent(gameObjImpl, StateChangedEvent.TURN_CHANGED)); + } + + // from PropertySetListener + public void propertyWasSet (PropertySetEvent event) + { + // notify the user game + dispatchUserEvent(new PropertyChangedEvent( + gameObjImpl, event.getName(), event.getValue(), + event.getOldValue(), event.getIndex())); + } + + // from MessageListener + public void messageReceived (MessageEvent event) + { + String name = event.getName(); + if (EZGameObject.USER_MESSAGE.equals(name)) { + dispatchUserMessage(event.getArgs()); + + } else if (EZGameObject.GAME_CHAT.equals(name)) { + // this is chat send by the game, let's route it like + // localChat, which is also sent by the game + gameObjImpl.localChat((String) event.getArgs()[0]); + } + } + + /** + * Dispatch the user message. + */ + protected void dispatchUserMessage (Object[] args) + { + dispatchUserEvent(new MessageReceivedEvent( + gameObjImpl, (String) args[0], + EZObjectMarshaller.decode(args[1]))); + } + + @Override + protected PlaceView createPlaceView (CrowdContext ctx) + { + return new EZGamePanel(ctx, this); + } + + @Override + protected void gameDidStart () + { + super.gameDidStart(); + dispatchUserEvent( + new StateChangedEvent(gameObjImpl, StateChangedEvent.GAME_STARTED)); + } + + @Override + protected void gameDidEnd () + { + super.gameDidEnd(); + dispatchUserEvent( + new StateChangedEvent(gameObjImpl, StateChangedEvent.GAME_ENDED)); + } + + protected void dispatchUserEvent (EZEvent event) + { + gameObjImpl.dispatch(event); + } + + protected EZGameObject _ezObj; + + protected TurnGameControllerDelegate _turnDelegate; + + /** Listens for message events on the user object. */ + protected MessageListener _userListener = new MessageListener() { + public void messageReceived (MessageEvent event) { + // see if it's a message about user games + String msgName = EZGameObject.USER_MESSAGE + ":" + _ezObj.getOid(); + if (msgName.equals(event.getName())) { + dispatchUserMessage(event.getArgs()); + } + } + }; +} diff --git a/src/java/com/threerings/ezgame/client/EZGamePanel.java b/src/java/com/threerings/ezgame/client/EZGamePanel.java new file mode 100644 index 00000000..d4852bca --- /dev/null +++ b/src/java/com/threerings/ezgame/client/EZGamePanel.java @@ -0,0 +1,134 @@ +package com.threerings.ezgame.client; + +import java.awt.Component; +import java.awt.Container; + +import java.awt.event.ContainerEvent; +import java.awt.event.ContainerListener; + +import java.util.WeakHashMap; + +import javax.swing.JPanel; + +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.ezgame.data.EZGameConfig; +import com.threerings.ezgame.data.EZGameObject; + +import com.threerings.ezgame.Game; + +public class EZGamePanel extends JPanel + implements ContainerListener, PlaceView +{ + public EZGamePanel (CrowdContext ctx, EZGameController ctrl) + { + super(new VGroupLayout(VGroupLayout.STRETCH)); + + _ctx = ctx; + _ctrl = ctrl; + + // add a listener so that we hear about all new children + addContainerListener(this); + + EZGameConfig cfg = (EZGameConfig) ctrl.getPlaceConfig(); + try { + // TODO + _gameView = (Component) Class.forName(cfg.configData).newInstance(); + add(_gameView); + } catch (RuntimeException re) { + throw re; + + } catch (Exception e) { + throw new RuntimeException(e); + } + + // TODO: Add a standard chat display? + //addChild(new ChatDisplayBox(ctx)); + } + + // from PlaceView + public void willEnterPlace (final PlaceObject plobj) + { + // don't start notifying anything of the game until we've + // notified the game manager that we're in the game + // (done in GameController, and it uses callLater, so we do it twice!) + _ctx.getClient().getRunQueue().postRunnable(new Runnable() { + public void run () { + _ctx.getClient().getRunQueue().postRunnable(new Runnable() { + public void run () { + _ezObj = (EZGameObject) plobj; + notifyOfGame(_gameView); + } + }); + } + }); + } + + // from PlaceView + public void didLeavePlace (PlaceObject plobj) + { + removeListeners(_gameView); + _ezObj = null; + } + + // from ContainerListener + public void componentAdded (ContainerEvent event) + { + if (_ezObj != null) { + notifyOfGame(event.getChild()); + } + } + + // from ContainerListener + public void componentRemoved (ContainerEvent event) + { + if (_ezObj != null) { + removeListeners(event.getChild()); + } + } + + /** + * Find any children of the specified object that implement + * com.metasoy.game.Game and provide them with the GameObject. + */ + protected void notifyOfGame (Component root) + { + SwingUtil.applyToHierarchy(root, new SwingUtil.ComponentOp() { + public void apply (Component comp) { + if (comp instanceof Game) { + // only notify the Game if we haven't seen it before + if (null == _seenGames.put(comp, Boolean.TRUE)) { + ((Game) comp).setGameObject(_ctrl.gameObjImpl); + } + } + // always check to see if it's a listener + _ctrl.gameObjImpl.registerListener(comp); + } + }); + } + + protected void removeListeners (Component root) + { + SwingUtil.applyToHierarchy(root, new SwingUtil.ComponentOp() { + public void apply (Component comp) { + _ctrl.gameObjImpl.unregisterListener(comp); + } + }); + } + + protected CrowdContext _ctx; + protected EZGameController _ctrl; + + protected Component _gameView; + + /** A weak-key hash of the Game interfaces we've already seen. */ + protected WeakHashMap _seenGames = + new WeakHashMap(); + + protected EZGameObject _ezObj; +} diff --git a/src/java/com/threerings/ezgame/client/EZGameService.java b/src/java/com/threerings/ezgame/client/EZGameService.java index f280b2f4..e23e861f 100644 --- a/src/java/com/threerings/ezgame/client/EZGameService.java +++ b/src/java/com/threerings/ezgame/client/EZGameService.java @@ -13,6 +13,10 @@ public interface EZGameService extends InvocationService { /** * Request to set the specified property. + * + * @param value either a byte[] if setting a non-array property + * or a property at an array index, or a byte[][] if setting + * an array property where index is -1. */ public void setProperty ( Client client, String propName, Object value, int index, @@ -35,6 +39,10 @@ public interface EZGameService extends InvocationService /** * Request to send a private message to one other player in * the game. + * + * @param value either a byte[] if setting a non-array property + * or a property at an array index, or a byte[][] if setting + * an array property where index is -1. */ public void sendMessage ( Client client, String msgName, Object value, int playerIdx, diff --git a/src/java/com/threerings/ezgame/client/GameObjectImpl.java b/src/java/com/threerings/ezgame/client/GameObjectImpl.java new file mode 100644 index 00000000..ac923073 --- /dev/null +++ b/src/java/com/threerings/ezgame/client/GameObjectImpl.java @@ -0,0 +1,473 @@ +package com.threerings.ezgame.client; + +import java.io.Externalizable; +import java.io.Serializable; + +import java.lang.reflect.Array; + +import java.util.ArrayList; +import java.util.HashMap; + +import com.samskivert.util.CompactIntListUtil; +import com.samskivert.util.ObserverList; +import com.samskivert.util.StringUtil; + +import com.threerings.util.MessageBundle; +import com.threerings.util.Name; + +import com.threerings.presents.client.InvocationService; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.Log; // well, fine + +import com.threerings.ezgame.data.EZGameObject; +import com.threerings.ezgame.data.PropertySetEvent; +import com.threerings.ezgame.util.EZObjectMarshaller; + +import com.threerings.ezgame.EZGame; +import com.threerings.ezgame.EZEvent; +import com.threerings.ezgame.DealListener; +import com.threerings.ezgame.MessageReceivedEvent; +import com.threerings.ezgame.MessageReceivedListener; +import com.threerings.ezgame.PropertyChangedEvent; +import com.threerings.ezgame.PropertyChangedListener; +import com.threerings.ezgame.StateChangedEvent; +import com.threerings.ezgame.StateChangedListener; + +public class GameObjectImpl + implements EZGame +{ + public GameObjectImpl (CrowdContext ctx, EZGameObject ezObj) + { + _ctx = ctx; + _ezObj = ezObj; + _props = _ezObj.getUserProps(); + } + + // from EZGame + public Object get (String propName) + { + return _props.get(propName); + } + + // from EZGame + public Object get (String propName, int index) + { + return ((Object[]) get(propName))[index]; + } + + // from EZGame + public void set (String propName, Object value) + { + set(propName, value, -1); + } + + // from EZGame + public void set (String propName, Object value, int index) + { + validatePropertyChange(propName, value, -1); + + Object encoded = EZObjectMarshaller.encode(value); + Object reconstituted = EZObjectMarshaller.decode(encoded); + _ezObj.ezGameService.setProperty( + _ctx.getClient(), propName, encoded, index, + createLoggingListener("setProperty")); + + // set it immediately in the game object + _ezObj.applyPropertySet(propName, reconstituted, index); + } + + // from EZGame + public void registerListener (Object obj) + { + if ((obj instanceof MessageReceivedListener) || + (obj instanceof PropertyChangedListener) || + (obj instanceof StateChangedListener)) { + + // silently ignore requests to listen twice + if (!_listeners.contains(obj)) { + _listeners.add(obj); + } + } + } + + // from EZGame + public void unregisterListener (Object obj) + { + _listeners.remove(obj); + } + + // from EZGame + public void setCollection (String collName, Object values) + { + populateCollection(collName, values, true); + } + + // from EZGame + public void addToCollection (String collName, Object values) + { + populateCollection(collName, values, false); + } + + // from EZGame + public void pickFromCollection ( + String collName, int count, String propName) + { + getFromCollection(collName, count, propName, -1, false, null); + } + + // from EZGame + public void pickFromCollection ( + String collName, int count, String msgName, int playerIndex) + { + getFromCollection(collName, count, msgName, playerIndex, false, null); + } + + // from EZGame + public void dealFromCollection ( + String collName, int count, String propName, + DealListener listener) + { + getFromCollection(collName, count, propName, -1, true, listener); + } + + // from EZGame + public void dealFromCollection ( + String collName, int count, String msgName, + DealListener listener, int playerIndex) + { + getFromCollection(collName, count, msgName, playerIndex, true, listener); + } + + // from EZGame + public void mergeCollection (String srcColl, String intoColl) + { + validateName(srcColl); + validateName(intoColl); + _ezObj.ezGameService.mergeCollection(_ctx.getClient(), + srcColl, intoColl, createLoggingListener("mergeCollection")); + } + + // from EZGame + public void sendMessage (String messageName, Object value) + { + sendMessage(messageName, value, -1); + } + + // from EZGame + public void sendMessage (String messageName, Object value, int playerIndex) + { + validateName(messageName); + validateValue(value); + + Object encoded = EZObjectMarshaller.encode(value); + _ezObj.ezGameService.sendMessage(_ctx.getClient(), + messageName, encoded, playerIndex, + createLoggingListener("sendMessage")); + } + + // from EZGame + public void sendChat (String msg) + { + validateChat(msg); + // Post a message to the game object, the controller + // will listen and call localChat(). + _ezObj.postMessage(EZGameObject.GAME_CHAT, new Object[] { msg }); + } + + // from EZGame + public void localChat (String msg) + { + validateChat(msg); + // The sendChat() messages will end up being routed + // through this method on each client. + // TODO: make this look distinct from other system chat + _ctx.getChatDirector().displayInfo(null, MessageBundle.taint(msg)); + } + + // from EZGame + public String[] getPlayerNames () + { + String[] names = new String[_ezObj.players.length]; + int index = 0; + for (Name name : _ezObj.players) { + names[index++] = (name == null) ? null : name.toString(); + } + return names; + } + + // from EZGame + public int getMyIndex () + { + return _ezObj.getPlayerIndex(getUsername()); + } + + // from EZGame + public int getTurnHolderIndex () + { + return _ezObj.getPlayerIndex(_ezObj.turnHolder); + } + + // from EZGame + public int[] getWinnerIndexes () + { + int[] winners = new int[0]; + if (_ezObj.winners != null) { + for (int ii = 0; ii < _ezObj.winners.length; ii++) { + if (_ezObj.winners[ii]) { + winners = CompactIntListUtil.add(winners, ii); + } + } + } + return winners; + } + + // from EZGame + public boolean isMyTurn () + { + return getUsername().equals(_ezObj.turnHolder); + } + + // from EZGame + public boolean isInPlay () + { + return _ezObj.isInPlay(); + } + + // from EZGame + public void endTurn () + { + endTurn(-1); + } + + // from EZGame + public void endTurn (int nextPlayerIndex) + { + _ezObj.ezGameService.endTurn(_ctx.getClient(), nextPlayerIndex, + createLoggingListener("endTurn")); + } + + // from EZGame + public void endGame (int... winners) + { + _ezObj.ezGameService.endGame(_ctx.getClient(), winners, + createLoggingListener("endGame")); + } + + /** + * Secret function to dispatch property changed events. + */ + void dispatch (EZEvent event) + { + ObserverList.ObserverOp op; + + if (event instanceof PropertyChangedEvent) { + final PropertyChangedEvent pce = (PropertyChangedEvent) event; + op = new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + if (obs instanceof PropertyChangedListener) { + ((PropertyChangedListener) obs).propertyChanged(pce); + } + return true; + } + }; + + } else if (event instanceof StateChangedEvent) { + final StateChangedEvent sce = (StateChangedEvent) event; + op = new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + if (obs instanceof StateChangedListener) { + ((StateChangedListener) obs).stateChanged(sce); + } + return true; + } + }; + + } else if (event instanceof MessageReceivedEvent) { + final MessageReceivedEvent mre = (MessageReceivedEvent) event; + op = new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + if (obs instanceof MessageReceivedListener) { + ((MessageReceivedListener) obs).messageReceived(mre); + } + return true; + } + }; + + } else { + throw new IllegalArgumentException("Please implement"); + } + + // and apply the operation + _listeners.apply(op); + } + + /** + * Convenience function to get our name. + */ + private Name getUsername () + { + BodyObject body = (BodyObject) _ctx.getClient().getClientObject(); + return body.getVisibleName(); + } + + /** + * Create a listener for service requests. + */ + private InvocationService.ConfirmListener createLoggingListener ( + final String service) + { + return new InvocationService.ConfirmListener() { + public void requestFailed (String cause) + { + Log.warning("Service failure " + + "[service=" + service + ", cause=" + cause + "]."); + } + + public void requestProcessed () + { + // nada + } + }; + } + + /** + * Helper method for setCollection and addToCollection. + */ + private void populateCollection ( + String collName, Object values, boolean clearExisting) + { + validateName(collName); + if (values == null) { + throw new IllegalArgumentException( + "Collection values may not be null."); + } + validateValue(values); + + byte[][] encodedValues = (byte[][]) EZObjectMarshaller.encode(values); + + _ezObj.ezGameService.addToCollection( + _ctx.getClient(), collName, encodedValues, clearExisting, + createLoggingListener("populateCollection")); + } + + /** + * Helper method for pickFromCollection and dealFromCollection. + */ + private void getFromCollection( + String collName, final int count, String msgOrPropName, int playerIndex, + boolean consume, final DealListener dealy) + { + validateName(collName); + validateName(msgOrPropName); + if (count < 1) { + throw new IllegalArgumentException( + "Must retrieve at least one element!"); + } + + InvocationService.ConfirmListener listener; + if (dealy != null) { + // TODO: Figure out the method sig of the callback, and what it + // means + listener = new InvocationService.ConfirmListener() { + public void requestFailed (String cause) { + try { + dealy.dealt(Integer.parseInt(cause)); + } catch (NumberFormatException nfe) { + // nada + } + } + + public void requestProcessed () { + dealy.dealt(count); + } + }; + + } else { + listener = createLoggingListener("getFromCollection"); + } + + _ezObj.ezGameService.getFromCollection( + _ctx.getClient(), collName, consume, count, msgOrPropName, + playerIndex, listener); + } + + /** + * Verify that the property name / value are valid. + */ + private void validatePropertyChange ( + String propName, Object value, int index) + { + validateName(propName); + + // check that we're setting an array element on an array + if (index >= 0) { + if (!(get(propName) instanceof Object[])) { + throw new IllegalArgumentException("Property " + propName + + " is not an Array."); + } + } + + // validate the value too + validateValue(value); + } + + /** + * Verify that the specified name is valid. + */ + private void validateName (String name) + { + if (name == null) { + throw new IllegalArgumentException( + "Property, message, and collection names must not be null."); + } + } + + private void validateChat (String msg) + { + if (StringUtil.isBlank(msg)) { + throw new IllegalArgumentException( + "Empty chat may not be displayed."); + } + } + + /** + * Verify that the value is legal to be streamed to other clients. + */ + private void validateValue (Object value) + { + if (value == null) { + return; + + } else if (value instanceof Externalizable) { + throw new IllegalArgumentException( + "IExternalizable is not yet supported"); + + } else if (value.getClass().isArray()) { + int length = Array.getLength(value); + for (int ii=0; ii < length; ii++) { + validateValue(Array.get(value, ii)); + } + + } else if (value instanceof Iterable) { + for (Object o : (Iterable) value) { + validateValue(o); + } + + } else if (!(value instanceof Serializable)) { + throw new IllegalArgumentException( + "Non-serializable properties may not be set."); + } + } + + protected CrowdContext _ctx; + + protected EZGameObject _ezObj; + + protected HashMap _props; + + protected ObserverList _listeners = + new ObserverList(ObserverList.SAFE_IN_ORDER_NOTIFY); +} diff --git a/src/java/com/threerings/ezgame/data/EZGameConfig.java b/src/java/com/threerings/ezgame/data/EZGameConfig.java index f2c779f6..570a2962 100644 --- a/src/java/com/threerings/ezgame/data/EZGameConfig.java +++ b/src/java/com/threerings/ezgame/data/EZGameConfig.java @@ -29,7 +29,8 @@ public class EZGameConfig extends GameConfig // from abstract GameConfig public GameConfigurator createConfigurator () { - return null; // nothing here on the java side + // TODO + return null; } @Override diff --git a/src/java/com/threerings/ezgame/data/EZGameObject.java b/src/java/com/threerings/ezgame/data/EZGameObject.java index 829e93ab..efbc4d18 100644 --- a/src/java/com/threerings/ezgame/data/EZGameObject.java +++ b/src/java/com/threerings/ezgame/data/EZGameObject.java @@ -17,6 +17,8 @@ import com.threerings.io.Streamable; import com.threerings.parlor.game.data.GameObject; import com.threerings.parlor.turn.data.TurnGameObject; +import com.threerings.ezgame.util.EZObjectMarshaller; + /** * Contains the data for an ez game. */ @@ -43,6 +45,14 @@ public class EZGameObject extends GameObject /** The service interface for requesting special things from the server. */ public EZGameMarshaller ezGameService; + /** + * Access the underlying user properties + */ + public HashMap getUserProps () + { + return _props; + } + // from TurnGameObject public String getTurnHolderFieldName () { @@ -64,23 +74,40 @@ public class EZGameObject extends GameObject /** * Called by PropertySetEvent to effect the property update. */ - protected void applyPropertySet (String propName, Object data, int index) + public Object applyPropertySet (String propName, Object data, int index) { + Object oldValue = _props.get(propName); 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); + if (isOnServer()) { + byte[][] arr = (oldValue instanceof byte[][]) + ? (byte[][]) oldValue : 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; } - _props.put(propName, newArr); - arr = newArr; + oldValue = arr[index]; + arr[index] = (byte[]) data; + + } else { + Object[] arr = (oldValue instanceof Object[]) + ? (Object[]) oldValue : null; + if (arr == null || arr.length <= index) { + Object[] newArr = new Object[index + 1]; + if (arr != null) { + System.arraycopy(arr, 0, newArr, 0, arr.length); + } + _props.put(propName, newArr); + arr = newArr; + } + oldValue = arr[index]; + arr[index] = data; } - arr[index] = (byte[]) data; } else if (data != null) { _props.put(propName, data); @@ -88,6 +115,8 @@ public class EZGameObject extends GameObject } else { _props.remove(propName); } + + return oldValue; } // AUTO-GENERATED: METHODS START @@ -132,11 +161,15 @@ public class EZGameObject extends GameObject { out.defaultWriteObject(); - // write the number of properties, followed by each one - out.writeInt(_props.size()); - for (Map.Entry entry : _props.entrySet()) { - out.writeUTF(entry.getKey()); - out.writeObject(entry.getValue()); + if (isOnServer()) { + // write the number of properties, followed by each one + out.writeInt(_props.size()); + for (Map.Entry entry : _props.entrySet()) { + out.writeUTF(entry.getKey()); + out.writeObject(entry.getValue()); + } + } else { + throw new IllegalStateException(); } } @@ -150,13 +183,31 @@ public class EZGameObject extends GameObject _props.clear(); int count = ins.readInt(); + boolean onClient = !isOnServer(); while (count-- > 0) { String key = ins.readUTF(); - _props.put(key, ins.readObject()); + Object o = ins.readObject(); + if (onClient) { + o = EZObjectMarshaller.decode(o); + } + _props.put(key, o); } } - /** The current state of game data, opaque to us here on the server. */ + /** + * Called internally and by PropertySetEvent to determine if we're + * on the server or on the client. + */ + boolean isOnServer () + { + return _omgr.isManager(this); + } + + /** The current state of game data. + * On the server, this will be a byte[] for normal properties + * and a byte[][] for array properties. + * On the client, the actual values are kept whole. + */ protected transient HashMap _props = new HashMap(); } diff --git a/src/java/com/threerings/ezgame/data/PropertySetEvent.java b/src/java/com/threerings/ezgame/data/PropertySetEvent.java index c2e70ec2..e00a53fe 100644 --- a/src/java/com/threerings/ezgame/data/PropertySetEvent.java +++ b/src/java/com/threerings/ezgame/data/PropertySetEvent.java @@ -9,10 +9,12 @@ 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; +import com.threerings.ezgame.util.EZObjectMarshaller; + /** * Represents a property change on the actionscript object we * use in EZGameObject. @@ -35,16 +37,55 @@ public class PropertySetEvent extends NamedEvent _index = index; } + /** + * Get the value that was set for the property. + */ + public Object getValue () + { + return _data; + } + + /** + * Get the old value. + */ + public Object getOldValue () + { + return _oldValue; + } + + /** + * Get the index, or -1 if not applicable. + */ + public int getIndex () + { + return _index; + } + // from abstract DEvent public boolean applyToObject (DObject target) { - ((EZGameObject) target).applyPropertySet(_name, _data, _index); + EZGameObject ezObj = (EZGameObject) target; + if (!ezObj.isOnServer()) { + _data = EZObjectMarshaller.decode(_data); + } + _oldValue = ezObj.applyPropertySet(_name, _data, _index); return true; } + @Override + protected void notifyListener (Object listener) + { + if (listener instanceof PropertySetListener) { + ((PropertySetListener) listener).propertyWasSet(this); + } + } + /** The index. */ protected int _index; /** The client-side data that is assigned to this property. */ protected Object _data; + + /** The old value. */ + protected transient Object _oldValue; } diff --git a/src/java/com/threerings/ezgame/data/PropertySetListener.java b/src/java/com/threerings/ezgame/data/PropertySetListener.java new file mode 100644 index 00000000..20f406cd --- /dev/null +++ b/src/java/com/threerings/ezgame/data/PropertySetListener.java @@ -0,0 +1,16 @@ +package com.threerings.ezgame.data; + +import com.threerings.presents.dobj.ChangeListener; + +/** + * A listener that will be notified of PropertySet events. + */ +public interface PropertySetListener extends ChangeListener +{ + /** + * Called when a property was set on a flash game object. + * This will be called after the event has been applied + * to the object. + */ + public void propertyWasSet (PropertySetEvent event); +} diff --git a/src/java/com/threerings/ezgame/util/EZObjectMarshaller.java b/src/java/com/threerings/ezgame/util/EZObjectMarshaller.java new file mode 100644 index 00000000..3b2b9144 --- /dev/null +++ b/src/java/com/threerings/ezgame/util/EZObjectMarshaller.java @@ -0,0 +1,102 @@ +package com.threerings.ezgame.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import java.lang.reflect.Array; + +import java.util.ArrayList; + +/** + * Utility methods for transferring flash properties via + * the presents dobj system. + */ +public class EZObjectMarshaller +{ + /** + * Encode the specified object as either a byte[] or a byte[][] if it + * is an array. The specific mechanism of encoding is not important, + * as long as decode returns a clone of the original object. + * + * Currently, cycles in the object graph are preserved on the other end. + * TODO: serialize Externalizable implementations, and take + * into account the ClassLoader. + */ + public static Object encode (Object obj) + { + return encode(obj, true); + } + + protected static Object encode (Object obj, boolean encodeArrayElements) + { + if (obj == null) { + return null; + } + if (encodeArrayElements) { + if (obj instanceof Iterable) { + ArrayList list = new ArrayList(); + for (Object o : (Iterable) obj) { + list.add((byte[]) encode(o, false)); + } + byte[][] retval = new byte[list.size()][]; + list.toArray(retval); + return retval; + } + + if (obj.getClass().isArray()) { + int length = Array.getLength(obj); + byte[][] retval = new byte[length][]; + for (int ii=0; ii < length; ii++) { + retval[ii] = (byte[]) encode(Array.get(obj, ii), false); + } + return retval; + } + } + + // TODO: Our own encoding? + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(obj); + oos.flush(); + return baos.toByteArray(); + } catch (IOException ioe) { + // TODO: pass outward? + return null; // just crush the object into nothingness + } + } + + // TODO: we may extend this to take the client ezgame's ClassLoader + // and reconstitute custom classes that implement Externalizable + public static Object decode (Object encoded) + { + if (encoded == null) { + return null; + } + if (encoded instanceof byte[][]) { + byte[][] src = (byte[][]) encoded; + Object[] retval = new Object[src.length]; + for (int ii=0; ii < src.length; ii++) { + retval[ii] = decode(src[ii]); + } + return retval; + } + + try { + ByteArrayInputStream bais = + new ByteArrayInputStream((byte[]) encoded); + ObjectInputStream ois = new ObjectInputStream(bais); + return ois.readObject(); + + } catch (ClassNotFoundException cnse) { + return null; + + } catch (IOException ioe) { + // TODO: pass outward? + return null; // must not have been that important! + } + } +}