diff --git a/.hgtags b/.hgtags
index 6eb0813d1b16cc42578d221dcd661e53ecdffc41..2b143b0013622a68e5d8441220e9915b4e416436 100644
--- a/.hgtags
+++ b/.hgtags
@@ -52,6 +52,7 @@ a82e5b1e22c7f90e3c7977d146b80588f004ed0d 2.5.0-start
 345b17e7cf630db77e840b4fe3451bd476d750a3 76f586a8e22b
 0000000000000000000000000000000000000000 76f586a8e22b
 54d772d8687c69b1d773f6ce14bbc7bdc9d6c05f 2.5.0-beta2
+b542f8134a2bb5dd054ff4e509a44b2ee463b1bf nat-eventapi2-base
 7076e22f9f43f479a4ea75eac447a36364bead5a DRTVWR-5_2.2.0-beta1
 9822eb3e25f7fe0c28ffd8aba45c507caa383cbc DRTVWR-3_2.2.0-beta2
 b0cd7e150009809a0b5b0a9d5785cd4bb230413a DRTVWR-7_2.2.0-beta3
diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index 9342a22d46f9d0a3a0b0017544af3aa6a19383bd..dc9f93df3b14f8e89769faaebec5a1349fcce887 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -313,6 +313,7 @@ if (LL_TESTS)
   LL_ADD_INTEGRATION_TEST(lluri "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(reflection "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(lleventdispatcher "" "${test_libs}")
 
   # *TODO - reenable these once tcmalloc libs no longer break the build.
   #ADD_BUILD_TEST(llallocator llcommon)
diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp
index d6e820d793c38de10bfbf249d0dff316b5af61a4..5b6d4efbe98b8259d09197444b7cc914d80bf554 100644
--- a/indra/llcommon/lleventdispatcher.cpp
+++ b/indra/llcommon/lleventdispatcher.cpp
@@ -41,6 +41,354 @@
 #include "llevents.h"
 #include "llerror.h"
 #include "llsdutil.h"
+#include "stringize.h"
+#include <memory>                   // std::auto_ptr
+
+/*****************************************************************************
+*   LLSDArgsSource
+*****************************************************************************/
+/**
+ * Store an LLSD array, producing its elements one at a time. Die with LL_ERRS
+ * if the consumer requests more elements than the array contains.
+ */
+class LL_COMMON_API LLSDArgsSource
+{
+public:
+    LLSDArgsSource(const std::string function, const LLSD& args);
+    ~LLSDArgsSource();
+
+    LLSD next();
+
+    void done() const;
+
+private:
+    std::string _function;
+    LLSD _args;
+    LLSD::Integer _index;
+};
+
+LLSDArgsSource::LLSDArgsSource(const std::string function, const LLSD& args):
+    _function(function),
+    _args(args),
+    _index(0)
+{
+    if (! (_args.isUndefined() || _args.isArray()))
+    {
+        LL_ERRS("LLSDArgsSource") << _function << " needs an args array instead of "
+                                  << _args << LL_ENDL;
+    }
+}
+
+LLSDArgsSource::~LLSDArgsSource()
+{
+    done();
+}
+
+LLSD LLSDArgsSource::next()
+{
+    if (_index >= _args.size())
+    {
+        LL_ERRS("LLSDArgsSource") << _function << " requires more arguments than the "
+                                  << _args.size() << " provided: " << _args << LL_ENDL;
+    }
+    return _args[_index++];
+}
+
+void LLSDArgsSource::done() const
+{
+    if (_index < _args.size())
+    {
+        LL_WARNS("LLSDArgsSource") << _function << " only consumed " << _index
+                                   << " of the " << _args.size() << " arguments provided: "
+                                   << _args << LL_ENDL;
+    }
+}
+
+/*****************************************************************************
+*   LLSDArgsMapper
+*****************************************************************************/
+/**
+ * From a formal parameters description and a map of arguments, construct an
+ * arguments array.
+ *
+ * That is, given:
+ * - an LLSD array of length n containing parameter-name strings,
+ *   corresponding to the arguments of a function of interest
+ * - an LLSD collection specifying default parameter values, either:
+ *   - an LLSD array of length m <= n, matching the rightmost m params, or
+ *   - an LLSD map explicitly stating default name=value pairs
+ * - an LLSD map of parameter names and actual values for a particular
+ *   function call
+ * construct an LLSD array of actual argument values for this function call.
+ *
+ * The parameter-names array and the defaults collection describe the function
+ * being called. The map might vary with every call, providing argument values
+ * for the described parameters.
+ *
+ * The array of parameter names must match the number of parameters expected
+ * by the function of interest.
+ *
+ * If you pass a map of default parameter values, it provides default values
+ * as you might expect. It is an error to specify a default value for a name
+ * not listed in the parameters array.
+ *
+ * If you pass an array of default parameter values, it is mapped to the
+ * rightmost m of the n parameter names. It is an error if the default-values
+ * array is longer than the parameter-names array. Consider the following
+ * parameter names: ["a", "b", "c", "d"].
+ *
+ * - An empty array of default values (or an isUndefined() value) asserts that
+ *   every one of the above parameter names is required.
+ * - An array of four default values [1, 2, 3, 4] asserts that every one of
+ *   the above parameters is optional. If the current parameter map is empty,
+ *   they will be passed to the function as [1, 2, 3, 4].
+ * - An array of two default values [11, 12] asserts that parameters "a" and
+ *   "b" are required, while "c" and "d" are optional, having default values
+ *   "c"=11 and "d"=12.
+ *
+ * The arguments array is constructed as follows:
+ *
+ * - Arguments-map keys not found in the parameter-names array are ignored.
+ * - Entries from the map provide values for an improper subset of the
+ *   parameters named in the parameter-names array. This results in a
+ *   tentative values array with "holes." (size of map) + (number of holes) =
+ *   (size of names array)
+ * - Holes are filled with the default values.
+ * - Any remaining holes constitute an error.
+ */
+class LL_COMMON_API LLSDArgsMapper
+{
+public:
+    /// Accept description of function: function name, param names, param
+    /// default values
+    LLSDArgsMapper(const std::string& function, const LLSD& names, const LLSD& defaults);
+
+    /// Given arguments map, return LLSD::Array of parameter values, or LL_ERRS.
+    LLSD map(const LLSD& argsmap) const;
+
+private:
+    static std::string formatlist(const LLSD&);
+
+    // The function-name string is purely descriptive. We want error messages
+    // to be able to indicate which function's LLSDArgsMapper has the problem.
+    std::string _function;
+    // Store the names array pretty much as given.
+    LLSD _names;
+    // Though we're handed an array of name strings, it's more useful to us to
+    // store it as a map from name string to position index. Of course that's
+    // easy to generate from the incoming names array, but why do it more than
+    // once?
+    typedef std::map<LLSD::String, LLSD::Integer> IndexMap;
+    IndexMap _indexes;
+    // Generated array of default values, aligned with the array of param names.
+    LLSD _defaults;
+    // Indicate whether we have a default value for each param.
+    typedef std::vector<char> FilledVector;
+    FilledVector _has_dft;
+};
+
+LLSDArgsMapper::LLSDArgsMapper(const std::string& function,
+                               const LLSD& names, const LLSD& defaults):
+    _function(function),
+    _names(names),
+    _has_dft(names.size())
+{
+    if (! (_names.isUndefined() || _names.isArray()))
+    {
+        LL_ERRS("LLSDArgsMapper") << function << " names must be an array, not " << names << LL_ENDL;
+    }
+    LLSD::Integer nparams(_names.size());
+    // From _names generate _indexes.
+    for (LLSD::Integer ni = 0, nend = _names.size(); ni < nend; ++ni)
+    {
+        _indexes[_names[ni]] = ni;
+    }
+
+    // Presize _defaults() array so we don't have to resize it more than once.
+    // All entries are initialized to LLSD(); but since _has_dft is still all
+    // 0, they're all "holes" for now.
+    if (nparams)
+    {
+        _defaults[nparams - 1] = LLSD();
+    }
+
+    if (defaults.isUndefined() || defaults.isArray())
+    {
+        LLSD::Integer ndefaults = defaults.size();
+        // defaults is a (possibly empty) array. Right-align it with names.
+        if (ndefaults > nparams)
+        {
+            LL_ERRS("LLSDArgsMapper") << function << " names array " << names
+                                      << " shorter than defaults array " << defaults << LL_ENDL;
+        }
+
+        // Offset by which we slide defaults array right to right-align with
+        // _names array
+        LLSD::Integer offset = nparams - ndefaults;
+        // Fill rightmost _defaults entries from defaults, and mark them as
+        // filled
+        for (LLSD::Integer i = 0, iend = ndefaults; i < iend; ++i)
+        {
+            _defaults[i + offset] = defaults[i];
+            _has_dft[i + offset] = 1;
+        }
+    }
+    else if (defaults.isMap())
+    {
+        // defaults is a map. Use it to populate the _defaults array.
+        LLSD bogus;
+        for (LLSD::map_const_iterator mi(defaults.beginMap()), mend(defaults.endMap());
+             mi != mend; ++mi)
+        {
+            IndexMap::const_iterator ixit(_indexes.find(mi->first));
+            if (ixit == _indexes.end())
+            {
+                bogus.append(mi->first);
+                continue;
+            }
+
+            LLSD::Integer pos = ixit->second;
+            // Store default value at that position in the _defaults array.
+            _defaults[pos] = mi->second;
+            // Don't forget to record the fact that we've filled this
+            // position.
+            _has_dft[pos] = 1;
+        }
+        if (bogus.size())
+        {
+            LL_ERRS("LLSDArgsMapper") << function << " defaults specified for nonexistent params "
+                                      << formatlist(bogus) << LL_ENDL;
+        }
+    }
+    else
+    {
+        LL_ERRS("LLSDArgsMapper") << function << " defaults must be a map or an array, not "
+                                  << defaults << LL_ENDL;
+    }
+}
+
+LLSD LLSDArgsMapper::map(const LLSD& argsmap) const
+{
+    if (! (argsmap.isUndefined() || argsmap.isMap() || argsmap.isArray()))
+    {
+        LL_ERRS("LLSDArgsMapper") << _function << " map() needs a map or array, not "
+                                  << argsmap << LL_ENDL;
+    }
+    // Initialize the args array. Indexing a non-const LLSD array grows it
+    // to appropriate size, but we don't want to resize this one on each
+    // new operation. Just make it as big as we need before we start
+    // stuffing values into it.
+    LLSD args(LLSD::emptyArray());
+    if (_defaults.size() == 0)
+    {
+        // If this function requires no arguments, fast exit. (Don't try to
+        // assign to args[-1].)
+        return args;
+    }
+    args[_defaults.size() - 1] = LLSD();
+
+    // Get a vector of chars to indicate holes. It's tempting to just scan
+    // for LLSD::isUndefined() values after filling the args array from
+    // the map, but it's plausible for caller to explicitly pass
+    // isUndefined() as the value of some parameter name. That's legal
+    // since isUndefined() has well-defined conversions (default value)
+    // for LLSD data types. So use a whole separate array for detecting
+    // holes. (Avoid std::vector<bool> which is known to be odd -- can we
+    // iterate?)
+    FilledVector filled(args.size());
+
+    if (argsmap.isArray())
+    {
+        // Fill args from array. If there are too many args in passed array,
+        // ignore the rest.
+        LLSD::Integer size(argsmap.size());
+        if (size > args.size())
+        {
+            // We don't just use std::min() because we want to sneak in this
+            // warning if caller passes too many args.
+            LL_WARNS("LLSDArgsMapper") << _function << " needs " << args.size()
+                                       << " params, ignoring last " << (size - args.size())
+                                       << " of passed " << size << ": " << argsmap << LL_ENDL;
+            size = args.size();
+        }
+        for (LLSD::Integer i(0); i < size; ++i)
+        {
+            // Copy the actual argument from argsmap
+            args[i] = argsmap[i];
+            // Note that it's been filled
+            filled[i] = 1;
+        }
+    }
+    else
+    {
+        // argsmap is in fact a map. Walk the map.
+        for (LLSD::map_const_iterator mi(argsmap.beginMap()), mend(argsmap.endMap());
+             mi != mend; ++mi)
+        {
+            // mi->first is a parameter-name string, with mi->second its
+            // value. Look up the name's position index in _indexes.
+            IndexMap::const_iterator ixit(_indexes.find(mi->first));
+            if (ixit == _indexes.end())
+            {
+                // Allow for a map containing more params than were passed in
+                // our names array. Caller typically receives a map containing
+                // the function name, cruft such as reqid, etc. Ignore keys
+                // not defined in _indexes.
+                LL_DEBUGS("LLSDArgsMapper") << _function << " ignoring "
+                                            << mi->first << "=" << mi->second << LL_ENDL;
+                continue;
+            }
+            LLSD::Integer pos = ixit->second;
+            // Store the value at that position in the args array.
+            args[pos] = mi->second;
+            // Don't forget to record the fact that we've filled this
+            // position.
+            filled[pos] = 1;
+        }
+    }
+
+    // Fill any remaining holes from _defaults.
+    LLSD unfilled(LLSD::emptyArray());
+    for (LLSD::Integer i = 0, iend = args.size(); i < iend; ++i)
+    {
+        if (! filled[i])
+        {
+            // If there's no default value for this parameter, that's an
+            // error.
+            if (! _has_dft[i])
+            {
+                unfilled.append(_names[i]);
+            }
+            else
+            {
+                args[i] = _defaults[i];
+            }
+        }
+    }
+    // If any required args -- args without defaults -- were left unfilled
+    // by argsmap, that's a problem.
+    if (unfilled.size())
+    {
+        LL_ERRS("LLSDArgsMapper") << _function << " missing required arguments "
+                                  << formatlist(unfilled) << " from " << argsmap << LL_ENDL;
+    }
+
+    // done
+    return args;
+}
+
+std::string LLSDArgsMapper::formatlist(const LLSD& list)
+{
+    std::ostringstream out;
+    const char* delim = "";
+    for (LLSD::array_const_iterator li(list.beginArray()), lend(list.endArray());
+         li != lend; ++li)
+    {
+        out << delim << li->asString();
+        delim = ", ";
+    }
+    return out.str();
+}
 
 LLEventDispatcher::LLEventDispatcher(const std::string& desc, const std::string& key):
     mDesc(desc),
@@ -52,12 +400,181 @@ LLEventDispatcher::~LLEventDispatcher()
 {
 }
 
+/**
+ * DispatchEntry subclass used for callables accepting(const LLSD&)
+ */
+struct LLEventDispatcher::LLSDDispatchEntry: public LLEventDispatcher::DispatchEntry
+{
+    LLSDDispatchEntry(const std::string& desc, const Callable& func, const LLSD& required):
+        DispatchEntry(desc),
+        mFunc(func),
+        mRequired(required)
+    {}
+
+    Callable mFunc;
+    LLSD mRequired;
+
+    virtual void call(const std::string& desc, const LLSD& event) const
+    {
+        // Validate the syntax of the event itself.
+        std::string mismatch(llsd_matches(mRequired, event));
+        if (! mismatch.empty())
+        {
+            LL_ERRS("LLEventDispatcher") << desc << ": bad request: " << mismatch << LL_ENDL;
+        }
+        // Event syntax looks good, go for it!
+        mFunc(event);
+    }
+
+    virtual LLSD addMetadata(LLSD meta) const
+    {
+        meta["required"] = mRequired;
+        return meta;
+    }
+};
+
+/**
+ * DispatchEntry subclass for passing LLSD to functions accepting
+ * arbitrary argument types (convertible via LLSDParam)
+ */
+struct LLEventDispatcher::ParamsDispatchEntry: public LLEventDispatcher::DispatchEntry
+{
+    ParamsDispatchEntry(const std::string& desc, const invoker_function& func):
+        DispatchEntry(desc),
+        mInvoker(func)
+    {}
+
+    invoker_function mInvoker;
+
+    virtual void call(const std::string& desc, const LLSD& event) const
+    {
+        LLSDArgsSource src(desc, event);
+        mInvoker(boost::bind(&LLSDArgsSource::next, boost::ref(src)));
+    }
+};
+
+/**
+ * DispatchEntry subclass for dispatching LLSD::Array to functions accepting
+ * arbitrary argument types (convertible via LLSDParam)
+ */
+struct LLEventDispatcher::ArrayParamsDispatchEntry: public LLEventDispatcher::ParamsDispatchEntry
+{
+    ArrayParamsDispatchEntry(const std::string& desc, const invoker_function& func,
+                             LLSD::Integer arity):
+        ParamsDispatchEntry(desc, func),
+        mArity(arity)
+    {}
+
+    LLSD::Integer mArity;
+
+    virtual LLSD addMetadata(LLSD meta) const
+    {
+        LLSD array(LLSD::emptyArray());
+        // Resize to number of arguments required
+        if (mArity)
+            array[mArity - 1] = LLSD();
+        llassert_always(array.size() == mArity);
+        meta["required"] = array;
+        return meta;
+    }
+};
+
+/**
+ * DispatchEntry subclass for dispatching LLSD::Map to functions accepting
+ * arbitrary argument types (convertible via LLSDParam)
+ */
+struct LLEventDispatcher::MapParamsDispatchEntry: public LLEventDispatcher::ParamsDispatchEntry
+{
+    MapParamsDispatchEntry(const std::string& name, const std::string& desc,
+                           const invoker_function& func,
+                           const LLSD& params, const LLSD& defaults):
+        ParamsDispatchEntry(desc, func),
+        mMapper(name, params, defaults),
+        mRequired(LLSD::emptyMap())
+    {
+        // Build the set of all param keys, then delete the ones that are
+        // optional. What's left are the ones that are required.
+        for (LLSD::array_const_iterator pi(params.beginArray()), pend(params.endArray());
+             pi != pend; ++pi)
+        {
+            mRequired[pi->asString()] = LLSD();
+        }
+
+        if (defaults.isArray() || defaults.isUndefined())
+        {
+            // Right-align the params and defaults arrays.
+            LLSD::Integer offset = params.size() - defaults.size();
+            // Now the name of every defaults[i] is at params[i + offset].
+            for (LLSD::Integer i(0), iend(defaults.size()); i < iend; ++i)
+            {
+                // Erase this optional param from mRequired.
+                mRequired.erase(params[i + offset].asString());
+                // Instead, make an entry in mOptional with the default
+                // param's name and value.
+                mOptional[params[i + offset].asString()] = defaults[i];
+            }
+        }
+        else if (defaults.isMap())
+        {
+            // if defaults is already a map, then it's already in the form we
+            // intend to deliver in metadata
+            mOptional = defaults;
+            // Just delete from mRequired every key appearing in mOptional.
+            for (LLSD::map_const_iterator mi(mOptional.beginMap()), mend(mOptional.endMap());
+                 mi != mend; ++mi)
+            {
+                mRequired.erase(mi->first);
+            }
+        }
+    }
+
+    LLSDArgsMapper mMapper;
+    LLSD mRequired;
+    LLSD mOptional;
+
+    virtual void call(const std::string& desc, const LLSD& event) const
+    {
+        // Just convert from LLSD::Map to LLSD::Array using mMapper, then pass
+        // to base-class call() method.
+        ParamsDispatchEntry::call(desc, mMapper.map(event));
+    }
+
+    virtual LLSD addMetadata(LLSD meta) const
+    {
+        meta["required"] = mRequired;
+        meta["optional"] = mOptional;
+        return meta;
+    }
+};
+
+void LLEventDispatcher::addArrayParamsDispatchEntry(const std::string& name,
+                                                    const std::string& desc,
+                                                    const invoker_function& invoker,
+                                                    LLSD::Integer arity)
+{
+    mDispatch.insert(
+        DispatchMap::value_type(name, DispatchMap::mapped_type(
+                                    new ArrayParamsDispatchEntry(desc, invoker, arity))));
+}
+
+void LLEventDispatcher::addMapParamsDispatchEntry(const std::string& name,
+                                                  const std::string& desc,
+                                                  const invoker_function& invoker,
+                                                  const LLSD& params,
+                                                  const LLSD& defaults)
+{
+    mDispatch.insert(
+        DispatchMap::value_type(name, DispatchMap::mapped_type(
+                                    new MapParamsDispatchEntry(name, desc, invoker, params, defaults))));
+}
+
 /// Register a callable by name
 void LLEventDispatcher::add(const std::string& name, const std::string& desc,
                             const Callable& callable, const LLSD& required)
 {
-    mDispatch.insert(DispatchMap::value_type(name,
-                                             DispatchMap::mapped_type(callable, desc, required)));
+    mDispatch.insert(
+        DispatchMap::value_type(name, DispatchMap::mapped_type(
+                                    new LLSDDispatchEntry(desc, callable, required))));
 }
 
 void LLEventDispatcher::addFail(const std::string& name, const std::string& classname) const
@@ -83,7 +600,7 @@ bool LLEventDispatcher::remove(const std::string& name)
 /// such callable exists, die with LL_ERRS.
 void LLEventDispatcher::operator()(const std::string& name, const LLSD& event) const
 {
-    if (! attemptCall(name, event))
+    if (! try_call(name, event))
     {
         LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << "): '" << name
                                      << "' not found" << LL_ENDL;
@@ -98,44 +615,29 @@ void LLEventDispatcher::operator()(const LLSD& event) const
     // This could/should be implemented in terms of the two-arg overload.
     // However -- we can produce a more informative error message.
     std::string name(event[mKey]);
-    if (! attemptCall(name, event))
+    if (! try_call(name, event))
     {
         LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << "): bad " << mKey
                                      << " value '" << name << "'" << LL_ENDL;
     }
 }
 
-bool LLEventDispatcher::attemptCall(const std::string& name, const LLSD& event) const
+bool LLEventDispatcher::try_call(const LLSD& event) const
 {
-    DispatchMap::const_iterator found = mDispatch.find(name);
-    if (found == mDispatch.end())
-    {
-        // The reason we only return false, leaving it up to our caller to die
-        // with LL_ERRS, is that different callers have different amounts of
-        // available information.
-        return false;
-    }
-    // Found the name, so it's plausible to even attempt the call. But first,
-    // validate the syntax of the event itself.
-    std::string mismatch(llsd_matches(found->second.mRequired, event));
-    if (! mismatch.empty())
-    {
-        LL_ERRS("LLEventDispatcher") << "LLEventDispatcher(" << mDesc << ") calling '" << name
-                                     << "': bad request: " << mismatch << LL_ENDL;
-    }
-    // Event syntax looks good, go for it!
-    (found->second.mFunc)(event);
-    return true;                    // tell caller we were able to call
+    return try_call(event[mKey], event);
 }
 
-LLEventDispatcher::Callable LLEventDispatcher::get(const std::string& name) const
+bool LLEventDispatcher::try_call(const std::string& name, const LLSD& event) const
 {
     DispatchMap::const_iterator found = mDispatch.find(name);
     if (found == mDispatch.end())
     {
-        return Callable();
+        return false;
     }
-    return found->second.mFunc;
+    // Found the name, so it's plausible to even attempt the call.
+    found->second->call(STRINGIZE("LLEventDispatcher(" << mDesc << ") calling '" << name << "'"),
+                        event);
+    return true;                    // tell caller we were able to call
 }
 
 LLSD LLEventDispatcher::getMetadata(const std::string& name) const
@@ -147,9 +649,8 @@ LLSD LLEventDispatcher::getMetadata(const std::string& name) const
     }
     LLSD meta;
     meta["name"] = name;
-    meta["desc"] = found->second.mDesc;
-    meta["required"] = found->second.mRequired;
-    return meta;
+    meta["desc"] = found->second->mDesc;
+    return found->second->addMetadata(meta);
 }
 
 LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key):
