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"); }