diff --git a/src/as/com/threerings/README.txt b/src/as/com/threerings/README.txt
index 8a500fe86..cf0e0d5c8 100644
--- a/src/as/com/threerings/README.txt
+++ b/src/as/com/threerings/README.txt
@@ -247,3 +247,6 @@ ActionScript
staticInit(); // will be placed inside the real static initializer
}
+
+- Unlike in Java, most operators are overloaded for strings:
+ if (str1 > str2) { // compares asciibetically
diff --git a/src/as/com/threerings/crowd/chat/client/ChatDirector.as b/src/as/com/threerings/crowd/chat/client/ChatDirector.as
index 35a79ec9a..5930b58bf 100644
--- a/src/as/com/threerings/crowd/chat/client/ChatDirector.as
+++ b/src/as/com/threerings/crowd/chat/client/ChatDirector.as
@@ -20,6 +20,10 @@
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.crowd.chat.client {
+
+import mx.collections.ArrayCollection;
+
+
// TODO: this class is in progress
//
//
@@ -107,17 +111,17 @@ public class ChatDirector extends BasicDirector
// 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;
}
var msg :MessageBundle = _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());
+ // register our default chat handlers
+ registerCommandHandler(msg, "help", new HelpHandler(this));
+ registerCommandHandler(msg, "clear", new ClearHandler(this));
+ registerCommandHandler(msg, "speak", new SpeakHandler(this));
+ registerCommandHandler(msg, "emote", new EmoteHandler(this));
+ registerCommandHandler(msg, "think", new ThinkHandler(this));
}
/**
@@ -125,18 +129,21 @@ public class ChatDirector extends BasicDirector
* subsequently be notified of incoming chat messages as well as tell
* responses.
*/
- public void addChatDisplay (ChatDisplay display)
+ public function addChatDisplay (display :ChatDisplay) :void
{
- _displays.add(display);
+ _displays.addItem(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)
+ public function removeChatDisplay (display :ChatDisplay) :void
{
- _displays.remove(display);
+ var idx :int = _displays.getItemIndex(display);
+ if (idx != -1) {
+ _displays.removeItemAt(idx);
+ }
}
/**
@@ -144,45 +151,45 @@ public class ChatDirector extends BasicDirector
* 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);
- }
+// 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);
- }
+// 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());
- }
+// 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);
- }
+// 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;
- }
+// public void setChatterValidator (ChatterValidator validator)
+// {
+// _chatterValidator = validator;
+// }
/**
* Registers a chat command handler.
@@ -195,14 +202,14 @@ public class ChatDirector extends BasicDirector
* as /tell).
* @param handler the chat command handler itself.
*/
- public void registerCommandHandler (
- MessageBundle msg, String command, CommandHandler handler)
+ public function registerCommandHandler (
+ msg :MessageBundle, command :String, handler :CommandHandler) :void
{
- String key = "c." + command;
+ var key :String = "c." + command;
if (msg.exists(key)) {
- StringTokenizer st = new StringTokenizer(msg.get(key));
- while (st.hasMoreTokens()) {
- _handlers.put(st.nextToken(), handler);
+ var tokens :Array = msg.get(key).split(/\s+/);
+ for (var ii :int = 0; ii < tokens.length; ii++) {
+ _handlers.put(tokens[ii], handler);
}
} else {
// fall back to just using the English command
@@ -213,39 +220,36 @@ public class ChatDirector extends BasicDirector
/**
* Return the current size of the history.
*/
- public int getCommandHistorySize ()
+ public function getCommandHistorySize () :int
{
- return _history.size();
+ return _history.length;
}
/**
* Get the chat history entry at the specified index,
* with 0 being the oldest.
*/
- public String getCommandHistory (int index)
+ public function getCommandHistory (index :int) :String
{
- return (String)_history.get(index);
+ return (_history[index] as String);
}
/**
* Clear the chat command history.
*/
- public void clearCommandHistory ()
+ public function clearCommandHistory () :void
{
- _history.clear();
+ _history.length = 0;
}
/**
* Requests that all chat displays clear their contents.
*/
- public void clearDisplays ()
+ public function clearDisplays () :void
{
- _displays.apply(new ObserverList.ObserverOp() {
- public boolean apply (Object observer) {
- ((ChatDisplay)observer).clear();
- return true;
- }
- });
+ for (var ii :int = 0; ii < _displays.length; ii++) {
+ (_displays.getItemAt(ii) as ChatDisplay).clear();
+ }
}
/**
@@ -255,7 +259,7 @@ public class ChatDirector extends BasicDirector
* 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)
+ public function displayInfo (bundle :String, message :String) :void
{
displaySystem(bundle, message, SystemMessage.INFO, PLACE_CHAT_TYPE);
}
@@ -266,7 +270,8 @@ public class ChatDirector extends BasicDirector
* 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)
+ public function displayInfo (
+ bundle :String, message :String, localtype :String) :void
{
displaySystem(bundle, message, SystemMessage.INFO, localtype);
}
@@ -278,7 +283,7 @@ public class ChatDirector extends BasicDirector
* 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)
+ public function displayFeedback (bundle :String, message :String) :void
{
displaySystem(
bundle, message, SystemMessage.FEEDBACK, PLACE_CHAT_TYPE);
@@ -291,7 +296,7 @@ public class ChatDirector extends BasicDirector
* 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)
+ public function displayAttention (bundle :String, message :String) :void
{
displaySystem(
bundle, message, SystemMessage.ATTENTION, PLACE_CHAT_TYPE);
@@ -300,10 +305,11 @@ public class ChatDirector extends BasicDirector
/**
* Dispatches the provided message to our chat displays.
*/
- public void dispatchMessage (ChatMessage message)
+ public function dispatchMessage (message :ChatMessage) :void
{
- _displayMessageOp.setMessage(message);
- _displays.apply(_displayMessageOp);
+ for (var ii :int = 0; ii < _displays.length; ii++) {
+ (_displays.getItemAt(ii) as ChatDisplay).displayMessage(message);
+ }
}
/**
@@ -320,35 +326,33 @@ public class ChatDirector extends BasicDirector
* and sent correctly, a translatable error string if there was some
* problem.
*/
- public String requestChat (
- SpeakService speakSvc, String text, boolean record)
+ public function requestChat (
+ speakSvc :SpeakService, text :String, record :Boolean) :String
{
- if (text.startsWith("/")) {
+ if (text.indexOf("/") == 0) {
// 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(" ");
+ var command :String = text.substring(1).toLowerCase();
+ var hist :Array = new Array();
+ var args :String = "";
+ var sidx :int = text.indexOf(" ");
if (sidx != -1) {
command = text.substring(1, sidx).toLowerCase();
- args = text.substring(sidx+1).trim();
+ args = StringUtil.trim(text.substring(sidx+1));
}
- HashMap possibleCommands = getCommandHandlers(command);
+ var possibleCommands :SimpleMap = getCommandHandlers(command);
switch (possibleCommands.size()) {
case 0:
- StringTokenizer tok = new StringTokenizer(text);
return MessageBundle.tcompose(
- "m.unknown_command", tok.nextToken());
+ "m.unknown_command", text.split(/\s/)[0]);
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)) {
+ var cmdName :String = possibleCommands.keys()[0];
+ var cmd :CommandHandler =
+ (possibleCommands.get(cmdName) as CommandHandler);
+ var result :String =
+ cmd.handleCommand(speakSvc, cmdName, args, hist);
+ if (result != ChatCodes.SUCCESS) {
return result;
}
@@ -360,23 +364,18 @@ public class ChatDirector extends BasicDirector
// 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;
- }
+ var cmds :Array = possibleCommands.keys();
+ var alternativeCommands :String = "/" + cmds.join(", /");
return MessageBundle.tcompose(
"m.unspecific_command", alternativeCommands);
}
}
// if not a command then just speak
- String message = text.trim();
+ var message :String = StringUtil.trim(text);
if (StringUtil.isBlank(message)) {
// report silent failure for now
return ChatCodes.SUCCESS;
@@ -399,8 +398,8 @@ public class ChatDirector extends BasicDirector
* ChatDisplay} implementations that eventually display this speak
* message.
*/
- public void requestSpeak (
- SpeakService speakService, String message, byte mode)
+ public function requestSpeak (
+ speakService :SpeakService, message :String, mode :int) :void
{
if (speakService == null) {
if (_place == null) {
@@ -424,7 +423,7 @@ public class ChatDirector extends BasicDirector
*
* @param message the contents of the message.
*/
- public void requestBroadcast (String message)
+ public function requestBroadcast (message :String) :void
{
message = filter(message, null, true);
if (message == null) {
@@ -433,14 +432,15 @@ public class ChatDirector extends BasicDirector
return;
}
- _cservice.broadcast(
- _ctx.getClient(), message, new ChatService.InvocationListener() {
- public void requestFailed (String reason) {
- reason = MessageBundle.compose(
- "m.broadcast_failed", reason);
- displayFeedback(_bundle, reason);
- }
- });
+ _cservice.broadcast(_ctx.getClient(), message,
+ new InvocationAdapter(broadcastFailed));
+ }
+
+ // Called above, when we fail to broadcast
+ private function broadcastFailed (reason :String) :void
+ {
+ reason = MessageBundle.compose("m.broadcast_failed", reason);
+ displayFeedback(_bundle, reason);
}
/**
@@ -453,11 +453,11 @@ public class ChatDirector extends BasicDirector
* @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)
+ public function requestTell (
+ target :Name, msg :String, rl :ResultListener) :void
{
// make sure they can say what they want to say
- final String message = filter(msg, target, true);
+ var message :String = filter(msg, target, true);
if (message == null) {
if (rl != null) {
rl.requestFailed(null);
@@ -468,50 +468,64 @@ public class ChatDirector extends BasicDirector
// 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);
+ _cservice.tell(_ctx.getClient(), target, message,
+ new TellAdapter(tellFailed, tellSucceeded, [target, message, rl]));
+ }
+
+ private function tellFailed (reason :String, args :Array) :void
+ {
+ var msg :String = MessageBundle.compose(
+ "m.tell_failed", MessageBundle.taint(args[0]), reason);
+ displayFeedback(_bundle, msg);
+ var rl:ResultListener = (args[2] as ResultListener);
+ if (rl != null) {
+ rl.requestFailed(null);
+ }
+ }
+
+ private function tellSucceeded (
+ idleTime :long, awayMessage :String, args :Array) :void
+ {
+ var target :Name = (args[0] as Target);
+ var message :String = (args[1] as String);
+ var rl:ResultListener = (args[2] as ResultListener);
+ var feedback :String = xlate(_bundle, MessageBundle.tcompose(
+ "m.told_format", target, message));
+ dispatchMessage(new TellFeedbackMessage(feedback));
+ addChatter(target);
+ if (rl != null) {
+ rl.requestCompleted(target);
+ }
+
+ // if they have an away message, report that
+ if (awayMessage != null) {
+ awayMessage = filter(awayMessage, target, false);
+ if (awayMessage != null) {
+ var msg :String = MessageBundle.tcompose(
+ "m.recipient_afk", target, awayMessage);
+ displayFeedback(_bundle, msg);
+ }
+ }
+
+ // if they are idle, report that
+ if (idletime > 0) {
+ // adjust by the time it took them to become idle
+ idletime += _ctx.getConfig().getValue(
+ IDLE_TIME_KEY, DEFAULT_IDLE_TIME);
+ var msg :String = MessageBundle.compose(
+ "m.recipient_idle", MessageBundle.taint(target),
+ TimeUtil.getTimeOrderString(idletime, TimeUtil.MINUTE));
+ displayFeedback(_bundle, msg);
+ }
}
/**
@@ -560,11 +574,14 @@ public class ChatDirector extends BasicDirector
/**
* Run a message through all the currently registered filters.
*/
- public String filter (String msg, Name otherUser, boolean outgoing)
+ public function filter (
+ msg :String, otherUser :Name, outgoing :Boolean) :String
{
- _filterMessageOp.setMessage(msg, otherUser, outgoing);
- _filters.apply(_filterMessageOp);
- return _filterMessageOp.getMessage();
+ // TODO
+ return msg;
+// _filterMessageOp.setMessage(msg, otherUser, outgoing);
+// _filters.apply(_filterMessageOp);
+// return _filterMessageOp.getMessage();
}
/**
@@ -1046,64 +1063,60 @@ public class ChatDirector extends BasicDirector
}
/** Our active chat context. */
- protected CrowdContext _ctx;
+ protected var _ctx :CrowdContext;
/** Provides access to chat-related server-side services. */
- protected ChatService _cservice;
+ protected var _cservice :ChatService;
/** The message manager. */
- protected MessageManager _msgmgr;
+ protected var _msgmgr :MessageManager;
/** The bundle to use for our own internal messages. */
- protected String _bundle;
+ protected var _bundle :String;
/** The place object that we currently occupy. */
- protected PlaceObject _place;
+ protected var _place :PlaceObject;
/** The client object that we're listening to for tells. */
- protected ClientObject _clobj;
+ protected var _clobj :ClientObject;
/** A list of registered chat displays. */
- protected ObserverList _displays =
- new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
+ protected var _displays :ArrayCollection = new ArrayCollection();
/** A list of registered chat filters. */
- protected ObserverList _filters =
- new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
+// 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();
+ protected var _auxes :SimpleMap = new SimpleMap();
- /** 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);
+// /** Validator of who may be added to the chatters list. */
+// protected var _chatterValidator :ChatterValidator;
+//
+// /** Usernames of users we've recently chatted with. */
+// protected var _chatters :LinkedList = 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();
+ protected static const _handlers :SimpleMap = new SimpleMap();
/** A history of chat commands. */
- protected static ArrayList _history = new ArrayList();
+ protected static const _history :Array = new Array();
/** Used by {@link #nextRequestId}. */
- protected int _requestId;
+ protected var _requestId :int;
- /** Operation used to filter chat messages. */
- protected FilterMessageOp _filterMessageOp = new FilterMessageOp();
-
- /** Operation used to display chat messages. */
- protected DisplayMessageOp _displayMessageOp = new DisplayMessageOp();
+// /** Operation used to filter chat messages. */
+// protected FilterMessageOp _filterMessageOp = new FilterMessageOp();
/** The maximum number of chatter usernames to track. */
- protected static final int MAX_CHATTERS = 6;
+ protected static const MAX_CHATTERS :int = 6;
/** The maximum number of commands to keep in the chat history. */
- protected static final int MAX_COMMAND_HISTORY = 10;
+ protected static const MAX_COMMAND_HISTORY :int = 10;
}
}
diff --git a/src/as/com/threerings/crowd/chat/client/TellAdapter.as b/src/as/com/threerings/crowd/chat/client/TellAdapter.as
new file mode 100644
index 000000000..97d00a02c
--- /dev/null
+++ b/src/as/com/threerings/crowd/chat/client/TellAdapter.as
@@ -0,0 +1,26 @@
+package com.threerings.crowd.chat.client {
+
+import com.threerings.util.long;
+
+import com.threerings.presents.client.InvocationAdapter;
+
+public class TellAdapter extends InvocationAdapter
+ implements TellListener
+{
+ public function TellAdapter (
+ failedFunc :Function, successFunc :Function, args :Array = null)
+ {
+ super(failedFunc, args);
+ _successFunc = successFunc;
+ }
+
+ // documentation inherited from interface TellListener
+ public function tellSucceeded (idleTime :long, awayMsg :String)
+ {
+ _successFunc(idleTime, awayMsg, args);
+ }
+
+ /** The method to call on success. */
+ protected var _successFunc :Function;
+}
+}
diff --git a/src/as/com/threerings/crowd/util/CrowdContext.as b/src/as/com/threerings/crowd/util/CrowdContext.as
new file mode 100644
index 000000000..4f1b3886d
--- /dev/null
+++ b/src/as/com/threerings/crowd/util/CrowdContext.as
@@ -0,0 +1,76 @@
+//
+// $Id: CrowdContext.java 3237 2004-11-25 00:21:46Z 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.util {
+
+import com.threerings.presents.util.PresentsContext;
+import com.threerings.crowd.client.LocationDirector;
+import com.threerings.crowd.client.OccupantDirector;
+import com.threerings.crowd.client.PlaceView;
+
+import com.threerings.crowd.chat.client.ChatDirector;
+
+/**
+ * The crowd context provides access to the various managers, etc. that
+ * are needed by the crowd client code.
+ */
+public interface CrowdContext extends PresentsContext
+{
+ /**
+ * Returns a reference to the location director.
+ */
+ function getLocationDirector () :LocationDirector;
+
+ /**
+ * Returns a reference to the occupant director.
+ */
+ function getOccupantDirector () :OccupantDirector;
+
+ /**
+ * Provides access to the chat director.
+ */
+ function getChatDirector () :ChatDirector;
+
+ /**
+ * When the client enters a new place, the location director creates a
+ * place controller which then creates a place view to visualize the
+ * place for the user. The place view created by the place controller
+ * will be passed to this function to actually display it in whatever
+ * user interface is provided for the user. We don't require any
+ * particular user interface toolkit, so it is expected that the place
+ * view implementation will coordinate with the client implementation
+ * so that the client can display the view provided by the place
+ * controller.
+ *
+ *
Though the place view is created before we enter the place, it + * won't be displayed (via a call to this function) until we have + * fully entered the place and are ready for user interaction. + */ + function setPlaceView (view :PlaceView) :void; + + /** + * When the client leaves a place, the place controller will remove + * any place view it set previously via {@link #setPlaceView} with a + * call to this method. + */ + function clearPlaceView (view :PlaceView) :void; +} +} diff --git a/src/as/com/threerings/presents/client/BasicDirector.as b/src/as/com/threerings/presents/client/BasicDirector.as new file mode 100644 index 000000000..4d1a19ed6 --- /dev/null +++ b/src/as/com/threerings/presents/client/BasicDirector.as @@ -0,0 +1,99 @@ +// +// $Id: BasicDirector.java 3393 2005-03-10 02:06:43Z andrzej $ +// +// 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.presents.client { + +import com.threerings.presents.util.PresentsContext; + +/** + * Handles functionality common to nearly all client directors. They + * generally need to be session observers so that they can set themselves + * up when the client logs on (by overriding {@link #clientDidLogon}) and + * clean up after themselves when the client logs off (by overriding + * {@link #clientDidLogoff}). + */ +public class BasicDirector + implements SessionObserver +{ + /** + * Derived directors will need to provide the basic director with a + * context that it can use to register itself with the necessary + * entities. + */ + protected function BasicDirector (ctx :PresentsContext) + { + // save context + _ctx = ctx; + + // listen for session start and end + var client :Client = ctx.getClient(); + client.addClientObserver(this); + + // if we're already logged on, fire off a call to fetch services + if (client.isLoggedOn()) { + fetchServices(client); + clientObjectUpdated(client); + } + } + + // documentation inherited from interface SessionObserver + public function clientDidLogon (event :ClientEvent) :void + { + var client :Client = event.getClient(); + fetchServices(client); + clientObjectUpdated(client); + } + + // documentation inherited from interface SessionObserver + public function clientObjectDidChange (event :ClientEvent) :void + { + clientObjectUpdated(event.getClient()); + } + + // documentation inherited from interface SessionObserver + public function clientDidLogoff (event :ClientEvent) :void + { + } + + /** + * Called in three circumstances: when a director is created and we've + * already logged on; when we first log on and when the client object + * changes after we've already logged on. + */ + protected function clientObjectUpdated (client :Client) :void + { + } + + /** + * Derived directors can override this method and obtain any services + * they'll need during their operation via calls to {@link + * Client#getService}. If the director is available, it will automatically + * be called when the client logs on or when the director is constructed + * if it is constructed after the client is already logged on. + */ + protected function fetchServices (client :Client) :void + { + } + + /** The application context. */ + protected var _ctx :PresentsContext; +} +} diff --git a/src/as/com/threerings/presents/client/Client.as b/src/as/com/threerings/presents/client/Client.as index b7c73e704..4f0670cf9 100644 --- a/src/as/com/threerings/presents/client/Client.as +++ b/src/as/com/threerings/presents/client/Client.as @@ -26,6 +26,58 @@ public class Client extends EventDispatcher _creds = creds; } + /** + * Registers the supplied observer with this client. While registered + * the observer will receive notifications of state changes within the + * client. The function will refuse to register an already registered + * observer. + * + * @see ClientObserver + * @see SessionObserver + */ + public function addClientObserver (observer :SessionObserver) :void + { + addEventListener(ClientEvent.CLIENT_DID_LOGON, + observer.clientDidLogon); + addEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, + observer.clientObjectDidChange); + addEventListener(ClientEvent.CLIENT_DID_LOGOFF, + observer.clientDidLogoff); + if (observer is ClientObserver) { + var cliObs :ClientObserver = (observer as ClientObserver); + addEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, + cliObs.clientFailedToLogon); + addEventListener(ClientEvent.CLIENT_CONNECTION_FAILED, + cliObs.clientConnectionFailed); + addEventListener(ClientEvent.CLIENT_WILL_LOGOFF, + cliObs.clientWillLogoff); + } + } + + /** + * Unregisters the supplied observer. Upon return of this function, + * the observer will no longer receive notifications of state changes + * within the client. + */ + public function removeClientObserver (observer :SessionObserver) :void + { + removeEventListener(ClientEvent.CLIENT_DID_LOGON, + observer.clientDidLogon); + removeEventListener(ClientEvent.CLIENT_OBJECT_CHANGED, + observer.clientObjectDidChange); + removeEventListener(ClientEvent.CLIENT_DID_LOGOFF, + observer.clientDidLogoff); + if (observer is ClientObserver) { + var cliObs :ClientObserver = (observer as ClientObserver); + removeEventListener(ClientEvent.CLIENT_FAILED_TO_LOGON, + cliObs.clientFailedToLogon); + removeEventListener(ClientEvent.CLIENT_CONNECTION_FAILED, + cliObs.clientConnectionFailed); + removeEventListener(ClientEvent.CLIENT_WILL_LOGOFF, + cliObs.clientWillLogoff); + } + } + public function setServer (hostname :String, port :int) :void { _hostname = hostname; diff --git a/src/as/com/threerings/presents/client/ClientObserver.as b/src/as/com/threerings/presents/client/ClientObserver.as new file mode 100644 index 000000000..2b29f7fce --- /dev/null +++ b/src/as/com/threerings/presents/client/ClientObserver.as @@ -0,0 +1,72 @@ +// +// $Id: ClientObserver.java 3099 2004-08-27 02:21: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.presents.client { + +/** + * A client observer is a more detailed version of the {@link + * SessionObserver} for entities that are interested in more detail about + * the logon/logoff process. + * + *
In the normal course of affairs, clientDidLogon will
+ * be called after the client successfully logs on to the server and
+ * clientDidLogoff will be called after the client logs off
+ * of the server. If logon fails for any reson,
+ * clientFailedToLogon will be called to explain the failure.
+ *
+ *
clientWillLogoff will only be called when an abortable
+ * logoff is requested (like when the user clicks on a logoff button of
+ * some sort). It will not be called during non-abortable logoff requests
+ * (like when the browser calls stop on the applet and is about to yank
+ * the rug out from under us). If an observer aborts the logoff request,
+ * it should notify the user in some way why the request was aborted
+ * (but it shouldn't do so on the thread that calls
+ * clientWillLogoff).
+ *
+ *
If the client connection fails unexpectedly,
+ * clientConnectionFailed will be called to let the
+ * observers know that we lost our connection to the
+ * server. clientDidLogoff will be called immediately
+ * afterwards as a normal logoff procedure is effected.
+ */
+public interface ClientObserver extends SessionObserver
+{
+ /**
+ * Called if anything fails during the logon attempt. This could be a
+ * network failure, authentication failure or otherwise. The exception
+ * provided will indicate the cause of the failure.
+ */
+ function clientFailedToLogon (event :ClientEvent) :void;
+
+ /**
+ * Called when the connection to the server went away for some
+ * unexpected reason. This will be followed by a call to
+ * clientDidLogoff.
+ */
+ function clientConnectionFailed (event :ClientEvent) :void;
+
+ /**
+ * Called when an abortable logoff request is made. If the observer
+ * calls event.preventDefault() then it will abort the logoff request.
+ */
+ function clientWillLogoff (event :ClientEvent) :void;
+}
+}
diff --git a/src/as/com/threerings/presents/client/InvocationAdapter.as b/src/as/com/threerings/presents/client/InvocationAdapter.as
new file mode 100644
index 000000000..55e9b0453
--- /dev/null
+++ b/src/as/com/threerings/presents/client/InvocationAdapter.as
@@ -0,0 +1,28 @@
+package com.threerings.presents.client {
+
+public class InvocationAdapter
+ implements InvocationListener
+{
+ /**
+ * Construct an InvocationAdapter that will call the specified
+ * function on error.
+ */
+ public function InvocationAdapter (failedFunc :Function, args :Array = null)
+ {
+ _failedFunc = failedFunc;
+ _args = args;
+ }
+
+ // documentation inherited from interface InvocationListener
+ public function requestFailed (cause :String) :void
+ {
+ _failedFunc(cause, _args);
+ }
+
+ /** The Function to call when we've recevied our failure response. */
+ protected var _failedFunc :Function;
+
+ /** Any other extra information to pass along to the function. */
+ protected var _args :Array;
+}
+}
diff --git a/src/as/com/threerings/presents/client/SessionObserver.as b/src/as/com/threerings/presents/client/SessionObserver.as
new file mode 100644
index 000000000..772e56809
--- /dev/null
+++ b/src/as/com/threerings/presents/client/SessionObserver.as
@@ -0,0 +1,54 @@
+//
+// $Id: SessionObserver.java 3099 2004-08-27 02:21: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.presents.client {
+
+/**
+ * A session observer is registered with the client instance to be
+ * notified when the client establishes and ends their session with the
+ * server.
+ *
+ * @see ClientObserver
+ */
+public interface SessionObserver
+{
+ /**
+ * Called after the client successfully connected to and authenticated
+ * with the server. The entire object system is up and running by the
+ * time this method is called.
+ */
+ function clientDidLogon (event :ClientEvent) :void;
+
+ /**
+ * For systems that allow switching screen names after logon, this
+ * method is called whenever a screen name change takes place to
+ * report that the client object has been replaced to potential
+ * client-side subscribers.
+ */
+ function clientObjectDidChange (event :ClientEvent) :void;
+
+ /**
+ * Called after the client has been logged off of the server and has
+ * disconnected.
+ */
+ function clientDidLogoff (event :ClientEvent) :void;
+}
+}
diff --git a/src/as/com/threerings/presents/client/TestClient.as b/src/as/com/threerings/presents/client/TestClient.as
index d6c0b482b..70f688f88 100644
--- a/src/as/com/threerings/presents/client/TestClient.as
+++ b/src/as/com/threerings/presents/client/TestClient.as
@@ -2,6 +2,8 @@ package com.threerings.presents.client {
import flash.util.describeType;
+import mx.collections.ArrayCollection;
+
import com.threerings.util.Name;
import com.threerings.presents.Log;
import com.threerings.presents.data.TimeBaseMarshaller;
@@ -41,6 +43,26 @@ public class TestClient extends Client
Log.debug("arr[0]: " + arr[0]);
Log.debug("arr[1]: " + arr[1]);
Log.debug("arr[2]: " + arr[2]);
+ //testFunc(2);
+
+ var bob :Person = new Person("Bob");
+ var jim :Person = new Person("Jim");
+ var bob2 :Person = new Person("Bob");
+ bob.callPrintName(jim.printName);
+
+ Log.debug("bob == jim: " + (bob == jim));
+ Log.debug("bob === jim: " + (bob === jim));
+ Log.debug("bob == bob2: " + (bob == bob2));
+ Log.debug("bob === bob2: " + (bob === bob2));
+ Log.debug("bob == bob: " + (bob == bob));
+ Log.debug("bob === bob: " + (bob === bob));
+
+ var list :ArrayCollection = new ArrayCollection();
+ list.addItem(bob);
+ Log.debug("jim's indeX: " + list.getItemIndex(jim));
+ Log.debug("bob2's indeX: " + list.getItemIndex(bob2));
+
+
/*
var a :Object = new OldClass();
@@ -94,12 +116,45 @@ public class TestClient extends Client
{
var i :int = TimeBaseMarshaller.GET_TIME_OID;
}
+
+ public function testFunc (one :int, two :int = 0) :void
+ {
+ Log.debug("this: " + ", args: " + arguments.length +
+ ": " + arguments);
+ }
+
+ prototype var _foo :String;
}
}
import com.threerings.presents.Log;
import com.threerings.presents.client.TestClient;
+class Person
+{
+ public function Person (name :String)
+ {
+ _name = name;
+ }
+
+ public function printName () :void
+ {
+ Log.debug("printName called on " + _name + "! (this=" + this + ")");
+ }
+
+ public function callPrintName (funcy :Function) :void
+ {
+ funcy();
+ }
+
+ public function toString () :String
+ {
+ return "Person[" + _name + "]";
+ }
+
+ protected var _name :String;
+}
+
dynamic class OldClass
{
public function OldClass ()
diff --git a/src/as/com/threerings/presents/util/PresentsContext.as b/src/as/com/threerings/presents/util/PresentsContext.as
new file mode 100644
index 000000000..fb9cb2949
--- /dev/null
+++ b/src/as/com/threerings/presents/util/PresentsContext.as
@@ -0,0 +1,55 @@
+//
+// $Id: PresentsContext.java 3099 2004-08-27 02:21: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.presents.util {
+
+//import com.samskivert.util.Config;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.dobj.DObjectManager;
+
+/**
+ * Provides access to standard services needed by code that is part of or
+ * uses the Presents package.
+ */
+public interface PresentsContext
+{
+// /**
+// * Provides a configuration object from which various services can
+// * obtain configuration values and via the properties file that forms
+// * the basis of the configuration object, those services can be
+// * customized.
+// */
+// public Config getConfig ();
+
+ /**
+ * Returns a reference to the client. This reference should be valid
+ * for the life of the application.
+ */
+ function getClient () :Client;
+
+ /**
+ * Returns a reference to the distributed object manager. This
+ * reference is only valid for the duration of a session.
+ */
+ function getDObjectManager () :DObjectManager;
+}
+}
diff --git a/src/as/com/threerings/util/MessageBundle.as b/src/as/com/threerings/util/MessageBundle.as
new file mode 100644
index 000000000..2dfb9efdc
--- /dev/null
+++ b/src/as/com/threerings/util/MessageBundle.as
@@ -0,0 +1,393 @@
+//
+// $Id: MessageBundle.java 3099 2004-08-27 02:21: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.util;
+
+/**
+ * A message bundle provides an easy mechanism by which to obtain
+ * translated message strings from a resource bundle. It uses the {@link
+ * MessageFormat} class to substitute arguments into the translation
+ * strings. Message bundles would generally be obtained via the {@link
+ * MessageManager}, but could be constructed individually if so desired.
+ */
+public class MessageBundle
+{
+ /**
+ * Initializes the message bundle which will obtain localized messages
+ * from the supplied resource bundle. The path is provided purely for
+ * reporting purposes.
+ */
+ public function init (
+ msgmgr :MessageManager, path :String, bundle :ResourceBundle,
+ parent :MessageBundle) :void
+ {
+ _msgmgr = msgmgr;
+ _path = path;
+ _bundle = bundle;
+ _parent = parent;
+ }
+
+ /**
+ * Returns true if we have a translation mapping for the supplied key,
+ * false if not.
+ */
+ public function exists (key :String) :Boolean
+ {
+ return getResourceString(key, false) != null;
+ }
+
+ /**
+ * Get a String from the resource bundle, or null if there was an error.
+ */
+ protected function getResourceString (key :String) :String
+ {
+ return getResourceString(key, true);
+ }
+
+ /**
+ * Get a String from the resource bundle, or null if there was an
+ * error.
+ *
+ * @param key the resource key.
+ * @param reportMissing whether or not the method should log an error
+ * if the resource didn't exist.
+ */
+ protected function getResourceString (
+ key :String, reportMissing :Boolean) :String
+ {
+ // TODO!!!
+// try {
+// if (_bundle != null) {
+// return _bundle.getString(key);
+// }
+// } catch (MissingResourceException mre) {
+// // fall through and try the parent
+// }
+//
+// // if we have a parent, try getting the string from them
+// if (_parent != null) {
+// String value = _parent.getResourceString(key, false);
+// if (value != null) {
+// return value;
+// }
+// // if we didn't find it in our parent, we want to fall
+// // through and report missing appropriately
+// }
+//
+// if (reportMissing) {
+// Log.warning("Missing translation message " +
+// "[bundle=" + _path + ", key=" + key + "].");
+// Thread.dumpStack();
+// }
+//
+ return null;
+ }
+
+ /**
+ * Obtains the translation for the specified message key. The
+ * specified arguments are substituted into the translated string.
+ *
+ *
If the first argument in the array is an {@link Integer}
+ * object, a translation will be selected accounting for plurality in
+ * the following manner. Assume a message key of
+ * m.widgets, the following translations should be
+ * defined:
+ *
+ * m.widgets.0 = no widgets.
+ * m.widgets.1 = {0} widget.
+ * m.widgets.n = {0} widgets.
+ *
+ *
+ * The specified argument is substituted into the translated string as
+ * appropriate. Consider using:
+ *
+ *
+ * m.widgets.n = {0,number,integer} widgets.
+ *
+ *
+ * to obtain proper insertion of commas and dots as appropriate for
+ * the locale.
+ *
+ * See {@link MessageFormat} for more information on how the + * substitution is performed. If a translation message does not exist + * for the specified key, an error is logged and the key itself (plus + * the arguments) is returned so that the caller need not worry about + * handling a null response. + */ + public function get (key :String, ... args) :String + { + // if this string is tainted, we don't translate it, instead we + // simply remove the taint character and return it to the caller + if (key.chatAt(0) === TAINT_CHAR) { + return key.substring(1); + } + + // if this is a qualified key, we need to pass the buck to the + // appropriate message bundle + if (key.indexOf(QUAL_PREFIX) == 0) { + var qbundle :MessageBundle = _msgmgr.getBundle(getBundle(key)); + return qbundle.get(getUnqualifiedKey(key), args); + } + + // look up our message string, selecting the proper plurality + // string if our first argument is an Integer + var msg :String = getResourceString(key + getSuffix(args), false); + + // if the base key is not found, look to see if we should try to + // convert our first argument to an Integer and try again + if (msg == null) { + Log.warning("Missing translation message " + + "[bundle=" + _path + ", key=" + key + "]."); + } + + return (msg != null) ? + MessageFormat.format(MessageUtil.escape(msg), args) + : (key + StringUtil.toString(args)); + } + + /** + * A helper function for {@link #get(String,Object[])} that allows us + * to automatically perform plurality processing if our first argument + * is an {@link Integer}. + */ + protected function getSuffix (args :Array) :String + { + if (args.length > 0 && args[0] is int) { + switch (args[0]) { + case 0: return ".0"; + case 1: return ".1"; + default: return ".n"; + } + } + return ""; + } + + /** + * Obtains the translation for the specified compound message key. A + * compound key contains the message key followed by a tab separated + * list of message arguments which will be subsituted into the + * translation string. + * + *
See {@link MessageFormat} for more information on how the
+ * substitution is performed. If a translation message does not exist
+ * for the specified key, an error is logged and the key itself (plus
+ * the arguments) is returned so that the caller need not worry about
+ * handling a null response.
+ */
+ public function xlate (compoundKey :String) :String
+ {
+ // if this is a qualified key, we need to pass the buck to the
+ // appropriate message bundle; we have to do it here because we
+ // want the compound arguments of this key to be translated in the
+ // context of the containing message bundle qualification
+ if (compoundKey.indexOf(QUAL_PREFIX) == 0) {
+ var qbundle :MessageBundle = _msgmgr.getBundle(
+ getBundle(compoundKey));
+ return qbundle.xlate(getUnqualifiedKey(compoundKey));
+ }
+
+ // to be more efficient about creating unnecessary objects, we
+ // do some checking before splitting
+ var tidx :int = compoundKey.indexOf("|");
+ if (tidx == -1) {
+ return get(compoundKey);
+
+ } else {
+ var key :String = compoundKey.substring(0, tidx);
+ var argstr :String = compoundKey.substring(tidx+1);
+ var args :Array = argstr.split("|");
+ // unescape and translate the arguments
+ for (var ii :int = 0; ii < args.length; ii++) {
+ // if the argument is tainted, do no further translation
+ // (it might contain |s or other fun stuff)
+ if (args[ii].indexOf(TAINT_CHAR) == 0) {
+ args[ii] = unescape(args[ii].substring(1));
+ } else {
+ args[ii] = xlate(unescape(args[ii]));
+ }
+ }
+ return get(key, args);
+ }
+ }
+
+ /**
+ * Call this to "taint" any string that has been entered by an entity
+ * outside the application so that the translation code knows not to
+ * attempt to translate this string when doing recursive translations
+ * (see {@link #xlate}).
+ */
+ public static function taint (text :Object) :String
+ {
+ return TAINT_CHAR + text;
+ }
+
+ /**
+ * Composes a message key with an array of arguments. The message can
+ * subsequently be translated in a single call using {@link #xlate}.
+ */
+ public static function compose (key :String, ... args) :String
+ {
+ var buf :StringBuilder = new StringBuilder();
+ buf.append(key, "|");
+ for (var ii :int = 0; ii < args.length; ii++) {
+ if (ii > 0) {
+ buf.append("|");
+ }
+ var arg :String = String(args[ii]);
+ for (var p :int = 0; p < arg.length; p++) {
+ var ch :String = arg.charAt(p);
+ if (ch == "|") {
+ buf.append("\\!");
+ } else if (ch == "\\") {
+ buf.append("\\\\");
+ } else {
+ buf.append(ch);
+ }
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * A convenience method for calling {@link #compose(String,Object[])}
+ * with a single argument that will be automatically tainted (see
+ * {@link #taint}).
+ */
+ public static function tcompose (key :String, ... args) :String
+ {
+ for (var ii :int = 0; ii < args.length; ii++) {
+ args[ii] = taint(args[ii]);
+ }
+ return compose(key, args);
+ }
+
+ /**
+ * Returns a fully qualified message key which, when translated by
+ * some other bundle, will know to resolve and utilize the supplied
+ * bundle to translate this particular key.
+ */
+ public static function qualify (bundle :String, key :String) :String
+ {
+ if (bundle.indexOf(QUAL_PREFIX) != -1 ||
+ bundle.indexOf(QUAL_SEP) != -1) {
+ throw new Error("Message bundle may not contain " + QUAL_PREFIX +
+ " or " + QUAL_SEP);
+ }
+
+ return QUAL_PREFIX + bundle + QUAL_SEP + key;
+ }
+
+ /**
+ * Returns the bundle name from a fully qualified message key.
+ *
+ * @see #qualify
+ */
+ public static function getBundle (qualifiedKey :String) :String
+ {
+ if (qualifiedKey.indexOf(QUAL_PREFIX) != 0) {
+ throw new Error(qualifiedKey +
+ " is not a fully qualified message key.");
+ }
+ var qsidx :int = qualifiedKey.indexOf(QUAL_SEP);
+ if (qsidx == -1) {
+ throw new Error(qualifiedKey +
+ " is not a valid fully qualified key.");
+ }
+
+ return qualifiedKey.substring(QUAL_PREFIX.length, qsidx);
+ }
+
+ /**
+ * Returns the unqualified portion of the key from a fully qualified
+ * message key.
+ *
+ * @see #qualify
+ */
+ public static function getUnqualifiedKey (qualifiedKey :String) :String
+ {
+ if (qualifiedKey.indexOf(QUAL_PREFIX) != 0) {
+ throw new Error(qualifiedKey +
+ " is not a fully qualified message key.");
+ }
+ var qsidx :int = qualifiedKey.indexOf(QUAL_SEP);
+ if (qsidx == -1) {
+ throw new Error(qualifiedKey +
+ " is not a fully qualified message key.");
+ }
+ return qualifiedKey.substring(qsidx + 1);
+ }
+
+ /**
+ * Used to escape single quotes so that they are not interpreted by
+ * {@link MessageFormat}. As we assume all single quotes are to be
+ * escaped, we cannot use the characters { and
+ * } in our translation strings, but this is a small
+ * price to pay to have to differentiate between messages that will
+ * and won't eventually be parsed by a {@link MessageFormat} instance.
+ */
+ public static function escape (val :String) :String
+ {
+ return val.replace("'", "''");
+ }
+
+ /**
+ * Unescapes characters that are escaped in a call to compose.
+ */
+ public static function unescape (val :String) :String
+ {
+ var bsidx :int = val.indexOf("\\");
+ if (bsidx == -1) {
+ return val;
+ }
+
+ var buf :StringBuilder = new StringBuilder();
+ for (var ii :int = 0; ii < val.length; ii++) {
+ var ch :String = value.charAt(0);
+ if (ch != "\\" || ii == val.length-1) {
+ buf.append(ch);
+ } else {
+ // look at the next character
+ ch = val.charAt(++ii);
+ buf.append((ch == "!") ? "|" : ch);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ /** The message manager via whom we'll resolve fully qualified
+ * translation strings. */
+ protected var _msgmgr :MessageManager;
+
+ /** The path that identifies the resource bundle we are using to
+ * obtain our messages. */
+ protected var _path :String;
+
+ /** The resource bundle from which we obtain our messages. */
+ protected var _bundle :ResourceBundle;
+
+ /** Our parent bundle if we're not the global bundle. */
+ protected var _parent :MessageBundle;
+
+ protected static const TAINT_CHAR :String = "~";
+ protected static const QUAL_PREFIX :String = "%";
+ protected static const QUAL_SEP :String = ":";
+}
diff --git a/src/as/com/threerings/util/MessageManager.as b/src/as/com/threerings/util/MessageManager.as
new file mode 100644
index 000000000..e1946bea8
--- /dev/null
+++ b/src/as/com/threerings/util/MessageManager.as
@@ -0,0 +1,190 @@
+//
+// $Id: MessageManager.java 3749 2005-11-09 04:00:16Z 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.util {
+
+/**
+ * The message manager provides a thin wrapper around Java's built-in
+ * localization support, supporting a policy of dividing up localization
+ * resources into logical units, all of the translations for which are
+ * contained in a single messages file.
+ *
+ *
The message manager assumes that the locale remains constant for
+ * the duration of its operation. If the locale were to change during the
+ * operation of the client, a call to {@link #setLocale} should be made to
+ * inform the message manager of the new locale (which will clear the
+ * message bundle cache).
+ */
+public class MessageManager
+{
+ /** The name of the global resource bundle (which other bundles revert
+ * to if they can't locate a message within themselves). It must be
+ * named global.properties and live at the top of the
+ * bundle hierarchy. */
+ public static const GLOBAL_BUNDLE :String = "global";
+
+ /**
+ * Constructs a message manager with the supplied resource prefix and
+ * the default locale. The prefix will be prepended to the path of all
+ * resource bundles prior to their resolution. For example, if a
+ * prefix of rsrc.messages was provided and a message
+ * bundle with the name game.chess was later requested,
+ * the message manager would attempt to load a resource bundle with
+ * the path rsrc.messages.game.chess and would eventually
+ * search for a file in the classpath with the path
+ * rsrc/messages/game/chess.properties.
+ *
+ *
See the documentation for {@link + * ResourceBundle#getBundle(String,Locale,ClassLoader)} for a more + * detailed explanation of how resource bundle paths are resolved. + */ + public function MessageManager (resourcePrefix :String) + { + // keep the prefix + _prefix = resourcePrefix; + + // make sure the prefix ends with a dot + if (_prefix.charAt(_prefix.length - 1) != ".") { + _prefix += "."; + } + + // load up the global bundle + _global = getBundle(GLOBAL_BUNDLE); + } + + /** + * Get the locale that is being used to translate messages. + * This may be useful if using standard translations, for example + * new SimpleDateFormat("EEEE", getLocale()) to get the name of a weekday + * that matches the language being used for all other client translations. + */ +// public Locale getLocale () +// { +// return _locale; +// } + + /** + * Sets the locale to the specified locale. Subsequent message bundles + * fetched via the message manager will use the new locale. The + * message bundle cache will also be cleared. + */ +// public void setLocale (Locale locale) +// { +// _locale = locale; +// _cache.clear(); +// } + + /** + * Allows a custom classloader to be configured for locating + * translation resources. + */ +// public void setClassLoader (ClassLoader loader) +// { +// _loader = loader; +// } + + /** + * Fetches the message bundle for the specified path. If no bundle can + * be located with the specified path, a special bundle is returned + * that returns the untranslated message identifiers instead of an + * associated translation. This is done so that error code to handle a + * failed bundle load need not be replicated wherever bundles are + * used. Instead an error will be logged and the requesting service + * can continue to function in an impaired state. + */ + public function getBundle (path :String) :MessageBundle + { + // first look in the cache + var bundle :MessageBundle = (_cache.get(path) as MessageBundle); + if (bundle != null) { + return bundle; + } + + // if it's not cached, we'll need to resolve it + var fqpath :String = _prefix + path; + var rbundle :ResourceBundle = null; + try { +// if (_loader != null) { +// rbundle = ResourceBundle.getBundle(fqpath, _locale, _loader); +// } else { + rbundle = ResourceBundle.getBundle(fqpath, _locale); +// } + } catch (MissingResourceException mre) { + Log.warning("Unable to resolve resource bundle " + + "[path=" + fqpath + ", locale=" + _locale + "]."); + } + + // if the resource bundle contains a special resource, we'll + // interpret that as a derivation of MessageBundle to instantiate + // for handling that class + if (rbundle != null) { + String mbclass = null; + try { + mbclass = rbundle.getString(MBUNDLE_CLASS_KEY); + if (!StringUtil.isBlank(mbclass)) { + bundle = (MessageBundle) + Class.forName(mbclass).newInstance(); + } + + } catch (MissingResourceException mre) { + // nothing to worry about + + } catch (Throwable t) { + Log.warning("Failure instantiating custom message bundle " + + "[mbclass=" + mbclass + ", error=" + t + "]."); + } + } + + // if there was no custom class, or we failed to instantiate the + // custom class, use a standard message bundle + if (bundle == null) { + bundle = new MessageBundle(); + } + + // initialize our message bundle, cache it and return it (if we + // couldn't resolve the bundle, the message bundle will cope with + // it's null resource bundle) + bundle.init(this, path, rbundle, _global); + _cache.put(path, bundle); + return bundle; + } + + /** The prefix we prepend to resource paths prior to loading. */ + protected var _prefix :String; + + /** The locale for which we're obtaining message bundles. */ +// protected var _locale :Locale; + + /** A custom class loader that we use to load resource bundles. */ + protected var _loader :ClassLoader; + + /** A cache of instantiated message bundles. */ + protected var _cache :SimpleMap = new SimpleMap(); + + /** Our top-level message bundle, from which others obtain messages if + * they can't find them within themselves. */ + protected var _global :MessageBundle; + + /** A key that can contain the classname of a custom message bundle + * class to be used to handle messages for a particular bundle. */ +// protected static const MBUNDLE_CLASS_KEY :String = "msgbundle_class"; +} +} diff --git a/src/as/com/threerings/util/SimpleMap.as b/src/as/com/threerings/util/SimpleMap.as index bd3ea5d3b..1480ad94b 100644 --- a/src/as/com/threerings/util/SimpleMap.as +++ b/src/as/com/threerings/util/SimpleMap.as @@ -1,8 +1,7 @@ package com.threerings.util { /** - * Marker class that allows us to use ActionScript's built-in hashing - * function without hassle. + * I will likely extend this out to be a fully-featured map. */ public class SimpleMap extends Object { diff --git a/src/as/com/threerings/util/StringUtil.as b/src/as/com/threerings/util/StringUtil.as index 44beef563..3a078a43e 100644 --- a/src/as/com/threerings/util/StringUtil.as +++ b/src/as/com/threerings/util/StringUtil.as @@ -6,5 +6,25 @@ public class StringUtil { return (str == null) || (str.search("\\S") == -1); } + + /** + * Utility function that strips whitespace from the ends of a String. + */ + public static function trim (str :String) :String + { + while (str.search(/\s/) == 0) { + str = str.substring(1); + } + do { + var endstr :String = str.substring(str.length - 1); + if (endstr.search(/\s/) != -1) { + str = str.substring(0, str.length - 1); + } else { + break; + } + } while (true); + + return str; + } } }