@@ -164,3 +665,8 @@ bool LLDispatchListener::process(const LLSD& event)
     (*this)(event);
     return false;
 }
+
+LLEventDispatcher::DispatchEntry::DispatchEntry(const std::string& desc):
+    mDesc(desc)
+{}
+
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index dfffd59eb6481cc0064494832db7b62d41de6d22..7acc61de4ecd76b3a02a01741857b48d3cb1c23f 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -27,18 +27,56 @@
  * 
  * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
  * $/LicenseInfo$
+ *
+ * The invoker machinery that constructs a boost::fusion argument list for use
+ * with boost::fusion::invoke() is derived from
+ * http://www.boost.org/doc/libs/1_45_0/libs/function_types/example/interpreter.hpp
+ * whose license information is copied below:
+ *
+ * "(C) Copyright Tobias Schwinger
+ *
+ * Use modification and distribution are subject to the boost Software License,
+ * Version 1.0. (See http://www.boost.org/LICENSE_1_0.txt)."
  */
 
 #if ! defined(LL_LLEVENTDISPATCHER_H)
 #define LL_LLEVENTDISPATCHER_H
 
+// nil is too generic a term to be allowed to be a global macro. In
+// particular, boost::fusion defines a 'class nil' (properly encapsulated in a
+// namespace) that a global 'nil' macro breaks badly.
+#if defined(nil)
+// Capture the value of the macro 'nil', hoping int is an appropriate type.
+static const int nil_(nil);
+// Now forget the macro.
+#undef nil
+// Finally, reintroduce 'nil' as a properly-scoped alias for the previously-
+// defined const 'nil_'. Make it static since otherwise it produces duplicate-
+// symbol link errors later.
+static const int& nil(nil_);
+#endif
+
 #include <string>
-#include <map>
+#include <boost/shared_ptr.hpp>
 #include <boost/function.hpp>
 #include <boost/bind.hpp>
 #include <boost/iterator/transform_iterator.hpp>
+#include <boost/utility/enable_if.hpp>
+#include <boost/function_types/is_nonmember_callable_builtin.hpp>
+#include <boost/function_types/parameter_types.hpp>
+#include <boost/function_types/function_arity.hpp>
+#include <boost/type_traits/remove_cv.hpp>
+#include <boost/type_traits/remove_reference.hpp>
+#include <boost/fusion/include/push_back.hpp>
+#include <boost/fusion/include/cons.hpp>
+#include <boost/fusion/include/invoke.hpp>
+#include <boost/mpl/begin.hpp>
+#include <boost/mpl/end.hpp>
+#include <boost/mpl/next.hpp>
+#include <boost/mpl/deref.hpp>
 #include <typeinfo>
 #include "llevents.h"
+#include "llsdutil.h"
 
 class LLSD;
 
@@ -54,12 +92,18 @@ class LL_COMMON_API LLEventDispatcher
     LLEventDispatcher(const std::string& desc, const std::string& key);
     virtual ~LLEventDispatcher();
 
-    /// Accept any C++ callable, typically a boost::bind() expression
+    /// @name Register functions accepting(const LLSD&)
+    //@{
+
+    /// Accept any C++ callable with the right signature, typically a
+    /// boost::bind() expression
     typedef boost::function<void(const LLSD&)> Callable;
 
     /**
-     * Register a @a callable by @a name. The optional @a required parameter
-     * is used to validate the structure of each incoming event (see
+     * Register a @a callable by @a name. The passed @a callable accepts a
+     * single LLSD value and uses it in any way desired, e.g. extract
+     * parameters and call some other function. The optional @a required
+     * parameter is used to validate the structure of each incoming event (see
      * llsd_matches()).
      */
     void add(const std::string& name,
@@ -67,10 +111,24 @@ class LL_COMMON_API LLEventDispatcher
              const Callable& callable,
              const LLSD& required=LLSD());
 
+    /**
+     * The case of a free function (or static method) accepting(const LLSD&)
+     * could also be intercepted by the arbitrary-args overload below. Ensure
+     * that it's directed to the Callable overload above instead.
+     */
+    void add(const std::string& name,
+             const std::string& desc,
+             void (*f)(const LLSD&),
+             const LLSD& required=LLSD())
+    {
+        add(name, desc, Callable(f), required);
+    }
+
     /**
      * Special case: a subclass of this class can pass an unbound member
-     * function pointer without explicitly specifying the
-     * <tt>boost::bind()</tt> expression.
+     * function pointer (of an LLEventDispatcher subclass) without explicitly
+     * specifying the <tt>boost::bind()</tt> expression. The passed @a method
+     * accepts a single LLSD value, presumably containing other parameters.
      */
     template <class CLASS>
     void add(const std::string& name,
@@ -81,7 +139,8 @@ class LL_COMMON_API LLEventDispatcher
         addMethod<CLASS>(name, desc, method, required);
     }
 
-    /// Overload for both const and non-const methods
+    /// Overload for both const and non-const methods. The passed @a method
+    /// accepts a single LLSD value, presumably containing other parameters.
     template <class CLASS>
     void add(const std::string& name,
              const std::string& desc,
@@ -91,15 +150,106 @@ class LL_COMMON_API LLEventDispatcher
         addMethod<CLASS>(name, desc, method, required);
     }
 
-    /// Convenience: for LLEventDispatcher, not every callable needs a
-    /// documentation string.
-    template <typename CALLABLE>
-    void add(const std::string& name,
-             CALLABLE callable,
-             const LLSD& required=LLSD())
-    {
-        add(name, "", callable, required);
-    }
+    //@}
+
+    /// @name Register functions with arbitrary param lists
+    //@{
+
+    /**
+     * Register a free function with arbitrary parameters. (This also works
+     * for static class methods.)
+     *
+     * @note This supports functions with up to about 6 parameters -- after
+     * that you start getting dismaying compile errors in which
+     * boost::fusion::joint_view is mentioned a surprising number of times.
+     *
+     * When calling this name, pass an LLSD::Array. Each entry in turn will be
+     * converted to the corresponding parameter type using LLSDParam.
+     */
+    template<typename Function>
+    typename boost::enable_if< boost::function_types::is_nonmember_callable_builtin<Function>
+                               >::type add(const std::string& name,
+                                           const std::string& desc,
+                                           Function f);
+
+    /**
+     * Register a nonstatic class method with arbitrary parameters.
+     *
+     * @note This supports functions with up to about 6 parameters -- after
+     * that you start getting dismaying compile errors in which
+     * boost::fusion::joint_view is mentioned a surprising number of times.
+     *
+     * To cover cases such as a method on an LLSingleton we don't yet want to
+     * instantiate, instead of directly storing an instance pointer, accept a
+     * nullary callable returning a pointer/reference to the desired class
+     * instance. If you already have an instance in hand,
+     * boost::lambda::var(instance) or boost::lambda::constant(instance_ptr)
+     * produce suitable callables.
+     *
+     * When calling this name, pass an LLSD::Array. Each entry in turn will be
+     * converted to the corresponding parameter type using LLSDParam.
+     */
+    template<typename Method, typename InstanceGetter>
+    typename boost::enable_if< boost::function_types::is_member_function_pointer<Method>
+                               >::type add(const std::string& name,
+                                           const std::string& desc,
+                                           Method f,
+                                           const InstanceGetter& getter);
+
+    /**
+     * Register a free function with arbitrary parameters. (This also works
+     * for static class methods.)
+     *
+     * @note This supports functions with up to about 6 parameters -- after
+     * that you start getting dismaying compile errors in which
+     * boost::fusion::joint_view is mentioned a surprising number of times.
+     *
+     * Pass an LLSD::Array of parameter names, and optionally another
+     * LLSD::Array of default parameter values, a la LLSDArgsMapper.
+     *
+     * When calling this name, pass an LLSD::Map. We will internally generate
+     * an LLSD::Array using LLSDArgsMapper and then convert each entry in turn
+     * to the corresponding parameter type using LLSDParam.
+     */
+    template<typename Function>
+    typename boost::enable_if< boost::function_types::is_nonmember_callable_builtin<Function>
+                               >::type add(const std::string& name,
+                                           const std::string& desc,
+                                           Function f,
+                                           const LLSD& params,
+                                           const LLSD& defaults=LLSD());
+
+    /**
+     * Register a nonstatic class method with arbitrary parameters.
+     *
+     * @note This supports functions with up to about 6 parameters -- after
+     * that you start getting dismaying compile errors in which
+     * boost::fusion::joint_view is mentioned a surprising number of times.
+     *
+     * To cover cases such as a method on an LLSingleton we don't yet want to
+     * instantiate, instead of directly storing an instance pointer, accept a
+     * nullary callable returning a pointer/reference to the desired class
+     * instance. If you already have an instance in hand,
+     * boost::lambda::var(instance) or boost::lambda::constant(instance_ptr)
+     * produce suitable callables.
+     *
+     * Pass an LLSD::Array of parameter names, and optionally another
+     * LLSD::Array of default parameter values, a la LLSDArgsMapper.
+     *
+     * When calling this name, pass an LLSD::Map. We will internally generate
+     * an LLSD::Array using LLSDArgsMapper and then convert each entry in turn
+     * to the corresponding parameter type using LLSDParam.
+     */
+    template<typename Method, typename InstanceGetter>
+    typename boost::enable_if< boost::function_types::is_member_function_pointer<Method>
+                               >::type add(const std::string& name,
+                                           const std::string& desc,
+                                           Method f,
+                                           const InstanceGetter& getter,
+                                           const LLSD& params,
+                                           const LLSD& defaults=LLSD());
+
+    //@}    
 
     /// Unregister a callable
     bool remove(const std::string& name);
@@ -109,12 +259,25 @@ class LL_COMMON_API LLEventDispatcher
     /// the @a required prototype specified at add() time, die with LL_ERRS.
     void operator()(const std::string& name, const LLSD& event) const;
 
+    /// Call a registered callable with an explicitly-specified name and
+    /// return <tt>true</tt>. If no such callable exists, return
+    /// <tt>false</tt>. If the @a event fails to match the @a required
+    /// prototype specified at add() time, die with LL_ERRS.
+    bool try_call(const std::string& name, const LLSD& event) const;
+
     /// Extract the @a key value from the incoming @a event, and call the
     /// callable whose name is specified by that map @a key. If no such
     /// callable exists, die with LL_ERRS. If the @a event fails to match the
     /// @a required prototype specified at add() time, die with LL_ERRS.
     void operator()(const LLSD& event) const;
 
+    /// Extract the @a key value from the incoming @a event, call the callable
+    /// whose name is specified by that map @a key and return <tt>true</tt>.
+    /// If no such callable exists, return <tt>false</tt>. If the @a event
+    /// fails to match the @a required prototype specified at add() time, die
+    /// with LL_ERRS.
+    bool try_call(const LLSD& event) const;
+
     /// @name Iterate over defined names
     //@{
     typedef std::pair<std::string, std::string> NameDesc;
@@ -122,16 +285,22 @@ class LL_COMMON_API LLEventDispatcher
 private:
     struct DispatchEntry
     {
-        DispatchEntry(const Callable& func, const std::string& desc, const LLSD& required):
-            mFunc(func),
-            mDesc(desc),
-            mRequired(required)
-        {}
-        Callable mFunc;
+        DispatchEntry(const std::string& desc);
+        virtual ~DispatchEntry() {} // suppress MSVC warning, sigh
+
         std::string mDesc;
-        LLSD mRequired;
+
+        virtual void call(const std::string& desc, const LLSD& event) const = 0;
+        virtual LLSD addMetadata(LLSD) const = 0;
     };
-    typedef std::map<std::string, DispatchEntry> DispatchMap;
+    // Tried using boost::ptr_map<std::string, DispatchEntry>, but ptr_map<>
+    // wants its value type to be "clonable," even just to dereference an
+    // iterator. I don't want to clone entries -- if I have to copy an entry
+    // around, I want it to continue pointing to the same DispatchEntry
+    // subclass object. However, I definitely want DispatchMap to destroy
+    // DispatchEntry if no references are outstanding at the time an entry is
+    // removed. This looks like a job for boost::shared_ptr.
+    typedef std::map<std::string, boost::shared_ptr<DispatchEntry> > DispatchMap;
 
 public:
     /// We want the flexibility to redefine what data we store per name,
@@ -149,10 +318,6 @@ class LL_COMMON_API LLEventDispatcher
     }
     //@}
 
-    /// Fetch the Callable for the specified name. If no such name was
-    /// registered, return an empty() Callable.
-    Callable get(const std::string& name) const;
-
     /// Get information about a specific Callable
     LLSD getMetadata(const std::string& name) const;
 
@@ -175,18 +340,184 @@ class LL_COMMON_API LLEventDispatcher
         }
     }
     void addFail(const std::string& name, const std::string& classname) const;
