diff --git a/src/as/com/threerings/README.txt b/src/as/com/threerings/README.txt
index b488a98a3..8a500fe86 100644
--- a/src/as/com/threerings/README.txt
+++ b/src/as/com/threerings/README.txt
@@ -1,6 +1,16 @@
This document contains a couple of notes about some design decisions
and some notes about flash that you may find useful.
+TODO
+----
+- Write code that processes a dobj class in java and outputs the
+ corresponding class in actionscript. This is sorta fucked because
+ we want to exclude things not applicable to client code, not because
+ we're trying to save every byte in the class definition, but because
+ some of those methods involve whole classes we don't need on the as client.
+- Write code that generates actionscript service, listener and marshaller
+ classes from a java Service class definition.
+
Design decisions
----------------
diff --git a/src/as/com/threerings/crowd/Log.as b/src/as/com/threerings/crowd/Log.as
new file mode 100644
index 000000000..27d87b7d7
--- /dev/null
+++ b/src/as/com/threerings/crowd/Log.as
@@ -0,0 +1,36 @@
+package com.threerings.crowd {
+
+import mx.logging.ILogger;
+
+import com.threerings.util.LogDaddy;
+
+public class Log extends LogDaddy
+{
+ /** The Logger for this package. */
+ public static var log :ILogger = getLogger("crowd");
+
+ /** Convenience function. */
+ public static function debug (message :String, ... rest) :void
+ {
+ log.debug(message, rest);
+ }
+
+ /** Convenience function. */
+ public static function info (message :String, ... rest) :void
+ {
+ log.info(message, rest);
+ }
+
+ /** Convenience function. */
+ public static function warning (message :String, ... rest) :void
+ {
+ log.warn(message, rest);
+ }
+
+ /** Convenience function. */
+ public static function logStackTrace (err :Error) :void
+ {
+ log.warn(err.getStackTrace());
+ }
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/ChatDirector.as b/src/as/com/threerings/crowd/chat/client/ChatDirector.as
new file mode 100644
index 000000000..591c231a1
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/ChatDirector.as
@@ -0,0 +1,1216 @@
+//
+// $Id: ChatDirector.java 3770 2005-11-29 19:33:04Z ray $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.client {
+// TODO: this class is in progress
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.samskivert.util.Collections;
+import com.samskivert.util.HashIntMap;
+import com.samskivert.util.ObserverList;
+import com.samskivert.util.ResultListener;
+import com.samskivert.util.StringUtil;
+
+import com.threerings.presents.client.BasicDirector;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.dobj.DObject;
+import com.threerings.presents.dobj.MessageEvent;
+import com.threerings.presents.dobj.MessageListener;
+
+import com.threerings.util.MessageBundle;
+import com.threerings.util.MessageManager;
+import com.threerings.util.Name;
+import com.threerings.util.TimeUtil;
+
+import com.threerings.crowd.Log;
+import com.threerings.crowd.client.LocationObserver;
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.PlaceObject;
+import com.threerings.crowd.util.CrowdContext;
+
+import com.threerings.crowd.chat.data.ChatCodes;
+import com.threerings.crowd.chat.data.ChatMessage;
+import com.threerings.crowd.chat.data.SystemMessage;
+import com.threerings.crowd.chat.data.TellFeedbackMessage;
+import com.threerings.crowd.chat.data.UserMessage;
+import com.threerings.crowd.chat.data.UserSystemMessage;
+
+/**
+ * The chat director is the client side coordinator of all chat related
+ * services. It handles both place constrained chat as well as direct
+ * messaging.
+ */
+public class ChatDirector extends BasicDirector
+ implements ChatCodes, LocationObserver, MessageListener
+{
+ /**
+ * Creates a chat director and initializes it with the supplied
+ * context. The chat director will register itself as a location
+ * observer so that it can automatically process place constrained
+ * chat.
+ *
+ * @param msgmgr the message manager via which we do our translations.
+ * @param bundle the message bundle from which we obtain our
+ * chat-related translation strings.
+ */
+ public ChatDirector (CrowdContext ctx, MessageManager msgmgr, String bundle)
+ {
+ super(ctx);
+
+ // keep the context around
+ _ctx = ctx;
+ _msgmgr = msgmgr;
+ _bundle = bundle;
+
+ // register ourselves as a location observer
+ _ctx.getLocationDirector().addLocationObserver(this);
+
+ // register our default chat handlers
+ if (_bundle == null || _msgmgr == null) {
+ Log.warning("Null bundle or message manager given to ChatDirector");
+ return;
+ }
+ MessageBundle msg = _msgmgr.getBundle(_bundle);
+ registerCommandHandler(msg, "help", new HelpHandler());
+ registerCommandHandler(msg, "clear", new ClearHandler());
+ registerCommandHandler(msg, "speak", new SpeakHandler());
+ registerCommandHandler(msg, "emote", new EmoteHandler());
+ registerCommandHandler(msg, "think", new ThinkHandler());
+ }
+
+ /**
+ * Adds the supplied chat display to the chat display list. It will
+ * subsequently be notified of incoming chat messages as well as tell
+ * responses.
+ */
+ public void addChatDisplay (ChatDisplay display)
+ {
+ _displays.add(display);
+ }
+
+ /**
+ * Removes the specified chat display from the chat display list. The
+ * display will no longer receive chat related notifications.
+ */
+ public void removeChatDisplay (ChatDisplay display)
+ {
+ _displays.remove(display);
+ }
+
+ /**
+ * Adds the specified chat filter to the list of filters. All
+ * chat requests and receipts will be filtered with all filters
+ * before they being sent or dispatched locally.
+ */
+ public void addChatFilter (ChatFilter filter)
+ {
+ _filters.add(filter);
+ }
+
+ /**
+ * Removes the specified chat validator from the list of chat validators.
+ */
+ public void removeChatFilter (ChatFilter filter)
+ {
+ _filters.remove(filter);
+ }
+
+ /**
+ * Adds an observer that watches the chatters list, and updates it
+ * immediately.
+ */
+ public void addChatterObserver (ChatterObserver co)
+ {
+ _chatterObservers.add(co);
+ co.chattersUpdated(_chatters.listIterator());
+ }
+
+ /**
+ * Removes an observer from the list of chatter observers.
+ */
+ public void removeChatterObserver (ChatterObserver co)
+ {
+ _chatterObservers.remove(co);
+ }
+
+ /**
+ * Sets the validator that decides if a username is valid to be
+ * added to the chatter list, or null if no such filtering is desired.
+ */
+ public void setChatterValidator (ChatterValidator validator)
+ {
+ _chatterValidator = validator;
+ }
+
+ /**
+ * Registers a chat command handler.
+ *
+ * @param msg the message bundle via which the slash command will be
+ * translated (as c.command). If no translation
+ * exists the command will be /command.
+ * @param command the name of the command that will be used to invoke
+ * this handler (e.g. tell if the command will be invoked
+ * as /tell).
+ * @param handler the chat command handler itself.
+ */
+ public void registerCommandHandler (
+ MessageBundle msg, String command, CommandHandler handler)
+ {
+ String key = "c." + command;
+ if (msg.exists(key)) {
+ StringTokenizer st = new StringTokenizer(msg.get(key));
+ while (st.hasMoreTokens()) {
+ _handlers.put(st.nextToken(), handler);
+ }
+ } else {
+ // fall back to just using the English command
+ _handlers.put(command, handler);
+ }
+ }
+
+ /**
+ * Return the current size of the history.
+ */
+ public int getCommandHistorySize ()
+ {
+ return _history.size();
+ }
+
+ /**
+ * Get the chat history entry at the specified index,
+ * with 0 being the oldest.
+ */
+ public String getCommandHistory (int index)
+ {
+ return (String)_history.get(index);
+ }
+
+ /**
+ * Clear the chat command history.
+ */
+ public void clearCommandHistory ()
+ {
+ _history.clear();
+ }
+
+ /**
+ * Requests that all chat displays clear their contents.
+ */
+ public void clearDisplays ()
+ {
+ _displays.apply(new ObserverList.ObserverOp() {
+ public boolean apply (Object observer) {
+ ((ChatDisplay)observer).clear();
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Display a system INFO message as if it had come from the server.
+ * The localtype of the message will be PLACE_CHAT_TYPE.
+ *
+ * Info messages are sent when something happens that was neither
+ * directly triggered by the user, nor requires direct action.
+ */
+ public void displayInfo (String bundle, String message)
+ {
+ displaySystem(bundle, message, SystemMessage.INFO, PLACE_CHAT_TYPE);
+ }
+
+ /**
+ * Display a system INFO message as if it had come from the server.
+ *
+ * Info messages are sent when something happens that was neither
+ * directly triggered by the user, nor requires direct action.
+ */
+ public void displayInfo (String bundle, String message, String localtype)
+ {
+ displaySystem(bundle, message, SystemMessage.INFO, localtype);
+ }
+
+ /**
+ * Display a system FEEDBACK message as if it had come from the server.
+ * The localtype of the message will be PLACE_CHAT_TYPE.
+ *
+ * Feedback messages are sent in direct response to a user action,
+ * usually to indicate success or failure of the user's action.
+ */
+ public void displayFeedback (String bundle, String message)
+ {
+ displaySystem(
+ bundle, message, SystemMessage.FEEDBACK, PLACE_CHAT_TYPE);
+ }
+
+ /**
+ * Display a system ATTENTION message as if it had come from the server.
+ * The localtype of the message will be PLACE_CHAT_TYPE.
+ *
+ * Attention messages are sent when something requires user action
+ * that did not result from direct action by the user.
+ */
+ public void displayAttention (String bundle, String message)
+ {
+ displaySystem(
+ bundle, message, SystemMessage.ATTENTION, PLACE_CHAT_TYPE);
+ }
+
+ /**
+ * Dispatches the provided message to our chat displays.
+ */
+ public void dispatchMessage (ChatMessage message)
+ {
+ _displayMessageOp.setMessage(message);
+ _displays.apply(_displayMessageOp);
+ }
+
+ /**
+ * Parses and delivers the supplied chat message. Slash command
+ * processing and mogrification are performed and the message is added
+ * to the chat history if appropriate.
+ *
+ * @param speakSvc the SpeakService representing the target dobj of
+ * the speak or null if we should speak in the "default" way.
+ * @param text the text to be parsed and sent.
+ * @param record if text is a command, should it be added to the history?
+ *
+ * @return ChatCodes#SUCCESS if the message was parsed
+ * and sent correctly, a translatable error string if there was some
+ * problem.
+ */
+ public String requestChat (
+ SpeakService speakSvc, String text, boolean record)
+ {
+ if (text.startsWith("/")) {
+ // split the text up into a command and arguments
+ String command = text.substring(1).toLowerCase();
+ String[] hist = new String[1];
+ String args = "";
+ int sidx = text.indexOf(" ");
+ if (sidx != -1) {
+ command = text.substring(1, sidx).toLowerCase();
+ args = text.substring(sidx+1).trim();
+ }
+
+ HashMap possibleCommands = getCommandHandlers(command);
+ switch (possibleCommands.size()) {
+ case 0:
+ StringTokenizer tok = new StringTokenizer(text);
+ return MessageBundle.tcompose(
+ "m.unknown_command", tok.nextToken());
+
+ case 1:
+ Iterator itr = possibleCommands.entrySet().iterator();
+ Map.Entry entry = (Map.Entry) itr.next();
+ String cmdName = (String) entry.getKey();
+ CommandHandler cmd = (CommandHandler) entry.getValue();
+
+ String result = cmd.handleCommand(speakSvc, cmdName, args, hist);
+ if (!result.equals(ChatCodes.SUCCESS)) {
+ return result;
+ }
+
+ if (record) {
+ // get the final history-ready command string
+ hist[0] = "/" + ((hist[0] == null) ? command : hist[0]);
+
+ // remove from history if it was present and
+ // add it to the end
+ addToHistory(hist[0]);
+ }
+
+ return result;
+
+ default:
+ String alternativeCommands = "";
+ itr = Collections.getSortedIterator(possibleCommands.keySet());
+ while (itr.hasNext()) {
+ cmdName = (String)itr.next();
+ alternativeCommands += " /" + cmdName;
+ }
+ return MessageBundle.tcompose(
+ "m.unspecific_command", alternativeCommands);
+ }
+ }
+
+ // if not a command then just speak
+ String message = text.trim();
+ if (StringUtil.isBlank(message)) {
+ // report silent failure for now
+ return ChatCodes.SUCCESS;
+ }
+
+ return deliverChat(speakSvc, message, ChatCodes.DEFAULT_MODE);
+ }
+
+ /**
+ * Requests that a speak message with the specified mode be generated
+ * and delivered via the supplied speak service instance (which will
+ * be associated with a particular "speak object"). The message will
+ * first be validated by all registered {@link ChatFilter}s (and
+ * possibly vetoed) before being dispatched.
+ *
+ * @param speakService the speak service to use when generating the
+ * speak request or null if we should speak in the current "place".
+ * @param message the contents of the speak message.
+ * @param mode a speech mode that will be interpreted by the {@link
+ * ChatDisplay} implementations that eventually display this speak
+ * message.
+ */
+ public void requestSpeak (
+ SpeakService speakService, String message, byte mode)
+ {
+ if (speakService == null) {
+ if (_place == null) {
+ return;
+ }
+ speakService = _place.speakService;
+ }
+
+ // make sure they can say what they want to say
+ message = filter(message, null, true);
+ if (message == null) {
+ return;
+ }
+
+ // dispatch a speak request using the supplied speak service
+ speakService.speak(_ctx.getClient(), message, mode);
+ }
+
+ /**
+ * Requests to send a site-wide broadcast message.
+ *
+ * @param message the contents of the message.
+ */
+ public void requestBroadcast (String message)
+ {
+ message = filter(message, null, true);
+ if (message == null) {
+ displayFeedback(_bundle,
+ MessageBundle.compose("m.broadcast_failed", "m.filtered"));
+ return;
+ }
+
+ _cservice.broadcast(
+ _ctx.getClient(), message, new ChatService.InvocationListener() {
+ public void requestFailed (String reason) {
+ reason = MessageBundle.compose(
+ "m.broadcast_failed", reason);
+ displayFeedback(_bundle, reason);
+ }
+ });
+ }
+
+ /**
+ * Requests that a tell message be delivered to the specified target
+ * user.
+ *
+ * @param target the username of the user to which the tell message
+ * should be delivered.
+ * @param msg the contents of the tell message.
+ * @param rl an optional result listener if you'd like to be notified
+ * of success or failure.
+ */
+ public void requestTell (final Name target, String msg,
+ final ResultListener rl)
+ {
+ // make sure they can say what they want to say
+ final String message = filter(msg, target, true);
+ if (message == null) {
+ if (rl != null) {
+ rl.requestFailed(null);
+ }
+ return;
+ }
+
+ // create a listener that will report success or failure
+ ChatService.TellListener listener = new ChatService.TellListener() {
+ public void tellSucceeded (long idletime, String awayMessage) {
+ success(xlate(_bundle, MessageBundle.tcompose(
+ "m.told_format", target, message)));
+
+ // if they have an away message, report that
+ if (awayMessage != null) {
+ awayMessage = filter(awayMessage, target, false);
+ if (awayMessage != null) {
+ String msg = MessageBundle.tcompose(
+ "m.recipient_afk", target, awayMessage);
+ displayFeedback(_bundle, msg);
+ }
+ }
+
+ // if they are idle, report that
+ if (idletime > 0L) {
+ // adjust by the time it took them to become idle
+ idletime += _ctx.getConfig().getValue(
+ IDLE_TIME_KEY, DEFAULT_IDLE_TIME);
+ String msg = MessageBundle.compose(
+ "m.recipient_idle", MessageBundle.taint(target),
+ TimeUtil.getTimeOrderString(idletime, TimeUtil.MINUTE));
+ displayFeedback(_bundle, msg);
+ }
+ }
+
+ protected void success (String feedback) {
+ dispatchMessage(new TellFeedbackMessage(feedback));
+ addChatter(target);
+ if (rl != null) {
+ rl.requestCompleted(target);
+ }
+ }
+
+ public void requestFailed (String reason) {
+ String msg = MessageBundle.compose(
+ "m.tell_failed", MessageBundle.taint(target), reason);
+ displayFeedback(_bundle, msg);
+ if (rl != null) {
+ rl.requestFailed(null);
+ }
+ }
+ };
+
+ _cservice.tell(_ctx.getClient(), target, message, listener);
+ }
+
+ /**
+ * Configures a message that will be automatically reported to anyone
+ * that sends a tell message to this client to indicate that we are
+ * busy or away from the keyboard.
+ */
+ public void setAwayMessage (String message)
+ {
+ if (message != null) {
+ message = filter(message, null, true);
+ if (message == null) {
+ // they filtered away their own away message..
+ // change it to something
+ message = "...";
+ }
+ }
+ // pass the buck right on along
+ _cservice.away(_ctx.getClient(), message);
+ }
+
+ /**
+ * Adds an additional object via which chat messages may arrive. The
+ * chat director assumes the caller will be managing the subscription
+ * to this object and will remain subscribed to it for as long as it
+ * remains in effect as an auxiliary chat source.
+ *
+ * @param localtype a type to be associated with all chat messages
+ * that arrive on the specified DObject.
+ */
+ public void addAuxiliarySource (DObject source, String localtype)
+ {
+ source.addListener(this);
+ _auxes.put(source.getOid(), localtype);
+ }
+
+ /**
+ * Removes a previously added auxiliary chat source.
+ */
+ public void removeAuxiliarySource (DObject source)
+ {
+ source.removeListener(this);
+ _auxes.remove(source.getOid());
+ }
+
+ /**
+ * Run a message through all the currently registered filters.
+ */
+ public String filter (String msg, Name otherUser, boolean outgoing)
+ {
+ _filterMessageOp.setMessage(msg, otherUser, outgoing);
+ _filters.apply(_filterMessageOp);
+ return _filterMessageOp.getMessage();
+ }
+
+ /**
+ * Runs the supplied message through the various chat mogrifications.
+ */
+ public String mogrifyChat (String text)
+ {
+ return mogrifyChat(text, false, true);
+ }
+
+ // documentation inherited
+ public boolean locationMayChange (int placeId)
+ {
+ // we accept all location change requests
+ return true;
+ }
+
+ // documentation inherited
+ public void locationDidChange (PlaceObject place)
+ {
+ if (_place != null) {
+ // unlisten to our old object
+ _place.removeListener(this);
+ }
+
+ // listen to the new object
+ _place = place;
+ if (_place != null) {
+ _place.addListener(this);
+ }
+ }
+
+ // documentation inherited
+ public void locationChangeFailed (int placeId, String reason)
+ {
+ // nothing we care about
+ }
+
+ // documentation inherited
+ public void messageReceived (MessageEvent event)
+ {
+ if (CHAT_NOTIFICATION.equals(event.getName())) {
+ ChatMessage msg = (ChatMessage) event.getArgs()[0];
+ String localtype = getLocalType(event.getTargetOid());
+ String message = msg.message;
+ String autoResponse = null;
+ Name speaker = null;
+ byte mode = (byte) -1;
+
+ // figure out if the message was triggered by another user
+ if (msg instanceof UserMessage) {
+ UserMessage umsg = (UserMessage)msg;
+ speaker = umsg.speaker;
+ mode = umsg.mode;
+
+ } else if (msg instanceof UserSystemMessage) {
+ speaker = ((UserSystemMessage) msg).speaker;
+ }
+
+ // if there was an originating speaker, see if we want to hear it
+ if (speaker != null) {
+ if ((message = filter(message, speaker, false)) == null) {
+ return;
+ }
+
+ if (USER_CHAT_TYPE.equals(localtype) &&
+ mode == ChatCodes.DEFAULT_MODE) {
+ // if it was a tell, add the speaker as a chatter
+ addChatter(speaker);
+
+ // note whether or not we have an auto-response
+ BodyObject self = (BodyObject)
+ _ctx.getClient().getClientObject();
+ if (!StringUtil.isBlank(self.awayMessage)) {
+ autoResponse = self.awayMessage;
+ }
+ }
+ }
+
+ // initialize the client-specific fields of the message
+ msg.setClientInfo(xlate(msg.bundle, message), localtype);
+
+ // and send it off!
+ dispatchMessage(msg);
+
+ // if we auto-responded, report as much
+ if (autoResponse != null) {
+ String amsg = MessageBundle.tcompose(
+ "m.auto_responded", speaker, autoResponse);
+ displayFeedback(_bundle, amsg);
+ }
+ }
+ }
+
+ // documentation inherited
+ public void clientDidLogon (Client client)
+ {
+ super.clientDidLogon(client);
+
+ // listen on the client object for tells
+ addAuxiliarySource(_clobj = client.getClientObject(), USER_CHAT_TYPE);
+ }
+
+ // documentation inherited
+ public void clientObjectDidChange (Client client)
+ {
+ super.clientObjectDidChange(client);
+
+ // change what we're listening to for tells
+ removeAuxiliarySource(_clobj);
+ addAuxiliarySource(_clobj = client.getClientObject(), USER_CHAT_TYPE);
+
+ clearDisplays();
+ }
+
+ // documentation inherited
+ public void clientDidLogoff (Client client)
+ {
+ super.clientDidLogoff(client);
+
+ // stop listening to it for tells
+ if (_clobj != null) {
+ removeAuxiliarySource(_clobj);
+ _clobj = null;
+ }
+ // in fact, clear out all auxiliary sources
+ _auxes.clear();
+
+ clearDisplays();
+
+ // clear out the list of people we've chatted with
+ _chatters.clear();
+ notifyChatterObservers();
+
+ // clear the _place
+ locationDidChange(null);
+
+ // clear our service
+ _cservice = null;
+ }
+
+ /**
+ * Called to determine whether we are permitted to post the supplied
+ * chat message. Derived classes may wish to throttle chat or restrict
+ * certain types in certain circumstances for whatever reason.
+ *
+ * @return null if the chat is permitted, SUCCESS if the chat is permitted
+ * and has already been dealt with, or a translatable string
+ * indicating the reason for rejection if not.
+ */
+ protected String checkCanChat (
+ SpeakService speakSvc, String message, byte mode)
+ {
+ return null;
+ }
+
+ /**
+ * Delivers a plain chat message (not a slash command) on the
+ * specified speak service in the specified mode. The message will be
+ * mogrified and filtered prior to delivery.
+ *
+ * @return {@link ChatCodes#SUCCESS} if the message was delivered or a
+ * string indicating why it failed.
+ */
+ protected String deliverChat (
+ SpeakService speakSvc, String message, byte mode)
+ {
+ // run the message through our mogrification process
+ message = mogrifyChat(message, true, mode != ChatCodes.EMOTE_MODE);
+
+ // mogrification may result in something being turned into a slash
+ // command, in which case we have to run everything through again
+ // from the start
+ if (message.startsWith("/")) {
+ return requestChat(speakSvc, message, false);
+ }
+
+ // make sure this client is not restricted from performing this
+ // chat message for some reason or other
+ String errmsg = checkCanChat(speakSvc, message, mode);
+ if (errmsg != null) {
+ return errmsg;
+ }
+
+ // speak on the specified service
+ requestSpeak(speakSvc, message, mode);
+
+ return ChatCodes.SUCCESS;
+ }
+
+ /**
+ * Add the specified command to the history.
+ */
+ protected void addToHistory (String cmd)
+ {
+ // remove any previous instance of this command
+ _history.remove(cmd);
+
+ // append it to the end
+ _history.add(cmd);
+
+ // prune the history once it extends beyond max size
+ if (_history.size() > MAX_COMMAND_HISTORY) {
+ _history.remove(0);
+ }
+ }
+
+ /**
+ * Mogrify common literary crutches into more appealing chat or
+ * commands.
+ *
+ * @param transformsAllowed if true, the chat may transformed into a
+ * different mode. (lol -> /emote laughs)
+ * @param capFirst if true, the first letter of the text is
+ * capitalized. This is not desired if the chat is already an emote.
+ */
+ protected String mogrifyChat (
+ String text, boolean transformsAllowed, boolean capFirst)
+ {
+ int tlen = text.length();
+ if (tlen == 0) {
+ return text;
+
+ // check to make sure there aren't too many caps
+ } else if (tlen > 7) {
+ // count caps
+ int caps = 0;
+ for (int ii=0; ii < tlen; ii++) {
+ if (Character.isUpperCase(text.charAt(ii))) {
+ caps++;
+ if (caps > (tlen / 2)) {
+ // lowercase the whole string if there are
+ text = text.toLowerCase();
+ break;
+ }
+ }
+ }
+ }
+
+ StringBuffer buf = new StringBuffer(text);
+ buf = mogrifyChat(buf, transformsAllowed, capFirst);
+ return buf.toString();
+ }
+
+ /** Helper function for {@link #mogrifyChat}. */
+ protected StringBuffer mogrifyChat (
+ StringBuffer buf, boolean transformsAllowed, boolean capFirst)
+ {
+ // do the generic mogrifications and translations
+ buf = translatedReplacements("x.mogrifies", buf);
+
+ // perform themed expansions and transformations
+ if (transformsAllowed) {
+ buf = translatedReplacements("x.transforms", buf);
+ }
+
+ /*
+ // capitalize the first letter
+ if (capFirst) {
+ buf.setCharAt(0, Character.toUpperCase(buf.charAt(0)));
+ }
+ // and capitalize any letters after a sentence-ending punctuation
+ Pattern p = Pattern.compile("([^\\.][\\.\\?\\!](\\s)+\\p{Ll})");
+ Matcher m = p.matcher(buf);
+ if (m.find()) {
+ buf = new StringBuffer();
+ m.appendReplacement(buf, m.group().toUpperCase());
+ while (m.find()) {
+ m.appendReplacement(buf, m.group().toUpperCase());
+ }
+ m.appendTail(buf);
+ }
+ */
+
+ return buf;
+ }
+
+ /**
+ * Do all the replacements (mogrifications) specified in the
+ * translation string specified by the key.
+ */
+ protected StringBuffer translatedReplacements (String key, StringBuffer buf)
+ {
+ MessageBundle bundle = _msgmgr.getBundle(_bundle);
+ if (!bundle.exists(key)) {
+ return buf;
+ }
+ StringTokenizer st = new StringTokenizer(bundle.get(key), "#");
+ // apply the replacements to each mogrification that matches
+ while (st.hasMoreTokens()) {
+ String pattern = st.nextToken();
+ String replace = st.nextToken();
+ Matcher m = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE).
+ matcher(buf);
+ if (m.find()) {
+ buf = new StringBuffer();
+ m.appendReplacement(buf, replace);
+ // they may match more than once
+ while (m.find()) {
+ m.appendReplacement(buf, replace);
+ }
+ m.appendTail(buf);
+ }
+ }
+ return buf;
+ }
+
+ /**
+ * Returns a hashmap containing all command handlers that match the
+ * specified command (i.e. the specified command is a prefix of their
+ * registered command string).
+ */
+ protected HashMap getCommandHandlers (String command)
+ {
+ HashMap matches = new HashMap();
+ BodyObject user = (BodyObject)_ctx.getClient().getClientObject();
+ Iterator itr = _handlers.entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry entry = (Map.Entry) itr.next();
+ String cmd = (String) entry.getKey();
+ if (!cmd.startsWith(command)) {
+ continue;
+ }
+ CommandHandler handler = (CommandHandler)entry.getValue();
+ if (!handler.checkAccess(user)) {
+ continue;
+ }
+ matches.put(cmd, handler);
+ }
+ return matches;
+ }
+
+ /**
+ * Adds a chatter to our list of recent chatters.
+ */
+ protected void addChatter (Name name)
+ {
+ // check to see if the chatter validator approves..
+ if ((_chatterValidator != null) &&
+ (!_chatterValidator.isChatterValid(name))) {
+ return;
+ }
+
+ boolean wasthere = _chatters.remove(name);
+ _chatters.addFirst(name);
+
+ if (!wasthere) {
+ if (_chatters.size() > MAX_CHATTERS) {
+ _chatters.removeLast();
+ }
+
+ notifyChatterObservers();
+ }
+ }
+
+ /**
+ * Notifies all registered {@link ChatterObserver}s that the list of
+ * chatters has changed.
+ */
+ protected void notifyChatterObservers ()
+ {
+ _chatterObservers.apply(new ObserverList.ObserverOp() {
+ public boolean apply (Object observer) {
+ ((ChatterObserver)observer).chattersUpdated(
+ _chatters.listIterator());
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Translates the specified message using the specified bundle.
+ */
+ protected String xlate (String bundle, String message)
+ {
+ if (bundle != null && _msgmgr != null) {
+ MessageBundle msgb = _msgmgr.getBundle(bundle);
+ if (msgb == null) {
+ Log.warning(
+ "No message bundle available to translate message " +
+ "[bundle=" + bundle + ", message=" + message + "].");
+ } else {
+ message = msgb.xlate(message);
+ }
+ }
+ return message;
+ }
+
+ /**
+ * Display the specified system message as if it had come from the server.
+ */
+ protected void displaySystem (
+ String bundle, String message, byte attLevel, String localtype)
+ {
+ // nothing should be untranslated, so pass the default bundle if need
+ // be.
+ if (bundle == null) {
+ bundle = _bundle;
+ }
+ SystemMessage msg = new SystemMessage();
+ msg.attentionLevel = attLevel;
+ msg.setClientInfo(xlate(bundle, message), localtype);
+ dispatchMessage(msg);
+ }
+
+ /**
+ * Looks up and returns the message type associated with the specified
+ * oid.
+ */
+ protected String getLocalType (int oid)
+ {
+ String type = (String)_auxes.get(oid);
+ return (type == null) ? PLACE_CHAT_TYPE : type;
+ }
+
+ /**
+ * Used to assign unique ids to all speak requests.
+ */
+ protected synchronized int nextRequestId ()
+ {
+ return _requestId++;
+ }
+
+ // documentation inherited from interface
+ protected void fetchServices (Client client)
+ {
+ // get a handle on our chat service
+ _cservice = (ChatService)client.requireService(ChatService.class);
+ }
+
+ /**
+ * An operation that checks with all chat filters to properly filter
+ * a message prior to sending to the server or displaying.
+ */
+ protected static class FilterMessageOp implements ObserverList.ObserverOp
+ {
+ public void setMessage (String msg, Name otherUser, boolean outgoing)
+ {
+ _msg = msg;
+ _otherUser = otherUser;
+ _out = outgoing;
+ }
+
+ public boolean apply (Object observer)
+ {
+ if (_msg != null) {
+ _msg = ((ChatFilter) observer).filter(_msg, _otherUser, _out);
+ }
+ return true;
+ }
+
+ public String getMessage ()
+ {
+ return _msg;
+ }
+
+ protected Name _otherUser;
+ protected String _msg;
+ protected boolean _out;
+ }
+
+ /**
+ * An observer op used to dispatch ChatMessages on the client.
+ */
+ protected static class DisplayMessageOp implements ObserverList.ObserverOp
+ {
+ public void setMessage (ChatMessage message)
+ {
+ _message = message;
+ }
+
+ public boolean apply (Object observer)
+ {
+ ((ChatDisplay)observer).displayMessage(_message);
+ return true;
+ }
+
+ protected ChatMessage _message;
+ }
+
+ /** Implements /help. */
+ protected class HelpHandler extends CommandHandler
+ {
+ public String handleCommand (
+ SpeakService speakSvc, String command, String args, String[] history)
+ {
+ String hcmd = "";
+
+ // grab the command they want help on
+ if (!StringUtil.isBlank(args)) {
+ hcmd = args;
+ int sidx = args.indexOf(" ");
+ if (sidx != -1) {
+ hcmd = args.substring(0, sidx);
+ }
+ }
+
+ // let the user give commands with or with the /
+ if (hcmd.startsWith("/")) {
+ hcmd = hcmd.substring(1);
+ }
+
+ // handle "/help help" and "/help someboguscommand"
+ HashMap possibleCommands = getCommandHandlers(hcmd);
+ if (hcmd.equals("help") || possibleCommands.isEmpty()) {
+ possibleCommands = getCommandHandlers("");
+ possibleCommands.remove("help"); // remove help from the list
+ }
+
+ // if there is only one possible command display its usage
+ switch (possibleCommands.size()) {
+ case 1:
+ Iterator itr = possibleCommands.keySet().iterator();
+ // this is a little funny, but we display the feeback
+ // message by hand and return SUCCESS so that the chat
+ // entry field doesn't think that we've failed and
+ // preserve our command text
+ displayFeedback(null, "m.usage_" + (String)itr.next());
+ return ChatCodes.SUCCESS;
+
+ default:
+ Object[] commands = possibleCommands.keySet().toArray();
+ Arrays.sort(commands);
+ String commandList = "";
+ for (int ii = 0; ii < commands.length; ii++) {
+ commandList += " /" + commands[ii];
+ }
+ return MessageBundle.tcompose("m.usage_help", commandList);
+ }
+ }
+ }
+
+ /** Implements /clear. */
+ protected class ClearHandler extends CommandHandler
+ {
+ public String handleCommand (
+ SpeakService speakSvc, String command, String args, String[] history)
+ {
+ clearDisplays();
+ return ChatCodes.SUCCESS;
+ }
+ }
+
+ /** Implements /speak. */
+ protected class SpeakHandler extends CommandHandler
+ {
+ public String handleCommand (
+ SpeakService speakSvc, String command, String args, String[] history)
+ {
+ if (StringUtil.isBlank(args)) {
+ return "m.usage_speak";
+ }
+ // note the command to be stored in the history
+ history[0] = command + " ";
+ return requestChat(null, args, true);
+ }
+ }
+
+ /** Implements /emote. */
+ protected class EmoteHandler extends CommandHandler
+ {
+ public String handleCommand (
+ SpeakService speakSvc, String command, String args, String[] history)
+ {
+ if (StringUtil.isBlank(args)) {
+ return "m.usage_emote";
+ }
+ // note the command to be stored in the history
+ history[0] = command + " ";
+ return deliverChat(speakSvc, args, ChatCodes.EMOTE_MODE);
+ }
+ }
+
+ /** Implements /think. */
+ protected class ThinkHandler extends CommandHandler
+ {
+ public String handleCommand (
+ SpeakService speakSvc, String command, String args, String[] history)
+ {
+ if (StringUtil.isBlank(args)) {
+ return "m.usage_think";
+ }
+ // note the command to be stored in the history
+ history[0] = command + " ";
+ return deliverChat(speakSvc, args, ChatCodes.THINK_MODE);
+ }
+ }
+
+ /** Our active chat context. */
+ protected CrowdContext _ctx;
+
+ /** Provides access to chat-related server-side services. */
+ protected ChatService _cservice;
+
+ /** The message manager. */
+ protected MessageManager _msgmgr;
+
+ /** The bundle to use for our own internal messages. */
+ protected String _bundle;
+
+ /** The place object that we currently occupy. */
+ protected PlaceObject _place;
+
+ /** The client object that we're listening to for tells. */
+ protected ClientObject _clobj;
+
+ /** A list of registered chat displays. */
+ protected ObserverList _displays =
+ new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
+
+ /** A list of registered chat filters. */
+ protected ObserverList _filters =
+ new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
+
+ /** A mapping from auxiliary chat objects to the types under which
+ * they are registered. */
+ protected HashIntMap _auxes = new HashIntMap();
+
+ /** Validator of who may be added to the chatters list. */
+ protected ChatterValidator _chatterValidator;
+
+ /** Usernames of users we've recently chatted with. */
+ protected LinkedList _chatters = new LinkedList();
+
+ /** Observers that are watching our chatters list. */
+ protected ObserverList _chatterObservers =
+ new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
+
+ /** Registered chat command handlers. */
+ protected static HashMap _handlers = new HashMap();
+
+ /** A history of chat commands. */
+ protected static ArrayList _history = new ArrayList();
+
+ /** Used by {@link #nextRequestId}. */
+ protected int _requestId;
+
+ /** Operation used to filter chat messages. */
+ protected FilterMessageOp _filterMessageOp = new FilterMessageOp();
+
+ /** Operation used to display chat messages. */
+ protected DisplayMessageOp _displayMessageOp = new DisplayMessageOp();
+
+ /** The maximum number of chatter usernames to track. */
+ protected static final int MAX_CHATTERS = 6;
+
+ /** The maximum number of commands to keep in the chat history. */
+ protected static final int MAX_COMMAND_HISTORY = 10;
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/ChatDisplay.as b/src/as/com/threerings/crowd/chat/client/ChatDisplay.as
new file mode 100644
index 000000000..2b67b2506
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/ChatDisplay.as
@@ -0,0 +1,45 @@
+//
+// $Id: ChatDisplay.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.client {
+
+import com.threerings.crowd.chat.data.ChatMessage;
+
+/**
+ * A chat display provides a means by which chat messages can be
+ * displayed. The chat display will be notified when chat messages of
+ * various sorts have been received by the client.
+ */
+public interface ChatDisplay
+{
+ /**
+ * Called to clear the chat display.
+ */
+ public function clear () :void;
+
+ /**
+ * Called to display a chat message.
+ *
+ * @see ChatMessage
+ */
+ public function displayMessage (msg :ChatMessage) :void;
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/ChatService.as b/src/as/com/threerings/crowd/chat/client/ChatService.as
new file mode 100644
index 000000000..f3de36292
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/ChatService.as
@@ -0,0 +1,69 @@
+//
+// $Id: ChatService.java 3310 2005-01-24 23:08:21Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.client {
+
+import com.threerings.util.Name;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationListener;
+import com.threerings.presents.client.InvocationService;
+
+/**
+ * The chat services provide a mechanism by which the client can broadcast
+ * chat messages to all clients that are subscribed to a particular place
+ * object or directly to a particular client. These services should not be
+ * used directly, but instead should be accessed via the {@link
+ * ChatDirector}.
+ */
+public interface ChatService extends InvocationService
+{
+ /**
+ * Requests that a tell message be delivered to the user with username
+ * equal to target.
+ *
+ * @param client a connected, operational client instance.
+ * @param target the username of the user to which the tell message
+ * should be delivered.
+ * @param message the contents of the message.
+ * @param listener the reference that will receive the tell response.
+ */
+ function tell (
+ client :Client, target :Name, message :String, listener :TellListener)
+ :void;
+
+ /**
+ * Requests that a message be broadcast to all users in the system.
+ *
+ * @param client a connected, operational client instance.
+ * @param message the contents of the message.
+ * @param listener the reference that will receive a failure response.
+ */
+ function broadcast (
+ client :Client, message :String, listener :InvocationListener) :void;
+
+ /**
+ * Sets this client's away message. If the message is null or the
+ * empty string, the away message will be cleared.
+ */
+ function away (client :Client, message :String) :void;
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/CommandHandler.as b/src/as/com/threerings/crowd/chat/client/CommandHandler.as
new file mode 100644
index 000000000..7d9f06299
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/CommandHandler.as
@@ -0,0 +1,44 @@
+package com.threerings.crowd.chat.client {
+
+import com.threerings.crowd.data.BodyObject;
+
+/**
+ * Used to implement a slash command (e.g. /who).
+ */
+public /* abstract */ class CommandHandler
+{
+ /**
+ * Handles the specified chat command.
+ *
+ * @param speakSvc an optional SpeakService object representing
+ * the object to send the chat message on.
+ * @param command the slash command that was used to invoke this
+ * handler (e.g. /tell).
+ * @param args the arguments provided along with the command (e.g.
+ * Bob hello) or null if no arguments
+ * were supplied.
+ * @param history an in/out parameter that allows the command to
+ * modify the text that will be appended to the chat history. If
+ * this is set to null, nothing will be appended.
+ *
+ * @return an untranslated string that will be reported to the
+ * chat box to convey an error response to the user, or {@link
+ * ChatCodes#SUCCESS}.
+ */
+ public function handleCommand (
+ speakSvc :SpeakService, cmd :String, args :String, history :Array)
+ :void
+ {
+ throw new Error("abstract");
+ }
+
+ /**
+ * Returns true if this user should have access to this chat
+ * command.
+ */
+ public function checkAccess (user :BodyObject) :Boolean
+ {
+ return true;
+ }
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/SpeakService.as b/src/as/com/threerings/crowd/chat/client/SpeakService.as
new file mode 100644
index 000000000..78154d35b
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/SpeakService.as
@@ -0,0 +1,46 @@
+//
+// $Id: SpeakService.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.client {
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+
+/**
+ * Provides a means by which "speaking" can be allowed among subscribers
+ * of a particular distributed object.
+ */
+public interface SpeakService extends InvocationService
+{
+ /**
+ * Issues a request to speak "on" the distributed object via which
+ * this speak service was provided.
+ *
+ * @param message the message to be spoken.
+ * @param mode the "mode" of the message. This is an opaque value that
+ * will be passed back down via the {@link ChatDirector} to the {@link
+ * ChatDisplay} implementations which can interpret it in an
+ * application specific manner. It's useful for differentiating
+ * between regular speech, emotes, etc.
+ */
+ function speak (client :Client, message :String, mode :int) :void;
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/client/TellListener.as b/src/as/com/threerings/crowd/chat/client/TellListener.as
new file mode 100644
index 000000000..72baca6c2
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/TellListener.as
@@ -0,0 +1,11 @@
+package com.threerings.crowd.chat.client {
+
+import com.threerings.util.long;
+
+import com.threerings.presents.client.InvocationListener
+
+public interface TellListener extends InvocationListener
+{
+ function tellSucceeded (idleTime :long, awayMessage :String) :void;
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/data/ChatCodes.as b/src/as/com/threerings/crowd/chat/data/ChatCodes.as
new file mode 100644
index 000000000..7294d39c9
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/data/ChatCodes.as
@@ -0,0 +1,91 @@
+//
+// $Id: ChatCodes.java 3725 2005-10-08 22:21:19Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.data {
+
+import com.threerings.presents.data.InvocationCodes;
+
+import com.threerings.crowd.data.BodyObject;
+
+import com.threerings.crowd.chat.client.ChatDirector;
+import com.threerings.crowd.chat.client.SpeakService;
+
+/**
+ * Contains codes used by the chat invocation services.
+ */
+public class ChatCodes extends InvocationCodes
+{
+ /** The message identifier for a chat notification message. */
+ public static const CHAT_NOTIFICATION :String = "chat";
+
+ /** The access control identifier for normal chat privileges. See
+ * {@link BodyObject#checkAccess}. */
+ public static const CHAT_ACCESS :String = "crowd.chat.chat";
+
+ /** The access control identifier for broadcast chat privileges. See
+ * {@link BodyObject#checkAccess}. */
+ public static const BROADCAST_ACCESS :String = "crowd.chat.broadcast";
+
+ /** The configuration key for idle time. */
+ public static const IDLE_TIME_KEY :String = "narya.chat.idle_time";
+
+ /** The default time after which a player is assumed idle. */
+ public static const DEFAULT_IDLE_TIME :Number = 3 * 60 * 1000;
+
+ /** The chat localtype code for chat messages delivered on the place
+ * object currently occupied by the client. This is the only type of
+ * chat message that will be delivered unless the chat director is
+ * explicitly provided with other chat message sources via {@link
+ * ChatDirector#addAuxiliarySource}. */
+ public static const PLACE_CHAT_TYPE :String = "placeChat";
+
+ /** The chat localtype for messages received on the user object. */
+ public static const USER_CHAT_TYPE :String = "userChat";
+
+ /** The default mode used by {@link SpeakService#speak} requests. */
+ public static const DEFAULT_MODE :int = 0;
+
+ /** A {@link SpeakService#speak} mode to indicate that the user is
+ * thinking what they're saying, or is it that they're saying what
+ * they're thinking? */
+ public static const THINK_MODE :int = 1;
+
+ /** A {@link SpeakService#speak} mode to indicate that a speak is
+ * actually an emote. */
+ public static const EMOTE_MODE :int = 2;
+
+ /** A {@link SpeakService#speak} mode to indicate that a speak is
+ * actually a shout. */
+ public static const SHOUT_MODE :int = 3;
+
+ /** A {@link SpeakService#speak} mode to indicate that a speak is
+ * actually a server-wide broadcast. */
+ public static const BROADCAST_MODE :int = 4;
+
+ /** An error code delivered when the user targeted for a tell
+ * notification is not online. */
+ public static const USER_NOT_ONLINE :String = "m.user_not_online";
+
+ /** An error code delivered when the user targeted for a tell
+ * notification is disconnected. */
+ public static const USER_DISCONNECTED :String = "m.user_disconnected";
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/data/ChatMessage.as b/src/as/com/threerings/crowd/chat/data/ChatMessage.as
new file mode 100644
index 000000000..3137d86ed
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/data/ChatMessage.as
@@ -0,0 +1,81 @@
+//
+// $Id: ChatMessage.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.data {
+
+import com.samskivert.util.StringUtil;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamable;
+
+/**
+ * The abstract base class of all the client-side ChatMessage objects.
+ */
+public /*abstract*/ class ChatMessage
+ implements Streamable
+{
+ /** The actual text of the message. */
+ public var message :String;
+
+ /** The bundle to use when translating this message. */
+ public var bundle :String;
+
+ /** The client side 'localtype' of this chat, set to the type
+ * registered with an auxiliary source in the ChatDirector. */
+ public var localtype :String;
+
+ /**
+ * Once this message reaches the client, the information contained within
+ * is changed around a bit.
+ */
+ public function setClientInfo (msg :String, localtype :String) :void
+ {
+ message = msg;
+ this.localtype = localtype;
+ bundle = null;
+ //timestamp = System.currentTimeMillis();
+ }
+
+ /**
+ * Generates a string representation of this instance.
+ */
+ public function toString () :String
+ {
+ return ClassUtil.shortClassName(this) +
+ " [message=" + message + ", bundle=" + bundle + "]";
+ }
+
+ // documentation inherited from interface Streamable
+ public function readObject (ins :ObjectInputStream) :void
+ {
+ message = ins.readField(String);
+ bundle = ins.readField(String);
+ }
+
+ // documentation inherited from interface Streamable
+ public function writeObject (out :ObjectOutputStream) :void
+ {
+ out.writeField(message);
+ out.writeField(bundle);
+ }
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/data/SystemMessage.as b/src/as/com/threerings/crowd/chat/data/SystemMessage.as
new file mode 100644
index 000000000..928f7f7bc
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/data/SystemMessage.as
@@ -0,0 +1,58 @@
+//
+// $Id: SystemMessage.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.data {
+
+/**
+ * A ChatMessage that represents a message that came from the server
+ * and did not result from direct user action.
+ */
+public class SystemMessage extends ChatMessage
+{
+ /** Attention level constant to indicate that this message is merely
+ * providing the user with information. */
+ public static const INFO :int = 0;
+
+ /** Attention level constant to indicate that this message is the
+ * result of a user action. */
+ public static const FEEDBACK :int = 1;
+
+ /** Attention level constant to indicate that some action is required. */
+ public static const ATTENTION :int = 2;
+
+ //----
+
+ /** The attention level of this message. */
+ public var attentionLevel :int;
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ attentionLevel = ins.readByte();
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeByte(attentionLevel);
+ }
+}
+}
diff --git a/src/as/com/threerings/crowd/chat/data/UserMessage.as b/src/as/com/threerings/crowd/chat/data/UserMessage.as
new file mode 100644
index 000000000..930141f00
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/data/UserMessage.as
@@ -0,0 +1,54 @@
+//
+// $Id: UserMessage.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.chat.data {
+
+import com.threerings.util.Name;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * A ChatMessage representing a message that came from another user.
+ */
+public class UserMessage extends ChatMessage
+{
+ /** The user that the message came from. */
+ public var speaker :Name;
+
+ /** The mode of the message. @see ChatCodes.DEFAULT_MODE */
+ public var mode :int;
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ speaker = (ins.readObject() as Name);
+ mode = ins.readByte();
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeObject(speaker);
+ out.writeByte(mode);
+ }
+}
+}
diff --git a/src/as/com/threerings/crowd/data/BodyObject.as b/src/as/com/threerings/crowd/data/BodyObject.as
new file mode 100644
index 000000000..039807ef6
--- /dev/null
+++ b/src/as/com/threerings/crowd/data/BodyObject.as
@@ -0,0 +1,203 @@
+//
+// $Id: BodyObject.java 3774 2005-12-03 03:05:06Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.data {
+
+import com.threerings.util.Byte;
+import com.threerings.util.Name;
+
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationCodes;
+
+import com.threerings.crowd.chat.data.ChatCodes;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * The basic user object class for Crowd users. Bodies have a username, a
+ * location and a status.
+ */
+public class BodyObject extends ClientObject
+{
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the username field. */
+ public static const USERNAME :String = "username";
+
+ /** The field name of the location field. */
+ public static const LOCATION :String = "location";
+
+ /** The field name of the status field. */
+ public static const STATUS :String = "status";
+
+ /** The field name of the awayMessage field. */
+ public static const AWAY_MESSAGE :String = "awayMessage";
+ // AUTO-GENERATED: FIELDS END
+
+ /**
+ * The username associated with this body object. This should not be used
+ * directly; in general {@link #getVisibleName} should be used unless you
+ * specifically know that you want the username.
+ */
+ public var username :Name;
+
+ /**
+ * The oid of the place currently occupied by this body or -1 if they
+ * currently occupy no place.
+ */
+ public var location :int = -1;
+
+ /**
+ * The user's current status ({@link OccupantInfo#ACTIVE}, etc.).
+ */
+ public var status :int;
+
+ /**
+ * If non-null, this contains a message to be auto-replied whenever
+ * another user delivers a tell message to this user.
+ */
+ public var awayMessage :String;
+
+// /**
+// * Checks whether or not this user has access to the specified
+// * feature. Currently used by the chat system to regulate access to
+// * chat broadcasts but also forms the basis of an extensible
+// * fine-grained permissions system.
+// *
+// * @return null if the user has access, a fully-qualified translatable
+// * message string indicating the reason for denial of access (or just
+// * {@link InvocationCodes#ACCESS_DENIED} if you don't want to be
+// * specific).
+// */
+// public String checkAccess (String feature, Object context)
+// {
+// // our default access control policy; how quaint
+// if (ChatCodes.BROADCAST_ACCESS.equals(feature)) {
+// return getTokens().isAdmin() ? null : ChatCodes.ACCESS_DENIED;
+// } else if (ChatCodes.CHAT_ACCESS.equals(feature)) {
+// return null;
+// } else {
+// return InvocationCodes.ACCESS_DENIED;
+// }
+// }
+//
+// /**
+// * Returns this user's access control tokens.
+// */
+// public TokenRing getTokens ()
+// {
+// return EMPTY_TOKENS;
+// }
+
+ /**
+ * Returns the name that should be displayed to other users and used for
+ * the chat system. The default is to use {@link #username}.
+ */
+ public function getVisibleName () :Name
+ {
+ return username;
+ }
+
+ public override function writeObject (out :ObjectOutputStream) :void
+ {
+ super.writeObject(out);
+ out.writeObject(username);
+ out.writeInt(location);
+ out.writeByte(status);
+ out.writeField(awayMessage);
+ }
+
+ public override function readObject (ins :ObjectInputStream) :void
+ {
+ super.readObject(ins);
+ username = (ins.readObject() as Name);
+ location = ins.readInt();
+ status = ins.readByte();
+ awayMessage = (ins.readField(String) as String);
+ }
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that the username 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 setUsername (value :Name) :void
+ {
+ Name ovalue = this.username;
+ requestAttributeChange(
+ USERNAME, value, ovalue);
+ this.username = value;
+ }
+
+ /**
+ * Requests that the location 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 setLocation (value :int) :void
+ {
+ int ovalue = this.location;
+ requestAttributeChange(
+ LOCATION, value, ovalue);
+ this.location = value;
+ }
+
+ /**
+ * Requests that the status 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 setStatus (value :int) :void
+ {
+ var ovalue :int = this.status;
+ requestAttributeChange(
+ STATUS, new Byte(value), new Byte(ovalue));
+ this.status = value;
+ }
+
+ /**
+ * Requests that the awayMessage 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 setAwayMessage (value :String) :void
+ {
+ var ovalue :String = this.awayMessage;
+ requestAttributeChange(
+ AWAY_MESSAGE, value, ovalue);
+ this.awayMessage = value;
+ }
+ // AUTO-GENERATED: METHODS END
+}
+}
diff --git a/src/as/com/threerings/crowd/data/LocationCodes.as b/src/as/com/threerings/crowd/data/LocationCodes.as
new file mode 100644
index 000000000..796e8e260
--- /dev/null
+++ b/src/as/com/threerings/crowd/data/LocationCodes.as
@@ -0,0 +1,44 @@
+//
+// $Id: LocationCodes.java 3098 2004-08-27 02:12:55Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.data {
+
+import com.threerings.presents.data.InvocationCodes;
+
+/**
+ * Contains codes used by the location invocation services.
+ */
+public class LocationCodes extends InvocationCodes
+{
+ /** An error code indicating that a place identified by a particular
+ * place id does not exist. Usually generated by a failed moveTo
+ * request. */
+ public static const NO_SUCH_PLACE :String = "m.no_such_place";
+
+ /** An error code sent when a user requests to move to a new place but
+ * they are in the middle of moving somewhere already. */
+ public static const MOVE_IN_PROGRESS :String = "m.move_in_progress";
+
+ /** An error code sent when a user requests to move to a place, but
+ * they are already in the requested place. */
+ public static const ALREADY_THERE :String = "m.already_there";
+}
+}
diff --git a/src/as/com/threerings/crowd/data/OccupantInfo.as b/src/as/com/threerings/crowd/data/OccupantInfo.as
new file mode 100644
index 000000000..0bf8a9c13
--- /dev/null
+++ b/src/as/com/threerings/crowd/data/OccupantInfo.as
@@ -0,0 +1,102 @@
+//
+// $Id: OccupantInfo.java 3774 2005-12-03 03:05:06Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.data {
+
+import com.threerings.util.Integer;
+import com.threerings.util.Name;
+
+import com.threerings.presents.dobj.DSetEntry;
+
+import com.threerings.crowd.data.BodyObject;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+
+/**
+ * The occupant info object contains all of the information about an
+ * occupant of a place that should be shared with other occupants of the
+ * place. These objects are stored in the place object itself and are
+ * updated when bodies enter and exit a place.
+ *
+ *
A system that builds upon the Crowd framework can extend this class to + * include extra information about their occupants. They will need to provide a + * derived {@link BodyObject} that creates and configures their occupant info + * in {@link BodyObject#createOccupantInfo}. + * + *
Note also that this class implements {@link Cloneable} which means + * that if derived classes add non-primitive attributes, they are + * responsible for adding the code to clone those attributes when a clone + * is requested. + */ +public class OccupantInfo + implements DSetEntry +{ + /** Constant value for {@link #status}. */ + public static const ACTIVE :int = 0; + + /** Constant value for {@link #status}. */ + public static const IDLE :int = 1; + + /** Constant value for {@link #status}. */ + public static const DISCONNECTED :int = 2; + + /** Maps status codes to human readable strings. */ + public static const X_STATUS :Array = { "active", "idle", "discon" }; + + /** The body object id of this occupant (and our entry key). */ + public var bodyOid :Integer; + + /** The username of this occupant. */ + public var username :Name; + + /** The status of this occupant. */ + public var status :int = ACTIVE; + + /** Access to the body object id as an int. */ + public function getBodyOid () :int + { + return bodyOid.value; + } + + // documentation inherited from interface DSetEntry + public function getKey () :Object + { + return bodyOid; + } + + // documentation inherited from superinterface Streamable + public function writeObject (out :ObjectOutputStream) :void + { + out.writeObject(bodyOid); + out.writeObject(username); + out.writeByte(status); + } + + // documentation inherited from superinterface Streamable + public function readObject (ins :ObjectInputStream) :void + { + bodyOid = (ins.readObject() as Integer); + username = (ins.readObject() as Name); + status = ins.readByte(); + } +} +} diff --git a/src/as/com/threerings/crowd/data/PlaceConfig.as b/src/as/com/threerings/crowd/data/PlaceConfig.as new file mode 100644 index 000000000..0212a14cc --- /dev/null +++ b/src/as/com/threerings/crowd/data/PlaceConfig.as @@ -0,0 +1,64 @@ +// +// $Id: PlaceConfig.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.crowd.data { + +import com.threerings.io.Streamable; +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +import com.threerings.crowd.client.PlaceController; + +/** + * The place config class encapsulates the configuration information for a + * particular type of place. The hierarchy of place config objects mimics + * the hierarchy of place managers and controllers. Both the place manager + * and place controller are provided with the place config object when the + * place is created. + * + *
The place config object is also the mechanism used to instantiate
+ * the appropriate place manager and controller. Every place must have an
+ * associated place config derived class that overrides {@link
+ * #getControllerClass} and {@link #getManagerClassName}, returning the
+ * appropriate place controller and manager class for that place.
+ */
+public interface PlaceConfig extends Streamable
+{
+ /**
+ * Returns the class that should be used to create a controller for
+ * this place. The controller class must derive from {@link
+ * PlaceController}.
+ */
+ public function getControllerClass () :Class;
+
+ /**
+ * Returns the name of the class that should be used to create a
+ * manager for this place. The manager class must derive from {@link
+ * com.threerings.crowd.server.PlaceManager}. Note: this
+ * method differs from {@link #getControllerClass} because we want to
+ * avoid compile time linkage of the place config object (which is
+ * used on the client) to server code. This allows a code optimizer
+ * (DashO Pro, for example) to remove the server code from the client,
+ * knowing that it is never used.
+ */
+// public function getManagerClassName () :String;
+}
+}
diff --git a/src/as/com/threerings/crowd/data/PlaceObject.as b/src/as/com/threerings/crowd/data/PlaceObject.as
new file mode 100644
index 000000000..b3bdd818f
--- /dev/null
+++ b/src/as/com/threerings/crowd/data/PlaceObject.as
@@ -0,0 +1,179 @@
+//
+// $Id: PlaceObject.java 3406 2005-03-15 02:12:03Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.crowd.data {
+
+import com.threerings.util.Iterator;
+import com.threerings.util.Name;
+
+import com.threerings.presents.dobj.DObject;
+import com.threerings.presents.dobj.DSet;
+import com.threerings.presents.dobj.DSetEntry;
+import com.threerings.presents.dobj.OidList;
+
+import com.threerings.crowd.Log;
+import com.threerings.crowd.chat.data.SpeakMarshaller;
+import com.threerings.crowd.chat.data.SpeakObject;
+
+/**
+ * A distributed object that contains information on a place that is
+ * occupied by bodies. This place might be a chat room, a game room, an
+ * island in a massively multiplayer piratical universe, anything that has
+ * occupants that might want to chat with one another.
+ */
+public class PlaceObject extends DObject
+{
+ // AUTO-GENERATED: FIELDS START
+ /** The field name of the occupants field. */
+ public static const OCCUPANTS :String = "occupants";
+
+ /** The field name of the occupantInfo field. */
+ public static const OCCUPANT_INFO :String = "occupantInfo";
+
+ /** The field name of the speakService field. */
+ public static const SPEAK_SERVICE :String = "speakService";
+ // AUTO-GENERATED: FIELDS END
+
+ /**
+ * Tracks the oid of the body objects of all of the occupants of this
+ * place.
+ */
+ public var occupants :OidList = new OidList();
+
+ /**
+ * Contains an info record (of type {@link OccupantInfo}) for each
+ * occupant that contains information about that occupant that needs
+ * to be known by everyone in the place. Note: Don't obtain
+ * occupant info records directly from this set when on the server,
+ * use PlaceManager.getOccupantInfo() instead (along with
+ * PlaceManager.updateOccupantInfo()) because it does
+ * some special processing to ensure that readers and updaters don't
+ * step on one another even if they make rapid fire changes to a
+ * user's occupant info.
+ */
+ public var occupantInfo :DSet = new DSet();
+
+ /** Used to generate speak requests on this place object. */
+ public var speakService :SpeakMarshaller;
+
+ /**
+ * Looks up a user's occupant info by name.
+ *
+ * @return the occupant info record for the named user or null if no
+ * user in the room has that username.
+ */
+ public function getOccupantInfo (username :Name) :OccupantInfo
+ {
+ var itr :Iterator = occupantInfo.iterator();
+ while (itr.hasNext()) {
+ var info :OccupantInfo = (itr.next() as OccupantInfo);
+ if (info.username.equals(username)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ // AUTO-GENERATED: METHODS START
+ /**
+ * Requests that oid be added to the occupants
+ * oid list. The list will not change until the event is actually
+ * propagated through the system.
+ */
+ public function addToOccupants (oid :int) :void
+ {
+ requestOidAdd(OCCUPANTS, oid);
+ }
+
+ /**
+ * Requests that oid be removed from the
+ * occupants oid list. The list will not change until the
+ * event is actually propagated through the system.
+ */
+ public function removeFromOccupants (oid :int) :void
+ {
+ requestOidRemove(OCCUPANTS, oid);
+ }
+
+ /**
+ * Requests that the specified entry be added to the
+ * occupantInfo set. The set will not change until the event is
+ * actually propagated through the system.
+ */
+ public function addToOccupantInfo (elem :DSetEntry) :void
+ {
+ requestEntryAdd(OCCUPANT_INFO, occupantInfo, elem);
+ }
+
+ /**
+ * Requests that the entry matching the supplied key be removed from
+ * the occupantInfo set. The set will not change until the
+ * event is actually propagated through the system.
+ */
+ public function removeFromOccupantInfo (key :Object) :void
+ {
+ requestEntryRemove(OCCUPANT_INFO, occupantInfo, key);
+ }
+
+ /**
+ * Requests that the specified entry be updated in the
+ * occupantInfo set. The set will not change until the event is
+ * actually propagated through the system.
+ */
+ public function updateOccupantInfo (elem :DSetEntry) :void
+ {
+ requestEntryUpdate(OCCUPANT_INFO, occupantInfo, elem);
+ }
+
+ /**
+ * Requests that the occupantInfo field be set to the
+ * specified value. Generally one only adds, updates and removes
+ * entries of a distributed set, but certain situations call for a
+ * complete replacement of the set 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 setOccupantInfo (value :DSet) :void
+ {
+ requestAttributeChange(OCCUPANT_INFO, value, this.occupantInfo);
+ this.occupantInfo = (value == null) ? null : (DSet)value.clone();
+ }
+
+ /**
+ * Requests that the speakService 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 setSpeakService (value :SpeakMarshaller) :void
+ {
+ var ovalue :SpeakMarshaller = this.speakService;
+ requestAttributeChange(
+ SPEAK_SERVICE, value, ovalue);
+ this.speakService = value;
+ }
+ // AUTO-GENERATED: METHODS END
+}
+}
diff --git a/src/as/com/threerings/io/Streamer.as b/src/as/com/threerings/io/Streamer.as
index e151236ba..34d2951d7 100644
--- a/src/as/com/threerings/io/Streamer.as
+++ b/src/as/com/threerings/io/Streamer.as
@@ -7,10 +7,13 @@ import flash.util.ByteArray;
import com.threerings.util.SimpleMap;
import com.threerings.io.streamers.ArrayStreamer;
+import com.threerings.io.streamers.ByteyStreamer;
import com.threerings.io.streamers.ByteArrayStreamer;
-import com.threerings.io.streamers.IntStreamer;
+import com.threerings.io.streamers.FloatStreamer;
+import com.threerings.io.streamers.IntegerStreamer;
import com.threerings.io.streamers.NumberStreamer;
import com.threerings.io.streamers.ObjectArrayStreamer;
+import com.threerings.io.streamers.ShortStreamer;
import com.threerings.io.streamers.StringStreamer;
public class Streamer
@@ -125,10 +128,13 @@ public class Streamer
if (_streamers == null) {
_streamers = [
new StringStreamer(),
- new IntStreamer(),
new NumberStreamer(),
new ObjectArrayStreamer(),
- new ByteArrayStreamer()
+ new ByteArrayStreamer(),
+ new ByteStreamer(),
+ new ShortStreamer(),
+ new IntegerStreamer(),
+ new FloatStreamer()
];
}
}
diff --git a/src/as/com/threerings/io/streamers/IntStreamer.as b/src/as/com/threerings/io/streamers/ByteStreamer.as
similarity index 59%
rename from src/as/com/threerings/io/streamers/IntStreamer.as
rename to src/as/com/threerings/io/streamers/ByteStreamer.as
index a745ff9da..cab7f0a28 100644
--- a/src/as/com/threerings/io/streamers/IntStreamer.as
+++ b/src/as/com/threerings/io/streamers/ByteStreamer.as
@@ -1,35 +1,37 @@
package com.threerings.io.streamers {
+import com.threerings.util.Byte;
+
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Streamer;
/**
- * A Streamer for int objects.
+ * A Streamer for Byte objects.
*/
-public class IntStreamer extends Streamer
+public class ByteStreamer extends Streamer
{
- public function IntStreamer ()
+ public function ByteStreamer ()
{
- super(int, "java.lang.Integer");
+ super(Byte, "java.lang.Byte");
}
public override function createObject (ins :ObjectInputStream) :Object
{
- return ins.readInt();
+ return new Byte(ins.readByte());
}
public override function writeObject (obj :Object, out :ObjectOutputStream)
:void
{
- var i :int = (obj as int);
- out.writeInt(i);
+ var byte :Byte = (obj as Byte);
+ out.writeByte(byte.value);
}
public override function readObject (obj :Object, ins :ObjectInputStream)
:void
{
- // nothing here, the int is fully read in createObject()
+ // unneeded, done in createObject
}
}
}
diff --git a/src/as/com/threerings/io/streamers/FloatStreamer.as b/src/as/com/threerings/io/streamers/FloatStreamer.as
new file mode 100644
index 000000000..67d6cffc1
--- /dev/null
+++ b/src/as/com/threerings/io/streamers/FloatStreamer.as
@@ -0,0 +1,37 @@
+package com.threerings.io.streamers {
+
+import com.threerings.util.Float;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamer;
+
+/**
+ * A Streamer for Float objects.
+ */
+public class FloatStreamer extends Streamer
+{
+ public function FloatStreamer ()
+ {
+ super(Float, "java.lang.Float");
+ }
+
+ public override function createObject (ins :ObjectInputStream) :Object
+ {
+ return new Float(ins.readFloat());
+ }
+
+ public override function writeObject (obj :Object, out :ObjectOutputStream)
+ :void
+ {
+ var float :Float = (obj as Float);
+ out.writeFloat(float.value);
+ }
+
+ public override function readObject (obj :Object, ins :ObjectInputStream)
+ :void
+ {
+ // unneeded, done in createObject
+ }
+}
+}
diff --git a/src/as/com/threerings/io/streamers/IntegerStreamer.as b/src/as/com/threerings/io/streamers/IntegerStreamer.as
new file mode 100644
index 000000000..7fb6fd0b4
--- /dev/null
+++ b/src/as/com/threerings/io/streamers/IntegerStreamer.as
@@ -0,0 +1,37 @@
+package com.threerings.io.streamers {
+
+import com.threerings.util.Integer;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamer;
+
+/**
+ * A Streamer for Integer objects.
+ */
+public class IntegerStreamer extends Streamer
+{
+ public function IntegerStreamer ()
+ {
+ super(Integer, "java.lang.Integer");
+ }
+
+ public override function createObject (ins :ObjectInputStream) :Object
+ {
+ return new Integer(ins.readInt());
+ }
+
+ public override function writeObject (obj :Object, out :ObjectOutputStream)
+ :void
+ {
+ var inty :Integer = (obj as Integer);
+ out.writeInt(inty.value);
+ }
+
+ public override function readObject (obj :Object, ins :ObjectInputStream)
+ :void
+ {
+ // unneeded, done in createObject
+ }
+}
+}
diff --git a/src/as/com/threerings/io/streamers/ShortStreamer.as b/src/as/com/threerings/io/streamers/ShortStreamer.as
new file mode 100644
index 000000000..e7e0dba64
--- /dev/null
+++ b/src/as/com/threerings/io/streamers/ShortStreamer.as
@@ -0,0 +1,37 @@
+package com.threerings.io.streamers {
+
+import com.threerings.util.Short;
+
+import com.threerings.io.ObjectInputStream;
+import com.threerings.io.ObjectOutputStream;
+import com.threerings.io.Streamer;
+
+/**
+ * A Streamer for Short objects.
+ */
+public class ShortStreamer extends Streamer
+{
+ public function ShortStreamer ()
+ {
+ super(Short, "java.lang.Short");
+ }
+
+ public override function createObject (ins :ObjectInputStream) :Object
+ {
+ return new Short(ins.readShort());
+ }
+
+ public override function writeObject (obj :Object, out :ObjectOutputStream)
+ :void
+ {
+ var short :Short = (obj as Short);
+ out.writeShort(short.value);
+ }
+
+ public override function readObject (obj :Object, ins :ObjectInputStream)
+ :void
+ {
+ // unneeded, done in createObject
+ }
+}
+}
diff --git a/src/as/com/threerings/presents/client/TestClient.as b/src/as/com/threerings/presents/client/TestClient.as
index 5acc33338..d6c0b482b 100644
--- a/src/as/com/threerings/presents/client/TestClient.as
+++ b/src/as/com/threerings/presents/client/TestClient.as
@@ -5,6 +5,8 @@ import flash.util.describeType;
import com.threerings.util.Name;
import com.threerings.presents.Log;
import com.threerings.presents.data.TimeBaseMarshaller;
+import com.threerings.presents.dobj.DSet;
+import com.threerings.presents.dobj.QSet;
import com.threerings.presents.net.UsernamePasswordCreds;
public class TestClient extends Client
@@ -16,11 +18,11 @@ public class TestClient extends Client
logon();
var g1 :String = null;
- var g2 :String = String(g1);
- Log.debug("foo: " + (g1 === g2) + ", *" + g2 + "*, " + g2.length);
+ var g2 :String = (com.threerings.util.Util.cast(g1, String) as String);
+ Log.debug("foo: " + (g1 === g2) + ", *" + g2 + "*, "); // + g2.length);
- var duckie :Duck = new Goose();
- duckie.screw();
+ var ob :Object = "this is a string";
+ Log.debug("part of an object: " + ob.substring(1));
var arr :Array = new Array();
arr[0] = "Florp";
diff --git a/src/as/com/threerings/presents/dobj/DSet.as b/src/as/com/threerings/presents/dobj/DSet.as
index 9a6be86fb..e55a9e3e8 100644
--- a/src/as/com/threerings/presents/dobj/DSet.as
+++ b/src/as/com/threerings/presents/dobj/DSet.as
@@ -2,11 +2,11 @@ package com.threerings.presents.dobj {
import flash.util.StringBuilder;
-import mx.collections.IViewCursor;
-
import mx.utils.ObjectUtil;
import com.threerings.util.Equalable;
+import com.threerings.util.Iterator;
+import com.threerings.util.ArrayIterator;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
@@ -86,47 +86,9 @@ public class DSet
* made to the set). It should not be kept around as it can quickly
* become out of date.
*/
- public function getCursor () :IViewCursor
+ public function iterator () :Iterator
{
- return null; // jesus, what a pain in the ass to make our
- // own IViewCursor since we can't have inner classes
-
-// // the crazy sanity checks
-// if (_size < 0 ||_size > _entries.length ||
-// (_size > 0 && _entries[_size-1] == null)) {
-// Log.warning("DSet in a bad way [size=" + _size +
-// ", entries=" + StringUtil.toString(_entries) + "].");
-// Thread.dumpStack();
-// }
-//
-// return new Iterator() {
-// public boolean hasNext () {
-// checkComodification();
-// return (_index < _size);
-// }
-// public Object next () {
-// checkComodification();
-// return _entries[_index++];
-// }
-// public void remove () {
-// throw new UnsupportedOperationException();
-// }
-// protected void checkComodification () {
-// if (_modCount != _expectedModCount) {
-// throw new ConcurrentModificationException();
-// }
-// if (_ssize != _size) {
-// Log.warning("Size changed during iteration " +
-// "[ssize=" + _ssize + ", nsize=" + _size +
-// ", entsries=" + StringUtil.toString(_entries) +
-// "].");
-// Thread.dumpStack();
-// }
-// }
-// protected int _index = 0;
-// protected int _ssize = _size;
-// protected int _expectedModCount = _modCount;
-// };
+ return new ArrayIterator(_entries);
}
/**
diff --git a/src/as/com/threerings/util/ArrayIterator.as b/src/as/com/threerings/util/ArrayIterator.as
new file mode 100644
index 000000000..e8695cf6c
--- /dev/null
+++ b/src/as/com/threerings/util/ArrayIterator.as
@@ -0,0 +1,37 @@
+package com.threerings.util {
+
+/**
+ * Provides a generic iterator for an Array.
+ * No co-modification checking is done.
+ */
+public class ArrayIterator
+ implements Iterator
+{
+ /**
+ * Create an ArrayIterator.
+ */
+ public function ArrayIterator (arr :Array)
+ {
+ _arr = arr;
+ _index = 0;
+ }
+
+ // documentation inherited from interface Iterator
+ public function hasNext () :Boolean
+ {
+ return (_index < _arr.length);
+ }
+
+ // documentation inherited from interface Iterator
+ public function next () :Object
+ {
+ return _arr[_index++];
+ }
+
+ /** The array we're iterating over. */
+ protected var _arr :Array;
+
+ /** The current index. */
+ protected var _index :int;
+}
+}
diff --git a/src/as/com/threerings/util/Byte.as b/src/as/com/threerings/util/Byte.as
new file mode 100644
index 000000000..7faaae9e7
--- /dev/null
+++ b/src/as/com/threerings/util/Byte.as
@@ -0,0 +1,22 @@
+package com.threerings.util {
+
+/**
+ * Equivalent to java.lang.Byte.
+ */
+public class Byte
+ implements Equalable
+{
+ public var value :int;
+
+ public function Byte (value :int)
+ {
+ this.value = value;
+ }
+
+ // documentation inherited from interface Equalable
+ public function equals (other :Object) :Boolean
+ {
+ return (other is Byte) && (value === (other as Byte).value);
+ }
+}
+}
diff --git a/src/as/com/threerings/util/Float.as b/src/as/com/threerings/util/Float.as
new file mode 100644
index 000000000..37263f79b
--- /dev/null
+++ b/src/as/com/threerings/util/Float.as
@@ -0,0 +1,22 @@
+package com.threerings.util {
+
+/**
+ * Equivalent to java.lang.Float.
+ */
+public class Float
+ implements Equalable
+{
+ public var value :Number;
+
+ public function Float (value :Number)
+ {
+ this.value = value;
+ }
+
+ // documentation inherited from interface Equalable
+ public function equals (other :Object) :Boolean
+ {
+ return (other is Float) && (value === (other as Float).value);
+ }
+}
+}
diff --git a/src/as/com/threerings/util/Integer.as b/src/as/com/threerings/util/Integer.as
new file mode 100644
index 000000000..52fad49d5
--- /dev/null
+++ b/src/as/com/threerings/util/Integer.as
@@ -0,0 +1,22 @@
+package com.threerings.util {
+
+/**
+ * Equivalent to java.lang.Integer.
+ */
+public class Integer
+ implements Equalable
+{
+ public var value :int;
+
+ public function Integer (value :int)
+ {
+ this.value = value;
+ }
+
+ // documentation inherited from interface Equalable
+ public function equals (other :Object) :Boolean
+ {
+ return (other is Integer) && (value === (other as Integer).value);
+ }
+}
+}
diff --git a/src/as/com/threerings/util/Iterator.as b/src/as/com/threerings/util/Iterator.as
new file mode 100644
index 000000000..e843fabe5
--- /dev/null
+++ b/src/as/com/threerings/util/Iterator.as
@@ -0,0 +1,23 @@
+package com.threerings.util {
+
+/**
+ * Java has Iterator, ActionScript has IViewCursor.
+ * The problem is, IViewCursor defines 14 methods and 5 read-only properties.
+ * That is a serious PITA to write for every collection that might desire
+ * iteration. This provides a simpler alternative.
+ */
+public interface Iterator
+{
+ /**
+ * Is there another element available?
+ */
+ function hasNext () :Boolean;
+
+ /**
+ * Returns the next element.
+ */
+ function next () :Object;
+
+ // TODO: remove() ?
+}
+}
diff --git a/src/as/com/threerings/util/Short.as b/src/as/com/threerings/util/Short.as
new file mode 100644
index 000000000..051862a55
--- /dev/null
+++ b/src/as/com/threerings/util/Short.as
@@ -0,0 +1,22 @@
+package com.threerings.util {
+
+/**
+ * Equivalent to java.lang.Short.
+ */
+public class Short
+ implements Equalable
+{
+ public var value :int;
+
+ public function Short (value :int)
+ {
+ this.value = value;
+ }
+
+ // documentation inherited from interface Equalable
+ public function equals (other :Object) :Boolean
+ {
+ return (other is Short) && (value === (other as Short).value);
+ }
+}
+}
diff --git a/src/as/com/threerings/util/Util.as b/src/as/com/threerings/util/Util.as
index 9805916f0..3b89ed318 100644
--- a/src/as/com/threerings/util/Util.as
+++ b/src/as/com/threerings/util/Util.as
@@ -15,6 +15,15 @@ public class Util
return buf.toString();
}
+ public static function cast (obj :Object, clazz :Class) :Object
+ {
+ if (obj == null || obj is clazz) {
+ return obj;
+ } else {
+ throw new Error("wah");
+ }
+ }
+
private static const HEX :Array = new Array("0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
}