-    /// try to dispatch, return @c true if success
-    bool attemptCall(const std::string& name, const LLSD& event) const;
 
     std::string mDesc, mKey;
     DispatchMap mDispatch;
 
     static NameDesc makeNameDesc(const DispatchMap::value_type& item)
     {
-        return NameDesc(item.first, item.second.mDesc);
+        return NameDesc(item.first, item.second->mDesc);
+    }
+
+    struct LLSDDispatchEntry;
+    struct ParamsDispatchEntry;
+    struct ArrayParamsDispatchEntry;
+    struct MapParamsDispatchEntry;
+
+    // Step 2 of parameter analysis. Instantiating invoker<some_function_type>
+    // implicitly sets its From and To parameters to the (compile time) begin
+    // and end iterators over that function's parameter types.
+    template< typename Function
+              , class From = typename boost::mpl::begin< boost::function_types::parameter_types<Function> >::type
+              , class To   = typename boost::mpl::end< boost::function_types::parameter_types<Function> >::type
+              >
+    struct invoker;
+
+    // deliver LLSD arguments one at a time
+    typedef boost::function<LLSD()> args_source;
+    // obtain args from an args_source to build param list and call target
+    // function
+    typedef boost::function<void(const args_source&)> invoker_function;
+
+    template <typename Function>
+    invoker_function make_invoker(Function f);
+    template <typename Method, typename InstanceGetter>
+    invoker_function make_invoker(Method f, const InstanceGetter& getter);
+    void addArrayParamsDispatchEntry(const std::string& name,
+                                     const std::string& desc,
+                                     const invoker_function& invoker,
+                                     LLSD::Integer arity);
+    void addMapParamsDispatchEntry(const std::string& name,
+                                   const std::string& desc,
+                                   const invoker_function& invoker,
+                                   const LLSD& params,
+                                   const LLSD& defaults);
+};
+
+/*****************************************************************************
+*   LLEventDispatcher template implementation details
+*****************************************************************************/
+// Step 3 of parameter analysis, the recursive case.
+template<typename Function, class From, class To>
+struct LLEventDispatcher::invoker
+{
+    template<typename T>
+    struct remove_cv_ref
+        : boost::remove_cv< typename boost::remove_reference<T>::type >
+    { };
+
+    // apply() accepts an arbitrary boost::fusion sequence as args. It
+    // examines the next parameter type in the parameter-types sequence
+    // bounded by From and To, obtains the next LLSD object from the passed
+    // args_source and constructs an LLSDParam of appropriate type to try
+    // to convert the value. It then recurs with the next parameter-types
+    // iterator, passing the args sequence thus far.
+    template<typename Args>
+    static inline
+    void apply(Function func, const args_source& argsrc, Args const & args)
+    {
+        typedef typename boost::mpl::deref<From>::type arg_type;
+        typedef typename boost::mpl::next<From>::type next_iter_type;
+        typedef typename remove_cv_ref<arg_type>::type plain_arg_type;
+
+        invoker<Function, next_iter_type, To>::apply
+        ( func, argsrc, boost::fusion::push_back(args, LLSDParam<plain_arg_type>(argsrc())));
+    }
+
+    // Special treatment for instance (first) parameter of a non-static member
+    // function. Accept the instance-getter callable, calling that to produce
+    // the first args value. Since we know we're at the top of the recursion
+    // chain, we need not also require a partial args sequence from our caller.
+    template <typename InstanceGetter>
+    static inline
+    void method_apply(Function func, const args_source& argsrc, const InstanceGetter& getter)
+    {
+        typedef typename boost::mpl::next<From>::type next_iter_type;
+
+        // Instead of grabbing the first item from argsrc and making an
+        // LLSDParam of it, call getter() and pass that as the instance param.
+        invoker<Function, next_iter_type, To>::apply
+        ( func, argsrc, boost::fusion::push_back(boost::fusion::nil(), boost::ref(getter())));
+    }
+};
+
+// Step 4 of parameter analysis, the leaf case. When the general
+// invoker<Function, From, To> logic has advanced From until it matches To,
+// the compiler will pick this template specialization.
+template<typename Function, class To>
+struct LLEventDispatcher::invoker<Function,To,To>
+{
+    // the argument list is complete, now call the function
+    template<typename Args>
+    static inline
+    void apply(Function func, const args_source&, Args const & args)
+    {
+        boost::fusion::invoke(func, args);
     }
 };
 
+template<typename Function>
+typename boost::enable_if< boost::function_types::is_nonmember_callable_builtin<Function> >::type
+LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f)
+{
+    // Construct an invoker_function, a callable accepting const args_source&.
+    // Add to DispatchMap an ArrayParamsDispatchEntry that will handle the
+    // caller's LLSD::Array.
+    addArrayParamsDispatchEntry(name, desc, make_invoker(f),
+                                boost::function_types::function_arity<Function>::value);
+}
+
+template<typename Method, typename InstanceGetter>
+typename boost::enable_if< boost::function_types::is_member_function_pointer<Method> >::type
+LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
+                       const InstanceGetter& getter)
+{
+    // Subtract 1 from the compile-time arity because the getter takes care of
+    // the first parameter. We only need (arity - 1) additional arguments.
+    addArrayParamsDispatchEntry(name, desc, make_invoker(f, getter),
+                                boost::function_types::function_arity<Method>::value - 1);
+}
+
+template<typename Function>
+typename boost::enable_if< boost::function_types::is_nonmember_callable_builtin<Function> >::type
+LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f,
+                       const LLSD& params, const LLSD& defaults)
+{
+    // See comments for previous is_nonmember_callable_builtin add().
+    addMapParamsDispatchEntry(name, desc, make_invoker(f), params, defaults);
+}
+
+template<typename Method, typename InstanceGetter>
+typename boost::enable_if< boost::function_types::is_member_function_pointer<Method> >::type
+LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
+                       const InstanceGetter& getter,
+                       const LLSD& params, const LLSD& defaults)
+{
+    addMapParamsDispatchEntry(name, desc, make_invoker(f, getter), params, defaults);
+}
+
+template <typename Function>
+LLEventDispatcher::invoker_function
+LLEventDispatcher::make_invoker(Function f)
+{
+    // Step 1 of parameter analysis, the top of the recursion. Passing a
+    // suitable f (see add()'s enable_if condition) to this method causes it
+    // to infer the function type; specifying that function type to invoker<>
+    // causes it to fill in the begin/end MPL iterators over the function's
+    // list of parameter types.
+    // While normally invoker::apply() could infer its template type from the
+    // boost::fusion::nil parameter value, here we must be explicit since
+    // we're boost::bind()ing it rather than calling it directly.
+    return boost::bind(&invoker<Function>::template apply<boost::fusion::nil>,
+                       f,
+                       _1,
+                       boost::fusion::nil());
+}
+
+template <typename Method, typename InstanceGetter>
+LLEventDispatcher::invoker_function
+LLEventDispatcher::make_invoker(Method f, const InstanceGetter& getter)
+{
+    // Use invoker::method_apply() to treat the instance (first) arg specially.
+    return boost::bind(&invoker<Method>::template method_apply<InstanceGetter>,
+                       f,
+                       _1,
+                       getter);
+}
+
+/*****************************************************************************
+*   LLDispatchListener
+*****************************************************************************/
 /**
  * Bundle an LLEventPump and a listener with an LLEventDispatcher. A class
  * that contains (or derives from) LLDispatchListener need only specify the
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index 97e2bdeb5754288521d26fe379b987f19312a2d0..ff03506e841a8771913895f0a065934cd5f840ef 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -588,3 +588,16 @@ void LLReqID::stamp(LLSD& response) const
     }
     response["reqid"] = mReqid;
 }
+
+bool sendReply(const LLSD& reply, const LLSD& request, const std::string& replyKey)
+{
+    // Copy 'reply' to modify it.
+    LLSD newreply(reply);
+    // Get the ["reqid"] element from request
+    LLReqID reqID(request);
+    // and copy it to 'newreply'.
+    reqID.stamp(newreply);
+    // Send reply on LLEventPump named in request[replyKey]. Don't forget to
+    // send the modified 'newreply' instead of the original 'reply'.
+    return LLEventPumps::instance().obtain(request[replyKey]).post(newreply);
+}
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 2491cf1371f6821bb72f94ee3f12692321e2b9aa..65b0fef354eab6f78efaea6103d01f27dc1104d8 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -691,6 +691,20 @@ class LL_COMMON_API LLReqID
     LLSD mReqid;
 };
 
+/**
+ * Conventionally send a reply to a request event.
+ *
+ * @a reply is the LLSD reply event to send
+ * @a request is the corresponding LLSD request event
+ * @a replyKey is the key in the @a request event, conventionally ["reply"],
+ * whose value is the name of the LLEventPump on which to send the reply.
+ *
+ * Before sending the reply event, sendReply() copies the ["reqid"] item from
+ * the request to the reply.
+ */
+LL_COMMON_API bool sendReply(const LLSD& reply, const LLSD& request,
+                             const std::string& replyKey="reply");
+
 /**
  * Base class for LLListenerWrapper. See visit_and_connect() and llwrap(). We
  * provide virtual @c accept_xxx() methods, customization points allowing a
diff --git a/indra/llcommon/llsdutil.cpp b/indra/llcommon/llsdutil.cpp
index f8f9ece058cdcc8182f02b48be1511a778704d0a..803417d368b899d81a9cc8c8aca09e925103a097 100644
--- a/indra/llcommon/llsdutil.cpp
+++ b/indra/llcommon/llsdutil.cpp
@@ -41,6 +41,7 @@
 
 #include "llsdserialize.h"
 #include "stringize.h"
+#include "is_approx_equal_fraction.h"
 
 #include <map>
 #include <set>
@@ -571,7 +572,7 @@ std::string llsd_matches(const LLSD& prototype, const LLSD& data, const std::str
     return match_types(prototype.type(), TypeVector(), data.type(), pfx);
 }
 
-bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
+bool llsd_equals(const LLSD& lhs, const LLSD& rhs, unsigned bits)
 {
     // We're comparing strict equality of LLSD representation rather than
     // performing any conversions. So if the types aren't equal, the LLSD
@@ -588,6 +589,20 @@ bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
         // Both are TypeUndefined. There's nothing more to know.
         return true;
 
+    case LLSD::TypeReal:
+        // This is where the 'bits' argument comes in handy. If passed
+        // explicitly, it means to use is_approx_equal_fraction() to compare.
+        if (bits >= 0)
+        {
+            return is_approx_equal_fraction(lhs.asReal(), rhs.asReal(), bits);
+        }
+        // Otherwise we compare bit representations, and the usual caveats
+        // about comparing floating-point numbers apply. Omitting 'bits' when
+        // comparing Real values is only useful when we expect identical bit
+        // representation for a given Real value, e.g. for integer-valued
+        // Reals.
+        return (lhs.asReal() == rhs.asReal());
+
 #define COMPARE_SCALAR(type)                                    \
     case LLSD::Type##type:                                      \
         /* LLSD::URI has operator!=() but not operator==() */   \
@@ -596,10 +611,6 @@ bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
 
     COMPARE_SCALAR(Boolean);
     COMPARE_SCALAR(Integer);
-    // The usual caveats about comparing floating-point numbers apply. This is
-    // only useful when we expect identical bit representation for a given
-    // Real value, e.g. for integer-valued Reals.
-    COMPARE_SCALAR(Real);
     COMPARE_SCALAR(String);
     COMPARE_SCALAR(UUID);
     COMPARE_SCALAR(Date);
@@ -617,7 +628,7 @@ bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
         for ( ; lai != laend && rai != raend; ++lai, ++rai)
         {
             // If any one array element is unequal, the arrays are unequal.
-            if (! llsd_equals(*lai, *rai))
+            if (! llsd_equals(*lai, *rai, bits))
                 return false;
         }
         // Here we've reached the end of one or the other array. They're equal
@@ -644,7 +655,7 @@ bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
             if (rhskeys.erase(lmi->first) != 1)
                 return false;
             // Both maps have the current key. Compare values.
-            if (! llsd_equals(lmi->second, rhs[lmi->first]))
+            if (! llsd_equals(lmi->second, rhs[lmi->first], bits))
                 return false;
         }
         // We've now established that all the lhs keys have equal values in
@@ -657,7 +668,7 @@ bool llsd_equals(const LLSD& lhs, const LLSD& rhs)
         // We expect that every possible type() value is specifically handled
         // above. Failing to extend this switch to support a new LLSD type is
         // an error that must be brought to the coder's attention.
-        LL_ERRS("llsd_equals") << "llsd_equals(" << lhs << ", " << rhs << "): "
+        LL_ERRS("llsd_equals") << "llsd_equals(" << lhs << ", " << rhs << ", " << bits << "): "
             "unknown type " << lhs.type() << LL_ENDL;
         return false;               // pacify the compiler
     }
diff --git a/indra/llcommon/llsdutil.h b/indra/llcommon/llsdutil.h
index bb8c0690b13ed8b922e883f00f267c66ff90d0e1..65c7297cbf8a7151db93f6d20935717b05e62bb8 100644
--- a/indra/llcommon/llsdutil.h
+++ b/indra/llcommon/llsdutil.h
@@ -123,8 +123,10 @@ LL_COMMON_API BOOL compare_llsd_with_template(
  */
 LL_COMMON_API std::string llsd_matches(const LLSD& prototype, const LLSD& data, const std::string& pfx="");
 
-/// Deep equality
-LL_COMMON_API bool llsd_equals(const LLSD& lhs, const LLSD& rhs);
+/// Deep equality. If you want to compare LLSD::Real values for approximate
+/// equality rather than bitwise equality, pass @a bits as for
+/// is_approx_equal_fraction().
+LL_COMMON_API bool llsd_equals(const LLSD& lhs, const LLSD& rhs, unsigned bits=-1);
 
 // Simple function to copy data out of input & output iterators if
 // there is no need for casting.
@@ -138,4 +140,283 @@ template<typename Input> LLSD llsd_copy_array(Input iter, Input end)
 	return dest;
 }
 
+/*****************************************************************************
+*   LLSDArray
+*****************************************************************************/
+/**
+ * Construct an LLSD::Array inline, with implicit conversion to LLSD. Usage:
+ *
+ * @code
+ * void somefunc(const LLSD&);
+ * ...
+ * somefunc(LLSDArray("text")(17)(3.14));
+ * @endcode
+ *
+ * For completeness, LLSDArray() with no args constructs an empty array, so
+ * <tt>LLSDArray()("text")(17)(3.14)</tt> produces an array equivalent to the
+ * above. But for most purposes, LLSD() is already equivalent to an empty
+ * array, and if you explicitly want an empty isArray(), there's
+ * LLSD::emptyArray(). However, supporting a no-args LLSDArray() constructor
+ * follows the principle of least astonishment.
+ */
+class LLSDArray
+{
+public:
+    LLSDArray():
+        _data(LLSD::emptyArray())
+    {}
+
+    /**
+     * Need an explicit copy constructor. Consider the following:
+     *
+     * @code
+     * LLSD array_of_arrays(LLSDArray(LLSDArray(17)(34))
+     *                               (LLSDArray("x")("y")));
+     * @endcode
+     *
+     * The coder intends to construct [[17, 34], ["x", "y"]].
+     *
+     * With the compiler's implicit copy constructor, s/he gets instead
+     * [17, 34, ["x", "y"]].
+     *
+     * The expression LLSDArray(17)(34) constructs an LLSDArray with those two
+     * values. The reader assumes it should be converted to LLSD, as we always
+     * want with LLSDArray, before passing it to the @em outer LLSDArray
+     * constructor! This copy constructor makes that happen.
+     */
+    LLSDArray(const LLSDArray& inner):
+        _data(LLSD::emptyArray())
+    {
+        _data.append(inner);
+    }
+
+    LLSDArray(const LLSD& value):
+        _data(LLSD::emptyArray())
+    {
+        _data.append(value);
+    }
+
+    LLSDArray& operator()(const LLSD& value)
+    {
+        _data.append(value);
+        return *this;
+    }
+
+    operator LLSD() const { return _data; }
+    LLSD get() const { return _data; }
+
+private:
+    LLSD _data;
+};
+
+/*****************************************************************************
+*   LLSDMap
+*****************************************************************************/
+/**
+ * Construct an LLSD::Map inline, with implicit conversion to LLSD. Usage:
+ *
+ * @code
+ * void somefunc(const LLSD&);
+ * ...
+ * somefunc(LLSDMap("alpha", "abc")("number", 17)("pi", 3.14));
+ * @endcode
+ *
+ * For completeness, LLSDMap() with no args constructs an empty map, so
+ * <tt>LLSDMap()("alpha", "abc")("number", 17)("pi", 3.14)</tt> produces a map
+ * equivalent to the above. But for most purposes, LLSD() is already
+ * equivalent to an empty map, and if you explicitly want an empty isMap(),
+ * there's LLSD::emptyMap(). However, supporting a no-args LLSDMap()
+ * constructor follows the principle of least astonishment.
+ */
+class LLSDMap
+{
+public:
+    LLSDMap():
+        _data(LLSD::emptyMap())
+    {}
+    LLSDMap(const LLSD::String& key, const LLSD& value):
+        _data(LLSD::emptyMap())
+    {
+        _data[key] = value;
+    }
+
+    LLSDMap& operator()(const LLSD::String& key, const LLSD& value)
+    {
+        _data[key] = value;
+        return *this;
+    }
+
+    operator LLSD() const { return _data; }
+    LLSD get() const { return _data; }
+
+private:
+    LLSD _data;
+};
+
+/*****************************************************************************
+*   LLSDParam
+*****************************************************************************/
+/**
+ * LLSDParam is a customization point for passing LLSD values to function
+ * parameters of more or less arbitrary type. LLSD provides a small set of
+ * native conversions; but if a generic algorithm explicitly constructs an
+ * LLSDParam object in the function's argument list, a consumer can provide
+ * LLSDParam specializations to support more different parameter types than
+ * LLSD's native conversions.
+ *
+ * Usage:
+ *
+ * @code
+ * void somefunc(const paramtype&);
+ * ...
+ * somefunc(..., LLSDParam<paramtype>(someLLSD), ...);
+ * @endcode
+ */
+template <typename T>
+class LLSDParam
+{
+public:
+    /**
+     * Default implementation converts to T on construction, saves converted
+     * value for later retrieval
+     */
+    LLSDParam(const LLSD& value):
+        _value(value)
+    {}
+
+    operator T() const { return _value; }
+
+private:
+    T _value;
+};
+
+/**
+ * Turns out that several target types could accept an LLSD param using any of
+ * a few different conversions, e.g. LLUUID's constructor can accept LLUUID or
+ * std::string. Therefore, the compiler can't decide which LLSD conversion
+ * operator to choose, even though to us it seems obvious. But that's okay, we
+ * can specialize LLSDParam for such target types, explicitly specifying the
+ * desired conversion -- that's part of what LLSDParam is all about. Turns out
+ * we have to do that enough to make it worthwhile generalizing. Use a macro
+ * because I need to specify one of the asReal, etc., explicit conversion
+ * methods as well as a type. If I'm overlooking a clever way to implement
+ * that using a template instead, feel free to reimplement.
+ */
+#define LLSDParam_for(T, AS)                    \
+template <>                                     \
+class LLSDParam<T>                              \
+{                                               \
+public:                                         \
+    LLSDParam(const LLSD& value):               \
+        _value(value.AS())                      \
+    {}                                          \
+                                                \
+    operator T() const { return _value; }       \
+                                                \
+private:                                        \
+    T _value;                                   \
+}
+
+LLSDParam_for(float,        asReal);
+LLSDParam_for(LLUUID,       asUUID);
+LLSDParam_for(LLDate,       asDate);
+LLSDParam_for(LLURI,        asURI);
+LLSDParam_for(LLSD::Binary, asBinary);
+
+/**
+ * LLSDParam<const char*> is an example of the kind of conversion you can
+ * support with LLSDParam beyond native LLSD conversions. Normally you can't
+ * pass an LLSD object to a function accepting const char* -- but you can
+ * safely pass an LLSDParam<const char*>(yourLLSD).
+ */
+template <>
+class LLSDParam<const char*>
+{
+private:
+    // The difference here is that we store a std::string rather than a const
+    // char*. It's important that the LLSDParam object own the std::string.
+    std::string _value;
+    // We don't bother storing the incoming LLSD object, but we do have to
+    // distinguish whether _value is an empty string because the LLSD object
+    // contains an empty string or because it's isUndefined().
+    bool _undefined;
+
+public:
+    LLSDParam(const LLSD& value):
+        _value(value),
+        _undefined(value.isUndefined())
+    {}
+
+    // The const char* we retrieve is for storage owned by our _value member.
+    // That's how we guarantee that the const char* is valid for the lifetime
+    // of this LLSDParam object. Constructing your LLSDParam in the argument
+    // list should ensure that the LLSDParam object will persist for the
+    // duration of the function call.
+    operator const char*() const
+    {
+        if (_undefined)
+        {
+            // By default, an isUndefined() LLSD object's asString() method
+            // will produce an empty string. But for a function accepting
+            // const char*, it's often important to be able to pass NULL, and
+            // isUndefined() seems like the best way. If you want to pass an
+            // empty string, you can still pass LLSD(""). Without this special
+            // case, though, no LLSD value could pass NULL.
+            return NULL;
+        }
+        return _value.c_str();
+    }
+};
+
+namespace llsd
+{
+
+/*****************************************************************************
+*   BOOST_FOREACH() helpers for LLSD
+*****************************************************************************/
+/// Usage: BOOST_FOREACH(LLSD item, inArray(someLLSDarray)) { ... }
+class inArray
+{
+public:
+    inArray(const LLSD& array):
+        _array(array)
+    {}
+
+    typedef LLSD::array_const_iterator const_iterator;
+    typedef LLSD::array_iterator iterator;
+
+    iterator begin() { return _array.beginArray(); }
+    iterator end()   { return _array.endArray(); }
+    const_iterator begin() const { return _array.beginArray(); }
+    const_iterator end()   const { return _array.endArray(); }
+
+private:
+    LLSD _array;
+};
+
+/// MapEntry is what you get from dereferencing an LLSD::map_[const_]iterator.
+typedef std::map<LLSD::String, LLSD>::value_type MapEntry;
+
+/// Usage: BOOST_FOREACH([const] MapEntry& e, inMap(someLLSDmap)) { ... }
+class inMap
+{
+public:
+    inMap(const LLSD& map):
+        _map(map)
+    {}
+
+    typedef LLSD::map_const_iterator const_iterator;
+    typedef LLSD::map_iterator iterator;
+
+    iterator begin() { return _map.beginMap(); }
+    iterator end()   { return _map.endMap(); }
+    const_iterator begin() const { return _map.beginMap(); }
+    const_iterator end()   const { return _map.endMap(); }
+
+private:
+    LLSD _map;
+};
+
+} // namespace llsd
+
 #endif // LL_LLSDUTIL_H
diff --git a/indra/llcommon/tests/lleventdispatcher_test.cpp b/indra/llcommon/tests/lleventdispatcher_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..263c9b171f97cab9d30e844e016f5e565f6a5524
--- /dev/null
+++ b/indra/llcommon/tests/lleventdispatcher_test.cpp
@@ -0,0 +1,1324 @@
+/**
+ * @file   lleventdispatcher_test.cpp
+ * @author Nat Goodspeed
+ * @date   2011-01-20
+ * @brief  Test for lleventdispatcher.
+ * 
+ * $LicenseInfo:firstyear=2011&license=viewerlgpl$
+ * Copyright (c) 2011, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lleventdispatcher.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "llsd.h"
+#include "llsdutil.h"
+#include "stringize.h"
+#include "tests/wrapllerrs.h"
+
+#include <map>
+#include <string>
+#include <stdexcept>
+
+#include <boost/bind.hpp>
+#include <boost/function.hpp>
+#include <boost/range.hpp>
+#include <boost/foreach.hpp>
+#define foreach BOOST_FOREACH
+
+#include <boost/lambda/lambda.hpp>
+
+#include <iostream>
+#include <iomanip>
+
+using boost::lambda::constant;
+using boost::lambda::constant_ref;
+using boost::lambda::var;
+
+using namespace llsd;
+
+/*****************************************************************************
+*   Output control
+*****************************************************************************/
+#ifdef DEBUG_ON
+using std::cout;
+#else
+static std::ostringstream cout;
+#endif
+
+/*****************************************************************************
+*   Example data, functions, classes
+*****************************************************************************/
+// We don't need a whole lot of different arbitrary-params methods, just (no |
+// (const LLSD&) | arbitrary) args (function | static method | non-static
+// method), where 'arbitrary' is (every LLSD datatype + (const char*)).
+// But we need to register each one under different names for the different
+// registration styles. Don't forget LLEventDispatcher subclass methods(const
+// LLSD&).
+
+// However, the number of target parameter conversions we want to try exceeds
+// boost::fusion::invoke()'s supported parameter-list size. Break out two
+// different lists.
+#define NPARAMSa bool b, int i, float f, double d, const char* cp
+#define NPARAMSb const std::string& s, const LLUUID& uuid, const LLDate& date, \
+                 const LLURI& uri, const std::vector<U8>& bin
+#define NARGSa   b, i, f, d, cp
+#define NARGSb   s, uuid, date, uri, bin
+
+// For some registration methods we need methods on a subclass of
+// LLEventDispatcher. To simplify things, we'll use this Dispatcher subclass
+// for all our testing, including testing its own methods.
+class Dispatcher: public LLEventDispatcher
+{
+public:
+    Dispatcher(const std::string& name, const std::string& key):
+        LLEventDispatcher(name, key)
+    {}
+
+    // sensing member, mutable because we want to know when we've reached our
+    // const method too
+    mutable LLSD llsd;
+
+    void method1(const LLSD& obj) { llsd = obj; }
+    void cmethod1(const LLSD& obj) const { llsd = obj; }
+};
+
+// sensing vars, captured in a struct to make it convenient to clear them
+struct Vars
+{
+    LLSD llsd;
+    bool b;
+    int i;
+    float f;
+    double d;
+    // Capture param passed as char*. But merely storing a char* received from
+    // our caller, possibly the .c_str() from a concatenation expression,
+    // would be Bad: the pointer will be invalidated long before we can query
+    // it. We could allocate a new chunk of memory, copy the string data and
+    // point to that instead -- but hey, guess what, we already have a class
+    // that does that!
+    std::string cp;
+    std::string s;
+    LLUUID uuid;
+    LLDate date;
+    LLURI uri;
+    std::vector<U8> bin;
+
+    Vars():
+        // Only need to initialize the POD types, the rest should take care of
+        // default-constructing themselves.
+        b(false),
+        i(0),
+        f(0),
+        d(0)
+    {}
+
+    // Detect any non-default values for convenient testing
+    LLSD inspect() const
+    {
+        LLSD result;
+
+        if (llsd.isDefined())
+            result["llsd"] = llsd;
+        if (b)
+            result["b"] = b;
+        if (i)
+            result["i"] = i;
+        if (f)
+            result["f"] = f;
+        if (d)
+            result["d"] = d;
+        if (! cp.empty())
+            result["cp"] = cp;
+        if (! s.empty())
+            result["s"] = s;
+        if (uuid != LLUUID())
+            result["uuid"] = uuid;
+        if (date != LLDate())
+            result["date"] = date;
+        if (uri != LLURI())
+            result["uri"] = uri;
+        if (! bin.empty())
+            result["bin"] = bin;
+
+        return result;
+    }
+
+    /*------------- no-args (non-const, const, static) methods -------------*/
+    void method0()
+    {
+        cout << "method0()\n";
+        i = 17;
+    }
+
+    void cmethod0() const
+    {
+        cout << 'c';
+        const_cast<Vars*>(this)->method0();
+    }
+
+    static void smethod0();
+
+    /*------------ Callable (non-const, const, static) methods -------------*/
+    void method1(const LLSD& obj)
+    {
+        cout << "method1(" << obj << ")\n";
+        llsd = obj;
+    }
+
+    void cmethod1(const LLSD& obj) const
+    {
+        cout << 'c';
+        const_cast<Vars*>(this)->method1(obj);
+    }
+
+    static void smethod1(const LLSD& obj);
+
+    /*-------- Arbitrary-params (non-const, const, static) methods ---------*/
+    void methodna(NPARAMSa)
+    {
+        // Because our const char* param cp might be NULL, and because we
+        // intend to capture the value in a std::string, have to distinguish
+        // between the NULL value and any non-NULL value. Use a convention
+        // easy for a human reader: enclose any non-NULL value in single
+        // quotes, reserving the unquoted string "NULL" to represent a NULL ptr.
+        std::string vcp;
+        if (cp == NULL)
+            vcp = "NULL";
+        else
+            vcp = std::string("'") + cp + "'";
+
+        cout << "methodna(" << b
+             << ", " << i
+             << ", " << f
+             << ", " << d
+             << ", " << vcp
+             << ")\n";
+
+        this->b = b;
+        this->i = i;
+        this->f = f;
+        this->d = d;
+        this->cp = vcp;
+    }
+
+    void methodnb(NPARAMSb)
+    {
+        std::ostringstream vbin;
+        foreach(U8 byte, bin)
+        {
+            vbin << std::hex << std::setfill('0') << std::setw(2) << unsigned(byte);
+        }
+
+        cout << "methodnb(" << "'" << s << "'"
+             << ", " << uuid
+             << ", " << date
+             << ", '" << uri << "'"
+             << ", " << vbin.str()
+             << ")\n";
+
+        this->s = s;
+        this->uuid = uuid;
+        this->date = date;
+        this->uri = uri;
+        this->bin = bin;
+    }
+
+    void cmethodna(NPARAMSa) const
+    {
+        cout << 'c';
+        const_cast<Vars*>(this)->methodna(NARGSa);
+    }
+
+    void cmethodnb(NPARAMSb) const
+    {
+        cout << 'c';
+        const_cast<Vars*>(this)->methodnb(NARGSb);
+    }
+
+    static void smethodna(NPARAMSa);
+    static void smethodnb(NPARAMSb);
+};
+/*------- Global Vars instance for free functions and static methods -------*/
+static Vars g;
+
+/*------------ Static Vars method implementations reference 'g' ------------*/
+void Vars::smethod0()
+{
+    cout << "smethod0() -> ";
+    g.method0();
+}
+
+void Vars::smethod1(const LLSD& obj)
+{
+    cout << "smethod1(" << obj << ") -> ";
+    g.method1(obj);
+}
+
+void Vars::smethodna(NPARAMSa)
+{
+    cout << "smethodna(...) -> ";
+    g.methodna(NARGSa);
+}
+
+void Vars::smethodnb(NPARAMSb)
+{
+    cout << "smethodnb(...) -> ";
+    g.methodnb(NARGSb);
+}
+
+/*--------------------------- Reset global Vars ----------------------------*/
+void clear()
+{
+    g = Vars();
+}
+
+/*------------------- Free functions also reference 'g' --------------------*/
+void free0()
+{
+    cout << "free0() -> ";
+    g.method0();
+}
+
+void free1(const LLSD& obj)
+{
+    cout << "free1(" << obj << ") -> ";
+    g.method1(obj);
+}
+
+void freena(NPARAMSa)
+{
+    cout << "freena(...) -> ";
+    g.methodna(NARGSa);
+}
+
+void freenb(NPARAMSb)
+{
+    cout << "freenb(...) -> ";
+    g.methodnb(NARGSb);
+}
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct lleventdispatcher_data
+    {
+        WrapLL_ERRS redirect;
+        Dispatcher work;
+        Vars v;
+        std::string name, desc;
+        // Capture our own copy of all registered functions' descriptions
+        typedef std::map<std::string, std::string> DescMap;
+        DescMap descs;
+        // Capture the Vars instance on which we expect each function to operate
+        typedef std::map<std::string, Vars*> VarsMap;
+        VarsMap funcvars;
+        // Required structure for Callables with requirements
+        LLSD required;
+        // Parameter names for freena(), freenb()
+        LLSD params;
+        // Full, partial defaults arrays for params for freena(), freenb()
+        LLSD dft_array_full, dft_array_partial;
+        // Start index of partial defaults arrays
+        const LLSD::Integer partial_offset;
+        // Full, partial defaults maps for params for freena(), freenb()
+        LLSD dft_map_full, dft_map_partial;
+        // Most of the above are indexed by "a" or "b". Useful to have an
+        // array containing those strings for iterating.
+        std::vector<LLSD::String> ab;
+
+        lleventdispatcher_data():
+            work("test dispatcher", "op"),
+            // map {d=double, array=[3 elements]}
+            required(LLSDMap("d", LLSD::Real(0))("array", LLSDArray(LLSD())(LLSD())(LLSD()))),
+            // first several params are required, last couple optional
+            partial_offset(3)
+        {
+            // This object is reconstructed for every test<n> method. But
+            // clear global variables every time too.
+            ::clear();
+
+            const char* abs[] = { "a", "b" };
+            ab.assign(boost::begin(abs), boost::end(abs));
+
+            // Registration cases:
+            // - (Callable | subclass const method | subclass non-const method |
+            //   non-subclass method) (with | without) required
+            // - (Free function | static method | non-static method), (no | arbitrary) params,
+            //   array style
+            // - (Free function | static method | non-static method), (no | arbitrary) params,
+            //   map style, (empty | partial | full) (array | map) defaults
+            // - Map-style errors:
+            //   - (scalar | map) param names
+            //   - defaults scalar
+            //   - defaults array longer than params array
+            //   - defaults map with plural unknown param names
+
+            // I hate to have to write things twice, because of having to keep
+            // them consistent. If we had variadic functions, addf() would be
+            // a variadic method, capturing the name and desc and passing them
+            // plus "everything else" to work.add(). If I could return a pair
+            // and use that pair as the first two args to work.add(), I'd do
+            // that. But the best I can do with present C++ is to set two
+            // instance variables as a side effect of addf(), and pass those
+            // variables to each work.add() call. :-P
+
+            /*------------------------- Callables --------------------------*/
+
+            // Arbitrary Callable with/out required params
+            addf("free1", "free1", &g);
+            work.add(name, desc, free1);
+            addf("free1_req", "free1", &g);
+            work.add(name, desc, free1, required);
+            // Subclass non-const method with/out required params
+            addf("Dmethod1", "method1", NULL);
+            work.add(name, desc, &Dispatcher::method1);
+            addf("Dmethod1_req", "method1", NULL);
+            work.add(name, desc, &Dispatcher::method1, required);
+            // Subclass const method with/out required params
+            addf("Dcmethod1", "cmethod1", NULL);
+            work.add(name, desc, &Dispatcher::cmethod1);
+            addf("Dcmethod1_req", "cmethod1", NULL);
+            work.add(name, desc, &Dispatcher::cmethod1, required);
+            // Non-subclass method with/out required params
+            addf("method1", "method1", &v);
+            work.add(name, desc, boost::bind(&Vars::method1, boost::ref(v), _1));
+            addf("method1_req", "method1", &v);
+            work.add(name, desc, boost::bind(&Vars::method1, boost::ref(v), _1), required);
+
+            /*--------------- Arbitrary params, array style ----------------*/
+
+            // (Free function | static method) with (no | arbitrary) params, array style
+            addf("free0_array", "free0", &g);
+            work.add(name, desc, free0);
+            addf("freena_array", "freena", &g);
+            work.add(name, desc, freena);
+            addf("freenb_array", "freenb", &g);
+            work.add(name, desc, freenb);
+            addf("smethod0_array", "smethod0", &g);
+            work.add(name, desc, &Vars::smethod0);
+            addf("smethodna_array", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna);
+            addf("smethodnb_array", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb);
+            // Non-static method with (no | arbitrary) params, array style
+            addf("method0_array", "method0", &v);
+            work.add(name, desc, &Vars::method0, boost::lambda::var(v));
+            addf("methodna_array", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, boost::lambda::var(v));
+            addf("methodnb_array", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, boost::lambda::var(v));
+
+            /*---------------- Arbitrary params, map style -----------------*/
+
+            // We lay out each params list as an array, also each array of
+            // default values we'll register. We'll zip these into
+            // (param=value) maps. Why not define them as maps and just
+            // extract the keys and values to arrays? Because that wouldn't
+            // give us the right params-list order.
+
+            // freena(), methodna(), cmethodna(), smethodna() all take same param list.
+            // Same for freenb() et al.
+            params = LLSDMap("a", LLSDArray("b")("i")("f")("d")("cp"))
+                            ("b", LLSDArray("s")("uuid")("date")("uri")("bin"));
+            cout << "params:\n" << params << "\nparams[\"a\"]:\n" << params["a"] << "\nparams[\"b\"]:\n" << params["b"] << std::endl;
+            // default LLSD::Binary value   
+            std::vector<U8> binary;
+            for (size_t ix = 0, h = 0xaa; ix < 6; ++ix, h += 0x11)
+            {
+                binary.push_back(h);
+            }
+            // Full defaults arrays. We actually don't care what the LLUUID or
+            // LLDate values are, as long as they're different from the
+            // LLUUID() and LLDate() default values so inspect() will report
+            // them.
+            dft_array_full = LLSDMap("a", LLSDArray(true)(17)(3.14)(123456.78)("classic"))
+                                    ("b", LLSDArray("string")
+                                                   (LLUUID::generateNewID())
+                                                   (LLDate::now())
+                                                   (LLURI("http://www.ietf.org/rfc/rfc3986.txt"))
+                                                   (binary));
+            cout << "dft_array_full:\n" << dft_array_full << std::endl;
+            // Partial defaults arrays.
+            foreach(LLSD::String a, ab)
+            {
+                LLSD::Integer partition(std::min(partial_offset, dft_array_full[a].size()));
+                dft_array_partial[a] =
+                    llsd_copy_array(dft_array_full[a].beginArray() + partition,
+                                    dft_array_full[a].endArray());
+            }
+            cout << "dft_array_partial:\n" << dft_array_partial << std::endl;
+
+            foreach(LLSD::String a, ab)
+            {
+                // Generate full defaults maps by zipping (params, dft_array_full).
+                dft_map_full[a] = zipmap(params[a], dft_array_full[a]);
+
+                // Generate partial defaults map by zipping alternate entries from
+                // (params, dft_array_full). Part of the point of using map-style
+                // defaults is to allow any subset of the target function's
+                // parameters to be optional, not just the rightmost.
+                for (LLSD::Integer ix = 0, ixend = params[a].size(); ix < ixend; ix += 2)
+                {
+                    dft_map_partial[a][params[a][ix].asString()] = dft_array_full[a][ix];
+                }
+            }
+            cout << "dft_map_full:\n" << dft_map_full << "\ndft_map_partial:\n" << dft_map_partial << '\n';
+
+            // (Free function | static method) with (no | arbitrary) params,
+            // map style, no (empty array) defaults
+            addf("free0_map", "free0", &g);
+            work.add(name, desc, free0, LLSD::emptyArray());
+            addf("smethod0_map", "smethod0", &g);
+            work.add(name, desc, &Vars::smethod0, LLSD::emptyArray());
+            addf("freena_map_allreq", "freena", &g);
+            work.add(name, desc, freena, params["a"]);
+            addf("freenb_map_allreq", "freenb", &g);
+            work.add(name, desc, freenb, params["b"]);
+            addf("smethodna_map_allreq", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna, params["a"]);
+            addf("smethodnb_map_allreq", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb, params["b"]);
+            // Non-static method with (no | arbitrary) params, map style, no
+            // (empty array) defaults
+            addf("method0_map", "method0", &v);
+            work.add(name, desc, &Vars::method0, var(v), LLSD::emptyArray());
+            addf("methodna_map_allreq", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, var(v), params["a"]);
+            addf("methodnb_map_allreq", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, var(v), params["b"]);
+
+            // Except for the "more (array | map) defaults than params" error
+            // cases, tested separately below, the (partial | full)(array |
+            // map) defaults cases don't apply to no-params functions/methods.
+            // So eliminate free0, smethod0, method0 from the cases below.
+
+            // (Free function | static method) with arbitrary params, map
+            // style, partial (array | map) defaults
+            addf("freena_map_leftreq", "freena", &g);
+            work.add(name, desc, freena, params["a"], dft_array_partial["a"]);
+            addf("freenb_map_leftreq", "freenb", &g);
+            work.add(name, desc, freenb, params["b"], dft_array_partial["b"]);
+            addf("smethodna_map_leftreq", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna, params["a"], dft_array_partial["a"]);
+            addf("smethodnb_map_leftreq", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb, params["b"], dft_array_partial["b"]);
+            addf("freena_map_skipreq", "freena", &g);
+            work.add(name, desc, freena, params["a"], dft_map_partial["a"]);
+            addf("freenb_map_skipreq", "freenb", &g);
+            work.add(name, desc, freenb, params["b"], dft_map_partial["b"]);
+            addf("smethodna_map_skipreq", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna, params["a"], dft_map_partial["a"]);
+            addf("smethodnb_map_skipreq", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb, params["b"], dft_map_partial["b"]);
+            // Non-static method with arbitrary params, map style, partial
+            // (array | map) defaults
+            addf("methodna_map_leftreq", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_array_partial["a"]);
+            addf("methodnb_map_leftreq", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_array_partial["b"]);
+            addf("methodna_map_skipreq", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_map_partial["a"]);
+            addf("methodnb_map_skipreq", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_map_partial["b"]);
+
+            // (Free function | static method) with arbitrary params, map
+            // style, full (array | map) defaults
+            addf("freena_map_adft", "freena", &g);
+            work.add(name, desc, freena, params["a"], dft_array_full["a"]);
+            addf("freenb_map_adft", "freenb", &g);
+            work.add(name, desc, freenb, params["b"], dft_array_full["b"]);
+            addf("smethodna_map_adft", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna, params["a"], dft_array_full["a"]);
+            addf("smethodnb_map_adft", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb, params["b"], dft_array_full["b"]);
+            addf("freena_map_mdft", "freena", &g);
+            work.add(name, desc, freena, params["a"], dft_map_full["a"]);
+            addf("freenb_map_mdft", "freenb", &g);
+            work.add(name, desc, freenb, params["b"], dft_map_full["b"]);
+            addf("smethodna_map_mdft", "smethodna", &g);
+            work.add(name, desc, &Vars::smethodna, params["a"], dft_map_full["a"]);
+            addf("smethodnb_map_mdft", "smethodnb", &g);
+            work.add(name, desc, &Vars::smethodnb, params["b"], dft_map_full["b"]);
+            // Non-static method with arbitrary params, map style, full
+            // (array | map) defaults
+            addf("methodna_map_adft", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_array_full["a"]);
+            addf("methodnb_map_adft", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_array_full["b"]);
+            addf("methodna_map_mdft", "methodna", &v);
+            work.add(name, desc, &Vars::methodna, var(v), params["a"], dft_map_full["a"]);
+            addf("methodnb_map_mdft", "methodnb", &v);
+            work.add(name, desc, &Vars::methodnb, var(v), params["b"], dft_map_full["b"]);
+
+            // All the above are expected to succeed, and are setup for the
+            // tests to follow. Registration error cases are exercised as
+            // tests rather than as test setup.
+        }
+
+        void addf(const std::string& n, const std::string& d, Vars* v)
+        {
+            // This method is to capture in our own DescMap the name and
+            // description of every registered function, for metadata query
+            // testing.
+            descs[n] = d;
+            // Also capture the Vars instance on which each function should operate.
+            funcvars[n] = v;
+            // See constructor for rationale for setting these instance vars.
+            this->name = n;
+            this->desc = d;
+        }
+
+        void verify_descs()
+        {
+            // Copy descs to a temp map of same type.
+            DescMap forgotten(descs.begin(), descs.end());
+            // LLEventDispatcher intentionally provides only const_iterator:
+            // since dereferencing that iterator generates values on the fly,
+            // it's meaningless to have a modifiable iterator. But since our
+            // 'work' object isn't const, by default BOOST_FOREACH() wants to
+            // use non-const iterators. Persuade it to use the const_iterator.
+            foreach(LLEventDispatcher::NameDesc nd, const_cast<const Dispatcher&>(work))
+            {
+                DescMap::iterator found = forgotten.find(nd.first);
+                ensure(STRINGIZE("LLEventDispatcher records function '" << nd.first
+                                 << "' we didn't enter"),
+                       found != forgotten.end());
+                ensure_equals(STRINGIZE("LLEventDispatcher desc '" << nd.second <<
+                                        "' doesn't match what we entered: '" << found->second << "'"),
+                              nd.second, found->second);
+                // found in our map the name from LLEventDispatcher, good, erase
+                // our map entry
+                forgotten.erase(found);
+            }
+            if (! forgotten.empty())
+            {
+                std::ostringstream out;
+                out << "LLEventDispatcher failed to report";
+                const char* delim = ": ";
+                foreach(const DescMap::value_type& fme, forgotten)
+                {
+                    out << delim << fme.first;
+                    delim = ", ";
+                }
+                ensure(out.str(), false);
+            }
+        }
+
+        Vars* varsfor(const std::string& name)
+        {
+            VarsMap::const_iterator found = funcvars.find(name);
+            ensure(STRINGIZE("No Vars* for " << name), found != funcvars.end());
+            ensure(STRINGIZE("NULL Vars* for " << name), found->second);
+            return found->second;
+        }
+
+        void ensure_has(const std::string& outer, const std::string& inner)
+        {
+            ensure(STRINGIZE("'" << outer << "' does not contain '" << inner << "'").c_str(),
+                   outer.find(inner) != std::string::npos);
+        }
+
+        void call_exc(const std::string& func, const LLSD& args, const std::string& exc_frag)
+        {
+            std::string threw;
+            try
+            {
+                work(func, args);
+            }
+            catch (const std::runtime_error& e)
+            {
+                cout << "*** " << e.what() << '\n';
+                threw = e.what();
+            }
+            ensure_has(threw, exc_frag);
+        }
+
+        LLSD getMetadata(const std::string& name)
+        {
+            LLSD meta(work.getMetadata(name));
+            ensure(STRINGIZE("No metadata for " << name), meta.isDefined());
+            return meta;
+        }
+
+        // From two related LLSD arrays, e.g. a param-names array and a values
+        // array, zip them together into an LLSD map.
+        LLSD zipmap(const LLSD& keys, const LLSD& values)
+        {
+            LLSD map;
+            for (LLSD::Integer i = 0, iend = keys.size(); i < iend; ++i)
+            {
+                // Have to select asString() since you can index an LLSD
+                // object with either String or Integer.
+                map[keys[i].asString()] = values[i];
+            }
+            return map;
+        }
+
+        // If I call this ensure_equals(), it blocks visibility of all other
+        // ensure_equals() overloads. Normally I could say 'using
+        // baseclass::ensure_equals;' and fix that, but I don't know what the
+        // base class is!
+        void ensure_llsd(const std::string& msg, const LLSD& actual, const LLSD& expected, U32 bits)
+        {
+            std::ostringstream out;
+            if (! msg.empty())
+            {
+                out << msg << ": ";
+            }
+            out << "expected " << expected << ", actual " << actual;
+            ensure(out.str(), llsd_equals(actual, expected, bits));
+        }
+
+        void ensure_llsd(const LLSD& actual, const LLSD& expected, U32 bits)
+        {
+            ensure_llsd("", actual, expected, bits);
+        }
+    };
+    typedef test_group<lleventdispatcher_data> lleventdispatcher_group;
+    typedef lleventdispatcher_group::object object;
+    lleventdispatcher_group lleventdispatchergrp("lleventdispatcher");
+
+    // Call cases:
+    // - (try_call | call) (explicit name | event key) (real | bogus) name
+    // - Callable with args that (do | do not) match required
+    // - (Free function | non-static method), no args, (array | map) style
+    // - (Free function | non-static method), arbitrary args,
+    //   (array style with (scalar | map) | map style with scalar)
+    // - (Free function | non-static method), arbitrary args, array style with
+    //   array (too short | too long | just right)
+    //   [trap LL_WARNS for too-long case?]
+    // - (Free function | non-static method), arbitrary args, map style with
+    //   (array | map) (all | too many | holes (with | without) defaults)
+    // - const char* param gets ("" | NULL)
+
+    // Query cases:
+    // - Iterate over all (with | without) remove()
+    // - getDispatchKey()
+    // - Callable style (with | without) required
+    // - (Free function | non-static method), array style, (no | arbitrary) params
+    // - (Free function | non-static method), map style, (no | arbitrary) params,
+    //   (empty | full | partial (array | map)) defaults
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("map-style registration with non-array params");
+        // Pass "param names" as scalar or as map
+        LLSD attempts(LLSDArray(17)(LLSDMap("pi", 3.14)("two", 2)));
+        foreach(LLSD ae, inArray(attempts))
+        {
+            std::string threw;
+            try
+            {
+                work.add("freena_err", "freena", freena, ae);
+            }
+            catch (const std::exception& e)
+            {
+                threw = e.what();
+            }
+            ensure_has(threw, "must be an array");
+        }
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("map-style registration with badly-formed defaults");
+        std::string threw;
+        try
+        {
+            work.add("freena_err", "freena", freena, LLSDArray("a")("b"), 17);
+        }
+        catch (const std::exception& e)
+        {
+            threw = e.what();
+        }
+        ensure_has(threw, "must be a map or an array");
+    }
+
+    template<> template<>
+    void object::test<3>()
+    {
+        set_test_name("map-style registration with too many array defaults");
+        std::string threw;
+        try
+        {
+            work.add("freena_err", "freena", freena,
+                     LLSDArray("a")("b"),
+                     LLSDArray(17)(0.9)("gack"));
+        }
+        catch (const std::exception& e)
+        {
+            threw = e.what();
+        }
+        ensure_has(threw, "shorter than");
+    }
+
+    template<> template<>
+    void object::test<4>()
+    {
+        set_test_name("map-style registration with too many map defaults");
+        std::string threw;
+        try
+        {
+            work.add("freena_err", "freena", freena,
+                     LLSDArray("a")("b"),
+                     LLSDMap("b", 17)("foo", 3.14)("bar", "sinister"));
+        }
+        catch (const std::exception& e)
+        {
+            threw = e.what();
+        }
+        ensure_has(threw, "nonexistent params");
+        ensure_has(threw, "foo");
+        ensure_has(threw, "bar");
+    }
+
+    template<> template<>
+    void object::test<5>()
+    {
+        set_test_name("query all");
+        verify_descs();
+    }
+
+    template<> template<>
+    void object::test<6>()
+    {
+        set_test_name("query all with remove()");
+        ensure("remove('bogus') returned true", ! work.remove("bogus"));
+        ensure("remove('real') returned false", work.remove("free1"));
+        // Of course, remove that from 'descs' too...
+        descs.erase("free1");
+        verify_descs();
+    }
+
+    template<> template<>
+    void object::test<7>()
+    {
+        set_test_name("getDispatchKey()");
+        ensure_equals(work.getDispatchKey(), "op");
+    }
+
+    template<> template<>
+    void object::test<8>()
+    {
+        set_test_name("query Callables with/out required params");
+        LLSD names(LLSDArray("free1")("Dmethod1")("Dcmethod1")("method1"));
+        foreach(LLSD nm, inArray(names))
+        {
+            LLSD metadata(getMetadata(nm));
+            ensure_equals("name mismatch", metadata["name"], nm);
+            ensure_equals(metadata["desc"].asString(), descs[nm]);
+            ensure("should not have required structure", metadata["required"].isUndefined());
+            ensure("should not have optional", metadata["optional"].isUndefined());
+
+            std::string name_req(nm.asString() + "_req");
+            metadata = getMetadata(name_req);
+            ensure_equals(metadata["name"].asString(), name_req);
+            ensure_equals(metadata["desc"].asString(), descs[name_req]);
+            ensure_equals("required mismatch", required, metadata["required"]);
+            ensure("should not have optional", metadata["optional"].isUndefined());
+        }
+    }
+
+    template<> template<>
+    void object::test<9>()
+    {
+        set_test_name("query array-style functions/methods");
+        // Associate each registered name with expected arity.
+        LLSD expected(LLSDArray
+                      (LLSDArray
+                       (0)(LLSDArray("free0_array")("smethod0_array")("method0_array")))
+                      (LLSDArray
+                       (5)(LLSDArray("freena_array")("smethodna_array")("methodna_array")))
+                      (LLSDArray
+                       (5)(LLSDArray("freenb_array")("smethodnb_array")("methodnb_array"))));
+        foreach(LLSD ae, inArray(expected))
+        {
+            LLSD::Integer arity(ae[0].asInteger());
+            LLSD names(ae[1]);
+            LLSD req(LLSD::emptyArray());
+            if (arity)
+                req[arity - 1] = LLSD();
+            foreach(LLSD nm, inArray(names))
+            {
+                LLSD metadata(getMetadata(nm));
+                ensure_equals("name mismatch", metadata["name"], nm);
+                ensure_equals(metadata["desc"].asString(), descs[nm]);
+                ensure_equals(STRINGIZE("mismatched required for " << nm.asString()),
+                              metadata["required"], req);
+                ensure("should not have optional", metadata["optional"].isUndefined());
+            }
+        }
+    }
+
+    template<> template<>
+    void object::test<10>()
+    {
+        set_test_name("query map-style no-params functions/methods");
+        // - (Free function | non-static method), map style, no params (ergo
+        //   no defaults)
+        LLSD names(LLSDArray("free0_map")("smethod0_map")("method0_map"));
+        foreach(LLSD nm, inArray(names))
+        {
+            LLSD metadata(getMetadata(nm));
+            ensure_equals("name mismatch", metadata["name"], nm);
+            ensure_equals(metadata["desc"].asString(), descs[nm]);
+            ensure("should not have required",
+                   (metadata["required"].isUndefined() || metadata["required"].size() == 0));
+            ensure("should not have optional", metadata["optional"].isUndefined());
+        }
+    }
+
+    template<> template<>
+    void object::test<11>()
+    {
+        set_test_name("query map-style arbitrary-params functions/methods: "
+                      "full array defaults vs. full map defaults");
+        // With functions registered with no defaults ("_allreq" suffixes),
+        // there is of course no difference between array defaults and map
+        // defaults. (We don't even bother registering with LLSD::emptyArray()
+        // vs. LLSD::emptyMap().) With functions registered with all defaults,
+        // there should (!) be no difference beween array defaults and map
+        // defaults. Verify, so we can ignore the distinction for all other
+        // tests.
+        LLSD equivalences(LLSDArray
+                          (LLSDArray("freena_map_adft")("freena_map_mdft"))
+                          (LLSDArray("freenb_map_adft")("freenb_map_mdft"))
+                          (LLSDArray("smethodna_map_adft")("smethodna_map_mdft"))
+                          (LLSDArray("smethodnb_map_adft")("smethodnb_map_mdft"))
+                          (LLSDArray("methodna_map_adft")("methodna_map_mdft"))
+                          (LLSDArray("methodnb_map_adft")("methodnb_map_mdft")));
+        foreach(LLSD eq, inArray(equivalences))
+        {
+            LLSD adft(eq[0]);
+            LLSD mdft(eq[1]);
+            // We can't just compare the results of the two getMetadata()
+            // calls, because they contain ["name"], which are different. So
+            // capture them, verify that each ["name"] is as expected, then
+            // remove for comparing the rest.
+            LLSD ameta(getMetadata(adft));
+            LLSD mmeta(getMetadata(mdft));
+            ensure_equals("adft name", adft, ameta["name"]);
+            ensure_equals("mdft name", mdft, mmeta["name"]);
+            ameta.erase("name");
+            mmeta.erase("name");
+            ensure_equals(STRINGIZE("metadata for " << adft.asString()
+                                    << " vs. " << mdft.asString()),
+                          ameta, mmeta);
+        }
+    }
+
+    template<> template<>
+    void object::test<12>()
+    {
+        set_test_name("query map-style arbitrary-params functions/methods");
+        // - (Free function | non-static method), map style, arbitrary params,
+        //   (empty | full | partial (array | map)) defaults
+
+        // Generate maps containing all parameter names for cases in which all
+        // params are required. Also maps containing left requirements for
+        // partial defaults arrays. Also defaults maps from defaults arrays.
+        LLSD allreq, leftreq, rightdft;
+        foreach(LLSD::String a, ab)
+        {
+            // The map in which all params are required uses params[a] as
+            // keys, with all isUndefined() as values. We can accomplish that
+            // by passing zipmap() an empty values array.
+            allreq[a] = zipmap(params[a], LLSD::emptyArray());
+            // Same for leftreq, save that we use the subset of the params not
+            // supplied by dft_array_partial[a].
+            LLSD::Integer partition(params[a].size() - dft_array_partial[a].size());
+            leftreq[a] = zipmap(llsd_copy_array(params[a].beginArray(),
+                                                params[a].beginArray() + partition),
+                                LLSD::emptyArray());
+            // Generate map pairing dft_array_partial[a] values with their
+            // param names.
+            rightdft[a] = zipmap(llsd_copy_array(params[a].beginArray() + partition,
+                                                 params[a].endArray()),
+                                 dft_array_partial[a]);
+        }
+        cout << "allreq:\n" << allreq << "\nleftreq:\n" << leftreq << "\nrightdft:\n" << rightdft << std::endl;
+
+        // Generate maps containing parameter names not provided by the
+        // dft_map_partial maps.
+        LLSD skipreq(allreq);
+        foreach(LLSD::String a, ab)
+        {
+            foreach(const MapEntry& me, inMap(dft_map_partial[a]))
+            {
+                skipreq[a].erase(me.first);
+            }
+        }
+        cout << "skipreq:\n" << skipreq << std::endl;
+
+        LLSD groups(LLSDArray       // array of groups
+
+                    (LLSDArray      // group
+                     (LLSDArray("freena_map_allreq")("smethodna_map_allreq")("methodna_map_allreq"))
+                     (LLSDArray(allreq["a"])(LLSD()))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freenb_map_allreq")("smethodnb_map_allreq")("methodnb_map_allreq"))
+                     (LLSDArray(allreq["b"])(LLSD()))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freena_map_leftreq")("smethodna_map_leftreq")("methodna_map_leftreq"))
+                     (LLSDArray(leftreq["a"])(rightdft["a"]))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freenb_map_leftreq")("smethodnb_map_leftreq")("methodnb_map_leftreq"))
+                     (LLSDArray(leftreq["b"])(rightdft["b"]))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freena_map_skipreq")("smethodna_map_skipreq")("methodna_map_skipreq"))
+                     (LLSDArray(skipreq["a"])(dft_map_partial["a"]))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freenb_map_skipreq")("smethodnb_map_skipreq")("methodnb_map_skipreq"))
+                     (LLSDArray(skipreq["b"])(dft_map_partial["b"]))) // required, optional
+
+                    // We only need mention the full-map-defaults ("_mdft" suffix)
+                    // registrations, having established their equivalence with the
+                    // full-array-defaults ("_adft" suffix) registrations in another test.
+                    (LLSDArray        // group
+                     (LLSDArray("freena_map_mdft")("smethodna_map_mdft")("methodna_map_mdft"))
+                     (LLSDArray(LLSD::emptyMap())(dft_map_full["a"]))) // required, optional
+
+                    (LLSDArray        // group
+                     (LLSDArray("freenb_map_mdft")("smethodnb_map_mdft")("methodnb_map_mdft"))
+                     (LLSDArray(LLSD::emptyMap())(dft_map_full["b"])))); // required, optional
+
+        foreach(LLSD grp, inArray(groups))
+        {
+            // Internal structure of each group in 'groups':
+            LLSD names(grp[0]);
+            LLSD required(grp[1][0]);
+            LLSD optional(grp[1][1]);
+            cout << "For " << names << ",\n" << "required:\n" << required << "\noptional:\n" << optional << std::endl;
+
+            // Loop through 'names'
+            foreach(LLSD nm, inArray(names))
+            {
+                LLSD metadata(getMetadata(nm));
+                ensure_equals("name mismatch", metadata["name"], nm);
+                ensure_equals(nm.asString(), metadata["desc"].asString(), descs[nm]);
+                ensure_equals(STRINGIZE(nm << " required mismatch"),
+                              metadata["required"], required);
+                ensure_equals(STRINGIZE(nm << " optional mismatch"),
+                              metadata["optional"], optional);
+            }
+        }
+    }
+
+    template<> template<>
+    void object::test<13>()
+    {
+        set_test_name("try_call()");
+        ensure("try_call(bogus name, LLSD()) returned true", ! work.try_call("freek", LLSD()));
+        ensure("try_call(bogus name) returned true", ! work.try_call(LLSDMap("op", "freek")));
+        ensure("try_call(real name, LLSD()) returned false", work.try_call("free0_array", LLSD()));
+        ensure("try_call(real name) returned false", work.try_call(LLSDMap("op", "free0_map")));
+    }
+
+    template<> template<>
+    void object::test<14>()
+    {
+        set_test_name("call with bad name");
+        call_exc("freek", LLSD(), "not found");
+        // We don't have a comparable helper function for the one-arg
+        // operator() method, and it's not worth building one just for this
+        // case. Write it out.
+        std::string threw;
+        try
+        {
+            work(LLSDMap("op", "freek"));
+        }
+        catch (const std::runtime_error& e)
+        {
+            cout << "*** " << e.what() << "\n";
+            threw = e.what();
+        }
+        ensure_has(threw, "bad");
+        ensure_has(threw, "op");
+        ensure_has(threw, "freek");
+    }
+
+    template<> template<>
+    void object::test<15>()
+    {
+        set_test_name("call with event key");
+        // We don't need a separate test for operator()(string, LLSD) with
+        // valid name, because all the rest of the tests exercise that case.
+        // The one we don't exercise elsewhere is operator()(LLSD) with valid
+        // name, so here it is.
+        work(LLSDMap("op", "free0_map"));
+        ensure_equals(g.i, 17);
+    }
+
+    // Cannot be defined inside function body... remind me again why we use C++...  :-P
+    struct CallablesTriple
+    {
+        std::string name, name_req;
+        LLSD& llsd;
+    };
+
+    template<> template<>
+    void object::test<16>()
+    {
+        set_test_name("call Callables");
+        CallablesTriple tests[] =
+        {
+            { "free1",     "free1_req",     g.llsd },
+            { "Dmethod1",  "Dmethod1_req",  work.llsd },
+            { "Dcmethod1", "Dcmethod1_req", work.llsd },
+            { "method1",   "method1_req",   v.llsd }
+        };
+        // Arbitrary LLSD value that we should be able to pass to Callables
+        // without 'required', but should not be able to pass to Callables
+        // with 'required'.
+        LLSD answer(42);
+        // LLSD value matching 'required' according to llsd_matches() rules.
+        LLSD matching(LLSDMap("d", 3.14)("array", LLSDArray("answer")(true)(answer)));
+        // Okay, walk through 'tests'.
+        foreach(const CallablesTriple& tr, tests)
+        {
+            // Should be able to pass 'answer' to Callables registered
+            // without 'required'.
+            work(tr.name, answer);
+            ensure_equals("answer mismatch", tr.llsd, answer);
+            // Should NOT be able to pass 'answer' to Callables registered
+            // with 'required'.
+            call_exc(tr.name_req, answer, "bad request");
+            // But SHOULD be able to pass 'matching' to Callables registered
+            // with 'required'.
+            work(tr.name_req, matching);
+            ensure_equals("matching mismatch", tr.llsd, matching);
+        }
+    }
+
+    template<> template<>
+    void object::test<17>()
+    {
+        set_test_name("passing wrong args to (map | array)-style registrations");
+
+        // Pass scalar/map to array-style functions, scalar/array to map-style
+        // functions. As that validation happens well before we engage the
+        // argument magic, it seems pointless to repeat this with every
+        // variation: (free function | non-static method), (no | arbitrary)
+        // args. We should only need to engage it for one map-style
+        // registration and one array-style registration.
+        std::string array_exc("needs an args array");
+        call_exc("free0_array", 17, array_exc);
+        call_exc("free0_array", LLSDMap("pi", 3.14), array_exc);
+
+        std::string map_exc("needs a map");
+        call_exc("free0_map", 17, map_exc);
+        // Passing an array to a map-style function works now! No longer an
+        // error case!
+//      call_exc("free0_map", LLSDArray("a")("b"), map_exc);
+    }
+
+    template<> template<>
+    void object::test<18>()
+    {
+        set_test_name("call no-args functions");
+        LLSD names(LLSDArray
+                   ("free0_array")("free0_map")
+                   ("smethod0_array")("smethod0_map")
+                   ("method0_array")("method0_map"));
+        foreach(LLSD name, inArray(names))
+        {
+            // Look up the Vars instance for this function.
+            Vars* vars(varsfor(name));
+            // Both the global and stack Vars instances are automatically
+            // cleared at the start of each test<n> method. But since we're
+            // calling these things several different times in the same
+            // test<n> method, manually reset the Vars between each.
+            *vars = Vars();
+            ensure_equals(vars->i, 0);
+            // call function with empty array (or LLSD(), should be equivalent)
+            work(name, LLSD());
+            ensure_equals(vars->i, 17);
+        }
+    }
+
+    // Break out this data because we use it in a couple different tests.
+    LLSD array_funcs(LLSDArray
+                     (LLSDMap("a", "freena_array")   ("b", "freenb_array"))
+                     (LLSDMap("a", "smethodna_array")("b", "smethodnb_array"))
+                     (LLSDMap("a", "methodna_array") ("b", "methodnb_array")));
+
+    template<> template<>
+    void object::test<19>()
+    {
+        set_test_name("call array-style functions with too-short arrays");
+        // Could have two different too-short arrays, one for *na and one for
+        // *nb, but since they both take 5 params...
+        LLSD tooshort(LLSDArray("this")("array")("too")("short"));
+        foreach(const LLSD& funcsab, inArray(array_funcs))
+        {
+            foreach(const llsd::MapEntry& e, inMap(funcsab))
+            {
+                call_exc(e.second, tooshort, "requires more arguments");
+            }
+        }
+    }
+
+    template<> template<>
+    void object::test<20>()
+    {
+        set_test_name("call array-style functions with (just right | too long) arrays");
+        std::vector<U8> binary;
+        for (size_t h(0x01), i(0); i < 5; h+= 0x22, ++i)
+        {
+            binary.push_back(h);
+        }
+        LLSD args(LLSDMap("a", LLSDArray(true)(17)(3.14)(123.456)("char*"))
+                         ("b", LLSDArray("string")
+                                        (LLUUID("01234567-89ab-cdef-0123-456789abcdef"))
+                                        (LLDate("2011-02-03T15:07:00Z"))
+                                        (LLURI("http://secondlife.com"))
+                                        (binary)));
+        LLSD argsplus(args);
+        argsplus["a"].append("bogus");
+        argsplus["b"].append("bogus");
+        LLSD expect;
+        foreach(LLSD::String a, ab)
+        {
+            expect[a] = zipmap(params[a], args[a]);
+        }
+        // Adjust expect["a"]["cp"] for special Vars::cp treatment.
+        expect["a"]["cp"] = std::string("'") + expect["a"]["cp"].asString() + "'";
+        cout << "expect: " << expect << '\n';
+
+        // Use substantially the same logic for args and argsplus
+        LLSD argsarrays(LLSDArray(args)(argsplus));
+        // So i==0 selects 'args', i==1 selects argsplus
+        for (LLSD::Integer i(0), iend(argsarrays.size()); i < iend; ++i)
+        {
+            foreach(const LLSD& funcsab, inArray(array_funcs))
+            {
+                foreach(LLSD::String a, ab)
+                {
+                    // Reset the Vars instance before each call
+                    Vars* vars(varsfor(funcsab[a]));
+                    *vars = Vars();
+                    work(funcsab[a], argsarrays[i][a]);
+                    ensure_llsd(STRINGIZE(funcsab[a].asString() <<
+                                          ": expect[\"" << a << "\"] mismatch"),
+                                vars->inspect(), expect[a], 7); // 7 bits ~= 2 decimal digits
+
+                    // TODO: in the i==1 or argsplus case, intercept LL_WARNS
+                    // output? Even without that, using argsplus verifies that
+                    // passing too many args isn't fatal; it works -- but
+                    // would be nice to notice the warning too.
+                }
+            }
+        }
+    }
+
+    template<> template<>
+    void object::test<21>()
+    {
+        set_test_name("verify that passing LLSD() to const char* sends NULL");
+
+        ensure_equals("Vars::cp init", v.cp, "");
+        work("methodna_map_mdft", LLSDMap("cp", LLSD()));
+        ensure_equals("passing LLSD()", v.cp, "NULL");
+        work("methodna_map_mdft", LLSDMap("cp", ""));
+        ensure_equals("passing \"\"", v.cp, "''");
+        work("methodna_map_mdft", LLSDMap("cp", "non-NULL"));
+        ensure_equals("passing \"non-NULL\"", v.cp, "'non-NULL'");
+    }
+
+    template<> template<>
+    void object::test<22>()
+    {
+        set_test_name("call map-style functions with (full | oversized) (arrays | maps)");
+        const char binary[] = "\x99\x88\x77\x66\x55";
+        LLSD array_full(LLSDMap
+                        ("a", LLSDArray(false)(255)(98.6)(1024.5)("pointer"))
+                        ("b", LLSDArray("object")(LLUUID::generateNewID())(LLDate::now())(LLURI("http://wiki.lindenlab.com/wiki"))(LLSD::Binary(boost::begin(binary), boost::end(binary)))));
+        LLSD array_overfull(array_full);
+        foreach(LLSD::String a, ab)
+        {
+            array_overfull[a].append("bogus");
+        }
+        cout << "array_full: " << array_full << "\narray_overfull: " << array_overfull << std::endl;
+        // We rather hope that LLDate::now() will generate a timestamp
+        // distinct from the one it generated in the constructor, moments ago.
+        ensure_not_equals("Timestamps too close",
+                          array_full["b"][2].asDate(), dft_array_full["b"][2].asDate());
+        // We /insist/ that LLUUID::generateNewID() do so.
+        ensure_not_equals("UUID collision",
+                          array_full["b"][1].asUUID(), dft_array_full["b"][1].asUUID());
+        LLSD map_full, map_overfull;
+        foreach(LLSD::String a, ab)
+        {
+            map_full[a] = zipmap(params[a], array_full[a]);
+            map_overfull[a] = map_full[a];
+            map_overfull[a]["extra"] = "ignore";
+        }
+        cout << "map_full: " << map_full << "\nmap_overfull: " << map_overfull << std::endl;
+        LLSD expect(map_full);
+        // Twiddle the const char* param.
+        expect["a"]["cp"] = std::string("'") + expect["a"]["cp"].asString() + "'";
+        // Another adjustment. For each data type, we're trying to distinguish
+        // three values: the Vars member's initial value (member wasn't
+        // stored; control never reached the set function), the registered
+        // default param value from dft_array_full, and the array_full value
+        // in this test. But bool can only distinguish two values. In this
+        // case, we want to differentiate the local array_full value from the
+        // dft_array_full value, so we use 'false'. However, that means
+        // Vars::inspect() doesn't differentiate it from the initial value,
+        // so won't bother returning it. Predict that behavior to match the
+        // LLSD values.
+        expect["a"].erase("b");
+        cout << "expect: " << expect << std::endl;
+        // For this test, calling functions registered with different sets of
+        // parameter defaults should make NO DIFFERENCE WHATSOEVER. Every call
+        // should pass all params.
+        LLSD names(LLSDMap
+                   ("a", LLSDArray
+                         ("freena_map_allreq") ("smethodna_map_allreq") ("methodna_map_allreq")
+                         ("freena_map_leftreq")("smethodna_map_leftreq")("methodna_map_leftreq")
+                         ("freena_map_skipreq")("smethodna_map_skipreq")("methodna_map_skipreq")
+                         ("freena_map_adft")   ("smethodna_map_adft")   ("methodna_map_adft")
+                         ("freena_map_mdft")   ("smethodna_map_mdft")   ("methodna_map_mdft"))
+                   ("b", LLSDArray
+                         ("freenb_map_allreq") ("smethodnb_map_allreq") ("methodnb_map_allreq")
+                         ("freenb_map_leftreq")("smethodnb_map_leftreq")("methodnb_map_leftreq")
+                         ("freenb_map_skipreq")("smethodnb_map_skipreq")("methodnb_map_skipreq")
+                         ("freenb_map_adft")   ("smethodnb_map_adft")   ("methodnb_map_adft")
+                         ("freenb_map_mdft")   ("smethodnb_map_mdft")   ("methodnb_map_mdft")));
+        // Treat (full | overfull) (array | map) the same.
+        LLSD argssets(LLSDArray(array_full)(array_overfull)(map_full)(map_overfull));
+        foreach(const LLSD& args, inArray(argssets))
+        {
+            foreach(LLSD::String a, ab)
+            {
+                foreach(LLSD::String name, inArray(names[a]))
+                {
+                    // Reset the Vars instance
+                    Vars* vars(varsfor(name));
+                    *vars = Vars();
+                    work(name, args[a]);
+                    ensure_llsd(STRINGIZE(name << ": expect[\"" << a << "\"] mismatch"),
+                                vars->inspect(), expect[a], 7); // 7 bits, 2 decimal digits
+                    // intercept LL_WARNS for the two overfull cases?
+                }
+            }
+        }
+    }
+} // namespace tut
diff --git a/indra/llui/llfloaterreglistener.cpp b/indra/llui/llfloaterreglistener.cpp
index 821d4543aef2dfa7fe33e79aa15d3a69297e45c2..7525b8cab3a7506fd7a39d8a91ac087a1959bd53 100644
--- a/indra/llui/llfloaterreglistener.cpp
+++ b/indra/llui/llfloaterreglistener.cpp
@@ -60,6 +60,11 @@ LLFloaterRegListener::LLFloaterRegListener():
         "Ask to toggle the state of the floater specified in [\"name\"]",
         &LLFloaterRegListener::toggleInstance,
         requiredName);
+    add("instanceVisible",
+        "Return on [\"reply\"] an event whose [\"visible\"] indicates the visibility "
+        "of the floater specified in [\"name\"]",
+        &LLFloaterRegListener::instanceVisible,
+        requiredName);
     LLSD requiredNameButton;
     requiredNameButton["name"] = LLSD();
     requiredNameButton["button"] = LLSD();
@@ -71,9 +76,7 @@ LLFloaterRegListener::LLFloaterRegListener():
 
 void LLFloaterRegListener::getBuildMap(const LLSD& event) const
 {
-    // Honor the "reqid" convention by echoing event["reqid"] in our reply packet.
-    LLReqID reqID(event);
-    LLSD reply(reqID.makeResponse());
+    LLSD reply;
     // Build an LLSD map that mirrors sBuildMap. Since we have no good way to
     // represent a C++ callable in LLSD, the only part of BuildData we can
     // store is the filename. For each LLSD map entry, it would be more
@@ -86,7 +89,7 @@ void LLFloaterRegListener::getBuildMap(const LLSD& event) const
         reply[mi->first] = mi->second.mFile;
     }
     // Send the reply to the LLEventPump named in event["reply"].
-    LLEventPumps::instance().obtain(event["reply"]).post(reply);
+    sendReply(reply, event);
 }
 
 void LLFloaterRegListener::showInstance(const LLSD& event) const
@@ -104,6 +107,12 @@ void LLFloaterRegListener::toggleInstance(const LLSD& event) const
     LLFloaterReg::toggleInstance(event["name"], event["key"]);
 }
 
+void LLFloaterRegListener::instanceVisible(const LLSD& event) const
+{
+    sendReply(LLSDMap("visible", LLFloaterReg::instanceVisible(event["name"], event["key"])),
+              event);
+}
+
 void LLFloaterRegListener::clickButton(const LLSD& event) const
 {
     // If the caller requests a reply, build the reply.
diff --git a/indra/llui/llfloaterreglistener.h b/indra/llui/llfloaterreglistener.h
index 586656667ce4656d0354decb3dd2936f84d91ef4..24311a2dfa2610797e63570640b80483bb05a6fe 100644
--- a/indra/llui/llfloaterreglistener.h
+++ b/indra/llui/llfloaterreglistener.h
@@ -47,6 +47,7 @@ class LLFloaterRegListener: public LLEventAPI
     void showInstance(const LLSD& event) const;
     void hideInstance(const LLSD& event) const;
     void toggleInstance(const LLSD& event) const;
+    void instanceVisible(const LLSD& event) const;
     void clickButton(const LLSD& event) const;
 };
 
diff --git a/indra/llui/tests/llurlentry_stub.cpp b/indra/llui/tests/llurlentry_stub.cpp
index 96ebe83826c5137dd90cd0458620a15d2777a4c8..c11ad11de9c2a29c1d266739d747cd6d6d3e97ca 100644
--- a/indra/llui/tests/llurlentry_stub.cpp
+++ b/indra/llui/tests/llurlentry_stub.cpp
@@ -193,8 +193,8 @@ LLFontGL* LLFontGL::getFontDefault()
 	return NULL; 
 }
 
-char* _PREHASH_AgentData = "AgentData";
-char* _PREHASH_AgentID = "AgentID";
+char* _PREHASH_AgentData = const_cast<char*>("AgentData");
+char* _PREHASH_AgentID   = const_cast<char*>("AgentID");
 
 LLHost LLHost::invalid(INVALID_PORT,INVALID_HOST_IP_ADDRESS);
 
diff --git a/indra/llwindow/CMakeLists.txt b/indra/llwindow/CMakeLists.txt
index 4d2677fd91f062a2ea049ceefe7f22bb43c2ba45..00aaba20529e8ae76c6e1ef2c8d8969179a934ca 100644
--- a/indra/llwindow/CMakeLists.txt
+++ b/indra/llwindow/CMakeLists.txt
@@ -36,6 +36,7 @@ set(llwindow_SOURCE_FILES
     llkeyboard.cpp
     llwindowheadless.cpp
     llwindowcallbacks.cpp
+    llwindowlistener.cpp
     )
 
 set(llwindow_HEADER_FILES
@@ -44,6 +45,7 @@ set(llwindow_HEADER_FILES
     llkeyboard.h
     llwindowheadless.h
     llwindowcallbacks.h
+    llwindowlistener.h
     )
 
 set(viewer_SOURCE_FILES
diff --git a/indra/llwindow/llwindow.cpp b/indra/llwindow/llwindow.cpp
index 072f694c2465a4e4c0f2cee3ea66487c1a488825..2d00c377191a6e8bfb4edf953a94226d56c96a71 100644
--- a/indra/llwindow/llwindow.cpp
+++ b/indra/llwindow/llwindow.cpp
@@ -41,6 +41,7 @@
 #include "llkeyboard.h"
 #include "linked_lists.h"
 #include "llwindowcallbacks.h"
+#include "llwindowlistener.h"
 
 
 //
@@ -115,10 +116,15 @@ LLWindow::LLWindow(LLWindowCallbacks* callbacks, BOOL fullscreen, U32 flags)
 	  mHideCursorPermanent(FALSE),
 	  mFlags(flags),
 	  mHighSurrogate(0)
-{ }
+{
+	mListener = new LLWindowListener(callbacks, gKeyboard);
+}
 
 LLWindow::~LLWindow()
-{ }
+{
+	delete mListener;
+	mListener = NULL;
+}
 
 //virtual
 BOOL LLWindow::isValid()
diff --git a/indra/llwindow/llwindow.h b/indra/llwindow/llwindow.h
index e8a86a188072c90f6563f47681d266a4e9eea196..6bdc01ae88fcd0fa45af3fd804635b7965c54527 100644
--- a/indra/llwindow/llwindow.h
+++ b/indra/llwindow/llwindow.h
@@ -36,6 +36,7 @@
 class LLSplashScreen;
 class LLPreeditor;
 class LLWindowCallbacks;
+class LLWindowListener;
 
 // Refer to llwindow_test in test/common/llwindow for usage example
 
@@ -188,6 +189,7 @@ class LLWindow
 	BOOL		mHideCursorPermanent;
 	U32			mFlags;
 	U16			mHighSurrogate;
+	LLWindowListener* mListener;
 
  	// Handle a UTF-16 encoding unit received from keyboard.
  	// Converting the series of UTF-16 encoding units to UTF-32 data,
diff --git a/indra/llwindow/llwindowlistener.cpp b/indra/llwindow/llwindowlistener.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..22cc12acee1cac800959d970198628983ed8b943
--- /dev/null
+++ b/indra/llwindow/llwindowlistener.cpp
@@ -0,0 +1,174 @@
+/** 
+ * @file llwindowlistener.cpp
+ * @brief EventAPI interface for injecting input into LLWindow
+ *
+ * $LicenseInfo:firstyear=2001&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * 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;
+ * version 2.1 of the License only.
+ * 
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#include "linden_common.h"
+
+#include "llwindowlistener.h"
+
+#include "llcoord.h"
+#include "llkeyboard.h"
+#include "llwindowcallbacks.h"
+
+LLWindowListener::LLWindowListener(LLWindowCallbacks *window, LLKeyboard * keyboard)
+	: LLEventAPI("LLWindow", "Inject input events into the LLWindow instance"),
+	  mWindow(window),
+	  mKeyboard(keyboard)
+{
+	add("keyDown",
+		"Given [\"keycode\"] or [\"char\"], will inject the given keypress event.",
+		&LLWindowListener::keyDown);
+	add("keyUp",
+		"Given [\"keycode\"] or [\"char\"], will inject the given key release event.",
+		&LLWindowListener::keyUp);
+	add("mouseDown",
+		"Given [\"button\"], [\"x\"] and [\"y\"], will inject the given mouse click event.",
+		&LLWindowListener::mouseDown);
+	add("mouseUp",
+		"Given [\"button\"], [\"x\"] and [\"y\"], will inject the given mouse release event.",
+		&LLWindowListener::mouseUp);
+	add("mouseMove",
+		"Given [\"x\"] and [\"y\"], will inject the given mouse movement event.",
+		&LLWindowListener::mouseMove);
+	add("mouseScroll",
+		"Given a number of [\"clicks\"], will inject the given mouse scroll event.",
+		&LLWindowListener::mouseScroll);
+}
+
+void LLWindowListener::keyDown(LLSD const & evt)
+{
+	if(NULL == mKeyboard)
+	{
+		// *HACK to handle the fact that LLWindow subclasses have to initialize
+		// things in an inconvenient order
+		mKeyboard = gKeyboard;
+	}
+
+	KEY keycode = 0;
+	if(evt.has("keycode"))
+	{
+		keycode = KEY(evt["keycode"].asInteger());
+	}
+	else
+	{
+		keycode = KEY(evt["char"].asString()[0]);
+	}
+
+	// *TODO - figure out how to handle the mask
+	mKeyboard->handleTranslatedKeyDown(keycode, 0);
+}
+
+void LLWindowListener::keyUp(LLSD const & evt)
+{
+	if(NULL == mKeyboard)
+	{
+		// *HACK to handle the fact that LLWindow subclasses have to initialize
+		// things in an inconvenient order
+		mKeyboard = gKeyboard;
+	}
+
+	KEY keycode = 0;
+	if(evt.has("keycode"))
+	{
+		keycode = KEY(evt["keycode"].asInteger());
+	}
+	else
+	{
+		keycode = KEY(evt["char"].asString()[0]);
+	}
+
+	// *TODO - figure out how to handle the mask
+	mKeyboard->handleTranslatedKeyDown(keycode, 0);
+}
+
+void LLWindowListener::mouseDown(LLSD const & evt)
+{
+	LLCoordGL pos(evt["x"].asInteger(), evt["y"].asInteger());
+
+	std::string const & button = evt["button"].asString();
+
+	if(button == "LEFT")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleMouseDown(NULL, pos, 0);
+	}
+	else if (button == "RIGHT")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleRightMouseDown(NULL, pos, 0);
+	}
+	else if (button == "MIDDLE")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleMiddleMouseDown(NULL, pos, 0);
+	}
+	else
+	{
+		llwarns << "ignoring unknown mous button \"" << button << '\"' << llendl;
+	}
+}
+
+void LLWindowListener::mouseUp(LLSD const & evt)
+{
+	LLCoordGL pos(evt["x"].asInteger(), evt["y"].asInteger());
+
+	std::string const & button = evt["button"].asString();
+
+	if(button == "LEFT")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleMouseUp(NULL, pos, 0);
+	}
+	else if (button == "RIGHT")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleRightMouseUp(NULL, pos, 0);
+	}
+	else if (button == "MIDDLE")
+	{
+		// *TODO - figure out how to handle the mask
+		mWindow->handleMiddleMouseUp(NULL, pos, 0);
+	}
+	else
+	{
+		llwarns << "ignoring unknown mous button \"" << button << '\"' << llendl;
+	}
+}
+
+void LLWindowListener::mouseMove(LLSD const & evt)
+{
+	LLCoordGL pos(evt["x"].asInteger(), evt["y"].asInteger());
+
+	// *TODO - figure out how to handle the mask
+	mWindow->handleMouseMove(NULL, pos, 0);
+}
+
+void LLWindowListener::mouseScroll(LLSD const & evt)
+{
+	S32 clicks = evt["clicks"].asInteger();
+
+	mWindow->handleScrollWheel(NULL, clicks);
+}
+
diff --git a/indra/llwindow/llwindowlistener.h b/indra/llwindow/llwindowlistener.h
new file mode 100644
index 0000000000000000000000000000000000000000..5b234c5d1dd19dfb16bf2d989d3cd5147d4d1e8b
--- /dev/null
+++ b/indra/llwindow/llwindowlistener.h
@@ -0,0 +1,53 @@
+/** 
+ * @file llwindowlistener.h
+ * @brief EventAPI interface for injecting input into LLWindow
+ *
+ * $LicenseInfo:firstyear=2001&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, Linden Research, Inc.
+ * 
+ * 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;
+ * version 2.1 of the License only.
+ * 
+ * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_LLWINDOWLISTENER_H
+#define LL_LLWINDOWLISTENER_H
+
+#include "lleventapi.h"
+
+class LLKeyboard;
+class LLWindowCallbacks;
+
+class LLWindowListener : public LLEventAPI
+{
+public:
+	LLWindowListener(LLWindowCallbacks * window, LLKeyboard * keyboard);
+
+	void keyDown(LLSD const & evt);
+	void keyUp(LLSD const & evt);
+	void mouseDown(LLSD const & evt);
+	void mouseUp(LLSD const & evt);
+	void mouseMove(LLSD const & evt);
+	void mouseScroll(LLSD const & evt);
+
+private:
+	LLWindowCallbacks * mWindow;
+	LLKeyboard * mKeyboard;
+};
+
+
+#endif // LL_LLWINDOWLISTENER_H
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index af6beacdfa9a69e6f2bc3858dde7a6ff7f8b9cee..11bebf04efbae3340ae2b89378cfcfb855790584 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -412,6 +412,7 @@ set(viewer_SOURCE_FILES
     llsidepaneliteminfo.cpp
     llsidepaneltaskinfo.cpp
     llsidetray.cpp
+    llsidetraylistener.cpp
     llsidetraypanelcontainer.cpp
     llsky.cpp
     llslurl.cpp
@@ -947,6 +948,7 @@ set(viewer_HEADER_FILES
     llsidepaneliteminfo.h
     llsidepaneltaskinfo.h
     llsidetray.h
+    llsidetraylistener.h
     llsidetraypanelcontainer.h
     llsky.h
     llslurl.h
diff --git a/indra/newview/llagentlistener.cpp b/indra/newview/llagentlistener.cpp
index d520debc31f9f37e70b53f552b815f2c75661b09..c453fe91f47a3a53f5e90482924bf78e65c67770 100644
--- a/indra/newview/llagentlistener.cpp
+++ b/indra/newview/llagentlistener.cpp
@@ -37,6 +37,8 @@
 #include "llviewerobject.h"
 #include "llviewerobjectlist.h"
 #include "llviewerregion.h"
+#include "llsdutil.h"
+#include "llsdutil_math.h"
 
 LLAgentListener::LLAgentListener(LLAgent &agent)
   : LLEventAPI("LLAgent",
@@ -53,6 +55,15 @@ LLAgentListener::LLAgentListener(LLAgent &agent)
 	add("requestStand",
         "Ask to stand up",
         &LLAgentListener::requestStand);
+    add("resetAxes",
+        "Set the agent to a fixed orientation (optionally specify [\"lookat\"] = array of [x, y, z])",
+        &LLAgentListener::resetAxes);
+    add("getAxes",
+        "Send information about the agent's orientation on [\"reply\"]:\n"
+        "[\"euler\"]: map of {roll, pitch, yaw}\n"
+        "[\"quat\"]:  array of [x, y, z, w] quaternion values",
+        &LLAgentListener::getAxes,
+        LLSDMap("reply", LLSD()));
 }
 
 void LLAgentListener::requestTeleport(LLSD const & event_data) const
@@ -104,3 +115,28 @@ void LLAgentListener::requestStand(LLSD const & event_data) const
 	mAgent.setControlFlags(AGENT_CONTROL_STAND_UP);
 }
 
+void LLAgentListener::resetAxes(const LLSD& event) const
+{
+    if (event.has("lookat"))
+    {
+        mAgent.resetAxes(ll_vector3_from_sd(event["lookat"]));
+    }
+    else
+    {
+        // no "lookat", default call
+        mAgent.resetAxes();
+    }
+}
+
+void LLAgentListener::getAxes(const LLSD& event) const
+{
+    LLQuaternion quat(mAgent.getQuat());
+    F32 roll, pitch, yaw;
+    quat.getEulerAngles(&roll, &pitch, &yaw);
+    // The official query API for LLQuaternion's [x, y, z, w] values is its
+    // public member mQ...
+    sendReply(LLSDMap
+              ("quat", llsd_copy_array(boost::begin(quat.mQ), boost::end(quat.mQ)))
+              ("euler", LLSDMap("roll", roll)("pitch", pitch)("yaw", yaw)),
+              event);
+}
diff --git a/indra/newview/llagentlistener.h b/indra/newview/llagentlistener.h
index 9b585152f4c3bebcaebcaffe4881f4e27122efc4..0aa58d0b16fdaf06796a4c0546b605f4caa7657f 100644
--- a/indra/newview/llagentlistener.h
+++ b/indra/newview/llagentlistener.h
@@ -44,6 +44,8 @@ class LLAgentListener : public LLEventAPI
 	void requestTeleport(LLSD const & event_data) const;
 	void requestSit(LLSD const & event_data) const;
 	void requestStand(LLSD const & event_data) const;
+	void resetAxes(const LLSD& event) const;
+	void getAxes(const LLSD& event) const;
 
 private:
 	LLAgent & mAgent;
diff --git a/indra/newview/lllogininstance.cpp b/indra/newview/lllogininstance.cpp
index 33e051bfab8e6286f73c64862a8fe6ee05019c54..abcd8588dc517a202dfba9d6d545b97297f7d5ff 100644
--- a/indra/newview/lllogininstance.cpp
+++ b/indra/newview/lllogininstance.cpp
@@ -480,10 +480,12 @@ LLLoginInstance::LLLoginInstance() :
 {
 	mLoginModule->getEventPump().listen("lllogininstance", 
 		boost::bind(&LLLoginInstance::handleLoginEvent, this, _1));
-	mDispatcher.add("fail.login", boost::bind(&LLLoginInstance::handleLoginFailure, this, _1));
-	mDispatcher.add("connect",    boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1));
-	mDispatcher.add("disconnect", boost::bind(&LLLoginInstance::handleDisconnect, this, _1));
-	mDispatcher.add("indeterminate", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1));
+	// This internal use of LLEventDispatcher doesn't really need
+	// per-function descriptions.
+	mDispatcher.add("fail.login", "", boost::bind(&LLLoginInstance::handleLoginFailure, this, _1));
+	mDispatcher.add("connect",    "", boost::bind(&LLLoginInstance::handleLoginSuccess, this, _1));
+	mDispatcher.add("disconnect", "", boost::bind(&LLLoginInstance::handleDisconnect, this, _1));
+	mDispatcher.add("indeterminate", "", boost::bind(&LLLoginInstance::handleIndeterminate, this, _1));
 }
 
 LLLoginInstance::~LLLoginInstance()
@@ -625,11 +627,7 @@ bool LLLoginInstance::handleLoginEvent(const LLSD& event)
 
 	// Call the method registered in constructor, if any, for more specific
 	// handling
-	LLEventDispatcher::Callable method(mDispatcher.get(event["change"]));
-	if (! method.empty())
-	{
-		method(event);
-	}
+	mDispatcher.try_call(event);
 	return false;
 }
 
diff --git a/indra/newview/llsidetray.cpp b/indra/newview/llsidetray.cpp
index eb537c7d7b987a8b6e5efa8ad9eab04adf2d0a0b..85efebabfe98867da752851bc1b8b661bd373d10 100644
--- a/indra/newview/llsidetray.cpp
+++ b/indra/newview/llsidetray.cpp
@@ -53,6 +53,8 @@
 
 #include "llsidepanelappearance.h"
 
+#include "llsidetraylistener.h"
+
 //#include "llscrollcontainer.h"
 
 using namespace std;
@@ -71,6 +73,8 @@ static const std::string TAB_PANEL_CAPTION_TITLE_BOX = "sidetray_tab_title";
 
 LLSideTray* LLSideTray::sInstance = 0;
 
+static LLSideTrayListener sSideTrayListener(LLSideTray::getInstance);
+
 // static
 LLSideTray* LLSideTray::getInstance()
 {
@@ -417,6 +421,11 @@ LLSideTrayTab*  LLSideTrayTab::createInstance	()
 	return tab;
 }
 
+// Now that we know the definition of LLSideTrayTab, we can implement
+// tab_cast.
+template <>
+LLPanel* tab_cast<LLPanel*>(LLSideTrayTab* tab) { return tab; }
+
 //////////////////////////////////////////////////////////////////////////////
 // LLSideTrayButton
 // Side Tray tab button with "tear off" handling.
@@ -530,6 +539,8 @@ LLSideTray::LLSideTray(const Params& params)
 	// register handler function to process data from the xml. 
 	// panel_name should be specified via "parameter" attribute.
 	commit.add("SideTray.ShowPanel", boost::bind(&LLSideTray::showPanel, this, _2, LLUUID::null));
+	commit.add("SideTray.Toggle", boost::bind(&LLSideTray::onToggleCollapse, this));
+	commit.add("SideTray.Collapse", boost::bind(&LLSideTray::collapseSideBar, this));
 	LLTransientFloaterMgr::getInstance()->addControlView(this);
 	LLView* side_bar_tabs  = gViewerWindow->getRootView()->getChildView("side_bar_tabs");
 	if (side_bar_tabs != NULL)
diff --git a/indra/newview/llsidetray.h b/indra/newview/llsidetray.h
index 184d78845f8a7fd7e88d26e00fe416394d87121f..c4d3c2626c992e13a72728b94f78631f13632053 100644
--- a/indra/newview/llsidetray.h
+++ b/indra/newview/llsidetray.h
@@ -33,6 +33,13 @@
 class LLAccordionCtrl;
 class LLSideTrayTab;
 
+// Deal with LLSideTrayTab being opaque. Generic do-nothing cast...
+template <class T>
+T tab_cast(LLSideTrayTab* tab) { return tab; }
+// specialized for implementation in presence of LLSideTrayTab definition
+template <>
+LLPanel* tab_cast<LLPanel*>(LLSideTrayTab* tab);
+
 // added inheritance from LLDestroyClass<LLSideTray> to enable Side Tray perform necessary actions 
 // while disconnecting viewer in LLAppViewer::disconnectViewer().
 // LLDestroyClassList::instance().fireCallbacks() calls destroyClass method. See EXT-245.
@@ -217,6 +224,9 @@ class LLSideTray : public LLPanel, private LLDestroyClass<LLSideTray>
 	}
 	
 private:
+	// Since we provide no public way to query mTabs and mDetachedTabs, give
+	// LLSideTrayListener friend access.
+	friend class LLSideTrayListener;
 	LLPanel*						mButtonsPanel;
 	typedef std::map<std::string,LLButton*> button_map_t;
 	button_map_t					mTabButtons;
diff --git a/indra/newview/llsidetraylistener.cpp b/indra/newview/llsidetraylistener.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6db13e517d9c933e321c3e341d60879c5e88936b
--- /dev/null
+++ b/indra/newview/llsidetraylistener.cpp
@@ -0,0 +1,162 @@
+/**
+ * @file   llsidetraylistener.cpp
+ * @author Nat Goodspeed
+ * @date   2011-02-15
+ * @brief  Implementation for llsidetraylistener.
+ * 
+ * $LicenseInfo:firstyear=2011&license=lgpl$
+ * Copyright (c) 2011, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "llviewerprecompiledheaders.h"
+// associated header
+#include "llsidetraylistener.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llsidetray.h"
+#include "llsdutil.h"
+
+LLSideTrayListener::LLSideTrayListener(const Getter& getter):
+    LLEventAPI("LLSideTray",
+               "Operations on side tray (e.g. query state, query tabs)"),
+    mGetter(getter)
+{
+    add("getCollapsed", "Send on [\"reply\"] an [\"open\"] Boolean",
+        &LLSideTrayListener::getCollapsed, LLSDMap("reply", LLSD()));
+    add("getTabs",
+        "Send on [\"reply\"] a map of tab names and info about them",
+        &LLSideTrayListener::getTabs, LLSDMap("reply", LLSD()));
+    add("getPanels",
+        "Send on [\"reply\"] data about panels available with SideTray.ShowPanel",
+        &LLSideTrayListener::getPanels, LLSDMap("reply", LLSD()));
+}
+
+void LLSideTrayListener::getCollapsed(const LLSD& event) const
+{
+    sendReply(LLSDMap("open", ! mGetter()->getCollapsed()), event);
+}
+
+void LLSideTrayListener::getTabs(const LLSD& event) const
+{
+    LLSD reply;
+
+    LLSideTray* tray = mGetter();
+    LLSD::Integer ord(0);
+    for (LLSideTray::child_list_const_iter_t chi(tray->beginChild()), chend(tray->endChild());
+         chi != chend; ++chi, ++ord)
+    {
+        LLView* child = *chi;
+        // How much info is important? Toss in as much as seems reasonable for
+        // each tab. But to me, at least for the moment, the most important
+        // item is the tab name.
+        LLSD info;
+        // I like the idea of returning a map keyed by tab name. But as
+        // compared to an array of maps, that loses sequence information.
+        // Address that by indicating the original order in each map entry.
+        info["ord"] = ord;
+        info["visible"] = bool(child->getVisible());
+        info["enabled"] = bool(child->getEnabled());
+        info["available"] = child->isAvailable();
+        reply[child->getName()] = info;
+    }
+
+    sendReply(reply, event);
+}
+
+static LLSD getTabInfo(LLPanel* tab)
+{
+    LLSD panels;
+    for (LLPanel::tree_iterator_t ti(tab->beginTreeDFS()), tend(tab->endTreeDFS());
+         ti != tend; ++ti)
+    {
+        // *ti is actually an LLView*, which had better not be NULL
+        LLView* view(*ti);
+        if (! view)
+        {
+            LL_ERRS("LLSideTrayListener") << "LLSideTrayTab '" << tab->getName()
+                                          << "' has a NULL child LLView*" << LL_ENDL;
+        }
+
+        // The logic we use to decide what "panel" names to return is heavily
+        // based on LLSideTray::showPanel(): the function that actually
+        // implements the "SideTray.ShowPanel" operation. showPanel(), in
+        // turn, depends on LLSideTray::openChildPanel(): when
+        // openChildPanel() returns non-NULL, showPanel() stops searching
+        // attached and detached LLSideTrayTab tabs.
+
+        // For each LLSideTrayTab, openChildPanel() first calls
+        // findChildView(panel_name, true). In other words, panel_name need
+        // not be a direct LLSideTrayTab child, it's sought recursively.
+        // That's why we use (begin|end)TreeDFS() in this loop.
+
+        // But this tree_iterator_t loop will actually traverse every widget
+        // in every panel. Returning all those names will not help our caller:
+        // passing most such names to openChildPanel() would not do what we
+        // want. Even though the code suggests that passing ANY valid
+        // side-panel widget name to openChildPanel() will open the tab
+        // containing that widget, results could get confusing since followup
+        // (onOpen()) logic wouldn't be invoked, and showPanel() wouldn't stop
+        // searching because openChildPanel() would return NULL.
+
+        // We must filter these LLView items, using logic that (sigh!) mirrors
+        // openChildPanel()'s own.
+
+        // openChildPanel() returns a non-NULL LLPanel* when either:
+        // - the LLView is a direct child of an LLSideTrayPanelContainer
+        // - the LLView is itself an LLPanel.
+        // But as LLSideTrayPanelContainer can directly contain LLView items
+        // that are NOT themselves LLPanels (e.g. "sidebar_me" contains an
+        // LLButton called "Jump Right Arrow"), we'd better focus only on
+        // LLSideTrayPanelContainer children that are themselves LLPanel
+        // items. Which means that the second test completely subsumes the
+        // first.
+        LLPanel* panel(dynamic_cast<LLPanel*>(view));
+        if (panel)
+        {
+            // Maybe it's overkill to construct an LLSD::Map for each panel, but
+            // the possibility remains that we might want to deliver more info
+            // about each panel than just its name.
+            panels.append(LLSDMap("name", panel->getName()));
+        }
+    }
+
+    return LLSDMap("panels", panels);
+}
+
+void LLSideTrayListener::getPanels(const LLSD& event) const
+{
+    LLSD reply;
+
+    LLSideTray* tray = mGetter();
+    // Iterate through the attached tabs.
+    LLSD::Integer ord(0);
+    for (LLSideTray::child_vector_t::const_iterator
+             ati(tray->mTabs.begin()), atend(tray->mTabs.end());
+         ati != atend; ++ati)
+    {
+        // We don't have access to LLSideTrayTab: the class definition is
+        // hidden in llsidetray.cpp. But as LLSideTrayTab isa LLPanel, use the
+        // LLPanel API. Unfortunately, without the LLSideTrayTab definition,
+        // the compiler doesn't even know this LLSideTrayTab* is an LLPanel*.
+        // Persuade it.
+        LLPanel* tab(tab_cast<LLPanel*>(*ati));
+        reply[tab->getName()] = getTabInfo(tab).with("attached", true).with("ord", ord);
+    }
+
+    // Now iterate over the detached tabs. These can also be opened via
+    // SideTray.ShowPanel.
+    ord = 0;
+    for (LLSideTray::child_vector_t::const_iterator
+             dti(tray->mDetachedTabs.begin()), dtend(tray->mDetachedTabs.end());
+         dti != dtend; ++dti)
+    {
+        LLPanel* tab(tab_cast<LLPanel*>(*dti));
+        reply[tab->getName()] = getTabInfo(tab).with("attached", false).with("ord", ord);
+    }
+
+    sendReply(reply, event);
+}
diff --git a/indra/newview/llsidetraylistener.h b/indra/newview/llsidetraylistener.h
new file mode 100644
index 0000000000000000000000000000000000000000..0dd2067433b8ebb1b45f081be0624c2ac304184e
--- /dev/null
+++ b/indra/newview/llsidetraylistener.h
@@ -0,0 +1,36 @@
+/**
+ * @file   llsidetraylistener.h
+ * @author Nat Goodspeed
+ * @date   2011-02-15
+ * @brief  
+ * 
+ * $LicenseInfo:firstyear=2011&license=lgpl$
+ * Copyright (c) 2011, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LLSIDETRAYLISTENER_H)
+#define LL_LLSIDETRAYLISTENER_H
+
+#include "lleventapi.h"
+#include <boost/function.hpp>
+
+class LLSideTray;
+class LLSD;
+
+class LLSideTrayListener: public LLEventAPI
+{
+    typedef boost::function<LLSideTray*()> Getter;
+
+public:
+    LLSideTrayListener(const Getter& getter);
+
+private:
+    void getCollapsed(const LLSD& event) const;
+    void getTabs(const LLSD& event) const;
+    void getPanels(const LLSD& event) const;
+
+    Getter mGetter;
+};
+
+#endif /* ! defined(LL_LLSIDETRAYLISTENER_H) */
diff --git a/indra/newview/lluilistener.cpp b/indra/newview/lluilistener.cpp
index 4d6eac4958e25c95a46f6129a416fb859d0d6acf..d02d126cb5493bfdd906f2c2c5121cb16daacacc 100644
--- a/indra/newview/lluilistener.cpp
+++ b/indra/newview/lluilistener.cpp
@@ -32,11 +32,14 @@
 #include "lluilistener.h"
 // STL headers
 // std headers
+#include <sstream>
 // external library headers
 // other Linden headers
+#include "llviewerwindow.h" // to get root view
 #include "lluictrl.h"
 #include "llerror.h"
 
+
 LLUIListener::LLUIListener():
     LLEventAPI("UI",
                "LLUICtrl::CommitCallbackRegistry listener.\n"
@@ -47,6 +50,12 @@ LLUIListener::LLUIListener():
         "as if from a user gesture on a menu -- or a button click.",
         &LLUIListener::call,
         LLSD().with("function", LLSD()));
+
+    add("getValue",
+        "For the UI control identified by the path in [\"path\"], return the control's\n"
+        "current value as [\"value\"] reply.",
+        &LLUIListener::getValue,
+        LLSD().with("path", LLSD()));
 }
 
 void LLUIListener::call(const LLSD& event) const
@@ -71,3 +80,103 @@ void LLUIListener::call(const LLSD& event) const
         (*func)(NULL, event["parameter"]);
     }
 }
+
+// Split string given a single character delimiter.
+// Note that this returns empty strings for leading, trailing, and adjacent
+// delimiters, such as "/foo/bar//baz/" -> ["", "foo", "bar", "", "baz", "" ]
+std::vector<std::string> split(const std::string& s, char delim)
+{
+	std::stringstream ss(s);
+	std::string item;
+	std::vector<std::string> items;
+	while (std::getline(ss, item, delim))
+	{
+		items.push_back(item);
+	}
+	return items;
+}
+
+// Walk the LLView tree to resolve a path
+// Paths can be discovered using Develop > XUI > Show XUI Paths
+//
+// A leading "/" indicates the root of the tree is the starting
+// position of the search, (otherwise the context node is used)
+//
+// Adjacent "//" mean that the next level of the search is done
+// recursively ("descendant" rather than "child").
+//
+// Return values: If no match is found, NULL is returned,
+// otherwise the matching LLView* is returned.
+//
+// Examples:
+//
+// "/" -> return the root view
+// "/foo" -> find "foo" as a direct child of the root
+// "foo" -> find "foo" as a direct child of the context node
+// "//foo" -> find the first "foo" child anywhere in the tree
+// "/foo/bar" -> find "foo" as direct child of the root, and
+//      "bar" as a direct child of "foo"
+// "//foo//bar/baz" -> find the first "foo" anywhere in the
+//      tree, the first "bar" anywhere under it, and "baz"
+//      as a direct child of that
+//
+const LLView* resolve_path(const LLView* context, const std::string path) 
+{
+	std::vector<std::string> parts = split(path, '/');
+	
+	if (parts.size() == 0)
+	{
+		return context;
+	}
+
+	std::vector<std::string>::iterator it = parts.begin();
+	
+	// leading / means "start at root"
+	if ((*it).length() == 0)
+	{
+		context = (LLView*)(gViewerWindow->getRootView());
+		it++;
+	}
+	
+	bool recurse = false;
+	for (; it != parts.end() && context; it++) 
+    {
+		std::string part = *it;
+
+		if (part.length() == 0) 
+        {
+			recurse = true;
+        }
+		else
+        {
+			const LLView* found = context->findChildView(part, recurse);
+			if (!found) 
+				return NULL;
+
+			context = found;
+			recurse = false;
+        }
+    }
+	
+    return context;
+}
+
+void LLUIListener::getValue(const LLSD&event) const
+{
+    LLSD reply = LLSD::emptyMap();
+
+    const LLView* root = (LLView*)(gViewerWindow->getRootView());
+    const LLView* view = resolve_path(root, event["path"].asString());
+    const LLUICtrl* ctrl(dynamic_cast<const LLUICtrl*>(view));
+
+    if (ctrl) 
+    {
+        reply["value"] = ctrl->getValue();
+    }
+    else
+    {
+        // *TODO: ??? return something indicating failure to resolve
+    }
+    
+    sendReply(reply, event);
+}
diff --git a/indra/newview/lluilistener.h b/indra/newview/lluilistener.h
index e7847f01e8a3a4b6345364c63adf9db2c873a0e6..08724024dc84401b6add6c79d2f26bdf6d108ac0 100644
--- a/indra/newview/lluilistener.h
+++ b/indra/newview/lluilistener.h
@@ -41,6 +41,7 @@ class LLUIListener: public LLEventAPI
 
 private:
     void call(const LLSD& event) const;
+    void getValue(const LLSD&event) const;
 };
 
 #endif /* ! defined(LL_LLUILISTENER_H) */
diff --git a/indra/newview/llviewerwindowlistener.cpp b/indra/newview/llviewerwindowlistener.cpp
index 0b52948680ecab659bccfe972d3b7a57950f204f..1fe5fc9800c0591c0e2d072f2182c68904b20493 100644
--- a/indra/newview/llviewerwindowlistener.cpp
+++ b/indra/newview/llviewerwindowlistener.cpp
@@ -65,7 +65,6 @@ LLViewerWindowListener::LLViewerWindowListener(LLViewerWindow* llviewerwindow):
 
 void LLViewerWindowListener::saveSnapshot(const LLSD& event) const
 {
-    LLReqID reqid(event);
     typedef std::map<LLSD::String, LLViewerWindow::ESnapshotType> TypeMap;
     TypeMap types;
 #define tp(name) types[#name] = LLViewerWindow::SNAPSHOT_TYPE_##name
@@ -98,9 +97,7 @@ void LLViewerWindowListener::saveSnapshot(const LLSD& event) const
         type = found->second;
     }
     bool ok = mViewerWindow->saveSnapshot(event["filename"], width, height, showui, rebuild, type);
-    LLSD response(reqid.makeResponse());
-    response["ok"] = ok;
-    LLEventPumps::instance().obtain(event["reply"]).post(response);
+    sendReply(LLSDMap("ok", ok), event);
 }
 
 void LLViewerWindowListener::requestReshape(LLSD const & event_data) const